Eero Mutka

Game Programmer



Home | Linkedin | Github | Youtube


Immediate-mode UI library

Intro

When building software that require user interfaces, I want to make it intuitive and responsive. In the past, I've used the Dear Imgui library, which popularized the immediate-mode paradigm by being really easy to use. But at some point, I wanted more control, so I decided to make a UI library taking the good ideas of Dear Imgui and adding my own flare on top.

Features

  • Buttons (0:00)
  • Number edit boxes (0:04)
  • Text edit boxes (0:14)
  • Checkboxes (0:30)
  • Layout algorithm with flexibility parameters (0:34)
  • Color picker (0:40)
  • Rearrangeable boxes (0:58)
  • Scroll-regions (1:10)
  • Dropdown menus (1:35)
  • Keyboard navigation (1:47)
  • You can find the UI library and demo code at: https://github.com/EeroMutka/Fire

    Implementation

    The traditional approach to creating UIs is using the so-called retained-mode style. Retained mode systems typically define a C++ class hierarchy of widget types, which the user manually instantiates and manages. In immediate-mode style, the user doesn't manage the UI objects explicitly, but rather generates the entire UI each frame from scratch. This means that the UI object 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.

    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 are a great learning resource.

    Messages and 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 are 64-bit integers. To make dealing with keys easier, the provided UI_KEY() macro expands into a callsite-unique key and relies on some __COUNTER__ and __FILE__ hashing trickery. Using UI_KEY() is usually enough, but if the same code gets called multiple times in a frame, then more information is needed to separate them into unique keys, or otherwise the code asserts. To separate the keys, the UI_KEY1() or UI_KEY2() macros may be used to hash together other keys into new, unique ones. Examples of this can be found in the UI demo code as well as the fire_ui library itself.

    Drawing

    After the box tree has been built and the rectangles have been calculated, it gets drawn to the screen. The drawing code has gone through a few iterations. Originally, I made a similar system to Fleury's, where rectangles are submitted to the GPU and rendered using signed distance functions with parameters for edge roundness, softness, thickness, etc. I also defined a few other shapes like circles. I scrapped it in favour of Dear Imgui-style rendering, where you build and submit just a plain-old vertex buffer each frame, where each vertex has a position and a color and optionally a texture. All geometry is then generated on the CPU, but because of this it's also more flexible. A new library user can look at the provided drawing functions, copy code from it, make variations, and do whatever they like, all without touching any shader code.