ui
UI system — screens, styles, themes, widget responses, focus, custom-widget builders, per-widget state.
require("@builtin::modules.api.engine.ui") -- ui (also available as global 'ui')
The ui namespace: 216 functions.
globals/ui/blur
ui.blur()
Surrender keyboard focus from whichever widget currently holds it, in the editor's UI and the game's alike.
globals/ui/bringAreaToFront
ui.bringAreaToFront(id: string)
Raise a movable area to the top of the window stacking order —
the programmatic equivalent of clicking it. Areas sharing a stacking
band order by interaction, so this is the call that brings one forward
from code: use it when a taskbar button, focus change, or app launch
should raise a window. Moving a screen to a higher layer band raises
it over the bands below.
Parameters
idstring— Area widget id.
globals/ui/captureWindow
ui.captureWindow(screen: string, window: string, opts: CaptureOpts?) -> CaptureResult?
Render a single Window widget to its own offscreen texture
and write the result as PNG at
/runtime/render_surfaces/<rtHandle>.png. The screen does NOT
need to be visible. Returns { rtHandle, texturePath } or nil
on invalid inputs (width/height clamped to [1, 8192],
defaults 600x400).
Parameters
screenstring— Screen id containing the target Window.windowstring— Widget id of the Window.optsCaptureOpts(optional) —{ width, height }(optional).
Returns CaptureResult? — { rtHandle, texturePath } or nil.
globals/ui/click
ui.click(callbackId: string, value: any?) -> boolean
Simulate a widget click / interaction by its callback id. The call
carries no screen, so an id that names widgets on several screens reaches
every component that declared it, once each. The callback id is the one
a widget emits: the string its onClick / onChange / other on* prop
names, or its element id where it leaves that prop unnamed (a button
clicked without an onClick, a slider changed without an onChange).
Given the element id of a widget whose interactions emit other ids, it
raises an error naming those ids, and clicks nothing.
Parameters
callbackIdstring— Callback id the widget emits (itsonClick/onChange/on*prop).valueany(optional) — Optional value the callback receives asdata.value: a number, a string, a boolean, or a table with the fields a structured interaction delivers (a code editor's{ token, line, col }, a canvas drag's deltas).
Returns boolean — true when a widget on a registered screen emits the id; false when none does, and the click then still reaches every ui.registerCallbackEnv env and every component and scene entrypoint that receives callbacks no screen claims.
globals/ui/copyText
ui.copyText(text: string)
Put text on the host clipboard — the operating system's clipboard
on a native build, the browser's in a web build. What a "Copy path" or
"Copy reference" action calls.
Parameters
textstring— The text to copy.
ui.copyText("/zero/source/scenes/main.scene")
globals/ui/defineStyle
ui.defineStyle(name: string, style: StyleProps)
Define a named style. Style keys follow
<widgetType>.<className> (e.g. "label.h1", "button.primary")
or bare <className> to apply across widget types. Widgets
reference styles via the classes (or class) prop.
Parameters
namestring— Style name.styleStyleProps— Style properties table.
globals/ui/defineStyles
ui.defineStyles(styles: { [string]: StyleProps })
Define multiple named styles at once.
Parameters
styles{ [string]: StyleProps }— Map of style name to style properties.
globals/ui/defineWidget
ui.defineWidget(name: string, builderFn: (WidgetTree, { WidgetTree }) -> WidgetTree)
Register a custom widget kind. When a tree contains
{ type = name, props = ..., children = ... }, the decoder
calls builderFn(props, children) at register / update time and
substitutes the returned widget table in place. Errors surface
through ui.lastValidation() with codes widget-builder-error
/ widget-builder-bad-return / decode-recursion-depth-exceeded.
Parameters
namestring— Custom widget kind name.builderFn(WidgetTree, { WidgetTree }) -> WidgetTree— Builder closure(props, children) -> widgetTable.
globals/ui/diagnose
ui.diagnose(widgetId: string) -> WidgetPaint?
Why one widget did or did not reach the last frame. Returns that
widget's row from ui.observe() — the same fields, resolved against the
same reading. An id no registered screen carries reads noSuchWidget,
which is how a misspelling separates from a widget whose screen is
hidden and from one the frame laid out no box for.
Parameters
widgetIdstring— The id the widget records layout under.
Returns WidgetPaint? — The widget's row, or nil before the UI has published a frame.
local row = ui.diagnose("hud-healthbar")
globals/ui/displayScale
ui.displayScale() -> DisplayScale
The physical scale the UI is sized at. density is the physical
pixels one density-independent point covers on a handheld display (a
phone, a tablet, a page in a touch browser): a point of the UI covers at
least that many pixels whatever the screen's width or orientation, so
text and buttons keep their physical size. It is nil on a desktop
window, whose UI is sized by its width alone. textScale is the user's
text size setting, which multiplies every font size the UI typesets
(labels, buttons, inputs, editor chrome; canvas text is drawn at the
size it names). touchTarget is the smallest target, in points, a touch
reaches an interactive widget within: 48 on a handheld display, 0 on a
desktop. pixelRatio is ui.pixelRatio() of the same frame.
ui.simulateDisplayScale previews a phone's on any engine.
Returns DisplayScale — { density?, textScale, touchTarget, pixelRatio }.
local scale = ui.displayScale()
local buttonHeight = math.max(32, scale.touchTarget)
globals/ui/dragState
ui.dragState() -> { payload: string, x: number, y: number, label: string?, ghost: boolean }?
The in-flight drag-and-drop payload while a dragPayload widget is
being dragged, else nil. x/y are the pointer's position in the
logical space ui.getLayoutInfo rects live in, so the reading resolves
directly against widget rects. label is the text the drag ghost beside
the pointer reads (dragLabel, else the source's text; nil for an
outline ghost), and ghost whether the engine draws one (dragGhost) —
a capture never shows the ghost, so this is how a drag's name-at-cursor
is checked. Poll during a drag to drive live feedback; the drop itself
still lands through the target's onDrop. Snapshotted each frame.
Returns { payload: string, x: number, y: number, label: string?, ghost: boolean }? — { payload, x, y, label?, ghost } during a drag, nil otherwise.
globals/ui/elementTree
ui.elementTree(screenName: string) -> ElementNode?
Introspect a screen's rendered widget hierarchy with each
element's layout rect. Every node the renderer draws appears, nested
exactly as the widgets nest, under the id it records layout against:
the id set on the node when the author gave it one, otherwise
<screen>/<type>@<path>. bounds is that element's rect — the same
table ui.getLayoutInfo(id) returns — and appears once the element has
been measured. Kinds registered through ui.defineWidget appear
expanded into the primitives they build. Feeds the gui.captureElement
tool: list the tree, pick the ids to frame, capture their region.
Parameters
screenNamestring— Screen id passed toui.registerScreen.
Returns ElementNode? — An ElementNode tree, or nil when no screen is registered under that name.
globals/ui/focus
ui.focus(widgetId: string)
Programmatically request keyboard focus on a widget. Queued
as a one-shot; the next render of the matching widget calls
response.request_focus().
Parameters
widgetIdstring— Widget id to focus.
globals/ui/focusedWidget
ui.focusedWidget() -> (string?, string?)
Return the widget id of whichever widget currently holds
keyboard focus, or nil, and the space it draws in: "window" for the
editor's UI around the game view, "camera" for the game's own UI in
the game view. Snapshotted post-render each frame.
Returns (string?, string?) — Focused widget id or nil, and its space.
local id, space = ui.focusedWidget()
globals/ui/getAreaPos
ui.getAreaPos(id: string) -> AreaPos?
Read the current pivot position of an area widget,
including any user drag deltas. Returns { x, y } or nil if
the area didn't render this frame.
Parameters
idstring— Area widget id.
Returns AreaPos? — { x, y } or nil.
globals/ui/getAreaSize
ui.getAreaSize(id: string) -> AreaSize?
Read the measured size of an area widget, including any user
resize-grip drags if the area is resizable. Returns { w, h }
or nil if the area didn't render this frame.
Parameters
idstring— Area widget id.
Returns AreaSize? — { w, h } or nil.
globals/ui/getDockLayout
ui.getDockLayout(id: string) -> string?
Read the current serialized layout (split/tab arrangement) of
a dockArea widget as a JSON string. Returns nil if the dockArea
didn't render this frame. Persist the string and pass it back via
the dockArea's layout prop to restore the arrangement.
Parameters
idstring— DockArea widget id.
Returns string? — Serialized DockState JSON string, or nil.
globals/ui/getLayoutInfo
ui.getLayoutInfo(widgetId: string?) -> LayoutInfo?
Get layout info (position, size, content bounds) for UI
containers. If widgetId is given, returns info for that
widget only; otherwise returns all.
Parameters
widgetIdstring(optional) — Optional widget id to query.
Returns LayoutInfo? — Layout info table or nil.
globals/ui/getPersistentDockLayout
ui.getPersistentDockLayout(id: string) -> string?
Read a dockArea's arrangement as a JSON string with every tab
whose dockPanel is transient left out — the form to save and
restore across sessions. Returns nil if the dockArea didn't render
this frame.
Parameters
idstring— DockArea widget id.
Returns string? — Serialized DockState JSON string without transient tabs, or nil.
globals/ui/getScreenTree
ui.getScreenTree(screenName: string) -> WidgetTree?
Return the last widget tree table passed to
registerScreen / updateScreen for screenName.
Parameters
screenNamestring— Screen name to query.
Returns WidgetTree? — Widget tree or nil.
globals/ui/getTheme
ui.getTheme() -> string
Get the name of the currently active theme.
Returns string — Active theme name.
globals/ui/getToken
ui.getToken(name: string) -> string?
Look up a single design token value from the active theme.
Parameters
namestring— Token name (without$prefix).
Returns string? — Token value or nil.
globals/ui/getTokens
ui.getTokens() -> { [string]: string }
Get all design tokens from the active theme as a key-value map.
Returns { [string]: string } — Token map.
globals/ui/getWidgetProps
ui.getWidgetProps(typeName: string) -> { WidgetPropDescriptor }?
Get the property definitions for a widget type.
Parameters
typeNamestring— Widget type name.
Returns { WidgetPropDescriptor }? — Array of property descriptors, or nil if type not found.
globals/ui/getWidgetTypes
ui.getWidgetTypes() -> { string }
Get all available widget type names that can be used in widget trees.
Returns { string } — Array of widget type names.
globals/ui/hideScreen
ui.hideScreen(name: string) -> boolean
Hide a registered screen, and report whether a screen by that name
is registered. The engine applies the hide later in the frame;
listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to hide.
Returns boolean — True when a screen by this name is registered.
globals/ui/hitTest
ui.hitTest(x: number, y: number) -> PaintHitTest?
Which widget a pointer at (x, y) reaches, and the stack beneath it.
Coordinates are logical points on the window, the space getLayoutInfo
and gui.bounds rects are in wherever a widget's screen draws (a screen
in the Scene view's picture included). A widget that takes no
pointer, a canvas declaring pointerEvents = "none", is passed over: the
answer is the widget beneath it.
Parameters
xnumber— Logical X.ynumber— Logical Y.
Returns PaintHitTest? — { widget, screen, stack, x, y } — widget nil when the point is over no UI — or nil before the UI has published a frame.
local hit = ui.hitTest(640, 360)
globals/ui/idCollisions
ui.idCollisions() -> { [string]: { string } }
Element ids more than one screen recorded, each mapped to the
screens that recorded it. An id addresses one element: two screens
sharing one leave ui.getLayoutInfo and the gui element tools
answering for whichever painted last, and the gui tools refuse such
an id. Empty while every id is unique.
Returns { [string]: { string } } — Colliding id -> the screens that recorded it.
for id, screens in ui.idCollisions() do print(id, table.concat(screens, ", ")) end
globals/ui/invisibilityReasons
ui.invisibilityReasons() -> { string }
Every verdict ui.diagnose can report, as a closed list.
Returns { string } — The reason names.
globals/ui/lastRegistration
ui.lastRegistration() -> { name: string, layer: number? }?
The name and layer passed to the most recent ui.registerScreen
call, recorded synchronously at call time. A host that mounts a nested
app reads this immediately after the mount to learn which screen the
nested code registered, without intercepting the ui table.
Returns { name: string, layer: number? }? — { name, layer } for the last registration, or nil if none yet.
globals/ui/lastValidation
ui.lastValidation(screenName: string?) -> any
Validation diagnostics produced at the most recent
registerScreen / updateScreen, plus what the render stage
found while painting — unknown-font-family reports a
style.fontFamily that named no registered font family, once
per family per screen, wrap-label-no-width reports a
label with props.wrap whose box came out narrower than its
own longest word, once per label per screen, and
svg-not-as-declared reports an svg widget whose document
declared something the picture does not hold — a <text>
whose font-family selected no registered family, a <text>
the picture holds no glyph of, or a shape whose url(#id)
paint resolved to nothing — once per finding per widget per
screen. With no args
returns a
{ [screen] = entry } map; with a name returns that screen's
entry or nil. Validation gated by world setting
ui.validation = "off" | "warn" | "strict" (default "warn").
Parameters
screenNamestring(optional) — Optional screen name.
Returns any — Validation entry, full map, or nil.
globals/ui/listFonts
ui.listFonts() -> { FontFamilyInfo }
Every font family a style.fontFamily can select. Read from
the registry the UI text renderer resolves a family token
through, so a family this returns is one a label renders in.
family and every name in aliases are accepted as a
fontFamily, case-insensitively; aliases carries the
web-font names, CSS generic families and face names that
select the same group. faces names the concrete face in each
weight/style slot, so a fontWeight = 700 against a family
with no bold face gets a synthesised heavy. more lists the
other faces registered in the family's slots, each drawing the
codepoints the slot's face lacks: the installed chunks of an
on-demand face (font.onDemand() lists them all). system = true
marks a family taken from the host OS — present on this
machine, absent on one without it, and absent on WASM — so a
UI that must look the same everywhere picks a family with
system = false. A fontFamily naming nothing in this list
is reported as an unknown-font-family warning through
ui.lastValidation(screen) once the screen paints, and the
text renders in the default proportional face.
Returns { FontFamilyInfo } — Array of { family, aliases, faces, more, system }, by family.
for _, f in ui.listFonts() do print(f.family) end
globals/ui/listScreens
ui.listScreens() -> { ScreenSummary }
List every registered screen with its current visibility,
z-order layer, renderLayer (the render-layer membership mask it
draws under: the mask setScreenRenderLayer stated, else the one the
surface setScreenSpace named implies, else the ui bit),
editorChrome (whether the mask setScreenRenderLayer stated carries
the EditorUI bit, which makes the screen the editor's own chrome; a
screen placed on the window without stating it is not chrome), whether
the screen has a populated root widget tree, and the space it draws in
("window" for the editor's UI, "camera" for the game's),
including the register / show / hide / unregister calls the running
script has already made. Sorted by layer ascending, then name.
Returns { ScreenSummary } — Array of screen summaries.
globals/ui/listThemes
ui.listThemes() -> { string }
List all registered theme names.
Returns { string } — Array of theme names.
globals/ui/lockOrientation
ui.lockOrientation(lock: OrientationLock)
Hold the display to the orientations lock allows: "landscape"
(both landscape directions, so turning the device over turns the app
with it), "portrait" (both portrait directions) or "any" (the display
follows the device). A world states the orientation it plays in as
world.orientation, which the engine locks to in play mode and releases
in edit mode, so the editor follows the device; call this to change it
while the world runs. Android turns the app at once, a browser grants the
lock only to a page in fullscreen in most cases (the page asks again each
time it enters fullscreen), and a desktop window presents in the shape it
is given. ui.orientation() reports the lock and whether the platform
holds it from the next frame. Any other value is refused by name.
Parameters
lockOrientationLock—"any","landscape"or"portrait".
ui.lockOrientation("portrait")
ui.lockOrientation("any")
globals/ui/missingGlyphs
ui.missingGlyphs() -> { string }
The characters the UI is showing without a glyph: each appears in
text a UI surface (the window, the game view, a camera's UI overlay, a
screen drawn into a texture) drew in what it last painted, and no face of
the family it was drawn in has a glyph for it, so it shows as the
replacement box.
Every family falls back glyph by glyph to every face the UI holds, so a
character here is one no installed face covers. A character a declared
on-demand face covers (font.onDemand()) requests that face the frame
it is met, and leaves this list once the face is installed and the next
frame draws it.
Returns { string } — Sorted array of single-character strings; empty when every character drawn had a glyph.
if #ui.missingGlyphs() > 0 then print(table.concat(ui.missingGlyphs(), " ")) end
globals/ui/observe
ui.observe(screenName: string?) -> PaintObservation?
What the last UI frame painted. Returns
{ generation, viewport, pointer, pointerOverUi, pointerWidget, widgets }
with one widgets row per widget any registered screen holds — its
layout box, the clip chain it painted under, the part of that box which
reached the frame (visible), the order it painted in (paintIndex),
and its reason from the closed set ui.invisibilityReasons() lists.
Every rect, the reading's viewport (the window) and pointer are in
logical points on the window; a row's viewport is the region the pass
that drew it paints into (the whole window, or the rect a Scene panel
presents the camera's picture in), and offViewport is judged against it.
generation advances once per re-rendered frame, so two calls reporting
the same number describe the same frame.
Parameters
screenNamestring(optional) — Narrow the rows to one screen. Omit for every screen.
Returns PaintObservation? — The reading; nil before the UI has published a frame, and nil for a screenName no registered screen answers to.
local reading = ui.observe("hud")
globals/ui/onOrientationChange
ui.onOrientationChange(callback: (reading: OrientationReading) -> ()) -> () -> ()
Call callback with the new reading every time ui.orientation()
changes: the display turns between landscape and portrait or to another
rotation, the lock changes, or the platform starts or stops holding it.
Each listener is watched by a task of the context that registered it, so
a listener a component registered ends when that component is removed,
and every other listener goes on hearing changes.
Parameters
callback(reading: OrientationReading) -> ()— Receives the reading, asui.orientation()returns it.
Returns () -> () — A function that removes this listener.
local off = ui.onOrientationChange(function(o) print(o.current, o.rotation) end)
globals/ui/orientation
ui.orientation() -> OrientationReading
The orientation the display presents the app in, and the lock it is
held to. current is "landscape" (wider than tall) or "portrait":
the shape of the window the app presents in. lock is what
ui.lockOrientation last asked, "any" when the display follows the
device. enforced is whether the platform holds the display to it:
Android always does, a browser does once it grants the Screen
Orientation lock, and a desktop window never does, presenting in the
shape it is given; reason names why when it does not. rotation is the
display's rotation from its natural orientation in degrees (0, 90, 180,
270) where the platform states one, and nil on a desktop window.
revision advances every time any of these changes; ui.onOrientationChange
calls back on each change.
Returns OrientationReading — { current, lock, enforced, reason?, rotation?, revision }.
if ui.orientation().current == "portrait" then print("lay the HUD out for a tall screen") end
globals/ui/paintOrder
ui.paintOrder(a: string, b: string) -> number?
Which of two widgets paints later: -1 when a paints before b,
1 when after, 0 when level. This is what separates two widgets whose
rects are identical.
Parameters
astring— First widget id.bstring— Second widget id.
Returns number? — -1, 0, 1, or nil when the reading holds no row for one of them.
local order = ui.paintOrder("panel-a", "panel-b")
globals/ui/pixelRatio
ui.pixelRatio() -> number
Physical pixels per logical point — the factor between the logical
space ui.screenSize() / getLayoutInfo rects live in and the physical
space input.mousePosition, the camera viewport rect and
input.simulateMouse* coordinates live in. Multiply a layout coordinate
by this to aim a simulated pointer at a widget.
Returns number — Physical pixels per logical point (1.0 when unscaled).
globals/ui/pointerWidget
ui.pointerWidget() -> PointerRead?
Whether the UI is consuming the pointer, and which widget holds it —
the pointer counterpart of ui.focusedWidget(). x, y are in logical
points on the window, the space getLayoutInfo rects are in.
Returns PointerRead? — { x, y, overUi, widget, screen }, or nil before the UI has published a frame.
globals/ui/registerBackgroundShader
ui.registerBackgroundShader(shaderHandle: any?, width: number?, height: number?)
Register a screen-domain .shader as a UI background, drawn
via the backgroundShader style. Takes the shader's asset handle
from asset.resolve. A shader whose compile failed
(shaderHandle:compileStatus() reads failed), or whose program
the compile gate refuses, is not registered: the call raises an
error naming the shader and the compiler's message, the one
compileStatus().error reads. A shader whose compileStatus()
still reads pending raises too: register it once it reads
compiled.
Parameters
shaderHandleany(optional) — The screen.shader's asset handle, fromasset.resolve.widthnumber(optional) — Render target width (default 1280).heightnumber(optional) — Render target height (default 720).
globals/ui/registerCallbackEnv
ui.registerCallbackEnv(key: string, env: { [string]: any })
Register an environment table to receive widget-callback
broadcasts: its global onCallback(id, value) fires for any widget
callback not owned by a specific component instance — the same
broadcast a component's onCallback receives. Keyed by key;
re-registering the same key replaces the previous env. A component
instance is folded into the callback dispatch automatically, so reach
for this from a non-component context that hosts a UI surface (a scene
entrypoint registering its own screen). Pair with
ui.unregisterCallbackEnv(key) so the ref is released.
Parameters
keystring— Stable identifier for this registration (re-register replaces).env{ [string]: any }— Environment table whoseonCallbackreceives the broadcasts.
globals/ui/registerScreen
ui.registerScreen(name: string, widgetTree: WidgetTree, layer: number?)
Register a named UI screen with a widget tree. Optional
layer controls z-ordering (higher = on top), in bands: below 0
behind everything, 0-99 ordinary app depth, 100-999 always-on-top
chrome, 1000+ menu and popup depth. A screen in a higher band
covers one in a lower band whatever their roots are; inside a band
a floating area or window root sits over ordinary content, and
a modal root sits over the whole stack. Tag-based
grouping lives in Z.tags (Z.tags.set(name, { "editor" })
after register). A screen belongs to the component instance whose
own code registers it (its hooks and methods, and any closure they
create, such as a listener it subscribes): that instance's
onCallback receives its callbacks, and the screen is unregistered
when the instance ends. Code that is not an instance's own (a scene
entrypoint, a module, a listener either subscribed) registers a
screen no instance owns, which stays until it is unregistered,
whichever component's hook ran that code. topics/ui has the rule.
Parameters
namestring— Unique screen identifier.widgetTreeWidgetTree— Root widget table.layernumber(optional) — Z-order layer (optional).
ui.registerScreen("hud", tree)
globals/ui/registerTheme
ui.registerTheme(name: string, theme: ThemeDefinition)
Register a theme from a flat Luau table. Most callers
should use Z.theme.register(name, table) which runs the
cascade for them.
Parameters
namestring— Theme name to register.themeThemeDefinition— Flat-resolved theme table.
globals/ui/removeScreen
ui.removeScreen(name: string) -> boolean
Alias for ui.unregisterScreen.
Parameters
namestring— Screen identifier to remove.
Returns boolean — True when a screen by this name was registered.
globals/ui/resetAreaSize
ui.resetAreaSize(id: string)
Clear a resizable area's remembered size (from a grip drag or
ui.setAreaSize) so its declared — or content — size takes over again.
Parameters
idstring— Area widget id.
globals/ui/response
ui.response(widgetId: string) -> WidgetResponse?
Per-widget interaction snapshot for the most recent frame.
Returns { clicked, hovered, focused, changed, value } where
clicked / changed mark transitions and hovered / focused
mark current state.
Parameters
widgetIdstring— The widget id (NOT the onClick / onChange callback id).
Returns WidgetResponse? — WidgetResponse or nil.
globals/ui/safeArea
ui.safeArea() -> SafeArea
The part of the screen no system bar, display cutout or on-screen
keyboard covers, in the logical points ui.screenSize() reports.
top / right / bottom / left are how far in from each edge the
covered band reaches; x / y / width / height are the rect they
leave; cutouts lists each display cutout (a camera hole or a notch) as
{ x, y, width, height }. Screens lay out inside this rect, so anchors,
top-level % sizes and a window's default position already keep clear
of it; read it to place something by absolute pos, or to run a
background under the bars while its controls stay inside. revision
advances every time the area changes (a rotation, the system bars shown
or hidden, the keyboard opening, a window resize): compare it with the
one you last laid out against. All zero on a desktop window;
ui.simulateSafeArea lays the UI out as a phone would on any engine.
Returns SafeArea — { top, right, bottom, left, x, y, width, height, cutouts, revision } in logical UI points.
local safe = ui.safeArea()
ui.setAreaPos("hud", safe.x + 8, safe.y + 8)
globals/ui/screen
ui.screen(name: string) -> { [string]: any }?
Get a screen proxy with methods like setResolution and
rasterize.
Parameters
namestring— Screen name.
Returns { [string]: any }? — Screen proxy table, or nil.
globals/ui/screenSize
ui.screenSize() -> { width: number, height: number }
The UI coordinate space as { width, height } (logical points). This is
the space area pos, anchors, and getLayoutInfo rects use — and it is
NOT the pixel size of a capture screenshot, which may be downscaled. Use
this for absolute area positioning (e.g. pinning a menu above a bottom
taskbar) instead of guessing the size from a capture image.
It follows the size the engine draws at, and renderer.setViewportSize
changes that size for the task that asks, which is how a layout written
against this is checked at a second shape without rebooting the engine,
read in the call that set the size before the surface goes back. The pixel size
behind these points is renderer.surfaceSize() — the whole drawing
surface a screen is laid out over, which an editor layout makes larger
than the rect renderer.viewportSize() draws the scene into — and
ui.pixelRatio() is the factor between the two spaces.
Returns { width: number, height: number } — { width, height } in logical UI points.
globals/ui/scroll
ui.scroll(deltaX: number, deltaY: number)
Simulate a mouse-wheel scroll event on the UI.
Parameters
deltaXnumber— Horizontal scroll delta.deltaYnumber— Vertical scroll delta.
globals/ui/setAreaPos
ui.setAreaPos(id: string, x: number, y: number)
Programmatically move a movable area widget to (x, y).
Applied for one frame; subsequent frames let drag tracking
take over.
Parameters
idstring— Area widget id.xnumber— Target pivot x (screen coords).ynumber— Target pivot y (screen coords).
globals/ui/setAreaSize
ui.setAreaSize(id: string, w: number, h: number)
Programmatically set a resizable area's size (the user-size
override) — for maximize / restore / tile. Persists until the area's
declared width/height changes or ui.resetAreaSize(id) clears it.
Parameters
idstring— Area widget id.wnumber— Target width (screen coords).hnumber— Target height (screen coords).
globals/ui/setDockWindowRect
ui.setDockWindowRect(dockId: string, panelId: string, x: number, y: number, width: number, height: number)
Place the floating window of a dockArea panel at (x, y) with
size (width, height). Applies once the panel occupies a window —
a request made before then waits for it.
Parameters
dockIdstring— DockArea widget id.panelIdstring— Id of the panel held by the window to place.xnumber— Window left edge (screen coords).ynumber— Window top edge (screen coords).widthnumber— Window width (screen coords).heightnumber— Window height (screen coords).
globals/ui/setScreenRenderLayer
ui.setScreenRenderLayer(name: string, mask: number)
Set a screen's render-layer membership bitmask. A screen draws into a
camera or capture only when this mask intersects the camera's include
mask, the same rule geometry follows. Content UI carries the ui bit;
the editor places its chrome on EditorUI so agent captures can drop it,
and a screen carrying that bit is part of the window rather than the
camera's picture. Stating a mask here fixes the membership: a screen left
to itself takes the one the surface it is placed on implies, EditorUI
for the window and ui for the camera's picture. __renderLayers.bit(name)
answers with a layer's bit INDEX, so a mask sets the bit at that index and
__renderLayers.names(mask) reads it back as the names it holds.
Parameters
namestring— Screen identifier.masknumber— Render-layer membership bitmask — the OR ofbit32.lshift(1, __renderLayers.bit(name))over the layers the screen is on.
ui.setScreenRenderLayer("hud", bit32.lshift(1, __renderLayers.bit("ui")))
globals/ui/setScreenSpace
ui.setScreenSpace(name: string, space: string)
Set which surface a screen is part of. "camera" is the picture the
viewport camera produces: the screen lays out over the camera's target
and is drawn into it, so it sits inside the Scene panel presenting the
scene and appears in every capture of the camera — the default for
content UI. "window" is the window itself, whatever presents the scene
— where the editor's chrome lives, and the default for a screen on the
EditorUI layer. The two are one statement made from either end, so a
screen placed on the window takes the EditorUI membership unless
setScreenRenderLayer states another: the window goes on drawing it while
a capture that drops the authoring layers, which is what an agent takes by
default, leaves it out. Where the scene presents on the window, the two
surfaces coincide. A name that is neither raises.
Parameters
namestring— Screen identifier.spacestring—"camera"|"window".
ui.setScreenSpace("agent-banner", "window")
globals/ui/setScrollPosition
ui.setScrollPosition(widgetId: string, offsetY: number)
Set the scroll offset of a scrollArea widget.
Parameters
widgetIdstring— Scroll area widget id.offsetYnumber— Vertical scroll offset in pixels.
globals/ui/setShaderUniforms
ui.setShaderUniforms(name: string, uniforms: { [string]: number })
Set uniform values on a registered background shader.
Parameters
namestring— Shader name identifier.uniforms{ [string]: number }— Map of uniform name to number value.
globals/ui/setTheme
ui.setTheme(name: string)
Switch the active global theme by name.
Parameters
namestring— Theme name to activate.
globals/ui/showScreen
ui.showScreen(name: string) -> boolean
Make a registered screen visible, and report whether a screen by
that name is registered. The engine applies the show later in the
frame; listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to show.
Returns boolean — True when a screen by this name is registered.
globals/ui/simulateDisplayScale
ui.simulateDisplayScale(scale: DisplayScaleSpec?)
Size the UI as a display stating scale would, in place of what the
platform states. A density sizes it as a handheld display of that
many physical pixels per point (omit it for a desktop display sized by
its width), and textScale multiplies every font size the UI typesets
(1 when omitted). Previews a phone's UI on a desktop or headless engine,
and makes a layout test independent of the device it runs on.
ui.displayScale(), ui.screenSize() and ui.pixelRatio() report it
from the next frame. Call it with no argument (or nil) to return to
the platform's own scale. A value that is not a number above 0 and at
most 8 is refused by name.
Parameters
scaleDisplayScaleSpec(optional) —{ density?, textScale? }; nil returns to the platform's scale.
ui.simulateDisplayScale({ density = 2.625, textScale = 1.3 })
ui.simulateDisplayScale()
globals/ui/simulateSafeArea
ui.simulateSafeArea(area: SafeAreaSpec?)
Lay the UI out as if the system covered the screen by area
(logical points) in place of what the platform states: each inset
defaults to 0 and cutouts is a list of { x, y, width, height }.
Previews a phone's status bar, camera cutout and navigation bar on a
desktop or headless engine, and makes a layout test independent of the
device it runs on. ui.safeArea() reports it from the next frame. Call
it with no argument (or nil) to return to the platform's own safe
area. A negative or non-number inset is refused by name.
Parameters
areaSafeAreaSpec(optional) —{ top?, right?, bottom?, left?, cutouts? }in logical points; nil returns to the platform's safe area.
ui.simulateSafeArea({ top = 40, bottom = 30, cutouts = { { x = 600, y = 0, width = 80, height = 40 } } })
ui.simulateSafeArea()
globals/ui/unregisterCallbackEnv
ui.unregisterCallbackEnv(key: string)
Remove an environment registered with ui.registerCallbackEnv. Its
onCallback stops receiving broadcasts. No-op if key isn't registered.
Parameters
keystring— The key passed toui.registerCallbackEnv.
globals/ui/unregisterScreen
ui.unregisterScreen(name: string) -> boolean
Remove a screen from the registry entirely. Unlike
hideScreen, this deletes the entry so it no longer appears in
listScreens or render iteration.
Parameters
namestring— Screen identifier to unregister.
Returns boolean — True when a screen by this name was registered.
globals/ui/unregisterWidget
ui.unregisterWidget(name: string)
Drop a registered custom widget kind. Subsequent references
produce an unknown-widget-type diagnostic.
Parameters
namestring— Custom widget kind name.
globals/ui/updateScreen
ui.updateScreen(name: string, widgetTree: WidgetTree)
Replace the widget tree of an already-registered screen.
Parameters
namestring— Screen identifier to update.widgetTreeWidgetTree— New root widget table.
globals/ui/useStyles
ui.useStyles(themeName: string)
Apply a registered style file's classes additively without changing the active theme.
Parameters
themeNamestring— Name of the registered style / theme asset.
globals/ui/widgetState
ui.widgetState(widgetId: string, key: string, default: any?) -> any
Read per-widget cross-frame state. Returns the value
previously written via widgetStateSet, or default (or nil).
State is keyed by widget id and persists across re-renders
within a screen's lifetime; cleared automatically when the
owning screen is unregistered.
Parameters
widgetIdstring— Widget id whose state to read.keystring— State key.defaultany(optional) — Value to return when nothing has been written.
Returns any — Stored value, default, or nil.
globals/ui/widgetStateClear
ui.widgetStateClear(widgetId: string, key: string)
Remove a per-widget state entry.
Parameters
widgetIdstring— Widget id whose state to clear.keystring— State key.
globals/ui/widgetStateSet
ui.widgetStateSet(widgetId: string, key: string, value: any?)
Write per-widget cross-frame state. Replaces any existing
value under (widgetId, key). Tables are stored by reference.
Parameters
widgetIdstring— Widget id to scope the state under.keystring— State key.valueany(optional) — Value to store (must be non-nil).
modules/ui/blur
blur()
Surrender keyboard focus from whichever widget currently holds it, in the editor's UI and the game's alike.
modules/ui/bringAreaToFront
bringAreaToFront(id: string)
Raise a movable area to the top of the window stacking order —
the programmatic equivalent of clicking it. Areas sharing a stacking
band order by interaction, so this is the call that brings one forward
from code: use it when a taskbar button, focus change, or app launch
should raise a window. Moving a screen to a higher layer band raises
it over the bands below.
Parameters
idstring— Area widget id.
modules/ui/captureWindow
captureWindow(screen: string, window: string, opts: CaptureOpts?): CaptureResult?
Render a single Window widget to its own offscreen texture
and write the result as PNG at
/runtime/render_surfaces/<rtHandle>.png. The screen does NOT
need to be visible. Returns { rtHandle, texturePath } or nil
on invalid inputs (width/height clamped to [1, 8192],
defaults 600x400).
Parameters
screenstring— Screen id containing the target Window.windowstring— Widget id of the Window.optsCaptureOpts?(optional) —{ width, height }(optional).
modules/ui/click
click(callbackId: string, value: any?): boolean
Simulate a widget click / interaction by its callback id. The call
carries no screen, so an id that names widgets on several screens reaches
every component that declared it, once each. The callback id is the one
a widget emits: the string its onClick / onChange / other on* prop
names, or its element id where it leaves that prop unnamed (a button
clicked without an onClick, a slider changed without an onChange).
Given the element id of a widget whose interactions emit other ids, it
raises an error naming those ids, and clicks nothing.
Parameters
callbackIdstring— Callback id the widget emits (itsonClick/onChange/on*prop).valueany?(optional) — Optional value the callback receives asdata.value: a number, a string, a boolean, or a table with the fields a structured interaction delivers (a code editor's{ token, line, col }, a canvas drag's deltas).
modules/ui/copyText
copyText(text: string)
Put text on the host clipboard — the operating system's clipboard
on a native build, the browser's in a web build. What a "Copy path" or
"Copy reference" action calls.
Parameters
textstring— The text to copy.
ui.copyText("/zero/source/scenes/main.scene")
modules/ui/defineStyle
defineStyle(name: string, style: StyleProps)
Define a named style. Style keys follow
<widgetType>.<className> (e.g. "label.h1", "button.primary")
or bare <className> to apply across widget types. Widgets
reference styles via the classes (or class) prop.
Parameters
namestring— Style name.styleStyleProps— Style properties table.
modules/ui/defineStyles
defineStyles(styles: { [string]: StyleProps })
Define multiple named styles at once.
Parameters
styles{ [string]: StyleProps }— Map of style name to style properties.
modules/ui/defineWidget
defineWidget(name: string, builderFn: (WidgetTree, { WidgetTree }) -> WidgetTree)
Register a custom widget kind. When a tree contains
{ type = name, props = ..., children = ... }, the decoder
calls builderFn(props, children) at register / update time and
substitutes the returned widget table in place. Errors surface
through ui.lastValidation() with codes widget-builder-error
/ widget-builder-bad-return / decode-recursion-depth-exceeded.
Parameters
namestring— Custom widget kind name.builderFn(WidgetTree, { WidgetTree }) -> WidgetTree— Builder closure(props, children) -> widgetTable.
modules/ui/diagnose
diagnose(widgetId: string): WidgetPaint?
Why one widget did or did not reach the last frame. Returns that
widget's row from ui.observe() — the same fields, resolved against the
same reading. An id no registered screen carries reads noSuchWidget,
which is how a misspelling separates from a widget whose screen is
hidden and from one the frame laid out no box for.
Parameters
widgetIdstring— The id the widget records layout under.
local row = ui.diagnose("hud-healthbar")
modules/ui/displayScale
displayScale(): DisplayScale
The physical scale the UI is sized at. density is the physical
pixels one density-independent point covers on a handheld display (a
phone, a tablet, a page in a touch browser): a point of the UI covers at
least that many pixels whatever the screen's width or orientation, so
text and buttons keep their physical size. It is nil on a desktop
window, whose UI is sized by its width alone. textScale is the user's
text size setting, which multiplies every font size the UI typesets
(labels, buttons, inputs, editor chrome; canvas text is drawn at the
size it names). touchTarget is the smallest target, in points, a touch
reaches an interactive widget within: 48 on a handheld display, 0 on a
desktop. pixelRatio is ui.pixelRatio() of the same frame.
ui.simulateDisplayScale previews a phone's on any engine.
local scale = ui.displayScale()
local buttonHeight = math.max(32, scale.touchTarget)
modules/ui/dragState
dragState(): { payload: string, x: number, y: number, label: string?, ghost: boolean }?
The in-flight drag-and-drop payload while a dragPayload widget is
being dragged, else nil. x/y are the pointer's position in the
logical space ui.getLayoutInfo rects live in, so the reading resolves
directly against widget rects. label is the text the drag ghost beside
the pointer reads (dragLabel, else the source's text; nil for an
outline ghost), and ghost whether the engine draws one (dragGhost) —
a capture never shows the ghost, so this is how a drag's name-at-cursor
is checked. Poll during a drag to drive live feedback; the drop itself
still lands through the target's onDrop. Snapshotted each frame.
modules/ui/elementTree
elementTree(screenName: string): ElementNode?
Introspect a screen's rendered widget hierarchy with each
element's layout rect. Every node the renderer draws appears, nested
exactly as the widgets nest, under the id it records layout against:
the id set on the node when the author gave it one, otherwise
<screen>/<type>@<path>. bounds is that element's rect — the same
table ui.getLayoutInfo(id) returns — and appears once the element has
been measured. Kinds registered through ui.defineWidget appear
expanded into the primitives they build. Feeds the gui.captureElement
tool: list the tree, pick the ids to frame, capture their region.
Parameters
screenNamestring— Screen id passed toui.registerScreen.
modules/ui/focus
focus(widgetId: string)
Programmatically request keyboard focus on a widget. Queued
as a one-shot; the next render of the matching widget calls
response.request_focus().
Parameters
widgetIdstring— Widget id to focus.
modules/ui/focusedWidget
focusedWidget(): (string?, string?)
Return the widget id of whichever widget currently holds
keyboard focus, or nil, and the space it draws in: "window" for the
editor's UI around the game view, "camera" for the game's own UI in
the game view. Snapshotted post-render each frame.
local id, space = ui.focusedWidget()
modules/ui/getAreaPos
getAreaPos(id: string): AreaPos?
Read the current pivot position of an area widget,
including any user drag deltas. Returns { x, y } or nil if
the area didn't render this frame.
Parameters
idstring— Area widget id.
modules/ui/getAreaSize
getAreaSize(id: string): AreaSize?
Read the measured size of an area widget, including any user
resize-grip drags if the area is resizable. Returns { w, h }
or nil if the area didn't render this frame.
Parameters
idstring— Area widget id.
modules/ui/getDockLayout
getDockLayout(id: string): string?
Read the current serialized layout (split/tab arrangement) of
a dockArea widget as a JSON string. Returns nil if the dockArea
didn't render this frame. Persist the string and pass it back via
the dockArea's layout prop to restore the arrangement.
Parameters
idstring— DockArea widget id.
modules/ui/getLayoutInfo
getLayoutInfo(widgetId: string?): LayoutInfo?
Get layout info (position, size, content bounds) for UI
containers. If widgetId is given, returns info for that
widget only; otherwise returns all.
Parameters
widgetIdstring?(optional) — Optional widget id to query.
modules/ui/getPersistentDockLayout
getPersistentDockLayout(id: string): string?
Read a dockArea's arrangement as a JSON string with every tab
whose dockPanel is transient left out — the form to save and
restore across sessions. Returns nil if the dockArea didn't render
this frame.
Parameters
idstring— DockArea widget id.
modules/ui/getScreenTree
getScreenTree(screenName: string): WidgetTree?
Return the last widget tree table passed to
registerScreen / updateScreen for screenName.
Parameters
screenNamestring— Screen name to query.
modules/ui/getTheme
getTheme(): string
Get the name of the currently active theme.
modules/ui/getToken
getToken(name: string): string?
Look up a single design token value from the active theme.
Parameters
namestring— Token name (without$prefix).
modules/ui/getTokens
getTokens(): { [string]: string }
Get all design tokens from the active theme as a key-value map.
modules/ui/getWidgetProps
getWidgetProps(typeName: string): { WidgetPropDescriptor }?
Get the property definitions for a widget type.
Parameters
typeNamestring— Widget type name.
modules/ui/getWidgetTypes
getWidgetTypes(): { string }
Get all available widget type names that can be used in widget trees.
modules/ui/hideScreen
hideScreen(name: string): boolean
Hide a registered screen, and report whether a screen by that name
is registered. The engine applies the hide later in the frame;
listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to hide.
modules/ui/hitTest
hitTest(x: number, y: number): PaintHitTest?
Which widget a pointer at (x, y) reaches, and the stack beneath it.
Coordinates are logical points on the window, the space getLayoutInfo
and gui.bounds rects are in wherever a widget's screen draws (a screen
in the Scene view's picture included). A widget that takes no
pointer, a canvas declaring pointerEvents = "none", is passed over: the
answer is the widget beneath it.
Parameters
xnumber— Logical X.ynumber— Logical Y.
local hit = ui.hitTest(640, 360)
modules/ui/idCollisions
idCollisions(): { [string]: { string } }
Element ids more than one screen recorded, each mapped to the
screens that recorded it. An id addresses one element: two screens
sharing one leave ui.getLayoutInfo and the gui element tools
answering for whichever painted last, and the gui tools refuse such
an id. Empty while every id is unique.
for id, screens in ui.idCollisions() do print(id, table.concat(screens, ", ")) end
modules/ui/invisibilityReasons
invisibilityReasons(): { string }
Every verdict ui.diagnose can report, as a closed list.
modules/ui/lastRegistration
lastRegistration(): { name: string, layer: number? }?
The name and layer passed to the most recent ui.registerScreen
call, recorded synchronously at call time. A host that mounts a nested
app reads this immediately after the mount to learn which screen the
nested code registered, without intercepting the ui table.
modules/ui/lastValidation
lastValidation(screenName: string?): any
Validation diagnostics produced at the most recent
registerScreen / updateScreen, plus what the render stage
found while painting — unknown-font-family reports a
style.fontFamily that named no registered font family, once
per family per screen, wrap-label-no-width reports a
label with props.wrap whose box came out narrower than its
own longest word, once per label per screen, and
svg-not-as-declared reports an svg widget whose document
declared something the picture does not hold — a <text>
whose font-family selected no registered family, a <text>
the picture holds no glyph of, or a shape whose url(#id)
paint resolved to nothing — once per finding per widget per
screen. With no args
returns a
{ [screen] = entry } map; with a name returns that screen's
entry or nil. Validation gated by world setting
ui.validation = "off" | "warn" | "strict" (default "warn").
Parameters
screenNamestring?(optional) — Optional screen name.
modules/ui/listFonts
listFonts(): { FontFamilyInfo }
Every font family a style.fontFamily can select. Read from
the registry the UI text renderer resolves a family token
through, so a family this returns is one a label renders in.
family and every name in aliases are accepted as a
fontFamily, case-insensitively; aliases carries the
web-font names, CSS generic families and face names that
select the same group. faces names the concrete face in each
weight/style slot, so a fontWeight = 700 against a family
with no bold face gets a synthesised heavy. more lists the
other faces registered in the family's slots, each drawing the
codepoints the slot's face lacks: the installed chunks of an
on-demand face (font.onDemand() lists them all). system = true
marks a family taken from the host OS — present on this
machine, absent on one without it, and absent on WASM — so a
UI that must look the same everywhere picks a family with
system = false. A fontFamily naming nothing in this list
is reported as an unknown-font-family warning through
ui.lastValidation(screen) once the screen paints, and the
text renders in the default proportional face.
for _, f in ui.listFonts() do print(f.family) end
modules/ui/listScreens
listScreens(): { ScreenSummary }
List every registered screen with its current visibility,
z-order layer, renderLayer (the render-layer membership mask it
draws under: the mask setScreenRenderLayer stated, else the one the
surface setScreenSpace named implies, else the ui bit),
editorChrome (whether the mask setScreenRenderLayer stated carries
the EditorUI bit, which makes the screen the editor's own chrome; a
screen placed on the window without stating it is not chrome), whether
the screen has a populated root widget tree, and the space it draws in
("window" for the editor's UI, "camera" for the game's),
including the register / show / hide / unregister calls the running
script has already made. Sorted by layer ascending, then name.
modules/ui/listThemes
listThemes(): { string }
List all registered theme names.
modules/ui/lockOrientation
lockOrientation(lock: OrientationLock)
Hold the display to the orientations lock allows: "landscape"
(both landscape directions, so turning the device over turns the app
with it), "portrait" (both portrait directions) or "any" (the display
follows the device). A world states the orientation it plays in as
world.orientation, which the engine locks to in play mode and releases
in edit mode, so the editor follows the device; call this to change it
while the world runs. Android turns the app at once, a browser grants the
lock only to a page in fullscreen in most cases (the page asks again each
time it enters fullscreen), and a desktop window presents in the shape it
is given. ui.orientation() reports the lock and whether the platform
holds it from the next frame. Any other value is refused by name.
Parameters
lockOrientationLock—"any","landscape"or"portrait".
ui.lockOrientation("portrait")
ui.lockOrientation("any")
modules/ui/missingGlyphs
missingGlyphs(): { string }
The characters the UI is showing without a glyph: each appears in
text a UI surface (the window, the game view, a camera's UI overlay, a
screen drawn into a texture) drew in what it last painted, and no face of
the family it was drawn in has a glyph for it, so it shows as the
replacement box.
Every family falls back glyph by glyph to every face the UI holds, so a
character here is one no installed face covers. A character a declared
on-demand face covers (font.onDemand()) requests that face the frame
it is met, and leaves this list once the face is installed and the next
frame draws it.
if #ui.missingGlyphs() > 0 then print(table.concat(ui.missingGlyphs(), " ")) end
modules/ui/observe
observe(screenName: string?): PaintObservation?
What the last UI frame painted. Returns
{ generation, viewport, pointer, pointerOverUi, pointerWidget, widgets }
with one widgets row per widget any registered screen holds — its
layout box, the clip chain it painted under, the part of that box which
reached the frame (visible), the order it painted in (paintIndex),
and its reason from the closed set ui.invisibilityReasons() lists.
Every rect, the reading's viewport (the window) and pointer are in
logical points on the window; a row's viewport is the region the pass
that drew it paints into (the whole window, or the rect a Scene panel
presents the camera's picture in), and offViewport is judged against it.
generation advances once per re-rendered frame, so two calls reporting
the same number describe the same frame.
Parameters
screenNamestring?(optional) — Narrow the rows to one screen. Omit for every screen.
local reading = ui.observe("hud")
modules/ui/onOrientationChange
onOrientationChange(callback: (reading: OrientationReading) -> ()): () -> ()
Call callback with the new reading every time ui.orientation()
changes: the display turns between landscape and portrait or to another
rotation, the lock changes, or the platform starts or stops holding it.
Each listener is watched by a task of the context that registered it, so
a listener a component registered ends when that component is removed,
and every other listener goes on hearing changes.
Parameters
callback(reading: OrientationReading) -> ()— Receives the reading, asui.orientation()returns it.
Returns () — A function that removes this listener.
local off = ui.onOrientationChange(function(o) print(o.current, o.rotation) end)
modules/ui/orientation
orientation(): OrientationReading
The orientation the display presents the app in, and the lock it is
held to. current is "landscape" (wider than tall) or "portrait":
the shape of the window the app presents in. lock is what
ui.lockOrientation last asked, "any" when the display follows the
device. enforced is whether the platform holds the display to it:
Android always does, a browser does once it grants the Screen
Orientation lock, and a desktop window never does, presenting in the
shape it is given; reason names why when it does not. rotation is the
display's rotation from its natural orientation in degrees (0, 90, 180,
270) where the platform states one, and nil on a desktop window.
revision advances every time any of these changes; ui.onOrientationChange
calls back on each change.
if ui.orientation().current == "portrait" then print("lay the HUD out for a tall screen") end
modules/ui/paintOrder
paintOrder(a: string, b: string): number?
Which of two widgets paints later: -1 when a paints before b,
1 when after, 0 when level. This is what separates two widgets whose
rects are identical.
Parameters
astring— First widget id.bstring— Second widget id.
local order = ui.paintOrder("panel-a", "panel-b")
modules/ui/pixelRatio
pixelRatio(): number
Physical pixels per logical point — the factor between the logical
space ui.screenSize() / getLayoutInfo rects live in and the physical
space input.mousePosition, the camera viewport rect and
input.simulateMouse* coordinates live in. Multiply a layout coordinate
by this to aim a simulated pointer at a widget.
modules/ui/pointerWidget
pointerWidget(): PointerRead?
Whether the UI is consuming the pointer, and which widget holds it —
the pointer counterpart of ui.focusedWidget(). x, y are in logical
points on the window, the space getLayoutInfo rects are in.
modules/ui/registerBackgroundShader
registerBackgroundShader(shaderHandle: any, width: number?, height: number?)
Register a screen-domain .shader as a UI background, drawn
via the backgroundShader style. Takes the shader's asset handle
from asset.resolve. A shader whose compile failed
(shaderHandle:compileStatus() reads failed), or whose program
the compile gate refuses, is not registered: the call raises an
error naming the shader and the compiler's message, the one
compileStatus().error reads. A shader whose compileStatus()
still reads pending raises too: register it once it reads
compiled.
Parameters
shaderHandleany(optional) — The screen.shader's asset handle, fromasset.resolve.widthnumber?(optional) — Render target width (default 1280).heightnumber?(optional) — Render target height (default 720).
modules/ui/registerCallbackEnv
registerCallbackEnv(key: string, env: { [string]: any })
Register an environment table to receive widget-callback
broadcasts: its global onCallback(id, value) fires for any widget
callback not owned by a specific component instance — the same
broadcast a component's onCallback receives. Keyed by key;
re-registering the same key replaces the previous env. A component
instance is folded into the callback dispatch automatically, so reach
for this from a non-component context that hosts a UI surface (a scene
entrypoint registering its own screen). Pair with
ui.unregisterCallbackEnv(key) so the ref is released.
Parameters
keystring— Stable identifier for this registration (re-register replaces).env{ [string]: any }— Environment table whoseonCallbackreceives the broadcasts.
modules/ui/registerScreen
registerScreen(name: string, widgetTree: WidgetTree, layer: number?)
Register a named UI screen with a widget tree. Optional
layer controls z-ordering (higher = on top), in bands: below 0
behind everything, 0-99 ordinary app depth, 100-999 always-on-top
chrome, 1000+ menu and popup depth. A screen in a higher band
covers one in a lower band whatever their roots are; inside a band
a floating area or window root sits over ordinary content, and
a modal root sits over the whole stack. Tag-based
grouping lives in Z.tags (Z.tags.set(name, { "editor" })
after register). A screen belongs to the component instance whose
own code registers it (its hooks and methods, and any closure they
create, such as a listener it subscribes): that instance's
onCallback receives its callbacks, and the screen is unregistered
when the instance ends. Code that is not an instance's own (a scene
entrypoint, a module, a listener either subscribed) registers a
screen no instance owns, which stays until it is unregistered,
whichever component's hook ran that code. topics/ui has the rule.
Parameters
namestring— Unique screen identifier.widgetTreeWidgetTree— Root widget table.layernumber?(optional) — Z-order layer (optional).
ui.registerScreen("hud", tree)
modules/ui/registerTheme
registerTheme(name: string, theme: ThemeDefinition)
Register a theme from a flat Luau table. Most callers
should use Z.theme.register(name, table) which runs the
cascade for them.
Parameters
namestring— Theme name to register.themeThemeDefinition— Flat-resolved theme table.
modules/ui/removeScreen
removeScreen(name: string): boolean
Alias for ui.unregisterScreen.
Parameters
namestring— Screen identifier to remove.
modules/ui/resetAreaSize
resetAreaSize(id: string)
Clear a resizable area's remembered size (from a grip drag or
ui.setAreaSize) so its declared — or content — size takes over again.
Parameters
idstring— Area widget id.
modules/ui/response
response(widgetId: string): WidgetResponse?
Per-widget interaction snapshot for the most recent frame.
Returns { clicked, hovered, focused, changed, value } where
clicked / changed mark transitions and hovered / focused
mark current state.
Parameters
widgetIdstring— The widget id (NOT the onClick / onChange callback id).
modules/ui/safeArea
safeArea(): SafeArea
The part of the screen no system bar, display cutout or on-screen
keyboard covers, in the logical points ui.screenSize() reports.
top / right / bottom / left are how far in from each edge the
covered band reaches; x / y / width / height are the rect they
leave; cutouts lists each display cutout (a camera hole or a notch) as
{ x, y, width, height }. Screens lay out inside this rect, so anchors,
top-level % sizes and a window's default position already keep clear
of it; read it to place something by absolute pos, or to run a
background under the bars while its controls stay inside. revision
advances every time the area changes (a rotation, the system bars shown
or hidden, the keyboard opening, a window resize): compare it with the
one you last laid out against. All zero on a desktop window;
ui.simulateSafeArea lays the UI out as a phone would on any engine.
local safe = ui.safeArea()
ui.setAreaPos("hud", safe.x + 8, safe.y + 8)
modules/ui/screen
screen(name: string): { [string]: any }?
Get a screen proxy with methods like setResolution and
rasterize.
Parameters
namestring— Screen name.
modules/ui/screenSize
screenSize(): { width: number, height: number }
The UI coordinate space as { width, height } (logical points). This is
the space area pos, anchors, and getLayoutInfo rects use — and it is
NOT the pixel size of a capture screenshot, which may be downscaled. Use
this for absolute area positioning (e.g. pinning a menu above a bottom
taskbar) instead of guessing the size from a capture image.
It follows the size the engine draws at, and renderer.setViewportSize
changes that size for the task that asks, which is how a layout written
against this is checked at a second shape without rebooting the engine,
read in the call that set the size before the surface goes back. The pixel size
behind these points is renderer.surfaceSize() — the whole drawing
surface a screen is laid out over, which an editor layout makes larger
than the rect renderer.viewportSize() draws the scene into — and
ui.pixelRatio() is the factor between the two spaces.
modules/ui/scroll
scroll(deltaX: number, deltaY: number)
Simulate a mouse-wheel scroll event on the UI.
Parameters
deltaXnumber— Horizontal scroll delta.deltaYnumber— Vertical scroll delta.
modules/ui/setAreaPos
setAreaPos(id: string, x: number, y: number)
Programmatically move a movable area widget to (x, y).
Applied for one frame; subsequent frames let drag tracking
take over.
Parameters
idstring— Area widget id.xnumber— Target pivot x (screen coords).ynumber— Target pivot y (screen coords).
modules/ui/setAreaSize
setAreaSize(id: string, w: number, h: number)
Programmatically set a resizable area's size (the user-size
override) — for maximize / restore / tile. Persists until the area's
declared width/height changes or ui.resetAreaSize(id) clears it.
Parameters
idstring— Area widget id.wnumber— Target width (screen coords).hnumber— Target height (screen coords).
modules/ui/setDockWindowRect
setDockWindowRect(
Place the floating window of a dockArea panel at (x, y) with
size (width, height). Applies once the panel occupies a window —
a request made before then waits for it.
modules/ui/setScreenRenderLayer
setScreenRenderLayer(name: string, mask: number)
Set a screen's render-layer membership bitmask. A screen draws into a
camera or capture only when this mask intersects the camera's include
mask, the same rule geometry follows. Content UI carries the ui bit;
the editor places its chrome on EditorUI so agent captures can drop it,
and a screen carrying that bit is part of the window rather than the
camera's picture. Stating a mask here fixes the membership: a screen left
to itself takes the one the surface it is placed on implies, EditorUI
for the window and ui for the camera's picture. __renderLayers.bit(name)
answers with a layer's bit INDEX, so a mask sets the bit at that index and
__renderLayers.names(mask) reads it back as the names it holds.
Parameters
namestring— Screen identifier.masknumber— Render-layer membership bitmask — the OR ofbit32.lshift(1, __renderLayers.bit(name))over the layers the screen is on.
ui.setScreenRenderLayer("hud", bit32.lshift(1, __renderLayers.bit("ui")))
modules/ui/setScreenSpace
setScreenSpace(name: string, space: string)
Set which surface a screen is part of. "camera" is the picture the
viewport camera produces: the screen lays out over the camera's target
and is drawn into it, so it sits inside the Scene panel presenting the
scene and appears in every capture of the camera — the default for
content UI. "window" is the window itself, whatever presents the scene
— where the editor's chrome lives, and the default for a screen on the
EditorUI layer. The two are one statement made from either end, so a
screen placed on the window takes the EditorUI membership unless
setScreenRenderLayer states another: the window goes on drawing it while
a capture that drops the authoring layers, which is what an agent takes by
default, leaves it out. Where the scene presents on the window, the two
surfaces coincide. A name that is neither raises.
Parameters
namestring— Screen identifier.spacestring—"camera"|"window".
ui.setScreenSpace("agent-banner", "window")
modules/ui/setScrollPosition
setScrollPosition(widgetId: string, offsetY: number)
Set the scroll offset of a scrollArea widget.
Parameters
widgetIdstring— Scroll area widget id.offsetYnumber— Vertical scroll offset in pixels.
modules/ui/setShaderUniforms
setShaderUniforms(name: string, uniforms: { [string]: number })
Set uniform values on a registered background shader.
Parameters
namestring— Shader name identifier.uniforms{ [string]: number }— Map of uniform name to number value.
modules/ui/setTheme
setTheme(name: string)
Switch the active global theme by name.
Parameters
namestring— Theme name to activate.
modules/ui/showScreen
showScreen(name: string): boolean
Make a registered screen visible, and report whether a screen by
that name is registered. The engine applies the show later in the
frame; listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to show.
modules/ui/simulateDisplayScale
simulateDisplayScale(scale: DisplayScaleSpec?)
Size the UI as a display stating scale would, in place of what the
platform states. A density sizes it as a handheld display of that
many physical pixels per point (omit it for a desktop display sized by
its width), and textScale multiplies every font size the UI typesets
(1 when omitted). Previews a phone's UI on a desktop or headless engine,
and makes a layout test independent of the device it runs on.
ui.displayScale(), ui.screenSize() and ui.pixelRatio() report it
from the next frame. Call it with no argument (or nil) to return to
the platform's own scale. A value that is not a number above 0 and at
most 8 is refused by name.
Parameters
scaleDisplayScaleSpec?(optional) —{ density?, textScale? }; nil returns to the platform's scale.
ui.simulateDisplayScale({ density = 2.625, textScale = 1.3 })
ui.simulateDisplayScale()
modules/ui/simulateSafeArea
simulateSafeArea(area: SafeAreaSpec?)
Lay the UI out as if the system covered the screen by area
(logical points) in place of what the platform states: each inset
defaults to 0 and cutouts is a list of { x, y, width, height }.
Previews a phone's status bar, camera cutout and navigation bar on a
desktop or headless engine, and makes a layout test independent of the
device it runs on. ui.safeArea() reports it from the next frame. Call
it with no argument (or nil) to return to the platform's own safe
area. A negative or non-number inset is refused by name.
Parameters
areaSafeAreaSpec?(optional) —{ top?, right?, bottom?, left?, cutouts? }in logical points; nil returns to the platform's safe area.
ui.simulateSafeArea({ top = 40, bottom = 30, cutouts = { { x = 600, y = 0, width = 80, height = 40 } } })
ui.simulateSafeArea()
modules/ui/unregisterCallbackEnv
unregisterCallbackEnv(key: string)
Remove an environment registered with ui.registerCallbackEnv. Its
onCallback stops receiving broadcasts. No-op if key isn't registered.
Parameters
keystring— The key passed toui.registerCallbackEnv.
modules/ui/unregisterScreen
unregisterScreen(name: string): boolean
Remove a screen from the registry entirely. Unlike
hideScreen, this deletes the entry so it no longer appears in
listScreens or render iteration.
Parameters
namestring— Screen identifier to unregister.
modules/ui/unregisterWidget
unregisterWidget(name: string)
Drop a registered custom widget kind. Subsequent references
produce an unknown-widget-type diagnostic.
Parameters
namestring— Custom widget kind name.
modules/ui/updateScreen
updateScreen(name: string, widgetTree: WidgetTree)
Replace the widget tree of an already-registered screen.
Parameters
namestring— Screen identifier to update.widgetTreeWidgetTree— New root widget table.
modules/ui/useStyles
useStyles(themeName: string)
Apply a registered style file's classes additively without changing the active theme.
Parameters
themeNamestring— Name of the registered style / theme asset.
modules/ui/widgetState
widgetState(widgetId: string, key: string, default: any?): any
Read per-widget cross-frame state. Returns the value
previously written via widgetStateSet, or default (or nil).
State is keyed by widget id and persists across re-renders
within a screen's lifetime; cleared automatically when the
owning screen is unregistered.
Parameters
widgetIdstring— Widget id whose state to read.keystring— State key.defaultany?(optional) — Value to return when nothing has been written.
modules/ui/widgetStateClear
widgetStateClear(widgetId: string, key: string)
Remove a per-widget state entry.
Parameters
widgetIdstring— Widget id whose state to clear.keystring— State key.
modules/ui/widgetStateSet
widgetStateSet(widgetId: string, key: string, value: any)
Write per-widget cross-frame state. Replaces any existing
value under (widgetId, key). Tables are stored by reference.
Parameters
widgetIdstring— Widget id to scope the state under.keystring— State key.valueany(optional) — Value to store (must be non-nil).
typed/builtin//modules/api/engine/ui/ui/blur
ui.blur()
Surrender keyboard focus from whichever widget currently holds it, in the editor's UI and the game's alike.
typed/builtin//modules/api/engine/ui/ui/bringAreaToFront
ui.bringAreaToFront(id: string)
Raise a movable area to the top of the window stacking order —
the programmatic equivalent of clicking it. Areas sharing a stacking
band order by interaction, so this is the call that brings one forward
from code: use it when a taskbar button, focus change, or app launch
should raise a window. Moving a screen to a higher layer band raises
it over the bands below.
Parameters
idstring— Area widget id.
typed/builtin//modules/api/engine/ui/ui/captureWindow
ui.captureWindow(screen: string, window: string, opts: CaptureOpts?) -> CaptureResult?
Render a single Window widget to its own offscreen texture
and write the result as PNG at
/runtime/render_surfaces/<rtHandle>.png. The screen does NOT
need to be visible. Returns { rtHandle, texturePath } or nil
on invalid inputs (width/height clamped to [1, 8192],
defaults 600x400).
Parameters
screenstring— Screen id containing the target Window.windowstring— Widget id of the Window.optsCaptureOpts(optional) —{ width, height }(optional).
Returns CaptureResult? — { rtHandle, texturePath } or nil.
typed/builtin//modules/api/engine/ui/ui/click
ui.click(callbackId: string, value: any?) -> boolean
Simulate a widget click / interaction by its callback id. The call
carries no screen, so an id that names widgets on several screens reaches
every component that declared it, once each. The callback id is the one
a widget emits: the string its onClick / onChange / other on* prop
names, or its element id where it leaves that prop unnamed (a button
clicked without an onClick, a slider changed without an onChange).
Given the element id of a widget whose interactions emit other ids, it
raises an error naming those ids, and clicks nothing.
Parameters
callbackIdstring— Callback id the widget emits (itsonClick/onChange/on*prop).valueany(optional) — Optional value the callback receives asdata.value: a number, a string, a boolean, or a table with the fields a structured interaction delivers (a code editor's{ token, line, col }, a canvas drag's deltas).
Returns boolean — true when a widget on a registered screen emits the id; false when none does, and the click then still reaches every ui.registerCallbackEnv env and every component and scene entrypoint that receives callbacks no screen claims.
typed/builtin//modules/api/engine/ui/ui/copyText
ui.copyText(text: string)
Put text on the host clipboard — the operating system's clipboard
on a native build, the browser's in a web build. What a "Copy path" or
"Copy reference" action calls.
Parameters
textstring— The text to copy.
ui.copyText("/zero/source/scenes/main.scene")
typed/builtin//modules/api/engine/ui/ui/defineStyle
ui.defineStyle(name: string, style: StyleProps)
Define a named style. Style keys follow
<widgetType>.<className> (e.g. "label.h1", "button.primary")
or bare <className> to apply across widget types. Widgets
reference styles via the classes (or class) prop.
Parameters
namestring— Style name.styleStyleProps— Style properties table.
typed/builtin//modules/api/engine/ui/ui/defineStyles
ui.defineStyles(styles: { [string]: StyleProps })
Define multiple named styles at once.
Parameters
styles{ [string]: StyleProps }— Map of style name to style properties.
typed/builtin//modules/api/engine/ui/ui/defineWidget
ui.defineWidget(name: string, builderFn: (WidgetTree, { WidgetTree }) -> WidgetTree)
Register a custom widget kind. When a tree contains
{ type = name, props = ..., children = ... }, the decoder
calls builderFn(props, children) at register / update time and
substitutes the returned widget table in place. Errors surface
through ui.lastValidation() with codes widget-builder-error
/ widget-builder-bad-return / decode-recursion-depth-exceeded.
Parameters
namestring— Custom widget kind name.builderFn(WidgetTree, { WidgetTree }) -> WidgetTree— Builder closure(props, children) -> widgetTable.
typed/builtin//modules/api/engine/ui/ui/diagnose
ui.diagnose(widgetId: string) -> WidgetPaint?
Why one widget did or did not reach the last frame. Returns that
widget's row from ui.observe() — the same fields, resolved against the
same reading. An id no registered screen carries reads noSuchWidget,
which is how a misspelling separates from a widget whose screen is
hidden and from one the frame laid out no box for.
Parameters
widgetIdstring— The id the widget records layout under.
Returns WidgetPaint? — The widget's row, or nil before the UI has published a frame.
local row = ui.diagnose("hud-healthbar")
typed/builtin//modules/api/engine/ui/ui/displayScale
ui.displayScale() -> DisplayScale
The physical scale the UI is sized at. density is the physical
pixels one density-independent point covers on a handheld display (a
phone, a tablet, a page in a touch browser): a point of the UI covers at
least that many pixels whatever the screen's width or orientation, so
text and buttons keep their physical size. It is nil on a desktop
window, whose UI is sized by its width alone. textScale is the user's
text size setting, which multiplies every font size the UI typesets
(labels, buttons, inputs, editor chrome; canvas text is drawn at the
size it names). touchTarget is the smallest target, in points, a touch
reaches an interactive widget within: 48 on a handheld display, 0 on a
desktop. pixelRatio is ui.pixelRatio() of the same frame.
ui.simulateDisplayScale previews a phone's on any engine.
Returns DisplayScale — { density?, textScale, touchTarget, pixelRatio }.
local scale = ui.displayScale()
local buttonHeight = math.max(32, scale.touchTarget)
typed/builtin//modules/api/engine/ui/ui/dragState
ui.dragState() -> { payload: string, x: number, y: number, label: string?, ghost: boolean }?
The in-flight drag-and-drop payload while a dragPayload widget is
being dragged, else nil. x/y are the pointer's position in the
logical space ui.getLayoutInfo rects live in, so the reading resolves
directly against widget rects. label is the text the drag ghost beside
the pointer reads (dragLabel, else the source's text; nil for an
outline ghost), and ghost whether the engine draws one (dragGhost) —
a capture never shows the ghost, so this is how a drag's name-at-cursor
is checked. Poll during a drag to drive live feedback; the drop itself
still lands through the target's onDrop. Snapshotted each frame.
Returns { payload: string, x: number, y: number, label: string?, ghost: boolean }? — { payload, x, y, label?, ghost } during a drag, nil otherwise.
typed/builtin//modules/api/engine/ui/ui/elementTree
ui.elementTree(screenName: string) -> ElementNode?
Introspect a screen's rendered widget hierarchy with each
element's layout rect. Every node the renderer draws appears, nested
exactly as the widgets nest, under the id it records layout against:
the id set on the node when the author gave it one, otherwise
<screen>/<type>@<path>. bounds is that element's rect — the same
table ui.getLayoutInfo(id) returns — and appears once the element has
been measured. Kinds registered through ui.defineWidget appear
expanded into the primitives they build. Feeds the gui.captureElement
tool: list the tree, pick the ids to frame, capture their region.
Parameters
screenNamestring— Screen id passed toui.registerScreen.
Returns ElementNode? — An ElementNode tree, or nil when no screen is registered under that name.
typed/builtin//modules/api/engine/ui/ui/focus
ui.focus(widgetId: string)
Programmatically request keyboard focus on a widget. Queued
as a one-shot; the next render of the matching widget calls
response.request_focus().
Parameters
widgetIdstring— Widget id to focus.
typed/builtin//modules/api/engine/ui/ui/focusedWidget
ui.focusedWidget() -> (string?, string?)
Return the widget id of whichever widget currently holds
keyboard focus, or nil, and the space it draws in: "window" for the
editor's UI around the game view, "camera" for the game's own UI in
the game view. Snapshotted post-render each frame.
Returns (string?, string?) — Focused widget id or nil, and its space.
local id, space = ui.focusedWidget()
typed/builtin//modules/api/engine/ui/ui/getAreaPos
ui.getAreaPos(id: string) -> AreaPos?
Read the current pivot position of an area widget,
including any user drag deltas. Returns { x, y } or nil if
the area didn't render this frame.
Parameters
idstring— Area widget id.
Returns AreaPos? — { x, y } or nil.
typed/builtin//modules/api/engine/ui/ui/getAreaSize
ui.getAreaSize(id: string) -> AreaSize?
Read the measured size of an area widget, including any user
resize-grip drags if the area is resizable. Returns { w, h }
or nil if the area didn't render this frame.
Parameters
idstring— Area widget id.
Returns AreaSize? — { w, h } or nil.
typed/builtin//modules/api/engine/ui/ui/getDockLayout
ui.getDockLayout(id: string) -> string?
Read the current serialized layout (split/tab arrangement) of
a dockArea widget as a JSON string. Returns nil if the dockArea
didn't render this frame. Persist the string and pass it back via
the dockArea's layout prop to restore the arrangement.
Parameters
idstring— DockArea widget id.
Returns string? — Serialized DockState JSON string, or nil.
typed/builtin//modules/api/engine/ui/ui/getLayoutInfo
ui.getLayoutInfo(widgetId: string?) -> LayoutInfo?
Get layout info (position, size, content bounds) for UI
containers. If widgetId is given, returns info for that
widget only; otherwise returns all.
Parameters
widgetIdstring(optional) — Optional widget id to query.
Returns LayoutInfo? — Layout info table or nil.
typed/builtin//modules/api/engine/ui/ui/getPersistentDockLayout
ui.getPersistentDockLayout(id: string) -> string?
Read a dockArea's arrangement as a JSON string with every tab
whose dockPanel is transient left out — the form to save and
restore across sessions. Returns nil if the dockArea didn't render
this frame.
Parameters
idstring— DockArea widget id.
Returns string? — Serialized DockState JSON string without transient tabs, or nil.
typed/builtin//modules/api/engine/ui/ui/getScreenTree
ui.getScreenTree(screenName: string) -> WidgetTree?
Return the last widget tree table passed to
registerScreen / updateScreen for screenName.
Parameters
screenNamestring— Screen name to query.
Returns WidgetTree? — Widget tree or nil.
typed/builtin//modules/api/engine/ui/ui/getTheme
ui.getTheme() -> string
Get the name of the currently active theme.
Returns string — Active theme name.
typed/builtin//modules/api/engine/ui/ui/getToken
ui.getToken(name: string) -> string?
Look up a single design token value from the active theme.
Parameters
namestring— Token name (without$prefix).
Returns string? — Token value or nil.
typed/builtin//modules/api/engine/ui/ui/getTokens
ui.getTokens() -> { [string]: string }
Get all design tokens from the active theme as a key-value map.
Returns { [string]: string } — Token map.
typed/builtin//modules/api/engine/ui/ui/getWidgetProps
ui.getWidgetProps(typeName: string) -> { WidgetPropDescriptor }?
Get the property definitions for a widget type.
Parameters
typeNamestring— Widget type name.
Returns { WidgetPropDescriptor }? — Array of property descriptors, or nil if type not found.
typed/builtin//modules/api/engine/ui/ui/getWidgetTypes
ui.getWidgetTypes() -> { string }
Get all available widget type names that can be used in widget trees.
Returns { string } — Array of widget type names.
typed/builtin//modules/api/engine/ui/ui/hideScreen
ui.hideScreen(name: string) -> boolean
Hide a registered screen, and report whether a screen by that name
is registered. The engine applies the hide later in the frame;
listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to hide.
Returns boolean — True when a screen by this name is registered.
typed/builtin//modules/api/engine/ui/ui/hitTest
ui.hitTest(x: number, y: number) -> PaintHitTest?
Which widget a pointer at (x, y) reaches, and the stack beneath it.
Coordinates are logical points on the window, the space getLayoutInfo
and gui.bounds rects are in wherever a widget's screen draws (a screen
in the Scene view's picture included). A widget that takes no
pointer, a canvas declaring pointerEvents = "none", is passed over: the
answer is the widget beneath it.
Parameters
xnumber— Logical X.ynumber— Logical Y.
Returns PaintHitTest? — { widget, screen, stack, x, y } — widget nil when the point is over no UI — or nil before the UI has published a frame.
local hit = ui.hitTest(640, 360)
typed/builtin//modules/api/engine/ui/ui/idCollisions
ui.idCollisions() -> { [string]: { string } }
Element ids more than one screen recorded, each mapped to the
screens that recorded it. An id addresses one element: two screens
sharing one leave ui.getLayoutInfo and the gui element tools
answering for whichever painted last, and the gui tools refuse such
an id. Empty while every id is unique.
Returns { [string]: { string } } — Colliding id -> the screens that recorded it.
for id, screens in ui.idCollisions() do print(id, table.concat(screens, ", ")) end
typed/builtin//modules/api/engine/ui/ui/invisibilityReasons
ui.invisibilityReasons() -> { string }
Every verdict ui.diagnose can report, as a closed list.
Returns { string } — The reason names.
typed/builtin//modules/api/engine/ui/ui/lastRegistration
ui.lastRegistration() -> { name: string, layer: number? }?
The name and layer passed to the most recent ui.registerScreen
call, recorded synchronously at call time. A host that mounts a nested
app reads this immediately after the mount to learn which screen the
nested code registered, without intercepting the ui table.
Returns { name: string, layer: number? }? — { name, layer } for the last registration, or nil if none yet.
typed/builtin//modules/api/engine/ui/ui/lastValidation
ui.lastValidation(screenName: string?) -> any
Validation diagnostics produced at the most recent
registerScreen / updateScreen, plus what the render stage
found while painting — unknown-font-family reports a
style.fontFamily that named no registered font family, once
per family per screen, wrap-label-no-width reports a
label with props.wrap whose box came out narrower than its
own longest word, once per label per screen, and
svg-not-as-declared reports an svg widget whose document
declared something the picture does not hold — a <text>
whose font-family selected no registered family, a <text>
the picture holds no glyph of, or a shape whose url(#id)
paint resolved to nothing — once per finding per widget per
screen. With no args
returns a
{ [screen] = entry } map; with a name returns that screen's
entry or nil. Validation gated by world setting
ui.validation = "off" | "warn" | "strict" (default "warn").
Parameters
screenNamestring(optional) — Optional screen name.
Returns any — Validation entry, full map, or nil.
typed/builtin//modules/api/engine/ui/ui/listFonts
ui.listFonts() -> { FontFamilyInfo }
Every font family a style.fontFamily can select. Read from
the registry the UI text renderer resolves a family token
through, so a family this returns is one a label renders in.
family and every name in aliases are accepted as a
fontFamily, case-insensitively; aliases carries the
web-font names, CSS generic families and face names that
select the same group. faces names the concrete face in each
weight/style slot, so a fontWeight = 700 against a family
with no bold face gets a synthesised heavy. more lists the
other faces registered in the family's slots, each drawing the
codepoints the slot's face lacks: the installed chunks of an
on-demand face (font.onDemand() lists them all). system = true
marks a family taken from the host OS — present on this
machine, absent on one without it, and absent on WASM — so a
UI that must look the same everywhere picks a family with
system = false. A fontFamily naming nothing in this list
is reported as an unknown-font-family warning through
ui.lastValidation(screen) once the screen paints, and the
text renders in the default proportional face.
Returns { FontFamilyInfo } — Array of { family, aliases, faces, more, system }, by family.
for _, f in ui.listFonts() do print(f.family) end
typed/builtin//modules/api/engine/ui/ui/listScreens
ui.listScreens() -> { ScreenSummary }
List every registered screen with its current visibility,
z-order layer, renderLayer (the render-layer membership mask it
draws under: the mask setScreenRenderLayer stated, else the one the
surface setScreenSpace named implies, else the ui bit),
editorChrome (whether the mask setScreenRenderLayer stated carries
the EditorUI bit, which makes the screen the editor's own chrome; a
screen placed on the window without stating it is not chrome), whether
the screen has a populated root widget tree, and the space it draws in
("window" for the editor's UI, "camera" for the game's),
including the register / show / hide / unregister calls the running
script has already made. Sorted by layer ascending, then name.
Returns { ScreenSummary } — Array of screen summaries.
typed/builtin//modules/api/engine/ui/ui/listThemes
ui.listThemes() -> { string }
List all registered theme names.
Returns { string } — Array of theme names.
typed/builtin//modules/api/engine/ui/ui/lockOrientation
ui.lockOrientation(lock: OrientationLock)
Hold the display to the orientations lock allows: "landscape"
(both landscape directions, so turning the device over turns the app
with it), "portrait" (both portrait directions) or "any" (the display
follows the device). A world states the orientation it plays in as
world.orientation, which the engine locks to in play mode and releases
in edit mode, so the editor follows the device; call this to change it
while the world runs. Android turns the app at once, a browser grants the
lock only to a page in fullscreen in most cases (the page asks again each
time it enters fullscreen), and a desktop window presents in the shape it
is given. ui.orientation() reports the lock and whether the platform
holds it from the next frame. Any other value is refused by name.
Parameters
lockOrientationLock—"any","landscape"or"portrait".
ui.lockOrientation("portrait")
ui.lockOrientation("any")
typed/builtin//modules/api/engine/ui/ui/missingGlyphs
ui.missingGlyphs() -> { string }
The characters the UI is showing without a glyph: each appears in
text a UI surface (the window, the game view, a camera's UI overlay, a
screen drawn into a texture) drew in what it last painted, and no face of
the family it was drawn in has a glyph for it, so it shows as the
replacement box.
Every family falls back glyph by glyph to every face the UI holds, so a
character here is one no installed face covers. A character a declared
on-demand face covers (font.onDemand()) requests that face the frame
it is met, and leaves this list once the face is installed and the next
frame draws it.
Returns { string } — Sorted array of single-character strings; empty when every character drawn had a glyph.
if #ui.missingGlyphs() > 0 then print(table.concat(ui.missingGlyphs(), " ")) end
typed/builtin//modules/api/engine/ui/ui/observe
ui.observe(screenName: string?) -> PaintObservation?
What the last UI frame painted. Returns
{ generation, viewport, pointer, pointerOverUi, pointerWidget, widgets }
with one widgets row per widget any registered screen holds — its
layout box, the clip chain it painted under, the part of that box which
reached the frame (visible), the order it painted in (paintIndex),
and its reason from the closed set ui.invisibilityReasons() lists.
Every rect, the reading's viewport (the window) and pointer are in
logical points on the window; a row's viewport is the region the pass
that drew it paints into (the whole window, or the rect a Scene panel
presents the camera's picture in), and offViewport is judged against it.
generation advances once per re-rendered frame, so two calls reporting
the same number describe the same frame.
Parameters
screenNamestring(optional) — Narrow the rows to one screen. Omit for every screen.
Returns PaintObservation? — The reading; nil before the UI has published a frame, and nil for a screenName no registered screen answers to.
local reading = ui.observe("hud")
typed/builtin//modules/api/engine/ui/ui/onOrientationChange
ui.onOrientationChange(callback: (reading: OrientationReading) -> ()) -> () -> ()
Call callback with the new reading every time ui.orientation()
changes: the display turns between landscape and portrait or to another
rotation, the lock changes, or the platform starts or stops holding it.
Each listener is watched by a task of the context that registered it, so
a listener a component registered ends when that component is removed,
and every other listener goes on hearing changes.
Parameters
callback(reading: OrientationReading) -> ()— Receives the reading, asui.orientation()returns it.
Returns () -> () — A function that removes this listener.
local off = ui.onOrientationChange(function(o) print(o.current, o.rotation) end)
typed/builtin//modules/api/engine/ui/ui/orientation
ui.orientation() -> OrientationReading
The orientation the display presents the app in, and the lock it is
held to. current is "landscape" (wider than tall) or "portrait":
the shape of the window the app presents in. lock is what
ui.lockOrientation last asked, "any" when the display follows the
device. enforced is whether the platform holds the display to it:
Android always does, a browser does once it grants the Screen
Orientation lock, and a desktop window never does, presenting in the
shape it is given; reason names why when it does not. rotation is the
display's rotation from its natural orientation in degrees (0, 90, 180,
270) where the platform states one, and nil on a desktop window.
revision advances every time any of these changes; ui.onOrientationChange
calls back on each change.
Returns OrientationReading — { current, lock, enforced, reason?, rotation?, revision }.
if ui.orientation().current == "portrait" then print("lay the HUD out for a tall screen") end
typed/builtin//modules/api/engine/ui/ui/paintOrder
ui.paintOrder(a: string, b: string) -> number?
Which of two widgets paints later: -1 when a paints before b,
1 when after, 0 when level. This is what separates two widgets whose
rects are identical.
Parameters
astring— First widget id.bstring— Second widget id.
Returns number? — -1, 0, 1, or nil when the reading holds no row for one of them.
local order = ui.paintOrder("panel-a", "panel-b")
typed/builtin//modules/api/engine/ui/ui/pixelRatio
ui.pixelRatio() -> number
Physical pixels per logical point — the factor between the logical
space ui.screenSize() / getLayoutInfo rects live in and the physical
space input.mousePosition, the camera viewport rect and
input.simulateMouse* coordinates live in. Multiply a layout coordinate
by this to aim a simulated pointer at a widget.
Returns number — Physical pixels per logical point (1.0 when unscaled).
typed/builtin//modules/api/engine/ui/ui/pointerWidget
ui.pointerWidget() -> PointerRead?
Whether the UI is consuming the pointer, and which widget holds it —
the pointer counterpart of ui.focusedWidget(). x, y are in logical
points on the window, the space getLayoutInfo rects are in.
Returns PointerRead? — { x, y, overUi, widget, screen }, or nil before the UI has published a frame.
typed/builtin//modules/api/engine/ui/ui/registerBackgroundShader
ui.registerBackgroundShader(shaderHandle: any?, width: number?, height: number?)
Register a screen-domain .shader as a UI background, drawn
via the backgroundShader style. Takes the shader's asset handle
from asset.resolve. A shader whose compile failed
(shaderHandle:compileStatus() reads failed), or whose program
the compile gate refuses, is not registered: the call raises an
error naming the shader and the compiler's message, the one
compileStatus().error reads. A shader whose compileStatus()
still reads pending raises too: register it once it reads
compiled.
Parameters
shaderHandleany(optional) — The screen.shader's asset handle, fromasset.resolve.widthnumber(optional) — Render target width (default 1280).heightnumber(optional) — Render target height (default 720).
typed/builtin//modules/api/engine/ui/ui/registerCallbackEnv
ui.registerCallbackEnv(key: string, env: { [string]: any })
Register an environment table to receive widget-callback
broadcasts: its global onCallback(id, value) fires for any widget
callback not owned by a specific component instance — the same
broadcast a component's onCallback receives. Keyed by key;
re-registering the same key replaces the previous env. A component
instance is folded into the callback dispatch automatically, so reach
for this from a non-component context that hosts a UI surface (a scene
entrypoint registering its own screen). Pair with
ui.unregisterCallbackEnv(key) so the ref is released.
Parameters
keystring— Stable identifier for this registration (re-register replaces).env{ [string]: any }— Environment table whoseonCallbackreceives the broadcasts.
typed/builtin//modules/api/engine/ui/ui/registerScreen
ui.registerScreen(name: string, widgetTree: WidgetTree, layer: number?)
Register a named UI screen with a widget tree. Optional
layer controls z-ordering (higher = on top), in bands: below 0
behind everything, 0-99 ordinary app depth, 100-999 always-on-top
chrome, 1000+ menu and popup depth. A screen in a higher band
covers one in a lower band whatever their roots are; inside a band
a floating area or window root sits over ordinary content, and
a modal root sits over the whole stack. Tag-based
grouping lives in Z.tags (Z.tags.set(name, { "editor" })
after register). A screen belongs to the component instance whose
own code registers it (its hooks and methods, and any closure they
create, such as a listener it subscribes): that instance's
onCallback receives its callbacks, and the screen is unregistered
when the instance ends. Code that is not an instance's own (a scene
entrypoint, a module, a listener either subscribed) registers a
screen no instance owns, which stays until it is unregistered,
whichever component's hook ran that code. topics/ui has the rule.
Parameters
namestring— Unique screen identifier.widgetTreeWidgetTree— Root widget table.layernumber(optional) — Z-order layer (optional).
ui.registerScreen("hud", tree)
typed/builtin//modules/api/engine/ui/ui/registerTheme
ui.registerTheme(name: string, theme: ThemeDefinition)
Register a theme from a flat Luau table. Most callers
should use Z.theme.register(name, table) which runs the
cascade for them.
Parameters
namestring— Theme name to register.themeThemeDefinition— Flat-resolved theme table.
typed/builtin//modules/api/engine/ui/ui/removeScreen
ui.removeScreen(name: string) -> boolean
Alias for ui.unregisterScreen.
Parameters
namestring— Screen identifier to remove.
Returns boolean — True when a screen by this name was registered.
typed/builtin//modules/api/engine/ui/ui/resetAreaSize
ui.resetAreaSize(id: string)
Clear a resizable area's remembered size (from a grip drag or
ui.setAreaSize) so its declared — or content — size takes over again.
Parameters
idstring— Area widget id.
typed/builtin//modules/api/engine/ui/ui/response
ui.response(widgetId: string) -> WidgetResponse?
Per-widget interaction snapshot for the most recent frame.
Returns { clicked, hovered, focused, changed, value } where
clicked / changed mark transitions and hovered / focused
mark current state.
Parameters
widgetIdstring— The widget id (NOT the onClick / onChange callback id).
Returns WidgetResponse? — WidgetResponse or nil.
typed/builtin//modules/api/engine/ui/ui/safeArea
ui.safeArea() -> SafeArea
The part of the screen no system bar, display cutout or on-screen
keyboard covers, in the logical points ui.screenSize() reports.
top / right / bottom / left are how far in from each edge the
covered band reaches; x / y / width / height are the rect they
leave; cutouts lists each display cutout (a camera hole or a notch) as
{ x, y, width, height }. Screens lay out inside this rect, so anchors,
top-level % sizes and a window's default position already keep clear
of it; read it to place something by absolute pos, or to run a
background under the bars while its controls stay inside. revision
advances every time the area changes (a rotation, the system bars shown
or hidden, the keyboard opening, a window resize): compare it with the
one you last laid out against. All zero on a desktop window;
ui.simulateSafeArea lays the UI out as a phone would on any engine.
Returns SafeArea — { top, right, bottom, left, x, y, width, height, cutouts, revision } in logical UI points.
local safe = ui.safeArea()
ui.setAreaPos("hud", safe.x + 8, safe.y + 8)
typed/builtin//modules/api/engine/ui/ui/screen
ui.screen(name: string) -> { [string]: any }?
Get a screen proxy with methods like setResolution and
rasterize.
Parameters
namestring— Screen name.
Returns { [string]: any }? — Screen proxy table, or nil.
typed/builtin//modules/api/engine/ui/ui/screenSize
ui.screenSize() -> { width: number, height: number }
The UI coordinate space as { width, height } (logical points). This is
the space area pos, anchors, and getLayoutInfo rects use — and it is
NOT the pixel size of a capture screenshot, which may be downscaled. Use
this for absolute area positioning (e.g. pinning a menu above a bottom
taskbar) instead of guessing the size from a capture image.
It follows the size the engine draws at, and renderer.setViewportSize
changes that size for the task that asks, which is how a layout written
against this is checked at a second shape without rebooting the engine,
read in the call that set the size before the surface goes back. The pixel size
behind these points is renderer.surfaceSize() — the whole drawing
surface a screen is laid out over, which an editor layout makes larger
than the rect renderer.viewportSize() draws the scene into — and
ui.pixelRatio() is the factor between the two spaces.
Returns { width: number, height: number } — { width, height } in logical UI points.
typed/builtin//modules/api/engine/ui/ui/scroll
ui.scroll(deltaX: number, deltaY: number)
Simulate a mouse-wheel scroll event on the UI.
Parameters
deltaXnumber— Horizontal scroll delta.deltaYnumber— Vertical scroll delta.
typed/builtin//modules/api/engine/ui/ui/setAreaPos
ui.setAreaPos(id: string, x: number, y: number)
Programmatically move a movable area widget to (x, y).
Applied for one frame; subsequent frames let drag tracking
take over.
Parameters
idstring— Area widget id.xnumber— Target pivot x (screen coords).ynumber— Target pivot y (screen coords).
typed/builtin//modules/api/engine/ui/ui/setAreaSize
ui.setAreaSize(id: string, w: number, h: number)
Programmatically set a resizable area's size (the user-size
override) — for maximize / restore / tile. Persists until the area's
declared width/height changes or ui.resetAreaSize(id) clears it.
Parameters
idstring— Area widget id.wnumber— Target width (screen coords).hnumber— Target height (screen coords).
typed/builtin//modules/api/engine/ui/ui/setDockWindowRect
ui.setDockWindowRect(dockId: string, panelId: string, x: number, y: number, width: number, height: number)
Place the floating window of a dockArea panel at (x, y) with
size (width, height). Applies once the panel occupies a window —
a request made before then waits for it.
Parameters
dockIdstring— DockArea widget id.panelIdstring— Id of the panel held by the window to place.xnumber— Window left edge (screen coords).ynumber— Window top edge (screen coords).widthnumber— Window width (screen coords).heightnumber— Window height (screen coords).
typed/builtin//modules/api/engine/ui/ui/setScreenRenderLayer
ui.setScreenRenderLayer(name: string, mask: number)
Set a screen's render-layer membership bitmask. A screen draws into a
camera or capture only when this mask intersects the camera's include
mask, the same rule geometry follows. Content UI carries the ui bit;
the editor places its chrome on EditorUI so agent captures can drop it,
and a screen carrying that bit is part of the window rather than the
camera's picture. Stating a mask here fixes the membership: a screen left
to itself takes the one the surface it is placed on implies, EditorUI
for the window and ui for the camera's picture. __renderLayers.bit(name)
answers with a layer's bit INDEX, so a mask sets the bit at that index and
__renderLayers.names(mask) reads it back as the names it holds.
Parameters
namestring— Screen identifier.masknumber— Render-layer membership bitmask — the OR ofbit32.lshift(1, __renderLayers.bit(name))over the layers the screen is on.
ui.setScreenRenderLayer("hud", bit32.lshift(1, __renderLayers.bit("ui")))
typed/builtin//modules/api/engine/ui/ui/setScreenSpace
ui.setScreenSpace(name: string, space: string)
Set which surface a screen is part of. "camera" is the picture the
viewport camera produces: the screen lays out over the camera's target
and is drawn into it, so it sits inside the Scene panel presenting the
scene and appears in every capture of the camera — the default for
content UI. "window" is the window itself, whatever presents the scene
— where the editor's chrome lives, and the default for a screen on the
EditorUI layer. The two are one statement made from either end, so a
screen placed on the window takes the EditorUI membership unless
setScreenRenderLayer states another: the window goes on drawing it while
a capture that drops the authoring layers, which is what an agent takes by
default, leaves it out. Where the scene presents on the window, the two
surfaces coincide. A name that is neither raises.
Parameters
namestring— Screen identifier.spacestring—"camera"|"window".
ui.setScreenSpace("agent-banner", "window")
typed/builtin//modules/api/engine/ui/ui/setScrollPosition
ui.setScrollPosition(widgetId: string, offsetY: number)
Set the scroll offset of a scrollArea widget.
Parameters
widgetIdstring— Scroll area widget id.offsetYnumber— Vertical scroll offset in pixels.
typed/builtin//modules/api/engine/ui/ui/setShaderUniforms
ui.setShaderUniforms(name: string, uniforms: { [string]: number })
Set uniform values on a registered background shader.
Parameters
namestring— Shader name identifier.uniforms{ [string]: number }— Map of uniform name to number value.
typed/builtin//modules/api/engine/ui/ui/setTheme
ui.setTheme(name: string)
Switch the active global theme by name.
Parameters
namestring— Theme name to activate.
typed/builtin//modules/api/engine/ui/ui/showScreen
ui.showScreen(name: string) -> boolean
Make a registered screen visible, and report whether a screen by
that name is registered. The engine applies the show later in the
frame; listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to show.
Returns boolean — True when a screen by this name is registered.
typed/builtin//modules/api/engine/ui/ui/simulateDisplayScale
ui.simulateDisplayScale(scale: DisplayScaleSpec?)
Size the UI as a display stating scale would, in place of what the
platform states. A density sizes it as a handheld display of that
many physical pixels per point (omit it for a desktop display sized by
its width), and textScale multiplies every font size the UI typesets
(1 when omitted). Previews a phone's UI on a desktop or headless engine,
and makes a layout test independent of the device it runs on.
ui.displayScale(), ui.screenSize() and ui.pixelRatio() report it
from the next frame. Call it with no argument (or nil) to return to
the platform's own scale. A value that is not a number above 0 and at
most 8 is refused by name.
Parameters
scaleDisplayScaleSpec(optional) —{ density?, textScale? }; nil returns to the platform's scale.
ui.simulateDisplayScale({ density = 2.625, textScale = 1.3 })
ui.simulateDisplayScale()
typed/builtin//modules/api/engine/ui/ui/simulateSafeArea
ui.simulateSafeArea(area: SafeAreaSpec?)
Lay the UI out as if the system covered the screen by area
(logical points) in place of what the platform states: each inset
defaults to 0 and cutouts is a list of { x, y, width, height }.
Previews a phone's status bar, camera cutout and navigation bar on a
desktop or headless engine, and makes a layout test independent of the
device it runs on. ui.safeArea() reports it from the next frame. Call
it with no argument (or nil) to return to the platform's own safe
area. A negative or non-number inset is refused by name.
Parameters
areaSafeAreaSpec(optional) —{ top?, right?, bottom?, left?, cutouts? }in logical points; nil returns to the platform's safe area.
ui.simulateSafeArea({ top = 40, bottom = 30, cutouts = { { x = 600, y = 0, width = 80, height = 40 } } })
ui.simulateSafeArea()
typed/builtin//modules/api/engine/ui/ui/unregisterCallbackEnv
ui.unregisterCallbackEnv(key: string)
Remove an environment registered with ui.registerCallbackEnv. Its
onCallback stops receiving broadcasts. No-op if key isn't registered.
Parameters
keystring— The key passed toui.registerCallbackEnv.
typed/builtin//modules/api/engine/ui/ui/unregisterScreen
ui.unregisterScreen(name: string) -> boolean
Remove a screen from the registry entirely. Unlike
hideScreen, this deletes the entry so it no longer appears in
listScreens or render iteration.
Parameters
namestring— Screen identifier to unregister.
Returns boolean — True when a screen by this name was registered.
typed/builtin//modules/api/engine/ui/ui/unregisterWidget
ui.unregisterWidget(name: string)
Drop a registered custom widget kind. Subsequent references
produce an unknown-widget-type diagnostic.
Parameters
namestring— Custom widget kind name.
typed/builtin//modules/api/engine/ui/ui/updateScreen
ui.updateScreen(name: string, widgetTree: WidgetTree)
Replace the widget tree of an already-registered screen.
Parameters
namestring— Screen identifier to update.widgetTreeWidgetTree— New root widget table.
typed/builtin//modules/api/engine/ui/ui/useStyles
ui.useStyles(themeName: string)
Apply a registered style file's classes additively without changing the active theme.
Parameters
themeNamestring— Name of the registered style / theme asset.
typed/builtin//modules/api/engine/ui/ui/widgetState
ui.widgetState(widgetId: string, key: string, default: any?) -> any
Read per-widget cross-frame state. Returns the value
previously written via widgetStateSet, or default (or nil).
State is keyed by widget id and persists across re-renders
within a screen's lifetime; cleared automatically when the
owning screen is unregistered.
Parameters
widgetIdstring— Widget id whose state to read.keystring— State key.defaultany(optional) — Value to return when nothing has been written.
Returns any — Stored value, default, or nil.
typed/builtin//modules/api/engine/ui/ui/widgetStateClear
ui.widgetStateClear(widgetId: string, key: string)
Remove a per-widget state entry.
Parameters
widgetIdstring— Widget id whose state to clear.keystring— State key.
typed/builtin//modules/api/engine/ui/ui/widgetStateSet
ui.widgetStateSet(widgetId: string, key: string, value: any?)
Write per-widget cross-frame state. Replaces any existing
value under (widgetId, key). Tables are stored by reference.
Parameters
widgetIdstring— Widget id to scope the state under.keystring— State key.valueany(optional) — Value to store (must be non-nil).