Skip to main content
Your script’s own window in the menu. Declare pages and controls once, the menu builds real widgets and saves their values into the user’s configs.
Two ideas carry the whole module:
  • Every control is a signal. fov.value is tracked, so a control can feed computed, effect or another control’s prop directly: visible: enabled.
  • Any prop can be reactive. Give label, visible, items, min, tooltip and friends a value, a function, or a signal / control / hotkey, and it follows.
Declare at the top level, not in tick or render. Everything goes away when the script unloads.

Overview

Layout

page, group, section, row, stack, each.

Controls

switch, slider, range, combo, multi, selector, list, color, input, bind.

Everything else

button, text, separator, image, canvas, menus, tooltips.

Reactivity

signal, computed, effect, batch, untrack.

Hotkeys

key, mode, active, press / release.

Window and dialogs

open, confirm, prompt, snapshot, JSX.

Layout

page

A tab. Asking for the same title (or id) again returns the same page; build runs every time. icon is a Tabler icon name, tabs sort by order. page.show() brings it to the front.

group

A panel, get-or-create like pages. Without column, a page’s groups split down the middle.

section

A collapsible block with a header. It adds nothing to paths, so moving controls into a section keeps their saved values. section.expanded is readable, writable and reactive.

row, stack

Children side by side, or one below the other with no chrome. Neither adds to paths.

each

Follows a list. render declares an item’s nodes on the same container (they land in the block); items that leave take their nodes with them, new ones render, moved ones move. A string or number key also scopes the item’s paths, so per-item values save.

Controls

Every container has the same builders. The first argument is the label. A control has:

switch

slider, range

precision: 0 (the default) makes an integer slider. min, max and unit are reactive; the value follows a narrowed range. A range has two handles and a [low, high] value.

combo, multi, selector

The value is the item text (a multi’s: the picked items, in list order); writing takes items or indices. The items type the value: combo("Mode", ["Legit", "Rage"]) is "Legit" | "Rage". items is reactive. A selector draws the same thing as chips.

list

Chips the user drags into an order; the value is every item, in that order. New items append, the order of the rest is kept.

color, input, bind

A color reads as a fresh Color. bind is a hotkey on a row of its own; its value is whether it’s active, so visible: panic works.

Buttons, text, images

button.click() runs the listeners like a real click. A text never saves.

canvas

A widget you draw yourself. g is render’s API in canvas-local pixels. It redraws when something draw read changes, or every tick with animate: true. info has width, height, hovered, pressed, pointer and time, all tracked. Pointer input comes as events: pointerdown, pointerup, pointermove, pointerenter, pointerleave and wheel, with x, y, button and deltaY.
Any node takes a right-click menu: items with label, icon, danger, disabled, onClick and items for a submenu, and "-" for a separator. Assign null to remove it.

Reactivity

A computed, effect or reactive prop re-runs when a value it read changes: a control’s, a signal’s, a hotkey’s active, a section’s expanded, ui.isOpen. It’s glitch-free: one re-run per change, never a half-updated state.
signal.subscribe((value, previous) => ...) listens without an effect. Exceptions in handlers and reactive functions go to the global error event.

Hotkeys

A switch, color or toggle group declared with key has a KeyBind at control.key. The key option (true, a key code, a mode or { key, mode }) is only a default: once the user binds or clears the key, theirs wins.

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, stacks and sections add nothing.
  • Two controls on one path throw EEXIST. Give one an id.
  • 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.
  • After dispose(), reads and writes throw ESTALE; alive and id still answer.

Window and dialogs

Dialogs are the menu’s own modal, as promises. Closing one any other way than OK is a no.

JSX

In a .tsx file (the template’s tsconfig already sets "jsxImportSource": "@native/ui") the same tree reads like markup. It’s the same builders underneath.
model binds a control to a signal both ways, ref hands you the node, <For each> is each().

Example

A crosshair with its settings and a live preview.