GG-UI — Foundations

← GG-UI Design | Home

What every author of GG-UI code needs: the pieces, how a build works, how UI stays current, and where each detail is documented. The reference pages (Core, Interaction, Styling, Rendering) go deeper; this page is the map.


The Pieces

PieceWhat it isWhere
GGUI libraryThe UI: nodes, the builder, layout, bindings, input, styles. Depends only on TnxFoundation (enforced by CMake), runs headlesssrc/GGUI
TnxFoundationWhat the UI shares with the engine: allocation, handles, the lock-free ring, TnxName, JSON, Fixed32src/Foundation
UIContextOne document: a tree of nodes, built by a root function, updated once a frameUIContext.h
UIThe builder passed to every UI function: ui.Row, ui.Label, ui.Button, ...UI.h
UIHostThe engine side: a document per window, input in, the published snapshot out to the renderer, the dock, styles, stringssrc/Runtime/UI

The engine talks to GGUI through three interfaces: platform events in, published snapshots out, and a binding reader for game data. Nothing else crosses, which is why the whole library can be tested without a window (Testbed/src/Tests/GGUI_*.cpp).

A Document

GGUI::UIContext context;
context.Mount([](UI& ui)
{
    if (auto page = ui.Column({ .Gap = 6, .Padding = GGEdges::All(8) }))
    {
        ui.Label("Game.Menu.Title");
        ui.Button("Game.Menu.Play", [] { StartGame(); });
    }
});
context.Update();        // runs what needs running; with nothing changed, runs nothing
context.UpdateLayout();

Under the engine, UIHost::Mount(window, root) does this for a window and updates it every frame. A UI function describes the tree; it doesn't draw. Running it again with the same result changes nothing.

Writing UI

Elements and props. Each element takes its subject, then props (a designated-initialiser struct), then what it shows, then what a change or click does. Props can be left out.

ui.Label("Editor.Stats.Live", { .Class = "stats-name" });
ui.Slider({ .Min = 0, .Max = 100 }, volume, [&](float value) { volume = value; });
ui.Button("Editor.Scene.Save", [editor] { ... });

Two forms for containers. A scope object builds children now; a lambda is content the container stores and can re-run on its own.

if (auto row = ui.Row({ .Gap = 8 }))           // now: children follow in this function
{
    ui.Label("Name");
    ui.Spacer();
}
ui.Column({}, [=](UI& ui) { ... });            // stored: captures by value

Plain C++ is the control flow. Branch with if, repeat with for. In a loop, give each element a key so it keeps its identity (focus, scroll, state) when the list changes: ui.Key(item.ID).

Shorthand. using namespace GGUI::StyleKeywords; allows .Width = Grow, .Padding = 8, .Align = Center, and a class where props would be: ui.Label("x", "muted").

Keeping It Current

Two mechanisms, each with one job.

Re-runBinding
Use whenData decides which elements existAn element shows a value that changes
HowThe component watches a GGRevision; its owner bumps itThe element is given a GGBinding
CostThe function runs and its result is matched to the existing nodesOne read and compare per update, no function runs
ui.Watch(selectionChanged);                    // re-run this component when it is bumped

const TnxBinding health = TnxBind(player->Body, &CHealth<>::Value);
ui.Label(health, { .Decimals = 0 });           // a bound number: updates with no re-run
ui.Progress({}, health / TnxBind(player->Body, &CHealth<>::Max));   // a bound fill
  • Components are the re-run boundaries. A stored lambda is one; so is ui.Component(fn). An unchanged component (same function, equal captures or properties) is skipped when its parent re-runs.
  • Local state lives with the node: auto open = ui.State(false); then open.Get() / open.Set(true). Setting it re-runs the component it belongs to.
  • Bindings read the published frame, never live objects. Game values are bound with TnxBind(entity, &CHealth<>::Value); editor and tool values through their own targets (EngineStats for the stats panel).
  • Idle is free. With nothing watched bumped and no bound value changed, a document does no work.

Text, Looks, and Input

  • Text is a string ID. "Editor.Log.Title" is looked up in the string table (Trinyx/assets/strings/en.strings for the engine's own text); GGText::Verbatim(name) shows data as it is. Titles and tooltips always come from the table (Localisation).
  • Looks are not in code. Code names a class; a style sheet (.ggss, hot reloaded) gives it colours, borders, and states (:hover, :focus, :disabled). The engine theme is Trinyx/assets/ui/trinyx.ggss (Styling).
  • Handlers are lambdas, taking the event or nothing. They run on the thread that updates the document, so they change UI state freely and hand anything else to its owner (in the editor, EditorContext::Do).
  • Shortcuts are input actions, not keys in code: ui.Command(action, handler), and menu items and tool buttons given the action show its chord (Input Actions).

What Is Built In

KindCalls
LayoutRow, Column, Stack, Grid, Place, Spacer, Split, Tabs, Dock, SafeArea
ChoosingSwitcher (one child by a value), Repeat (an item a number of times), Present (in layout or not)
ContentLabel, Image, Swatch, Progress, Viewport, Canvas
ValuesButton, Toggle (both may take content), Slider, Number, Vector, Dropdown, Carousel, Choice, Color, TextField, TextArea, KeyBinding
DataList, Tree, Table, Tiles, Picker over a GGDataSource; Fields and Properties from reflection
PopupsTooltip, Menu, ContextMenu, Popup, Dialog, Ask, notices
ToolsMenuBar, Toolbar, ToolButton, LogView, CommandLine, Graph
AssetsUse: a widget asset made as data (Widget Assets)

A widget is a function: a new one needs no class and no registration. A widget that keeps its own state across frames (a text editor, a graph) is a struct with hooks (UIWidget.h).

Testing

A document runs headless: build it, read the nodes (Nodes(), Text(node), Type(node), Paint(node)), click (Click(node), Dispatch(event)), and update bindings against a fake reader. Authoring mistakes are reported through UIContext::SetDiagnostics, which a test can collect. The GGUI_* tests are the examples.

Next