Immediate-mode UI library
C / C++
Immediate-mode UI library with auto-layout for easily making intuitive UIs with buttons, text boxes, color pickers, and more!
Demos
Link to the example code which generates the UI in the above video
Showcasing the UI in a separate work-in-progress project of mine (connected to my level editor work)
About
In the past, I've used Dear ImGui for quickly making prototype UIs. It's great! But it became clear that I wanted more fine control over the UI code, particularly for the plans I have for my level editor project, but for other projects as well. So this UI library has been like a little garden that I come back to improve every now and then, and it has grown enough that I think others can benefit from it too! My goal has been to keep the code really easy to use and simple, but also very extensible as it's not possible (or my goal) for me to implement every possible UI feature imaginable.
Implementation
The classical way to implement a UI system is use the "retained-mode" style, which is typically an object-oriented class hierarchy of widget types which the user manually instantiates and manages. In "immediate-mode" style, the user doesn't create or destroy the UI objects explicitly, but rather generates the entire UI each frame from scratch. This means that the UI objects don't need to be manually updated whenever something changes in the user program, and it's thus easier and less error-prone to make dynamic UIs. Dear ImGui popularized this paradigm, and I wanted my code to follow a similar pattern.
The basis of my UI system is a hierarchical tree of "boxes" that get placed one after another vertically or horizontally. A box is a rectangle with a set of customizable and optional behaviours. For example, to make a button, you would add a "box" with the behaviours "has text", "clickable", "draw border" and "draw background". This could of course be wrapped in an "AddButton" function, but the point is that composition makes it easy to customize and build specialized UI widgets. This idea is talked about in depth in Ryan Fleury's UI Articles, which were a great learning resource for me.
// Example code from the UI demo, which builds the area with the 4 flexible buttons (0:45 in the video).
// This code gets run every frame.
UI_Box* row = UI_BOX();
UI_AddBox(row, UI_SizeFlex(1.f), UI_SizeFit(), UI_BoxFlag_Horizontal | UI_BoxFlag_DrawBorder);
row->inner_padding.x = 10.f;
row->inner_padding.y = 10.f;
UI_PushBox(row);
UI_Box* button1 = UI_BOX();
UI_Box* button2 = UI_BOX();
UI_Box* button3 = UI_BOX();
UI_Box* button4 = UI_BOX();
UI_AddButton(button1, UI_SizeFlex(1.f), UI_SizeFit(), 0, "Flex button");
UI_AddButton(button2, UI_SizeFit(), UI_SizeFit(), 0, "Fit button");
UI_AddButton(button3, UI_SizeFlex(1.f), UI_SizeFit(), 0, "Another flex button");
UI_AddButton(button4, UI_SizeFit(), UI_SizeFit(), 0, "Another fit button");
if (UI_Clicked(button1)) printf("Pressed flexible button!\n");
if (UI_Clicked(button2)) printf("Pressed fitting button!\n");
if (UI_Clicked(button3)) printf("Pressed another flexible button!\n");
if (UI_Clicked(button4)) printf("Pressed another fitting button!\n");
UI_PopBox(row);
Keys
In immediate-mode UIs, it's common to process and return input messages immediately as widgets are added, hence the name "immediate-mode". For example, Dear Imgui's `Button()` function returns a boolean that says if it has been pressed. This is convenient, as code which is logically related to that button can be implemented right then and there. There is just one problem: if we have a smart layouting system with flexible elements, like I do, then the box rectangles can only be calculated once the entire UI has been built. So while we're adding a button and in the middle of some UI generation code, how can we check if the mouse has clicked inside a rectangle that cannot yet be determined? My solution is to use the rectangle from last frame for checking mouse overlaps. We must then identify the boxes using "keys" for looking up information from the previous frame. Giving keys to the boxes becomes necessary for other reasons as well, such as for storing the "active" box for keyboard navigation, text input, etc.
There are multiple possible ways to go about keying. Dear Imgui hashes the name of a box and allows for providing extra hashing digits at the end of names. The problem is that not all UI boxes have names, or multiple boxes might share the same name. In my UI, keys must be provided explicitly for all boxes and they are 64-bit integers. To make dealing with keys easier, I provide macros to easily generate boxes that are unique to the callsite as well as to generate unique keys from the combined hashes of other keys (see here and here). If the same key is used twice within a frame, the library asserts letting you know that you've made a mistake: in my experience, the fix is always trivial when this happens.
Drawing
After the box tree has been built and the rectangles have been calculated, it gets drawn to the screen. Originally, my drawing code was similar to Fleury's, where rectangles were submitted to the GPU and rendered using signed distance functions with parameters for edge roundness, softness and thickness. I also defined a few other shapes like circles. I scrapped it in favour of Dear Imgui -style rendering, where you build and submit a regular vertex buffer each frame, where each vertex has a position, color and optionally a texture. All geometry is then generated on the CPU, but because of this it's also more flexible. A new user can look at the provided drawing functions, copy code from it, make variations, and do whatever they like, all without touching any backend-specific shader code.
Code composition
Designing the library has been an excercise in code composition and extensibility. The requirements for UI widgets can vary a lot per application. Implementing everything imaginable directly into the "core" of the library would make it really complicated and difficult to work with. That's why whenever I've wanted to make a new UI widget, I've asked myself: how easy is it to build it in the application code without modifying the library itself? If this is easy for the majority of the times, then the library is well structured. Even if some UI widget is broadly useful enough to include as part of the UI library, it's still great if it can be separated to keep it self-contained and to keep the library core simple.
There are a few principles that can improve extensibility. The most obvious way is to provide lots of styling parameters: text color, border roundness, etc. But that's only possible to a certain point. The second way is to avoid "private" or "internal" library functions. If all the UI functions are public, easy-to-understand, and well-documented, then in theory the user can just copy one, tweak it, and call it instead of the library-provided function. The problem is, if the function is deep within a call hierarchy of some other functions, then the caller functions need to be duplicated as well. The way to break these nested call hierarchies is with customizable function pointers, they're sort of like styling parameters! For example, I store a function pointer per UI box for custom size calculation, and one for custom drawing. The third way to improve extensibility is to make the system more dynamically typed. Imagine that I wanted to make a new feature that required storing some data in a UI box. Naively, I would add a struct member to the UI_Box struct. But that would require tweaking the library code. To fix this, we can make it possible to attach arbitrary data into UI boxes using keys. I use this in many places around the library, and it has been a great help in making the code more flexible.