Skip to content

Instantly share code, notes, and snippets.

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

Graphical User Interfaces

For the last few weeks I spend some time coding, writing and cleaning up my notes from almost a year since I published nuklear.

Basically this is a possible implementation for a graphical user interface builder backend with support for an immediate mode style API. So it provides a way to define non-mutating UI state, an immediate mode style API for dynamic UI components (lists,trees,...) and a combination of both.

The core implementation is ~800 LOC without any kind of default widgets or extensions. At first this seems quite counter intuitive. However since the inherent design allows for lots of different ways to define any widget like buttons it does not make sense to provide a specific default implementation. The way this code was architectured furthermore removes the need for style/skinning configurations used in Nuklear since widget painting is just calling a small number of simple drawing routines. So by design it probably is closer to Oui from @paniq than nuklear or other imgui libraries.

The reason for keeping the design simple and minimal is not lazyness but more with a fundamental truth that UI is a rewritable problem. As soon as basics concepts are researched it becomes extremly easy to write your own specific version.
This is basically the main reason I stopped developing nuklear any further for quite some time. A library for UI inherently does not make sense. As long as drawing, font, platform and input handling is provided writing a custom UI solving your specific problem is both easy and produces simpler code than a generic solution.

However not all basic concept have been researched since it takes quite some time to often remove a lot of artifical abstractions found in GUI libraries including my own and get to the core of the problem. For example the main reason I was able to cram this implementation into ~800 LOC without losing functionality was using two very simple concepts. If you take a peek into nuklears widget implementation you will probably notice how most code does nothing and Everything is made out of rectangles placed by very simple linear functions. It makes sense since rectangles are the basic building blocks for input and drawing while simple linear functions are used for layouting.

Another big step forward in understanding was the fact that I finally understood what I did not like about immediate mode GUIs. Immediate mode APIs by nature are very dynamic. While this is the main advantage given for using imguis it also introduces a lot of unneeded complexity in places that do not require it. The actual problem however is that most UI are predefined. If you actually look at your imgui code almost nothing changes on frame to frame basis. The main benefactor of imgui style code are datastructure based widgets like trees, lists, graphs.... Granted these however need to be as dynamic as possible and even support cases like being to big to display as a whole.

The biggest advantage of specifically using compile time UI tables for definitions is very simple and short code. Basically every time I encounter very small but at the same time very powerful code I often see a lot of compile time data tables. Especially in compression circles. One of my favorite examples of this are some of Jon Olicks (@JonOlick) single file libraries and he even used it for animations files. Another great example is suckless who use compile time tables for configuration.

Side note it was suckless as well from whom I even got the idea of heavily using tables a lot more in the first place. I even successfully used it for an university OpenGL project to manage the OpenGL state machine.

Update (xx.4.2017)

Another month went by and I further tested and extended the UI core a little bit. Spend some time thinking about the design and hopefully solving some of the bigger problems I currently see with this approach.

Compile time tables or immutable data structures have some really nice properties. Simpler, shorter and easier to understand code, no memory management overhead and free hash tables. But they also have more less measureable advantages like forcing you to think about the problem by focusing on the relation of data and the core of the problem outside artifical abstractions or limitations of programming language.

However there are some problems as well. The most obvious is well they are by definition immutable so cannot change. While this is not a problem for probably 95% of UI applications if combined with an immediate mode API it still makes it unusable otherwise. Another problem is how to generate these tables. It sounds convient on the designers side to just say: "use an external UI tool to generate these tables". In reality by forcing the user to use an outside ui tool to create ui is a somewhat of catch 22. So just proclaiming "just write an editor" is not the right approach here at least as only option. Writing these tables by hand is not an option either (These specific table in this example are written by hand and it was quite a tedious task).

A further problem encountered is a layouting step I forgot provided in Slate. While in general I cover all the UI steps I missed or lets say I prefered to overlook one. That is components computing their prefered size and the ability for components which contain sub-components to layout without neccessarly having to rely on constraints. The biggest problem space to cover here are text based wigets with different amount of space requirements based on the currently selected language. An easy but imo not simple solution would have been to just introduce another callback. I don't like relying on callbacks alone very much especially for something so problem core specific. Something was missing for me at that point and I knew if I don't come up with a better solution I will just fallback to callbacks.

So these are the three biggest problems I worked on the last month and hopefully solved. They all seemed solvable from the beginning however I had my doubts these solution could be achieved in the same style the rest of the UI was written in and feared the design would fall in on itself. To prevent complexity I decided to not make prisoners or trade offs and if the design falls short then it only proves that it was not good to begin with. But I also know that design takes time, constraints and more importantly ideas.

I decided to look at some references again and crossed out into browserland with react and redux and what they are all called. I took some time to understand the fundamental ideas and I got intrigued by redux. For one it explains why websites take up so much memory. But the more interesting concept was behind redux comming from functional programming. Personally I was never a big fan of functional programming but I also never went deep enough and I have seen too many good developers dive into it only to come back years later to try it myself.

Basically all application state in Redux is or rather should be immutable. All state mutation thereby is done by what is called a reducer which is just an callback in redux taking in an action identifier (eg.: add user) and return a new object copied from the old total application state mutated by given modified action state. In functional programming this concept is a higher order function called fold. While I think it is insane to copy around application state maybe it makes sense for web application because of how little state they have (?) or some interpreter magic (?) but it probably has some negative memory consumption effect.

While Redux not really impressed me it still got me thinking and reminded me of some great talks given by Rich Hickey(1, 2, 3). However it sounded to wasteful to copy state around for every state change. But I still spend some time thinking about it.

First up I noticed that it does not make sense to think of fold as a callback in this concept. Where there is one there are many and even the definition of fold/reduce works on lists. So inherently you do not have to copy everything on every state mutation but instead gather up and commit these changes (comparable to repositories). So while fold is conceptually coping memory for every single state mutation the only thing important in the end is the result of all state changes and its adherence to folds definition.

Furthermore since this design already supports imui to handle the more dynamic UI portions there are very little changes happending at all. Not only that but the total cost of coping UI state at least in comparison with copying application state is rather small both in performance and memory. Biggest problem was however custom user UI state which needs to be copied as well which required some additional thought but proved solvable.

So I was able to solve the first problem of all state being immutable by using some ideas borrowed from functional programming. One nice side effect of using folded immutable data structures is having values over time which can be quite nice for debugging (Think of replayablity).

Next up was how to generate source compile time tables. The solution originating from my UI state mutating solution was pretty straight forward. Just provide a way to serialize the current UI state by generating a header file containing compile time tables. While serializing default widget implementations is quite straightfoward, custom widget extension with state proved to by a little bit more complex but still managable. So at this point you are able to mutate UI state and write these changes out, restart and all UI changes are now in compile time tables.

So you can use your code/text editor as your UI editor with all changes flowing into compile time tables without having to change any code. This approach especially in combination with runtime code reloading is quite powerful while it still makes it possible to have external tools to create UI which is an absolute total win on all fronts. So preferably you only need mutating state in development process or outside for plugin support.

The cost associated with all these improvements is of course more core code. However I was able to keep everything down to ~250 LOC for both mutating state and serialization. However custom widget extensions with state now need a function to relink state on changes which was already required previously on initialization and a custom function to serialize its state to header file which should be bearable.

The solution to the final problem of having component calculate their prefered size and function or rather code based layouting. Like I said the first solution is just providing a callback which is probably a good solution for UI editor generated UI. However for code we can also reuse declaration used for immutable state to precompute the prefered size if wanted. So this problem itself was solved indirectly as well.

So after all these changes the general sequence of UI execution is:

1.) Declaration

Widgets declarations are an umbrella term for multiple possible ways to setup new, allocated or compile time widgets. The interesting part is each widgets shoud be able transition in the course of the development livetime. So you begin by defining a new widgets in the first run, then state gets mutated and now the widget is allocated and setup each frame. Finally you serialize the widgets into source file and it is compile time.

Declaration can also have a big variarty of widget setup functionality. For example declarations could define prefered size, bind interactions to slots, create new widgets, reset user pointers, .... Important however is that no ui event handling happens in this declaration phase This seems to a step backwards from imgui which only required one function call for everything. However I think separating UI declaration from event handling is even desireable since the UI does not dictate your code structure while handling ui events unlike pure imui code. Also for most default widgets you can use bindings to simplify event handling to one functions as well or go full immediate mode and provide an API similar to imgui (eg. button in example).

Example:

panel_begin(...);
{
    layout_row(...); /* stolen from nuklear not required just an abstraction over constraints only called for new widgets */
    button(...);
    button(...);
    slider(...);
}
panel_end(...);
/* ... */
2.) Processing

Main core UI functionality with layouting, component processing, event handling and painting. Pseudo code:

blueprint(...); /* calculate prefered size and do function based layouting */
layouting(...); /* constraint interpreter validating component placement */
update(...); /* input handling and update of UI state */
layouting(...); /* validate all layouting changes caused by input handling */
paint(...); /* calls paint callbacks and draws to surface/texture. Does no blitting! */
3.) imgui and input event handling

In this phase all ui event handling is done in combination with running all immediate mode UI code. Handling ui events can be done in three different ways. By callbacks, slots + bindings and finally in this phase in code. In addition it is possible to implement or plugin a imgui library here as long as all drawing is finalized into a surface/texure inside a component. Example:

/* handle click of previously declared button */
if (button(BUTTON_ID,...)) {
    /* event handling */
}
/* 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, ...)
}
4.) Draw/Blit

Finally all drawable components will be blit to screen. However you could also implement a dirty rect approach and only bit what is actually needed. In general you can also optimize painting and blitting to only happen on changes, so there is a lot of room for optimization.

Core

Enough talk at this point I should get into some code. First up is the core architecture. It basically consists of a number of utility functions as well as the main constraint interpreter and functions for handling input, painting, drawing, fold and commit.

#ifndef UI_INCLUDED
#define UI_INCLUDED

#include <stdio.h> /* fputs, fprintf */
#include <string.h> /* memcpy */

#define api extern
#define hook static
#define intern static
#define global static
#define storage static

#define stringify(x) #x
#define unused(...) unused_impl(0,__VA_ARGS__)
#define copy(d,s,sz) memcpy(d,s,(size_t)(sz))
#define szof(a) ((int)sizeof(a))
#define cntof(a) ((int)(sizeof(a)/sizeof((a)[0])))
#define alignof(t) ((char*)(&((struct {char c; t _h;}*)0)->_h) - (char*)0)
#define align(x, mask) ((void*)(((long long)((const char*)(x) + (mask-1)) & ~(mask-1))))
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 min3(a,b,c) min(min(a,b),c)
#define max3(a,b,c) max(max(a,b),c)
#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)))

struct layout;
struct context;
struct component;
struct extension;

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*, const XEvent*);
typedef void(*signal_f)(struct context*,struct layout*,struct component*, void *usr);
typedef void(*blueprint_f)(struct context*,struct layout*,struct component*);
typedef void(*apply_f)(struct layout*, const struct extension*, void*);

/* interaction */
#define MAX_BINDING_PER_INTERACTION 4
enum {Enter = LASTEvent, Leave, Drag};
enum binding_type {
    BINDING_SET,
    BINDING_SETE, BINDING_SETNE,
    BINDING_SETS, BINDING_SETNS,
    BINDING_SETG, BINDING_SETGE,
    BINDING_SETL, BINDING_SETLE,
    BINDING_XCHG, BINDING_CMPXCHG,
    BINDING_ENABLE, BINDING_DISABLE,
    BINDING_DRAG_X, BINDING_DRAG_Y,
    BINDING_PROJECT, BINDING_SIGNAL
};
struct binding {
    enum binding_type type;
    union {
        int i;
        unsigned f;
        signal_f s;
        struct {int o, n;} ex;
        struct {const int *v,*a,*b;} cis;
        struct {
            const int *min,*val;
            const int *max,*tar;
        } proj;
    } src;
    void *dst;
};
struct interaction {
    struct binding b[MAX_BINDING_PER_INTERACTION];
    int cnt;
};
struct interaction_slots {
    struct interaction left;
    struct interaction right;
    struct interaction dragged;
    struct interaction enter;
    struct interaction leave;
};

/* component */
#define COMPONENT_ROOT 0
#define MAX_NODES_PER_NODE 32
#define TBL(hn,pn,bp) .handle=hn,.paint=pn,.blueprint = bp,.h=#hn, .p=#pn, .b=#bp
enum attributes {INVALID, ACT, L, T, R, B, W, H, CX, CY, ATTR_CNT};
enum component_flags {
    COMP_DEFAULT,
    COMP_HIDDEN = flag(0),
    COMP_INTERACTIVE = flag(1),
    COMP_PAINTABLE = flag(2),
    COMP_LAYER = flag(3),

    COMP_IS_MOVABLE_X = flag(4),
    COMP_IS_MOVABLE_Y = flag(5),
    COMP_IS_MOVABLE = COMP_IS_MOVABLE_X|COMP_IS_MOVABLE_Y,

    COMP_MOVABLE_X = COMP_IS_MOVABLE_X|COMP_INTERACTIVE,
    COMP_MOVABLE_Y = COMP_IS_MOVABLE_Y|COMP_INTERACTIVE,
    COMP_MOVABLE = COMP_IS_MOVABLE|COMP_INTERACTIVE,
};
struct node {
    int id, parent;
    int cnt, sub[MAX_NODES_PER_NODE];
};
struct definition {
    handle_f handle;
    paint_f paint;
    blueprint_f blueprint;
    struct rect sz;
    unsigned flags;
    const char *h, *p, *b;
};
struct component {
    int id, attr[ATTR_CNT];
    struct v2 off, total;
    int zorder;
    unsigned flags;
    struct XSurface *canvas;
    addr usr;

    unsigned pressed:1;
    unsigned released:1;
    unsigned hovered:1;
    unsigned dragged:1;
    struct interaction_slots slot;
};

/* constraint */
enum constraint_eq {
    CONSTRAINT_CPY, CONSTRAINT_SET,
    CONSTRAINT_MIN, CONSTRAINT_MAX
};
enum condition_eq {
    CONDITION_TRUE, CONDITION_FALSE,
    CONDITION_EQ, CONDITION_NEQ,
    CONDITION_GR, CONDITION_LS,
    CONDITION_GRE, CONDITION_LSE
};
struct cons {float mul; int off;};
struct var {int comp, attr;};
struct function {
    int eq;
    struct var dst;
    struct var src;
    struct cons cons;
};
struct constraint {
    struct function self;
    enum attributes anch;
    struct function cond;
};

/* input */
enum keys {
    KEY_NONE,
    /* ... */
    KEY_MAX
};
#define TEXT_INPUT_MAX 16
struct key {int down, pressed, released;};
struct input {
    struct mouse {
        struct key left;
        struct key right;
        struct v2 pos,delta,click;
        int wheel;
    } mouse;
    struct key keys[KEY_MAX];
    char txt[TEXT_INPUT_MAX];
    int txt_len;
};

/* reducer */
enum action_id {
    ACTION_MKCOMP, ACTION_MKCON,
    ACTION_LNKNODE, ACTION_CUSTOM,
};
struct header {int type, nxt;};
union action {
    struct header h;
    struct {struct header h; int id; struct definition d; addr usr;} mkcomp;
    struct {struct header h; struct constraint con;} mkcon;
    struct {struct header h; int child, parent;} lnknode;
    struct {struct header h; int type, align, sz, off;} custom;
};
struct reducer {
    int max_com, max_con;
    int max_ext, custom;
    void *buf; int sz, cap;
};

/* layout */
struct extension {
    int type;
    int sz, algn;
    addr addr;
};
struct layout {
    int *buffer;
    struct component *comp;
    const struct definition *def;
    const struct node *tree;
    const struct constraint *con;
    const struct extension *ext;
    int comp_cnt, con_cnt, ext_cnt;
    int custom;
};

/* context */
struct context {
    struct layout *layout;
    struct input in;
    int active;
    int origin;
    int hot;
};

/* context */
api void init(struct context*, struct layout*, struct XSurface*);
api void frame(struct context*, struct layout*);
api void update(struct context*, struct layout*, XEvent*);
api void blueprint(struct context*, struct layout*, const int root_id);
api void paint(struct context*,struct layout*);
api void draw(struct context*, struct layout*, const int root);
api void clear(struct context*, struct layout*);

/* layout */
api void layouting(struct layout*);
api int find(const struct layout*, const struct component *root, int x, int y);
api void reorder(struct layout*, struct component*);
api void dispatch(struct context*,struct layout*, struct component*, const XEvent*);
api void commit(FILE*, const struct layout*);
#define lookup(ui,id) ((ui)->comp + (id))
#define apply(l,f,a,p) for(int _=0; _<l->ext_cnt; ++_) if (l->ext[_].type==f) a(l,l->ext+_,p);
#define map(ui,t,e) e=ui->ext; for(int _=0;_<ui->ext_cnt;e = ++_ + ui->ext)if(e->type == t)

/* fold */
api void reducer_begin(struct reducer*, void *mem, int sz);
api void reducer_cons(struct reducer*, union action*, const void *data, int sz, int ualign);
#define mkcomp(r,def,user) reducer_cons(r,&(union action){.mkcomp={.h={.type=ACTION_MKCOMP},.d=def,.usr=user}},0,0,0)
#define mkcon(r,c) reducer_cons(r,&(union action){.mkcon={.h={.type=ACTION_MKCON},.con=c}},0,0,0)
#define lnknode(r,c,p) reducer_cons(r,&(union action){.lnknode={.h={.type=ACTION_LNKNODE},.child=c,.parent=p}},0,0,0)
#define addcustom(r,t,p,s,a) reducer_cons(r,&(union action){.custom={.h={.type=ACTION_CUSTOM},.type=t}},p,s,a)
api void reducer_end(struct reducer*, const struct layout*, int *mem);
api void fold(struct layout **out, const struct reducer*, const struct layout*, void *mem);

/* component */
api void component_build(struct component*, struct rect);
api struct component* component_parent(struct layout*, int id);

/* bindings */
#define slot_con(i,f) do{assert((i)->cnt < cntof((i)->b)); (i)->b[(i)->cnt++] = (f);}while(0)
#define bind_drag_x(d) (struct binding){BINDING_DRAG_X, .dst = d}
#define bind_drag_y(d) (struct binding){BINDING_DRAG_Y, .dst = d}
#define bind_enable(d,v) (struct binding){BINDING_ENABLE, .dst = d, .src = {.f = v}}
#define bind_disable(d,v) (struct binding){BINDING_DISABLE, .dst = d, .src = {.f = v}}
#define bind_proj(sys,lo,val,hi,tar) (struct binding){BINDING_PROJECT, .dst=sys,.src ={.proj={lo,val,hi,tar}}}
#define bind_signal(d,u) (struct binding){BINDING_SIGNAL, .dst = u, .src = {.s = d}}
#define bind_not(d) (struct binding){BINDING_NOT, .dst = d}
#define bind_xchg(d,o,n) (struct binding){BINDING_XCHG,.dst=d,.src={.ex={o,n}}}
#define bind_cmpxchg(d,a,b) (struct binding){BINDING_CMPXCHG,.dst=d,.src={.ex={a,b}}}

#define bind_set(d,v) (struct binding){BINDING_SET, .dst = d, .src = {.i = v}}
#define bind_sete(d,v,a,b) (struct binding){BINDING_SETE, .dst = d, .src = {.cis = {v,a,b}}}
#define bind_setne(d,v,a,b) (struct binding){BINDING_SETNE, .dst = d, .src = {.cis = {v,a,b}}}
#define bind_sets(d,v,a) (struct binding){BINDING_SETS, .dst = d, .src = {.cis = {v,a,0}}}
#define bind_setns(d,v,a) (struct binding){BINDING_SETNS, .dst = d, .src = {.cis = {v,a,0}}}
#define bind_setg(d,v,a,b) (struct binding){BINDING_SETG, .dst = d, .src = {.cis = {v,a,b}}}
#define bind_setge(d,v,a,b) (struct binding){BINDING_SETGE, .dst = d, .src = {.cis = {v,a,b}}}
#define bind_setl(d,v,a,b) (struct binding){BINDING_SETL, .dst = d, .src = {.cis = {v,a,b}}}
#define bind_setle(d,v,a,b) (struct binding){BINDING_SETLE, .dst = d, .src = {.cis = {v,a,b}}}

#endif /* UI_INCLUDED */

#ifndef UI_FILE_ONLY

#include <assert.h>

intern int
ceili(float x)
{
    if (x >= 0) return (int)x;
    int t = (int)x;
    float r = x - (float)t;
    return (r > 0.0f) ? t+1: t;
}
intern 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 struct component*
component_parent(struct layout *ui, int id)
{
    const struct node *n = ui->tree + id;
    if (n->parent != n->id)
        return lookup(ui,n->parent);
    return 0;
}
intern void
adjust(struct context *ctx, struct layout *ui, struct component *c, void *usr)
{
    unused(ctx,ui,usr);
    solve(c, L, R, CX, W, L, W);
    solve(c, T, B, CY, H, T, H);
}
intern void
component_setup(struct component *c, const struct definition *d, XSurface *scrn)
{
    c->flags = d->flags;
    component_build(c, d->sz);
    if (!(d->flags & COMP_HIDDEN))
        c->attr[ACT] = True;
    if (d->flags & COMP_PAINTABLE)
        c->canvas = xsurf_create(scrn->dpy, scrn->root,
            scrn->screen, scrn->font, c->attr[W], c->attr[H]);
    if (d->flags & COMP_IS_MOVABLE_X)
        slot_con(&c->slot.dragged, bind_drag_x(&c->attr[L]));
    if (d->flags & COMP_IS_MOVABLE_Y)
        slot_con(&c->slot.dragged, bind_drag_y(&c->attr[T]));
    if ((d->flags & COMP_IS_MOVABLE))
        slot_con(&c->slot.dragged, bind_signal(adjust, 0));
}
api void
component_build(struct component *c, struct rect r)
{
    c->attr[L] = r.x; c->attr[T] = r.y;
    c->attr[W] = max(r.w,0); c->attr[H] = max(r.h,0);
    c->attr[R] = r.x+r.w; c->attr[B] = r.y+r.h;
    c->attr[CX] = r.x+(r.w/2); c->attr[CY] = r.y+(r.h/2);
}
intern int
cond(const struct function *f, int d, int s)
{
         if (f->eq == CONDITION_TRUE) return 1;
    else if (f->eq == CONDITION_FALSE) return 0;
    else if (f->eq == CONDITION_EQ) return d == s;
    else if (f->eq == CONDITION_NEQ) return d != s;
    else if (f->eq == CONDITION_GR) return d > s;
    else if (f->eq == CONDITION_LS) return d < s;
    else if (f->eq == CONDITION_GRE) return d >= s;
    else if (f->eq == CONDITION_LSE) return d <= s;
    return 0;
}
intern void
eval(const struct function *f, int *d, const int *s)
{
    if (f->eq == CONSTRAINT_CPY) *d = *s;
    else if (f->eq == CONSTRAINT_SET)
        *d = ceili(f->cons.mul * (float)(*s)) + f->cons.off;
    else if (f->eq == CONSTRAINT_MIN)
        *d = min(*d, ceili(f->cons.mul * (float)(*s)) + f->cons.off);
    else if (f->eq == CONSTRAINT_MAX)
        *d = max(*d, ceili(f->cons.mul * (float)(*s)) + f->cons.off);
}
api int
find(const struct layout *ui, const struct component *c, int x, int y)
{
    loop:; int id = c->id;
    int tbl[MAX_NODES_PER_NODE];
    const struct node *n = ui->tree + id;
    for (int i = 0; i < n->cnt; ++i)
        tbl[ui->comp[n->sub[i]].zorder] = n->sub[i];

    x += c->off.x; y += c->off.y;
    for (int i = n->cnt-1; i >= 0; --i) {
        const struct component *s = lookup(ui, tbl[i]);
        if (!s->attr[ACT] || !(s->flags & COMP_INTERACTIVE)) continue;
        if (inbox(x, y, s->attr[L], s->attr[T], s->attr[W], s->attr[H]))
            {c = s; goto loop;}
    }
    return c->id;
}
api void
reorder(struct layout *ui, struct component *c)
{
    const struct node *n = ui->tree + c->id;
    while (n->parent != n->id) {
        struct component *p = component_parent(ui, c->id);
        n = ui->tree + p->id;
        for (int i = 0; i < n->cnt; ++i) {
            struct component *ch = lookup(ui, n->sub[i]);
            if (ch->zorder > c->zorder)
                ch->zorder--;
        }
        c->zorder = max(n->cnt-1, 0);
        c = p;
    }
    return;
}
api void
dispatch(struct context *ctx, struct layout *ui,
    struct component *c, const XEvent *e)
{
    do  if (ui->def[c->id].handle)
        if (ui->def[c->id].handle(ctx,ui,c,e)) break;
    while ((c = component_parent(ui,c->id)));
}
intern void
interact(struct context *ctx, struct layout *ui, struct component *c,
    struct interaction *in, const XEvent *e)
{
    for (int i = 0; i < in->cnt; ++i) {
        struct binding *b = in->b + i;
        int t = b->type;
        if (BINDING_SIGNAL == t)
            b->src.s(ctx,ui, c, b->dst);
        else if (BINDING_SET == t)
            {int *v = (int*)b->dst; *v = b->src.i;}
        else if (BINDING_SETE == t)
            {int *v = (int*)b->dst;
            *v = (*b->src.cis.a == *b->src.cis.b) ? *b->src.cis.v :*v;}
        else if (BINDING_SETNE == t)
            {int *v = (int*)b->dst;
            *v = (*b->src.cis.a != *b->src.cis.b) ? *b->src.cis.v :*v;}
        else if (BINDING_SETS == t)
            {int *v = (int*)b->dst;
            *v = (*b->src.cis.a < 0) ? *b->src.cis.v :*v;}
        else if (BINDING_SETNS == t)
            {int *v = (int*)b->dst;
            *v = (*b->src.cis.a >= 0) ? *b->src.cis.v :*v;}
        else if (BINDING_SETG == t)
            {int *v = (int*)b->dst;
            *v = (*b->src.cis.a > *b->src.cis.b) ? *b->src.cis.v :*v;}
        else if (BINDING_SETGE == t)
            {int *v = (int*)b->dst;
            *v = (*b->src.cis.a >= *b->src.cis.b) ? *b->src.cis.v :*v;}
        else if (BINDING_SETL == t)
            {int *v = (int*)b->dst;
            *v = (*b->src.cis.a < *b->src.cis.b) ? *b->src.cis.v :*v;}
        else if (BINDING_SETLE == t)
            {int *v = (int*)b->dst;
            *v = (*b->src.cis.a <= *b->src.cis.b) ? *b->src.cis.v :*v;}
        else if (BINDING_CMPXCHG == t)
            {int *v = (int*)b->dst;
            *v = (b->src.ex.o == *v)?b->src.ex.n:*v;}
        else if (BINDING_XCHG == t)
            {int *v = (int*)b->dst;
            *v = (b->src.ex.o == *v)?b->src.ex.n:(b->src.ex.n == *v)?b->src.ex.o:*v;}
        else if (BINDING_ENABLE == t)
            {unsigned *v = (unsigned*)b->dst; *v |= b->src.f;}
        else if (BINDING_DISABLE == t)
            {unsigned *v = (unsigned*)b->dst; *v &= ~b->src.f;}
        else if (BINDING_PROJECT == t) {
            int *d = (int*)b->dst;
            float a = (float)(*b->src.proj.val - *b->src.proj.min);
            *d = (int)((a/(float)(*b->src.proj.max)) * (float)(*b->src.proj.tar));
        } else if (e->type == MotionNotify) {
            int *d = (int*)b->dst;
            *d += (t == BINDING_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 = lookup(ui, i);
        xsurf_resize(c->canvas, c->attr[W], c->attr[H]);
        if (!(c->flags & COMP_PAINTABLE) || (c->flags & COMP_LAYER))
            continue;
        if (ui->def[i].paint)
            ui->def[i].paint(ctx,ui,c);
    }
}
api void
draw(struct context *ctx, struct layout *ui, const int root_id)
{
    unsigned head = 0;
    int *stk = ui->buffer;
    int tbl[MAX_NODES_PER_NODE];
    struct component *r = lookup(ui,root_id);

    stk[head++] = root_id;
    while (head > 0) {
        struct component *c = lookup(ui, stk[--head]);
        if (c->canvas && c->attr[ACT] && c->id != root_id) {
            if ((c->flags & COMP_LAYER) && (c->flags & COMP_PAINTABLE)) {
                ui->buffer = stk + head;
                if (ui->def[c->id].paint)
                    ui->def[c->id].paint(ctx,ui,c);
            }
            xsurf_blit(r->canvas, c->canvas, 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 & COMP_LAYER) && (c->id != root_id))) continue;
        const struct node *n = ui->tree + c->id;
        for (int i = 0; i < n->cnt; ++i)
            tbl[ui->comp[n->sub[i]].zorder] = 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, const int root_id)
{
    unsigned head = 0, tail = 1;
    int *que = ui->buffer;
    que[tail] = root_id;
    while (head < tail) {
        const struct node *n;
        n = ui->tree + que[++head];
        for (int i = 0; i < n->cnt; ++i)
            que[++tail] = n->sub[i];
    }
    for (unsigned i = tail; i > 0; i--) {
        const struct definition *d;
        struct component *c;
        d = ui->def + que[i];
        c = lookup(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 = lookup(ui, co->dst.comp);
        cs = lookup(ui, co->src.comp);
        d = lookup(ui, con->self.dst.comp);
        s = lookup(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: {
        struct component *a;
        a = lookup(ui, ctx->active);
        dispatch(ctx, ui, a, e);
    } break;
    case Expose:
    case ConfigureNotify: {
        /* resize event  */
        struct component *c = lookup(ui,COMPONENT_ROOT);
        int w = (e->type == Expose) ? e->xexpose.width: e->xconfigure.width;
        int h = (e->type == Expose) ? e->xexpose.height: e->xconfigure.height;
        component_build(c, rect(0, 0, w, h));
        xsurf_resize(c->canvas, w, h);
        layouting(ui);
        for (int i = 0; i < ui->comp_cnt; ++i)
            dispatch(ctx, ui, lookup(ui,i), e);
    } break;
    case MotionNotify: {
        int mx = e->xmotion.x, my = e->xmotion.y;
        ctx->in.mouse.delta.x = mx - ctx->in.mouse.pos.x;
        ctx->in.mouse.delta.y = my - ctx->in.mouse.pos.y;
        ctx->in.mouse.pos.x = mx, ctx->in.mouse.pos.y = my;

        /* enter and leave event */
        int last_hot = ctx->hot;
        ctx->hot = find(ui,ui->comp,mx,my);
        if (last_hot != ctx->hot) {
            struct component *prev = lookup(ui,last_hot);
            struct component *cur = lookup(ui,ctx->hot);

            prev->hovered = False;
            e->xmotion.type = Leave;
            interact(ctx, ui, prev, &prev->slot.leave, e);
            dispatch(ctx, ui, prev, e);

            cur->hovered = True;
            e->xmotion.type = Enter;
            interact(ctx, ui, cur, &cur->slot.enter, e);
            dispatch(ctx, ui, cur, e);
        }
        /* cursor movement event */
        e->xmotion.type = MotionNotify;
        for (int i = 0; i < ui->comp_cnt; ++i) {
            struct component *c = lookup(ui, i);
            if (ui->def[c->id].handle)
                ui->def[c->id].handle(ctx,ui,c,e);
        }
        /* dragging event */
        if (ctx->active != ctx->origin) break;
        struct component *a = lookup(ui, ctx->active);
        a->dragged = True;
        interact(ctx,ui, a, &a->slot.dragged, e);
        e->xmotion.type = Drag;
        dispatch(ctx,ui,a,e);
        e->xmotion.type = MotionNotify;
    } break;
    case ButtonRelease:
    case ButtonPress: {
        int down = e->type == ButtonPress;
        if (down) ui->comp[ctx->hot].pressed = True;
        else ui->comp[ctx->origin].released = True;
        ctx->active = (down) ? ctx->hot: ctx->active;
        ctx->origin = (down) ? ctx->hot: COMPONENT_ROOT;

        struct component *a = lookup(ui,ctx->active);
        if (e->xbutton.button == Button1) {
            /* left button event */
            if (down) reorder(ui, a);
            if (down) interact(ctx,ui, a, &a->slot.left, e);
        } else if (e->xbutton.button == Button3){
            /* right button event */
            if (down) reorder(ui, a);
            if (down) interact(ctx,ui, a, &a->slot.right,e);
        }
        dispatch(ctx,ui, a, e);
    } break;}
}
api void
reducer_begin(struct reducer *r, void *mem, int sz)
{
    r->buf = mem; r->cap = sz;
    r->max_com = r->max_con = 0;
    r->max_ext = r->custom =  r->sz = 0;
}
api void
reducer_cons(struct reducer *r, union action *a,
    const void *dat, int sz, int ualign)
{
    storage const int align = alignof(union action);
    storage const int asz = szof(union action);
    assert((r->sz + asz + sz + ualign + align) < r->cap);

    char *end = (char*)r->buf + r->sz;
    char *nxt = align(end + r->sz + asz + sz + ualign, align);
    char *usr = align(end + asz, ualign);
    a->h.nxt = (int)(nxt - end);

    if (ACTION_CUSTOM == a->h.type) {
        r->custom = sz + a->custom.align;
        a->custom.off = (int)(usr - end);
        a->custom.align = ualign;
        a->custom.sz = sz;
        r->max_ext++;
    } else if (ACTION_MKCOMP == a->h.type) {
        a->mkcomp.id = r->max_com++;
    } else if (ACTION_MKCON == a->h.type)
        r->max_con++;

    copy(end, a, asz);
    copy(usr, dat, sz);
    r->sz += a->h.nxt;
}
api void
reducer_end(struct reducer *r, const struct layout *ui, int *mem)
{
    r->max_ext += ui->ext_cnt;
    r->max_com += ui->comp_cnt; r->max_con += ui->con_cnt;
    *mem = r->max_com * szof(struct component);
    *mem += r->max_com * szof(struct node);
    *mem += r->max_com * szof(struct definition);
    *mem += (r->max_com+1) * szof(int);
    *mem += r->max_con * szof(struct constraint);
    *mem += r->max_ext * szof(struct extension);
    *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->custom + r->custom;
}
api void
fold(struct layout **out, const struct reducer *r,
    const struct layout *ui, void *mem)
{
    struct node *tree;
    struct definition *def;
    struct constraint *con;
    struct layout *res;
    struct extension *ext;
    void *custom;

    /* setup layout in memory block */
    res = align(mem, alignof(struct layout));
    res->buffer = align(res+1, alignof(int));
    res->comp = align(res->buffer+r->max_com+1, 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));
    res->custom = ui->custom + r->custom;
    res->comp_cnt = r->max_com;
    res->con_cnt = ui->con_cnt;
    res->ext_cnt = ui->ext_cnt;
    custom = ext + r->max_ext;
    *out = res;

    /* 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; i < ui->ext_cnt; ++i) {
        ext[i] = ui->ext[i];
        ext[i].addr = (addr)align(custom, ext[i].algn);
        copy((void*)ext[i].addr, (void*)ui->ext[i].addr, ext[i].sz);
        custom = (char*)ext[i].addr + ext[i].sz;
    }
    /* add new state */
    char *end = (char*)r->buf + r->sz;
    for (union action *a = r->buf; (char*)a < end; a = (void*)((char*)a + a->h.nxt)) {
        if (ACTION_MKCOMP == a->h.type) {
            const int id = a->mkcomp.id + ui->comp_cnt;
            ui->comp[id].id = tree[id].id = id;
            ui->comp[id].usr = a->mkcomp.usr;
            def[id] = a->mkcomp.d;
            component_setup(ui->comp + id, def + id, ui->comp[0].canvas);
        } else if (ACTION_MKCON == a->h.type) {
            con[res->con_cnt++] = a->mkcon.con;
        } else if (ACTION_LNKNODE == a->h.type) {
            struct node *n = tree + a->lnknode.parent;
            n->sub[n->cnt] = a->lnknode.child;
            res->comp[a->lnknode.child].zorder = n->cnt++;
        } else if (ACTION_CUSTOM == a->h.type) {
            const void *obj = (const void*)((const char*)a + a->custom.off);
            char *dst = align(custom, a->custom.align);
            copy(dst, obj, a->custom.sz);
            custom = dst + a->custom.sz;

            ext[res->ext_cnt].sz = a->custom.sz;
            ext[res->ext_cnt].type = a->custom.type;
            ext[res->ext_cnt].algn = a->custom.align;
            ext[res->ext_cnt++].addr = (addr)dst;
        }
    }
}
api void
commit(FILE *fp, const struct layout *ui)
{
    const char *attrs[] = {"0","ACT","L","T","R","B","W","H","CX","CY"};
    const char *cons[] = {"CONSTRAINT_CPY", "CONSTRAINT_SET", "CONSTRAINT_MIN", "CONSTRAINT_MAX"};
    fputs("#ifndef UI_CORE_DATA_H_\n", fp);
    fputs("#define UI_CORE_DATA_H_\n\n", fp);
    fputs("global const struct constraint g_constraint_table[] = {\n", fp);
    for (int i = 0; i < ui->con_cnt; ++i) {
        const struct constraint *c = ui->con + i;
        fprintf(fp, "    {{.eq = %s, .dst = {%d,%s}, .src = {%d,%s}, .cons = {.mul = %.2ff, .off = %d}}, .anch = %s},\n",
            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]);
    } fputs("};\n", fp);
    fputs("global const struct definition g_definition_table[] = {\n", fp);
    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 & COMP_HIDDEN)
            {copy(buf+n, "|COMP_HIDDEN", 12); n += 12;}
        if (d->flags & COMP_INTERACTIVE)
            {copy(buf+n, "|COMP_INTERACTABLE", 18); n += 18;}
        if (d->flags & COMP_PAINTABLE)
            {copy(buf+n, "|COMP_PAINTABLE", 15); n += 15;}
        if (d->flags & COMP_LAYER)
            {copy(buf+n, "|COMP_LAYER", 11); n += 11;}
        if (d->flags & COMP_IS_MOVABLE_X)
            {copy(buf+n, "|COMP_MOVABLE_X", 15); n += 15;}
        if (d->flags & COMP_IS_MOVABLE_Y)
            {copy(buf+n, "|COMP_MOVABLE_Y", 15); n += 15;}
        buf[n] = 0;
        fprintf(fp, "    {TBL(%s,%s,%s),.sz = {%d,%d,%d,%d},.flags = %s},\n",
            d->h, d->p, d->b, d->sz.x, d->sz.y, d->sz.w, d->sz.h, buf);
    } fputs("};\n", fp);
    fputs("global const struct node g_tree_table[] = {\n", fp);
    for (int i = 0; i < ui->comp_cnt; ++i) {
        const struct node *n = ui->tree + i;
        fprintf(fp, "    {.id = %d, .parent = %d, .cnt = %d, .sub = {", n->id, n->parent, n->cnt);
        for (int j = 0; j < n->cnt; ++j)
            fprintf(fp, "%d,", n->sub[j]);
        if (!n->cnt) fputs("0", fp);
        fputs("}},\n", fp);
    } fputs("};\n", fp);
    fputs("#endif\n\n", fp);
}
api void
init(struct context *ctx, struct layout *layout, struct XSurface *scrn)
{
    struct layout *ui = ctx->layout = layout;
    ui->comp[COMPONENT_ROOT].canvas = scrn;
    for (int i = 0; i < ui->comp_cnt; ++i) {
        const struct node *n = ui->tree + i;
        struct component *c = ui->comp + i; c->id = i;
        component_setup(c, ui->def + i, scrn);
        for (int j = 0; j < n->cnt; ++j)
            ui->comp[n->sub[j]].zorder = j;
    }
}
api void
frame(struct context *ctx, struct layout *ui)
{
    ctx->in.mouse.left.pressed = ctx->in.mouse.left.released = 0;
    ctx->in.mouse.right.pressed = ctx->in.mouse.right.released = 0;
    ctx->in.mouse.delta.x = ctx->in.mouse.delta.y = 0;
    ctx->in.mouse.wheel = ctx->in.txt_len = 0;
    for (int i = 0; i < KEY_MAX; ++i)
        ctx->in.keys[i].pressed = ctx->in.keys[i].released = 0;
    for (int i = 0; i < ui->comp_cnt; ++i)
        ui->comp[i].pressed = ui->comp[i].dragged = ui->comp[i].released = 0;
}
api void
clear(struct context *ctx, struct layout *ui)
{
    unused(ctx);
    for (int i = COMPONENT_ROOT+1; i < ui->comp_cnt; ++i) {
        if (ui->comp[i].canvas)
            xsurf_del(ui->comp[i].canvas);
    }
}
#endif /* UI_FILE_ONLY */

Next up is the our generated data tables. It only consists out of a list of component definitions constraints to be executed at runtime to place components on screen, a component hierarchy and custom extensions. The actual data has no knowledge of specific widgets or styling at all. The UI builder or core user can completely decide styling, padding, spacing, configurations and runtime will only executing constraints.

enum component_id {
    SCREEN = 0,
    /* sidebar */
    SIDEBAR,
    SIDEBAR_BAR,
    SIDEBAR_PANEL,
    SIDEBAR_SCALER,
    SIDEBAR_SCROLLBAR,
    SIDEBAR_SCROLLCUR,
    SIDEBAR_MAIN,
    /* widgets */
    TEST,
    BUTTON_OK,
    BUTTON_CANCEL,
    /* ... */
    COMPONENT_COUNT,
    COMPONENT_BUFFER,
};

#ifndef UI_CORE_DATA_H_
#define UI_CORE_DATA_H_

global const struct constraint g_constraint_table[] = {
    {{.eq = CONSTRAINT_SET, .dst = {1,H}, .src = {0,H}, .cons = {.mul = 0.80f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {1,W}, .src = {0,W}, .cons = {.mul = 0.50f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_CPY, .dst = {1,CY}, .src = {0,CY}, .cons = {.mul = 1.00f, .off = 0}}, .anch = H},
    {{.eq = CONSTRAINT_SET, .dst = {1,L}, .src = {0,L}, .cons = {.mul = 1.00f, .off = 0}}, .anch = W},
    {{.eq = CONSTRAINT_CPY, .dst = {2,T}, .src = {1,T}, .cons = {.mul = 1.00f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {2,W}, .src = {1,W}, .cons = {.mul = 0.00f, .off = 12}}, .anch = 0},
    {{.eq = CONSTRAINT_CPY, .dst = {2,B}, .src = {1,B}, .cons = {.mul = 1.00f, .off = 0}}, .anch = H},
    {{.eq = CONSTRAINT_CPY, .dst = {2,L}, .src = {1,L}, .cons = {.mul = 1.00f, .off = 0}}, .anch = W},
    {{.eq = CONSTRAINT_SET, .dst = {3,T}, .src = {2,T}, .cons = {.mul = 1.00f, .off = 16}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {3,B}, .src = {2,B}, .cons = {.mul = 1.00f, .off = -16}}, .anch = T},
    {{.eq = CONSTRAINT_SET, .dst = {3,L}, .src = {2,R}, .cons = {.mul = 1.00f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {4,W}, .src = {1,W}, .cons = {.mul = 0.00f, .off = 8}}, .anch = 0},
    {{.eq = CONSTRAINT_CPY, .dst = {4,T}, .src = {3,T}, .cons = {.mul = 1.00f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_CPY, .dst = {4,B}, .src = {3,B}, .cons = {.mul = 0.50f, .off = 0}}, .anch = T},
    {{.eq = CONSTRAINT_MAX, .dst = {4,R}, .src = {1,W}, .cons = {.mul = 0.50f, .off = 0}}, .anch = W},
    {{.eq = CONSTRAINT_MIN, .dst = {4,R}, .src = {1,W}, .cons = {.mul = 0.90f, .off = 0}}, .anch = W},
    {{.eq = CONSTRAINT_CPY, .dst = {3,R}, .src = {4,L}, .cons = {.mul = 1.00f, .off = 0}}, .anch = L},
    {{.eq = CONSTRAINT_CPY, .dst = {1,R}, .src = {4,R}, .cons = {.mul = 1.00f, .off = 0}}, .anch = L},
    {{.eq = CONSTRAINT_SET, .dst = {1,R}, .src = {4,R}, .cons = {.mul = 1.00f, .off = 5}}, .anch = L},
    {{.eq = CONSTRAINT_SET, .dst = {5,T}, .src = {3,T}, .cons = {.mul = 1.00f, .off = 5}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {5,B}, .src = {3,B}, .cons = {.mul = 1.00f, .off = -5}}, .anch = T},
    {{.eq = CONSTRAINT_SET, .dst = {5,W}, .src = {3,W}, .cons = {.mul = 0.00f, .off = 8}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {5,R}, .src = {3,R}, .cons = {.mul = 1.00f, .off = -2}}, .anch = W},
    {{.eq = CONSTRAINT_CPY, .dst = {6,L}, .src = {5,L}, .cons = {.mul = 1.00f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_CPY, .dst = {6,R}, .src = {5,R}, .cons = {.mul = 1.00f, .off = 0}}, .anch = L},
    {{.eq = CONSTRAINT_MAX, .dst = {6,T}, .src = {5,T}, .cons = {.mul = 1.00f, .off = 0}}, .anch = W},
    {{.eq = CONSTRAINT_MIN, .dst = {6,B}, .src = {5,B}, .cons = {.mul = 1.00f, .off = 0}}, .anch = W},
    {{.eq = CONSTRAINT_SET, .dst = {7,T}, .src = {3,T}, .cons = {.mul = 1.00f, .off = 5}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {7,L}, .src = {3,L}, .cons = {.mul = 1.00f, .off = 5}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {7,B}, .src = {3,B}, .cons = {.mul = 1.00f, .off = -5}}, .anch = T},
    {{.eq = CONSTRAINT_SET, .dst = {7,R}, .src = {5,L}, .cons = {.mul = 1.00f, .off = -5}}, .anch = L},
    {{.eq = CONSTRAINT_CPY, .dst = {8,T}, .src = {7,T}, .cons = {.mul = 1.00f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_CPY, .dst = {8,L}, .src = {7,L}, .cons = {.mul = 1.00f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {8,B}, .src = {7,B}, .cons = {.mul = 1.00f, .off = 50}}, .anch = T},
    {{.eq = CONSTRAINT_CPY, .dst = {8,R}, .src = {7,R}, .cons = {.mul = 1.00f, .off = 0}}, .anch = L},
    {{.eq = CONSTRAINT_SET, .dst = {9,T}, .src = {8,B}, .cons = {.mul = 1.00f, .off = 5}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {9,L}, .src = {7,L}, .cons = {.mul = 1.00f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {9,W}, .src = {7,W}, .cons = {.mul = 0.40f, .off = 0}}, .anch = L},
    {{.eq = CONSTRAINT_SET, .dst = {9,H}, .src = {7,H}, .cons = {.mul = 0.00f, .off = 30}}, .anch = T},
    {{.eq = CONSTRAINT_CPY, .dst = {10,T}, .src = {9,T}, .cons = {.mul = 1.00f, .off = 0}}, .anch = 0},
    {{.eq = CONSTRAINT_SET, .dst = {10,L}, .src = {9,R}, .cons = {.mul = 1.00f, .off = 5}}, .anch = 0},
    {{.eq = CONSTRAINT_CPY, .dst = {10,W}, .src = {9,W}, .cons = {.mul = 0.40f, .off = 0}}, .anch = L},
    {{.eq = CONSTRAINT_CPY, .dst = {10,H}, .src = {9,H}, .cons = {.mul = 0.00f, .off = 0}}, .anch = T},
};
global const struct definition g_definition_table[] = {
    {TBL(0,0,0),.sz = {0,0,0,0},.flags = 0|COMP_INTERACTABLE},
    {TBL(0,0,0),.sz = {0,0,0,0},.flags = 0|COMP_INTERACTABLE},
    {TBL(0,sidebar_control_paint,0),.sz = {0,0,0,0},.flags = 0|COMP_PAINTABLE|COMP_INTERACTABLE},
    {TBL(0,window_paint,0),.sz = {0,0,0,0},.flags = 0|COMP_INTERACTABLE|COMP_PAINTABLE},
    {TBL(0,sidebar_paint,0),.sz = {0,0,0,0},.flags = 0|COMP_INTERACTABLE|COMP_PAINTABLE|COMP_MOVABLE_X},
    {TBL(0,scroll_paint,0),.sz = {0,0,0,0},.flags = 0|COMP_INTERACTABLE|COMP_PAINTABLE},
    {TBL(0,scroll_cursor_paint,0),.sz = {0,0,0,0},.flags = 0|COMP_INTERACTABLE|COMP_PAINTABLE|COMP_MOVABLE_Y},
    {TBL(scroll_region_dispatch,scroll_panel_paint,0),.sz = {0,0,0,0},.flags = 0|COMP_INTERACTABLE|COMP_PAINTABLE|COMP_LAYER},
    {TBL(0,test_paint,0),.sz = {0,0,0,0},.flags = 0|COMP_PAINTABLE},
    {TBL(0,button_paint,0),.sz = {0,0,0,0},.flags = 0|COMP_INTERACTABLE|COMP_PAINTABLE},
    {TBL(0,button_paint,0),.sz = {0,0,0,0},.flags = 0|COMP_INTERACTABLE|COMP_PAINTABLE},
};
global const struct node g_tree_table[] = {
    {.id = 0, .parent = 0, .cnt = 1, .sub = {1,}},
    {.id = 1, .parent = 0, .cnt = 3, .sub = {2,4,3,}},
    {.id = 2, .parent = 1, .cnt = 0, .sub = {0}},
    {.id = 3, .parent = 1, .cnt = 2, .sub = {5,7,}},
    {.id = 4, .parent = 1, .cnt = 0, .sub = {0}},
    {.id = 5, .parent = 3, .cnt = 1, .sub = {6,}},
    {.id = 6, .parent = 5, .cnt = 0, .sub = {0}},
    {.id = 7, .parent = 3, .cnt = 3, .sub = {9,10,8,}},
    {.id = 8, .parent = 7, .cnt = 0, .sub = {0}},
    {.id = 9, .parent = 7, .cnt = 0, .sub = {0}},
    {.id = 10, .parent = 7, .cnt = 0, .sub = {0}},
};
#endif

#ifndef UI_EXTENSION_DATA_H_
#define UI_EXTENSION_DATA_H_
global const struct scroll_region g_scroll_table[] = {
    {.comp = 7, .cnt = 1, .s = {{.ref = 5, .ctr = 6, .flags = SCROLL_Y}}},
};
global struct sidebar g_sidebar_table[] = {
    {.id = 1, .scaler = 4, .panel = 3, .bar = 2},
};
global const struct extension g_extension_table[] = {
    {.type = 0, .sz = szof(g_scroll_table[0]), .algn = alignof(struct scroll_region), .addr = (addr)&g_scroll_table[0]},
    {.type = 1, .sz = szof(g_sidebar_table[0]), .algn = alignof(struct sidebar), .addr = (addr)&g_sidebar_table[0]},
};
#endif

One very nice property of using tables is that it is very easy to extend. This next code block shows how to extend the core with scrolling functionality. Interesting especially is that no use of clipping rectangles are required. Instead everything is drawn into a surfaces (this implementation uses X11 but could just as well be implemented in win32 or hardware with framebuffers).

#define MAX_SCROLLS_PER_NODE 2
enum scroll_type {
    SCROLL_X = 0x01,
    SCROLL_Y = 0x02,
    SCROLL_XY = SCROLL_X|SCROLL_Y
};
struct scroll_region {
    int comp, cnt;
    struct scroll {
        int ref, ctr;
        unsigned flags;
    } s[MAX_SCROLLS_PER_NODE];
};
intern void scroll_region_validate(struct component*, const struct layout*);
intern void scroll_paint(struct context *ui, struct layout*l,struct component *c)
    {unused(ui,l); xsurf_fill_rect(c->canvas,0,0,c->attr[W],c->attr[H],0,rgb(230,230,230));}
intern void scroll_cursor_paint(struct context *ui, struct layout*l,struct component *c)
    {unused(ui,l); xsurf_fill_rect(c->canvas,0,0,c->attr[W], c->attr[H],0,rgb(205,205,205));}
intern void scroll_region_paint(struct context *ui, struct layout*l,struct component *c)
    {scroll_region_validate(c, l); draw(ui, ui->layout,c->id);}
intern void scroll_panel_paint(struct context *ui, struct layout*l,struct component *c)
    {xsurf_fill_rect(c->canvas,0,0,c->attr[W],c->attr[H],0,rgb(255,255,255)); scroll_region_paint(ui,l,c);}

intern struct v2
scroll_region_measure(const struct layout *ui,
    const struct component *c)
{
    struct v2 res = {c->attr[W], c->attr[H]};
    const struct node *n = ui->tree + c->id;
    for (int i = 0; i < n->cnt; ++i) {
        const struct component *s = ui->comp + n->sub[i];
        res.x = max(res.x, s->attr[R] - c->attr[L]);
        res.y = max(res.y, s->attr[B] - c->attr[T]);
    }
    return res;
}
intern void
validate(struct component *ctr, const struct component *ref,
    const struct component *c, int off, int total,
    int lo, int mid, int hi, int len)
{
    ctr->attr[len] = (int)(((float)c->attr[len] / (float)max(total,1)) * (float)ref->attr[len]);
    ctr->attr[lo] = ref->attr[lo] + (int)((float)off / (float)max(total,1) * (float)ref->attr[len]);
    ctr->attr[hi] = ctr->attr[lo] + ctr->attr[len];
    ctr->attr[mid] = ctr->attr[lo] + (ctr->attr[len]/2);
}
intern void
scroll_region_validate(struct component *c, const struct layout *ui)
{
    c->total = scroll_region_measure(ui,c);
    c->off.x = clamp(0, c->off.x, c->total.x - c->attr[W]);
    c->off.y = clamp(0, c->off.y, c->total.y - c->attr[H]);

    const struct scroll_region *self = (void*)c->usr;
    for (int i = 0; i < self->cnt; ++i) {
        /* validate scrollbar cursor component */
        const struct scroll *s = self->s + i;
        if (s->flags & SCROLL_X)
            validate(ui->comp+s->ctr,ui->comp+s->ref,c,c->off.x,c->total.x,L,CX,R,W);
        if (s->flags & SCROLL_Y)
            validate(ui->comp+s->ctr,ui->comp+s->ref,c,c->off.y,c->total.y,T,CY,B,H);

        int act; /* scrollbar hiding */
        if (s->flags == SCROLL_X) act = c->attr[W] < c->total.x;
        else if (s->flags == SCROLL_Y) act = c->attr[H] < c->total.y;
        else act = (c->attr[H] < c->total.y) || (c->attr[W] < c->total.x);
        ui->comp[s->ref].attr[ACT] = ui->comp[s->ctr].attr[ACT] = act;
    }
}
intern int
scroll_region_dispatch(struct context *ctx, struct layout *ui,
    struct component *c, const XEvent *e)
{
    unused(ctx);
    switch (e->type) {
    default: return 0;
    case ButtonPress: {
        /* mouse scrolling */
        if (e->xbutton.button == Button5)
            c->off.y += (int)((float)c->attr[H] * 0.1f);
        else if (e->xbutton.button == Button4)
            c->off.y -= (int)((float)c->attr[H] * 0.1f);
    } break;
    case KeyPress: {
        /* shortcuts */
        #define KEY_HOME        (110)
        #define KEY_END         (115)
        #define KEY_PAGE_UP     (112)
        #define KEY_PAGE_DOWN   (117)
        unsigned key = e->xkey.keycode;
        if (key == KEY_PAGE_UP) c->off.y -= c->attr[H];
        else if (key == KEY_PAGE_DOWN) c->off.y += c->attr[H];
        else if (key == KEY_HOME) c->off.y = 0;
        else if (key == KEY_END) c->off.y = c->total.y - c->attr[H];
    } break;}
    scroll_region_validate(c, ui);
    return 0;
}
intern void
lnkscrl(struct layout *ui, const struct scroll_region *sr)
{
    struct component *t = ui->comp + sr->comp;
    t->usr = (unsigned long long)sr;
    for (int j = 0; j < sr->cnt; ++j) {
        struct component *r, *cr;
        const struct scroll *s = sr->s + j;
        cr->slot.dragged.cnt = 0;
        t = ui->comp + sr->comp, r = ui->comp + s->ref; cr = ui->comp + s->ctr;
        if (s->flags & SCROLL_X)
            slot_con(&cr->slot.dragged, bind_proj(&t->off.x,&r->attr[L],&cr->attr[L],&r->attr[W],&t->total.x));
        if (s->flags & SCROLL_Y)
            slot_con(&cr->slot.dragged, bind_proj(&t->off.y,&r->attr[T],&cr->attr[T],&r->attr[H],&t->total.y));
    }
}
intern void
reposcrl(struct layout *ui, const struct extension *e, void *usr)
{
    unused(ui); FILE *fp = usr;
    const struct scroll_region *r = (const void*)e->addr;
    fprintf(fp, "    {.comp = %d, .cnt = %d, .s = {{", r->comp, r->cnt);
    for (int i = 0; i < r->cnt; ++i) {
        fprintf(fp, ".ref = %d, .ctr = %d, .flags = ", r->s[i].ref, r->s[i].ctr);
        if (r->s[i].flags == SCROLL_XY) fputs("SCROLL_XY", fp);
        else if (r->s[i].flags == SCROLL_X) fputs("SCROLL_X", fp);
        else if (r->s[i].flags == SCROLL_Y) fputs("SCROLL_Y", fp);
    }
    if (!r->cnt) fputs("0", fp);
    fputs("}},\n", fp);
}

This perticular scroll region only binds together a number of components and does not have any internal state itself to keep track of. The next example is litte bit different and shows how more complex extensions can be achieved. The complexity hereby does not come from any data source but instead from widget specific state. For this example I choose a sidebar. Each sidebar has state with either open, closed or pinned. The behavior implementation is quite straight forward:

struct sidebar {
    int id, scaler;
    int panel, bar;
    #define SIDEBAR_STATE_OPEN      0
    #define SIDEBAR_STATE_CLOSED    1
    #define SIDEBAR_STATE_FIXED     2
    int state;
};
intern void sidebar_paint(struct context *ctx, struct layout*l,struct component *c)
    {xsurf_fill_rect(c->canvas,0,0,c->attr[W],c->attr[H],0,rgb(9,101,176)); unused(ctx,l);}

intern void
sidebar_control_paint(struct context *ui, struct layout *l, struct component *c)
{
    struct v2 p[4] = {v2(0,0), v2(c->attr[W],16), v2(c->attr[W], c->attr[H]-16), v2(0, c->attr[H])};
    xsurf_fill_rect(c->canvas,0,0,c->attr[W],c->attr[H],0,rgb(255,255,255));
    xsurf_fill_polygon(c->canvas,p,4,rgb(9,101,176));
    unused(ui,l);
}
intern void
lnksb(struct layout *ui, struct sidebar *s)
{
    storage const int false = 0, true = 1;
    storage const int sclosed = SIDEBAR_STATE_CLOSED;

    struct component *sc = ui->comp + s->id;
    struct component *ss = ui->comp + s->scaler;
    struct component *sp = ui->comp + s->panel;
    struct component *sb = ui->comp + s->bar;
    sb->usr = sc->usr = ss->usr = sp->usr = (unsigned long long)s;
    sc->slot.leave.cnt = sb->slot.enter.cnt = sb->slot.left_pressed.cnt = 0;

    /* open */
    slot_con(&sb->slot.enter, bind_cmpxchg(&s->state, SIDEBAR_STATE_CLOSED, SIDEBAR_STATE_OPEN));
    slot_con(&sb->slot.enter, bind_setne(&ss->attr[ACT], &true, &s->state, &sclosed));
    slot_con(&sb->slot.enter, bind_setne(&sp->attr[ACT], &true, &s->state, &sclosed));

    /* close */
    slot_con(&sc->slot.leave, bind_cmpxchg(&s->state, SIDEBAR_STATE_OPEN, SIDEBAR_STATE_CLOSED));
    slot_con(&sc->slot.leave, bind_sete(&ss->attr[ACT], &false, &s->state, &sclosed));
    slot_con(&sc->slot.leave, bind_sete(&sp->attr[ACT], &false, &s->state, &sclosed));

    /* pin/unpin */
    slot_con(&sb->slot.left_pressed, bind_xchg(&s->state, SIDEBAR_STATE_OPEN, SIDEBAR_STATE_FIXED));
    slot_con(&sb->slot.left_pressed, bind_set(&ss->attr[ACT], True));
    slot_con(&sb->slot.left_pressed, bind_set(&sp->attr[ACT], True));

    ss->attr[ACT] = sp->attr[ACT] = False;
    s->state = SIDEBAR_STATE_CLOSED;
}
intern void
reposb(struct layout *ui, const struct extension *e, void *usr)
{
    unused(ui); FILE *fp = usr;
    struct sidebar *s = (void*)e->addr;
    fprintf(fp, "    {.id = %d, .scaler = %d, .panel = %d, .bar = %d},\n",
        s->id, s->scaler, s->panel, s->bar);
}

To write extensions data to file compile time tables you call each extensions reposit function. While after each fold and init each extensions link function has to be called.

api void
lnk(struct layout *ui)
{
    for (int i=0; i < ui->ext_cnt; ++i) {
        const struct extension *ext = ui->ext + i;
        if (WIDGET_SCROLL == ext->type)
            lnkscrl(ui, (void*)ext->addr);
        else if (WIDGET_SIDEBAR == ext->type)
            lnksb(ui, (void*)ext->addr);
    }
}
api void
resposit(FILE *fp, struct layout *ui)
{
    const struct extension *e = 0;
    unsigned cnt[WIDGET_COUNT] = {0};
    storage const char *tbl[WIDGET_COUNT] = {
        "g_scroll_table",
        "g_sidebar_table",
    };
    storage const char *typ[WIDGET_COUNT] = {
        "struct scroll_region",
        "struct sidebar"
    };
    commit(fp, ui);
    fputs("#ifndef UI_EXTENSION_DATA_H_\n",fp);
    fputs("#define UI_EXTENSION_DATA_H_\n",fp);
    fprintf(fp, "global const %s %s[] = {\n", typ[WIDGET_SCROLL], tbl[WIDGET_SCROLL]);
    apply(ui, WIDGET_SCROLL, reposcrl, fp);
    fputs("};\n",fp);
    fprintf(fp, "global %s %s[] = {\n",typ[WIDGET_SCROLL], tbl[WIDGET_SIDEBAR]);
    apply(ui, WIDGET_SIDEBAR, reposb, fp);
    fputs("};\n",fp);
    fputs("global const struct extension g_extension_table[] = {\n", fp);
    map(ui, e->type, e) {
        fprintf(fp, "    {.type = %d, .sz = szof(%s[%u]), .algn = alignof(%s), .addr = (addr)&%s[%u]},\n",
            e->type, tbl[e->type], cnt[e->type], typ[e->type], tbl[e->type], cnt[e->type]);
        cnt[e->type]++;
    }
    fputs("};\n",fp);
    fputs("#endif\n\n",fp);
}

Finally I want to show how it is possible to combine immediate mode UI with compile time table UI. For this particular example I implemented an abstracted button widget but you could implement any other widget as well. I skipped writing an extra example for pure immediate mode since it is basically 100% the same as in any immediate mode library in existence.

#define button(ui, i, name, ...) struct button name = {__VA_ARGS__}; do_button(ui, i, &name)
struct button {
    const char *txt;
    unsigned pressed:1;
    unsigned released:1;
    unsigned hovered:1;
};
hook void
button_paint(struct context *ctx, struct layout *ui, struct component *c)
{
    struct color bg = (c->pressed) ? rgb(200,200,200): (c->hovered) ? rgb(9,101,176): rgb(240,240,240);
    xsurf_fill_rect(c->canvas, 0, 0, c->attr[W], c->attr[H], 0, bg);
    xsurf_stroke_rect(c->canvas, 0, 0, c->attr[W]-1, c->attr[H]-1, 0, 1, rgb(9,101,176));
}
static void
do_button(struct context *ctx, int id, struct button *b)
{
    const struct layout *ui = ctx->layout;
    const struct component *c = ui->comp + id;
    const int fh = c->canvas->font->height;
    int tw = xfont_get_text_width(c->canvas->font, fh, b->txt, (int)strlen(b->txt));
    b->pressed = c->pressed; b->released = c->released; b->hovered = c->hovered;

    struct rect r;
    r.w = c->attr[W]-4; r.h = c->attr[H]-4;
    r.x = (r.w/2) - (tw/2); r.y = (r.h/2) - (fh/2);
    r.w = min(tw, r.w); r.h = min(fh, r.h);

    struct color fg = (b->pressed) ? rgb(9,101,176): (b->hovered) ? rgb(240,240,240): rgb(9,101,176);
    struct color bg = (b->pressed) ? rgb(200,200,200): (b->hovered) ? rgb(9,101,176): rgb(240,240,240);
    xsurf_draw_text(c->canvas, r.x, r.y, r.w, r.h, b->txt, (int)strlen(b->txt), c->canvas->font, bg, fg);
}
while (1) {
    /* ... */
    button(&ui, BUTTON_OK, ok_button, .txt = "ok");
    if (ok_button.pressed)
        printf("ok pressed!\n");
    button(&ui, BUTTON_CANCEL, cancel_button, .txt = "cancel");
    if (cancel_button.pressed)
        printf("cancel pressed!\n");
    /* ... */
}
@cgbystrom
Copy link

Thanks for sharing! I'd love to read more about your thoughts on the subject.

@dumblob
Copy link

dumblob commented Apr 20, 2017

I finally found some time to read it through and take a shallow look at the code. I'm really impressed as it matches my general understanding of abstractions, APIs, etc. in the IT world.

Anyway, let me ask few clarifying questions.

  1. You're using the wording "widgets" and "components" - what is the relation between the two? If they are exactly the same, could please point it out at the very beginning of this document? Or maybe rather use component everywhere and only once at the beginning write into parenthesis, that it's a slightly more general term referring to widgets).

  2. I saw you're structuring widgets in a tree with basically "unlimited" branching factor. Does the proposed GUI API support also non-tree structures in a sense, that the tree is independent e.g. from the canvases into which single widgets/components draw (thus e.g. a canvas of a child must not necessarily be completely inside of the canvas of it's parent)?

  3. How would a support for a "multibuffer canvas/surface" (to accommodate different DPIs -
    see vurtun/nuklear#283 (comment) ) be implemented? It seems each widget would need to paint to multiple surfaces and the final blitting would need to be done for each of these surfaces separately. This means though, that each widget would need to do clipping, which I didn't spot in the examples.

  4. How would overlapping "in one frame" look like (think of opening a menu with few partly overlapping submenus due to a small surface size and at the same time a tooltip over these submenus due to a second mouse cursor being used)?

  5. How is clipping handled (as the widgets seem to not have enough information to do the clipping themself while painting)? Maybe I missed it though - any pointers?

  6. What everything does widget nesting (or any other "binding") support and how loose/firm the binding is? E.g. when a parent is repositioned, are all it's children also repositioned exactly as the parent? Can the canvas of children span outside of the parent's canvas? Do the nested children have exactly the same options as root/parents when it comes to UI handling (i.e. would it be possible to take out the child from a parent without even modifying any bits of it)?

  7. How are timers (double click, drag'n drop, animations, ...) handled? Especially when it comes to combination of immediate mode and the immutable tables "mode"?

@vurtun
Copy link
Author

vurtun commented Apr 20, 2017

@cgbystrom I am mostly working on it in my free time besides work and maintaining nuklear so progress often takes a lot of time. I will write further updates (probably in another gist since it this one is already quite long) but only if I have something new to report.

@dumblob

You're using the wording "widgets" and "components" - what is the relation between the two? If they are exactly the same, could please point it out at the very beginning of this document? Or maybe rather use component everywhere and only once at the beginning write into parenthesis, that it's a slightly more general term referring to widgets).

The reason why I usually don't use the word widget is because it is an overloaded term. However in context to the common meaning widgets in this implementation are made out of components. Components are nothing but simple rectangles, an optional surface (pixelmap) and some behavior flags. For an example look at the scroll extension. A scroll is composed out of 3 components. Scrollbar, cursor and content region. They all make up a widget. However components are not required to be a widget like a button. They could also be what in classical GUIs is called an container. I won't go further here since the resulting code will probably be smaller and less complex than anything I could write here to explain it.

By the way this is not an "offical" release post. Instead it is more like a progress report. So I don't necessarily want to go to deep into explaining since a lot is changing. Also I rather not talk about my code. If the code does not speak for itself no words can help. But I will write additional blog posts in the future which will hopefully make everything a little bit more clear,

I saw you're structuring widgets in a tree with basically "unlimited" branching factor. Does the proposed GUI API support also non-tree structures in a sense, that the tree is independent e.g. from the canvases into which single widgets/components draw (thus e.g. a canvas of a child must not necessarily be completely inside of the canvas of it's parent)?

The tree structures components not widgets. This is very important to distinguish. Like I said the core does not know anything about widgets only components. Interactive parents without total == size (read non-scrollable interactive components with children) always have to contain all children. But children do not draw into parent surfaces (unless they have flag COMPONENT_LAYER set). So in theory yes a child components can draw outside its parent. At least if I understood your question correctly. If not please clarify. This tree is used for both hit detection find(...) as well as hierarchy. More later in another update.

How would a support for a "multibuffer canvas/surface" (to accommodate different DPIs -
see vurtun/nuklear#283 (comment) ) be implemented? It seems each widget would need to paint to multiple surfaces and the final blitting would need to be done for each of these surfaces separately. This means though, that each widget would need to do clipping, which I didn't spot in the examples.

DPI is a drawing (and font but I will put it under drawing as well) problem not a UI problem since input is still in correct coordinate system. So the surface has to manage scaling not the UI. This particular design only cares about the UI. At least at the moment so I can focus on these specific UI problems.

How would overlapping "in one frame" look like (think of opening a menu with few partly overlapping submenus due to a small surface size and at the same time a tooltip over these submenus due to a second mouse cursor being used)?

I already have a solution in mind. But I need to test these out first before I talk about it. If it works as planned the solution will be so simple/short it probably would take more work/time explaining it in words in contrast to just reading the code.

How is clipping handled (as the widgets seem to not have enough information to do the clipping themself while painting)? Maybe I missed it though - any pointers?

Again explaining it would take a lot of time I could spend writing more code. But short version. Look at flag COMPONENT_LAYER. These are special components which all child components draw into. So the surface itself is your clipping rectangle (look at paint and draw.

What everything does widget nesting (or any other "binding") support and how loose/firm the binding is? E.g. when a parent is repositioned, are all it's children also repositioned exactly as the parent? Can the canvas of children span outside of the parent's canvas? Do the nested children have exactly the same options as root/parents when it comes to UI handling (i.e. would it be possible to take out the child from a parent without even modifying any bits of it)?

I think you are to focused on the surface/canvas. The surface is optional and all component surfaces are independent from each other (outside the root component and components with COMPONENT_LAYER flags). So yes child components can have bigger surfaces than their parent. Positioning is completely handled by constraint. Also constraint are not limited by the tree. Each component could have a constraint from any another component. So don't take the tree to seriously in context with constraints . By the way there is also no concept of scaling. Instead all components only have the concept of moving and constraints will do the scaling. Also not every component needs or rather has to have constraints. You could just set a fixed pixel position and it will never change.

Like I said this code does not look like much but I spend ~1 year on it (of course not all day everyday but still a lot of time) and everything is quite dense. Almost every line was written and rewritten at least 3-6 times and each code line is well thought through so it hard would take a lot of time to explain everything. I was hoping everything was a lot clearer but I hope future post make it better.

How are timers (double click, drag'n drop, animations, ...) handled? Especially when it comes to combination of immediate mode and the immutable tables "mode"?

These are all in the future and I have some ideas but there is a lot of stuff that I have to workout first. As for double click. This is not really a direct UI problem. For example SDL2 already supports the notion of double click so it is more a platform specific problem than UI problem. Side note: it is very important here to distinguish between core UI problems and problems outside like drawing, font, platform,.... While UI in itself is a rewritable problem everything around it is absolutely not.

This is all a little bit rushed but I already spend a lot of time writing this. Lots of talk but in the end the only thing that counts is working code.

@dumblob
Copy link

dumblob commented Apr 26, 2017

This is all a little bit rushed but I already spend a lot of time writing this.

Thank you very much! I'm really thankful you answered my questions.

Lots of talk but in the end the only thing that counts is working code.

Actually at least for me it counts even without a working code! It somehow calms me down when I know, that someone has a very clear vision of how to handle the issues I'm concerned with. So thank you once again!

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