In this guide
- HUD nodesEvery node type and the fields it accepts.
- LayoutDock, stack, anchors, margins, padding, and safe areas.
- Text, theme & localeFonts, variables, localization, and styles.
- HUD from LuaSetters, events, tweens, and runtime-created nodes.
- Orbit UI kitNative controls, configurable palettes and compound-control bindings.
- UI prefabsParameterized reusable subtrees.
Retained HUD trees, layout, themes, localization, prefabs, and accessibility.
UI & HUD
Runtime UI is a retained tree authored in *.hud.json documents. You describe the layout once; scripts mutate text, values, and visibility at runtime. The editor HUD stage edits the same tree the runtime renders.
*.hud.json.Document shape
{
"format_version": 1,
"canvas_size": [1280, 720],
"theme": "../ui/themes/dark.json",
"localization_file": "../ui/en.localization.json",
"variables": ["game_title"],
"action_map": { "submit": "confirm" },
"action_effects": {
"menu_accept": { "focus": "start" }
},
"root": {
"id": "menu",
"type": "container",
"dock": "fill",
"stack": "column",
"pad": 24,
"gap": 12,
"children": [
{ "id": "title", "type": "label", "text": "${game_title}", "font_size": 32 },
{ "id": "start", "type": "button", "text": "Start", "action": "menu_accept" },
{ "id": "options", "type": "button", "text": "Options", "action": "menu_options" }
]
}
}
Walkthrough: canvas_size is the design resolution every position and size is measured against. theme and localization_file are paths relative to the HUD file. root is a full-screen column with 24 units of padding and 12 units between children. The title interpolates ${game_title}; the top-level variables array declares variable names — never values — and Hud.set_variable fills them at runtime. A button's action names the UI action it emits. action_effects can focus or show/hide nodes when that UI action fires. action_map maps navigation slots (next, previous, submit, cancel) to input action names.
You can omit root and write top-level children instead — an implicit fill container is inserted. Prefer dock, stack, pad, and at before raw anchors.
Node types
| Type | Role | Useful fields |
|---|---|---|
container, panel | Layout and grouping | stack, gap, alignment, columns |
label, text | Static / editable text | text, font_size, text_wrap, text_alignment |
image | Texture display | texture (asset://…), animation frames |
button, toggle, slider, text_input | Interactive controls | action, disabled, focusable, placeholder; slider takes minimum/maximum (defaults 0 and 1) |
scroll, list, progress | Scrolling, lists, progress | children / value |
modal | Focus-trapping dialog | children |
virtual_button, virtual_stick | Touch controls | control (publishes virtual:*) |
Layout
- Every node accepts
anchor_min/anchor_max,margin,padding,min_size,max_size. stackisrow,column, orgrid, withalignment(start|center|end|stretch),gap, andcolumns.dockpins a node:fill|top|bottom|left|right|center.atplaces a node at an explicit canvas point;sizesets its extent in canvas units.- Safe areas protect readable and interactive content by default. Set
respect_safe_area: falseonly for intentional edge-to-edge backgrounds.
Colors accept a [r, g, b, a] array (0…1) or a hex string like #1b1c27cc.
Text, fonts, and localization
"font": "asset://fonts/body"picks aFont2Dasset. The default is embedded Inter Variable.text_wrap:none|word|grapheme.text_overflow:visible|clip|ellipsis.text_alignment/text_vertical_alignment:start|center|end.max_linescaps the block.Text.parse_richcan parse[color],[em],[strong],[link], and[icon]into spans for scripts; HUD nodes currently render plain text.${name}intextinterpolates a native variable — a mechanism separate from localization. Localized strings are selected per node withlocalization_keyand come fromlocalization_file(or an embeddedlocalesobject).
local Hud = require("demi.hud")
Hud.set_variable("game_title", "Skyforge II")
Hud.set_locale("de") -- requires a loaded "de" entry in HUD locales
Hud.set_pseudo_locale(true) -- pseudo-localization for layout QA
Hud.has_variable(name) returns a boolean; Hud.get_variable(name) returns the string or nil; Hud.reset_variables() clears runtime values, so unresolved names render as ${name}.
Themes
A theme document supplies styles and an optional action_map. A localization file supplies the initially loaded localization object. Hud.set_locale selects among embedded locales in the HUD; it does not load another localization file. Reference a style with "style": "menu_button". Theme styles support colors, padding, gap, font size, minimum size, and control/row height; explicitly authored text color, font size, and minimum size take precedence.
Mutating UI from Lua
Scripts change content, state, and rectangles through demi.hud:
local Hud = require("demi.hud")
Hud.set_text("title", "Ready?") -- label/text content
print(Hud.get_text("seed_input")) -- read a text_input
Hud.set_font("title", "asset://fonts/head")
Hud.set_font_size("title", 40)
Hud.set_color("title", 1, 1, 1, 1)
Hud.set_background_color("panel", 0.11, 0.11, 0.15, 0.8)
Hud.set_value("health_bar", 0.75) -- progress/slider within its range
Hud.set_checked("toggle_music", true) -- toggle state
Hud.set_disabled("start", false) -- enable a button
Hud.set_visible("hint", false) -- show/hide
Hud.set_opacity("fade", 0.5) -- 0…1
Hud.set_image("portrait", "asset://ui/hero", 0, 0, 256, 256)
Hud.set_position("popup", 120, 80) -- canvas units
Hud.set_size("popup", 480, 260)
Hud.focus_next() -- move keyboard/gamepad focus
local focused = Hud.focused() -- node ID or empty string
Every setter takes a node ID string first. Hud.find(id) returns a generation-checked handle (or nil if the node is gone) for structural operations and tweens.
Runtime-created UI
local node, err = Hud.create("menu", {
id = "continue",
type = "button",
text = "Continue",
action = "confirm",
})
if not node then error(err) end
print(node.id) -- ID on the returned handle
local copy = Hud.clone(node, "continue_copy") -- duplicate under a new root id
Hud.reparent(copy, "other_panel")
local kids = Hud.children("menu") -- array of child ids
Hud.remove(node) -- delete a runtime node
Hud.create accepts flat node fields such as id, type, text, x, y, width, and height; it does not parse nested children or layout shorthand. It returns a handle plus an error string. Keep handles for structural operations.
UI prefabs
Reusable subtrees live as *.ui.prefab.json with typed parameters (string, number, integer, boolean, array, object):
{
"id": "confirm",
"prefab": "ui-prefab://controls/menu_button",
"arguments": { "label": "Confirm" },
"overrides": {"$root": { "style": "primary" }}
}
arguments fills the prefab’s declared parameters; overrides patches fields on the expanded root node (id, children, and prefab are reserved). Expanded descendant IDs use dotted paths (confirm.hint). Unknown parameters, type mismatches, unresolved ${…} markers, missing files, reference cycles, reserved override fields, and duplicate IDs fail validation with a clear diagnostic.
Virtualization
For long inventories and chat logs, only build what is visible:
local first, count = Hud.visible_range(500, 48, 240, 480)
-- first → one-based first visible row; count → live rows needed
local layout = Hud.virtual_layout({ 48, 64, 48 })
-- layout:visible_range(scroll_offset, viewport_extent, overscan?)
-- layout:set_extent(index, extent), layout:item_offset(index),
-- layout:item_count(), layout:total_extent()
local rows = Hud.recycle_rows("inventory", template, keys, extents,
scroll_offset, viewport_extent)
Hud.clear_recycled_rows("inventory") -- release the pool
visible_range(item_count, item_extent, scroll_offset, viewport_extent) covers uniform rows. virtual_layout(item_extents) returns a layout object for rows with different heights; recycle_rows(collection_id, row_template, keys, extents, scroll_offset, viewport_extent) keeps only the visible range alive against a hidden template row and returns generation-checked row bindings. Details in HUD from Lua.
Tweens and reduced motion
local fade = Hud.find("fade")
local handle = Hud.tween(fade, "opacity", 0.0, 0.35) -- handle, property, target, seconds
Hud.tween(Hud.find("panel"), "y", 120, 0.25)
Hud.cancel_tween(handle) -- takes the tween handle
Hud.set_reduced_motion(true) -- honor accessibility preference
Animatable properties: opacity, x, y, scale. Hud.tween returns the tween handle plus an error string; Hud.cancel_tween takes that handle.
Events
local Events = require("demi.events")
function Menu:on_start()
self.ui_subscription = Events.subscribe("ui_event", function(e)
if e.type == "press" and e.id == "start" then
print("start pressed by", e.source)
elseif e.type == "value_changed" then
print(e.id, e.value, e.checked)
elseif e.type == "submit" then
print(e.id, e.text)
end
end)
end
function Menu:on_destroy()
Events.unsubscribe(self.ui_subscription)
end
Event types: value_changed, focus_gained, focus_lost, submit, cancel, pointer_enter, pointer_exit, press, release, drag_start, drag, drag_end, drop, scroll.
Only the fields relevant to the type are filled — check e.type first. Events.subscribe("ui_event", …) receives the whole stream. Entity callbacks such as on_ui_submit only receive events from a HUD node whose ID matches that script's entity ID.
Accessibility
local snapshot = Hud.accessibility_snapshot()
-- array of { id, parent, role, label, description, value_text,
-- value, minimum, maximum, x, y, width, height,
-- focused, disabled, checked, focusable, offscreen }
value is numeric and value_text is its string form. Node fields: accessibility_label, accessibility_description, accessibility_hidden. Focus is shared across Tab, arrows, D-pad, and pointer; modals trap focus until closed.
Next
- Input for virtual controls and action mapping
- Examples tour —
ui_showcase,main_menu_*