Using GG-UI
← GG-UI Foundations | First Steps with GG-UI | Widget Assets | Home
How to put GG-UI to work: first on its own, as a library any program can host, then inside Trinyx, where the engine hosts it and games build their screens from widget assets and widget classes. Foundations covers writing UI in code; this page covers everything around that: hosting a document, feeding it data, input, text and images, drawing it, and the engine's way of doing each.
Part 1: GG-UI as a Library
GG-UI is the GGUI static library. It depends only on TnxFoundation, and CMake fails the configure if anything adds an engine path to it. GGUIText adds real text (FreeType, HarfBuzz, SheenBidi) and depends only on GGUI. Neither needs a GPU or a window: a host supplies those.
target_link_libraries(MyTool PRIVATE GGUI GGUIText)
What a host provides
A document asks its host for a few things, each through an interface it is given. Every one is optional except the surface; what is missing has a plain fallback.
| What | How it is given | Without it |
|---|---|---|
| The surface: size in UI units, pixels per unit | SetSurface(width, height, scale) | nothing is laid out |
| Looks | SetStyles(&styleTable) | only text is drawn |
| Text measurement and glyphs | SetTextMeasurer, SetGlyphSource (a TextEngine) | a fixed metric: 8 units a character, 16 a line; text draws as blocks |
| Strings for the active language | SetStrings(&table) | text is shown as written |
| Images | SetImageSource(&source) | images have no size and draw nothing |
| Data for bindings | a BindingReader, passed to UpdateBindings each frame | bound values don't change |
| Widget assets | SetWidgets(&library), and SetWidgetActions for what they do | ui.Use reports there is no library |
| Clipboard, popup windows, notices, file dialogs | SetClipboard, SetPopupWindows, SetNotices, SetFiles | no clipboard; popups drawn in-window; notices kept by the document; no file dialog |
A document, frame by frame
#include "UIContext.h" #include "UIStyle.h" #include "TextEngine.h" using namespace GGUI; UIContext context; StyleTable styles; TextEngine text; void Start() { styles.AddDefaults(); // the built-in widgets' looks text.Initialize(); const FontID body = text.LoadFont("fonts/Manrope-Regular.ttf"); text.SetDefaultStyle({ body, 14.0f }); context.SetStyles(&styles); context.SetTextMeasurer(text.Measurer()); context.SetGlyphSource(text.Glyphs()); context.Mount([](UI& ui) // stored; Update runs it { if (auto page = ui.Column({ .Gap = 6, .Padding = GGEdges::All(12) })) { ui.Label("Settings"); ui.Slider({ .Max = 100 }, Volume, [](float value) { Volume = value; }); } }); } void Frame(float seconds, std::span<const InputEvent> inputs, const BindingReader& reader) { context.SetSurface(WindowWidth / Scale, WindowHeight / Scale, Scale); for (const InputEvent& input : inputs) context.Dispatch(input); // hover, press, focus, and the handlers they run context.AdvanceTime(seconds); // tooltip delays, transitions, clips context.Update(); // run what changed; nothing if nothing did context.UpdateBindings(reader); // bound values; no function runs context.UpdateLayout(); // positions of what changed context.PublishIfChanged(); // a snapshot for whoever draws context.AdvanceFrame(); // lets destroyed nodes' slots be reused }
This is the order the engine's host uses, and the one that gives a click's effect in the next frame drawn. All of it runs on one thread, the document's update thread. Other threads change a document only through a MutationQueue (context.Apply(queue) at the start of the frame) or by bumping a GGRevision the document watches.
Data
Two ways for UI to follow data, as Foundations describes: a component that watches a GGRevision runs again when it is bumped, and a binding reads a value every frame with no function running. A host defines what a binding target and a field are:
// Targets are any 64-bit identity; fields are numbered by the host. BindingReader reader; reader.Context = &game; reader.Read = [](void* context, BindTarget target, FieldID field, bool& bValid) -> float { const Game& game = *static_cast<const Game*>(context); const Player* who = game.Find(target); bValid = who != nullptr; return who ? who->Values[field] : 0.0f; }; ui.Label(GGBind(playerTarget, GGExpr::Field(GoldField)), { .Notation = NumberNotation::Suffix }); ui.Progress({}, GGBind(playerTarget, GGExpr::Field(HealthField) / GGExpr::Field(MaxHealthField)));
An expression is data, not code, so it can be checked, shown by tools, and in world UI evaluated on the GPU. BindLambda covers what an expression can't, on the CPU only. Numbers too large for a float are BigFields, read through BindingReader::ReadBig.
Input
A host turns its platform's events into InputEvents in document space (UI units from the top left) and passes them to Dispatch on the update thread. A host that reads input on another thread decides there whether the UI takes an event, without touching the document: PointerRouter::Route and KeyRouter::Route answer from the last published snapshot, so input the UI doesn't want goes straight to the game.
Keys the UI gives a meaning to are UIKeys (arrows, Enter, Escape, Tab); every key also carries its physical GGKey, which input actions match (ui.Command(action, handler)). A gamepad's buttons arrive as the same keys (the d-pad as arrows, accept as Enter) with bGamepad set, so focus moves the same way.
Text and images
TextEngine shapes text once, keeps the result as a run, and packs glyphs into atlas pages as they are first drawn. It answers documents on several threads at once. Whoever draws reads its pages while holding the atlas still (TextEngine::AtlasHold), and uploads only what changed (TakePageChanges).
Images are ImageIDs that the host's ImageSource issues and describes: a texture, a region of it, and a size. A path given to ui.Image("ui/coin.png") is resolved through the source's Lookup.
Drawing
A published snapshot is everything needed to draw the document, and it stays valid for as long as a reader holds it, while the document moves on:
const SnapshotRead view = context.ReadSnapshot(); // any thread for (const uint32_t slot : view->Order.Slots()) if (const UIPrimitive* p = view->Primitives.Find(slot)) Draw(*p, view->Clips[p->Clip]); for (const UIPrimitive& p : view->Overlay) // scroll bars, caret, popups Draw(p, view->Clips[p.Clip]);
Every primitive is one 48-byte instanced quad (UIPrimitive, Paint.h) under a clip table entry (UIClip): rounded boxes, borders, glyphs, images, shadows, lines, triangles, and arcs. A renderer draws those and nothing else. Each snapshot also says which slots changed since earlier ones (GGSnapshot::Deltas), so a GPU copy is updated in place.
Without a GPU, RasterizeReference(*view, background, &textures) (ReferenceRaster.h) draws the same snapshot on the CPU with the shaders' maths. It is slow, and meant for tests and tools that need to see a document.
Widget assets without the engine
GGWidgetLibrary reads .tnxwidget, .tnxtable, .ggss, and .tnxcurve text that the host gives it; loading a file again is hot reload. What assets name outside themselves goes through resolvers the host may set: game state (SetContextResolver), widget classes (SetClassResolver), commands (SetCommandChecker), references by ID (SetReferenceResolver), and list items (SetItemSources). Without them, assets that use none of these work as they are; the rest are reported where they are written.
GGWidgetLibrary widgets; std::vector<GGWidgetProblem> problems; widgets.Load("StatBar.tnxwidget", ReadFile("StatBar.tnxwidget"), problems); context.SetWidgets(&widgets); context.SetWidgetActions([](void*, const GGWidgetAction& action) { Handle(action); }, nullptr); ui.Use("StatBar", { { "Value", GGBind(player, GGExpr::Field(HealthField)) }, { "Label", "Health" } });
Testing
A document runs headless, so UI is tested as code: build it, find nodes by text, press them, and check what happened. PressAt and FindText in Testbed/src/Tests/GGUI_Lists.cpp are the pattern most tests follow, and SetDiagnostics collects every authoring problem a build reports.
Part 2: GG-UI in Trinyx
In the engine, UIHost (src/Runtime/UI, TrinyxEngine::) is the host from Part 1. It owns a document per window, updates them each display frame on the Encoder (unrelated documents in parallel, on jobs), routes input from Sentinel, hands snapshots to the renderer, and owns what every document shares: styles, strings, the widget library, input actions, the dock, notices, and the file dialog. The file description of UIHost.h says which thread may do what.
A game rarely talks to UIHost directly. Its screens are HUDs (TnxUI), made of widget assets with widget classes behind them; editor tools are dock panels (Building an Editor Panel).
What the engine sets up
At startup the engine:
- reads the starter widgets (
Trinyx/assets/ui/widgets: bars, counters, rows, toasts) and every widget asset, table, sheet, and curve in the project'scontentfolder, and reads them again when saved; - reads string files from
Trinyx/assets/stringsand the project'scontent/strings(en.strings,de.strings, ...), the project's over the engine's; - keeps key bindings in the project's
Keybinds.ini; - sets the binding reader to entity fields, then view models, then engine statistics;
- in a game build, mounts the game screen in the main window, styled by the engine theme (
Trinyx/assets/ui/trinyx.ggss): every local player's HUD, and the stats overlay with--stats.
Shift+F2 opens the UI inspector on the game window, and Shift+F3 the console (--inspect, --console; both are turned off for a shipped build in the config).
Game data in UI
| Data | Bound with | Notes |
|---|---|---|
| A field of an entity | TnxBind(view, &CHealth<>::Value), or =Player.CHealth.Value in an asset | Read from the frame on screen; a changed value repaints, nothing runs |
| A value that lives in no component | a view model: TNX_, TnxViewModel<T>, TnxBind(model, &T::Field) | For display only: set from any thread |
| A list of entities, or records | "source": { "entities": "EUpgrade" }, or TnxUI:: | Each row gets its item as Item |
| A whole entity, for editing | EntityFields in a property grid | Edits go to the Brain as FieldEdits |
UI never writes simulation state. What a player asks for is a command (TNX_COMMAND): sent from code with TnxSim::Send, or from an asset with { "command": "Game.BuyUpgrade", "Tier": "=Item.CUpgrade.Tier" }. It goes into the player's input for the next simulated frame, so it is recorded, replicated, and replayed on rollback like a key press, and the Construct that handles it checks what it is asked.
A game with data of its own to bind sets a reader over the engine's before the engine runs, handling its targets and passing the rest on: GetUI().SetReader(MyReader(GetUI().GetReader())).
HUDs and widget classes
Everything on a player's screen belongs to their HUD: an asset of kind hud, laid out for the whole screen, with regions that widgets are added to while the game runs, and three layers above it (Menu, Modal, Toast). The code behind it is its widget class:
class PlayerHud : public TnxWidget<PlayerHud> { public: explicit PlayerHud(TnxContextOf<EPlayer<>> player) : Player(player) {} TnxContextOf<EPlayer<>> Player; // "=Player.CTransform.PosY" in the asset TnxInput<int32_t> Score; // "=Score" void Pause(); // { "call": "Pause" } TNX_WIDGET(PlayerHud, Player, Score, Pause) }; TNX_REGISTER_WIDGET(PlayerHud) TnxHud hud = TnxUI::SetHud<PlayerHud>(world, soul, player->Body); // shows PlayerHud.tnxwidget hud.Widget<PlayerHud>()->Score = 1200; // shown as it changes hud.Add("Notifications", "Toast", { { "Text", "Game.Saved" } }); // into a region
- The class says what its asset reads, is given, and calls, so all of it is typed and checked when the asset loads. The asset says how it looks and moves.
- A context the HUD can't do without is a constructor parameter: setting it without one doesn't compile.
- A member that is itself a widget class is the element of its name using an asset of that class, so a HUD is built of smaller classed widgets.
- A HUD lasts as long as its world by default;
SetLifetimekeeps it across levels or worlds.
Widget Assets is the full reference: inputs, contexts, slots, regions, lists, actions, styles, tokens, animation, and the designer.
Code widgets and code-built screens
Not everything is an asset. A widget that needs its own drawing or state is a code widget (GG_, UIWidgetTraits.h and UIWidgetRegistry.h) that assets then use like any element; the designer lists it under Code widgets. A tool or debug screen can also be built entirely in code and mounted in a window of its own with UIHost::.
Strings, looks, and input actions
- Text is a string ID. Add the project's text to
content/strings/en.strings(Game.Hud.Health = Health) and other languages beside it;GetUI().UseLanguage("de")switches every document. - Looks are style sheets. The engine theme gives the built-in widgets their colours as tokens; a project layers its own sheet over it, and an asset carries its own styles and tokens. Sheets reload when saved.
- Keys are input actions. Declare
GGInputActions, bind them withui.Commandor an asset's actions, and give playersui.KeyBindingon a settings page; their chords are saved inKeybinds.ini.
The widget designer
Double-clicking a .tnxwidget in the Content Browser opens it in the Widget Designer, with its panels docked around the canvas: Hierarchy, Palette, Details, Inputs and Contexts, Styles, Animation, Problems, and Cost.
- Layout is dragged on the canvas, snapping to edges and centres, and set exactly in Details.
- Inputs and Contexts picks the asset's class and gives the canvas preview values. The canvas makes an instance of the class, so what its code sets shows, and its methods run.
- Interact (I) hands the pointer to the widget as a player would use it; what its actions do is listed on the canvas.
- Animation keys properties on a timeline, plays curve assets, and fires event tracks.
- Preview from play: during a play session, a context can follow what the game gave a HUD, or a live entity.
- Saving writes the file; a running game picks it up at once.
Testing in the engine
Headless tests mount documents in a UIHost (host.Mount, host.UpdateAll) and check them as in Part 1; the TnxUI_* and TnxWidget_* tests set HUDs on a real world. The designer has a self-test (TNX_DESIGNER_SELFTEST) that edits an asset through the same paths its panels use.
Where Next
- A first HUD, step by step: First Steps with GG-UI
- Writing UI in code: Foundations, Core
- Focus, value widgets, popups, text, actions: Interaction
- Sheets, tokens, transitions: Styling
- Primitives, the UI pass, text and images in the renderer: Rendering
- The API reference: the GG-UI and Engine UI modules