Skip to content

GUI

The base game's interface is written in Teal with a React-style framework: components are functions ("recipes") that return a tree of built-in widgets, keep state in hooks and are re-run when that state changes. api.gui sits underneath and "Contains all GUI functionality apart from react" (api/tealdef/api/gui.d.tl). All GUI code runs in the GUI state (see Scripting).

The official wiki has no GUI page yet; its modding index lists "User Interface" as coming soon. This page is built from the base game files, and the wiki only adds a few details, noted below.

The pieces

Module Loaded with Definition Content
React ug_require "::/gui/main/react.lua" as React react RegisterRecipe, hooks (useState, useRef, onStep, onEvent, ...), extension points and plugins
Builtin ug_require "::/gui/main/builtin.lua" as Builtin builtin the widgets: BoxLayout, FlowLayout, FloatingLayout, TextView, RichTextView, ImageView, Button, ToggleButton, CheckBox, ComboBox, Slider, TextInputField, List, ScrollArea, TabWidget, TableLayout, Window, Chart, ...
GUI helpers ug_require "::/gui/main/gui_react_util.tl" and others in gui/main gui/main layout shortcuts, usePlugins, colours, tooltips
api.gui global api.gui camera, sound, input actions, byId, genericRep for .gres resources, fireGuiScriptEvent

A recipe

React.RegisterRecipe(name, fn) turns a function into a recipe that can be called like a widget. The function returns a node tree built from builtin.* calls, each taking a parameter table. The game bar's earnings display is a small, complete example. From base/content/gui/gui/game_bar/game_bar_display_earnings_plugin/game_bar_display_earnings.script.tl:

local react = ug_require "/gui/main/react.lua" as React
local builtin = ug_require "/gui/main/builtin.lua" as Builtin
local engine_react_util = ug_require "/gui/main/engine_react_util.tl" as EngineReactUtil

local game_bar_widgets = ug_require "/gui/game_bar/game_bar_widgets.tl" as GameBarWidgets

local GameBarEarningsPlugin = react.RegisterPluginRecipe(game_bar_widgets.GameBarInfoDisplayExtension, "GameBarEarningsPlugin", function() : TreeNodeId
    local isMapEditorRef : ReactRefT<boolean> = react.useRefLazy(function() : boolean return api.gui.game.isMapEditor() end)

    local makeState = function() : integer
        return api.engine.util.finance.calculateEarnings(api.engine.util.getPlayer())
    end
    local earningsAndIsMapEditor = engine_react_util.useStepStateTimer(makeState)

    if isMapEditorRef:get() then
        return nil
    end

    local className = earningsAndIsMapEditor:old() >= 0 and "positive" or "negative"
    return builtin.BoxLayout{
        orientation = builtin.type.Orientation.Horizontal,
        children = {
            builtin.Component {
                meta = { tooltip = _("Total Earnings") },
                layout = builtin.BoxLayout{
                    orientation = builtin.type.Orientation.Horizontal,
                    children = {
                        builtin.TextView{
                            meta = { class = "font-scale-headline" },
                            text = _("Earnings"),
                        },
                        builtin.TextView{
                            meta = { class = "font-scale-headline, " .. className },
                            text = api.util.formatMoney(earningsAndIsMapEditor:old())
                        },
                    },
                },
            },
        },
    }
end)

local result = {
    GameBarEarningsPlugin = GameBarEarningsPlugin,
}

return result

What it shows:

  • Returning nil renders nothing.
  • meta is on every widget (Meta): id, class (style classes, comma-separated), tooltip, enabled, onMouseOver, onFocusChange and more.
  • The recipe reads the engine with api.engine.util.finance.calculateEarnings. engine_react_util.useStepStateTimer(makeState) re-runs makeState on a timer and returns a state whose :old() is the last value (engine_react_util).
  • The module returns its recipes in a table, so a .res file can reference them as ...script@GameBarEarningsPlugin.

Hooks

The hooks are on the React module and are called at the top of a recipe, as in React for the web:

Hook Definition
useState(initial) returns a ReactStateT<T> with old(), set(v) and transform(fn); setting it re-runs the recipe
useRef(initial), useRefLazy(fn) a ReactRefT<T> with get/set, without re-running
useMirrorState, useDependentState state derived from another ref or state
onStep(fn), onStepTimer(fn, intervalSeconds?, jitter?) run code every step or on an interval
onMount(fn), onUnmount(fn) lifecycle
onEvent(name, fn), fireEvent(source, name, param?) GUI events between recipes
useNodeRef(recipe?), ref(nodeRef) handle to a child node, for focus and position
useInputAction(name, handler), iaHandler(fn, ...) keyboard and gamepad input actions

Buttons call back into Lua with onClick, and that is where scripts usually send commands or events. From the game bar (base/content/gui/gui/game_bar/game_bar_widgets.tl):

builtin.Button{
    meta = {
        class = "secondary, skip-to-next",
        tooltip = _("Skip to Next Time of Day"),
        enabled = stateMode:old() == "Dynamic",
    },
    onClick = function()
        api.cmd.sendCommand(api.cmd.makeScriptingSendEventCmd("", "GameTime", "SkipPhase", { }))
    end,
    content = builtin.ImageView{
        path = "::/gui/game_bar/icons/gametime_cycle.tga"
    }
}

Styles

Each GUI module has a stylesheet next to it, a .css.lua file that builds a table of selectors with stylesheetutil.lua. From game_bar_display_earnings.css.lua:

local ssu = require "::/gui/main/stylesheetutil.lua"

function data()
    local result = { }

    local a = ssu.makeAdder(result)

    a("R::GameBarEarningsPlugin", {
        gravity = { 0.5, 0.5 },
    })

    a("R::GameBarEarningsPlugin BoxLayout", {
        innerSpacing = { 8, 0 },
        outerSpacing = { 14, 0 },
    })

    return result
end

R::<RecipeName> selects the nodes produced by a recipe; widget names (BoxLayout) and classes from meta.class narrow it down. The style properties a widget accepts are in StyleSheet in api.gui (backgroundColor, borderWidth, padding, size, ...). The full selector syntax is not documented in the definitions or the wiki. The wiki's resource types table confirms .css.lua as the stylesheet type, and the base loader has a loadStyleSheet modifier key, so a mod can change existing stylesheets at load time (see Resource modifiers).

Extending the base GUI: plugins

The base GUI leaves named slots, extension points, where extra recipes can be inserted. A plugin is two files: a recipe and a generic resource that registers it. The resource for the recipe above, game_bar_display_earnings.res.lua:

function data()
    return {
        type = "react-plugin ::GameBarInfoDisplayExtension",
        data = {
            filePath = "::/gui/game_bar/game_bar_display_earnings_plugin/game_bar_display_earnings.script@GameBarEarningsPlugin",
            priority = -2, -- note: this is negative so default 0 will be appended to the right
        }
    }
end

The parts fit together like this:

  1. The base GUI declares the extension point: game_bar_widgets.GameBarInfoDisplayExtension = react.RegisterExtensionPoint0(getOwningModId(), "GameBarInfoDisplayExtension"). In react.lua the id becomes modId .. "::" .. name; for the base game, whose mod id is empty, that is ::GameBarInfoDisplayExtension.
  2. Where the slot is drawn, the base GUI calls gui_react_util.usePlugins(extensionPoint) or react.getPlugins(extensionPoint).
  3. getPlugins (in base/content/gui/gui/main/react.lua) loads every generic resource of type "react-plugin " .. extensionPoint.id, resolves filePath to a recipe and an optional condition to a function, and sorts by order (ReactPluginDesc: filePath, condition, order).
  4. The recipe registers itself with react.RegisterPluginRecipe(extensionPoint, name, fn).

The entity window plugins use order and condition, for example base/content/gui/gui/entity_window/warehouse/warehouse_eow_stocks.res.lua with order = 20, or the town window plugins with condition = "::/gui/entity_window/town/town_eow.script@isSandboxMode".

Note

The two game bar plugins set priority instead of order. getPlugins in react.lua only reads order, so how priority is used is unclear from the files.

Extension points registered in the base GUI:

Extension point Where
ModEntryPointExtension gui/main/mod_entry_point.tl; the main game view adds every plugin as a floating child (gui/main/game.tl)
MainModButtonAreaExtension gui/main/main_mod_button_area.tl; the radial menu gets a mod area when plugins exist
GameBarInfoDisplayExtension gui/game_bar/game_bar_widgets.tl
AnimalEowExtensionPoint, EmptyConstructionEowExtensionPoint, FunElementsEowExtensionPoint, IndustryEowExtensionPoint, LineEowExtensionPoint, MaintenanceStationEowExtensionPoint, PerkEowExtensionPoint, SimPersonEowExtensionPoint, StationGroupEowExtensionPoint, TownBuildingEowExtensionPoint, TownEowExtensionPoint, TownEowEditorExtensionPoint, VehicleEowExtensionPoint, WarehouseEowExtensionPoint the entity windows in gui/entity_window/

The names ModEntryPointExtension and MainModButtonAreaExtension suggest they are meant for mods. None of the shipped mods use them, so there is no official example; a mod would follow the pattern above with type = "react-plugin ::ModEntryPointExtension". A mod can also declare its own extension points; the id then starts with its mod id.

Replacing a base recipe

A mod can swap out an existing recipe for its own. Campaign mission 01 does this to hide the HUD icons when a mission task disables them. The resource, mods/release/urbangames_campaign_mission_01/content/mission/mission/hud_replacement.res.lua:

function data()
    return {
        type = "react-replacement-config",
        data = {
            filePath = "urbangames_campaign_mission_01::/mission/hud_replacement.script",
            doReplaceFn = "doReplaceFn"
        }
    }
end

and the script, hud_replacement.script.tl:

local builtin = ug_require "::/gui/main/builtin.lua" as Builtin
local game_react_globals = ug_require "::/gui/main/game_react_globals.tl" as GameReactGlobals
local hud_icon_toolbox = ug_require "::/gui/main/hud_icon_toolbox.tl" as HudIconToolbox
local react = ug_require "::/gui/main/react.lua" as React

local HudIconMasterGameReplacement = react.RegisterRecipe("HudIconMasterGame", function(params : Builtin.HudIconParam, userParam : HudIconToolbox.HudIconMasterUserParam) : TreeNodeId
    if game_react_globals.getDisableFeatures()["HudIconMaster"] then
        return nil
    end
    return builtin.BoxLayout{
        children = { react.CallOriginalRecipe(hud_icon_toolbox.HudIconMasterGame, params, userParam) }
    }
end)

local function doReplaceFn(replacementApi : ReactReplacementApi)
    replacementApi.ReplaceRecipe(hud_icon_toolbox.HudIconMasterGame, HudIconMasterGameReplacement)
end

local result = {
    doReplaceFn = doReplaceFn
}

return result

base/content/gui/gui/main/bootstrap_game.tl collects all react-replacement-config resources, sorts them by order (ReactReplacementConfigDesc), calls each doReplaceFn with a ReactReplacementApi, and then sets recipeReplacementAllowed = false, so replacements only work in that phase. The definition warns: "the replacement must be fully compatible, e.g. if the original provides an Api, the replacement must provide the same one!" Inside the replacement, react.CallOriginalRecipe renders the original; the definition notes that this "will not work for builtins".

GUI code in construction files

Constructions can bring their own GUI through script references in the .con.lua. The wiki documents these on Construction Basics, and Constructions lists them among the construction fields. The pollution cleanup facility (base/content/game_mechanics/game_mechanics/emission/pollution_cleanup_facility.con.lua) sets four of them:

constr.hudIconScript = {
    fileName = "::pollution_cleanup_facility_hud.script@hudIconFn",
}
constr.configureHudIconsScript = {
    fileName = "::/gui/construction/construction_desc_hud_icons.script@configureLandmarkConstructionHudIconsFn",
}
constr.entityWindowScript = {
    fileName = "::pollution_cleanup_facility_hud.script@entityWindowFn",
}
constr.configureLayerScript = {
    fileName = "pollution_cleanup_facility_layer.script@configureLayerFn",
    params = {
    }
}
Reference What the function returns (wiki)
hudIconScript the construction's HUD icon as a node tree; ::/gui/main/hud_icon_toolbox.tl has helpers such as PerkHudIcon
configureHudIconsScript a ConstructionActionHudIcons table that says which HUD icons show while the construction is placed (componentTypes, showDistricts, showTownBorders, showPerksAndTowns); the defaults are in ::/gui/construction/construction_desc_hud_icons.script.tl
entityWindowScript the content of the entity window: a recipe (usually make_entity_window.ConstructionWindow from ::/gui/entity_window/make_entity_window.tl), the param table passed to it and a fallbackTitle
configureLayerScript the map layer configuration to show while the entity window is open

Like every script reference, the functions get the reference's params as the first argument (captureParams).

Talking to game scripts

GUI code does not change the engine directly. It reads with api.engine and sends commands or script events (see Scripting). For GUI-only logic the game scripts have guiUpdate and guiHandleEvent, which receive events fired with api.gui.fireGuiScriptEvent(id, name, param) and can return a value to the caller. The calendar widget in game_bar_widgets.tl uses it as api.gui.fireGuiScriptEvent("calendar", "onManualTimeChanged", value). The wiki's Missions page says the same for mission tasks: handleEvent receives the game script events and guiHandleEvent the GUI events.

To look at a GUI value while you work, the wiki's API Reference page suggests the in-game console and debugPrint(<someTable>).

Official wiki: Construction Basics (HUD icon, entity window and layer scripts), Resource types, Missions, API Reference. The wiki's user interface page is not published yet.