“Trece años dedicò a esas heterogéneas fatigas, pero la mano de un forastero lo asesinò y su novela era insensata y nadie encontrò el laberinto.”
J.L. Borges, El jardìn de senderos que se bifurcan.

A rule-based styling system for immediate-mode UIs

In this post I describe how I designed the styling system for my UI toolkit Milepost. It is a rule-based system, which seems unusual among immediate mode UIs. Those tend to use style stacks, which comes with some limitations on the ability to build and compose reusable and stylable widgets. After giving a bit of context on immediate mode UIs, I discuss those limitations, and then present Milepost’s rule-based styling system design and implementation.

A bit of context: immediate-mode UIs

Milepost’s UI lands in the vague category of “immediate-Mode UI” or IMGUI, like DearImgui, microui or nuklear. I say vague because the term “Immediate-Mode UI” is a bit overloaded and seems to denote very different properties to different people, which often results in not-so-interesting definitional quarrels.

To preemptively clear some potential confusion without indulging in those arguments, let’s just say that my personal usage of the expression IMGUI only refers to the design of the API. It means that the UI building code is called each time a new frame is needed, to procedurally define the shape and style of the UI and perform its logic. It does not imply, for instance, that no state is ever retained or that everything is done in one pass.

The Fat Boxes approach

A number of IMGUI libraries provide a pre-defined set of widgets, like buttons, checkboxes, sliders, scrolling panels, drop-down menus, and so on. This generally means that either you find the widget that suits your needs in that catalog, or you have to build one from the ground up. Moreover, you then have to fit it into the library’s internal systems, that were probably designed only with the limited set of pre-defined widgets in mind.

I’m instead adopting an approach based on a hierarchy of boxes, that can be composed to form complex widgets, and are uniformly processed by the library’s internal systems for input, layout, and drawing. Parts of that processing can be turned on or off based on per-box flags, and parameterized by simple properties attached to the boxes. For example, a box can individually turn on or off the rendering paths for its background, its border, or its text. On top of those, the library can of course still provide helper functions for common widgets. But now extending the repertoire of widgets, and combining existing widgets into more complex ones in a way that still plays well with the library’s systems, is much easier.

I’ve first been made aware of this approach by Ryan Fleury, who mentionned it a number of times in Handmade Network’s Discord server, and has now proceeded to write an entire series on his way of doing UI. Since he’s doing a tremendous job explaining this, I encourage you to check it out and won’t rehash everything here.

Instead, I’ll just describe how style attributes are used in the layout and drawing passes of Milepost’s UI, and then proceed to discuss style stacks as a way of specifying styles in the UI building code. I’ll finally describe how I’m handling styling (and re-styling) in my UI code.

Styling Milepost’s UI

As I said above, the box hierarchy is explicitely re-built each time a new frame is needed. That could be each time a new input event is processed, or when a value that’s displayed by the UI changed, or at regular intervals if the UI needs to animate. The box tree is built inside a ui_frame() block.

ui_frame(defaultStyle)
{
    // build the UI tree here
    ui_box_make("Hello, world", UI_FLAG_DRAW_TEXT);
}

The boxes layout is computed at the end of the frame, when the whole box tree have been specified. When convenient, the user can then trigger a drawing pass by calling ui_draw().

Milepost draws its UI using a GPU-rendered vector graphics drawing layer, which I wrote about here. Boxes flags control a number of predefined drawing paths, such as filling the box’s background, drawing its border, or rendering its text. In addition, for boxes that have custom rendering needs, the user can still register a callback in which they can use the canvas API to draw the box.

Both the layout and drawing passes are parameterized by per-box style attributes:

typedef struct ui_style
{
    ui_box_size size;
    ui_layout layout;
    ui_box_floating floating;
    vec2 floatTarget;
    mg_color color;
    mg_color bgColor;
    mg_color borderColor;
    mg_font font;
    f32 fontSize;
    f32 borderSize;
    f32 roundness;
    f32 animationTime;
    ui_style_mask animationMask;
} ui_style;

The size attribute specifies symbolic constraints on the size for both axis. Size constraints can be expressed as the size of the box’s text, as a number of pixels, as the size of the children, as a ratio of the parent’s size, or as the parent’s size minus some pixels:

typedef enum
{
    UI_AXIS_X,
    UI_AXIS_Y,
    UI_AXIS_COUNT
} ui_axis;

typedef enum ui_size_kind
{
    UI_SIZE_TEXT,
    UI_SIZE_PIXELS,
    UI_SIZE_CHILDREN,
    UI_SIZE_PARENT,
    UI_SIZE_PARENT_MINUS_PIXELS,

} ui_size_kind;

typedef struct ui_size
{
    ui_size_kind kind;
    f32 value;
    f32 relax;
} ui_size;

typedef union ui_box_size
{
    struct
    {
        ui_size width;
        ui_size height;
    };
    ui_size c[UI_AXIS_COUNT];
} ui_box_size;

Using the union trick above, the size for each axis can be accessed either as .size.width and .size.height, or by indexing into the .size.c component array, using the ui_axis enum.

The layout attribute parameterizes the layout of children boxes. This includes the layout axis (vertical or horizontal), the spacing of boxes along the layout axis, the alignment of boxes along each axis, and the margins between children boxes and the parent’s border.

typedef enum
{
    UI_ALIGN_START,
    UI_ALIGN_END,
    UI_ALIGN_CENTER,
} ui_align;

typedef union ui_layout_align
{
    struct
    {
        ui_align x;
        ui_align y;
    };
    ui_align c[UI_AXIS_COUNT];
} ui_layout_align;

typedef struct ui_layout
{
    ui_axis axis;
    f32 spacing;
    union
    {
        struct
        {
            f32 x;
            f32 y;
        };
        f32 c[UI_AXIS_COUNT];
    } margin;
    ui_layout_align align;

} ui_layout;

The floating attributes determines if the box is floating (i.e., not taken into account in the normal layout flow) along each axis. If the box is floating on a given axis, its position is given by the floatTarget attribute.

The animationTime and animationMask attributes control the animation of other style attributes. When a style is set for a box, it doesn’t immediately overwrite the current style (as computed in the last frame) of that box. Instead, the style will be recomputing by animating between the current style and the new target style. The animation equation used is a classic RC circuit exponential discharge, where animationTime is the 95% fall time. The set of attributes that are animated in this way is defined by the combination of flags in animationMask. Other attributes are set instantly to their new value.

Styling and Layout passes

Styling and layout are decomposed in a number of conceptual passes, some of which can be fused in the implementation:

The size attribute for each axis specifies a relax value, which is the ratio of its own size the box is willing to “give up” to constraint resolution. The product of the box size and the relax value can be thought of as the “slack” of the box’s size. The effective size removed from a child is computed as follows:

Style stacks (and their limitations)

We’ve seen that the layout and drawing passes are parameterized by per-box style attributes. But how are styles specified in the UI building code?

It would be impractical to explicitly specify all style attributes for every box in the tree. IMGUI libraries that have a well-defined set of widgets can declare a huge struct of styles attributes to apply to each specific widget kind, but we can’t do that. We could of course have some default style applied to every box, but since style attributes are so generic and boxes are combined to create all kinds of widgets, this wouldn’t be any help.

A number of IMGUI libraries reduce the burden of individually styling boxes by providing style stacks. A style stack allows temporarily pushing a style to apply to several boxes: When a box is created, its style is initialized with whatever style is currently on the top of the stack. A style stack can also be fragmented into multiple style stacks (e.g. one by attribute) to allow modifying the currently applied style in a more granular fashion.

This way of specifying box styles implicitly along with the building code is very simple (just push and pop attributes), and quite effective, in the sense that it greatly reduces the number of styles to manually specify. It works well because elements specified together or at the same level generally have a lot of common style attributes. You can thus apply a common style to a whole set of downstream boxes, and get selectively more specific as you descend towards specific widgets and sub-elements.

Limitations of style stacks

A problem with style stacks is that when you push something into a style attribute’s stack, you prevent downstream boxes to have that attribute changed from upstream. You also can’t style one box without also applying that style to its downstream boxes if they’re not themselves surrounded by push/pop calls.

Of course in a scenario where you’re able (and willing) to control the specification of every downstream box, this is not an issue: you can always add the push/pop calls where needed. This becomes more of a problem when you want to write reusable widget functions. For example, let’s say I’m making a slider() widget, that’s composed of an outer box for the track, and an inner box for the thumb, i.e. something arranged as the following:

ui_slider(char* label, f32* sliderValue)
{
    ui_container("label") // track
    {
        ui_box_make("thumb"); // thumb
        
        /* slider's logic here */
    }
}

Now, in a stack-based styling system there’s no way to pass a different style to the track and the thumb from the caller of ui_slider(). They will peek at whatever style is on the top of the stack when the widget function is called.

Since I probably want to at least differentiate the slider and the thumb’s color, I would have to do something like this:

ui_slider(char* label, f32* sliderValue)
{
    ui_container("label") // track
    {
        ui_style_push(/* push the thumb style */);
        ui_box_make("thumb"); // thumb
        ui_style_pop();
        
        /* slider's logic here */
    }
}

Now the track and the thumb can have different styles, but I can’t change the style of the thumb from the outside anymore. It will always get the distinctive style defined inside the widget function. Now you could explicitly pass the thumb’s style as an argument to the slider function, but you can see that when building more complex or composite widgets you would have an explosion of styles to pass downstream.

You can mitigate this limitation, for example by associating flags to each component of a widget, and qualifying each style attributes you push on a stack with a combination of the flags it applies to, e.g

ui_style_push_color(&colorStack, UI_ANY, trackColor);
ui_style_push_color(&colorStack, UI_SLIDER_THUMB, thumbColor);

ui_slider("My slider", &value);

Widgets would then not only peek at the top of the stack, but descend down the stack until they find an attribute that matches their own flags.

This has the drawback of needing to define a (limited) number of flags beforehands. As you compose more complex widgets you would need to add more and more specific flags to allow styling specific sub-components of those widgets.

This also does not fundamentally solve the stack problem, but merely kicks the can down the road. If you compose simple widgets to form more complex widgets, you can’t define default styles for the inner widgets without preventing them from being re-styled from outside the outer widget.

A rule-based styling system

Ideally, we need some system that allows us to define default styles locally for a whole sub-tree, but also allows us to “reach inside” a subtree to select specific inner elements and overwrite their default style. For example, we want to style all buttons with the text “OK” that are children of boxes named “dialog”. Those re-styling actions should themselves be over-writable from upward boxes, and so on.

Specifying rules

Since we want to select specific boxs to style in a subtree, and ignore others, we need to specify some kind of “pattern” along with the style to apply. The styling system will then traverse the subtree and apply the style only to boxs that match that pattern. This looks like a rule-based system, but one that is specified procedurally along with the UI builder code.

The stack model can be thought of as applying styling rules defined for a subtree before rules defined in downstream boxs. But we also need to apply some rules on a subtree after all downstream rules have been applied. For each box we can keep two lists of rules, one for rules to be applied before the downstream rules, and one for rules to be applied after downstream rules.

I find it generally practical to specify all styling rules for a box just before making that box, regardless if the rules are to be applied before or after its children. The box to which a rule is added is thus implicit (it is the next box to be built), and the API has one function for each set of rules:

void ui_style_match_before(ui_pattern pattern, 
                           ui_style* style, 
                           ui_style_mask);

void ui_style_match_after(ui_pattern pattern, 
                          ui_style* style, 
                          ui_style_mask);

Instead of having different versions of the functions for each style attribute type, you pass a pointer to a ui_style structure containing all style attributes, and a style mask that indicates which attributes are to be applied. Using C compound literates, you can set only the attributes you want to modify, in a pretty convenient way:

ui_style_match_before(pattern, 
                      &(ui_style){.size.width = {UI_SIZE_TEXT},
                                  .size.height = {UI_SIZE_PARENT, 1},
                                  .color = {1, 1, 1, 1},
                                  .bgColor = {0.2, 0.2, 0.2, 1}},
                       UI_STYLE_SIZE
                      |UI_STYLE_COLOR
                      |UI_STYLE_BG_COLOR);

I also found that creating “before” rules that either match the whole next subtree, or only the next box (and not its desendants), was sufficiently common to call for helper functions:

void ui_style_next(ui_style* style, ui_style_mask mask);
void ui_style_next_box(ui_style* style, ui_style_mask mask);

Specifying Patterns

A pattern is the part of a rule that allows selecting boxes to style in a subtree. A pattern is a linked list of selectors, which specifies properties that need to be matched at different levels of the subtree. Those properties can be the text of the box, its unique key identifier, its hovered/active states, etc.

typedef struct ui_pattern
{
    list_info list;
} ui_pattern;

typedef struct ui_selector
{
    list_elt listElt;
    ui_selector_kind kind;
    ui_selector_op op;
    union
    {
        str8 string;
        ui_tag tag;
        ui_key key;
        ui_status status;
    };
} ui_selector;

The kind of a selector can be one of the following:

The operator defines how the selector combines with previous selectors in the pattern. If the operator is UI_SEL_DESCENDANT, the selector tries to match a descendant of the box matched by the previous selector. If the operator is UI_SEL_AND, the selector combines with the previous selector by adding a condition to be matched by the same box.

Patterns can be constructed by pushing selectors to them, using an arena allocator:

ui_pattern pattern = {0};
ui_pattern_push(arena, &pattern, 
                (ui_selector){.kind = UI_SEL_TEXT, 
                              .string = STR8("foo")});
ui_pattern_push(arena, &pattern, 
                (ui_selector){.op = UI_SEL_STATUS,
                              .kind = UI_SEL_TEXT,
                              .status = UI_HOVER});                              
ui_pattern_push(arena, &pattern, 
                (ui_selector){.kind = UI_TAG, 
                              .tag = ui_tag_make("button")});

This pattern will match boxes that have the tag button and are descendants of a box that is both hovered and has the text foo.

Applying rules

The sets of rules that apply to a box are the following:

A simple way to implement the pattern matching mechanism can be derived from the following realization: a pattern is matched by a box if the leading combined selectors are matched by an ancestor, and the rest of the pattern is matched by the box.

This means that at every level of the tree, if the box matches the first selector and all following combined selectors from a rule’s pattern, and there are still selectors left, we can create a new rule with those remaining selectors and pass it downstream. If there are no remaining selectors, the current box matches the pattern. Those derived rules (from the Brzozowski derivative) are inserted in the list just after the rule they are derived from.

Upon returning from a box, rules that were added in that box (either rules defined in that box or derived rules) are removed from the lists of rules.

The algorithm outline is as follows:

check_pattern(box, rule, runList, tmpList)
    if head of rule matches box
        if rest of rule is empty
            apply rule.style to box
        else
            d = derivation of rule
            insert d in runList after rule
            push d in tmpList 

ui_apply_rules(box, beforeList, afterList)
    list tmpBefore = {0}
    append box.beforeRules to beforeList and tmpBefore
    for rule in beforeList
        check_pattern(box, rule, beforeList, tmpBefore)
        
    list tmpAfter = {0}
    prepend box.afterRules to afterList and tmpAfter
    for rule in afterList
        check_pattern(box, rule, afterList, tmpAfter)
        
    for child in box
        ui_apply_rules(child, beforeList, afterList)
        
    remove rules in tmpBefore from beforeList
    remove rules in tmpAfter from afterList

Conclusion

Style stack are a simple and effective way to specify styles along UI building code in an immediate-mode UI. They are flexible enough if you’re specifying the whole UI with pre-determined styles, but make building re-stylable widgets harder unless you’re willing to pass style parameters to widget functions (or inline and manually re-style them).

In contrast, the rule-based approach I presented allows reaching into a widget’s subtree to selectively re-style its components, while still allowing stack-like semantics when desired. This means one can write helper functions for reusable widgets, while retaining the ability to overwrite the style of deeply-nested components of those widgets from the caller.

Arguably, that approach has a higher complexity budget than a style stack. In fact, uppon reading the words “rule-based styling system”, a few of you might have had a surge of bad feelings. Maybe the letters CSS even crossed you minds. But I do hope I convinced you that a small yet decently expressive rule-based styling system doesn’t have to be that kind of monstrosity. Its API can be expressed as a couple of simple functions instead of a whole declarative language. And its implementation can in fact be quite simple! An arena allocator and a few linked list go a long way.