Ui
Retained-mode UI: author a panel/text/button tree once in a .ui file, load it, and render it
each frame into a screen rectangle. Unlike the immediate-mode Gui (Nuklear, rebuilt every
frame in code), a retained UI is data — a document you load, query, and mutate. The engine
letterboxes the document’s design size into whatever rectangle you give it (aspect preserved,
centered), so one layout scales cleanly across window sizes. Buttons dispatch clicks straight to your
game as Game.<name>() calls.
Panels and images draw as textured quads; text labels reuse the Text (SDF) pass, so they stay crisp at any scale. The retained UI draws over the 3D scene and under the immediate-mode Gui overlay.
A static entry point. You do not construct Ui.
Ui.load(path)
Section titled “Ui.load(path)”Returns: UiDocument — or null if the file is missing or fails to parse.
Parameters:
path(String) — Project-relative path to a.uifile in the mounted project.
Loads and validates a .ui document. Malformed input never crashes — a bad file logs its errors and
returns null (validation happens at the parser boundary: duplicate ids, dangling/cyclic parents, and
out-of-range values are all rejected or clamped).
import "engine" for Ui
var doc = Ui.load("ui/main.ui")if (doc == null) System.print("UI failed to load")Ui.render(doc, x, y, w, h)
Section titled “Ui.render(doc, x, y, w, h)”Returns: List — the resolved content rect [x, y, w, h] in pixels (the letterboxed area the UI
actually occupies), or null if doc is null.
Parameters:
doc(UiDocument) — The document to render.x,y,w,h(Num) — The target screen rectangle in pixels (y-down, top-left origin).
Call this in Game.draw(). It resolves the layout, letterboxes the design into (x, y, w, h), runs
the pointer interaction (hover/press states, and a button released inside fires
Game.<onClick>()), and submits the draws. Pass the full window for a full-screen UI:
import "engine" for Ui, Window
class Game { construct new() { _doc = null } init() { _doc = Ui.load("ui/main.ui") } draw() { if (_doc != null) Ui.render(_doc, 0, 0, Window.getWidth(), Window.getHeight()) } // Fired when the button with on_click = "onPlay" is clicked. onPlay() { System.print("play!") }}Clicks are dispatched after draw() returns, from native engine context — never re-entrantly
inside render. A handler named by on_click that your Game does not define surfaces a runtime
error rather than failing silently.
Ui.get(doc, id)
Section titled “Ui.get(doc, id)”Returns: UiElement — or null if there is no element with that id.
Parameters:
doc(UiDocument) — The document to look in.id(String) — The element’sidfrom the.uifile.
Get a handle to a named element so you can read or change it at runtime.
UiDocument
Section titled “UiDocument”A loaded document. Obtain one from Ui.load; pass it to Ui.render / Ui.get.
designWidth(Num) — the authored design width (px).designHeight(Num) — the authored design height (px).elementCount(Num) — number of elements in the document.
UiElement
Section titled “UiElement”A non-owning handle to one element, from Ui.get. It stays valid only while its
document is alive and unchanged structurally — check valid if you keep one across frames.
valid(Bool) —falseonce the document changed structurally or was freed.id(String) — the element’s id.visible/visible = (Bool)— is it drawn and hit-testable?text/text = (String)— the label text (for text elements) or the current value (for an input field).onClick = (String)— set a button’s handler name (dispatches toGame.<name>()).x,y,width,height(Num) — the resolved rect (in design px), valid afterUi.render.checked/checked = (Bool)— a toggle’s or radio’s state (a click sets it; poll it here).value/value = (Num)— a slider’s or progress bar’s value in[min, max](a drag sets a slider; poll it here — for a progress bar, set it here to move the fill).
var label = Ui.get(doc, "score")if (label.valid) label.text = "Score: %(score)"
var volume = Ui.get(doc, "volume") // a slidersetVolume(volume.value)The .ui document format
Section titled “The .ui document format”A .ui file is TOML: one [document] table plus an [[element]] array. Elements reference their
parent by id (parent = "" or omitted is a root). The layout model is Unity-uGUI-style: anchors
are fractions [0..1] of the parent, offsets are pixels from the anchored corner.
[document]design_width = 960 # the reference resolution the design is authored atdesign_height = 540
[[element]]id = "root"type = "panel" # "panel" | "text" | "button"anchor_min = [0.0, 0.0] # full-stretch to fill the parentanchor_max = [1.0, 1.0]offset_min = [0.0, 0.0]offset_max = [0.0, 0.0]color = [0.1, 0.11, 0.14, 1.0] # RGBA 0..1 (panel/button fill)
[[element]]id = "play"type = "button"parent = "root"anchor_min = [0.5, 1.0] # a point anchor (min == max) sized by the offsets belowanchor_max = [0.5, 1.0]offset_min = [-90.0, -84.0]offset_max = [90.0, -32.0]on_click = "onPlay" # -> Game.onPlay()normal_color = [0.24, 0.42, 0.78, 1.0]hover_color = [0.30, 0.52, 0.92, 1.0]pressed_color = [0.18, 0.32, 0.60, 1.0]
[[element]]id = "playLabel"type = "text"parent = "play"anchor_min = [0.0, 0.0]anchor_max = [1.0, 1.0]offset_min = [0.0, 0.0]offset_max = [0.0, 0.0]text = "Play"font_size = 22.0text_color = [1.0, 1.0, 1.0, 1.0]text_align = "center" # "left" | "center" | "right"Common element fields: id, type, parent, visible (default true), raycast_target
(default true, false = transparent to clicks), the anchor_min/max + offset_min/max rect, and
pivot (default [0.5, 0.5]).
Panel/button fill: color (RGBA); an image, referenced either by image = "ui/panel.png" (a
mounted path — the friendly form, resolved on first render) or by texture (a raw renderer texture
handle; 0 = solid color); and nine_slice (bool) + border = [L, R, T, B] (px) for 9-slice scaling.
See Textured panels & 9-slice below.
Text (type = "text"): text, font_size (px), text_color, text_align.
Button (type = "button"): on_click (handler name → Game.<name>()), and the state tints
normal_color / hover_color / pressed_color.
Toggle (type = "toggle"): a checkbox. checked (bool, default false), check_color (the inner
fill when checked), color (the box), and an optional on_change → Game.<name>(). A click flips
checked; the game reads it via element.checked.
Radio (type = "radio"): a grouped toggle. The same fields as a toggle — checked (bool, default
false), check_color (the inner dot when selected), color (the box), an optional on_change →
Game.<name>() — plus a group (string). Radios that share the same non-empty group are mutually
exclusive: clicking one selects it (checked = true) and deselects its group siblings, and a radio
never toggles back off. The game reads which one is selected by polling each radio’s element.checked
(there is no group accessor — the grouping is authored in the .ui file).
Slider (type = "slider"): a horizontal slider. value in [min, max] (defaults 0.5 in [0, 1]),
color (track), fill_color, handle_color, and an optional on_change. Dragging sets value; the
game reads it via element.value. Bounds are clamped to a finite range on load.
Progress (type = "progress"): a display-only progress bar — no handle and no pointer
interaction. It shares the slider’s value/range model: value in [min, max], color (the track),
and fill_color (the fill drawn up to the normalized value). Drive it from script by setting
element.value. This is the retained-UI progress element; for an immediate-mode bar rebuilt in code
each frame, use the Gui instead.
Input (type = "input"): a focusable, editable single-line text field. text is the initial
(and current) value; placeholder is a dimmed hint shown while the field is empty; color fills the
field frame; caret_color tints the caret bar. Click the field to focus it (a caret appears) and
type to edit — insert / backspace / delete and Left/Right/Home/End caret movement are all
UTF-8-aware (whole codepoints), and a press elsewhere blurs it. Two optional handlers dispatch to
Game.<name>(): on_change fires whenever the buffer changes, and on_submit fires on Enter. The
game reads (and may set) the current value through the existing element.text accessor. This is the v1
field — selection, clipboard, multi-line, and IME composition are a documented follow-up (they do
not work yet).
Canvas (type = "canvas"): a region (with a background color) the game draws custom primitives
into with the Canvas API — the enabler for graph/diagram editors.
Unknown type values are dropped with a warning (forward-compatible); unknown fields are ignored.
Layout groups
Section titled “Layout groups”By default every element is positioned by its own anchors/offsets. A container element can instead
auto-position its direct children by flow — set layout on the container and the engine places each
child in turn, so rows, columns, and grids lay themselves out instead of being hand-placed.
layout("horizontal" | "vertical" | "grid") — the flow direction. Horizontal packs children left→right, Vertical top→bottom, and Grid placescolumnschildren per row then wraps to the next (each row is as tall as its tallest child). Absent (the default) leaves the container’s children on plain anchor positioning, unchanged.spacing(Num, px) — the gap between adjacent items.padding([L, R, T, B], px) — insets the content origin.L/Tseed the flow cursor;R/Bare reserved for a future width-constrained wrap.columns(Int, Grid only, ≥ 1) — how many items fill a row before it wraps.
A layout group sets only each child’s position — the child keeps its own size (from its
offset_min/max). Children of a flow-positioned element still anchor relative to that element’s resolved
rect, so layout groups compose with the anchor system: a grandchild can centre itself inside a
flow-placed cell. Adding layout is additive — a .ui with no layout field loads and positions exactly
as before.
[[element]]id = "menu"type = "panel"parent = "root"anchor_min = [0.5, 0.5] # a centered panel that holds the buttonsanchor_max = [0.5, 0.5]offset_min = [-120.0, -100.0]offset_max = [120.0, 100.0]layout = "vertical" # stack its direct children top-to-bottomspacing = 12.0 # 12 px between thempadding = [16.0, 16.0, 16.0, 16.0] # L, R, T, B inset (L/T seed the flow)
[[element]]id = "playBtn"type = "button"parent = "menu"anchor_min = [0.0, 0.0] # anchors/offsets still give the SIZE (200×44);anchor_max = [0.0, 0.0] # the vertical flow sets the POSITIONoffset_min = [0.0, 0.0]offset_max = [200.0, 44.0]Textured panels & 9-slice
Section titled “Textured panels & 9-slice”A panel or button can draw an image instead of (or tinted under) a flat color. There are two ways to
name the image, and a 9-slice mode that scales it without distorting its corners.
Referencing an image
Section titled “Referencing an image”- By path —
image = "ui/panel.png"(recommended). A project-relative path to a PNG in the mounted project. The engine resolves it to a texture on the firstUi.render(loaded through the PhysicsFS sandbox, so a path can never escape the project) and caches the handle on the element. This is the authorable form: paths are stable and live in the.uifile. - By handle —
texture = <int>. A raw renderer texture handle the game obtained at runtime. Handles aren’t stable across runs and can’t be authored by hand; preferimageunless you already hold a handle.
image and texture coexist for backward compatibility — every existing .ui that used a bare texture
loads and renders identically. When both are present the resolved image handle is used. PNG only for now
(PSD/XCF are deferred).
9-slice scaling
Section titled “9-slice scaling”Set nine_slice = true and border = [L, R, T, B] (a margin in texture pixels on each side) to scale
the image as a 9-slice: the four corners keep their pixel size, the four edges stretch along one axis
only (top/bottom horizontally, left/right vertically), and the center stretches both ways. This keeps rounded
corners and borders crisp while the panel resizes to any rect — the same image backs a small tooltip and a
full-screen frame without the border thickening or the corners smearing. With nine_slice = false (the
default) the image is drawn as a single stretched quad.
[[element]]id = "frame"type = "panel"parent = "root"anchor_min = [0.0, 0.0]anchor_max = [1.0, 1.0]offset_min = [24.0, 24.0]offset_max = [-24.0, -24.0]image = "ui/panel.png" # a mounted PNGnine_slice = trueborder = [16, 16, 16, 16] # L, R, T, B, in texture pixelsThe .ui.component sidecar
Section titled “The .ui.component sidecar”Slicing metadata is a property of the image, not of each .ui that uses it — so it can be authored once,
next to the asset, in a small TOML sidecar named <image path>.ui.component (e.g. a PNG at
ui/panel.png gets ui/panel.png.ui.component). It carries the same fields:
nine_slice = trueborder = [16, 16, 16, 16] # L, R, T, B in texture pixelsWhen a .ui element references an image by path, the engine loads any sidecar sitting beside it and
applies its slicing for values the .ui left at its defaults — so one image = "ui/panel.png" line
picks up 9-slice automatically, and many .ui files reuse the same asset without repeating the borders. An
explicit border in the .ui element wins over the sidecar (the sidecar can add slicing but not override an
element that set its own). The sidecar is optional, PNG-only, and untrusted-input-hardened at the parser
boundary (a missing or malformed file is ignored, borders are clamped ≥ 0). See the
UI 9-Slice Demo.
Custom drawing (Canvas)
Section titled “Custom drawing (Canvas)”A type = "canvas" element is a blank region. After Ui.render, draw into it with these static
methods, using canvas-local design-pixel coordinates (0, 0 is the canvas’s top-left) and
[r, g, b, a] (0..1) colours. The engine maps canvas-local coordinates to screen through the same
letterbox as the rest of the document, so custom drawing scales with it.
Ui.canvasRect(canvas, x, y, w, h, color)— a filled rectangle.Ui.canvasLine(canvas, x0, y0, x1, y1, thickness, color)— a thick line.Ui.canvasBezier(canvas, x0, y0, cx0, cy0, cx1, cy1, x1, y1, thickness, color)— a cubic bezier.Ui.canvasText(canvas, x, y, text, size, color)— text at(x, y).
Primitives composite in the retained-UI pass (over panels, under the immediate-mode Gui). See UI Canvas for a node-graph example.
Editor authoring
Section titled “Editor authoring”Tools (like apps/ui_editor) build and modify documents at runtime with an authoring tier. Every structural edit keeps the tree valid (a parent always precedes its children) and invalidates existing element handles, so re-fetch by id after an edit.
Ui.newDocument(w, h)→ an emptyUiDocumentwith the given design size.Ui.createElement(doc, type, parentId)→ a newUiElementof"panel"/"text"/"button"underparentId(""= root).Ui.deleteElement(element)— remove an element and its whole subtree.Ui.reparent(element, newParentId)→Bool— move an element (""= root);falseif it would form a cycle.Ui.save(doc, path)→Bool— serialize and write the document to a mounted-project path.Ui.hitTest(doc, x, y)→UiElementornull— the topmost element at a design-space point (call afterrender/renderEdithas laid the document out; convert a screen click to design space with the content rect those return).Ui.renderEdit(doc, x, y, w, h)— likerender, but in edit mode: a click selects rather than fires, for a visual editor.
On UiDocument: elementAt(i) (the i-th element, for enumerating a tree). On UiElement: type,
parentId, id = (String), fontSize / fontSize=, onClick, rect / rect = (List) (the
RectTransform as a flat 10-list — anchorMin.xy, anchorMax.xy, offsetMin.xy, offsetMax.xy,
pivot.xy), and color / color = (List) ([r, g, b, a], the element’s main colour).
See also
Section titled “See also”-
UI Editor (
apps/ui_editor) — the WYSIWYG visual editor built on this tier. -
UI Demo (
apps/ui_demo) — a.uicard with a title, subtitle, and an interactive button, rendered full-window. -
UI 9-Slice Demo (
apps/ui_nineslice_demo) — two different-sized frames share oneimage-by-path PNG + a.ui.componentsidecar, so their corners stay unstretched. -
UI Input Demo (
apps/ui_input_demo) — editabletype = "input"fields: a Name field with initial text and a City field showing a placeholder, focused by click. -
Gui — the immediate-mode (Nuklear) GUI, for tools and debug overlays.
-
Text — the SDF text pass the UI’s labels reuse.