A Data-Driven GUI System in a Custom C++ Engine

Ask my second engine for a screen that doesn't exist and it doesn't crash, and it doesn't show nothing. The loader has a fallback branch:

else{
    loadFile("404.gui");
}

404.gui is a real file in the shipped data: one text control, "404 File not found", red, 25 points, centered.

That fallback is the tell for the whole design. The GUI system in this engine is a small web browser. Screens are text files. A stylesheet restyles every button in the project from one place. A button, an image, or a piece of text can be a hyperlink that navigates to another screen. Pages include other pages. Sizes can be percentages of the parent. And a broken link renders a 404 instead of failing.

None of that was a metaphor I was reaching for in 2012. I'd been building websites, and when I needed to describe interfaces in data, the web was what my head already reached for. This continues the series taking one system at a time out of my custom engines. This one belongs to the second engine, the one with the twenty-four projects and the editor tools, and it still runs: the tree was ported to SDL2 a week ago and the GUI came through mostly by renaming things.

The library is 4,439 lines of C++. The repository carries 8,451 lines of .gui text describing interfaces built with it, across a few generations of menus, close to two lines of interface for every line of the code that interprets them. The data outweighs the implementation, which is roughly what a data-driven system should look like.

A Screen Is a Page

A .gui file is a list of control blocks. Here's a real excerpt from the particle editor's main screen:

window
name=wndColorPicker
size=360,340
text=Color Picker
visible=false
contents=ManicParticleEditor/colorpicker-contents.inc.gui
end

layout.autoPosition.exact=15,24

button
name=btnNew
text=New
end

A bare word opens a control, key=value lines set properties, end closes it. The parser is one function with an else-if ladder 83 string comparisons long, every property any control can have, in one place. There are sixteen control types: the ordinary ones (button, text, image, checkbox, textbox, slider, progress, dropbox, listbox, line), an image slider, a draggable window, and then the ones that give away who this system was really for. A thumbnail file browser, a saturation/value color picker, a multi-handle gradient slider, and a viewport.

Composition is web-shaped too. include= splices another file into this one, which is how every screen pulls in the stylesheet. contents= hands a whole file to a control's own child manager, so a window's interior is a separate page (an iframe, essentially). The two editors that both need a color picker share it the crude way: identical copies of colorpicker-contents.inc.gui in both folders.

Names are identity. Every control is registered in a map by name, duplicate names throw an error dialog, and controls the game will never talk to get name=auto, which auto-numbers them. The shipped data does that 59 times. Names are also the z-order: the draw order is a vector of strings, resolved through the map into a cached list of pointers. The commented-out loop above the cache still does a map lookup per control per frame, left in place like a fossil of the day the profiler said no.

The Style Engine Is the Assignment Operator

The stylesheet is java.style, 214 lines, included at the top of every screen. It defines class blocks:

class
name=button
exactSize=true
position=auto
font.face=segoeui.ttf
font.size=8
font.color=0,0,0
anchor=left
clamp=top left
image.inactive=ui/java/button.tga
image.hover=ui/java/button-hover.tga
image.active=ui/java/button-down.tga
end

A class named button restyles every plain button in every file loaded after it. A class named button.med is a variant opted into by opening a block with button.med instead of button. Element selectors and modifier classes, in other words, and the cascade is exactly CSS's: constructor defaults, then the stylesheet, then the block's own inline properties, each layer overwriting the last.

The mechanism behind it is my favorite thing in the library. When the parser sees a control open, it looks the line up in the style table and does this:

control = new ManicGUIButton;
ManicGUIControlManager * tmp = control->controlManager;
if(guiManager.styleExists(line)) *control = *(guiManager.styles[line]);
control->controlManager = tmp;

Styles are prototype objects (actual constructed controls, parked in a map), and applying one is the compiler-generated assignment operator. The entire style engine is operator=, plus one pointer saved and restored around it.

There's a quirk with teeth. Clearing a control manager also wipes the global style table, and loading a contents= file starts with a clear. So every window-interior page begins with include=styles/java.style, re-registering the styles its own load just destroyed. Every frame in the frameset carries its own link tag. It works because the files are disciplined, and it would be a genuinely confusing afternoon for anyone who didn't know.

Every Control Is Every Control

That assignment-operator trick has a precondition, and the precondition explains the strangest-looking thing in the library: the base class. ManicGUIControl is two hundred lines of public fields, the union of every field any control needs. A contiguous excerpt:

float hue;
float sat;
float val;

float huebk;
float satbk;
float valbk;

GLuint satvaltex;

Every button in the engine carries the color picker's hue and an OpenGL texture handle for a gradient it will never render. Every text label carries a password flag, a field-of-view for the viewport, nine window-chrome texture slots, and four scroll-arrow images. The derived classes add behavior only. ManicGUIButton declares seven virtual overrides and not one data member. Even the multi-handle gradient slider keeps its handle positions in the base class's string vector and calls atof on them every frame.

This looks like the god object anti-pattern, and in most respects it is one. But it's also the single decision the rest of the system stands on. Because all state lives in the base, the parser can set any of its 82 property keys on any control without knowing the type, a style prototype can be a plain base-class instance, and copying one over a derived control with operator= slices off exactly nothing, because there is nothing to slice. Uniform state is the price of data-driven everything, paid up front, in one class.

The other consequence is that nothing is encapsulated, and the games lean on that hard. The particle editor opens its color picker by writing wndColorPicker->visible = true; and pushes the current color into it with svpPicker->hue = h/360.0f;. State goes in through public fields, and events come out through a callback. Two channels, both blunt.

Layout Is Two Anchors and a Typewriter

Every control has an anchor (which of nine points on itself its position refers to) and a clamp (which of nine points on its parent that position is measured from). The earlier post about the mobile engine's layout system described the same two anchors, and this is where they came from, in rougher form. Geometry is resolved at query time by walking the parent chain. Nothing stores a screen rectangle, and the root manager holds pointers to the live window size, so a resize re-resolves everything with no invalidation pass. The same lesson as the mobile engine, learned here first: a derived value can't go stale.

The walk has one hardcoded opinion. Children of a window are offset by the title bar and padding:

v1.x +=  0 + 15;
v1.y += 18 + 15;

The drawing code sizes the window chrome from the skin's actual textures. The layout code assumes the title bar is 18 pixels. The default skin's title bar is 18 pixels. Nobody ever made a second skin.

Sizes and positions go through a converter that accepts 280, 100%, and 100%-32, meaning percent of the parent axis, with one addition or subtraction. It's a homemade calc(), and a worse one, because the expression evaluator has a real bug: a percentage on the right side of the operator computes something wild (it applies the percent to the running total, not the operand). The bug has never fired. All 8,451 lines of shipped data write the percent on the left, and nothing enforces that except that I wrote both the parser and the data.

Then there's flow layout, which isn't containers at all. It's a typewriter. The file maintains a cursor. position=auto places a control at the cursor, and after every control the parser does:

if(control->autoPosition) control->position = autoPosition;
autoPosition.x = autoPosition.x + control->size.x;

Controls are words. layout.autoPosition.break=16,24 is the carriage return, resetting x and dropping down. A cursor can be saved under a name and jumped back to later. It reads exactly like typesetting a page, because that's what it is.

Input Still Speaks Win32

The message entry point is this:

void message(unsigned int message, unsigned int wParam, unsigned int lParam);

For fourteen years those were literal WM_KEYDOWN and WM_LBUTTONUP codes with the mouse position packed into lParam, exactly as Windows delivered them. The SDL2 port left the signature alone. It renamed the constants to MANIC_MSG_* one for one and taught the window to translate:

case SDL_MOUSEWHEEL:
    message = MANIC_MSG_MOUSEWHEEL;
    // The Win32 path reported wheel notches in units of WHEEL_DELTA.
    wParam = (unsigned int)(event.wheel.y * 120);
    break;

A Linux build in 2026 multiplies scroll events by 120 because that's what WHEEL_DELTA was worth in windows.h. The API outlived the platform that defined it. When every consumer agrees on a dialect, the dialect stays.

Routing is a top-down walk of the z-order with sensible game-tool politics. Open dropdowns get first refusal, because they overhang everything. A click only counts if the press and release land on the same control. Clicking a window raises it. Focus is a boolean named active that clicking anywhere else turns off, and keys are broadcast to whatever is active.

Mouse capture is my favorite implementation in the file. While a window is being dragged, its hit test does this:

if(active){
    v1.x -= 2000;
    v1.y -= 2000;
    v3.x += 2000;
    v3.y += 2000;
}

Mid-drag, the window is four thousand pixels bigger in every direction, so the cursor can't outrun it. Sliders do the same thing while their handle is held. That's the whole capture system. It is indefensible in principle and has never once failed in practice.

The genuinely dangerous case is reentrancy. A button's link= property loads another screen from inside the button's own click handler, which destroys every control, including the button whose member function is currently executing. The system survives on a flag: clearControls sets controlReset, the handler returns immediately without touching another member, and the dispatch loop checks the flag before taking another step through a list that no longer exists. Retained-mode UI's classic hazard, handled not with deferred deletion but with a tripwire and discipline.

The Game Gets Strings

Everything that happens in the tree is reported to one function pointer as (name, action, button, position). The game's side of the contract looks like this, from the dungeon crawler's main menu:

int Menu::callback(string control, int action, int button, Vector3 position){
    if(control.compare("txtGame")==0 && action==MOUSE_UP && button==MOUSE_LEFT){
        runEditor=false;
        changeState(StateGame);
    }

Each game state swaps in its own callback, so "screens" in the game are different switchboards. The particle editor's switchboard is a 551-line function servicing 242 controls described across two thousand lines of layout. Events that hit no control arrive with an empty name. The click fell through to the page background, and the game treats that as "the user clicked the world."

The weakness is the obvious one: rename a control in the layout file and it silently disconnects from the game, because the compiler can't see the string. I hit that about once per tool. The strength is that the entire UI-to-game boundary is one function, which takes a single breakpoint and reads like a transcript.

There was also the start of a scripting language, onclick=battlewindow.toggleVisible in the data, from an abandoned space game whose menus toggled hangar and cargo windows. The interpreter supports exactly one verb. toggleVisible turned out to be all the declarative behavior anyone needed, and everything else went through the callback.

One Quad, a Scissor, and a Texture Matrix

Rendering is OpenGL 1.x immediate mode with one modern habit: every textured rectangle in every interface is the same unit quad in a single VBO, drawn under a translate/scale/rotate. Windows are nine-slice, sized from their corner textures. Interiors clip children with glScissor, and off-screen quads are culled before submission, with counters.

Text is cached: each control keeps a handle to a GL buffer of glyph quads, and invalidates it by comparing its string against the cached copy, every control, every frame. Cache invalidation by strcmp, sixty times a second, and at tool scale it's completely fine.

Two controls do something sneakier. Any image slot can name a .anim instead of a texture (a flipbook strip with a frame rate), and playback happens entirely in the texture matrix: scale U by 1/frames, translate by the frame index, draw the same quad. Animation with zero vertex changes. And the saturation/value picker regenerates its gradient the brute-force way: when the hue changes, it computes every pixel on the CPU and re-uploads the texture. It runs on every drag of the hue slider, and nobody has ever noticed the cost.

All of it wears one skin: 46 TGA files under ui/java/ (buttons, checkboxes, scroll arrows, window chrome) with Segoe UI at 8 points on top. The folder is named after what the result looks like, which is a Java Swing application, and I have decided to find that charming rather than apologize for it.

The Tools Are the Real Users

Nine executables in the tree load .gui screens, and the interesting ones are the editors, the tools from the post about editors sharing the engine's runtime. This GUI system is how they share it, and two controls exist purely for their benefit.

The viewport is a control whose render() is an empty function. It draws nothing. Its job is to be a named rectangle: the editor calls activateViewport("vpMain") and the engine sets glViewport to that control's rectangle and renders the 3D scene into it. The scene view is a hole cut in the page, positioned by the same anchors as the buttons around it.

The thumbnail browser is a file manager as a control: it lists the actual directory, shows folder icons at four sizes, ellipsizes long filenames, plays .anim thumbnails live in the grid, and navigates into folders on click. The particle editor's texture picking is this one control and a CONTENT_CHANGE event.

And the "GUI editor" is the punchline of the whole design: 228 lines, no editing features. It loads a screen, then polls the modification time of every file that screen came from and reloads when one changes, per subtree, so a window's interior page reloads without rebuilding the screen around it. The actual editor is whatever text editor is already open. This program is the refresh button, held down. Total-replace loading of a text format made hot reload a forty-line feature, and hot reload is nine tenths of what a GUI editor is for.

The games use the tree for menus (the dungeon crawler's title screen is text controls whose stylesheet grows them from 100 to 128 points on hover, the entire menu feel expressed in font.hover.*) and then skip the tree for HUDs. The RTS draws health bars and command buttons by calling renderQuad directly, using the library as an immediate-mode 2D API. The retained tree is for interfaces somebody authors, and the draw helpers are for interfaces the game computes. Both turned out to be things a GUI library should sell.

The Ghosts

Reading old code of mine, the absences say as much as the code.

The type-code list reserves GUI_MENUBAR, GUI_MENUBUTTON, and GUI_MENUITEM. No menu classes exist. Every tool that wanted a menu bar made a row of ordinary buttons, which is why a real menu system never became urgent enough to build. There's a property-grid control that draws a tidy two-column table, reachable only from C++, because it never got a keyword in the parser. A holdfocus flag is carefully set on textboxes and dropdowns and read by nothing.

The textbox itself is a monument to sufficiency: it synthesizes characters from raw key codes with manual shift and caps-lock handling, supports append and backspace, and has no cursor movement, no selection, no clipboard. There is no way to type a colon into it. For naming emitters and typing numbers into editor fields, it never needed more.

And the ghost I keep going back to: the engine has a Quake-style console. Backtick summons it, it's a third of the screen tall, it fades away after it's been quiet, and for a while the engine's console(n) debug macro piped every log line into it. The engine's printf was a GUI control. The macro history is still in the header, one commented line at a time: GUI console, then stdout, and today SDL_Log. The console object is still constructed and still updates every frame. The line that would draw it is commented out in both places it appears. It's not dead code so much as a room nobody goes into anymore, with the furniture still arranged.

What Transfers

Give missing content a rendering. The 404 page costs nothing and converts a class of crashes into a message that names what to fix. Any data-driven system can have one.

A text format, plus loading that throws the tree away and rebuilds it, buys hot reload almost for free, and hot reload is worth more than any editor UI built in its place.

Prototype-based styling through plain assignment is a real technique, and its precondition is uniform state. The god-object base class wasn't the mistake in this design. It was the payment for the parser and the style engine both being trivial. Anyone who wants the trick has to flatten the state knowingly.

Resolve layout from live inputs at query time. Pointers to the parent's size meant window resizing was never a feature. It was a property the system had by construction.

And a string-keyed boundary between UI and logic is a real trade: one debuggable transcript of everything the interface does, bought with silent breakage on rename. I'd take it again for tools, and I'd want it compiler-checked for anything bigger.

The web shape (pages, a stylesheet, links, a 404) was a good fit for tool interfaces in 2012. Fourteen years and one platform port later, the same files still load, which suggests it still is.