Data Driven Immediate Mode UI

Taking the good parts of IMGUI

Something that doesn't take you long to learn when making an application is that gluing together code and UI is tedius, error prone, and incredibly boring. There are a number of common approaches to solving this problem of keeping state synchronized between code and UI: immediate mode architectures, data binding, and good old fashioned spaghetti code to name a few. All these methods come with their with their own sets of pros and cons.

Immediate mode GUI became popular in some circles as a way to tackle problems caused by more traditional designs. The biggest of these issues is state. With a traditional "retained mode" GUI the developer has to put a lot of effort into maintaining state. Even when being careful, time consuming bugs tend to pop up at an uncomfortable frequency.

Immediate mode GUI, or IMGUI for short, works by interlocking the applications data and the current state of the GUI. If state in either the GUI or the application changes the other will correctly reflect that change no matter what, which eliminates state miss-match bugs entirely.

IMGUI accomplishes this by using a code path that is run every update. No GUI state is retained from previous updates. This is a very convenient approach from the programmer's perspective: IMGUI gives you a straightforward, flexible solution for integrating a GUI with the application.

While typical IMGUI solutions take a step forward for one class of problems, they take two steps back in other areas like separation of logic and presentation. Because an IMGUI's structure is tightly coupled with application code, the GUI's presentation is very inflexible. Changes to what widgets are where and how they look usually takes programming. Complex GUIs can quickly get out of hand as UI logic sprawls throughout the codebase.

What if we take only the good parts of IMGUI? Its main draw is how easy integrating the GUI's state with the application becomes. When you think through it, all the application should care about is the UI's data. The application shouldn't care if a value is edited by the user with a text field or with a slider widget, only about value changes. Likewise the presentation of the GUI doesn't need to know how code stores or manipulates a value, only that it's up-to-date. To word this another way, immediate mode data is a great solution for connecting an application and its GUI.

When we restrict code's access to the GUI to only data, the designer's job also becomes much easier. So long as the type of data the GUI works with stays the same, the designer can change the presentation freely. Complete overhauls in design don't affect code.

Practical example

Lets start with a simple inventory UI. We have slots and items that can go in those slots. Each item might have other information such as a name and an "equipped" status.

Explicitly setting and reading all GUI data each frame means the two are never out of sync. You'll notice that when we set the data, we only say which slot an item is in. There is no information about where that slot is or if the item will even render in that slot. This gives the presentation layer, the designer's domain, complete control. For example if the designer wanted to add a "sort by name" feature, the "slot" data can be temporarily ignored and the items rendered in a custom position.

Items ignoring the slot data to sort by name.
Defining sorting and other logic in Quill without code.

Custom presentation of data happens without code having to get involved. The underlying data that the application works with is also completely unchanged. The designer has only changed how that data is interpreted and displayed to the user.

My favorite thing about this data-centric approach is that you can mix and match immediate mode and retained mode data very cleanly. Instead of rebuilding the "slots" data each update we could build it once. We could continue to rebuild the item data every from or only when we know it might have changed in application state. Maybe we want some implicit state for searching items or a drag and drop system that lives only in the GUI.

Implicit state for drag and drop can exist separate from the explicit application state until it needs to apply an action, like changing which slot an item is placed in.

When you restrict your application code to only working with data, never presentation, using immediate or retained mode becomes a matter of "use whichever is best in each situation." And when using a capable GUI toolkit, the data code has to manage can be aggressively simplified. We've worked hard to make Quill powerful enough to handle the entire presentation layer without code. You application code should only need to worry about your core data.

Two ways code can manage state. I'm partial to the one on the right. But when you're only working with data, even the left option works.