Immediate mode GUI (IMGUI)
An immediate mode graphic user interface (IMGUI) is a GUI design paradigm that stands opposed to the more common style of retained mode GUIs.
Popular implementations of IMGUIs include:
The term is derived from early immediate-mode rendering APIs such as used in legacy OpenGL (see below) - until the API switched to a retain mode pattern for performance reasons.
glBegin(GL_TRIANGLES); glColor3f(1.0f, 0.0f, 0.0f); glVertex2f(0.0f, 1.0f); glColor3f(0.0f, 1.0f, 0.0f); glVertex2f(0.87f, -0.5f); glColor3f(0.0f, 0.0f, 1.0f); glVertex2f(-0.87f, -0.5f); glEnd();
RectCut - Simple UI Layouts
Simple API
Define functions that cut a smaller rectangle of an input rectangle and return it. They also modify the input rectangle.
struct Rect { float minx, miny, maxx, maxy; }; Rect cut_left(Rect* rect, float a) { float minx = rect->minx; rect->minx = min(rect->max.x, rect->minx + a); return (Rect){ minx, rect->miny, rect->minx, rect->maxy }; } Rect cut_right(Rect* rect, float a) { float maxx = rect->maxx; rect->maxx = max(rect->minx, rect->maxx - a); return (Rect){ rect->maxx, rect->miny, maxx, rect->maxy }; } Rect cut_top(Rect* rect, float a) { float miny = rect->miny; rect->miny = min(rect->max.y, rect->miny + a); return (Rect){ rect->minx, miny, rect->maxx, rect->miny }; } Rect cut_bottom(Rect* rect, float a) { float maxy = rect->maxy; rect->maxy = max(rect->miny, rect->maxy - a); return (Rect){ rect->minx, rect->maxy, rect->maxx, maxy }; }
Example: Toolbar
Rect layout = { 0, 0, 180, 16 }; Rect r1 = cut_left(&layout, 16); Rect r2 = cut_left(&layout, 16); Rect r3 = cut_left(&layout, 16); Rect r4 = cut_right(&layout, 16); Rect r5 = cut_right(&layout, 16);
Example: Two panel application
// Top bar with icons and title Rect top = cut_top(&layout, 16); Rect button_close = cut_right(&top, 16); Rect button_maximize = cut_right(&top, 16); Rect button_minimize = cut_right(&top, 16); Rect title = top; // Bottom bar. Rect bottom = cut_bottom(&layout, 16); // Left and right panels. Rect panel_left = cut_left(&layout, w / 2); Rect panel_right = layout;
Example: Button sized by its label
- If needed, first calculate the size and then cut.
- E.g., a button that sizes by the label
- This demonstrates building widgets using this method
- However, no way of specifying
cut_left
,cut_right
, etc.
bool button(Rect* layout, const char* label) { float size = measure_text(label); Rect rect = cut_left(layout, size); /* interaction with button */ /* draw the button */ }
Extended API
To enable control of the cut side from the caller for widgets, specify the side
inside a new RectCut
struct.
enum RectCutSide { RectCut_Left, RectCut_Right, RectCut_Top, RectCut_Bottom, }; struct RectCut { Rect* rect; RectCutSide side; }; RectCut rectcut(Rect* rect, RectCutSide side) { return (RectCut) { .rect = rect, .side = side }; } /* constructor */ Rect rectcut_cut(RectCut rectcut, float a) { switch (rectcut.side) { case RectCut_Left: return cut_left(rectcut->rect, a); case RectCut_Right: return cut_right(rectcut->rect, a); case RectCut_Top: return cut_top(rectcut->rect, a); case RectCut_Bottom: return cut_bottom(rectcut->rect, a); default: abort(); } } // Same as cut, except they keep the input rect intact. // Useful for decorations (9-patch-much?). Rect get_left(const Rect* rect, float a); Rect get_right(const Rect* rect, float a); Rect get_top(const Rect* rect, float a); Rect get_bottom(const Rect* rect, float a); // These will add a rectangle outside of the input rectangle. // Useful for tooltips and other overlay elements. Rect add_left(const Rect* rect, float a); Rect add_right(const Rect* rect, float a); Rect add_top(const Rect* rect, float a); Rect add_bottom(const Rect* rect, float a); /* Further you can implement extend and contract functions for Rect that are useful for borders and overhangs. */
UI Code
Widgets API
/* basic widgets */ UI_Signal UI_Label(String8 string); UI_Signal UI_Image(R_Slice2F32 slice, String8 string); void UI_Spacer(UI_Size size); #define UI_Padding(size) DeferLoop(UI_Spacer(size), UI_Spacer(size)) UI_Signal UI_Button(String8 string); UI_Signal UI_Check(B32 checked, String8 string); UI_Signal UI_Radio(B32 selected, String8 string); UI_Signal UI_Expander(B32 expanded, String8 string); UI_Signal UI_SliderF32(F32 *value, Rng1F32 range, String8 string); UI_Signal UI_LineEdit(TxtPt *cursor, TxtPt *mark, U64 buffer_size, U8 *buffer, U64 *string_size, String8 string); void UI_ColorPickerTooltipInfo(Vec3F32 hsv); UI_Signal UI_SatValPicker(F32 hue, F32 *out_sat, F32 *out_val, String8 string); UI_Signal UI_HuePicker(F32 *out_hue, F32 sat, F32 val, String8 string); /* layout parents */ void UI_NamedColumnBegin(String8 string); void UI_ColumnBegin(); void UI_ColumnEnd(); #define UI_NamedColumn(s) DeferLoop(UI_NamedColumnBegin(s), UI_ColumnEnd()) #define UI_NamedColumnF(...) DeferLoop(UI_NamedColumnBeginF(__VA_ARGS__), UI_ColumnEnd()) #define UI_Column DeferLoop(UI_ColumnBegin(), UI_ColumnEnd()) void UI_NamedRowBegin(String8 string); void UI_RowBegin(); void UI_RowEnd(); #define UI_NamedRow(s) DeferLoop(UI_NamedRowBegin(s), UI_RowEnd()) #define UI_NamedRowF(...) DeferLoop(UI_NamedRowBeginF(__VA_ARGS__), UI_RowEnd()) #define UI_Row DeferLoop(UI_RowBegin(), UI_RowEnd())
UI Separation
UI separation into two main parts:
- Core: implements common codepaths and helpers
- Builder: uses the core and arranges widgets to produce interfaces
- However, the core also provides builder code for implementing specialized widgets using "escape hatches".
Build It Every Frame (Immediate Mode)
Widget Hierarchy
In an IMGUI, the widget hierarchy is constructed every frame (instead of coming from state that must be managed).
UI_Slider(&my_float, 0.0f, 100.0f, "My Float"); UI_Checkbox(&my_bool, "My Bool"); UI_Radio(&radio, 0, "Radio 0"); UI_Radio(&radio, 1, "Radio 1"); UI_Radio(&radio, 2, "Radio 2"); UI_ColorPicker(&color, "Color"); UI_Checkbox(&show_buttons, "Show Buttons"); if (show_buttons) { UI_Button("Foo"); UI_Button("Bar"); UI_Button("Baz"); }
As we see, IMGUI code…
- shrinks and centralizes code for each widget
- unifies and shrinks builder code
- Building code, hierarchy modification code, and interaction response code are all encoded in the same location
Manual Layout management
- Specify coordinates and size of every widget manually
- Adding new widgets in the middle now requires adjustments
if(UI_Button(0, 0, 200, 30, "Foo")) // draw a 200 by 30 button at (0,0) { /* clicked */ }
Encapsulate layout in a structure
UI_Layout layout = UI_MakeLayout(...); if(UI_Button(&layout, "Foo")) { /* ... */ } /* or */ UI_Layout layout = UI_MakeLayout(...); UI_SelectLayout(&layout); if(UI_Button("Foo")) { /* ... */ }
However: No possibility of establishing a nested hierarchy of layouts (with a root layout at the top)
Pushing Layouts on to a Stack
A stack can be used to return to a parent layout after building a subtree of widgets.
UI_Layout layout = UI_MakeLayout(...); UI_PushLayout(&layout); if(UI_Button("Foo")) { /* ... */ } if(UI_Button("Bar")) { /* ... */ } UI_PopLayout();
The hierarchy is not just of layouts:
- A window in our UI may need to have a clipping rectangle for any widgets that are "inside" it
- We may want some subtree in the hierarchy to be scrolled by a scrollbar.
- We may want to apply certain widget-like behavior to the entirety of a layout (e.g., if a user clicks the background of a window, select the window)
General Widget Hierarchy
Reframe hierarchy to being of widgets (a parent of widgets is not different from a button widget).
UI_Widget *parent = ...; UI_PushParent(parent); if(UI_Button("Foo")) { /* ... */ } if(UI_Button("Bar")) { /* ... */ } UI_PopParent(parent);
- You only have a partial widget hierarchy at the time that a button construction happens.
- In order to use higher-level specifications, we'd need parts of the widget tree that we don't have at that point in the frame
- I.e., we want an offline algorithm for autolayout, but don't want to have to specify all of the data beforehand
Rendering for the widget hierarchy is deferred in an IMGUI, since the order that it needs to be rendered is actually the reverse of the order in which it is specified. Widgets that consume input events first must be rendered last (on top).
The structure of a frame, then, is this:
- Build widget hierarchy (one pass)
- Render (separate pass)
Offline Autolayout
- We accept a single frame of delay for the rectangles that we use for consumption of events
- We preserve no-frame-delay for the final rendering of each frame.
New frame structure:
- Build widget hierarchy (using last frame's data)
- Autolayout pass (produce fresh layout data)
- Render (use up-to-date layout data)
Here's how semantic sizes are expressed in this algorithm:
enum UI_SizeKind { UI_SizeKind_Null, UI_SizeKind_Pixels, // encode a direct size in pixels UI_SizeKind_TextContent, // size is determined by, e.g., a label string UI_SizeKind_PercentOfParent, // percentage value of parent widget's size on same axis UI_SizeKind_ChildrenSum, // size on a given axis is sum of the sizes of children widgets }; struct UI_Size { UI_SizeKind kind; F32 value; // unused when kind is ChildrenSum F32 strictness; // percentage of size you refuse to give up }; enum Axis2 { Axis2_X, Axis2_Y, Axis2_COUNT }; struct UI_Widget { // ... UI_Size semantic_size[Axis2_COUNT]; // recomputed every frame F32 computed_rel_position[Axis2_COUNT]; // pos relative to the parent pos F32 computed_size[Axis2_COUNT]; // size in pixels Rng2F32 rect; // final on-screen rect coordinates // ... };
- The input (the semantic size) and output (the computed position, size, and final rectangle) of the autolayout algorithm are all in the same type.
- The rect member can be used in…
- input event consumption on the frame following that of the autolayout pass
- the rendering pass of the current frame.
- UIWidget doubles as a cache, and an immediate-mode data structure. On the following frame, the UIWidget correlated from the previous frame can be used, with its hierarchical placement (and thus the entire hierarchical structure) being potentially reorganized. Despite the fact that these UIWidgets are being cached as if it is a “retained-mode” data structure, the API remains immediate-mode.
Offline Autolayout Algorithm
Each stage of the algorithm iterates over the widget hierarchy in a recursive, depth-first fashion. Pay careful attention to which stages require pre-order iterations or post-order iterations.
For each axis:
- (Any order is acceptable) Calculate “standalone” sizes. These are sizes that do not depend on other widgets and can be calculated purely with the information that comes from the single widget that is having its size calculated. (UISizeKindPixels, UISizeKindTextContent)
- (Pre-order) Calculate “upwards-dependent” sizes. These are sizes that strictly depend on an ancestor’s size, other than ancestors that have “downwards-dependent” sizes on the given axis. (UISizeKindPercentOfParent)
- (Post-order) Calculate “downwards-dependent” sizes. These are sizes that depend on sizes of descendants. (UISizeKindChildrenSum)
- (Pre-order) Solve violations. For each level in the hierarchy, this will verify that the children do not extend past the boundaries of a given parent (unless explicitly allowed to do so; for example, in the case of a parent that is scrollable on the given axis), to the best of the algorithm’s ability. If there is a violation, it will take a proportion of each child widget’s size (on the given axis) proportional to both the size of the violation, and (1-strictness), where strictness is that specified in the semantic size on the child widget for the given axis.
- (Pre-order) Finally, given the calculated sizes of each widget, compute the relative positions of each widget (by laying out on an axis which can be specified on any parent node). This stage can also compute the final screen-coordinates rectangle.
The Tree hierarchy
struct UI_Widget { UI_Widget *first; // first child UI_Widget *next; // next sibling UI_Widget *last; // useful for appending UI_Widget *prev; // enables traversal in reverse UI_Widget *parent; // iterating upwards // ... };
Imagine that UIWidget is instead the type that is only used for caching persistent data across frames. What does that look like?
struct UI_Key { ... }; // some keying mechanism struct UI_Widget { UI_Widget *hash_next; UI_Widget *hash_prev; UI_Key key; U64 last_frame_touched_index; // ... };
This is just one simple way to do it, but the basic idea is to just throw these into a hash-table, keyed by key. At the end of every frame, if a widget’s lastframetouchedindex < currentframeindex (where, on each frame, the frame index increments), then that widget should be “pruned”.
struct UI_Widget { // tree links UI_Widget *first; UI_Widget *last; UI_Widget *next; UI_Widget *prev; UI_Widget *parent; // hash links UI_Widget *hash_next; UI_Widget *hash_prev; // key+generation info UI_Key key; U64 last_frame_touched_index; // ... };
On every frame, the tree links section will be rewritten from scratch for the entire hierarchy. The hash links are used to look up the persistent part of the structure every frame.
UI Keys or UI IDs
A keying strategy needs to solve one problem: correlating one frame's call-site with another frame's call-site.
This is not as trivial as it first may seem. Many people will initially try to
cleverly use source code coordinates (e.g. __LINE__
and __FILE__
in a C macro
expansion) as a way to generate keys. But that, then, raises the question of
what happens here:
for(int i = 0; i < 100; i += 1) { UI_Button("I am a button!"); }
- Anything after a ## is hashed, but not displayed
- If a ### occurs in the string, then only everything after it is hashed, and only anything before it is displayed
for(int i = 0; i < 100; i += 1) { UI_Button("I am a button!##%i", i); }