← notes

UI development

Created: 2025-11-15

Edited: 2026-04-13 second read with some study into the raddbg actual implementation. Working through these notes after having implemented a UI system myself (atlr/lib/ui) clarified a lot of the architecture principles I had previously misunderstood.

The Interaction medium

source

We use interfaces to map inputs to operations and outputs. They help us transmit information.

When building a user interface, a goal should be to have the smallest bits sent to useful bits received ratio, in the smallest possible time spent by the program.

Also, we should reduce the amount of time a user needs to spend learning how to operate the software. Take advantage and reuse existing knowledge. (Buttons, checkboxes, radio buttons, sliders, text fields, scroll bars, etc)

Signifiers are another big component in letting a user understand how to interact with an interface. (Embossed buttons, animation changing given some state like hover or pressed)

Splitting user interface into core code (clicking, rendering, animation, layout) and builder code (widgets) lets us handle complexity.

Changing UI should be a simple task, as it is not the core of our program, it is just how we communicate information

The core provides mechanisms to describe any number of components, with indefinite behaviours. Maintaining consistency across several UI components requires managing increasing complexity, which leads us to designing common code paths (like what a press is) rather than defining behaviour per component.

On "escape hatches": conceptually understood as mechanisms for builder code to inject arbitrary behaviour into the core (custom draw functions, UI_BoxCustomDrawFunctionType-style function pointers, and the "equipment" system in raddbg). From navigating the raddbg code, the "equipments" combined with this function pointers are rather interesting.

Build it every frame

source

Keeping interface building code and interaction response code separate, increases complexity, so trying to keep everything localized (appearance and behaviour) helps construct interfaces than don't rely on managing and mutating state

We then try to put building, widget hierarchy and interaction response code in the same location.

Widget hierarchy begets layout, which itself becomes part of the widget hierarchy. Widgets hierarchy is a tree.

In order to have autolayout we need semantic definitions of position and size.

With the autolayout we should consider that there will be components which:

And so, the order in which we process each widget may change depending on its semantic definition of size and position

From studying raddbg, the layout is computed in distinct passes, in order:

https://github.com/EpicGames/raddebugger/blob/master/src/ui/ui_core.c

ui_calc_sizes_standalone__in_place(root, axis);
ui_calc_sizes_upwards_dependent__in_place(root, axis);
ui_calc_sizes_downwards_dependent__in_place(root, axis);
ui_layout_enforce_constraints__in_place(root, axis);
ui_layout_position__in_place(root, axis);

Semantic sizes in raddbg are expressed via a size kind:

typedef enum UI_SizeKind {
    UI_SizeKind_Null,
    UI_SizeKind_Pixels,      // size is computed via a preferred pixel value
    UI_SizeKind_TextContent, // size is computed via the dimensions of box's rendered string
    UI_SizeKind_ParentPct,   // size is computed via a well-determined parent or grandparent size
    UI_SizeKind_ChildrenSum, // size is computed via summing well-determined sizes of children
} UI_SizeKind;

The key word is well-determined: in my own implementation I've tried to allow arbitrary semantic constraints, having a node with size kind "childrenSum" whose children have "parentPct", making a cyclic dependency hard to resolve (I'll abstain from saying impossible as there might be ways to do it), but from the comments it "seems" like we should not even try to solve for it(?). Constraining the semantic sizes to well-determined ones sidesteps a whole class of problems.

Layout computed in order is also what lets the UI react to input correctly, which requires last frame data, or ultimately, having a cache for each frame.

Personal note to temper myself: don't expect to write your own version and have all of these features available in less than a week, or even a year, you are learning the architecture principles, and Ryan has been building this brick by brick daily for I don't know how many years. It is the collection of many learnings and iterations. Maybe if he started from scratch it could take him less than a month to have a replica, but you're just learning.

In order to avoid moving ui data (ie. animation state) into builder code, which resets every frame, it is necessary to have a cache solution within the core code. This means having a hash-table identifying widgets with unique keys, and have the widgets encode the tree with this keys.

Generating the keys to identify the widgets is a task in and of itself. ImGUI generates keys with the string passed to the widget and some encoded information after ## and ###

The widget building language

source

The core code allows builder code to be kept small and flexible. It accomplishes this by being the layer supporting several features/effects that result in something on screen. (ie. drawn with a border, centered-text, can be clicked).

The list of features enable a set of combinations that grow exponentially (2^N).

Defining individual widget types, instead of having a single widget with flags, hinders our ability to manage every combination of features available.

Handling interaction results on a widget, or making a widget interactable (clicked, hovering, etc), can become a separate struct pointing to the widget at hand

Building a widget becomes a task of working with the underlying features enabled and interaction result handling. Creating presets (like buttons or checkboxes) is just having predefined calls to the construction of a "base" widget

The widget is a lie (Node composition)

source

A widget is not the core concept in which we can decompose our UI.

This is important because several widgets can be created by composing smaller components, for example, a list box:

The author proposes naming this smaller components a "Box".

Visual content

source

Spacing: Is important as it communicates grouping, intent, and can help improve readability and clarity.

With our previous redefinition of our base component, creating spacing becomes a Box without any available interaction

Styling: Styling has not been passed down the creation of boxes as they are pulled from an Style Stack.

Meaning we have a place where we can push, pop, top the styles we are currently applying, and the creation of a unit will read the style from the stack.

For example

UI_PushTextColor("#000000"); 
UI_PushBorderColor("#FFFFFF");
UI_Button("Press A");
UI_Button("Press B");
UI_PopBorderColor(); 
UI_PopTextColor(); 

Rendering

source

Do not over-commit how every rendering layer works, and build upon each one. Layer the constraints.

The example builds this layering in the following order:

Where ImGUI Ends

source

Application specific behaviour like handling windows/tabs/panes requires handling state that our core code should have no knowledge about.

Do not mix application state with ui state.

State mutation, jank, and hotkeys

source

The purpose of the UI is to communicate information (state) and enabling users its manipulation, transforming it into new information.

This becomes a cyclical dependency as it has to constantly update given these mutations.

State transformation could be decoupled from UI interaction results, as they can come from different sources. Therefore, we could implement a State Delta Buffer (Command buffer?), that transforms interactions into state mutations.

Changing state in place, is also viable, use your best judgement as to what is the best path to follow.

Keyboard and Gamepad navigation

source

UI navigation should reduce context switching from the user, so that it can interact with the program in a fast and reliable way.

Additional Resources

  1. ImGUI
  2. Glyph Atlas