# UiHandle

> Talos 0.6 · Since 0.6

`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

```ts
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`.

```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
```
