UiHandle
ctx.ui — the game UI's screen stack.
Every failure throws a UiError whose message begins with a stable code, so a behaviour can match on the code and a log line says what went wrong without a debugger: | code | when | |---|---| | ui.screen.unknown | no screen answers to that name; the message lists the ones that do | | ui.screen.bad-json | the description does not match the contract; the message names the path | | ui.screen.duplicate | push naming a screen already on the stack, or replace naming one below the top | | ui.stack.empty | pop with nothing on the stack | | ui.set.unknown-node | no described screen holds that node id | | ui.set.unknown-prop | that property is not settable; the message lists the ones that are | | ui.set.bad-value | the property is settable and the value is the wrong kind for it | | ui.get.unknown-node | no screen on the stack holds that node id | | ui.get.unknown-prop | that property is not readable; the message lists the ones that are | | ui.get.not-a-field | a text-field property on a node of another role | | ui.locale.unknown | this project ships no locale by that name; the message lists the ones it does | | ui.unavailable | this world has no UI runtime at all | The hud screen is always the bottom entry and is written through {@link HudHandle}, not through set.
Signature
export interface UiHandle {
/**
* Register a screen from its description, and return its name.
*
* Registering a name that is already live rebuilds it in place, carrying hover, press and
* focus across by id — the same path hot reload takes. So this is also how a screen is
* updated wholesale.
*/
screen(description: UiScreenDescription): string
/**
* Put a registered screen on top of the stack.
*
* `props` are the screen's own; scalar entries land on its root as `data-*`, so a stylesheet
* can select on them, and `GET /ui/screens` reports them whole.
*/
push(name: string, props?: Record<string, unknown>): void
/** Take the top screen off; returns its name. */
pop(): string
/**
* Pop the top screen and push this one.
*
* **Both halves happen or neither does.** A name that is already on the stack is refused
* with `ui.screen.duplicate` and the stack is left exactly as it was — the runtime cannot
* hold two instances of one name, and popping before finding that out would lose the screen
* that was on top while the word promises to swap it. `pop()` down to it, or register the
* second instance under its own name.
*
* **The screen already on top is an update.** It stays where it is and is re-rendered at
* `props`, as a push with them would render it: nothing is popped, so nothing is lost.
*/
replace(name: string, props?: Record<string, unknown>): void
/** The stack, bottom first. `["hud"]` when nothing else is up. */
stack(): string[]
/**
* Write one bound value.
*
* Takes the qualified id `GET /ui` reports (`pause:resume`) or the bare author id
* (`resume`) when exactly one screen on the stack answers to it; an ambiguous bare id is
* refused rather than guessed at.
*
* **Change-gated at the source.** The runtime holds the last value written for every node
* and property, and a `set` that matches it does nothing at all — no restyle, no relayout,
* no redraw. A HUD pushing its score at 60 Hz costs nothing on the fifty-nine frames where
* the score did not change, so a behaviour should write unconditionally rather than
* keeping its own copy to compare against.
*
* `null` clears the binding, and the node goes back to the value its screen was authored
* with. A `style:` property takes the declaration's text, e.g.
* `set("bar", "style:width", "62%")`.
*
* `text-param:<name>` fills one `{name}` slot of the node's string key (§2.11), e.g.
* `set("score", "text-param:n", 3)`. **Pass a number as a number**: the plural rule
* selects on it, so a count written as text would make the plural form depend on how the
* behaviour spelled its own integer. The text is re-resolved in Rust, so a later
* {@link UiHandle.setLocale} picks the right form in the new language with no script
* involvement.
*
* **What is written outlives a pop.** The values a screen was `set` are remembered per
* screen and replayed when it is pushed or replaced back onto the stack, so a menu a
* behaviour filled in comes back filled in rather than authored. Re-registering the screen
* with {@link UiHandle.screen} is the other half and goes the other way: a rebuild is the
* author saying what the screen is now, so the values it carries win and everything written
* into the old one is forgotten. Clearing one with `null` forgets it too.
*/
set(id: string, prop: UiProp, value: string | number | boolean | null): void
/**
* Read one property of one node back — what a behaviour reads a text field with.
*
* const name = ctx.ui.get("signin:name", "value") as string
*
* A read rather than an event, because nothing in this engine delivers a UI action into
* script: a button activation goes to `WS /stream` and to the editor's feed. An
* event-delivered value would mean an event bus built for one property, and a behaviour
* would still keep its own copy to answer "what is in the field now". A read is always
* current and is the mirror image of `set(id, "value", …)`.
*
* Takes the id either way, exactly as {@link UiHandle.set} does.
*
* | property | answers |
* |---|---|
* | `value` | what a `text-field` holds, as a string |
* | `length` | how many **characters** it holds, which is what `maxLength` counts |
* | `caret` | the caret's byte offset into `value` |
* | `selection` | `[from, to]` byte offsets, or `null` when nothing is selected |
* | `composition` | the IME preedit in flight, or `null` |
* | `text` | any node's drawn text, resolved from its key |
*
* A password field answers `value` like any other: it is this screen's own script asking.
* What a password is kept out of is `GET /ui`, which anything on the port can read.
*/
get(id: string, prop: UiReadable): string | number | [number, number] | null
/**
* The locale text keys resolve at.
*
* `"base"` when no locale file is active — the scene's own `meta.strings`, in whatever
* language its author wrote it.
*/
locale(): string
/** Every locale this project ships, `"base"` first and the rest sorted. */
locales(): string[]
/**
* `"ltr"` or `"rtl"` for the active locale.
*
* A call rather than something a behaviour derives from the tag: which languages read right
* to left belongs to the locale file, and a script's own table of them is a second copy
* that goes wrong the first time a project adds one.
*/
direction(): "ltr" | "rtl"
/**
* Switch language.
*
* Every text key on the stack is re-resolved in Rust, so this is a re-render rather than a
* rebuild: a menu that is already up comes back in the other language with its focus, its
* scroll position and its open dialog intact, and no behaviour runs again.
*
* Throws `ui.locale.unknown` when the project ships no locale by that name, with the
* message listing the ones it does. An unknown locale changes nothing — a screenshot matrix
* that silently rendered the base language for a locale nobody translated would report a
* clean pass over a language that does not exist.
*/
setLocale(tag: string): void
}
Import: import type { UiHandle } from "@argus/api"
Members
| Member | Signature | Description |
|---|---|---|
screen | screen(description: UiScreenDescription): string | Register a screen from its description, and return its name. Registering a name that is already live rebuilds it in place, carrying hover, press and focus across by id — the same path hot reload takes. So this is also how a screen is updated wholesale. |
push | push(name: string, props?: Record<string, unknown>): void | Put a registered screen on top of the stack. props are the screen's own; scalar entries land on its root as data-*, so a stylesheet can select on them, and GET /ui/screens reports them whole. |
pop | pop(): string | Take the top screen off; returns its name. |
replace | replace(name: string, props?: Record<string, unknown>): void | Pop the top screen and push this one. Both halves happen or neither does. A name that is already on the stack is refused with ui.screen.duplicate and the stack is left exactly as it was — the runtime cannot hold two instances of one name, and popping before finding that out would lose the screen that was on top while the word promises to swap it. pop() down to it, or register the second instance under its own name. The screen already on top is an update. It stays where it is and is re-rendered at props, as a push with them would render it: nothing is popped, so nothing is lost. |
stack | stack(): string[] | The stack, bottom first. ["hud"] when nothing else is up. |
set | set(id: string, prop: UiProp, value: string | number | boolean | null): void | Write one bound value. Takes the qualified id GET /ui reports (pause:resume) or the bare author id (resume) when exactly one screen on the stack answers to it; an ambiguous bare id is refused rather than guessed at. Change-gated at the source. The runtime holds the last value written for every node and property, and a set that matches it does nothing at all — no restyle, no relayout, no redraw. A HUD pushing its score at 60 Hz costs nothing on the fifty-nine frames where the score did not change, so a behaviour should write unconditionally rather than keeping its own copy to compare against. null clears the binding, and the node goes back to the value its screen was authored with. A style: property takes the declaration's text, e.g. set("bar", "style:width", "62%"). text-param:<name> fills one {name} slot of the node's string key (§2.11), e.g. set("score", "text-param:n", 3). Pass a number as a number: the plural rule selects on it, so a count written as text would make the plural form depend on how the behaviour spelled its own integer. The text is re-resolved in Rust, so a later {@link UiHandle.setLocale} picks the right form in the new language with no script involvement. What is written outlives a pop. The values a screen was set are remembered per screen and replayed when it is pushed or replaced back onto the stack, so a menu a behaviour filled in comes back filled in rather than authored. Re-registering the screen with {@link UiHandle.screen} is the other half and goes the other way: a rebuild is the author saying what the screen is now, so the values it carries win and everything written into the old one is forgotten. Clearing one with null forgets it too. |
get | get(id: string, prop: UiReadable): string | number | [number, number] | null | Read one property of one node back — what a behaviour reads a text field with. const name = ctx.ui.get("signin:name", "value") as string A read rather than an event, because nothing in this engine delivers a UI action into script: a button activation goes to WS /stream and to the editor's feed. An event-delivered value would mean an event bus built for one property, and a behaviour would still keep its own copy to answer "what is in the field now". A read is always current and is the mirror image of set(id, "value", …). Takes the id either way, exactly as {@link UiHandle.set} does. | property | answers | |---|---| | value | what a text-field holds, as a string | | length | how many characters it holds, which is what maxLength counts | | caret | the caret's byte offset into value | | selection | [from, to] byte offsets, or null when nothing is selected | | composition | the IME preedit in flight, or null | | text | any node's drawn text, resolved from its key | A password field answers value like any other: it is this screen's own script asking. What a password is kept out of is GET /ui, which anything on the port can read. |
locale | locale(): string | The locale text keys resolve at. "base" when no locale file is active — the scene's own meta.strings, in whatever language its author wrote it. |
locales | locales(): string[] | Every locale this project ships, "base" first and the rest sorted. |
direction | direction(): "ltr" | "rtl" | "ltr" or "rtl" for the active locale. A call rather than something a behaviour derives from the tag: which languages read right to left belongs to the locale file, and a script's own table of them is a second copy that goes wrong the first time a project adds one. |
setLocale | setLocale(tag: string): void | Switch language. Every text key on the stack is re-resolved in Rust, so this is a re-render rather than a rebuild: a menu that is already up comes back in the other language with its focus, its scroll position and its open dialog intact, and no behaviour runs again. Throws ui.locale.unknown when the project ships no locale by that name, with the message listing the ones it does. An unknown locale changes nothing — a screenshot matrix that silently rendered the base language for a locale nobody translated would report a clean pass over a language that does not exist. |
Example
The tested fixture fixtures/app/agent.ts.
import type { Behavior } from "@argus/api"
// Sends a nav agent to `props.target` once, then reports arrival through ctx.state so the
// test can read it without polling the crowd directly.
export default {
init(ctx) {
ctx.state.arrived = false
ctx.nav?.goTo(ctx.props.target as [number, number, number])
},
update(ctx) {
if (ctx.nav?.arrived) ctx.state.arrived = true
},
} satisfies Behavior