GG-UI — Widget Assets and the Designer

← GG-UI Design | UI Core | Styling | Interaction | Home

Game UI made as assets: small widgets combined into larger ones and then into a HUD, bound to game state by the assets themselves, styled at any level, animated, and edited in a designer. Code can still build any of it, and uses the same widgets.

Status: steps 1 to 7 are built, and the first part of step 8; the rest is designed. See StepsSteps.


The Model

PieceWhat it is
Widget assetA .tnxwidget file: a tree of elements, the inputs it takes, the slots it leaves, the contexts it reads, its styles and animations
ElementA built-in element (Row, Label, Progress, Button, ...), a registered code widget, or a use of another widget asset
InputA value a widget asset takes from whoever uses it: a number, text, a colour, an image
SlotA named place in a widget asset that whoever uses it fills with elements
ContextA typed source of game state the asset reads: an entity type, a component, or a view model
HUDThe root of what one local player sees: a widget asset of kind hud, with regions things are added to
RegionA named container in a HUD that widgets are added to while the game runs

A leaf widget knows nothing about the game: its parts bind to its inputs. A composed widget, usually the HUD, names the contexts it reads and wires its children's inputs to their fields. Game code's whole part is to supply the contexts when it sets the HUD.

{
    "widget": "StatBar", "version": 1,
    "inputs": { "Value": "number", "Max": { "type": "number", "default": 1 }, "Label": "text", "Fill": "color" },
    "tokens": { "bar-fill": "=Fill" },
    "root":
    { "name": "Bar", "type": "Column", "gap": 2, "width": "grow", "children": [
        { "name": "Caption", "type": "Label", "text": "=Label" },
        { "name": "Meter", "type": "Progress", "width": "grow", "height": 8, "value": "=Value / Max", "style": { "accent": "@bar-fill" } } ] }
}
{
    "widget": "PlayerHud", "class": "PlayerHud", "kind": "hud", "version": 1,
    "root":
    { "name": "Health", "type": "Place", "anchor": "BottomLeft", "x": 24, "y": -24, "children": [
        { "name": "HealthBar", "use": "StatBar", "Value": "=Player.CHealth.Value", "Max": "=Player.CHealth.Max", "Label": "Game.Hud.Health", "Fill": "@health" } ] }
}
// The code behind it: what it reads, is given, and calls (Widget Classes, below).
class PlayerHud : public TnxWidget<PlayerHud>
{
public:
    PlayerHud(TnxContextOf<EPlayer<>> player, TnxContextOf<MatchInfo> match) : Player(player), Match(match) {}
    TnxContextOf<EPlayer<>> Player;
    TnxContextOf<MatchInfo> Match;
    TNX_WIDGET(PlayerHud, Player, Match)
};
TNX_REGISTER_WIDGET(PlayerHud)

// The game, once the local player's body exists:
Hud = TnxUI::SetHud<PlayerHud>(world, localSoul, Player->Body, MatchState);

Nothing runs per frame after that. Bindings read the published frame as all UI does: a changed value patches its primitive, and the HUD is right across rollback, time travel, and play in editor without anything of its own.


Game State in Code

What assets build on, and what hand-written UI uses directly.

// Reading: a field of an entity, or of the entity a ConstructView shows, from the frame on screen.
ui.Label(TnxBind(hud.Wallet, &CWallet<>::Gold), { .Notation = NumberNotation::Suffix, .Decimals = 2 });
ui.Progress({}, TnxBind(Player->Body, &CHealth<>::Value) / TnxBind(Player->Body, &CHealth<>::Max));

// Acting: a command, sent from any thread, into the local player's input for the next frame.
struct BuyUpgrade
{
    uint8_t Tier = 0;
    TNX_COMMAND(BuyUpgrade, "Game.BuyUpgrade", Tier)
};
TNX_REGISTER_COMMAND(BuyUpgrade)   // so assets can send it by name
TnxSim::Send(world, BuyUpgrade{ .Tier = 2 });

class Shop : public Construct<Shop>
{
public:
    using Commands = TnxHandles<BuyUpgrade>;
    void OnCommand(const BuyUpgrade& buy, uint8_t ownerID); // on Brain, before PrePhysics
};
  • TnxBind names the field by member pointer, so a typo doesn't compile; a member that isn't in its component's field list is reported with the line. Bindings of one entity combine with arithmetic and comparisons; combining two entities is reported, since a binding reads one.
  • A command is a small trivially copyable struct (no pointers or strings) with a name and its fields listed. TNX_REGISTER_COMMAND records its name and layout, so an asset's { "command": "Game.BuyUpgrade", "Tier": 2 } is checked when it is built (a misspelt command or field is a problem naming the line) and is sent with its fields filled by name. Each player has 63 bytes of commands a frame, a command taking its size plus five; what doesn't fit goes in the next frame, in order.
  • Where it goes: into the player's input for the next frame the simulation runs. It is recorded in the frame's rollback record and replayed on resimulation, carried in the input window to the Authority, and kept in its input log. A frame the Authority has to predict has no commands. A paused or stalled frame doesn't take them, so none are lost.
  • Who gets it: each Construct listing its type, in the order sent, with the sending player's ownerID. The handlers are registered at compile time like ticks, and a listed command without an OnCommand doesn't compile. The Authority's run is the one that counts; an Owner's is its prediction. A handler checks what it is asked rather than trusting it.

Widget Assets

Data, built at runtime. A widget asset is JSON (the engine's format during R&D). Loading compiles it into a flat list of elements with their bindings, strings, and references resolved; building walks that list and calls the same ui.* functions code does. An asset costs what the same UI written in C++ costs.

  • Each element has a name, unique in its asset: the designer gives one (Label, Label2) and you rename it. Names, like every other name in a file (inputs, contexts, regions, tokens, classes, clips, commands), are TnxNames once loaded, so identity is an integer and the text stays readable. The name keys the element (so state, focus, and transitions stay with it), names it in animation tracks and messages, and lets a hot reload keep the tree. A dotted name reaches into an asset used inside: PlayerHud.HealthBar.Meter.
  • Each use of a widget asset is a component, with its inputs as its properties. A change to one instance's inputs re-runs that instance; the rest of the HUD is skipped.
  • Hot reload: saving an asset recompiles it and re-runs the documents that use it, in the editor and during play. A file that fails to load is reported with its line and the old version stays.
  • References to other assets (widget assets, images, sheets) can be by asset ID with the name beside it, as scene and prefab files write them: "use": { "guid": "3f2a...", "name": "StatBar" }. The ID finds the asset under its current name; the name is for reading and for an ID the engine doesn't know. A rename while running is followed: assets referring to the renamed one by ID are read again. A plain name works everywhere too.

The File

Written mostly by the designer, read in diffs, and fixed by hand now and then.

Rule
Any orderKeys and sections may come in any order, and a key added anywhere is read. The order of children is their layout order
ValuesPlain text is text ("Game.Hud.Health" is a string ID). "=..." is a formula: "=Value / Max", "=Player.CHealth.Value". "@name" is a token. "'=..." is text that starts with =
FormulasNames are inputs or context fields; numbers; + - * / < > <= >= == !=, &&, \|\|, !; min, max, clamp, lerp, select. They compile to GGExpr, so a formula runs wherever a binding does, on the GPU too. A formula reads from one entity or context, plus constants; a list's where and sort may read the item and other contexts together (Item.CUpgrade.Cost <= Player.CWallet.Gold), worked out on the CPU when the list refreshes
SizesA number (UI units), grow, grow 2 (a weight), fit, 50%
Inputs"Value": "number"; { "type": ..., "default": ... } when there is a default
A useAn asset's inputs are set as its properties: { "use": "StatBar", "Value": "=..." }. Inputs are PascalCase and built-in properties lowerCamel, so they never collide. Where it is used it takes width, height, layer, visible, and opacity, and its root fills that size; what lays out and styles its content (gap, padding, class, ...) is the asset's, and setting it on a use is reported
Notes"note" on any element, shown in the designer; // comments are read too
versionLets the loader read older files as the format grows
Old namesRenamed types and keys still load, each reported with its new name: Switch is Switcher, Selector is Carousel, layer is zOrder, an image's region is crop, shrink is minScale, and a Spacer's weight is its grow size. The designer writes the new names. colour, greyscale, and centre are read as color, grayscale, and center, which are written

Forgiving to read. The loader also takes trailing commas, // and /* */ comments, keys without quotes, and single words without quotes (type: Progress, width: grow). Saved files are plain JSON, with the comments that were written in them.

Errors say what to change, with the line: StatBar.tnxwidget line 7: there's no comma before "width", line 12: this ] closes a list, but the { from line 9 is still open: close it with } first.

Laid out as an outline when the designer saves: one element per line, children its last key, each child a tab deeper than its parent, closing brackets at the end of the last child's line. An element the designer didn't change keeps its keys in the order they were written, so a save shows only what changed. Hand-written files can be laid out any way.

Elements

KindElements
LayoutRow, Column, Stack, Grid, Place (anchored), Spacer, Scroll, SafeArea, ScaleBox
ChoosingSwitcher (one child by a bound value), Repeat (one item a bound number of times)
LeavesLabel, RichText, ActionPrompt, Image, Swatch, Shape, Progress, Viewport
ControlsButton, Toggle, Slider, Number, TextField, Dropdown, Carousel, Tabs
RepeatedList and Tiles over a data source, with an item widget asset
Use"use": "<widget asset>" with inputs and slot contents
Code widgetA widget written in C++ and registered with its properties, shown in the palette like the rest

Properties are the elements' existing props (BoxProps, TextProps, ...), reflected so the designer can list and validate them (The Widget RegistryThe Widget Registry). A property that changes layout (size, padding, text) may be bound in a screen document; in a world-UI template only paint properties may be (Core).

Controls take content. A Button is a container that is clickable and has the button's states: its children are its content (an icon, a label, a badge), laid out like a Row's. "text" alone is the plain labelled button. Toggles and tab headers work the same way.

{
    "widget": "IconButton", "version": 1,
    "inputs": { "Icon": "image", "Label": "text" },
    "root":
    { "name": "Button", "type": "Button", "class": "icon-button", "children": [
        { "name": "Icon", "use": "SpinningIcon", "Icon": "=Icon" },
        { "name": "Caption", "type": "Label", "text": "=Label" } ] },
    "triggers": [ { "when": { "hover": "Button" }, "play": "Wiggle" } ]
}

What a button does is set where it is used ("on": { "click": ... } on the use), so one asset serves every place.

{ "name": "Lives", "type": "Repeat", "count": "=Player.CHealth.Value / 20", "max": 10,
  "item": { "name": "Heart", "type": "Image", "image": "ui/heart.png" } },
{ "name": "Held", "type": "Switcher", "value": "=Player.CWeapon.Kind", "children": [
    { "name": "Sword", "use": "SwordIcon" }, { "name": "Bow", "use": "BowIcon" } ] },
{ "name": "Quality", "type": "Carousel", "options": [ "Game.Low", "Game.Medium", "Game.High" ],
  "value": "=Settings.Quality", "on": { "change": { "command": "Game.SetQuality" } } },
{ "name": "Buy", "type": "Button", "tooltip": "ItemTooltip", "tooltipInputs": { "Item": "=Item" }, "children": [ ... ] }
  • Switcher and Repeat build every case and item once and choose with a binding: the ones not shown are out of layout and drawing (Present, in code ui.Present; Visible hides but keeps the place), so a changing value runs nothing. A repeat's item reads its number as =Index; max is how many are built.
  • on.change is a value control's event (Toggle, Carousel): each of its actions is given the new value as the field Value, unless it sets one.
  • tooltip names a widget asset shown as the element's tooltip, with tooltipInputs.
  • SafeArea keeps its content inside the platform's safe area (a notch, rounded corners, overscan).
{ "name": "Health", "type": "Progress", "value": "=Player.CHealth.Value / Player.CHealth.Max", "direction": "left",
  "trailing": { "delay": 0.4, "time": "=Player.CHealth.RegenDelay" } },
{ "name": "Ammo", "type": "Progress", "value": "=Gun.CAmmo.Loaded / Gun.CAmmo.Size", "segments": "=Gun.CAmmo.Size" },
{ "name": "Cooldown", "type": "Progress", "class": "ring", "value": "=Ability.CCooldown.Left", "radial": { "start": 0, "sweep": 360 } },
{ "name": "Frame", "type": "Image", "image": "ui/panel.png", "slice": 12, "width": "grow", "height": 120 },
{ "name": "Portrait", "type": "Image", "image": "ui/faces.png", "crop": [ 64, 0, 64, 64 ], "grayscale": "=Locked" },
{ "name": "Quest", "type": "Label", "text": "=Title", "wrap": true, "maxLines": 2, "ellipsis": true },
{ "name": "Name", "type": "Label", "text": "=Name", "minScale": 0.7 },
{ "name": "Timer", "type": "Shape", "shape": "arc", "class": "ring", "sweep": "=Ability.CCooldown.Left * 360" }
  • Progress fills toward direction, in segments (pips), or round a ring (radial, its thickness the style's border-width), with a fillImage cut to the value. A trailing fill follows the value down after delay seconds, over time seconds; both may be bound, and a new drop restarts the wait. It is the style's trail colour, or the accent at half strength.
  • Image: slice (nine-slice borders in the image's pixels), tile, crop (a cell of a sheet, whose size is the element's natural size), flip (x, y, both), grayscale (bindable).
  • Label: maxLines and ellipsis cut text to fit, keeping the whole text for screen readers; minScale draws a line too wide for its place smaller, down to that fraction, before cutting it. Text over the scene takes text-outline and text-shadow from its style.
  • Shape: line and polygon (points), circle, rect, and arc (start, sweep in degrees from the top), filled or with a stroke. An arc whose sweep is bound follows it with no rebuild.

What the Elements Do

The elements a HUD is made of, and what each needs beyond what code-built UI has today:

ElementDoes
ProgressA fill by a bound value: left, right, up, or down; radial (start angle and sweep, for cooldowns and stamina wheels); segments (a bound count of pips or notches); a fill image or sprite masked by the value; a trailing fill that follows the value down after a delay (health lost); a tween on the value
ImageAn image, atlas region, or render target: fit modes, tint, corner radius, slice (nine-slice borders that don't stretch), tile, a bindable crop (portraits, sheet cells), flipping, the flipbook frame, and grayscale (a locked item)
LabelText from a string ID, data, or a bound number: wrap, maxLines, ellipsis, minScale (scaled down to fit, to a minimum size, for long translations), and text outline and shadow styles for text over the scene
RichTextStyled parts, inline images, and action prompts in one paragraph, from markup in the string (Rich TextRich Text)
ActionPromptAn input action's key, or its gamepad button while a gamepad is in use; it follows a rebind and the device. RichText embeds it inline
ShapeA line, arc, circle, polygon, or rounded rectangle from data, with bindable parameters: an arc whose sweep is a cooldown is a radial timer. Code still uses ui.Canvas
ViewportA render target laid out like an image: a minimap, a character preview, an item inspected
SwitcherShows one child, chosen by a bound value (which weapon icon, which state's panel), with an optional clip between them
RepeatOne item a bound number of times: hearts, ammo pips, stars
SafeAreaKeeps its content inside the platform's safe area, at any aspect
ScaleBoxDraws its content at its own layout, scaled to fit the box and centred: "fit" is contain (the default), width, or height. Nothing is laid out again when either size changes
CarouselOne of a list of options, changed with left and right ("< Medium >"): the gamepad form of a dropdown
List, TilesItems from a data table, a game's source, or every entity of a type, each built from a widget asset with the item as its context; filtered, sorted, selected, and activated (ListsLists)
TabsHeaders take content; switched with the action bound to next and previous tab (shoulder buttons by default)

Any element can name a widget asset as its tooltip. The show action adds an asset to a region or layer of the HUD (Menu, Modal), or as a popup beside an element.

The Widget Registry

Every element type is in one registry (GGWidgetRegistry, UIWidgetRegistry.h): the built-in ones and the code widgets a game adds. The registry holds each type's properties, with each property's file key, kind, default, and Details group. The loader checks files against it, and the designer builds its palette and Details from it.

  • Every element takes name, note, visible, opacity, zOrder, tooltip, tooltipInputs, and style. A leaf is ordered, faded, and given a tooltip just as a box is.
  • And a transform: "rotation": 12 (degrees, clockwise), "scale": 1.2 or [1.2, 1], "skew": 8 or [8, 0] (degrees: its top leaning right, its right side dropping), and "pivot": [0.5, 1] (what it turns, skews, and scales about, as fractions of its size). It is drawn scaled, then skewed, then turned, with its content, and nothing is laid out again; hit testing follows it. A clip's tracks add to it.
  • A key that does nothing is reported. A key the file knows, set on an element that ignores it (a gap on a Label), is a problem that lists the keys that element does take. A key the file doesn't know is reported with the nearest one it does know.
  • Properties are reflected from the props structs. GG_REGISTER_FIELDS (UIReflect.h) lists a struct's fields, as TNX_REGISTER_FIELDS does for components. A field's key is its name (bWrap is wrap, MaxLines is maxLines), its kind comes from its type, and its default comes from a default-made struct. Fields that are not listed, such as handlers, belong to code only.

Code Widgets

A widget written in C++ is a props struct with reflected fields and a function that builds it, registered once in a .cpp:

struct GaugeProps
{
    float Value    = 0.0f;
    GGColor Needle = GGColor::White();
    bool bTicks    = true;
    GG_REGISTER_FIELDS(GaugeProps, Value, Needle, bTicks)
};
void BuildGauge(UI& ui, const GaugeProps& props) { ... }
GG_REGISTER_WIDGET(Gauge, GaugeProps, BuildGauge)

An asset then writes it as any other element, { "name": "Speed", "type": "Gauge", "value": 0.5, "needle": "#FF0000", "ticks": false }. Its fields are read into its props, and a wrong key or value is reported with the field's name. It is built in a box of its own, so it takes everything every element takes. The designer lists it under Code widgets. A field's type needs GGFieldTraits (UIWidgetTraits.h): numbers, bool, std::string, GGSize, GGEdges, GGColor, and the layout enums have them.

Widget Classes

A widget asset is its layout, look, and animation. What it reads, is given, and does is a class's: the code behind it, named in the file ("class": "PlayerHud").

class PlayerHud : public TnxWidget<PlayerHud>
{
public:
    explicit PlayerHud(TnxContextOf<EPlayer<>> player) : Player(player) {}

    TnxContextOf<EPlayer<>> Player;      // "=Player.CHealth.Value"; an entity type, or a view model
    TnxInput<float> Gold;                // "=Gold"; float, int32_t, bool, std::string (a string ID), GGColor
    StatBar Health{ Player };            // { "name": "Health", "use": "StatBar" }: a widget member
    void OpenMap();                      // { "call": "OpenMap" }
    void Buy(const ShopOffer& offer);    // { "call": "Buy" } in a row of a list of offers

    TNX_WIDGET(PlayerHud, Player, Gold, Health, OpenMap, Buy)
};
TNX_REGISTER_WIDGET(PlayerHud)

TnxHud hud = TnxUI::SetHud<PlayerHud>(world, soul, Player->Body);   // its asset, PlayerHud
hud.Widget<PlayerHud>()->Gold = 1200.0f;                             // shown as it changes
  • One list names what an asset sees (TNX_WIDGET), as TNX_VIEW_MODEL and TNX_COMMAND do; what each is comes from its type. An asset naming the class has its contexts and inputs, and checks its calls against it, when it loads: a method it hasn't got is reported with the ones it has. The file declares neither.
  • A context it can't do without is a constructor parameter, so setting the HUD without it doesn't compile. One it can is a plain member, and an empty one is reported by name when the HUD is set (TnxUI::HudProblems).
  • A call runs the method on the UI update, on the instance the asset was shown with (a HUD's). It takes nothing, or the item of the list row it was called from: a view model's record, or an EntityHandle. To change game state it sends a command (TnxSim::Send); UI never writes state.
  • An input that is a number is read as it changes, with nothing built again; text and colour build the widget again when set. Set from any thread.
  • A widget member is a member that is itself a widget class. It is the element of its name that uses an asset of that class ({ "name": "Health", "use": "StatBar" }): that element is given what the member has, and its calls run on the member. Its contexts come from its owner's constructor, so a nested widget can't be left without one either, and the file gives it nothing. A use of an asset with a class that isn't a member of its user's class, or is in a widget with no class, is reported with the members there are.
  • A widget with no class is presentational: it is given inputs ("inputs" in its file), and a list's row its Item. Anything that reads game state or calls code has a class.
  • The designer reads the class and runs it. Inputs and Contexts picks it and shows what it declares, its methods, and its widget members; the actions editor offers its methods. The canvas makes an instance with the preview contexts when its constructor takes only contexts, shows what its code sets under the preview values, and runs its methods when its actions call them, in Interact and on event tracks. A change to the class takes a build and an editor restart until game code reloads (Code Reload).

Inputs

"inputs": { "Value": "number", "Label": "text", "Icon": "image", "Locked": { "type": "bool", "default": false } }

Types: number, bignumber, integer, bool, text (a string ID, or verbatim data), color, image, widget (an asset to use, for a part a user can swap). Whoever uses the asset sets each input to:

FormExample
A literal"Label": "Game.Hud.Health", "Max": 100
A context field"Value": "=Player.CHealth.Value"
Its own input"Value": "=Value"
A formula"Fill": "=Player.CHealth.Value / Player.CHealth.Max"
A token"Fill": "@health"

A missing input takes its default; an input with no default that isn't set is a validation error.

Slots

{
    "widget": "Panel", "version": 1,
    "inputs": { "Title": "text" },
    "root":
    { "name": "Frame", "type": "Column", "class": "panel", "children": [
        { "name": "Title", "type": "Label", "class": "panel-title", "text": "=Title" },
        { "name": "Content", "slot": true } ] }
}
{ "name": "Shop", "use": "Panel", "Title": "Game.Shop.Title", "slots": { "Content": [
    { "name": "Upgrades", "use": "UpgradeList" } ] } }

A slot left empty shows its default children, if the asset gives any.

Lists

{ "name": "Abilities", "type": "List", "source": { "table": "Trainer.Abilities" }, "item": "AbilityRow",
  "where": "=Item.Level <= Level", "sort": "-=Item.Cost", "selection": "Chosen",
  "on": { "select": { "call": "Preview" },
          "activate": { "command": "Game.LearnAbility", "Ability": "=Item.Id" } },
  "slots": { "empty": [ { "name": "None", "type": "Label", "text": "Game.Trainer.NothingToLearn" } ] } },
{ "name": "Details", "use": "AbilityDetails", "Ability": "=Chosen" }
  • source is where the items come from:
SourceItemsWritten
A data tableIts rows: fixed catalogues (a trainer's abilities, a vendor's stock){ "table": "Trainer.Abilities" }
A game's sourceWhat the game adds: entities, and records (view models)"Shop.Offers"
EntitiesEvery entity of a type in the HUD's world, kept current as they spawn and go{ "entities": "EUpgrade" }
  • item is the widget asset each item is built from. It declares the context Item (the table, entity type, or view model) and reads it like any context: =Item.Cost, =Item.CUpgrade.Tier.
  • where shows an item while it is true; sort orders them by a formula, ascending ("-=..." for the highest first). Both are formulas over Item and the asset's inputs and contexts. Values that change are read again a few times a second, and the list reorders only if the order changed.
  • selection names a context for the selected item, which the rest of the asset reads (=Chosen.Cost) or passes on (=Chosen). Nothing selected reads as nothing. A selected item that no longer shows is unselected.
  • Events: select when the selection becomes an item; activate on a double click, or Enter (a gamepad's accept) on the focused item. Their fields read Item.
  • Slots: header and footer around the items, empty instead of them when there are none.
  • Keys: every item has one: a table's key field, a record's first field, an entity's handle. Selection and focus follow the key, so they survive a re-sort, a reload, or a refill, and only an item whose key comes or goes is built or removed. Actions send Item.Id, never a position, so a command means the same on the Authority whatever order its list is in.
  • Sizes: rowHeight makes every row that high and builds only those in view, for long lists. Tiles take tileWidth and tileHeight and are always built that way.

A data table (.tnxtable, listed and created in the Content Browser) is rows of named values, read as constants:

{ "table": "Trainer.Abilities", "key": "Id",
  "fields": { "Id": "name", "Class": "integer", "Level": "integer", "Cost": "number", "Icon": "image", "Name": "text" },
  "rows": [
    { "Id": "Fireball", "Class": 1, "Level": 1, "Cost": 10, "Icon": "icons/fireball.png", "Name": "Game.Ability.Fireball" } ] }

Fields are number, integer, bool, text (a string ID), image (a path), name, and color. A name is an identifier: a key, compared and shown as itself, and sent in a command as its TnxName ID, into a uint32_t field. What changes while the game runs (learned, affordable) is a formula over the row and live data: "grayscale": "=Item.Cost > Player.CWallet.Gold".

A game's source is registered in code, per world, and filled again when it changes:

struct ShopOffer
{
    uint32_t Id  = 0;
    float Price  = 0.0f;
    bool bSoldOut = false;
    TNX_VIEW_MODEL(ShopOffer, "Game.ShopOffer", Id, Price, bSoldOut)
};
TNX_REGISTER_VIEW_MODEL(ShopOffer)

TnxUI::Source("Shop.Offers", world, [this](TnxItems& items)
{
    for (const ShopOffer& offer : Offers) items.Add(offer);   // records
    for (EntityHandle crate : Crates) items.Add(crate);       // or entities
});
TnxUI::SourceChanged("Shop.Offers");   // any thread: what it would add is different

The fill runs on the thread that calls Source or SourceChanged. A record is kept as a view model from one fill to the next while its first field is the same, so a changed price repaints in place, with no rebuild. A record's fields are what a view model holds: numbers, flags, enums, and Fixed32; its text comes from a table or a string ID the item asset chooses by a field.

ui.List and ui.Tiles in code take OnSelect and OnActivate the same way.

Rich Text and Prompts

The markup is in the string tables, where translators can move it with the words:

Game.Shop.Deal = Deal <class damage>{amount}</class> <img Coin/> damage
Game.Hint.Buy  = Press <action Game.Buy/> to buy for <class price>{cost}</class>
{ "name": "Hint", "type": "RichText", "text": "Game.Hint.Buy", "args": { "cost": "=Item.Cost" }, "notation": "suffix" },
{ "name": "Prompt", "type": "ActionPrompt", "action": "Game.Buy" }
ui.RichText(GGText("Game.Hint.Buy", { { "cost", TnxBind(item, &CUpgrade::Cost) } }));
ui.ActionPrompt(BuyAction);
{ "name": "Help", "type": "RichText", "text": "Game.Help.Intro",
  "links": { "Wiki": { "call": "OpenWiki" }, "Shop": { "show": "ShopPanel", "in": "Menu" } } }

ui.RichText(GGText("Game.Help.Intro"), { .OnLink = [](const TnxName& link) { ... } });

MarkupIs
<class name>...</class>What it holds, in that style class: colour, font, size, and a background with its padding. Classes nest
<link Name>...</link>A link: what it holds, in the link class, styled in its own state (link:hover, link:focus). What it does is the element's links entry for Name, or RichTextProps::OnLink in code
<img Name/>An image, by its texture asset's name, as high as the line of text it is in
<action Name/>An input action's prompt, as ActionPrompt
{name}An argument. Up to four may be bound: a change lays the paragraph out again and re-runs nothing
<<A literal <. An argument's own < is never read as markup

The paragraph is shaped as one: its parts wrap and break into lines together, a part with a bigger font makes its line taller, and right-to-left text keeps its order. wrap, maxLines, ellipsis, and minScale work as they do on a Label. A mistake (a tag it doesn't have, an image or action that doesn't exist, a <class> never closed) is reported once, with the string ID, and the text shows as written. A screen reader is given the plain text with each prompt's key or button, and without the images.

A paragraph with links is clicked where a link is, and takes focus: Left and Right (or the gamepad's directions) move between its links before focus moves on, and Enter or the gamepad's accept activates the one it is on. Its parts' classes, links included, are in the style scopes the paragraph is in.

A prompt shows the action's key on a key-cap. While a gamepad is in use it shows the action's button: the game's picture of it if there is one, a texture asset named Pad.<Family>.<Button> (Pad.Xbox.South, Pad.PlayStation.North, Pad.Nintendo.East, Pad.Generic.Start) in a pad-glyph, else its label on a pad-cap (A, B, X, Y; ✕, ○, □, △; B, A, Y, X; LB, R1, ZL, ...). The device in use is whatever was last pressed: a gamepad button, or a key or the mouse. A prompt follows it and any rebind, with no re-run. An action's gamepad button is part of the action (Interaction):

inline constexpr GGInputAction BuyAction{ "Game.Buy", "Game.Action.Buy", "Game.Action.Buy.Tip", GGChord(GGKeys::E), {}, GGPad::South };

HUDs and Regions

Everything on a player's screen belongs to that player's HUD: placed in it, or added while the game runs to a region it declares. Nothing is shown loose, so every widget has a known parent, layer, owner, and lifetime.

{
    "widget": "PlayerHud", "class": "PlayerHud", "kind": "hud", "version": 1,
    "root":
    { "name": "Screen", "type": "Stack", "children": [
        { "name": "Health", "type": "Place", "anchor": "BottomLeft", "children": [
            { "name": "HealthBar", "use": "ResourceBar", "Value": "=Player.CHealth.Value" } ] },
        { "name": "Notifications", "type": "Column", "region": true, "anchor": "TopRight", "max": 4, "accepts": [ "Toast", "QuestUpdate" ] },
        { "name": "Markers", "type": "Stack", "region": true } ] }
}
Hud = TnxUI::SetHud<PlayerHud>(world, localSoul, Player->Body);
const TnxWidgetRef note = Hud.Add("Notifications", "QuestUpdate", { { "Title", quest.Name } });
Hud.Remove(note);   // or it removes itself, with a hide action
  • A HUD is made as one asset (the Content Browser's New > HUD): the whole screen, laid out in the designer on the devices it will run on, with the regions things are added to. Its class (Widget Classes) is what it reads and calls.
  • Setting it can't leave a context out: the ones it can't do without are its class's constructor parameters. TnxUI::SetHud<T> makes the class and shows the asset of its name (SetHudAs<T> another asset of that class); TnxUI::SetHud by name shows a HUD with no class. Either says at once what is wrong (TnxUI::HudProblems): an asset that isn't a HUD, one of another class, or a context left empty.
  • One HUD per local player (each Soul in its world), set with TnxUI::SetHud. Split screen gives each player theirs, in that player's part of the screen: the local players of one world share it, two one above the other and three or four in quarters, and a game whose views split another way places each with TnxHud::SetArea. A HUD fills its part and is clipped to it.
  • Its lifetime is a ConstructLifetime tier (TnxHud::SetLifetime): World, the default, goes with its world; Level also with the level; Session outlives its world, waits unshown for the next one (where TnxHud::OnNewWorld gives its class the contexts of the new world), and goes when its player leaves; Persistent only when it is removed.
  • Where it is drawn: TnxUI records each HUD; whatever draws a player's view builds it over the image (TnxUI::BuildHuds). The game window's document does so for every local player; in the editor, each play view shows its world's, so play in editor shows every player's HUD as they see it and follows edits to its assets.
  • A region is a container element the HUD's author places and names ("region": true): it arranges what is added as its type does (a Column stacks them down, a Stack on top of each other), with an optional max (the oldest goes; 1 replaces) and an optional accepts list of widget assets. Adding to a region the HUD hasn't got, or a widget it doesn't accept, is reported naming both.
  • Added widgets get their inputs and contexts from whoever adds them, as a use does, and leave with an action ({ "hide": "self" }) or Remove. The show and toast actions name the region they add to.
  • Layers every HUD has, bottom first: Game (the HUD itself), Menu, Modal, Toast. A widget in Modal takes input focus and blocks what is under it; closing it gives focus back. A HUD can style and place these layers but not remove them, so menus and dialogs behave the same in every game.
  • World elements are the exception. A nameplate or an in-world panel belongs to its entity (CWorldWidget), not a HUD, and comes and goes with it. Screen markers are HUD elements, added to a region like Markers.
  • In the designer, regions show placeholder content so a layout can be judged full: under Inputs and Contexts, each region is given copies of an asset it accepts, as many as it holds, or a placeholder that names it. The copies get the inputs typed for them there, as the game's Add would give them, and any of the HUD's contexts they read by the same name.

Contexts

A context is a typed source the asset reads, a member of its class (TnxContextOf<T>). Field paths are Context.Component.Field, picked in the designer from the context type's reflected components, so they are never typed by hand.

Context typeReadsGiven as
Entity type (EPlayer)Any of its components' fieldsAn entity handle or a Construct's view
Component (CMatchState)That component's fieldsAn entity that has it
View modelValues a game registers for display onlyThe view model
  • Reading goes through the host's binding reader at the world's published read frame, as the editor's property grid does (Core). Nothing is copied or polled.
  • Passing down: a composed widget with a class is a widget member of its user's class, whose constructor gives it its contexts (TargetFrame Target{ Player };). A leaf widget usually has no class and takes inputs.
  • List items get their item as their context, Item: an upgrade list over the upgrade entities gives each item widget its upgrade (ListsLists

). A row with no class may say what its item is ("contexts": { "Item": "Game.Offer" }), so what it reads is checked; it is the one context such a widget has.

  • World-UI templates have one context, the entity they are drawn for, so an asset that passes the template rules is also a nameplate (World UI).
  • View models hold what has no component: settings, connection state, numbers computed for display. Gameplay state stays in components, where it serializes and rolls back.
struct ConnectionInfo
{
    float PingMs    = 0.0f;
    int32_t Players = 0;
    TNX_VIEW_MODEL(ConnectionInfo, "Net.Connection", PingMs, Players)
};
TNX_REGISTER_VIEW_MODEL(ConnectionInfo)

TnxViewModel<ConnectionInfo> Connection;
Connection.Set(&ConnectionInfo::PingMs, 42.0f);                    // any thread
Connection.Update([](ConnectionInfo& info) { info.Players = 3; });
ui.Use("Lobby", { { "Net", TnxContext(Connection) } });           // "=Net.PingMs"; Lobby's class has TnxContextOf<ConnectionInfo> Net
ui.Label(TnxBind(Connection, &ConnectionInfo::Players));

A view model has no components, so its paths are Context.Field. Its values are read through the same binding reader as entity fields: a change repaints what shows it, with no rebuild and nothing polled. Fields are numbers, flags, enums, and Fixed32. A destroyed view model reads as gone.

  • The context is gone (the entity was destroyed, the world reset): its bindings read as gone and their elements show their default, as entity bindings do today. Shown documents follow their ConstructLifetime tier (Lifetimes).

Validation

When an asset loads, every path, input, slot, region, and reference is resolved; when it is set or added, the contexts given are checked against what it declares. Errors name the asset, the element, and the property, and the rest still builds:

PlayerHud.tnxwidget line 6, PlayerHud.HealthBar.Value: Player.CHealth.Value: EPlayer has no CHealth
SetHud(PlayerHud): its class gave it no Match (a TnxContextOf left empty)
PlayerHud: Add("Notes", QuestUpdate): no region "Notes"; the HUD has Notifications, Markers
UpgradeRow.tnxwidget line 4, UpgradeRow.Name.text: there is no input "Name"; the inputs are Title, Cost

A component field renamed in code shows up as one of these at load, not as a bar that silently reads zero.


Actions

What an element does when used. No logic is written in a widget asset; a visual language for that comes later.

ActionExampleDone by
A sim command{ "command": "Game.BuyUpgrade", "Tier": 2 }The simulation, at a fixed step: recorded, replayed, rolled back, and sent to the Authority
A UI action{ "show": "ShopPanel", "in": "Menu" }, { "show": "Confirm", "in": "Modal" }, { "hide": "self" }, { "tab": 2 }, { "play": "Pulse" }, { "toast": "Game.Saved" }The HUD
A toast{ "toast": "Game.Saved" }, { "toast": "Game.LevelUp", "for": 5 }, { "toast": "Game.Quest", "in": "Notifications", "use": "QuestToast" }The HUD: the text for for seconds (3 if not said) in the Toast layer, where toasts stack, or the region in names; built with the Toast asset, or the one use names, given the text as its Text; a label in the toast class if there is none
A call{ "call": "OpenMap" }A method of the widget's class (Widget Classes), on the UI update; given the row's item in a list
{ "name": "Buy", "type": "Button", "text": "Game.Shop.Buy", "on": { "click": { "command": "Game.BuyUpgrade", "Tier": "=Item.CUpgrade.Tier" } } }

A command never writes state from the UI: it enters the sim's input stream like a key press. Its fields are set by name beside it, to literals or to values read when it is sent. Commands and their fields are registered in code (step 1), so a misspelt command or field is a validation error.

Drag and drop: an element can be a drag source carrying its context (an inventory item) and a drop target for a context type. A drop is an event like a click, usually a command ("on": { "drop": { "command": "Game.MoveItem", "Item": "=Dropped.Item", "Slot": 3 } }), and the target has the :drop state while something it accepts is over it.


Styling

Every element is styled by class, as code-built UI is (Styling). An asset is also a style scope: its sheets, styles, and tokens reach everything it builds, including the assets it uses.

{ "widget": "StatBar", "version": 1,
  "sheets": [ "StatBar.ggss" ],
  "tokens": { "bar-fill": "#3FB950", "bar-back": "#1B1F28" },
  "styles": {
    "bar":       { "fill": "@bar-back", "accent": "@bar-fill", "radius": 3 },
    "bar:hover": { "border": "1 @edge-bright" },
    "caption":   { "color": "@text-soft", "font-size": 12 } },
  "root": { ... } }
{ "name": "Health", "use": "StatBar", "Value": "=...", "tokens": { "bar-fill": "@danger" } },
{ "name": "Mana",   "use": "StatBar", "Value": "=...", "tokens": { "bar-fill": "#4C8DFF" } },
{ "name": "Notch",  "type": "Row", "class": "bar", "style": { "radius": 7 } }
WhereWritten asReaches
The asset"sheets": sheet files, in order; "styles": rules as in a sheet, over the sheets; "tokens": its tokens' valuesEverything it builds, and the assets it uses
A use"tokens"That instance
An element"style": properties as in a sheetThat element
Codeui.Tokens({ { "bar-fill", colour } }) in an element's content; UseProps::TokensThat element and everything inside it
  • Selectors, properties, and values are a sheet's (Styling), with tokens written @name. A style takes values and tokens, not formulas.
  • What an outer author writes wins over what an inner one does. A HUD's styles restyle every StatBar in it by class, and a token the HUD sets is used by every bar in it, over the bar's own value. Within one author, its styles are over its sheets, the tokens on a use over its own tokens, and an element's style over its other rules. Code is outside every asset, so ui.Tokens and UseProps::Tokens win over them all, the nearer over the farther.
  • An asset's tokens are its defaults. It declares the tokens it is recoloured by and their values; whoever uses it sets them, for one instance or for everything inside a composed widget.
  • A scope is who built the element, not where it sits. What a HUD puts in a StatBar's slot is styled as the HUD's. There are still no ancestry selectors: lookup is (scopes, classes, part, state), a table lookup.
  • A token's value can be a formula, on the asset or on a use, worked out per instance: over inputs ("bar-fill": "=Fill"), or over game data, which it then follows: "=Player.CHealth.Value * 2" for a number, "=select(Player.CHealth.Low, '@danger', '@ok')" for one of two values (tokens, colours, or numbers) as the condition is true or not. When it changes, the looks using it repaint and nothing is built again. Four tokens per use, and four of the asset's own, can follow game data.
  • Popups an element opens, menus and tooltips, are built in the scopes the element is in, and a widget's own parts (a text field's text) are in the widget's.
  • Sheet files are read from the widget folders with the assets and hot-reloaded with them. A sheet is named by its file name or the end of its path.
  • The inspector's style explanation names the asset, use, or ui.Tokens each value came from.

Animation

Named clips in the asset, played by actions or triggers.

"animations": {
    "Spin": { "length": 1.2, "loop": true, "rate": "=Speed", "tracks": [
        { "element": "Icon", "property": "rotation", "keys": [ [0, 0], [1.2, 360, "linear"] ] } ] },
    "Pulse": { "length": 0.35, "tracks": [
        { "element": "Meter", "property": "scale", "keys": [ [0, 1], [0.12, 1.15, "ease-out"], [0.35, 1] ] },
        { "element": "Meter", "property": "opacity", "keys": [ [0, 1], [0.12, 0.7], [0.35, 1] ] } ] } },
"triggers": [ { "when": { "rises": "Value" }, "play": "Pulse" } ]
  • Tracks animate an element's opacity, offset-x, offset-y, scale, scale-x, scale-y, rotation (degrees, clockwise), skew-x, and skew-y (degrees), which apply to it and everything in it, and its fill, accent, color, border, and radius. A key is [time, value] or [time, value, ease] (linear, ease-in, ease-out, ease-in-out, the default, or step), the ease shaping the way into it; a colour key is #RRGGBB or a token.
  • Nothing is laid out while a clip plays. A transform turns and scales the element and its content as a group about its centre, rewriting one clip entry and no row or primitive; a colour or opacity repaints the element, as a transition does. What a clip sets lies over what the builder and bindings set: an opacity multiplies, an offset adds, a scale multiplies, a rotation adds, a colour or radius replaces.
  • Clips play once, "loop": true forever, "loop": 3 three times, or "loop": "bounce" back and forth, at a rate that can be bound: a working spinner can speed up with progress. A clip that ends keeps its last values; one stopped lets go of them.
  • Triggers: "shown" (when the asset first appears), "hidden" (a HUD widget being removed, which stays until its clip has played), { "rises": "Value" } and falls, changes (an input or a formula, read each frame), and { "element": "Buy", "enters": "hover" } or leaves (hover, press, focus, checked), so an icon wiggles when the button holding it is hovered. Each runs "play" or "stop".
  • Actions: { "play": "Pulse" } and { "stop": "Spin" } act on the nearest asset with that clip, and { "play": "Pulse", "in": "Health" } on the asset used as the element Health.
  • Curve assets: a track can play a curve asset (.tnxcurve) in place of keys of its own: { "element": "Meter", "property": "offset-x", "curve": "Shake", "scale": 3, "length": 0.25 } plays Shake's values times scale, stretched to length seconds (its own length if none). A curve's keys are [time, value], reached along a cubic with automatic tangents, or with a third part: an ease word, or the key's tangents, { "tangent": "auto" | "user" | "broken" | "flat", "in": slope, "out": slope } (value per second). An automatic key is smooth through its neighbours and level at the ends and at a peak or a trough, so a curve never overshoots its keys. Curves are the foundation's (Curve.h, CurveFile.h): the engine's CurveTable takes the same assets (CurveTable::Register), and a curve evaluates the same in a widget and in the engine. Numbers only: a colour track has keys of its own.
  • Event tracks are what a clip does at a time: { "element": "Coin", "events": [ [0.3, { "call": "PlayCoin" }], [0.8, [ { "play": "Sparkle", "in": "Icon" }, { "toast": "Game.Bonus" }]] ] }. Each is [time, action], the action (or a list of them) written as in on, about the element named (what self and in mean), or the asset's root if there is none. Each runs once as playing passes it, either way when bouncing, and again each loop; posing a clip runs nothing. A clip's length reaches its last event. In code, GGClip::Events with the actions given to UIContext::DeclareClip, and UIContext::RunClipEvents for a tool scrubbing across them.
  • Tween: "tween": 0.4 or { "time": 0.4, "ease": "linear" } on a Label with a bound number (it rolls up) or a Progress (it slides).
  • In code: ui.Clip(name, GGClip) on the element being built into, UIContext::Play and Stop, ui.Tween, BoxProps::Transform, ui.Rotation, ui.Scale, and ui.ScaleBox.
  • What an animation names must be built by the asset itself: an element inside a list's items or another asset's slot isn't reached by name.
  • Flipbooks: an Image on a sprite sheet has a frame property, animated like any other track (a turning coin, a flickering fire). It uses the sprite sheets of step 11.
  • Time is the document's clock: reduced motion makes clips end at once, and a sim-bound document plays them identically on every resimulation.

The Designer

An editor window for widget assets, built from the editor's own panels:

PartWhat it does
PaletteBuilt-in elements, the engine's starter widgets, the project's widget assets, registered code widgets
HierarchyThe element tree; drag to reorder and reparent; slots shown as places to drop
CanvasThe asset as it will look: resolution and aspect presets, safe-area overlay, UI scale, language
DetailsThe selected element's properties in the property grid; each can be a literal, a binding (a context field picker), an input, or a token
Inputs and contextsDeclared here, with preview data for each so the asset looks right without the game
StylesThe asset's sheet and tokens, and where each shown value came from
AnimationsClips on a timeline, keys per element and property
ProblemsValidation errors, clickable to the element
CostElements, bindings, and build time per asset and per instance

Edits are undoable like every editor document, saved with the editor's safe writes, and reach a running game by hot reload.

  • It edits the file, not the compiled asset. An open asset is its JSON tree, in the order it was written; each edit compiles it again into a library the designer owns, beside copies of the project's other assets, and the canvas builds from that. Unsaved edits never reach a running game: saving writes the file and the game's hot reload picks it up.
  • Saving keeps the file as written. An element the designer didn't change keeps its keys in their order, and // and /* */ comments stay where they were, attached to what follows them. A comment just before an element can be turned into its note, which the designer shows.
  • Preview data is the editor's, not the asset's. Preview values for inputs and contexts, the resolution and aspect chosen, and placeholder region content are kept in editor/previews/ in the project (versioned, so a team shares them, and never packaged), one file per asset keyed by its asset ID, so a rename keeps them. The .tnxwidget holds nothing for the designer but notes.
  • Inputs and Contexts picks the asset's class and shows what it declares (its contexts, inputs, and the methods its actions can call), or, for a widget with no class, declares its inputs (name, type, and default); and holds the preview values. An input's preview is given to the canvas's use of the asset. A context is given as a preview target, and each of its fields that the asset's formulas read gets a value of its own. The field is found through the game's reflection, so a path the game doesn't have is reported in the designer just as it would be in the game.
  • A context can read the game instead. Each context's Reads choice is the values typed for it, or the game in a play session: what the game gave this HUD under that name (SetHud), else what it gave a HUD of that type, else a live entity of the type, or one picked from the play session's worlds by name (EPlayer #3 · Owner 1). The canvas then reads the published frame as the game's HUD does, so a bar moves as the player plays. With nothing playing, or nothing to read, the typed values are shown, and the choice is kept for the next session.
  • The canvas shows how the asset fits what it is given. It is a document of its own inside the canvas panel (ui.Document, below), built from the designer's library. A HUD is shown on a screen: its size in pixels, which gives its aspect ratio, and a DPI, the pixels to a UI unit, so it is laid out in pixels / DPI units. Turning it round swaps width and height. The common devices are presets of the two (1920×1080 at 1, 3840×2160 at 2, 3440×1440 at 1, 1280×800 at 1, 2532×1170 at 3). Any other asset is shown in a parent area whose size is dragged, at its own size if it fits its content. UI scale and language are chosen beside them.
  • Details reads the registry. The selected elements' properties are the ones the widget registry lists for their types, in the groups Element, Layout, Content, Look, and Behaviour. With several selected, it shows the properties they share, and an edit goes to each of them as one undo step. A value is edited with its kind's editor: a number, a toggle, a list of words, or a colour. A size is how it is had (fit, grow, fixed, or a percentage of what holds it) and its number, made fixed at the size it is drawn at; edges are left, top, right, and bottom, written as briefly as they allow; scale, skew, and the pivot are across and down; an anchor is picked from a grid (Anchors, above); an image is a path picked from the project's content, by folder; a tooltip, a list's item, and a use pick a widget asset. What an element does (on) opens an editor of its own: each event and its actions, each action's verb picked and what it names picked where the designer knows the choices (the commands the game registered, widget assets, the asset's clips), and its fields added from what the command or verb takes. A colour's tokens are the asset's own and the theme's. A property that can follow data has a button before it that makes it a formula, one of the asset's inputs, a token, or a binding picked from a context's fields: each context, then its components, then their fields, as the game's reflection lists them, so a path is never typed by hand. A dragged number is one undo step. A value set back to its default is left out of the file, and renaming an element in Details renames it everywhere it is referred to.
  • Styles edits the asset's own look in its file: its tokens (a colour gets a picker), its style rules and their properties, and the sheets it names. For the selected element it lists each property of the look the canvas drew, its value, the token it came through, and the rule and scope it came from.
  • Cost builds one instance in a document of its own, with the preview values, and counts its nodes (with the assets it uses), bindings, and widget state, the nodes each element makes, and the average time to build it and lay it out the first time. It is measured again when the asset changes.
  • Animation shows the asset's clips on a timeline: a row for each track (an element and a property), with its keys along it, under a ruler. Pressing or dragging on the ruler moves the playhead, and the canvas shows the clip held there (UIContext::Pose). A key is dragged in time as one undo step, added at the playhead with the value of the key before it, and its time, value, and ease are edited below the tracks. The clip's length, looping, and rate are set above them, Play and Stop run it on the canvas, and the asset's triggers are listed as the file writes them.
  • Curves are edited in the Animation panel's curve editor: the curve drawn through its keys, keys dragged in time and value, the selected key's handles dragged round it to set its slopes, and its mode (auto, user, broken, flat, linear, step, or an ease) picked. The wheel zooms the time, with Shift the values; a middle or right drag moves the values shown. Make curve turns a track's own keys into a curve asset it plays. A curve edit is a step in the asset's undo, and the curve is saved with the asset. Before changing a curve that other assets play as well, the designer asks whether to change it for all of them or to make a copy for this track.
  • On the canvas a Place is dragged to set its x and y, and the selected element is sized by any of its edges or corners, which write a fixed width and height. The edge held moves and the one across from it stays where it is: a Place's offset follows by its anchor, so a bar anchored at the bottom grows upwards from its top edge and downwards from its bottom. With Alt it sizes about its middle; with Shift a corner keeps its shape. Each drag is one undo step.
  • Selecting and moving on the canvas. A click selects the element under the pointer; Shift or Ctrl adds it or takes it away. A drag from inside the selection moves it, every selected Place together (Shift: along one axis); let go without moving and what was clicked is selected alone. A drag on an element in a Row or Column moves it among its siblings, a line showing where it will go. A drag from anything else draws a box that selects what lies wholly inside it, outermost only (Shift or Ctrl at the press: added to the selection). The arrow keys nudge the selected Places a unit, ten with Shift; presses close together are one undo step.
  • Snapping. A moved or sized element's edges and middle snap to those of what holds it, its siblings, the screen, and the safe area, with a guide drawn along the line it is on. Snap in the canvas bar, or the editor's Snap action, turns it off; Ctrl holds it off during one drag. A snapped value is written exactly (layout edges are often between whole units); otherwise moves and sizes are whole units. A turned, skewed, or scaled element doesn't snap, as it isn't drawn where it is laid out.
  • Anchors are picked in Details from a grid of the nine points of the box a Place is placed in, or None (laid out with its siblings). A new anchor keeps the element where it is drawn, its x and y changed to match; with Ctrl it goes to the point itself.
  • The canvas's tools are Move (W), Rotate (E), and Skew; Space cycles them, as in the 3D viewport. Rotate draws a ring round the pivot and turns the element as the pointer goes round it (Shift: in 15° steps). Skew leans its top as the pointer moves across and drops its right side as it moves down (Shift: in 5° steps). In both, the pivot is drawn and can be dragged anywhere, in the element or out of it (Shift: onto its middle, corners, and the middles of its edges). It moves along the element's own axes, and a Place moves back as it does, so a turned element stays where it is drawn. The outline, the handles, and the pivot follow the element as it is turned and skewed, and sizing works along its own axes.
  • Auto-key. With Auto-key on in the timeline, a change to what the clip in front can animate is a key at the playhead instead of a change to the element: opacity, rotation, scale, skew, and colour, in Details or on the canvas, and a Place moved, nudged, or given an x or y, which keys its offset from where it is placed. A key already at the playhead takes the value. The canvas is framed in red while it is on.
  • Event tracks are rows of markers on the timeline: + Events adds one for the element picked, a marker is dragged in time, and the selected event's actions are edited as an element's are. Played on the canvas, what they do is listed there; with Scrub runs events on, moving the playhead across one runs it too.
  • The graph shows any numeric track: a curve asset's keys and handles, or the track's own keys, dragged in time and value. The clip's other numeric tracks are drawn faintly behind it, each fitted to the values the graph shows so their timing reads against it, and picked from the legend above it.
  • Previewing behaviour. The selected element can be shown hovered, pressed, focused, or disabled, whatever the pointer does: its styles follow, and so do the clips its triggers play (UIContext::ForceState). Interact (I) gives the pointer to the asset instead, as a player would use it (GGDocument::Dispatch): it hovers, presses, and clicks, its clips play, and what its actions would do (a command, an event, show, a toast) is listed on the canvas and never sent. While a clip plays, by Play or a trigger, the timeline's playhead follows it (UIContext::ClipTime).
  • The screen has the player's UI scale and the area it keeps clear (a notch, a home bar, overscan), set under Inputs and Contexts. The phone and tablet presets come with theirs, and Turn turns it. The canvas lays the HUD out at the DPI times the UI scale, gives ui.SafeArea the safe area, and shades what lies outside it.
  • The canvas's language is picked beside the screen: the editor's, any the project has strings for, or pseudo-text, which shows every string about 40% longer and accented, in brackets ([Sàfé àréà ~~~~]), its placeholders and markup kept, so text a translation would cut off shows before there is one. It is kept with the asset's preview data, the editor stays in its own language, and a strings file saved is shown at once.
  • Docked, not split. Each part is its own dock panel that follows the designer tab in front, so the parts can be arranged, floated, and saved in layouts like any other. The UI workspace arranges them to start from.

Designer Steps

#What
8aThe document: opened from the Content Browser, a tab per asset; the JSON tree with comments kept, saved in the outline layout with safe writes; undo and redo; unsaved changes, the exit question, autosave and recovery; the designer's own library; the canvas with resolution and aspect presets; Problems; the UI workspace
8bStructure: the hierarchy (drag to reorder and reparent, slots as drop places, rename, delete, duplicate, copy and paste, wrap with, replace with); the palette, dragged into the hierarchy or the canvas; selecting and hovering on the canvas
8cDetails: the widget registry, which the loader and the designer share; each property a literal, an input, a token, or a binding picked from the context's reflected fields; the inputs and contexts panel with preview values
8dStyles and Cost: the asset's sheets, styles, and tokens, where each shown value came from, and elements, bindings, and build time per asset and instance
8eAnimation: clips on a timeline, tracks per element and property, keys dragged, scrubbing and playing; triggers
8fCanvas tools: dragging Place offsets, resizing, anchors, region placeholders and their items' inputs, the safe-area overlay, UI scale, language; preview from play (a context reading what the game gave the HUD, or a live entity, in a play session)
8gWorking in it all day: canvas handling (snapping and guides, box select, moving several, reordering in a Row or Column, nudging, the anchor grid); Details pickers (a binding picked from a context's reflected fields, image, widget, and curve pickers, tokens beside colours, sizes, edges, and actions edited as what they are); forcing hover, pressed, focused, and disabled, and an Interact mode that runs the asset's actions; the playhead following a playing clip; auto-key, several curves in the graph, event tracks; renaming with references, finding references, thumbnails

Starter Widgets

The engine ships widget assets to use or copy, in Trinyx/assets/ui/widgets. They are read before the project's content, and a project asset of the same name replaces one without a warning, whichever is read first; copy the file into the project to change it. They are styled through the theme's tokens (accent, good, surface-panel, text-soft, ...) and their own, so a theme or a use restyles them.

WidgetInputsIs
StatBarValue, Max (1), LabelA labelled bar that slides to a new value. Tokens bar-fill, bar-back
ResourceBarValue, Max (100), Label, Delay (0.4), Drain (0.5)Health, mana: the amount beside its label, a trailing fill behind a drop, and a shake when it falls. Tokens bar-fill, bar-back, bar-trail
CounterValue (a big number), LabelA big number in suffix notation (1.23M) that rolls up, and bumps when it rises
IconButtonIcon, LabelA button with an icon that wiggles on hover. What it does is set where it is used (on.click)
PanelTitleA titled frame as wide as its place, with a Content slot. Tokens panel-fill, panel-edge
TooltipFrameTitle, TextWhat a tooltip asset is built in, with a Content slot for more
ListRowIcon, Title, DetailA list item: an icon, two lines cut to fit, and a Trailing slot (a price, a button)
ToastTextWhat the toast action shows: it slides in and fades out
CooldownIconIcon, Left (1 to 0)An ability's icon, greyed under a ring that empties, and a pop when it is ready. Token cooldown-ring
HeartRowCount, IconOne image per point, up to ten, with a hop when one is lost
OptionRowLabel, HintA settings row: label and hint on the left, a Control slot on the right (a Carousel, by default, or a Toggle or Slider)

The built-in controls are elements already and take classes like any other.


Steps

#WhatStatus
1Game state in code: TnxBind(entityOrView, &Component::Field), sim commands (TnxSim::Send, a Construct's OnCommand)Built (belowbelow); view models with step 2b
2aWidget assets in the GG-UI core: the file and its forgiving reader, compile, build (ui.Use), inputs, slots, formulas, actions handed to callbacks, validation, hot reloadBuilt: GGWidgetLibrary, ui.Use, UIContext::SetWidgets / SetWidgetActions
2bWidget assets in the engine: contexts against reflection, commands by name, the asset type, view models; HUDs, regions, and layers (TnxUI::SetHud, Add, Remove)Built: contexts (TnxContext), commands by name (TNX_REGISTER_COMMAND), the host's library read from the project's content with hot reload, the asset type (listed and created in the Content Browser, with .tnxid sidecars), HUDs with regions, layers, show, hide, toast, and events, drawn in the game window and in play views, placed for split screen, with lifetime tiers; view models (TnxViewModel); references by asset ID
3aStructure: controls that take content (Button, Toggle), Switcher, Repeat, SafeArea, Carousel, tooltips as assets, on.changeBuilt
3bWhat paint draws: Progress, Image, and Label as above, ShapeBuilt, with a bindable image crop (each entry a number or a formula) and a blurred text-shadow. The flipbook frame is step 11
3cData: lists and tiles over game data with item contexts, selection, and slots; ViewportBuilt: List and Tiles over data tables (.tnxtable), a game's sources (TnxUI::Source), and entities; where, sort, selection, select and activate, empty, header, footer; ==, !=, &&, \|\|, ! in formulas; Viewport. A list's where and sort can read the item and other contexts together
4Rich text and action prompts: styled runs and inline images in one paragraph, action glyphs that follow the deviceBuilt: RichText and ActionPrompt (ui.RichText, ui.ActionPrompt), spans and inline boxes in the text engine, gamepad buttons on input actions (GGPad, saved in Keybinds.ini), the device in use. Links (<link Name>, links, RichTextProps::OnLink). Rebinding a gamepad button in ui.KeyBinding is step 9
5Style scopes: asset sheets, scoped tokens, instance and element stylesBuilt: an asset's sheets, styles, and tokens; tokens on a use; element style; formula tokens over inputs; ui.Tokens, UseProps::Tokens; ranks so the outer author wins; .ggss files in the widget folders; tokens that follow game data (a number, or select between two values); popups and widget parts in their element's scopes; the inspector's explanation through scopes. Popups also get the document's assets, so an asset tooltip builds
6Animation: clips (looping, bound rate), triggers (values, visibility, element states), tweens, the group transform, ScaleBoxBuilt: the group transform (an affine view per clip entry, on the GPU and in hit testing), clips and tweens in the core (UIAnimation.h), animations, triggers, play and stop, tween, and ScaleBox in assets; exit clips for HUD widgets. Not yet: the flipbook frame (step 11); a sim-bound document's clips (step 12); a popup anchored to an element under a transform is placed as if it weren't
7Starter widgetsBuilt: the eleven above, read by the engine before the project's content (UIHost::AddWidgetFiles(path, true), GGWidgetLibrary::LoadStarter); drawn by the GPU as the reference draws them. With them: components grow when what they build grows, a button given content is sized round it, an element's own style is over its control's look, and a number shown as a constant follows notation; a bar's trailing fill takes the trail style property
8The designer window (Designer StepsDesigner Steps)8a to 8f built: the document, the outline writer that keeps comments (GGWriteWidgetFile), the preview library, the canvas as a nested document (ui.Document), screens by size, DPI, UI scale, and safe area, Problems; the hierarchy, the palette, and the structure edits (GGWidgetFile); the widget registry, Details, inputs, contexts, and preview values; styles and cost; the animation timeline and curve assets; the canvas tools (move, size, rotate, skew, pivot), region previews, HUD assets, the canvas's language with pseudo-text, and preview from play. 8g under way: canvas handling, Details pickers, forced states, Interact, the live playhead, the actions editor, theme tokens, auto-key, the graph of any track, and event tracks built; widget classes (TnxWidget) behind assets, in place of declared contexts, named events, and the HUD's generated call
9Input settings a game fills in: focus neighbours and wrapping, what brings the keyboard to the UI, tab switching, steps, wheel distance (Interaction)
10World-UI templates on the GPU, from widget assets; screen markers (an element held at an entity's place on screen, kept at the edge with an arrow when it is off screen)
11Sprites and sprite sheets; flipbook frames on Image
12Sim-bound documents

Steps 1 to 8 make the HUD for the first game; 9 makes it work on a gamepad. Step 2 is split along the module boundary: the core knows nothing of the engine and is tested headless. The designer comes after the runtime so it edits a format that already runs.

Open

  • Later elements: video, a blurred backdrop behind a panel, charts, a scrolling ticker, an on-screen keyboard for consoles.
  • Effects that need the renderer: a material or shader on an element, a blurred backdrop, masks beyond rounded clipping.
  • Text that changes with a number or a person: plural forms and grammatical gender in the string tables.
  • A compact indented text form for people who write UI as text, loading into the same asset.
  • Whether a widget asset can declare a visual-logic graph once the visual language exists, and what it may touch.

Curves

  • Weighted handles. A key's handles set slopes only. A "weight" per side would let a handle be lengthened too; the curve is then a Bezier solved for time. The key format leaves room for it.
  • Opening a curve on its own. A .tnxcurve opened from the Content Browser should open in the curve editor without a widget track that plays it, with its own tab, undo, and save.
  • Curve editor working space. It lives in the Animation panel, under the tracks, at a fixed height. It needs room of its own: a height that follows the panel, or a panel of its own.
  • Editing several keys at once. Box selection, moving or scaling a selection, flattening its tangents, copying and pasting keys.
  • Several curves in one graph. Showing the clip's other tracks (and their curves) over the one being edited, to line up motions.
  • Snapping. Times to the ruler's marks and values to round numbers, and a key snapped to the playhead.
  • Value labels. The graph draws lines at round values but doesn't number them.
  • Curves for colours and other kinds. A curve plays numbers; a colour track has keys of its own. Vector and colour curves need CurveTable stores of their own.
  • The engine taking curve assets by name. CurveTable::Register takes a read curve file. Camera transitions and other engine users should name a curve asset and get its handle, followed on hot reload.
  • Region previews with inputs. A region's preview copies get no inputs, so one that needs them (a toast's text) shows empty. They should take preview values as the asset's own inputs do.
  • Live playhead. While a clip plays on the canvas the playhead doesn't move with it: there is no way to read a playing clip's time yet.