> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spurdoverse.app/llms.txt
> Use this file to discover all available pages before exploring further.

# UI

> The script's settings window: pages, controls, hotkeys and reactive values.

Your script's own window in the menu. Declare pages and controls once, and the menu builds the
widgets and saves their values into the user's configs.

```ts theme={null}
import ui from "@native/ui";
```

<Note>
  Declare at the top level, not in `tick` or `render`. There is no draw callback: you read
  `control.value` or listen for `change`. Everything is removed when the script unloads.
</Note>

## Overview

<CardGroup cols={2}>
  <Card title="Layout" icon="table-columns" href="#layout">
    `page`, `group`, `row`.
  </Card>

  <Card title="Controls" icon="toggle-on" href="#controls">
    `switch`, `slider`, `combo`, `multi`, `color`, `input`, `button`, `text`.
  </Card>

  <Card title="Reactive values" icon="bolt" href="#reactive-values">
    `signal`, `computed`, `effect`, `batch`.
  </Card>

  <Card title="Keybinds" icon="keyboard" href="#keybinds">
    `key`, `mode`, `active`, `press` / `release`.
  </Card>

  <Card title="Paths and persistence" icon="floppy-disk" href="#paths-and-persistence">
    `id`, `persist`, `dispose`.
  </Card>

  <Card title="Window" icon="window-maximize" href="#window">
    `open`, `close`, `on`.
  </Card>
</CardGroup>

```ts theme={null}
const aim = ui.page("Aim", { icon: "target" }).group("General", { toggle: true });
const fov = aim.slider("FOV", { min: 1, max: 30, default: 5, unit: "°" });

on("tick", () => {
    if (aim.value) console.log(fov.value);
});
```

***

## Layout

### page

<br />

```ts theme={null}
ui.page(title: string, options?: { id?: string; icon?: string; order?: number }): Page
```

A tab in the window. Asking for the same title (or `id`) again returns the same page, and the
options of that repeat call are ignored. `icon` is a Tabler icon name; tabs sort by `order`
(default `0`). `page.show()` brings the tab to the front.

```ts theme={null}
const main = ui.page("Main", { icon: "target", order: -1 });
```

### group

<br />

```ts theme={null}
container.group(title: string, options?: GroupOptions): Group
```

A panel. Same get-or-create rule as `page`. Without `column`, a page's groups are split down the
middle in declaration order.

| Option | |
| :- | :- |
| `column` | `"left"` or `"right"`. |
| `toggle` | Puts a switch in the header: `group.value`, `group.on("change")`, `group.reset()`. |
| `default`, `persist`, `key`, `onChange` | Like on a [switch](#switch). Only with `toggle`. |

```ts theme={null}
const trigger = main.group("Triggerbot", { column: "right", toggle: true, key: { mode: "hold" } });
```

### row

<br />

```ts theme={null}
container.row(build?: (row: Row) => void): Row
container.row(options: { visible?: Reactive<boolean>; disabled?: Reactive<boolean> }, build?: (row: Row) => void): Row
```

Lays its children out side by side. `build` runs right away with the row.

```ts theme={null}
trigger.row((row) => {
    row.button("Save", { style: "accent" });
    row.button("Reset", { style: "danger" });
});
```

***

## Controls

Every container (page, group, row) has the same builders. The first argument is the label.

| Option | Default | |
| :- | :- | :- |
| `id` | label, slugified | The path segment. See [paths](#paths-and-persistence). |
| `visible`, `disabled` | `true`, `false` | A boolean or a [reactive function](#reactive-values). |
| `persist` | `true` | `false` keeps the value, and its hotkey, out of configs. |
| `onChange` | | Same as `control.on("change", fn)`. |

A control has these members:

| Member | |
| :- | :- |
| `value` | Read or write. Writing updates the menu and fires `change`. |
| `label`, `visible`, `disabled` | Read or write. Writing a function makes it [reactive](#reactive-values). |
| `on("change", fn)` | Fires after the user, the script, `reset()` or a config changed the value. Returns a `Subscription`. |
| `reset()` | Back to the declared default. |
| `key` | The [keybind](#keybinds), or `null`. |
| `id`, `alive`, `dispose()` | Path, liveness, removal. |

### switch

<br />

```ts theme={null}
container.switch(label: string, options?: { default?: boolean; key?: boolean | KeyOptions }): Control<boolean>
```

A checkbox. `key` attaches a [hotkey](#keybinds); `true` is an unbound `"hold"` bind.

```ts theme={null}
const enabled = main.switch("Enabled", { default: true, key: { key: 0x05, mode: "toggle" } });
```

### slider

<br />

```ts theme={null}
container.slider(label: string, options?: SliderOptions): Control<number>
```

| Option | Default | |
| :- | :- | :- |
| `min`, `max` | `0`, `100` | `min` must not exceed `max`. |
| `default` | `min` | Must lie in `min..max`. |
| `step` | `0` | Snap step, `0` for none. |
| `precision` | `0` | Decimals shown. `0` makes it an integer slider. |
| `unit` | | Appended to the printed value, e.g. `"°"` or `" ms"`. |

```ts theme={null}
const smooth = main.slider("Smooth", { min: 1, max: 20, step: 0.5, precision: 1, default: 4 });
```

### combo

<br />

```ts theme={null}
container.combo(label: string, items: readonly string[], options?: { default?: string | number }): ListControl
```

One of `items`. The value is the item text; writing takes an item or its index. The items type
the value, so `combo("Mode", ["Legit", "Rage"])` is `"Legit" | "Rage"` without `as const`.

```ts theme={null}
const mode = main.combo("Mode", ["Legit", "Rage"]);
mode.value = 1;           // "Rage"
mode.items = ["Legit"];   // selection falls back to the default, change fires
```

Items must be unique and non-empty. Writing an unknown item or index throws.

### multi

<br />

```ts theme={null}
container.multi(label: string, items: readonly string[], options?: { default?: readonly (string | number)[] }): MultiControl
```

Any number of `items`. The value is the picked items in list order; writing takes items and/or
indices. Replacing `items` drops picks that are gone.

```ts theme={null}
const bones = main.multi("Bones", ["Head", "Chest", "Pelvis"], { default: ["Head"] });
bones.value = ["Head", 2]; // ["Head", "Pelvis"]
```

### color

<br />

```ts theme={null}
container.color(label: string, options?: { default?: ColorLike; key?: boolean | KeyOptions }): Control<Color, ColorLike>
```

A color picker. The value is a new [Color](/api/types/color) on every read; writing takes any
`ColorLike`. Default white. `key` works like on a switch.

```ts theme={null}
const boxColor = main.color("Box", { default: "#ff4040" });
```

### input

<br />

```ts theme={null}
container.input(label: string, options?: { default?: string; placeholder?: string }): Control<string>
```

A text field.

```ts theme={null}
const tag = main.input("Clan tag", { placeholder: "none" });
```

### button

<br />

```ts theme={null}
container.button(label: string, options?: { style?: "normal" | "accent" | "danger" | "dashed"; icon?: string; onClick?: () => void }): Button
```

Has no value. `button.on("click", fn)` subscribes, `button.click()` runs the listeners as a real
click would.

```ts theme={null}
main.button("Reset all", { style: "danger", onClick: () => ui.batch(() => { fov.reset(); smooth.reset(); }) });
```

### text

<br />

```ts theme={null}
container.text(text: Reactive<string>, options?: TextOptions): Text
```

A line of wrapped text. Pass a function to keep it up to date. Never saved.

```ts theme={null}
main.text(() => `FOV ${fov.value}°, smooth ${smooth.value}`);
```

***

## Reactive values

`label`, `visible`, `disabled` and a text's `text` accept a function, as an option or an
assignment. It runs now and again whenever a control `value`, a `key.active` or a signal it read
changes. Assigning again replaces it.

```ts theme={null}
main.slider("Max angle", { min: 1, max: 180, visible: () => mode.value === "Rage" });
```

Exceptions thrown in reactive functions and handlers are reported through the global `error`
event.

### signal

<br />

```ts theme={null}
ui.signal<T>(initial?: T): Signal<T>
```

A value holder. Setting `value` to something new fires `change` and re-runs whatever read it.
Mutating an object in place doesn't count.

```ts theme={null}
const kills = ui.signal(0);
main.text(() => `Kills: ${kills.value}`);
kills.value = (kills.value ?? 0) + 1;
```

### computed

<br />

```ts theme={null}
ui.computed<T>(compute: () => T): Computed<T>
```

A read-only value derived from others. `compute` re-runs when something it read changes; `change`
fires when the result differs.

```ts theme={null}
const radius = ui.computed(() => Math.tan(Math.toRadians(fov.value ?? 0)) * 400);
```

### effect

<br />

```ts theme={null}
ui.effect(fn: () => void, options?: { signal?: AbortSignal }): Subscription
```

Runs `fn` now and again whenever anything it read changes, until the Subscription is disposed.

```ts theme={null}
const stop = ui.effect(() => console.log("mode is", mode.value));
stop();
```

### batch

<br />

```ts theme={null}
ui.batch<T>(fn: () => T): T
```

Defers reactive re-runs until `fn` returns, then runs each affected function once. Returns what `fn` returns.

```ts theme={null}
ui.batch(() => {
    fov.value = 10;
    smooth.value = 2;
});
```

***

## Keybinds

A switch, color or toggle group declared with `key` has a `KeyBind` at `control.key`. The user can
rebind the key and change the mode in the menu; a persisted control saves both.

| Member | |
| :- | :- |
| `key` | Win32 virtual-key code, `0` when unbound. |
| `mode` | `"hold"` (active while down), `"toggle"` (flips on each press) or `"always"`. |
| `active` | Whether it's active right now. Reactive. |
| `on("press", fn)`, `on("release", fn)` | When it becomes active and when it stops. Returns a `Subscription`. |
| `alive` | Whether its control is alive. |

The `key` option (`{ key?, mode? }`) is only a default: once the user binds or clears the key, theirs wins.

```ts theme={null}
const aimbot = main.switch("Aimbot", { key: { key: 0x06, mode: "hold" } });

on("tick", () => {
    if (aimbot.value && aimbot.key?.active) {
        // aim
    }
});
aimbot.key?.on("release", () => console.log("released"));
```

***

## Paths and persistence

Saved values are keyed by path: the parent's path plus the `id` option or the slugified label.
`ui.page("Aim").group("Main").slider("FOV")` is `aim/main/fov`. Rows add nothing.

* Two controls on the same path throw `EEXIST`. Give one an `id` (buttons and texts don't clash).
* Renaming a label moves the path and loses the saved value. Set `id` when the label may change.
* `persist: false` keeps a value for the session only.

```ts theme={null}
main.slider("Field of view", { id: "fov", min: 1, max: 30 }); // path stays "main/fov"
```

### dispose

<br />

```ts theme={null}
node.dispose(): void
```

Removes a node and everything in it. After that `alive` is `false`, reads return `null` and
writes throw. Declaring the same path again creates a fresh node. Signals and computeds have `dispose()` too.

```ts theme={null}
const debug = main.group("Debug");
debug.dispose();
```

***

## Window

| Call | |
| :- | :- |
| `ui.open()` | Opens the window. Does nothing until there's a page. |
| `ui.close()` | Closes it. |
| `ui.on("open", fn)`, `ui.on("close", fn)` | Follows the window. |
| `ui.on("config", fn)` | After a config is loaded and its `change` events have fired. |

***

## Example

A crosshair with its settings.

```ts theme={null}
import ui from "@native/ui";
import render from "@native/render";

const main = ui.page("Crosshair", { icon: "crosshair" }).group("Crosshair", { toggle: true, default: true });
const style = main.combo("Style", ["Cross", "Dot", "Circle"]);
const size = main.slider("Size", { min: 2, max: 40, default: 8, unit: " px" });
const gap = main.slider("Gap", { min: 0, max: 20, default: 3, visible: () => style.value === "Cross" });
const color = main.color("Color", { default: "#40ff40" });
main.text(() => `${style.value}, ${size.value} px`);

on("render", () => {
    if (!main.value) return;
    const { x, y } = render.screenSize.scale(0.5);
    const s = size.value ?? 8;
    const g = gap.value ?? 0;
    const c = color.value ?? "#fff";

    if (style.value === "Dot") render.circleFilled([x, y], s / 4, c);
    else if (style.value === "Circle") render.circle([x, y], s, c, { thickness: 1.5 });
    else {
        render.line([x - s - g, y], [x - g, y], c);
        render.line([x + g, y], [x + s + g, y], c);
        render.line([x, y - s - g], [x, y - g], c);
        render.line([x, y + g], [x, y + s + g], c);
    }
});
```
