Skip to content

Instantly share code, notes, and snippets.

@vurtun
Last active October 4, 2023 15:44
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vurtun/0bf28ba10d5cbf69c11f5ef638e3ca45 to your computer and use it in GitHub Desktop.
Save vurtun/0bf28ba10d5cbf69c11f5ef638e3ca45 to your computer and use it in GitHub Desktop.

Graphical User Interfaces

Last time I wrote a little bit about my current GUI research progress. There are some things that were misunderstood so I want to clarify some fundamental design decisions and finally write up some current problems and their solutions.

First up I want to clarify a component is not another term for what is usually refered to as widget. Instead wigets in this implementation are made out of n >= 1 components. Components themself are just rectangles with attributes left, right, top, bottom, center_x, center_y, width and height some behavior flags and an optional surface to draw into. For example a scroll regions is made up out of at least three components: Content, Scrollbar and Cursor (optional five or more components with button up and down). However components are not only part of a widget they are also what is refered to containers in other retain mode GUI frameworks or other rectangle instances.

Definitions are compile time data counterparts to components and just define initial flags, size and callbacks for drawing, layouting and input handling for their counterpart. The mapping between component and definition are one to one just like with nodes. Nodes are tree nodes making up a component hierarchy (notice component NOT widget hierarchy). Each node has a parent node except the root node which has itself as parent and k >= 0 child nodes. So nodes and to an extend components can be both widget as well as component container as well as widget container.

Components are placed on screen by constraints. Each constraint is a combinations of a simple condition to check if it even needs to be executed and a simple linear functions for changing one attribute of a component. This particular implementation has three different linear functions. The default constraint is:

 destination.attribute = multiplier * source.attribute + offset

With both destination and source being components while attribute is one of each components attributes (left,right,top,bottom,center_x,center_y,width and height). The other two variables, multiplier and offset, are both constants values.

The default constraint can be extended. For example the = could be changed to support some additional assignment operators like += or -= or like the two other constraints in this implementation with min and max.

Important is as soon as a constraint was executed the rectangle in the destination component is unbalanced and needs to be put back into correct state since we have both relative as well as absolute attributes. A single axis looks like:

 min      center          max
 |============|==============|
 <----------- len ----------->

If one of these attributes changes then all the other attributes in that axis need to be recalculated. However each attributes can be calculated in multiple ways. So each recalculation requires an anchor and the attribute that changed to rebalance (solve in my implementation).

Finally to address a special component flag COMPONENT_LAYER. Layers are special components used with scroll components. All components are directly blitted to screen under normal circumstances. However the only exceptions are components with flag COMPONENT_LAYER. The child hierarchy of these components are directly drawn into these specific components surface/canvas and then finally blitted to screen. This allows blitting without clipping rectangles and even more interesting it is possible to have immediate mode UI inside of COMPONENT_LAYER or in other words scrollable components.

This is basically already everything needed to understand the core library. The rest are functions to call callbacks, handle interactions, hash table insert/lookup and fold + commit which I already described in the last post. So inherently there is not much to it but this design does quite a lot with very little.

Popups

One example of the power of this design are popups. Popups can be implemented without additional code inside the core library. Instead you just have to introduce some additional components/nodes. Currently there is only one fixed component which is the root component at the top of the component hierarchy. However you could add two additional high level components spanning the whole screen for popup control.

The main high-level popup component contains all popup component hierarchies which are all inactive by default and therefore are hidden and not interactive. The second high-level popup component is always in the background of the first high-level popup component and is used for blocking and non-blocking popup behavior. By default this component is interactive and allows underlying actual UI components to get input. However if a blocking popup was activated it will be set to non-interactive and blocks input from there on. For non-blocking components it will check if it was clicked and if so will close all non-blocking components in its parent component. Finally the underlying actual UI is a child component of the second high-level popup component.

Layers:
        ______________
        |Root(overlay)|
        --------------
              |
         _____v______
        |   Popup   |
        ------------  
             |
         ____v_______
        |   Block   |
        ------------  
             |
         ____v_______
        |    UI     |
        ------------  
Imgui

I talked a little bit about imgui last time but not very much. The reason so far was that I was not happy with just saying "add pure immediate mode library here". It requires both quite a lot of work and code to write these pure imgui versions doing something that should already be possible with already existing code in this implementation.

The reason I did not talk about it before in more depth were resource concerns. The fold model I proposed last time can already be abused as an immediate mode UI by recommiting each frame and keeping and working on the last and current layout versions (you could of course even work with more layouts but a double buffer is required at minimum). While this could make sense for very graphical heavy applications that only use this implementation for debugging purposes on machines with good hardware, it is way to resource consuming under normal circumstances. As a small rough estimate 10,000 components (paint.net has around 500) would cost about 5-10 MB of memory that would be needed to be copied and filled each frame (you could parallelize some operations like copy to achieve better speed). Granted many GUIs probably won't have these high number of components but we are talking about the general case. So going full non-pure immediate mode is possible but cannot be used as an easy answer for everything.

However I thought about it and noticed that it is possible to nest layouts. So you could create multiple layouts with each being either non-changing/declarative UI or full on immediate mode UI without having to write a pure imgui library ontop of this library. So you would have one code base for editor generated compile time tables, declarative UI and immediate mode UI. All this can be done with same core library. Downside however is that widgets themself become more complex since they need to be able to handle all these different use cases.

Sidenote: A layout is an instance of a tree with components, defintions and nodes. Of course if I describe it this way it seems to be obvious that you can outsource parts of the tree as a sub-tree but there were some uses cases like popups which I was not sure would work out.

Furthmore I just want to make clear there are differences between "pure" imgui libraries like nuklear and dear imgui and "normal" imgui libraries like this implementation. While pure imgui UI libraries pretend to be stateless on the API side they still keep track of some UI state internally but only selectively. On the other side this implementation keeps track of all UI state over time (theoretically indefinitely) and can go back to and lookup previous state. A concept which simplifies and solves a lot of nasty problems I encountered in nuklear.

However reusing layouts for immediate mode requires some small changes. The main change is that there is no free hash table (since using an index as key only works for static layouts) anymore. However since we still know the final number of components accumulate by the reducer we can just allocate a big int array to map between ids and indicies. Interestingly on the API side there are no differences between immediate mode and retain mode. Static UI can use a growing ID for identifications (generated by enum for example). Immediate mode however needs more extensive identification since widgets need to be identifiable between changes. Still the passed ID is a unique int identifier on the API side of things just like in retain mode but it is easier for the library user to generate a hash value from string. So in this implementation I added a compile time string hash macro. Once again the biggest downside is that each widget has to support all possible ways of using it. For immediate mode for example it has to lookup its past state or for declarations it has to construct itself initially from components.

Still Interestingly these core changes resulted in less/same code size since I could remove some parts meant for pure immediate mode. So the core implementation is still ~800 LOC composed out of ~280 lines of header and ~500 LOC implementation. So at this point the UI core already supports UI creation by:

  1. compile time tables generated by external editor or directly through code
  2. declarations run at initialization
  3. Immediate mode UI declared and run each frame or on each update
  4. Pure immediate mode UI after processing in the event handling stage (see last post)

Of course you could also combine these and use each depending on your use cases. In general the APi still look like previously stated:

QK_DECLARATION_BEGIN(app)
    QK_PANEL(APP_PANEL),
    QK_SCROLL_REGION(APP_PANEL_SCROLL),
    QK_BUTTON(APP_OK)
    QK_BUTTON(APP_CANCEL)
QK_DECLARATION_END

int main (...)
{
    while (1) {
        /* I.) Declarations stage */
        /* retain mode */
        qk_panel_begin(APP_PANEL,...);
        {
            qk_scroll_region_begin(APP_PANEL_SCROLL, ...);
            qk_button(APP_OK...);
            qk_button(APP_CANCEL...);
            qk_scroll_region_end(...);
        }
        qk_panel_end(...);

        /* immediate mode */
        qk_panel_begin(qk_id("Demo"),...);
        {
            qk_scroll_region_begin(qk_id("Demo Scroll"), ...);
            qk_button(qk_id("BUTTON_OK"),...);
            qk_button(qk_id("BUTTON_CANCLE"),...);
            qk_scroll_region_end(...);
        }
        qk_panel_end(...);

        /* II.) Processing stage */
        qk_blueprint(...);
        qk_layouting(...);
        update(...); /* update ui by input events */
        qk_layouting(...);
        qk_paint(...);

        /* III.) Handling + pure imgui */
        /* handle click of previously declared buttons */
        if (button(APP_OK,...)) {
            /* event handling */
        }
        if (button(qk_id("BUTTON_OK"),...)) {
            /* event handling */
        }  

        /* pure immediate mode list */
        struct list_view view;
        if (list_begin(LIST_ID, &view, ...)) {
            for (int i = view.from, i < view.to; ++i)
                list_push(...);
            list_end(&view, ...)
        }
    }
}
Construction

Finally I want to quickly show how widgets are constructed. All widgets are composed out of modules which each are themself made out of component, definition, node and constraints. So widgets are not constructed by code but instead through data tables that will be pushed into a qk_reducer buffer. Furthermore it is also possible to further push optional data as extensions.

qk_intern void
qk_scroll_region_construct(struct qk_reducer *buf, unsigned parent, unsigned id)
{
    qk_storage const int sz = 8;
    qk_storage const int pad = 5;
    const struct qk_module template[] = {
        /* vertical scrollbar */
        {.def = {QK_TBL(0, scroll_paint, 0), .flags = QK_INTERACTIVE|QK_PAINTABLE},
        .id = id + 1, .parent = parent, .cnt = 4, .con = {
            {{.eq = QK_CONSTRAINT_SET, .dst = {id+1,T}, .src = {parent,T}, .cons = {1.00f, +pad}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id+1,B}, .src = {parent,B}, .cons = {1.00f, -pad}}, .anch = T},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id+1,W}, .src = {parent,W}, .cons = {0.00f, +sz}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id+1,R}, .src = {parent,R}, .cons = {1.00f, -pad}}, .anch = W},
        }}, /* vertical scrollbar cursor */
        {.def = {QK_TBL(0, scroll_cursor_paint, 0),
        .flags = QK_INTERACTIVE|QK_PAINTABLE|QK_MOVABLE_Y},
        .id = id + 2, .parent = id + 1, .cnt = 4, .con = {
            {{.eq = QK_CONSTRAINT_CPY, .dst = {id+2,L}, .src = {id+1,L}, .cons = {1.00f, 0}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_CPY, .dst = {id+2,R}, .src = {id+1,R}, .cons = {1.00f, 0}}, .anch = L},
            {{.eq = QK_CONSTRAINT_MAX, .dst = {id+2,T}, .src = {id+1,T}, .cons = {1.00f, 0}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_MIN, .dst = {id+2,B}, .src = {id+1,B}, .cons = {1.00f, 0}}, .anch = T},
        }}, /* horizontal scrollbar */
        {.def = {QK_TBL(0, scroll_paint, 0), .flags = QK_INTERACTIVE|QK_PAINTABLE},
        .id = id + 3, .parent = parent, .cnt = 4, .con = {
            {{.eq = QK_CONSTRAINT_SET, .dst = {id+3,H}, .src = {parent,H}, .cons = {0.00f, +sz}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id+3,B}, .src = {parent,B}, .cons = {1.00f, +pad}}, .anch = H},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id+3,L}, .src = {parent,L}, .cons = {1.00f, +pad}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id+3,R}, .src = {parent,R}, .cons = {1.00f, -pad}}, .anch = L},
        }}, /* horizontal scrollbar cursor */
        {.def = {QK_TBL(0,scroll_cursor_paint,0),
        .flags = QK_INTERACTIVE|QK_PAINTABLE|QK_MOVABLE_X},
        .id = id + 4, .parent = id + 3, .cnt = 4, .con = {
            {{.eq = QK_CONSTRAINT_CPY, .dst = {id+4,T}, .src = {id+3,L}, .cons = {1.00f, 0}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_CPY, .dst = {id+4,B}, .src = {id+3,R}, .cons = {1.00f, 0}}, .anch = T},
            {{.eq = QK_CONSTRAINT_MAX, .dst = {id+4,L}, .src = {id+3,T}, .cons = {1.00f, 0}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_MIN, .dst = {id+4,R}, .src = {id+3,B}, .cons = {1.00f, 0}}, .anch = L},
        }}, /* content region */
        {.def = {QK_TBL(qk_scroll_region_dispatch, qk_scroll_panel_paint, 0),
        .flags = QK_INTERACTIVE|QK_LAYER|QK_PAINTABLE},
        .id = id, .parent = parent, .cnt = 6, .con = {
            {{.eq = QK_CONSTRAINT_CPY, .dst = {id,T}, .src = {id+1,T}, .cons = {1.00f, 0}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_CPY, .dst = {id,L}, .src = {id+3,L}, .cons = {1.00f, 0}}, .anch = 0},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id,B}, .src = {id+3,T}, .cons = {1.00f, -pad}}, .anch = T,
                .cond = {.eq = QK_CONDITION_NEQ, .dst = {id+3,ACT}, .cons = {0,0}}},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id,B}, .src = {parent,B}, .cons = {1.00f, -pad}}, .anch = T,
                .cond = {.eq = QK_CONDITION_EQ, .dst = {id+3,ACT}, .cons = {0,0}}},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id,R}, .src = {id+1,L}, .cons = {1.00f, -pad}}, .anch = L,
                .cond = {.eq = QK_CONDITION_NEQ, .dst = {id+1,ACT}, .cons = {0,0}}},
            {{.eq = QK_CONSTRAINT_SET, .dst = {id,R}, .src = {parent,R}, .cons = {1.00f, -pad}}, .anch = L,
                .cond = {.eq = QK_CONDITION_EQ, .dst = {id+1,ACT}, .cons = {0,0}}},
        }}
    }; /* custom scroll region extension */
    qk_storage const int rsz = qk_szof(struct qk_scroll_region);
    qk_storage const int ralign = qk_alignof(struct qk_scroll_region);
    const struct qk_scroll_region custom = {
        .comp = id, .cnt = 2, .flags = QK_SCROLL_XY,
        .s = {{id+1, id+2, QK_SCROLL_Y}, {id+3, id+4, QK_SCROLL_X}}};

    qk_reducer_push(buf, template, qk_cntof(template));
    qk_reducer_add(buf, QK_SCROLL_IDENTIFIER, &custom, rsz, ralign);
}
#ifndef UI_INCLUDED_
#define UI_INCLUDED_
#include <stdio.h> /* fputs, fprintf */
#include <string.h> /* memcpy, memset */
#define out
#define in const
#define api extern
#define hook static
#define intern static
#define global static
#define storage static
enum {false, true};
struct v2 {int x,y;};
struct rect {int x,y,w,h;};
#define v2(x,y) (struct v2){x,y}
#define rect(x,y,w,h) (struct rect){x,y,w,h}
#define cast(t,p) ((t)(p))
#define szof(a) ((int)sizeof(a))
#define zero(d,sz) memset(d,0,(size_t)(sz))
#define unused(...) unused_impl(0,__VA_ARGS__)
#define cntof(a) ((int)(sizeof(a)/sizeof((a)[0])))
#define fourcc(a,b,c,d) ((unsigned long)(((d)<<24)|((c)<<16)|((b)<<8)|(a)))
#define copy(d,s,sz) ((int)(((char*)d+sz)-((char*)memcpy(d,s,(size_t)(sz)))))
#define alignof(t) ((int)((char*)(&((struct {char c; t _h;}*)0)->_h) - (char*)0))
#define align(x, mask) ((void*)(((long long)((const char*)(x)+(mask-1))&~(mask-1))))
#define alignb(x, mask) ((void*)((long long)(x) & ~(mask-1)))
#define mod(x,N) ((uint)(((unsigned long long)(x)*(unsigned long long)(N))>>32))
static inline void unused_impl(int dummy,...){(void)dummy;}
#define flag(n) (1<<(n))
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
#define clamp(a,v,b) (max(min(b,v),a))
#define between(x,a,b) ((a) <= (x) && (x) <= (b))
#define inbox(px,py,x,y,w,h) (between(px,x,x+w) && between(py,y,y+h))
#define intersect(x0,y0,w0,h0,x1,y1,w1,h1) (!(((x1>(x0+w0))||((x1+w1)<x0)||(y1>(y0+h0))||(y1+h1)<y0)))
#define off(id) (((id)>>24u)&255u)
#define key(id) ((id)&(~(255u<<24u)))
#define tid(id,off) (((id)&(~(255u<<24u)))|((off&255u)<<24u))
#define len(s) (cntof(s)-1)
#define h1(s,i,x) (x*65599u+(unsigned char)s[(i)<len(s)?len(s)-1-(i):len(s)])
#define h4(s,i,x) h1(s,i,h1(s,i+1,h1(s,i+2,h1(s,i+3,x))))
#define h16(s,i,x) h4(s,i,h4(s,i+4,h4(s,i+8,h4(s,i+12,x))))
#define h64(s,i,x) h16(s,i,h16(s,i+16,h16(s,i+32,h16(s,i+48,x))))
#define idx(s,i) tid(((uint)(h64(s,0,i)^(h64(s,0,i)>>16))),0)
#define id(s) idx(s,1021)
struct layout;
struct context;
struct component;
typedef unsigned long uint;
typedef unsigned long long addr;
typedef void(*paint_f)(struct context*,struct layout*,struct component*);
typedef int(*handle_f)(struct context*,struct layout*,struct component*, XEvent*);
typedef void(*signal_f)(struct context*,struct layout*,struct component*, XEvent*,void *usr);
typedef void(*blueprint_f)(struct context*,struct layout*,struct component*);
typedef void(*link_f)(struct layout*, void*);
typedef void(*commit_f)(FILE*, void*);
/* interaction */
enum {Enter = LASTEvent, Leave, Drag};
enum interaction_type {
INTERACTION_NOP,
INTERACTION_SET,
INTERACTION_SIGNAL,
INTERACTION_ENABLE,
INTERACTION_DISABLE,
INTERACTION_DRAG_X,
INTERACTION_DRAG_Y,
INTERACTION_CNT,
};
struct interaction {
enum interaction_type type;
union {int i; uint f; signal_f s;} src;
void *dst;
};
struct interaction_slots {
struct interaction left_pressed;
struct interaction left_released;
struct interaction right_pressed;
struct interaction right_released;
struct interaction dragged;
struct interaction enter;
struct interaction leave;
};
/* component */
#define tbl(hn,pn,bp) .handle=hn,.paint=pn,.blueprint = bp,.i=#hn, .p=#pn, .b=#bp
enum attributes {ANIM, ACT, L, T, R, B, W, H, CX, CY, ATTR_CNT};
enum component_flags {
DEFAULT,
HIDDEN = flag(0),
INTERACTIVE = flag(1),
PAINTABLE = flag(2),
STATIC = flag(3),
SELETABLE = flag(4),
BACKGROUND = flag(5),
IS_LAYER = flag(6),
LAYER = IS_LAYER|PAINTABLE,
IS_MOVABLE_X = flag(7),
IS_MOVABLE_Y = flag(8),
IS_MOVABLE = IS_MOVABLE_X|IS_MOVABLE_Y,
MOVABLE_X = IS_MOVABLE_X|INTERACTIVE,
MOVABLE_Y = IS_MOVABLE_Y|INTERACTIVE,
MOVABLE = IS_MOVABLE|INTERACTIVE,
};
struct node {
uint id, parent;
int sub, cnt;
};
struct definition {
handle_f handle;
paint_f paint;
blueprint_f blueprint;
struct rect size;
unsigned flags;
int zorder;
const char *i, *p, *b;
};
struct component {
uint id;
int attr[ATTR_CNT];
unsigned flags;
int zorder;
unsigned pressed:1;
unsigned released:1;
unsigned entered:1;
unsigned hovered:1;
unsigned left:1;
unsigned dragged:1;
struct v2 off, total;
struct surface *surf;
struct interaction_slots slot;
addr usr;
};
/* constraint */
enum condition_eq {
COND_TRUE, COND_FALSE,
COND_EQ, COND_NEQ,
COND_GR, COND_LS,
COND_GRE, COND_LSE
};
enum constraint_eq {CON_CPY, CON_SET, CON_MIN, CON_MAX};
struct cons {float mul; int off;};
struct var {uint comp; int attr;};
struct function {
int eq;
struct var dst;
struct var src;
struct cons cons;
};
struct constraint {
struct function self;
struct function cond;
int anch;
};
/* reducer */
enum reducible_type {RED_COMP, RED_CON, RED_LNK, RED_EXT};
union reducible {
struct com {int type, next;} com;
struct comp {struct com _; uint tid; struct definition d; addr usr;} comp;
struct con {struct com _; struct constraint con;} con;
struct lnk {struct com _; uint child, parent;} lnk;
struct ext {struct com _; uint id; int align, size, off;} ext;
};
struct module {
struct definition def;
uint tid, parent;
#define MODULE_MAX_CON 16
struct constraint con[MODULE_MAX_CON];
int cnt;
};
struct mem {struct reducer *red; int cap;};
struct reducer {
int mode, payload;
int max_com, max_con;
int max_ext, max_nodes;
char *buf; int sz, cap;
};
/* extension */
struct extinfo {
const char *type;
uint id;
int size, align;
};
struct extdef {
struct extinfo info;
commit_f commit;
link_f link;
};
struct extension {
struct extinfo info;
addr addr;
};
/* commit */
struct commit {
const char *comp, *nodes;
const char *con, *def;
const char *tree, *ext;
const char *buffer, *seq;
const char *layout, *tab;
const struct extlnk {
const char *name;
const struct extdef *def;
} *ext_list;
int cnt;
};
/* layout */
enum layout_type {RETAINED,IMMEDIATE};
struct layout {
int init;
uint active;
uint origin;
uint hot;
const uint *nodes;
const struct node *tree;
struct component *comp;
const struct definition *def;
const struct constraint *con;
const struct extension *ext;
uint *buffer, *tbl, *seq;
int comp_cnt, con_cnt;
int ext_cnt, tbl_cnt;
int node_cnt;
int requested;
int payload;
int size;
};
/* context */
#define ROOT 0
struct context {
struct reducer red;
struct canvas *canvas;
struct layout *root;
struct layout *layout;
#define MAX_COMPONENT_DEPTH 32
uint cur, stk[MAX_COMPONENT_DEPTH];
};
/* context */
api void update(struct context*, struct layout*, XEvent*);
api void blueprint(struct context*,struct layout*);
api void paint(struct context*,struct layout*);
api void draw(struct context*, struct layout*, uint root);
/* declaration */
api void Begin(struct context*, struct layout*, enum layout_type, uint root, void *mem, int size, struct surface*);
#define Bind(ctx, id) (ctx)->stk[(ctx)->cur = 0] = id
#define Push(ctx,id) (ctx)->stk[++(ctx)->cur] = (id)
#define Peek(ctx) (ctx)->stk[(ctx)->cur]
#define Pop(ctx) (ctx)->cur = max((ctx)->cur-1,0);
api void End(struct context*);
/* layout */
api void clear(struct layout*);
api void layouting(struct layout*);
api void reposit(FILE*, in struct layout*, in struct commit*);
api inline uint find(const struct layout*, uint id);
api void fold(out struct layout**, in struct reducer*, in struct layout*, void*mem, int sz, in struct extdef**, int cnt);
#define map(ui,t,e) for((e)=(ui)->ext;(e)<(ui)->ext+(ui)->ext_cnt;++(e)) if((e)->info.id==(t))
api void reorder(struct layout*, struct component*);
/* reducer */
api void reducer_init(struct reducer*, enum layout_type, void *mem, int sz);
api void reducer_push(struct reducer*, union reducible*, in void *data, int sz, int ualign);
#define reducer_comp(r,ID,def,user) reducer_push(r,&(union reducible){.comp={.tid=ID,._={.type=RED_COMP},.d=def,.usr=user}},0,0,0)
#define reducer_con(r,c) reducer_push((r),&(union reducible){.con={._={.type=RED_CON},.con=(c)}},0,0,0)
#define reducer_lnk(r,c,p) reducer_push(r,&(union reducible){.lnk={._={.type=RED_LNK},.child=c,.parent=p}},0,0,0)
#define reducer_ext(r,t,p,s,a) reducer_push(r,&(union reducible){.ext={._={.type=RED_EXT},.id=t}},p,s,a)
api void reducer_add(struct reducer*, in struct module*, int cnt);
api void reducer_calc(struct reducer*, in struct layout*, out int *mem);
#define reducer_begin(r) align((r)->buf, alignof(union reducible))
#define reducer_end(r) (union reducible*)(void*)((char*)(r)->buf+(r)->sz)
#define reducer_next(i) (union reducible*)(void*)(((char*)(i)+(i)->com.next))
/* memory */
#define mem_begin(r) (struct mem){.red = (r), .cap = (r)->cap}
api void *mem_alloc(struct mem*, int size, int align);
#define mem_end(t) ((t)->red->cap = (t)->cap)
/* component */
api void adjust(struct context*, struct layout*, struct component*, XEvent*, void*);
api void dispatch(uint index, struct context*, struct layout*, XEvent*);
/* bindings */
#define bind_set(d,v) (struct interaction){INTERACTION_SET, .dst = d, .src = {.i = v}}
#define bind_toggle(d) (struct interaction){INTERACTION_NOT, .dst = d}
#define bind_enable(d,v) (struct interaction){INTERACTION_ENABLE, .dst = d, .src = {.f = v}}
#define bind_disable(d,v) (struct interaction){INTERACTION_DISABLE, .dst = d, .src = {.f = v}}
#define bind_drag_x(d) (struct interaction){INTERACTION_DRAG_X, .dst = d}
#define bind_drag_y(d) (struct interaction){INTERACTION_DRAG_Y, .dst = d}
#define bind_signal(d,u) (struct interaction){INTERACTION_SIGNAL, .dst = (void*)(addr)u, .src = {.s = d}}
#include <assert.h>
intern inline int
ceili(float x)
{
if (x >= 0) return cast(int,x);
int t = cast(int,x);
float r = x - cast(float,t);
return (r > 0.0f) ? t+1: t;
}
intern inline void
solve(struct component *c, int lo, int hi,
int center, int len, int mod, int an)
{
if (mod == lo) {
if (an == len)
c->attr[hi] = c->attr[lo] + c->attr[len];
else if (an == center) {
c->attr[len] = (c->attr[center] - c->attr[lo])*2;
c->attr[hi] = c->attr[lo] + c->attr[len];
} else c->attr[len] = c->attr[hi] - c->attr[lo];
} else if (mod == hi) {
if (an == len)
c->attr[lo] = c->attr[hi] - c->attr[len];
else if (an == center) {
c->attr[len] = (c->attr[hi] - c->attr[center])*2;
c->attr[lo] = c->attr[hi] - c->attr[len];
} else c->attr[len] = c->attr[hi] - c->attr[lo];
} else if (mod == len) {
if (an == lo)
c->attr[hi] = c->attr[lo] + c->attr[len];
else if (an == center) {
c->attr[lo] = c->attr[center] - (c->attr[len]/2);
c->attr[hi] = c->attr[center] + (c->attr[len]/2);
} else c->attr[lo] = c->attr[hi] - c->attr[len];
} else if (mod == center) {
if (an == lo) {
c->attr[len] = (c->attr[center] - c->attr[lo])*2;
c->attr[hi] = c->attr[lo] + c->attr[len];
} else if (an == hi) {
c->attr[len] = (c->attr[hi] - c->attr[center])*2;
c->attr[lo] = c->attr[hi] + c->attr[len];
} else {
c->attr[lo] = c->attr[center] - (c->attr[len]/2);
c->attr[hi] = c->attr[center] + (c->attr[len]/2);
}
}
c->attr[center] = c->attr[lo] + (c->attr[len]/2);
c->attr[len] = max(c->attr[len], 0);
}
api void
adjust(struct context *ctx, struct layout *ui,
struct component *c, XEvent *e, void *usr)
{
unused(usr);
if (c->flags & IS_MOVABLE_X)
c->attr[CX] += e->xmotion.x_root;
if (c->flags & IS_MOVABLE_Y)
c->attr[CY] += e->xmotion.y_root;
solve(c, L, R, CX, W, CX, W);
solve(c, T, B, CY, H, CY, H);
blueprint(ctx, ui);
layouting(ui);
}
intern inline void
build(struct component *c, int x, int y, int w, int h)
{
c->attr[L] = x; c->attr[T] = y;
c->attr[W] = max(w,0); c->attr[H] = max(h,0);
c->attr[R] = x+w; c->attr[B] = y+h;
c->attr[CX] = x+(w/2); c->attr[CY] = y+(h/2);
}
intern void
setup(struct component *c, const struct definition *d, struct surface *s)
{
c->flags = d->flags;
c->zorder = d->zorder;
build(c, d->size.x, d->size.y, d->size.w, d->size.h);
if (!(d->flags & HIDDEN))
c->attr[ACT] = true;
if (d->flags & PAINTABLE)
c->surf = surf_mk(s->canvas, c->attr[W], c->attr[H]);
if (d->flags & IS_MOVABLE)
c->slot.dragged = bind_signal(adjust, 0);
}
intern inline int
cond(const struct function *f, int d, int src)
{
float s = cast(float,src);
if (f->eq == COND_TRUE) return 1;
else if (f->eq == COND_FALSE) return 0;
else if (f->eq == COND_EQ)
return d == ceili(f->cons.mul*s)+f->cons.off;
else if (f->eq == COND_NEQ)
return d != ceili(f->cons.mul*s)+f->cons.off;
else if (f->eq == COND_GR)
return d > ceili(f->cons.mul*s)+f->cons.off;
else if (f->eq == COND_LS)
return d < ceili(f->cons.mul*s)+f->cons.off;
else if (f->eq == COND_GRE)
return d >= ceili(f->cons.mul*s)+f->cons.off;
else if (f->eq == COND_LSE)
return d <= ceili(f->cons.mul*s)+f->cons.off;
return 0;
}
intern inline void
eval(const struct function *f, int *d, int src)
{
float s = cast(float,src);
if (f->eq == CON_CPY) *d = src;
else if (f->eq == CON_SET)
*d = ceili(f->cons.mul*s)+f->cons.off;
else if (f->eq == CON_MIN)
*d = min(*d, ceili(f->cons.mul*s)+f->cons.off);
else if (f->eq == CON_MAX)
*d = max(*d, ceili(f->cons.mul*s)+f->cons.off);
}
intern void
insert(uint *tbl, int tbl_cnt, uint tid, uint val)
{
if (off(tid)) return;
uint begin = mod(key(tid), cast(uint, tbl_cnt)), id = begin;
do {uint idx = tbl[id];
if ((!id && val) || idx) continue;
tbl[id] = val; return;
} while ((mod(++id, cast(uint, tbl_cnt))) != begin);
}
api inline uint
find(const struct layout *ui, uint tid)
{
if (ui->tbl) {
uint begin = mod(key(tid), cast(uint, ui->tbl_cnt)), id = begin;
do {uint idx = ui->tbl[id];
if (!idx) return 0;
struct component *c = ui->comp + idx;
if (c->id == id) return id+off(tid);
} while ((mod(++id, cast(uint, ui->tbl_cnt))) != begin);
return 0;
} uint idx = key(tid) + off(tid);
return (idx >= cast(uint,ui->comp_cnt)) ? 0: idx;
}
intern uint
at(const struct layout *ui, const struct component *c, int *x, int *y)
{
loop:; uint *tbl = ui->seq;
const struct node *n = ui->tree + find(ui,c->id);
for (int i = 0; i < n->cnt; ++i)
tbl[ui->comp[find(ui, ui->nodes[n->sub+i])].zorder] = ui->nodes[n->sub+i];
*x += c->off.x; *y += c->off.y;
for (int i = n->cnt-1; i >= 0; --i) {
const struct component *sub = ui->comp + find(ui, tbl[i]);
if (!sub->attr[ACT] || !(sub->flags & INTERACTIVE)) continue;
if (inbox(*x, *y, sub->attr[L], sub->attr[T], sub->attr[W], sub->attr[H]))
{c = sub; goto loop;}
}
return c->id;
}
api void
reorder(struct layout *ui, struct component *c)
{
const struct node *n = ui->tree + find(ui,c->id);
while (n->parent != n->id) {
struct component *p = ui->comp + find(ui,n->parent);
if (p->flags & STATIC) goto nxt;
n = ui->tree + find(ui,p->id);
for (int i = 0; i < n->cnt; ++i) {
struct component *sub = ui->comp + find(ui,ui->nodes[n->sub+i]);
if (sub->flags & BACKGROUND) continue;
if (sub->zorder > c->zorder)
sub->zorder--;
} c->zorder = max(n->cnt-1, 0);
nxt: c = p;
}
return;
}
api inline void
dispatch(uint id, struct context *ctx, struct layout *ui, XEvent *e)
{
uint idx;
do {idx = find(ui, id);
if (ui->def[idx].handle)
if (ui->def[idx].handle(ctx,ui,ui->comp+idx,e)) break;
} while ((id = ui->tree[idx].parent));
}
intern void
interact(struct context *ctx, struct layout *ui, struct component *c,
struct interaction *i, XEvent *e)
{
if (INTERACTION_NOP == i->type);
else if (INTERACTION_SIGNAL == i->type)
i->src.s(ctx, ui, c, e, i->dst);
else if (INTERACTION_SET == i->type)
{int *v = (int*)i->dst; *v = i->src.i;}
else if (INTERACTION_ENABLE == i->type)
{uint *v = (uint*)i->dst; *v |= i->src.f;}
else if (INTERACTION_DISABLE == i->type)
{uint *v = (uint*)i->dst; *v &= ~i->src.f;}
else if (e->type == MotionNotify) {
int *d = (int*)i->dst;
*d += (i->type == INTERACTION_DRAG_X) ?
e->xmotion.x_root: e->xmotion.y_root;
}
}
api void
paint(struct context *ctx, struct layout *ui)
{
for (int i = 0; i < ui->comp_cnt; ++i) {
struct component *c = ui->comp + i;
surf_resize(c->surf, c->attr[W], c->attr[H]);
if (!(c->flags & PAINTABLE) || (c->flags & IS_LAYER))
continue;
if (ui->def[i].paint)
ui->def[i].paint(ctx,ui,c);
}
}
api void
draw(struct context *ctx, struct layout *ui, uint root_id)
{
uint head = 0;
struct component *r = ui->comp + find(ui, root_id);
uint *tbl = ui->seq, *stk = ui->buffer;
stk[head++] = root_id;
while (head > 0) {
struct component *c = ui->comp + find(ui, stk[--head]);
if (c->surf && c->attr[ACT] && c->id != root_id) {
if (c->flags & IS_LAYER) {
ui->buffer = stk + head;
if (ui->def[c->id].paint)
ui->def[c->id].paint(ctx,ui,c);
}
surf_blit(r->surf, c->surf, c->attr[L] - r->attr[L] - r->off.x,
c->attr[T] - r->attr[T] - r->off.y, 0, 0, c->attr[W], c->attr[H]);
}
if (!c->attr[ACT] || ((c->flags & IS_LAYER) && (c->id != root_id))) continue;
const struct node *n = ui->tree + find(ui, c->id);
for (int i = 0; i < n->cnt; ++i)
tbl[ui->comp[find(ui, ui->nodes[n->sub+i])].zorder] = ui->nodes[n->sub+i];
for (int i = n->cnt-1; i >= 0; --i)
stk[head++] = tbl[i];
}
ui->buffer = stk;
}
api void
blueprint(struct context *ctx, struct layout *ui)
{
uint head = 0, tail = 1;
uint *que = ui->buffer; que[tail] = ROOT;
while (head < tail) {
const struct node *n = ui->tree + find(ui, que[++head]);
for (int i = 0; i < n->cnt; ++i)
que[++tail] = ui->nodes[n->sub+i];
}
for (uint i = tail; i > 0; --i) {
const struct definition *d = ui->def + find(ui, que[i]);
struct component *c = ui->comp + find(ui, que[i]);
if (d->blueprint)
d->blueprint(ctx, ui, c);
}
}
api void
layouting(struct layout *ui)
{
for (int i = 0; i < ui->con_cnt; ++i) {
const struct constraint *con = ui->con + i;
const struct function *co = &con->cond;
struct component *cd, *cs, *d, *s;
cd = ui->comp + find(ui, co->dst.comp);
cs = ui->comp + find(ui, co->src.comp);
d = ui->comp + find(ui, con->self.dst.comp);
s = ui->comp + find(ui, con->self.src.comp);
if (cond(co, cd->attr[co->dst.attr], cs->attr[co->src.attr])) {
eval(&con->self, d->attr+con->self.dst.attr, s->attr[con->self.src.attr]);
solve(d, L, R, CX, W, con->self.dst.attr, con->anch);
solve(d, T, B, CY, H, con->self.dst.attr, con->anch);
}
}
}
api void
update(struct context *ctx, struct layout *ui, XEvent *e)
{
switch (e->type) {
default: dispatch(ui->active, ctx, ui, e); break;
case Expose:
case ConfigureNotify: {
/* resize event */
struct component *c = ui->comp + ROOT;
int w = (e->type == Expose) ? e->xexpose.width: e->xconfigure.width;
int h = (e->type == Expose) ? e->xexpose.height: e->xconfigure.height;
build(c, 0, 0, w, h);
surf_resize(c->surf, w, h);
blueprint(ctx, ui);
layouting(ui);
for (int i = 0; i < ui->comp_cnt; ++i)
dispatch(cast(uint,i),ctx,ui,e);
} break;
case MotionNotify: {
/* enter and leave event */
uint last_hot = ui->hot;
int mx = e->xmotion.x, my = e->xmotion.y;
ui->hot = at(ui, ui->comp, &e->xmotion.x, &e->xmotion.y);
if (last_hot != ui->hot) {
struct component *prev = ui->comp + last_hot;
struct component *cur = ui->comp + ui->hot;
prev->left = true;
prev->hovered = false;
e->xmotion.type = Leave;
interact(ctx, ui, prev, &prev->slot.leave, e);
dispatch(prev->id, ctx, ui, e);
cur->entered = cur->hovered = true;
e->xmotion.type = Enter;
interact(ctx, ui, cur, &cur->slot.enter, e);
dispatch(cur->id, ctx, ui, e);
} /* dragging event */
if (ui->active == ui->origin) {
struct component *a = ui->comp + find(ui,ui->active);
interact(ctx,ui, a, &a->slot.dragged, e);
e->xmotion.type = Drag;
dispatch(a->id,ctx,ui,e);
a->dragged = true;
}
e->xmotion.x = mx, e->xmotion.y = my;
e->xmotion.type = MotionNotify;
} break;
case ButtonRelease:
case ButtonPress: {
int down = e->type == ButtonPress;
ui->active = (down) ? ui->hot: ui->active;
ui->origin = (down) ? ui->hot: ROOT;
struct component *a = ui->comp + find(ui,ui->active);
if (down) a->pressed = true;
else a->released = true;
if (e->xbutton.button == Button1) {
reorder(ui, a); /* Left-Button */
if (down) interact(ctx,ui, a, &a->slot.left_pressed, e);
else interact(ctx,ui, a, &a->slot.left_released, e);
} else if (e->xbutton.button == Button3){
reorder(ui, a); /* Right-Button */
if (down) interact(ctx,ui, a, &a->slot.right_pressed, e);
else interact(ctx,ui, a, &a->slot.right_released, e);
} dispatch(a->id,ctx,ui,e);
} break;}
}
api void
clear(struct layout *ui)
{
for (int i = 1; i < ui->comp_cnt; ++i)
if (ui->comp[i].surf)
surf_del(ui->comp[i].surf);
}
api void
reducer_init(struct reducer *r, enum layout_type type, void *mem, int sz)
{
r->mode = type;
r->buf = mem; r->cap = sz;
r->max_com = r->max_con = 0;
r->max_ext = r->payload = r->sz = 0;
}
api void
reducer_push(struct reducer *r, union reducible *a,
const void *dat, int size, int align)
{
assert(r->sz + szof(union reducible) + size + align + alignof(union reducible) < r->cap);
char *end = r->buf + r->sz;
char *usr = align(end + szof(union reducible), max(align,1));
char *nxt = (align(usr + size + align, alignof(union reducible)));
a->com.next = cast(int,(nxt-end));
switch (a->com.type) {
case RED_COMP: r->max_com++; break;
case RED_CON: r->max_con++; break;
case RED_LNK: r->max_nodes++; break;
case RED_EXT:
r->payload += size + a->ext.align;
a->ext.off = cast(int,usr-end);
a->ext.align = align;
a->ext.size = size;
r->max_ext++;
default: break;}
copy(end, a, szof(*a));
copy(usr, dat, size);
r->sz += a->com.next;
}
api void*
mem_alloc(struct mem *t, int size, int align)
{
struct reducer *r = t->red;
assert(r->sz + size + align < r->cap);
char *begin = r->buf + r->cap - size;
char *ptr = alignb(begin, align);
r->cap -= (ptr - (r->buf + r->cap));
return ptr;
}
api void
reducer_add(struct reducer *r, in struct module *m, int cnt)
{
for (int i = 0; i < cnt; ++i) {
const struct module *c = m + i;
reducer_comp(r,c->tid,c->def,0);
reducer_lnk(r,c->tid,c->parent);
for (int j = 0; j < c->cnt; ++j)
reducer_con(r,c->con[j]);
}
}
api void
reducer_calc(struct reducer *r, in struct layout *ui, out int *mem)
{
r->max_nodes += ui->node_cnt; r->max_ext += ui->ext_cnt;
r->max_com += ui->comp_cnt; r->max_con += ui->con_cnt;
*mem = r->max_com * szof(struct node);
*mem += r->max_com * szof(struct definition);
*mem += r->max_con * szof(struct constraint);
*mem += r->max_com * szof(struct component);
*mem += r->max_ext * szof(struct extension);
*mem += (r->max_com+1) * szof(int) * ((r->mode == RETAINED) ?1:2);
*mem += (r->max_nodes) * szof(int) * 2;
*mem += szof(struct layout) + alignof(struct layout);
*mem += alignof(struct component) + alignof(struct definition);
*mem += alignof(struct constraint) + alignof(struct extension);
*mem += alignof(struct node) + alignof(int);
*mem += ui->payload + r->payload;
}
api void
fold(out struct layout **ret, in struct reducer *r, in struct layout *ui,
void *mem, int sz, in struct extdef **ed, int cnt)
{
union reducible *a;
struct node *tree;
struct definition *def;
struct constraint *con;
struct extension *ext;
uint *nodes;
/* 0.) setup temp node memory for stage III and IV */
assert((r->cap - r->sz) > (szof(int) * r->max_com) + alignof(int));
int *tbl = align(r->buf + r->sz, alignof(int));
for (int i = 0; i < ui->comp_cnt; ++i)
tbl[i] = ui->tree[i].cnt;
/* I.) setup layout in memory block */
struct layout *res = align(mem, alignof(struct layout));
if (r->mode == IMMEDIATE) {
res->tbl_cnt = r->max_com;
res->tbl = align(res+1, alignof(int));
res->buffer = res->tbl + res->tbl_cnt;
} else res->buffer = align(res+1, alignof(int));
res->seq = res->buffer + r->max_com+1;
res->nodes = nodes = res->seq + r->max_nodes;
res->comp = align(res->nodes + r->max_nodes, alignof(struct component));
res->def = def = align(res->comp + r->max_com, alignof(struct definition));
res->tree = tree = align(res->def + r->max_com, alignof(struct node));
res->con = con = align(res->tree + r->max_com, alignof(struct constraint));
res->ext = ext = align(res->con + r->max_con, alignof(struct extension));
void *payload = ext + r->max_ext;
res->payload = ui->payload + r->payload;
res->comp_cnt = ui->comp_cnt;
res->con_cnt = ui->con_cnt;
res->ext_cnt = ui->ext_cnt;
res->node_cnt = r->max_nodes;
res->init = true;
res->size = sz;
*ret = res;
/* II.) copy old state */
copy(res->comp, ui->comp, szof(struct component)*ui->comp_cnt);
copy(def, ui->def, szof(struct definition)*ui->comp_cnt);
copy(tree, ui->tree, szof(struct node)*ui->comp_cnt);
copy(con, ui->con, szof(struct constraint)*ui->con_cnt);
copy(ext, ui->ext, szof(struct extension)*ui->ext_cnt);
for (int i = 0; r->mode == IMMEDIATE && i < ui->comp_cnt; ++i)
insert(res->tbl, res->tbl_cnt, ui->comp[i].id, cast(uint,i));
for (int i = 0; i < ui->ext_cnt; ++i) {
ext[i] = ui->ext[i];
ext[i].addr = (addr)align(payload, ext[i].info.align);
copy((void*)ext[i].addr, (void*)ui->ext[i].addr, ext[i].info.size);
payload = (char*)ext[i].addr + ext[i].info.size;
}
/* III.) add new state */
for (a = reducer_begin(r); a < reducer_end(r); a = reducer_next(a)) {
if (RED_CON == a->com.type)
con[res->con_cnt++] = a->con.con;
else if (RED_LNK == a->com.type)
tbl[find(res, a->lnk.parent)]++;
else if (RED_COMP == a->com.type) {
const uint index = cast(uint, res->comp_cnt++);
res->comp[index].usr = a->comp.usr;
def[index] = a->comp.d;
if (r->mode == IMMEDIATE)
insert(res->tbl, res->tbl_cnt, a->comp.tid, index);
res->comp[index].id = tree[index].id = a->comp.tid;
setup(res->comp + index, def + index, ui->comp[0].surf);
} else if (RED_EXT == a->com.type) {
const void *obj = (const void*)((const char*)a + a->ext.off);
char *dst = align(payload, a->ext.align);
copy(dst, obj, a->ext.size);
payload = dst + a->ext.size;
ext[res->ext_cnt].info.id = a->ext.id;
ext[res->ext_cnt].info.size = a->ext.size;
ext[res->ext_cnt].info.align = a->ext.align;
ext[res->ext_cnt++].addr = (addr)dst;
}
} /* IV.) setup tree nodes */
for (int i = 1; i < r->max_com; ++i)
tree[i].sub = tbl[i-1] + tree[i-1].sub;
for (int i = 0; i < ui->comp_cnt; ++i)
copy(nodes+tree[i].sub, ui->nodes+ui->tree[i].sub, ui->tree[i].cnt*szof(int));
for (a = reducer_begin(r); a < reducer_end(r); a = reducer_next(a)) {
if (RED_LNK != a->com.type) continue;
const uint id = find(res, a->lnk.parent);
nodes[tree[id].sub + tree[id].cnt++] = a->lnk.child;
tree[find(res, a->lnk.child)].parent = a->lnk.parent;
} /* V.) setup extension state */
for (int i = 0; i < cnt; ++i) {
const struct extension *e = 0;
map(res, ed[i]->info.id, e)
ed[i]->link(res, (void*)e->addr);
}
}
api void
reposit(FILE *fp, in struct layout *ui, const struct commit *com)
{
const char *tab = (!com->tab) ? " ": com->tab;
const char *attrs[] = {"0","ACT","L","T","R","B","W","H","CX","CY"};
const char *cons[] = {"CON_CPY","CON_SET","CON_MIN","CON_MAX"};
const char *cond[] = {"COND_TRUE","COND_FALSE","COND_EQ","COND_NEQ","COND_GR","COND_LS","COND_GRE","COND_LSE"};
fprintf(fp,"global const struct constraint %s[] = {\n", com->con);
for (int i = 0; i < ui->con_cnt; ++i) {
const struct constraint *c = ui->con + i;
fprintf(fp, "%s{{.eq = %s, .dst = {%lu,%s}, .src = {%lu,%s}, .cons = {.mul = %.2ff, .off = %d}}, .anch = %s,",
tab,cons[c->self.eq], c->self.dst.comp, attrs[c->self.dst.attr], c->self.src.comp, attrs[c->self.src.attr],
(double)c->self.cons.mul, c->self.cons.off, attrs[c->anch]);
if (c->cond.eq != COND_TRUE) {
fprintf(fp, " .cond = {.eq = %s, .dst = {%lu,%s}, .src = {%lu,%s}, .cons = {.mul = %.2ff, .off = %d}}},\n",
cond[c->cond.eq], c->cond.dst.comp, attrs[c->cond.dst.attr], c->cond.src.comp,attrs[c->cond.src.attr],
(double)c->cond.cons.mul, c->cond.cons.off);
} else fputs("},\n",fp);
} fputs("};\n", fp);
fprintf(fp, "global const struct definition %s[] = {\n", com->def);
for (int i = 0; i < ui->comp_cnt; ++i) {
const struct definition *d = ui->def + i;
char buf[128]; int n = 0; buf[n++] = '0';
if (d->flags & HIDDEN) n += copy(buf+n, "|HIDDEN",7);
if (d->flags & INTERACTIVE) n += copy(buf+n, "|INTERACTIVE",12);
if (d->flags & PAINTABLE) n += copy(buf+n, "|PAINTABLE",10);
if (d->flags & IS_LAYER) n += copy(buf+n, "|LAYER",6);
if (d->flags & IS_MOVABLE_X) n += copy(buf+n, "|MOVABLE_X",10);
if (d->flags & IS_MOVABLE_Y) n += copy(buf+n, "|MOVABLE_Y",10);
buf[n] = 0;
fprintf(fp, "%s{tbl(%s,%s,%s), .size={%d,%d,%d,%d}, .flags = %s},\n",
tab,d->i, d->p, d->b, d->size.x, d->size.y, d->size.w, d->size.h, buf);
} fputs("};\n", fp);
fprintf(fp, "global const uint %s[] = {", com->nodes);
for (int i = 0; i < ui->node_cnt; ++i) {
if (!(i & 15)) fprintf(fp,"\n%s", tab);
fprintf(fp, "%lu,", ui->nodes[i]);
} fputs("\n};\n",fp);
fprintf(fp, "global const struct node %s[] = {\n", com->tree);
for (int i = 0; i < ui->comp_cnt; ++i) {
const struct node *n = ui->tree + i;
fprintf(fp, "%s{.id = %lu, .parent = %lu, .cnt = %d, .sub = %d},\n",
tab, n->id, n->parent, n->cnt, n->sub);
} fputs("};\n", fp);
for (int i = 0; i < com->cnt; ++i) {
const struct extension *e = 0;
const struct extlnk *lnk = com->ext_list + i;
fprintf(fp, "global %s %s[] = {\n", lnk->def->info.type, lnk->name);
map(ui, lnk->def->info.id, e) {
fprintf(fp, "%s",tab); lnk->def->commit(fp, (void*)e->addr);
} fputs("0};\n",fp);
}
fprintf(fp, "global const struct extension %s[] = {", com->ext);
for (int i = 0; i < com->cnt; ++i) {
const struct extension *e = 0; int cnt = 0;
const struct extlnk *lnk = com->ext_list + i;
map(ui, lnk->def->info.id, e)
fprintf(fp, "\n%s{.info = {.id = %lu, .size = %d, .align = %d}, .addr = (addr)&%s[%u]},",
tab,e->info.id, e->info.size, e->info.align, lnk->name, cnt++);
} if (!ui->ext_cnt) fputs("{{0}}};\n", fp);
else fputs("\n};\n",fp);
fprintf(fp, "global uint %s[cntof(%s)];\n",com->buffer,com->def);
fprintf(fp, "global uint %s[cntof(%s)];\n",com->seq,com->def);
fprintf(fp, "global struct component %s[cntof(%s)];\n", com->comp,com->def);
fprintf(fp, "global struct layout %s = {\n", com->layout);
fprintf(fp, "%s.comp = %s,\n%s.tree = %s,\n", tab,com->comp, tab, com->tree);
fprintf(fp, "%s.buffer = %s,\n%s.seq = %s,\n", tab,com->buffer, tab,com->seq);
fprintf(fp, "%s.def = %s,\n%s.con = %s,\n%s.ext = %s,\n",tab,com->def,tab,com->con,tab,com->ext);
fprintf(fp, "%s.comp_cnt = %d,\n%s.con_cnt = %d,\n", tab,ui->comp_cnt,tab,ui->con_cnt);
fprintf(fp, "%s.ext_cnt = %d,\n%s.node_cnt = %d,\n};\n", tab,ui->ext_cnt, tab,ui->node_cnt);
}
api void
Begin(struct context *ctx, struct layout *ui, enum layout_type type,
uint root, void *mem, int siz, struct surface *scrn)
{
ctx->layout = ui;
ctx->cur = ctx->stk[0] = 0;
ui->comp[ROOT].surf = scrn;
reducer_init(&ctx->red, type, mem, siz);
for (int i = 0; i < ui->comp_cnt; ++i)
ui->comp[i].pressed = ui->comp[i].released = ui->comp[i].entered =
ui->comp[i].left = ui->comp[i].dragged = 0;
if (!ui->init) {
for (int i = 0; i < ui->comp_cnt; ++i) {
const struct node *n = ui->tree + i;
struct component *c = ui->comp + i;
c->id = cast(uint, n->id);
setup(c, ui->def + i, scrn);
} ui->init = true;
} Push(ctx, root);
}
api void
End(struct context *ctx)
{
struct layout *ui = ctx->layout;
reducer_calc(&ctx->red, ui, &ui->requested);
ctx->layout = ctx->root;
ctx->stk[0] = ctx->cur = 0;
}
#endif
@sagacity
Copy link

sagacity commented May 7, 2017

On the subject of having to copy all state every frame, would it not be possible to run some kind of diff algorithm and then only store the changes between the current and the previous state? Persistent data structures such as this are what I'm talking about. I believe this would be fairly similar to what a library like React does in combination with the browser's own DOM implementation.

Computing the diff could become costly but perhaps this could be optimized by detecting which of the (sub-)layouts was affected.

@vurtun
Copy link
Author

vurtun commented May 9, 2017

Hm... not 100% what you mean. The reducer only takes the diff between current declaration pass and last commited state and takes that and generates a new layout. So I guess you mean why copy old state for the new layout instead of incorporating diffs directly into code? In general yes should be possible to do that. But it would complicate everything to an extend that it does not make sense at least in this instance to use immutable data at all.

You also wouldn't copy all UI each frame only those parts which actually need it. Probably 90-95% of anyUI is compile time known and therefore does not need to be mutated and can stay in compile time tables. Especially since you can deactivate components and the ability of constraints to be reactive through conditions. So what is left are data structure based widgets like list,trees,graphs and for more graphical intensive Apps widgets like node editors, timelines or in general highly dynamic widgets.

Furthermore you can nest layouts so you have very fine grained control over which portions of your UI needs to update to balance out performance and memory usage.

Finally the only state I personally would even copy in the first place are layout specific data structures (components, definitions, ...) or very light weight UI specific state. So memory intensive UI objects like text editor state or list selections should be directly allocated and modified and only the pointer should be copied around and lifetime managed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment