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.

WhatHow it is givenWithout it
The surface: size in UI units, pixels per unitSetSurface(width, height, scale)nothing is laid out
LooksSetStyles(&styleTable)only text is drawn
Text measurement and glyphsSetTextMeasurer, SetGlyphSource (a TextEngine)a fixed metric: 8 units a character, 16 a line; text draws as blocks
Strings for the active languageSetStrings(&table)text is shown as written
ImagesSetImageSource(&source)images have no size and draw nothing
Data for bindingsa BindingReader, passed to UpdateBindings each framebound values don't change
Widget assetsSetWidgets(&library), and SetWidgetActions for what they doui.Use reports there is no library
Clipboard, popup windows, notices, file dialogsSetClipboard, SetPopupWindows, SetNotices, SetFilesno 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::GetUI()) 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's content folder, and reads them again when saved;
  • reads string files from Trinyx/assets/strings and the project's content/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

DataBound withNotes
A field of an entityTnxBind(view, &CHealth<>::Value), or =Player.CHealth.Value in an assetRead from the frame on screen; a changed value repaints, nothing runs
A value that lives in no componenta view model: TNX_VIEW_MODEL, TnxViewModel<T>, TnxBind(model, &T::Field)For display only: set from any thread
A list of entities, or records"source": { "entities": "EUpgrade" }, or TnxUI::Source("Shop.Offers", world, fill)Each row gets its item as Item
A whole entity, for editingEntityFields in a property gridEdits 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; SetLifetime keeps 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_REGISTER_WIDGET, 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::Mount.

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 with ui.Command or an asset's actions, and give players ui.KeyBinding on a settings page; their chords are saved in Keybinds.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