@qxuken/kui 0.1.0-alpha.41 → 0.1.0-alpha.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +188 -0
- package/encoder.js +2 -1
- package/index.d.ts +2 -0
- package/index.js +6 -2
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/kui_node.node +0 -0
- package/prebuilds/linux-arm64/kui_node.node +0 -0
- package/prebuilds/linux-x64/kui_node.node +0 -0
- package/prebuilds/win32-x64/kui_node.node +0 -0
- package/props.md +380 -374
package/props.md
CHANGED
|
@@ -5,177 +5,178 @@
|
|
|
5
5
|
those, not this file.*
|
|
6
6
|
|
|
7
7
|
One prop schema serves every binding: JSX props are camelCase, Lua keys are
|
|
8
|
-
their snake_case,
|
|
9
|
-
below
|
|
8
|
+
their snake_case, C uses the `KuiSpec` / `KuiTextStyle` field named
|
|
9
|
+
below, and Odin the `Spec` / `Text_Style` field of the snake_case name
|
|
10
|
+
(packages/odin, whose generator checks every Odin spelling in this file). Container props apply to `<box>` (and to `<edit>` / `<image>`
|
|
10
11
|
where they make sense); text props apply to `<text>` and `<edit>`.
|
|
11
12
|
|
|
12
13
|
## Container props
|
|
13
14
|
|
|
14
|
-
| JSX | Lua | C | type | description |
|
|
15
|
-
|
|
16
|
-
| `accent` | `accent` | `accent` | boolean | Paint this node's background in the OS accent colour — `env.system.accent` — keeping the declared `bg` on a host that cannot tell what it is. The one prop whose paint depends on the environment, which is why it is opt-in: the same tree is a different colour on two machines, and that is the point here and a surprise anywhere else. On the stock button it does the whole job — the hover and pressed shades are derived from the accent, and the label goes black or white by its luminance, so a yellow accent is still readable — which is what `<button accent>` is for. |
|
|
17
|
-
| `anchor` | `anchor` | `anchor` | boolean | Scroll anchoring on a scrolling node (backlog C26, CSS's `overflow-anchor`): the first child in view keeps its place on screen when the content before it changes size — a chat that prepends history, a log that inserts rows above the viewport, a list whose row heights are corrected as they are measured — with no `setScroll` and no arithmetic in the view. The core remembers which child was first in view and where its edge was, and moves the offset by however far that edge moved in the next layout, before the offset is clamped; a wheel notch or a `setScroll` between the frames is kept and the correction added to it. The child is found by key, so give the rows stable keys (a `key` or an `index`); a child that is gone anchors nothing that frame. On the scroll axis that is the node's main axis only — `scrollY` on a column, `scrollX` on a row — and content appended *after* the anchor moves nothing, so a log that is tailing still asks for the end itself. |
|
|
18
|
-
| `animate` | `animate` | `animate` | boolean | Ask for another frame after this one, every frame this node is declared. What a `fragment` that reads `time` needs, and what anything driving itself off the clock rather than off input needs. Opt-in like `exit`, and for the same reason: it takes the loop off input-driven and onto the display's cadence for as long as it is declared, so a still node must not carry it. One node asking is enough for the whole window. |
|
|
19
|
-
| `aspectRatio` | `aspect_ratio` | `aspect_ratio` | number, or a `"$length"` token | Width over height — `16/9`, `1` for a square — CSS's `aspect-ratio`. It sizes the axis left `fit`: a fit height is the final width over the ratio (so `width: grow` and a ratio is a box that keeps its shape as the window resizes), and a fit width under a fixed height is that height times it. With both axes declared, or a fit width under a `grow` or percent height, it has nothing it can set and warns. The derived axis is neither shrunk nor fitted to the children, which overflow it; `minHeight: 'fit'` floors it at them. On an image it wins over the pixels' own aspect. |
|
|
20
|
-
| `bg` | `bg` | `bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background fill. |
|
|
21
|
-
| `bounce` | `bounce` | `bounce` | number, or a `"$length"` token | How far a spring overshoots its target: 0 glides in with none, 0.5 bounces visibly, and values past 0.9 are held there (a spring at 1 would never settle). It replaces a spring `easing`'s own bounce (`smooth` 0, `snappy` 0.15, `spring` 0.25, `bouncy` 0.5), and on a timed easing makes the transition a spring — so `transition` plus `bounce` is a spring of that length and bounce. |
|
|
22
|
-
| `buttons` | `buttons` | `buttons` (`KUI_BUTTONS_*` bits; zeroed, all three) | string | Which non-primary buttons `onButton` claims (backlog F105): `"secondary"`, `"middle"` and `"other"` (every button past those), separated by spaces or commas — `"middle"`, `"secondary middle"`. Unset, all three: a node that wants the middle button and leaves the secondary one to its context menu says `"middle"`. A word that is none of the three is skipped, so a string of none of them claims nothing, and a typo never takes the secondary button from a context menu. Meaningless without `onButton`. |
|
|
23
|
-
| `caret` | `caret` | `caret` with `KUI_VALUE_CARET` in `value_set` | number, or a `"$length"` token | On a `line` of a custom editor (a `textInput` / `multilineTextInput` role drawn by the app): the caret's byte offset into that line's text. |
|
|
24
|
-
| `caretSolid` | `caret_solid` | `caret_solid` | boolean | On a `line` declaring `caret`: the caret is solid — a block caret in a modal editor's normal mode — so the driver's blink clock is not armed on it and `caretVisible` stays true, while the offset still anchors the IME and reads to assistive technology. Without it a declared `caret` is a caret to blink, and the one thing that asks an idle app for a frame twice a second; an editor whose caret only blinks while typing declares this on every other mode's line. |
|
|
25
|
-
| `center` | `center` | `main_align` + `cross_align` = `KUI_CENTER` | boolean | Center children on both axes. |
|
|
26
|
-
| `checked` | `checked` | `checked` | boolean | The on state of a `checkbox` / `radio` / `switch` role. |
|
|
27
|
-
| `clickSound` | `click_sound` | `click_sound` | resource handle | A registered sound (addSound) played when the node is clicked; implies hover tracking. |
|
|
28
|
-
| `crossAlign` | `cross_align` | `cross_align` | `start` \\| `center` \\| `end` \\| `spaceBetween` \\| `spaceAround` \\| `spaceEvenly` \\| `baseline` | Child alignment across the main axis. On a row, `baseline` lines up the first baselines of the children's text, so a label and a larger value read as one line; a child with no text aligns by its bottom edge, a `grow` or percent height fills the line from its top, and a fit-height row grows to hold the aligned children. A column lays `baseline` out as `start` (as CSS does), and the three spreads mean nothing across an axis — each with a warning. |
|
|
29
|
-
| `crossGap` | `cross_gap` | `cross_gap` | number, or a `"$length"` token | Space between wrap lines, across the main axis (`gap` stays the space along it). |
|
|
30
|
-
| `cursor` | `cursor` | `cursor` (`KUI_CURSOR_*`) | `default` \\| `text` \\| `pointer` \\| `grab` \\| `grabbing` \\| `notAllowed` \\| `ewResize` \\| `nsResize` \\| `nwseResize` \\| `neswResize` | The pointer shape over this node. Unset, the pointer is `text` over an editor or a `selectable` scope and `default` over everything else — an `onClick`, `focusable` or `onDrag` node included, as a native button is — so a hand (`pointer`) over a control, a `grab` over a handle (and `grabbing` while its drag runs, which the view declares as its drag state changes), a splitter's `ewResize` / `nsResize` and a `disabled` control's `notAllowed` are all declared. The stock `button` declares `pointer` itself. A captured drag keeps the dragged node's shape wherever the pointer goes. |
|
|
31
|
-
| `delay` | `delay` | `delay_ms` | number, or a `"$length"` token | Holds the `keyframes` cycle back by this many ms (CSS `animation-delay`); siblings with different delays run out of phase. |
|
|
32
|
-
| `description` | `description` | `description` | string | The accessible description: the extra sentence a reader says after the name, for what the name cannot say on its own — what a button will do, why a control is disabled, what format a field wants. `tooltip` is the shorthand that also draws the string and hover-tracks the node; this is the description alone, for a hint that is spoken and never drawn. Both write the one slot, so a node declaring both keeps whichever its binding applied last. It reads only on a node that reaches the access tree — a role, a label, a control — since a plain box is elided and takes its description with it. |
|
|
33
|
-
| `disabled` | `disabled` | `disabled` | boolean | Inert: no click, drag or key sink, no hover / pressed / focus background, skipped by Tab, reported disabled to assistive technology; hover tracking stays so a `tooltip` can say why. |
|
|
34
|
-
| `dropBg` | `drop_bg` | `drop_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while files dragged in from the OS are over this node (ADR 0031); wins over pressedBg, focusBg and hoverBg, clears when they leave, land or the drag is cancelled. Implies hover tracking, eases with `transition`. |
|
|
35
|
-
| `easing` | `easing` | `easing` (`KUI_EASE_*`) | `easeOut` \\| `linear` \\| `easeIn` \\| `easeInOut` \\| `spring` \\| `bouncy` \\| `smooth` \\| `snappy` | Easing for `transition` (default easeOut). The springs — `smooth` (no overshoot), `snappy`, `spring` and `bouncy` (the most), each a `bounce` of its own — integrate with momentum, so a value retargeted mid-flight keeps moving the way it was; `transition` is then about how long one takes to get there. |
|
|
36
|
-
| `enter` | `enter` | `enter` (`KuiEnter`, with `set` bits) | entrance (`{ dx?, dy?, width?, height?, bg?, radius?, opacity? }`) | Where the node starts the first frame it is seen `{ dx?, dy?, width?, height?, bg?, radius?, opacity? }`: those slots ease in from there over `transition` ms instead of snapping (`dx`/`dy` slide it in from that far away, `opacity: 0` fades the whole subtree in). |
|
|
37
|
-
| `exit` | `exit` | `exit` (`KuiEnter`, with `set` bits) | entrance (`{ dx?, dy?, width?, height?, bg?, radius?, opacity? }`) | Where the node ends the frame after the view stops declaring it `{ dx?, dy?, width?, height?, bg?, radius?, opacity? }` — an `enter` read the other way. It plays when the node itself is removed, its parent still declared; a node that goes because an ancestor went — a tab switched away, a panel closed around it — goes at once with it, unless that ancestor has an `exit` of its own, whose picture carries it (backlog DX19; React's `AnimatePresence` rule). With a `transition`, the departing subtree is copied out of the last frame that had it and replayed frozen, in its place (the pass it painted in, just under the node that painted after it — a panel under a HUD leaves under it) and inert (no clicks, no Tab stop, no access row) while those slots ease from where they were, then dropped; without one it vanishes at once as it always did. `width`/`height` resize the departing node's own box only — the subtree inside it is a picture and is not laid out again. Needs a stable key across frames. |
|
|
38
|
-
| `expanded` | `expanded` | `expanded` (`KUI_EXPANDED_*`) | `collapsed` \\| `expanded` | A disclosure's state: what a node that shows and hides something (a twisty, an accordion header, a menu button) reads as. Unset, the node does not expand at all — which is why this names its state instead of being a flag. |
|
|
39
|
-
| `focusBg` | `focus_bg` | `focus_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while the node holds keyboard-visible focus (moved there by Tab or assistive technology, not a click); replaces the default focus ring. Pressed wins over focus wins over hover; eases with `transition`. |
|
|
40
|
-
| `focusRegion` | `focus_region` | `focus_region` | boolean | Makes this node's subtree a focus region: a Tab ring of its own that the ring outside never enters and that never leaves — a devtools dock, an inspector beside the app (`docs/adr/0022-focus-regions.md`). Entered on purpose: `focusRegion(name)` (`Ui::focus_region`, `env.focus_region`, `kui_focus_region`) moves focus in — to the focus the region last held, else its `initialFocus`, else its first stop — and `focusRegion(null)` moves it back to the main ring the same way; a press inside the region, or an explicit focus on a node in it, enters it too. Tab then walks that ring alone, wrapping inside it; with nothing focused, Tab enters the ring of the region in effect (`region()`). A region that stops being declared hands focus back to what the main ring last held. Only the ring is scoped: keys still bubble through the boundary to the sink above (a region that wants its own keymap is an `onKey` sink), the pointer and assistive technology see a plain node, and a `modal` in effect is the ring wherever it sits. Nested regions are skipped by the outer ring the way the main ring skips them. |
|
|
41
|
-
| `focusable` | `focusable` | `focusable` | boolean | Reachable by Tab (and focused by a click) without a click payload or a control role — a row that opens on Enter. Editors, key sinks, `onClick` boxes and the control roles are focusable already. |
|
|
42
|
-
| `gap` | `gap` | `gap` | number, or a `"$length"` token | Space between children along the main axis. |
|
|
43
|
-
| `gradient` | `gradient` | `gradient` (`const KuiGradient *`) | gradient (`{ to? \\| angle? \\| radial?, at?, stops: [color \\| [color, at], …] }`) | A gradient painted over the node's `bg` and under its border and its children (`docs/adr/0042-a-gradient-is-an-image-the-core-paints.md`): `{ to: 'bottom', stops: [...] }` towards a side or a corner (`right`, `bottom left`, …; the default is `bottom`), `{ angle: 0.125, stops }` in turns clockwise from east, or `{ radial: true, at: [0.5, 0], stops }` out from a centre (fractions of the box, the middle by default) to its farthest corner. A stop is a colour — a `$token` too — or `[colour, position]` with the position 0 to 1; stops without one are spaced evenly between those with. Two stops at one position are a hard edge. The gradient is defined on the box's unit square and stretched to it, so a side or a corner is CSS's and any other `angle` runs corner to corner at an eighth of a turn whatever the box's aspect, where CSS's pixel-measured `45deg` does not. Stops mix in straight sRGB with the alpha premultiplied, as CSS's do. What it costs is one image quad: the core rasterizes each distinct gradient once into the glyph atlas — a 256-texel strip along an axis, a 128-texel square otherwise, within half an 8-bit level of the gradient computed per pixel for a linear one and 1.2 for a radial — keyed by the gradient and not the box, so a box that resizes and a thousand boxes that share one rasterize nothing, and a host that draws an image draws it; a gradient box costs about 55 ns over a flat one, so ten thousand of them are half a millisecond. A hard edge is as soft as the raster stretched to the box (a 256th of its length along a strip); stripes are boxes. It does not tween — `transition` eases the `bg` under it and `opacity` fades it — and `hoverBg` and the other state backgrounds replace `bg`, not the gradient; one that changes every frame is a raster a frame, and a shimmer is a `fragment`'s. Ignored on a `line`, a `polygon` and a `path`. Fewer than two stops are an error in JSX and Lua, as a malformed `keyframes` is, and draw nothing in Rust and C. A stop whose `$token` misses is not an error: it is raised as `unknown-token` and left out, as a miss leaves any slot unset, and the rest are spaced as if it had not been declared — so a gradient left with fewer than two stops, a two-stop one with a typo, draws nothing over its `bg` (backlog RG118). |
|
|
44
|
-
| `height` | `height` | `height` (KuiSizing) | sizing (`number` \\| `"fit"` \\| `"grow"` \\| `"N%"` \\| a size expression \\| `"$length"`) | Vertical size: px \| "fit" \| "grow" \| "N%" \| a size expression (see `width`). |
|
|
45
|
-
| `hoverBg` | `hover_bg` | `hover_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while hovered (or while any node in its hoverGroup is); implies hover tracking, eases with `transition`. |
|
|
46
|
-
| `hoverGroup` | `hover_group` | `hover_group` (KuiStr) | string | Nodes sharing a group name show hoverBg/pressedBg together (a split button, a multi-piece shape). |
|
|
47
|
-
| `hoverSound` | `hover_sound` | `hover_sound` | resource handle | A registered sound (addSound) played when the pointer enters the node; implies hover tracking. |
|
|
48
|
-
| `hoverable` | `hoverable` | `hoverable` | boolean | Hover-track without a click payload (for isHovered-driven styling). |
|
|
49
|
-
| `initialFocus` | `initial_focus` | `initial_focus` | boolean | Where focus lands when the enclosing `modal` scope is entered: the first node in the modal's Tab ring declaring it, so a destructive confirm opens on its Cancel rather than on whichever control is declared first. Read on entry only — a Tab press afterwards stands, and the scope re-entered (a nested confirm closing) leaves focus where it was. Declared on nothing, or only on nodes the ring skips (disabled, `role="none"`, not focusable), entry stays the ring's first node. |
|
|
50
|
-
| `keepFocus` | `keep_focus` | `keep_focus` | boolean | A press on this node, or anywhere inside it, leaves keyboard focus where it was: a toolbar button, a tab or a divider that acts without taking the keyboard from the editor or key sink that had it. Without it a press on an `onClick` node focuses the node, and the app's keys stop reaching the sink until it takes focus back. The press also leaves a text or cell selection and the Tab ring where they were, so a Copy button copies what was selected. An `<edit>` inside still takes its caret and focus, as the keyboard's own owner. The click, drag and hover are unchanged, and Tab and assistive technology still reach the node. |
|
|
51
|
-
| `keyUp` | `key_up` | `key_up` | boolean | With `onKey`: releases arrive too, as the same payload with phase:"up" (`text` null, `repeat` false) — for a held-key interaction (WASD, press-and-hold, a key that arms a mode while it is down). A key only comes up where it went down: a release whose press the sink never got is dropped, and focus leaving while a key is held delivers the `up` first, so nothing is left stuck down. Without it a sink hears presses only, which is what a keymap wants — one that heard both halves would run every binding twice. |
|
|
52
|
-
| `keyframes` | `keyframes` | `keyframes` + `keyframes_len` (`KuiKeyframe[]`) | keyframe list (`[{ at?, width?, height?, bg?, radius?, opacity? }, …]`) | CSS-style stops `[{ at?, width?, height?, bg?, radius?, opacity? }, …]`: the slots they name cycle through them over `transition` ms, forever, without the view redrawing; `at` is 0..1 and spreads evenly when omitted. |
|
|
53
|
-
| `label` | `label` | `label` (KuiStr) | string | The accessible name. Without one a button, link, tab or heading is named by the text inside it; an image, an icon-only button and a `modal` dialog have none, and the core warns (`image-without-label`, `control-without-name`, `modal-without-name`). |
|
|
54
|
-
| `live` | `live` | `live` (`KUI_LIVE_*`) | `off` \\| `polite` \\| `assertive` | Marks this node a live region: when the text inside it changes, a screen reader reads the change without being asked — `polite` at the next pause, `assertive` interrupting. Put it on the smallest node that holds the message, since everything inside a live node is live. For a one-off with no node behind it ("Saved") the binding's `announce` verb is the other half. |
|
|
55
|
-
| `mainAlign` | `main_align` | `main_align` | `start` \\| `center` \\| `end` \\| `spaceBetween` \\| `spaceAround` \\| `spaceEvenly` \\| `baseline` | Child alignment along the main axis. `start`, `center` and `end` put the children together; `spaceBetween` deals the free space out between them (none at the ends), `spaceAround` gives each child an equal share split to its two sides, and `spaceEvenly` makes every gap and both ends equal — CSS's `justify-content`. The spread is added to `gap`, and there is none when nothing is free: a `grow` child takes it all, and an overflowing run keeps its gaps. `baseline` means nothing here and lays out as `start`, with a warning. |
|
|
56
|
-
| `maxHeight` | `max_height` | `max_height` | maximum (`number` \\| a size expression \\| `"$length"`) | Upper height clamp: logical px or a size expression (see `width`). |
|
|
57
|
-
| `maxWidth` | `max_width` | `max_width` | maximum (`number` \\| a size expression \\| `"$length"`) | Upper width clamp: logical px or a size expression (see `width`); grow+maxWidth is the responsive-width pattern. |
|
|
58
|
-
| `minHeight` | `min_height` | `min_height` | minimum (`number` \\| `"fit"` \\| a size expression \\| `"$length"`) | Lower height clamp: logical px, a size expression, or "fit" for the node's own fit height. Undeclared, it is the node's content where its column overflows — CSS's `min-height: auto`, none for a node that scrolls or clips — so a row keeps the height of its text; `0` asks for the squeeze back (see `minWidth`). |
|
|
59
|
-
| `minWidth` | `min_width` | `min_width` | minimum (`number` \\| `"fit"` \\| a size expression \\| `"$length"`) | Lower width clamp: logical px, a size expression (see `width`; a percentage clamp is none until the parent's width is known, as in CSS), or "fit" for the node's own fit width. "fit" under `width="grow"` is a content floor — CSS's `flex: 1 0 auto` — which is what an i3-style tab bar is: tabs that split the bar evenly while they fit and sit at their label's width, scrolling, once they do not. Opt-in, because a fit width is the unwrapped one: a paragraph in a grow column would stop wrapping under it. Left out, a child giving in an overflowing row that holds a percentage or a size expression stops at its content, CSS's `min-width: auto`; `0` lets it go below, CSS's `min-width: 0` (backlog RG92). A fit node across a column is no wider than the column's box — CSS's `fit-content` — down to this floor, 0 left out, unless the column scrolls x, so a text one wrapper deep in a capped card wraps there; "fit" keeps its content's width and runs past (backlog F116). |
|
|
60
|
-
| `mixed` | `mixed` | `mixed` | boolean | A `checkbox` that is neither on nor off — the select-all box over a list some of whose rows are selected (ADR 0034). Read as mixed by assistive technology whatever `checked` says, and drawn as a dash by the stock `<checkbox>`. Meaningful on the checkbox role alone. |
|
|
61
|
-
| `modal` | `modal` | `modal` (a borrowed `KuiValue*`, cloned while the node opens) | tag (a message merged into the event under `tag`, or `null` for none) | Modal surface: the Tab ring becomes this node's subtree, everything outside it is inert to the pointer, the wheel and assistive technology, and Escape or a press outside emits {kind:"dismiss", reason:"escape"\|"outside", tag} on it — the app stops declaring the node. The last one declared in tree order is the one in effect (a confirm inside a dialog); a modal that must cover the app is a float. The access tree is not pruned to the modal: it keeps every node of the frame and marks the one in effect `modal` (`docs/adr/0003-modal-surfaces.md`, decision 7), which is what assistive technology acts on. |
|
|
62
|
-
| `modifierKeys` | `modifier_keys` | `modifier_keys` | boolean | With `onKey`: the modifier and lock keys arrive as keys of their own (backlog F108) — `code` "shift", "ctrl", "alt", "super", "capslock", "numlock", "scrolllock", which side in `location` ("left" / "right"), releases too with `keyUp`. Without it a modifier is only ever held — the next key's `shift`, `ctrl`, … and the `modifiers` event — so a keymap mid-sequence never reads a Shift as a key between two others. For a terminal speaking kitty's keyboard protocol, or a game that binds a lone Shift. |
|
|
63
|
-
| `onButton` | `on_button` | `on_button` (a borrowed `KuiValue*`, cloned while the node opens) | tag (a message merged into the event under `tag`, or `null` for none) | Button tag (backlog F105): a press of a non-primary button — middle, secondary, or one past those — emits {kind:"button", phase:"press", button, x, y, clicks, tag} on the node, and the button is then captured by it: every pointer move while it is held arrives as phase:"move" and its release as phase:"release", on this node wherever the pointer is. `button` is `"secondary"`, `"middle"` or a further button's number (3 and up); `x`/`y` are logical viewport coordinates, and on a `cells` grid each event carries `cell: {row, col}` as a click does. Several buttons can be held at once, each its own capture, and a primary drag is untouched. `buttons` says which buttons it claims — all of them unless it narrows them. Asked of the topmost node under the pointer, and when that node claims no such button the press reaches the nearest enclosing node that does, the way a context menu's does: a disabled node's own is skipped and the walk stops at the modal boundary. A claimed secondary press is this event *instead of* a `contextmenu` event and the stock menu (a nearer `onContextMenu` still wins, being the nested declaration). Like every non-primary press it moves no focus, places no caret and touches no selection or scrollbar. For a terminal's middle-click paste, and the mouse reports a program in it asked for. |
|
|
64
|
-
| `onChange` | `on_change` | `on_change` | tag (a message merged into the event under `tag`, or `null` for none) | A `slider` role's changes (ADR 0034): the core turns a press on the node into the value under the pointer, a drag into the value under it, the arrows and assistive technology's increment / decrement into one `valueStep`, PageUp / PageDown into ten, Home / End into the range's ends — clamped to `valueMin`..`valueMax` (0..100 unset) and snapped to the step — and emits `{kind:"change", value, phase, tag}`: `phase` is `"move"` while the pointer holds the slider and `"end"` when it lets go or a key moved it. The value is proposed and never applied; the slider moves when the view declares it as `valueNow`. A key that lands where the slider already is proposes nothing. The pointer reads the node's content box along its main axis, so a `dir="column"` slider runs bottom to top. Without it a slider's arrows reach the app as `{kind:"access", action}`. Ignored on any other role. |
|
|
65
|
-
| `onClick` | `on_click` | `on_click` argument of `kui_open` / `kui_open_with` | message (any plain data) | Message emitted when clicked (data, not a callback). |
|
|
66
|
-
| `onContextMenu` | `on_context_menu` | `on_context_menu` (a borrowed `KuiValue*`, cloned while the node opens) | tag (a message merged into the event under `tag`, or `null` for none) | Context-menu tag: a secondary-button (right) press emits {kind:"contextmenu", x, y, tag} on the node, at the logical viewport point to open the menu at. The press moves no focus, places no caret and produces no click, so right-clicking a selection keeps it. Asked of the topmost node under the pointer, and when that node offers no menu the press reaches the nearest enclosing node that does — a container declaring a menu for everything inside it is the common case — the way an unclaimed key reaches the enclosing sink (`docs/adr/0011`): the event carries the *owner's* key and tag, a nested declaration wins over its ancestor's, a disabled node's own is skipped, and the walk stops at the modal boundary. |
|
|
67
|
-
| `onDrag` | `on_drag` | `on_drag` argument of `kui_open_draggable` / `kui_open_with` | tag (a message merged into the event under `tag`, or `null` for none) | Drag tag: emits {kind:"drag", phase, x, y, dx, dy, parent, tag} events, `dx`/`dy` measured from the press point in every phase. On a `cells` grid the events also carry `cell: {row, col}`; inside an `onKey` sink that draws `role="line"` rows they carry `line`, `byte` and `clicks` — see the events table. |
|
|
68
|
-
| `onDrop` | `on_drop` | `on_drop` (a borrowed `KuiValue*`, cloned while the node opens) | tag (a message merged into the event under `tag`, or `null` for none) | Drop-zone tag (`docs/adr/0031-a-drop-zone-is-a-row-and-the-files-are-an-event.md`): files dragged in from the OS over this node emit {kind:"drop", phase:"enter"\|"move"\|"leave"\|"drop", paths, x, y, tag} — `paths` the OS paths as strings, `x`/`y` the pointer in logical viewport coordinates (absent on `leave`). The zone under the files is the topmost zone by paint order: a node inside a zone is the zone's (a button in it, a field in it), and a node that is no zone and has none enclosing it is looked past, so an overlay shown on `enter` cannot make the zone lose the files. No `leave` follows a `drop`; a drop off every zone is refused by the driver. Implies hover tracking. No access row — a screen-reader user's way in is a button beside the zone. On Windows and Linux the position is the OS cursor at enter and release only, so `move` never fires there. |
|
|
69
|
-
| `onFocus` | `on_focus` | `on_focus` (a borrowed `KuiValue*`, cloned while the node opens) | tag (a message merged into the event under `tag`, or `null` for none) | Keyboard focus entering or leaving this node's subtree — the node itself, or anything focused inside it — emits `{kind:"focus", phase:"in"\|"out", by, tag}` (backlog DX18). `by` is what moved it: `pointer` (a press), `keyboard` (Tab, a key a control answered), `assistive` (a screen reader's request) or `program` (the view or the app — `keyFocus`, `setFocus`, a modal's entry). Reported once the move settles, after the input that made it or at the end of the frame that declared it, so an app hears a pane taking the keyboard instead of diffing the focused key every frame. Leaving is reported innermost first, entering outermost first. It makes nothing focusable or interactive. |
|
|
70
|
-
| `onForceClick` | `on_force_click` | `on_force_click` | tag (a message merged into the event under `tag`, or `null` for none) | Force-click tag: a press that deepens past the second stage of a Force Touch trackpad emits {kind:"forceclick", x, y, tag} on the node, at the logical viewport point it happened at (`docs/adr/0017-selection-as-a-scope.md`). Routed as a secondary press is — no focus moved, no caret placed, no click — but asked of the topmost node only, with no walk to an enclosing declaration — and the ordinary click the press is still producing arrives afterwards, as it does on macOS. Text needs none of this: a force click over an `edit` or a `selectable` scope selects the word under it and asks the host for its Look Up panel. macOS-only in practice, and there the user can switch the gesture off, so nothing may declare itself the only way to reach something. |
|
|
71
|
-
| `onHover` | `on_hover` | `on_hover` argument of `kui_open_with` | tag (a message merged into the event under `tag`, or `null` for none) | Hover tag: the pointer entering/leaving emits {kind:"hover", phase:"enter"\|"leave", tag} events. |
|
|
72
|
-
| `onKey` | `on_key` | `on_key` argument of `kui_open_with` | tag (a message merged into the event under `tag`, or `null` for none) | Key-sink tag: with key focus held, presses arrive as {kind:"key", phase:"down", code, ...} events. Releases only with `keyUp` beside it. |
|
|
73
|
-
| `onLayout` | `on_layout` | `on_layout` (a borrowed `KuiValue*`, cloned while the node opens) | tag (a message merged into the event under `tag`, or `null` for none) | Layout tag: the node's laid-out rect arrives as {kind:"layout", x, y, w, h, parent, tag} on its first frame and whenever it changes (needs a stable key). |
|
|
74
|
-
| `onScroll` | `on_scroll` | `on_scroll` (a borrowed `KuiValue*`, cloned while the node opens) | tag (a message merged into the event under `tag`, or `null` for none) | Scroll tag: the wheel over this node emits {kind:"scroll", x, y, dx, dy, lines, tag} on it instead of scrolling anything — `dx`/`dy` the delta in logical px as the driver reported it (positive `dy` is the wheel rolling up, toward earlier content), `x`/`y` the pointer, and `lines` on a `cells` grid the whole lines the delta covers (positive = later history, the sign `originLine` grows in; the fraction is carried to the next notch so a trackpad's small steps add up) and null on any other node. The node takes the wheel on the axes `scrollAxes` names (both unless it narrows them): a gesture that starts over it is its own whether or not it has anywhere to go — except on an axis the node also scrolls (`scrollX`/`scrollY`, its offset the app's to set), where it is answered by its room as a container is, so at its edge a gesture that way passes to the scroller around it (backlog F118) — and stays its own until it ends, wherever the pointer goes (backlog F107); it reaches no scroll container above it, and a scroller inside it still takes the axes it scrolls while it can move that way, passing this node the rest — the other axis, and a gesture that begins with that scroller at its limit (`overscroll: "contain"` on the scroller keeps it there). The core moves nothing — a grid re-declares `originLine`, a canvas zooms. A drag-select held past a `cells` grid's top or bottom edge arrives here too, once a frame with the lines that frame scrolled by (`docs/adr/0029-a-selection-follows-the-pointer-past-the-edge.md`). |
|
|
75
|
-
| `opacity` | `opacity` | `opacity` with `opacity_set` | number, or a `"$length"` token | Group opacity 0..1 (default 1): fades this node and its whole subtree. A per-quad alpha multiply rather than an offscreen composite, so overlapping pieces of one subtree show their seams through the fade. Layout, hit-testing and the access tree are untouched; eases with `transition`, and `enter: { opacity: 0 }` fades a panel in. |
|
|
76
|
-
| `overscroll` | `overscroll` | `overscroll` (`KUI_OVERSCROLL_*`; zeroed, auto) | `auto` \\| `contain` | What a scroll gesture that starts over this scroller does when it is already at its limit that way (backlog F107, CSS's `overscroll-behavior`): `auto` (the default) passes the gesture on to the scroller around it, `contain` keeps it here, moving nothing until it turns back. A gesture picks its target once, when it starts — the innermost scroller under the pointer that can still move the way it goes — and keeps it until it ends, wherever the pointer or the content has gone; one that reaches a limit midway stops there, whatever this says. Only on the axes the node scrolls: a `scrollY` list that contains still passes a sideways swipe to the strip around it. For a panel or a popup's list whose scrolling must never move what is behind it. |
|
|
77
|
-
| `pixelSnap` | `pixel_snap` | `pixel_snap` | boolean | Paint this node's background, border, shadow and fragment with each edge on a whole physical pixel: `x` and `x + width` rounded on their own, from where layout put them, as a text's span backgrounds are. Off by default, and a box is drawn where layout put it, so a 1 px `gap` between boxes is there at any scale. On, boxes that share an edge in layout meet on one pixel line, where a join inside a pixel was drawn by halves and left a seam — rows of a band stacked at a pitch that is not whole pixels, or a box that continues a text's selection. Layout, hit-testing, the clip and the children are untouched. A snapped box can draw up to half a pixel from its layout edge and its size can differ by a pixel, so a snapped hairline is 1 or 2 px thick by where it sits. |
|
|
78
|
-
| `pressedBg` | `pressed_bg` | `pressed_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while pressed (or while its hoverGroup is); implies hover tracking. |
|
|
79
|
-
| `radius` | `radius` | `radius` | number, or a `"$length"` token | Corner radius for all four corners (logical px); the per-corner props override it when listed after it. On a node that also clips or scrolls it rounds the clip as well, so children stay inside the corners. |
|
|
80
|
-
| `radiusBL` | `radius_bl` | `radius_bl` with `per_corner` | number, or a `"$length"` token | Bottom-left corner radius (logical px). |
|
|
81
|
-
| `radiusBR` | `radius_br` | `radius_br` with `per_corner` | number, or a `"$length"` token | Bottom-right corner radius (logical px). |
|
|
82
|
-
| `radiusTL` | `radius_tl` | `radius_tl` with `per_corner` | number, or a `"$length"` token | Top-left corner radius (logical px). |
|
|
83
|
-
| `radiusTR` | `radius_tr` | `radius_tr` with `per_corner` | number, or a `"$length"` token | Top-right corner radius (logical px). |
|
|
84
|
-
| `repeat` | `repeat` | `repeat` (`KUI_REPEAT_*`) | `normal` \\| `reverse` \\| `alternate` \\| `alternateReverse` | How `keyframes` cycle (CSS `animation-direction`, default normal). Lua: `direction`, since `repeat` is a keyword. |
|
|
85
|
-
| `role` | `role` | `role` (`KUI_ROLE_*`) | `none` \\| `button` \\| `checkbox` \\| `radio` \\| `switch` \\| `slider` \\| `tab` \\| `tabList` \\| `link` \\| `heading` \\| `list` \\| `listItem` \\| `image` \\| `dialog` \\| `group` \\| `textInput` \\| `multilineTextInput` \\| `line` \\| `radioGroup` \\| `menu` \\| `menuItem` \\| `terminal` | What the node is to assistive technology. Unset, the core derives one (an `onClick` node is a button, an editor a text input, a scrolling box a scroll view, a plain box nothing); `none` hides the node and its subtree from the access tree. A `radio` belongs inside a `radioGroup` and a `tab` inside a `tabList`, labelled with what the choice is: the pair is a composite (`docs/adr/0007-composite-keyboard-patterns.md`) — one Tab stop for the set, the arrows, Home and End moving the choice inside it (each step is the item's click, so the choice follows focus), and a screen reader reading "2 of 3". A `radio` or `tab` with no container above it is a Tab stop of its own that no arrow moves, and the core warns (`item-outside-container`). `menu` holds `menuItem`s and `list` holds `listItem`s the same way. |
|
|
86
|
-
| `ruleWidth` | `rule_width` | `rule_w` | number, or a `"$length"` token | The width of a table's `rules` in logical px; 1 when unset. |
|
|
87
|
-
| `rules` | `rules` | `rules` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | On a table (`dir="table"`, ADR 0033): grid lines of this colour between its columns and between its rows (backlog DX21) — down the middle of each gap between the columns of its widest row, from the first row's top to the last row's bottom, and across the middle of each gap between rows, the content box wide. Drawn with the table's box, under its cells and on whole pixels, so give the table and its rows a `gap` at least `ruleWidth` for the lines to show between cells; the outer edge is the table's `border`. Ignored on anything but a table. |
|
|
88
|
-
| `scrollAxes` | `scroll_axes` | `scroll_axes` (`KUI_SCROLL_AXES_*`; zeroed, both) | `both` \\| `x` \\| `y` | Which axes `onScroll` takes (backlog F107): `both` (the default), `x` or `y`. A scroll gesture on an axis the node does not take passes it by, to the scroller around it, and hears nothing here: a terminal that scrolls its history says `y`, and a sideways swipe that starts over it moves the strip it sits in. (A swipe that started elsewhere is not the node's either way: a gesture keeps the target it started with.) Meaningless without `onScroll`. |
|
|
89
|
-
| `scrollMods` | `scroll_mods` | `scroll_mods` (`KUI_KMOD_*` bits; zeroed, none) | string | The modifiers `onScroll` is for (backlog F122): `"shift"`, `"ctrl"`, `"alt"` and `"super"` (⌘, the Windows key), separated by spaces or commas — `"ctrl super"`. With any named, the node hears only a scroll gesture that began with one of them held, and hears it first: ahead of every scroll container and every `onScroll` that names none, wherever under the pointer the gesture began, the innermost such node winning — so a Ctrl-wheel zoom declared on the window's root is heard over a list, and the list does not scroll. A wheel with none of them held passes the node by, as if it had no `onScroll`: a node that scrolls as well (`overflow`) scrolls for it as any container does. Its `scroll` events carry `mods`, the modifiers held when the gesture began; the gesture stays the node's to the end of its glide, whatever is let go meanwhile, and one begun without them never becomes its. A word that is none of the four is skipped. Unset, a handler like any other. Meaningless without `onScroll`. |
|
|
90
|
-
| `scrollbar` | `scrollbar` | `scrollbar` | `visible` \\| `hidden` \\| `auto` | When a scrolling node draws its bars: `visible` (the default — the stock overlay thumb, drawn while the content overflows), `hidden` (no thumb, no track to press; the wheel, the keyboard, `reveal` and the caret still scroll it — for a list that draws its own indicator, or a pane whose bar would sit on a border), or `auto` (shown while the scroll state is changing — the offset or the content's extent moved, the pointer is on the track, a thumb is dragged — and for a second after, then faded out over a quarter of one; a node first seen shows it the same second; what an overlay bar does on macOS). `auto` needs the driver's clock and is `visible` without one. The bars are overlays and take no layout space in any mode. From the last change until it has faded — a second and a quarter — an `auto` bar asks for frames the way a transition of that length would (nothing else could wake the core when the hold ends); while the pointer holds it, it asks for none. |
|
|
91
|
-
| `scrollbarActiveColor` | `scrollbar_active_color` | `scrollbar_active_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | The thumb under the pointer or while dragged; the default is the theme's `scrollbar_active` role. |
|
|
92
|
-
| `scrollbarColor` | `scrollbar_color` | `scrollbar_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | The thumb at rest; the default is the theme's `scrollbar` role, a translucent wash over whatever it sits on. |
|
|
93
|
-
| `scrollbarWidth` | `scrollbar_width` | `scrollbar_width` | number, or a `"$length"` token | The thumb's width at rest, logical px (default 4); under the pointer or dragged it is 2 px wider. The grabbable track grows to fit a wide thumb. |
|
|
94
|
-
| `selectable` | `selectable` | `selectable` | boolean | Makes this node a selection scope: the text of every node inside it is one selectable run, in tree order, and a press-drag across them selects the lot — as do Shift with Left / Right / Home / End on a focused node inside it, a character or a word at a time, a scope with nothing selected anchoring at its start (`docs/adr/0017-selection-as-a-scope.md`). Declared on the container and not on each label, because what a reader selects is a paragraph or a card rather than one run of it — three labels in a column under one `selectable` select as three lines of one text. The selection is the window's: starting one anywhere clears the last, an editor's included. Scopes do not nest; an outer one around an inner one is warned about (`nested-selection-scope`) and the innermost owns the text. Text scrolled out of view inside the scope is still part of it — selection and copy reach it, hit-testing does not. On a `cells` grid the scope selects in cells rather than in bytes: a drag takes lines (with a modifier, a rectangle), a double click the word under the pointer and a triple click the whole row, its ends are absolute lines so a scroll does not move them, and a copy trims each line's trailing blanks. |
|
|
95
|
-
| `selected` | `selected` | `selected` | boolean | The current one of a set: which `tab` a `tabList` shows, which `listItem` a list has picked, which `link` is the page you are on. A `tab` always carries the state — its siblings read as "not selected" — while a list row or a link carries it only where it is set, since an ordinary list or navigation bar is not a selection and a reader saying "not selected" on every row of it is noise. |
|
|
96
|
-
| `selectionAnchor` | `selection_anchor` | `selection_anchor` with `KUI_VALUE_ANCHOR` in `value_set` | number, or a `"$length"` token | On a `line` of a custom editor: the byte offset where the selection's other end sits (the caret is `caret`, possibly on another line). |
|
|
97
|
-
| `shadowBlur` | `shadow_blur` | `shadow_blur` | number, or a `"$length"` token | Drop-shadow blur radius (logical px): the edge ramps over this distance and reaches this far past the shape. 0 = a hard edge. |
|
|
98
|
-
| `shadowColor` | `shadow_color` | `shadow_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Drop-shadow color; nothing else about a shadow draws without it. On its own it is a hard shadow exactly behind the node — add `shadowBlur` / `shadowY` to lift it. Outer shadows only, and the shape is not knocked out of the middle, so a translucent background shows it through. |
|
|
99
|
-
| `shadowSpread` | `shadow_spread` | `shadow_spread` | number, or a `"$length"` token | Grows (or, negative, shrinks) the drop shadow's shape before blurring (logical px). |
|
|
100
|
-
| `shadowX` | `shadow_x` | `shadow_x` | number, or a `"$length"` token | Drop-shadow horizontal offset (logical px). |
|
|
101
|
-
| `shadowY` | `shadow_y` | `shadow_y` | number, or a `"$length"` token | Drop-shadow vertical offset (logical px); positive casts downward. |
|
|
102
|
-
| `slide` | `slide` | `slide` | boolean | With transition: also ease the node's position (reordered siblings slide). While it eases, the node is drawn between where it was and where this frame put it — not at the declared `dx`/`dy`, or its slot in the row — so anything else positioned from those numbers drifts for the transition's length: a canvas of floats eases everything or nothing. |
|
|
103
|
-
| `transition` | `transition` | `transition_ms` | number, or a `"$length"` token | Animate sizing/colors/radius changes over this many ms — and, on a scroll container, the offset a reveal or a set_scroll moves it to (needs a stable key). |
|
|
104
|
-
| `valueMax` | `value_max` | `value_max` with `KUI_VALUE_MAX` in `value_set` | number, or a `"$length"` token | A `slider` role's maximum. |
|
|
105
|
-
| `valueMin` | `value_min` | `value_min` with `KUI_VALUE_MIN` in `value_set` | number, or a `"$length"` token | A `slider` role's minimum. |
|
|
106
|
-
| `valueNow` | `value_now` | `value_now` with `KUI_VALUE_NOW` in `value_set` | number, or a `"$length"` token | A `slider` role's current value (the drawing stays yours; this is what assistive technology reads). |
|
|
107
|
-
| `valueStep` | `value_step` | `value_step` | number, or a `"$length"` token | How far one arrow key moves a `slider` role, and the grid a value the pointer sets snaps to (ADR 0034). Unset, a hundredth of the range. PageUp / PageDown move ten steps. Read by the core only where the slider declares `onChange`; reported to assistive technology either way. |
|
|
108
|
-
| `valueText` | `value_text` | `value_text` (KuiStr) | string | What a `slider` role's position reads as (ARIA's `aria-valuetext`). Without one a reader has only `valueNow` and the range and says a percentage — 25 in [5..60] is "36 percent" — so a value whose unit carries the meaning says it here: "25 minutes". It replaces the number in the reading rather than joining it, and a nudge announces the new text. Meaningful on the slider role alone, like the three numbers; putting the reading in `label` instead renames the control on every nudge, which is the wrong attribute. |
|
|
109
|
-
| `width` | `width` | `width` (KuiSizing) | sizing (`number` \\| `"fit"` \\| `"grow"` \\| `"N%"` \\| a size expression \\| `"$length"`) | Horizontal size: px \| "fit" \| "grow" \| "N%" \| a size expression — `"clamp(400px, 80%, 1000px)"`, `"min(720px, 100%)"`, `"max(50%, 300)"`, nested — which layout resolves against the parent's content box, the box a percentage takes its cut of (backlog F109). An expression with no percentage in it is a length; a calc does not ease under `transition`. A percentage or an expression gives, with the fit children, when its parent overflows — two `"50%"` children and a gap fit their row (backlog F110) — where a px size keeps its own. A row holding one gives as CSS's flex items do: every child that can give gives in proportion to its size, and stops at its content — the widest thing in it that cannot wrap, a label's longest word — unless `minWidth` says otherwise (backlog RG92). The process keeps 65 536 distinct expressions and never lets one go: past that a new one leaves its prop at its default, with a `size-expressions-full` warning, so declare one per layout — a px size for the part that moves each frame, a splitter's drag — not one per frame (backlog RG93). |
|
|
110
|
-
| `window` | `window` | `window_role` (`KUI_WINDOW_*`) | `drag` \\| `close` \\| `minimize` \\| `maximize` | Window-chrome role: interactions become window commands, not events. |
|
|
111
|
-
| `wrapChildren` | `wrap_children` | `wrap_children` | boolean | Children that don't fit the main axis start a new line instead of overflowing or shrinking. Rows only (a column is ignored, with a warning), and never on a scrollX row. |
|
|
15
|
+
| JSX | Lua | C | Odin | type | description |
|
|
16
|
+
|---|---|---|---|---|---|
|
|
17
|
+
| `accent` | `accent` | `accent` | `Spec.accent` | boolean | Paint this node's background in the OS accent colour — `env.system.accent` — keeping the declared `bg` on a host that cannot tell what it is. The one prop whose paint depends on the environment, which is why it is opt-in: the same tree is a different colour on two machines, and that is the point here and a surprise anywhere else. On the stock button it does the whole job — the hover and pressed shades are derived from the accent, and the label goes black or white by its luminance, so a yellow accent is still readable — which is what `<button accent>` is for. |
|
|
18
|
+
| `anchor` | `anchor` | `anchor` | `Spec.anchor` | boolean | Scroll anchoring on a scrolling node (backlog C26, CSS's `overflow-anchor`): the first child in view keeps its place on screen when the content before it changes size — a chat that prepends history, a log that inserts rows above the viewport, a list whose row heights are corrected as they are measured — with no `setScroll` and no arithmetic in the view. The core remembers which child was first in view and where its edge was, and moves the offset by however far that edge moved in the next layout, before the offset is clamped; a wheel notch or a `setScroll` between the frames is kept and the correction added to it. The child is found by key, so give the rows stable keys (a `key` or an `index`); a child that is gone anchors nothing that frame. On the scroll axis that is the node's main axis only — `scrollY` on a column, `scrollX` on a row — and content appended *after* the anchor moves nothing, so a log that is tailing still asks for the end itself. |
|
|
19
|
+
| `animate` | `animate` | `animate` | `Spec.animate` | boolean | Ask for another frame after this one, every frame this node is declared. What a `fragment` that reads `time` needs, and what anything driving itself off the clock rather than off input needs. Opt-in like `exit`, and for the same reason: it takes the loop off input-driven and onto the display's cadence for as long as it is declared, so a still node must not carry it. One node asking is enough for the whole window. |
|
|
20
|
+
| `aspectRatio` | `aspect_ratio` | `aspect_ratio` | `Spec.aspect_ratio` | number, or a `"$length"` token | Width over height — `16/9`, `1` for a square — CSS's `aspect-ratio`. It sizes the axis left `fit`: a fit height is the final width over the ratio (so `width: grow` and a ratio is a box that keeps its shape as the window resizes), and a fit width under a fixed height is that height times it. With both axes declared, or a fit width under a `grow` or percent height, it has nothing it can set and warns. The derived axis is neither shrunk nor fitted to the children, which overflow it; `minHeight: 'fit'` floors it at them. On an image it wins over the pixels' own aspect. |
|
|
21
|
+
| `bg` | `bg` | `bg` | `Spec.bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background fill. |
|
|
22
|
+
| `bounce` | `bounce` | `bounce` | `Spec.bounce` | number, or a `"$length"` token | How far a spring overshoots its target: 0 glides in with none, 0.5 bounces visibly, and values past 0.9 are held there (a spring at 1 would never settle). It replaces a spring `easing`'s own bounce (`smooth` 0, `snappy` 0.15, `spring` 0.25, `bouncy` 0.5), and on a timed easing makes the transition a spring — so `transition` plus `bounce` is a spring of that length and bounce. |
|
|
23
|
+
| `buttons` | `buttons` | `buttons` (`KUI_BUTTONS_*` bits; zeroed, all three) | `Spec.buttons` | string | Which non-primary buttons `onButton` claims (backlog F105): `"secondary"`, `"middle"` and `"other"` (every button past those), separated by spaces or commas — `"middle"`, `"secondary middle"`. Unset, all three: a node that wants the middle button and leaves the secondary one to its context menu says `"middle"`. A word that is none of the three is skipped, so a string of none of them claims nothing, and a typo never takes the secondary button from a context menu. Meaningless without `onButton`. |
|
|
24
|
+
| `caret` | `caret` | `caret` with `KUI_VALUE_CARET` in `value_set` | `Spec.caret` | number, or a `"$length"` token | On a `line` of a custom editor (a `textInput` / `multilineTextInput` role drawn by the app): the caret's byte offset into that line's text. |
|
|
25
|
+
| `caretSolid` | `caret_solid` | `caret_solid` | `Spec.caret_solid` | boolean | On a `line` declaring `caret`: the caret is solid — a block caret in a modal editor's normal mode — so the driver's blink clock is not armed on it and `caretVisible` stays true, while the offset still anchors the IME and reads to assistive technology. Without it a declared `caret` is a caret to blink, and the one thing that asks an idle app for a frame twice a second; an editor whose caret only blinks while typing declares this on every other mode's line. |
|
|
26
|
+
| `center` | `center` | `main_align` + `cross_align` = `KUI_CENTER` | `Spec.center` | boolean | Center children on both axes. |
|
|
27
|
+
| `checked` | `checked` | `checked` | `Spec.checked` | boolean | The on state of a `checkbox` / `radio` / `switch` role. |
|
|
28
|
+
| `clickSound` | `click_sound` | `click_sound` | `Spec.click_sound` | resource handle | A registered sound (addSound) played when the node is clicked; implies hover tracking. |
|
|
29
|
+
| `crossAlign` | `cross_align` | `cross_align` | `Spec.cross_align` | `start` \\| `center` \\| `end` \\| `spaceBetween` \\| `spaceAround` \\| `spaceEvenly` \\| `baseline` | Child alignment across the main axis. On a row, `baseline` lines up the first baselines of the children's text, so a label and a larger value read as one line; a child with no text aligns by its bottom edge, a `grow` or percent height fills the line from its top, and a fit-height row grows to hold the aligned children. A column lays `baseline` out as `start` (as CSS does), and the three spreads mean nothing across an axis — each with a warning. |
|
|
30
|
+
| `crossGap` | `cross_gap` | `cross_gap` | `Spec.cross_gap` | number, or a `"$length"` token | Space between wrap lines, across the main axis (`gap` stays the space along it). |
|
|
31
|
+
| `cursor` | `cursor` | `cursor` (`KUI_CURSOR_*`) | `Spec.cursor` | `default` \\| `text` \\| `pointer` \\| `grab` \\| `grabbing` \\| `notAllowed` \\| `ewResize` \\| `nsResize` \\| `nwseResize` \\| `neswResize` | The pointer shape over this node. Unset, the pointer is `text` over an editor or a `selectable` scope and `default` over everything else — an `onClick`, `focusable` or `onDrag` node included, as a native button is — so a hand (`pointer`) over a control, a `grab` over a handle (and `grabbing` while its drag runs, which the view declares as its drag state changes), a splitter's `ewResize` / `nsResize` and a `disabled` control's `notAllowed` are all declared. The stock `button` declares `pointer` itself. A captured drag keeps the dragged node's shape wherever the pointer goes. |
|
|
32
|
+
| `delay` | `delay` | `delay_ms` | `Spec.delay` | number, or a `"$length"` token | Holds the `keyframes` cycle back by this many ms (CSS `animation-delay`); siblings with different delays run out of phase. |
|
|
33
|
+
| `description` | `description` | `description` | `Spec.description` | string | The accessible description: the extra sentence a reader says after the name, for what the name cannot say on its own — what a button will do, why a control is disabled, what format a field wants. `tooltip` is the shorthand that also draws the string and hover-tracks the node; this is the description alone, for a hint that is spoken and never drawn. Both write the one slot, so a node declaring both keeps whichever its binding applied last. It reads only on a node that reaches the access tree — a role, a label, a control — since a plain box is elided and takes its description with it. |
|
|
34
|
+
| `disabled` | `disabled` | `disabled` | `Spec.disabled` | boolean | Inert: no click, drag or key sink, no hover / pressed / focus background, skipped by Tab, reported disabled to assistive technology; hover tracking stays so a `tooltip` can say why. |
|
|
35
|
+
| `dropBg` | `drop_bg` | `drop_bg` | `Spec.drop_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while files dragged in from the OS are over this node (ADR 0031); wins over pressedBg, focusBg and hoverBg, clears when they leave, land or the drag is cancelled. Implies hover tracking, eases with `transition`. |
|
|
36
|
+
| `easing` | `easing` | `easing` (`KUI_EASE_*`) | `Spec.easing` | `easeOut` \\| `linear` \\| `easeIn` \\| `easeInOut` \\| `spring` \\| `bouncy` \\| `smooth` \\| `snappy` | Easing for `transition` (default easeOut). The springs — `smooth` (no overshoot), `snappy`, `spring` and `bouncy` (the most), each a `bounce` of its own — integrate with momentum, so a value retargeted mid-flight keeps moving the way it was; `transition` is then about how long one takes to get there. |
|
|
37
|
+
| `enter` | `enter` | `enter` (`KuiEnter`, with `set` bits) | `Spec.enter` | entrance (`{ dx?, dy?, width?, height?, bg?, radius?, opacity? }`) | Where the node starts the first frame it is seen `{ dx?, dy?, width?, height?, bg?, radius?, opacity? }`: those slots ease in from there over `transition` ms instead of snapping (`dx`/`dy` slide it in from that far away, `opacity: 0` fades the whole subtree in). |
|
|
38
|
+
| `exit` | `exit` | `exit` (`KuiEnter`, with `set` bits) | `Spec.exit` | entrance (`{ dx?, dy?, width?, height?, bg?, radius?, opacity? }`) | Where the node ends the frame after the view stops declaring it `{ dx?, dy?, width?, height?, bg?, radius?, opacity? }` — an `enter` read the other way. It plays when the node itself is removed, its parent still declared; a node that goes because an ancestor went — a tab switched away, a panel closed around it — goes at once with it, unless that ancestor has an `exit` of its own, whose picture carries it (backlog DX19; React's `AnimatePresence` rule). With a `transition`, the departing subtree is copied out of the last frame that had it and replayed frozen, in its place (the pass it painted in, just under the node that painted after it — a panel under a HUD leaves under it) and inert (no clicks, no Tab stop, no access row) while those slots ease from where they were, then dropped; without one it vanishes at once as it always did. `width`/`height` resize the departing node's own box only — the subtree inside it is a picture and is not laid out again. Needs a stable key across frames. |
|
|
39
|
+
| `expanded` | `expanded` | `expanded` (`KUI_EXPANDED_*`) | `Spec.expanded` | `collapsed` \\| `expanded` | A disclosure's state: what a node that shows and hides something (a twisty, an accordion header, a menu button) reads as. Unset, the node does not expand at all — which is why this names its state instead of being a flag. |
|
|
40
|
+
| `focusBg` | `focus_bg` | `focus_bg` | `Spec.focus_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while the node holds keyboard-visible focus (moved there by Tab or assistive technology, not a click); replaces the default focus ring. Pressed wins over focus wins over hover; eases with `transition`. |
|
|
41
|
+
| `focusRegion` | `focus_region` | `focus_region` | `Spec.focus_region` | boolean | Makes this node's subtree a focus region: a Tab ring of its own that the ring outside never enters and that never leaves — a devtools dock, an inspector beside the app (`docs/adr/0022-focus-regions.md`). Entered on purpose: `focusRegion(name)` (`Ui::focus_region`, `env.focus_region`, `kui_focus_region`) moves focus in — to the focus the region last held, else its `initialFocus`, else its first stop — and `focusRegion(null)` moves it back to the main ring the same way; a press inside the region, or an explicit focus on a node in it, enters it too. Tab then walks that ring alone, wrapping inside it; with nothing focused, Tab enters the ring of the region in effect (`region()`). A region that stops being declared hands focus back to what the main ring last held. Only the ring is scoped: keys still bubble through the boundary to the sink above (a region that wants its own keymap is an `onKey` sink), the pointer and assistive technology see a plain node, and a `modal` in effect is the ring wherever it sits. Nested regions are skipped by the outer ring the way the main ring skips them. |
|
|
42
|
+
| `focusable` | `focusable` | `focusable` | `Spec.focusable` | boolean | Reachable by Tab (and focused by a click) without a click payload or a control role — a row that opens on Enter. Editors, key sinks, `onClick` boxes and the control roles are focusable already. |
|
|
43
|
+
| `gap` | `gap` | `gap` | `Spec.gap` | number, or a `"$length"` token | Space between children along the main axis. |
|
|
44
|
+
| `gradient` | `gradient` | `gradient` (`const KuiGradient *`) | `Spec.gradient` | gradient (`{ to? \\| angle? \\| radial?, at?, stops: [color \\| [color, at], …] }`) | A gradient painted over the node's `bg` and under its border and its children (`docs/adr/0042-a-gradient-is-an-image-the-core-paints.md`): `{ to: 'bottom', stops: [...] }` towards a side or a corner (`right`, `bottom left`, …; the default is `bottom`), `{ angle: 0.125, stops }` in turns clockwise from east, or `{ radial: true, at: [0.5, 0], stops }` out from a centre (fractions of the box, the middle by default) to its farthest corner. A stop is a colour — a `$token` too — or `[colour, position]` with the position 0 to 1; stops without one are spaced evenly between those with. Two stops at one position are a hard edge. The gradient is defined on the box's unit square and stretched to it, so a side or a corner is CSS's and any other `angle` runs corner to corner at an eighth of a turn whatever the box's aspect, where CSS's pixel-measured `45deg` does not. Stops mix in straight sRGB with the alpha premultiplied, as CSS's do. What it costs is one image quad: the core rasterizes each distinct gradient once into the glyph atlas — a 256-texel strip along an axis, a 128-texel square otherwise, within half an 8-bit level of the gradient computed per pixel for a linear one and 1.2 for a radial — keyed by the gradient and not the box, so a box that resizes and a thousand boxes that share one rasterize nothing, and a host that draws an image draws it; a gradient box costs about 55 ns over a flat one, so ten thousand of them are half a millisecond. A hard edge is as soft as the raster stretched to the box (a 256th of its length along a strip); stripes are boxes. It does not tween — `transition` eases the `bg` under it and `opacity` fades it — and `hoverBg` and the other state backgrounds replace `bg`, not the gradient; one that changes every frame is a raster a frame, and a shimmer is a `fragment`'s. Ignored on a `line`, a `polygon` and a `path`. Fewer than two stops are an error in JSX and Lua, as a malformed `keyframes` is, and draw nothing in Rust and C. A stop whose `$token` misses is not an error: it is raised as `unknown-token` and left out, as a miss leaves any slot unset, and the rest are spaced as if it had not been declared — so a gradient left with fewer than two stops, a two-stop one with a typo, draws nothing over its `bg` (backlog RG118). |
|
|
45
|
+
| `height` | `height` | `height` (KuiSizing) | `Spec.height` | sizing (`number` \\| `"fit"` \\| `"grow"` \\| `"N%"` \\| a size expression \\| `"$length"`) | Vertical size: px \| "fit" \| "grow" \| "N%" \| a size expression (see `width`). |
|
|
46
|
+
| `hoverBg` | `hover_bg` | `hover_bg` | `Spec.hover_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while hovered (or while any node in its hoverGroup is); implies hover tracking, eases with `transition`. |
|
|
47
|
+
| `hoverGroup` | `hover_group` | `hover_group` (KuiStr) | `Spec.hover_group` | string | Nodes sharing a group name show hoverBg/pressedBg together (a split button, a multi-piece shape). |
|
|
48
|
+
| `hoverSound` | `hover_sound` | `hover_sound` | `Spec.hover_sound` | resource handle | A registered sound (addSound) played when the pointer enters the node; implies hover tracking. |
|
|
49
|
+
| `hoverable` | `hoverable` | `hoverable` | `Spec.hoverable` | boolean | Hover-track without a click payload (for isHovered-driven styling). |
|
|
50
|
+
| `initialFocus` | `initial_focus` | `initial_focus` | `Spec.initial_focus` | boolean | Where focus lands when the enclosing `modal` scope is entered: the first node in the modal's Tab ring declaring it, so a destructive confirm opens on its Cancel rather than on whichever control is declared first. Read on entry only — a Tab press afterwards stands, and the scope re-entered (a nested confirm closing) leaves focus where it was. Declared on nothing, or only on nodes the ring skips (disabled, `role="none"`, not focusable), entry stays the ring's first node. |
|
|
51
|
+
| `keepFocus` | `keep_focus` | `keep_focus` | `Spec.keep_focus` | boolean | A press on this node, or anywhere inside it, leaves keyboard focus where it was: a toolbar button, a tab or a divider that acts without taking the keyboard from the editor or key sink that had it. Without it a press on an `onClick` node focuses the node, and the app's keys stop reaching the sink until it takes focus back. The press also leaves a text or cell selection and the Tab ring where they were, so a Copy button copies what was selected. An `<edit>` inside still takes its caret and focus, as the keyboard's own owner. The click, drag and hover are unchanged, and Tab and assistive technology still reach the node. |
|
|
52
|
+
| `keyUp` | `key_up` | `key_up` | `Spec.key_up` | boolean | With `onKey`: releases arrive too, as the same payload with phase:"up" (`text` null, `repeat` false) — for a held-key interaction (WASD, press-and-hold, a key that arms a mode while it is down). A key only comes up where it went down: a release whose press the sink never got is dropped, and focus leaving while a key is held delivers the `up` first, so nothing is left stuck down. Without it a sink hears presses only, which is what a keymap wants — one that heard both halves would run every binding twice. |
|
|
53
|
+
| `keyframes` | `keyframes` | `keyframes` + `keyframes_len` (`KuiKeyframe[]`) | `Spec.keyframes` | keyframe list (`[{ at?, width?, height?, bg?, radius?, opacity? }, …]`) | CSS-style stops `[{ at?, width?, height?, bg?, radius?, opacity? }, …]`: the slots they name cycle through them over `transition` ms, forever, without the view redrawing; `at` is 0..1 and spreads evenly when omitted. |
|
|
54
|
+
| `label` | `label` | `label` (KuiStr) | `Spec.label` | string | The accessible name. Without one a button, link, tab or heading is named by the text inside it; an image, an icon-only button and a `modal` dialog have none, and the core warns (`image-without-label`, `control-without-name`, `modal-without-name`). |
|
|
55
|
+
| `live` | `live` | `live` (`KUI_LIVE_*`) | `Spec.live` | `off` \\| `polite` \\| `assertive` | Marks this node a live region: when the text inside it changes, a screen reader reads the change without being asked — `polite` at the next pause, `assertive` interrupting. Put it on the smallest node that holds the message, since everything inside a live node is live. For a one-off with no node behind it ("Saved") the binding's `announce` verb is the other half. |
|
|
56
|
+
| `mainAlign` | `main_align` | `main_align` | `Spec.main_align` | `start` \\| `center` \\| `end` \\| `spaceBetween` \\| `spaceAround` \\| `spaceEvenly` \\| `baseline` | Child alignment along the main axis. `start`, `center` and `end` put the children together; `spaceBetween` deals the free space out between them (none at the ends), `spaceAround` gives each child an equal share split to its two sides, and `spaceEvenly` makes every gap and both ends equal — CSS's `justify-content`. The spread is added to `gap`, and there is none when nothing is free: a `grow` child takes it all, and an overflowing run keeps its gaps. `baseline` means nothing here and lays out as `start`, with a warning. |
|
|
57
|
+
| `maxHeight` | `max_height` | `max_height` | `Spec.max_height` | maximum (`number` \\| a size expression \\| `"$length"`) | Upper height clamp: logical px or a size expression (see `width`). |
|
|
58
|
+
| `maxWidth` | `max_width` | `max_width` | `Spec.max_width` | maximum (`number` \\| a size expression \\| `"$length"`) | Upper width clamp: logical px or a size expression (see `width`); grow+maxWidth is the responsive-width pattern. |
|
|
59
|
+
| `minHeight` | `min_height` | `min_height` | `Spec.min_height` | minimum (`number` \\| `"fit"` \\| a size expression \\| `"$length"`) | Lower height clamp: logical px, a size expression, or "fit" for the node's own fit height. Undeclared, it is the node's content where its column overflows — CSS's `min-height: auto`, none for a node that scrolls or clips — so a row keeps the height of its text; `0` asks for the squeeze back (see `minWidth`). |
|
|
60
|
+
| `minWidth` | `min_width` | `min_width` | `Spec.min_width` | minimum (`number` \\| `"fit"` \\| a size expression \\| `"$length"`) | Lower width clamp: logical px, a size expression (see `width`; a percentage clamp is none until the parent's width is known, as in CSS), or "fit" for the node's own fit width. "fit" under `width="grow"` is a content floor — CSS's `flex: 1 0 auto` — which is what an i3-style tab bar is: tabs that split the bar evenly while they fit and sit at their label's width, scrolling, once they do not. Opt-in, because a fit width is the unwrapped one: a paragraph in a grow column would stop wrapping under it. Left out, a child giving in an overflowing row that holds a percentage or a size expression stops at its content, CSS's `min-width: auto`; `0` lets it go below, CSS's `min-width: 0` (backlog RG92). A fit node across a column is no wider than the column's box — CSS's `fit-content` — down to this floor, 0 left out, unless the column scrolls x, so a text one wrapper deep in a capped card wraps there; "fit" keeps its content's width and runs past (backlog F116). |
|
|
61
|
+
| `mixed` | `mixed` | `mixed` | `Spec.mixed` | boolean | A `checkbox` that is neither on nor off — the select-all box over a list some of whose rows are selected (ADR 0034). Read as mixed by assistive technology whatever `checked` says, and drawn as a dash by the stock `<checkbox>`. Meaningful on the checkbox role alone. |
|
|
62
|
+
| `modal` | `modal` | `modal` (a borrowed `KuiValue*`, cloned while the node opens) | `Spec.modal` | tag (a message merged into the event under `tag`, or `null` for none) | Modal surface: the Tab ring becomes this node's subtree, everything outside it is inert to the pointer, the wheel and assistive technology, and Escape or a press outside emits {kind:"dismiss", reason:"escape"\|"outside", tag} on it — the app stops declaring the node. The last one declared in tree order is the one in effect (a confirm inside a dialog); a modal that must cover the app is a float. The access tree is not pruned to the modal: it keeps every node of the frame and marks the one in effect `modal` (`docs/adr/0003-modal-surfaces.md`, decision 7), which is what assistive technology acts on. |
|
|
63
|
+
| `modifierKeys` | `modifier_keys` | `modifier_keys` | `Spec.modifier_keys` | boolean | With `onKey`: the modifier and lock keys arrive as keys of their own (backlog F108) — `code` "shift", "ctrl", "alt", "super", "capslock", "numlock", "scrolllock", which side in `location` ("left" / "right"), releases too with `keyUp`. Without it a modifier is only ever held — the next key's `shift`, `ctrl`, … and the `modifiers` event — so a keymap mid-sequence never reads a Shift as a key between two others. For a terminal speaking kitty's keyboard protocol, or a game that binds a lone Shift. |
|
|
64
|
+
| `onButton` | `on_button` | `on_button` (a borrowed `KuiValue*`, cloned while the node opens) | `Spec.on_button` | tag (a message merged into the event under `tag`, or `null` for none) | Button tag (backlog F105): a press of a non-primary button — middle, secondary, or one past those — emits {kind:"button", phase:"press", button, x, y, clicks, tag} on the node, and the button is then captured by it: every pointer move while it is held arrives as phase:"move" and its release as phase:"release", on this node wherever the pointer is. `button` is `"secondary"`, `"middle"` or a further button's number (3 and up); `x`/`y` are logical viewport coordinates, and on a `cells` grid each event carries `cell: {row, col}` as a click does. Several buttons can be held at once, each its own capture, and a primary drag is untouched. `buttons` says which buttons it claims — all of them unless it narrows them. Asked of the topmost node under the pointer, and when that node claims no such button the press reaches the nearest enclosing node that does, the way a context menu's does: a disabled node's own is skipped and the walk stops at the modal boundary. A claimed secondary press is this event *instead of* a `contextmenu` event and the stock menu (a nearer `onContextMenu` still wins, being the nested declaration). Like every non-primary press it moves no focus, places no caret and touches no selection or scrollbar. For a terminal's middle-click paste, and the mouse reports a program in it asked for. |
|
|
65
|
+
| `onChange` | `on_change` | `on_change` | `Spec.on_change` | tag (a message merged into the event under `tag`, or `null` for none) | A `slider` role's changes (ADR 0034): the core turns a press on the node into the value under the pointer, a drag into the value under it, the arrows and assistive technology's increment / decrement into one `valueStep`, PageUp / PageDown into ten, Home / End into the range's ends — clamped to `valueMin`..`valueMax` (0..100 unset) and snapped to the step — and emits `{kind:"change", value, phase, tag}`: `phase` is `"move"` while the pointer holds the slider and `"end"` when it lets go or a key moved it. The value is proposed and never applied; the slider moves when the view declares it as `valueNow`. A key that lands where the slider already is proposes nothing. The pointer reads the node's content box along its main axis, so a `dir="column"` slider runs bottom to top. Without it a slider's arrows reach the app as `{kind:"access", action}`. Ignored on any other role. |
|
|
66
|
+
| `onClick` | `on_click` | `on_click` argument of `kui_open` / `kui_open_with` | `Spec.on_click` | message (any plain data) | Message emitted when clicked (data, not a callback). |
|
|
67
|
+
| `onContextMenu` | `on_context_menu` | `on_context_menu` (a borrowed `KuiValue*`, cloned while the node opens) | `Spec.on_context_menu` | tag (a message merged into the event under `tag`, or `null` for none) | Context-menu tag: a secondary-button (right) press emits {kind:"contextmenu", x, y, tag} on the node, at the logical viewport point to open the menu at. The press moves no focus, places no caret and produces no click, so right-clicking a selection keeps it. Asked of the topmost node under the pointer, and when that node offers no menu the press reaches the nearest enclosing node that does — a container declaring a menu for everything inside it is the common case — the way an unclaimed key reaches the enclosing sink (`docs/adr/0011`): the event carries the *owner's* key and tag, a nested declaration wins over its ancestor's, a disabled node's own is skipped, and the walk stops at the modal boundary. |
|
|
68
|
+
| `onDrag` | `on_drag` | `on_drag` argument of `kui_open_draggable` / `kui_open_with` | `Spec.on_drag` | tag (a message merged into the event under `tag`, or `null` for none) | Drag tag: emits {kind:"drag", phase, x, y, dx, dy, parent, tag} events, `dx`/`dy` measured from the press point in every phase. On a `cells` grid the events also carry `cell: {row, col}`; inside an `onKey` sink that draws `role="line"` rows they carry `line`, `byte` and `clicks` — see the events table. |
|
|
69
|
+
| `onDrop` | `on_drop` | `on_drop` (a borrowed `KuiValue*`, cloned while the node opens) | `Spec.on_drop` | tag (a message merged into the event under `tag`, or `null` for none) | Drop-zone tag (`docs/adr/0031-a-drop-zone-is-a-row-and-the-files-are-an-event.md`): files dragged in from the OS over this node emit {kind:"drop", phase:"enter"\|"move"\|"leave"\|"drop", paths, x, y, tag} — `paths` the OS paths as strings, `x`/`y` the pointer in logical viewport coordinates (absent on `leave`). The zone under the files is the topmost zone by paint order: a node inside a zone is the zone's (a button in it, a field in it), and a node that is no zone and has none enclosing it is looked past, so an overlay shown on `enter` cannot make the zone lose the files. No `leave` follows a `drop`; a drop off every zone is refused by the driver. Implies hover tracking. No access row — a screen-reader user's way in is a button beside the zone. On Windows and Linux the position is the OS cursor at enter and release only, so `move` never fires there. |
|
|
70
|
+
| `onFocus` | `on_focus` | `on_focus` (a borrowed `KuiValue*`, cloned while the node opens) | `Spec.on_focus` | tag (a message merged into the event under `tag`, or `null` for none) | Keyboard focus entering or leaving this node's subtree — the node itself, or anything focused inside it — emits `{kind:"focus", phase:"in"\|"out", by, tag}` (backlog DX18). `by` is what moved it: `pointer` (a press), `keyboard` (Tab, a key a control answered), `assistive` (a screen reader's request) or `program` (the view or the app — `keyFocus`, `setFocus`, a modal's entry). Reported once the move settles, after the input that made it or at the end of the frame that declared it, so an app hears a pane taking the keyboard instead of diffing the focused key every frame. Leaving is reported innermost first, entering outermost first. It makes nothing focusable or interactive. |
|
|
71
|
+
| `onForceClick` | `on_force_click` | `on_force_click` | `Spec.on_force_click` | tag (a message merged into the event under `tag`, or `null` for none) | Force-click tag: a press that deepens past the second stage of a Force Touch trackpad emits {kind:"forceclick", x, y, tag} on the node, at the logical viewport point it happened at (`docs/adr/0017-selection-as-a-scope.md`). Routed as a secondary press is — no focus moved, no caret placed, no click — but asked of the topmost node only, with no walk to an enclosing declaration — and the ordinary click the press is still producing arrives afterwards, as it does on macOS. Text needs none of this: a force click over an `edit` or a `selectable` scope selects the word under it and asks the host for its Look Up panel. macOS-only in practice, and there the user can switch the gesture off, so nothing may declare itself the only way to reach something. |
|
|
72
|
+
| `onHover` | `on_hover` | `on_hover` argument of `kui_open_with` | `Spec.on_hover` | tag (a message merged into the event under `tag`, or `null` for none) | Hover tag: the pointer entering/leaving emits {kind:"hover", phase:"enter"\|"leave", tag} events. |
|
|
73
|
+
| `onKey` | `on_key` | `on_key` argument of `kui_open_with` | `Spec.on_key` | tag (a message merged into the event under `tag`, or `null` for none) | Key-sink tag: with key focus held, presses arrive as {kind:"key", phase:"down", code, ...} events. Releases only with `keyUp` beside it. |
|
|
74
|
+
| `onLayout` | `on_layout` | `on_layout` (a borrowed `KuiValue*`, cloned while the node opens) | `Spec.on_layout` | tag (a message merged into the event under `tag`, or `null` for none) | Layout tag: the node's laid-out rect arrives as {kind:"layout", x, y, w, h, parent, tag} on its first frame and whenever it changes (needs a stable key). |
|
|
75
|
+
| `onScroll` | `on_scroll` | `on_scroll` (a borrowed `KuiValue*`, cloned while the node opens) | `Spec.on_scroll` | tag (a message merged into the event under `tag`, or `null` for none) | Scroll tag: the wheel over this node emits {kind:"scroll", x, y, dx, dy, lines, tag} on it instead of scrolling anything — `dx`/`dy` the delta in logical px as the driver reported it (positive `dy` is the wheel rolling up, toward earlier content), `x`/`y` the pointer, and `lines` on a `cells` grid the whole lines the delta covers (positive = later history, the sign `originLine` grows in; the fraction is carried to the next notch so a trackpad's small steps add up) and null on any other node. The node takes the wheel on the axes `scrollAxes` names (both unless it narrows them): a gesture that starts over it is its own whether or not it has anywhere to go — except on an axis the node also scrolls (`scrollX`/`scrollY`, its offset the app's to set), where it is answered by its room as a container is, so at its edge a gesture that way passes to the scroller around it (backlog F118) — and stays its own until it ends, wherever the pointer goes (backlog F107); it reaches no scroll container above it, and a scroller inside it still takes the axes it scrolls while it can move that way, passing this node the rest — the other axis, and a gesture that begins with that scroller at its limit (`overscroll: "contain"` on the scroller keeps it there). The core moves nothing — a grid re-declares `originLine`, a canvas zooms. A drag-select held past a `cells` grid's top or bottom edge arrives here too, once a frame with the lines that frame scrolled by (`docs/adr/0029-a-selection-follows-the-pointer-past-the-edge.md`). |
|
|
76
|
+
| `opacity` | `opacity` | `opacity` with `opacity_set` | `Spec.opacity` | number, or a `"$length"` token | Group opacity 0..1 (default 1): fades this node and its whole subtree. A per-quad alpha multiply rather than an offscreen composite, so overlapping pieces of one subtree show their seams through the fade. Layout, hit-testing and the access tree are untouched; eases with `transition`, and `enter: { opacity: 0 }` fades a panel in. |
|
|
77
|
+
| `overscroll` | `overscroll` | `overscroll` (`KUI_OVERSCROLL_*`; zeroed, auto) | `Spec.overscroll` | `auto` \\| `contain` | What a scroll gesture that starts over this scroller does when it is already at its limit that way (backlog F107, CSS's `overscroll-behavior`): `auto` (the default) passes the gesture on to the scroller around it, `contain` keeps it here, moving nothing until it turns back. A gesture picks its target once, when it starts — the innermost scroller under the pointer that can still move the way it goes — and keeps it until it ends, wherever the pointer or the content has gone; one that reaches a limit midway stops there, whatever this says. Only on the axes the node scrolls: a `scrollY` list that contains still passes a sideways swipe to the strip around it. For a panel or a popup's list whose scrolling must never move what is behind it. |
|
|
78
|
+
| `pixelSnap` | `pixel_snap` | `pixel_snap` | `Spec.pixel_snap` | boolean | Paint this node's background, border, shadow and fragment with each edge on a whole physical pixel: `x` and `x + width` rounded on their own, from where layout put them, as a text's span backgrounds are. Off by default, and a box is drawn where layout put it, so a 1 px `gap` between boxes is there at any scale. On, boxes that share an edge in layout meet on one pixel line, where a join inside a pixel was drawn by halves and left a seam — rows of a band stacked at a pitch that is not whole pixels, or a box that continues a text's selection. Layout, hit-testing, the clip and the children are untouched. A snapped box can draw up to half a pixel from its layout edge and its size can differ by a pixel, so a snapped hairline is 1 or 2 px thick by where it sits. |
|
|
79
|
+
| `pressedBg` | `pressed_bg` | `pressed_bg` | `Spec.pressed_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while pressed (or while its hoverGroup is); implies hover tracking. |
|
|
80
|
+
| `radius` | `radius` | `radius` | `Spec.radius` | number, or a `"$length"` token | Corner radius for all four corners (logical px); the per-corner props override it when listed after it. On a node that also clips or scrolls it rounds the clip as well, so children stay inside the corners. |
|
|
81
|
+
| `radiusBL` | `radius_bl` | `radius_bl` with `per_corner` | `Spec.radius_bl` | number, or a `"$length"` token | Bottom-left corner radius (logical px). |
|
|
82
|
+
| `radiusBR` | `radius_br` | `radius_br` with `per_corner` | `Spec.radius_br` | number, or a `"$length"` token | Bottom-right corner radius (logical px). |
|
|
83
|
+
| `radiusTL` | `radius_tl` | `radius_tl` with `per_corner` | `Spec.radius_tl` | number, or a `"$length"` token | Top-left corner radius (logical px). |
|
|
84
|
+
| `radiusTR` | `radius_tr` | `radius_tr` with `per_corner` | `Spec.radius_tr` | number, or a `"$length"` token | Top-right corner radius (logical px). |
|
|
85
|
+
| `repeat` | `repeat` | `repeat` (`KUI_REPEAT_*`) | `Spec.repeat` | `normal` \\| `reverse` \\| `alternate` \\| `alternateReverse` | How `keyframes` cycle (CSS `animation-direction`, default normal). Lua: `direction`, since `repeat` is a keyword. |
|
|
86
|
+
| `role` | `role` | `role` (`KUI_ROLE_*`) | `Spec.role` | `none` \\| `button` \\| `checkbox` \\| `radio` \\| `switch` \\| `slider` \\| `tab` \\| `tabList` \\| `link` \\| `heading` \\| `list` \\| `listItem` \\| `image` \\| `dialog` \\| `group` \\| `textInput` \\| `multilineTextInput` \\| `line` \\| `radioGroup` \\| `menu` \\| `menuItem` \\| `terminal` | What the node is to assistive technology. Unset, the core derives one (an `onClick` node is a button, an editor a text input, a scrolling box a scroll view, a plain box nothing); `none` hides the node and its subtree from the access tree. A `radio` belongs inside a `radioGroup` and a `tab` inside a `tabList`, labelled with what the choice is: the pair is a composite (`docs/adr/0007-composite-keyboard-patterns.md`) — one Tab stop for the set, the arrows, Home and End moving the choice inside it (each step is the item's click, so the choice follows focus), and a screen reader reading "2 of 3". A `radio` or `tab` with no container above it is a Tab stop of its own that no arrow moves, and the core warns (`item-outside-container`). `menu` holds `menuItem`s and `list` holds `listItem`s the same way. |
|
|
87
|
+
| `ruleWidth` | `rule_width` | `rule_w` | `Spec.rule_width` | number, or a `"$length"` token | The width of a table's `rules` in logical px; 1 when unset. |
|
|
88
|
+
| `rules` | `rules` | `rules` | `Spec.rules` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | On a table (`dir="table"`, ADR 0033): grid lines of this colour between its columns and between its rows (backlog DX21) — down the middle of each gap between the columns of its widest row, from the first row's top to the last row's bottom, and across the middle of each gap between rows, the content box wide. Drawn with the table's box, under its cells and on whole pixels, so give the table and its rows a `gap` at least `ruleWidth` for the lines to show between cells; the outer edge is the table's `border`. Ignored on anything but a table. |
|
|
89
|
+
| `scrollAxes` | `scroll_axes` | `scroll_axes` (`KUI_SCROLL_AXES_*`; zeroed, both) | `Spec.scroll_axes` | `both` \\| `x` \\| `y` | Which axes `onScroll` takes (backlog F107): `both` (the default), `x` or `y`. A scroll gesture on an axis the node does not take passes it by, to the scroller around it, and hears nothing here: a terminal that scrolls its history says `y`, and a sideways swipe that starts over it moves the strip it sits in. (A swipe that started elsewhere is not the node's either way: a gesture keeps the target it started with.) Meaningless without `onScroll`. |
|
|
90
|
+
| `scrollMods` | `scroll_mods` | `scroll_mods` (`KUI_KMOD_*` bits; zeroed, none) | `Spec.scroll_mods` | string | The modifiers `onScroll` is for (backlog F122): `"shift"`, `"ctrl"`, `"alt"` and `"super"` (⌘, the Windows key), separated by spaces or commas — `"ctrl super"`. With any named, the node hears only a scroll gesture that began with one of them held, and hears it first: ahead of every scroll container and every `onScroll` that names none, wherever under the pointer the gesture began, the innermost such node winning — so a Ctrl-wheel zoom declared on the window's root is heard over a list, and the list does not scroll. A wheel with none of them held passes the node by, as if it had no `onScroll`: a node that scrolls as well (`overflow`) scrolls for it as any container does. Its `scroll` events carry `mods`, the modifiers held when the gesture began; the gesture stays the node's to the end of its glide, whatever is let go meanwhile, and one begun without them never becomes its. A word that is none of the four is skipped. Unset, a handler like any other. Meaningless without `onScroll`. |
|
|
91
|
+
| `scrollbar` | `scrollbar` | `scrollbar` | `Spec.scrollbar` | `visible` \\| `hidden` \\| `auto` | When a scrolling node draws its bars: `visible` (the default — the stock overlay thumb, drawn while the content overflows), `hidden` (no thumb, no track to press; the wheel, the keyboard, `reveal` and the caret still scroll it — for a list that draws its own indicator, or a pane whose bar would sit on a border), or `auto` (shown while the scroll state is changing — the offset or the content's extent moved, the pointer is on the track, a thumb is dragged — and for a second after, then faded out over a quarter of one; a node first seen shows it the same second; what an overlay bar does on macOS). `auto` needs the driver's clock and is `visible` without one. The bars are overlays and take no layout space in any mode. From the last change until it has faded — a second and a quarter — an `auto` bar asks for frames the way a transition of that length would (nothing else could wake the core when the hold ends); while the pointer holds it, it asks for none. |
|
|
92
|
+
| `scrollbarActiveColor` | `scrollbar_active_color` | `scrollbar_active_color` | `Spec.scrollbar_active_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | The thumb under the pointer or while dragged; the default is the theme's `scrollbar_active` role. |
|
|
93
|
+
| `scrollbarColor` | `scrollbar_color` | `scrollbar_color` | `Spec.scrollbar_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | The thumb at rest; the default is the theme's `scrollbar` role, a translucent wash over whatever it sits on. |
|
|
94
|
+
| `scrollbarWidth` | `scrollbar_width` | `scrollbar_width` | `Spec.scrollbar_width` | number, or a `"$length"` token | The thumb's width at rest, logical px (default 4); under the pointer or dragged it is 2 px wider. The grabbable track grows to fit a wide thumb. |
|
|
95
|
+
| `selectable` | `selectable` | `selectable` | `Spec.selectable` | boolean | Makes this node a selection scope: the text of every node inside it is one selectable run, in tree order, and a press-drag across them selects the lot — as do Shift with Left / Right / Home / End on a focused node inside it, a character or a word at a time, a scope with nothing selected anchoring at its start (`docs/adr/0017-selection-as-a-scope.md`). Declared on the container and not on each label, because what a reader selects is a paragraph or a card rather than one run of it — three labels in a column under one `selectable` select as three lines of one text. The selection is the window's: starting one anywhere clears the last, an editor's included. Scopes do not nest; an outer one around an inner one is warned about (`nested-selection-scope`) and the innermost owns the text. Text scrolled out of view inside the scope is still part of it — selection and copy reach it, hit-testing does not. On a `cells` grid the scope selects in cells rather than in bytes: a drag takes lines (with a modifier, a rectangle), a double click the word under the pointer and a triple click the whole row, its ends are absolute lines so a scroll does not move them, and a copy trims each line's trailing blanks. |
|
|
96
|
+
| `selected` | `selected` | `selected` | `Spec.selected` | boolean | The current one of a set: which `tab` a `tabList` shows, which `listItem` a list has picked, which `link` is the page you are on. A `tab` always carries the state — its siblings read as "not selected" — while a list row or a link carries it only where it is set, since an ordinary list or navigation bar is not a selection and a reader saying "not selected" on every row of it is noise. |
|
|
97
|
+
| `selectionAnchor` | `selection_anchor` | `selection_anchor` with `KUI_VALUE_ANCHOR` in `value_set` | `Spec.selection_anchor` | number, or a `"$length"` token | On a `line` of a custom editor: the byte offset where the selection's other end sits (the caret is `caret`, possibly on another line). |
|
|
98
|
+
| `shadowBlur` | `shadow_blur` | `shadow_blur` | `Spec.shadow_blur` | number, or a `"$length"` token | Drop-shadow blur radius (logical px): the edge ramps over this distance and reaches this far past the shape. 0 = a hard edge. |
|
|
99
|
+
| `shadowColor` | `shadow_color` | `shadow_color` | `Spec.shadow_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Drop-shadow color; nothing else about a shadow draws without it. On its own it is a hard shadow exactly behind the node — add `shadowBlur` / `shadowY` to lift it. Outer shadows only, and the shape is not knocked out of the middle, so a translucent background shows it through. |
|
|
100
|
+
| `shadowSpread` | `shadow_spread` | `shadow_spread` | `Spec.shadow_spread` | number, or a `"$length"` token | Grows (or, negative, shrinks) the drop shadow's shape before blurring (logical px). |
|
|
101
|
+
| `shadowX` | `shadow_x` | `shadow_x` | `Spec.shadow_x` | number, or a `"$length"` token | Drop-shadow horizontal offset (logical px). |
|
|
102
|
+
| `shadowY` | `shadow_y` | `shadow_y` | `Spec.shadow_y` | number, or a `"$length"` token | Drop-shadow vertical offset (logical px); positive casts downward. |
|
|
103
|
+
| `slide` | `slide` | `slide` | `Spec.slide` | boolean | With transition: also ease the node's position (reordered siblings slide). While it eases, the node is drawn between where it was and where this frame put it — not at the declared `dx`/`dy`, or its slot in the row — so anything else positioned from those numbers drifts for the transition's length: a canvas of floats eases everything or nothing. |
|
|
104
|
+
| `transition` | `transition` | `transition_ms` | `Spec.transition` | number, or a `"$length"` token | Animate sizing/colors/radius changes over this many ms — and, on a scroll container, the offset a reveal or a set_scroll moves it to (needs a stable key). |
|
|
105
|
+
| `valueMax` | `value_max` | `value_max` with `KUI_VALUE_MAX` in `value_set` | `Spec.value_max` | number, or a `"$length"` token | A `slider` role's maximum. |
|
|
106
|
+
| `valueMin` | `value_min` | `value_min` with `KUI_VALUE_MIN` in `value_set` | `Spec.value_min` | number, or a `"$length"` token | A `slider` role's minimum. |
|
|
107
|
+
| `valueNow` | `value_now` | `value_now` with `KUI_VALUE_NOW` in `value_set` | `Spec.value_now` | number, or a `"$length"` token | A `slider` role's current value (the drawing stays yours; this is what assistive technology reads). |
|
|
108
|
+
| `valueStep` | `value_step` | `value_step` | `Spec.value_step` | number, or a `"$length"` token | How far one arrow key moves a `slider` role, and the grid a value the pointer sets snaps to (ADR 0034). Unset, a hundredth of the range. PageUp / PageDown move ten steps. Read by the core only where the slider declares `onChange`; reported to assistive technology either way. |
|
|
109
|
+
| `valueText` | `value_text` | `value_text` (KuiStr) | `Spec.value_text` | string | What a `slider` role's position reads as (ARIA's `aria-valuetext`). Without one a reader has only `valueNow` and the range and says a percentage — 25 in [5..60] is "36 percent" — so a value whose unit carries the meaning says it here: "25 minutes". It replaces the number in the reading rather than joining it, and a nudge announces the new text. Meaningful on the slider role alone, like the three numbers; putting the reading in `label` instead renames the control on every nudge, which is the wrong attribute. |
|
|
110
|
+
| `width` | `width` | `width` (KuiSizing) | `Spec.width` | sizing (`number` \\| `"fit"` \\| `"grow"` \\| `"N%"` \\| a size expression \\| `"$length"`) | Horizontal size: px \| "fit" \| "grow" \| "N%" \| a size expression — `"clamp(400px, 80%, 1000px)"`, `"min(720px, 100%)"`, `"max(50%, 300)"`, nested — which layout resolves against the parent's content box, the box a percentage takes its cut of (backlog F109). An expression with no percentage in it is a length; a calc does not ease under `transition`. A percentage or an expression gives, with the fit children, when its parent overflows — two `"50%"` children and a gap fit their row (backlog F110) — where a px size keeps its own. A row holding one gives as CSS's flex items do: every child that can give gives in proportion to its size, and stops at its content — the widest thing in it that cannot wrap, a label's longest word — unless `minWidth` says otherwise (backlog RG92). The process keeps 65 536 distinct expressions and never lets one go: past that a new one leaves its prop at its default, with a `size-expressions-full` warning, so declare one per layout — a px size for the part that moves each frame, a splitter's drag — not one per frame (backlog RG93). |
|
|
111
|
+
| `window` | `window` | `window_role` (`KUI_WINDOW_*`) | `Spec.window` | `drag` \\| `close` \\| `minimize` \\| `maximize` | Window-chrome role: interactions become window commands, not events. |
|
|
112
|
+
| `wrapChildren` | `wrap_children` | `wrap_children` | `Spec.wrap_children` | boolean | Children that don't fit the main axis start a new line instead of overflowing or shrinking. Rows only (a column is ignored, with a warning), and never on a scrollX row. |
|
|
112
113
|
|
|
113
114
|
## Text props
|
|
114
115
|
|
|
115
|
-
| JSX | Lua | C | type | description |
|
|
116
|
-
|
|
117
|
-
| `color` | `color` | `KuiTextStyle.color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Text color; default foreground when omitted. |
|
|
118
|
-
| `ellipsis` | `ellipsis` | `KuiTextStyle.ellipsis` | boolean | End the last line with an ellipsis when the text is cut off: a single line unless `maxLines` says otherwise. |
|
|
119
|
-
| `family` | `family` | `KuiTextStyle.family` (`KUI_FONT_*`) | `sans` \\| `serif` \\| `mono` \\| a family name | Font family: `sans`, `serif` or `mono`, kui's own, or the name of an installed family or one loaded with `loadFontsDir` / `loadFontFile` — `"Berkeley Mono"` — drawn in its face in the frame that names it (ADR 0037). A name is matched as `addSystemFont` matches it and registered in the session on first sight, exactly as the font database spells it (`"menlo"` is not `"Menlo"`); the session's first registration of any font maps the installed font files once (~30 ms on a Mac, backlog DX24), which a family named in a view pays in that frame. `systemFonts()` lists the names there are. A name nothing matches shapes as sans and raises `unknown-family`. It and `font` set the same thing, so declare one. |
|
|
120
|
-
| `features` | `features` | `KuiTextStyle.features` (a `KuiStr`, the same spelling) | string | OpenType features for the shaper, as `tag=value` pairs separated by spaces or commas — a bare `tag` is 1, `-tag` is 0: `"liga=0 calt=0"` keeps a coding font from joining `->` and `!=` (what a terminal built on runs needs to hold its grid), `"tnum"` lines figures up in a gutter, `"ss01"` picks a stylistic set. Unset, the font's own defaults apply. At most 8; part of what the text is shaped as, so two texts differing only here are shaped twice. |
|
|
121
|
-
| `font` | `font` | `KuiTextStyle.font` (from `kui_font_add*`) | resource handle | A registered font handle (addFont / addSystemFont); overrides `family`. |
|
|
122
|
-
| `lineHeight` | `line_height` | `KuiTextStyle.line_height` | number, or a `"$length"` token | Line height (logical px); default size * 1.35. |
|
|
123
|
-
| `maxLines` | `max_lines` | `KuiTextStyle.max_lines` | number, or a `"$length"` token | Lay out at most this many lines (0 = unlimited); with `ellipsis`, a line clamp. |
|
|
124
|
-
| `strikethrough` | `strikethrough` | `KuiTextStyle.decoration` (`KUI_DECO_STRIKETHROUGH`); `KuiSpan.flags` (`KUI_SPAN_STRIKETHROUGH`) | boolean | A line through the text, where the face puts its strikeout. Paint only; on a `<span>` the span alone, per line. |
|
|
125
|
-
| `underline` | `underline` | `KuiTextStyle.decoration` (`KUI_DECO_UNDERLINE`); `KuiSpan.flags` (`KUI_SPAN_UNDERLINE`) | boolean | A line under the text, where the face puts its underline and as thick as it says, in the text colour. Paint only. On a `<span>` it covers the span alone and follows it across a wrap, one rect per line. `underlineColor` gives it a colour of its own and `underlineStyle` a shape; either implies it. |
|
|
126
|
-
| `underlineColor` | `underline_color` | `KuiTextStyle.underline_color`; `KuiSpan.underline_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | The underline's own colour — a diagnostic's red under keyword-coloured text (backlog K4). Implies `underline`. On a `<span>` the span's; a span with no colour of its own takes the text's. |
|
|
127
|
-
| `underlineStyle` | `underline_style` | `KuiTextStyle.underline_style` (`KUI_UNDERLINE_*`); `KuiSpan.underline_style` | `solid` \\| `wavy` \\| `dotted` | The underline's shape (backlog K4): `solid` (the face's line), `wavy` (three strokes tall around the line, a six-stroke period — a diagnostic's squiggle, a terminal's undercurl) or `dotted` (dots two strokes across, four apart). Implies `underline`. A wave or dots are runs of the segment primitive a `line` draws, so no backend learns a kind; the cost is two quads per period. |
|
|
128
|
-
| `wrap` | `wrap` | `KuiTextStyle.wrap` (`KUI_WRAP_*`) | `word` \\| `glyph` \\| `none` \\| `break-spaces` | Line breaking at the node's width: between words (default), anywhere, never (one line per paragraph, clipped to the node), or between words with whitespace taking its room (`break-spaces`: a space that does not fit starts the next row rather than hanging past the edge — an editor's wrapped line). On a single-line `edit` — a field, which otherwise takes one line and scrolls it — declaring it is what makes the field fold to its width like a document, by this mode, while Enter still submits (see `edit`). |
|
|
116
|
+
| JSX | Lua | C | Odin | type | description |
|
|
117
|
+
|---|---|---|---|---|---|
|
|
118
|
+
| `color` | `color` | `KuiTextStyle.color` | `Text_Style.color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Text color; default foreground when omitted. |
|
|
119
|
+
| `ellipsis` | `ellipsis` | `KuiTextStyle.ellipsis` | `Text_Style.ellipsis` | boolean | End the last line with an ellipsis when the text is cut off: a single line unless `maxLines` says otherwise. |
|
|
120
|
+
| `family` | `family` | `KuiTextStyle.family` (`KUI_FONT_*`) | `Text_Style.family` | `sans` \\| `serif` \\| `mono` \\| a family name | Font family: `sans`, `serif` or `mono`, kui's own, or the name of an installed family or one loaded with `loadFontsDir` / `loadFontFile` — `"Berkeley Mono"` — drawn in its face in the frame that names it (ADR 0037). A name is matched as `addSystemFont` matches it and registered in the session on first sight, exactly as the font database spells it (`"menlo"` is not `"Menlo"`); the session's first registration of any font maps the installed font files once (~30 ms on a Mac, backlog DX24), which a family named in a view pays in that frame. `systemFonts()` lists the names there are. A name nothing matches shapes as sans and raises `unknown-family`. It and `font` set the same thing, so declare one. |
|
|
121
|
+
| `features` | `features` | `KuiTextStyle.features` (a `KuiStr`, the same spelling) | `Text_Style.features` | string | OpenType features for the shaper, as `tag=value` pairs separated by spaces or commas — a bare `tag` is 1, `-tag` is 0: `"liga=0 calt=0"` keeps a coding font from joining `->` and `!=` (what a terminal built on runs needs to hold its grid), `"tnum"` lines figures up in a gutter, `"ss01"` picks a stylistic set. Unset, the font's own defaults apply. At most 8; part of what the text is shaped as, so two texts differing only here are shaped twice. |
|
|
122
|
+
| `font` | `font` | `KuiTextStyle.font` (from `kui_font_add*`) | `Text_Style.font` | resource handle | A registered font handle (addFont / addSystemFont); overrides `family`. |
|
|
123
|
+
| `lineHeight` | `line_height` | `KuiTextStyle.line_height` | `Text_Style.line_height` | number, or a `"$length"` token | Line height (logical px); default size * 1.35. |
|
|
124
|
+
| `maxLines` | `max_lines` | `KuiTextStyle.max_lines` | `Text_Style.max_lines` | number, or a `"$length"` token | Lay out at most this many lines (0 = unlimited); with `ellipsis`, a line clamp. |
|
|
125
|
+
| `strikethrough` | `strikethrough` | `KuiTextStyle.decoration` (`KUI_DECO_STRIKETHROUGH`); `KuiSpan.flags` (`KUI_SPAN_STRIKETHROUGH`) | `Text_Style.strikethrough` | boolean | A line through the text, where the face puts its strikeout. Paint only; on a `<span>` the span alone, per line. |
|
|
126
|
+
| `underline` | `underline` | `KuiTextStyle.decoration` (`KUI_DECO_UNDERLINE`); `KuiSpan.flags` (`KUI_SPAN_UNDERLINE`) | `Text_Style.underline` | boolean | A line under the text, where the face puts its underline and as thick as it says, in the text colour. Paint only. On a `<span>` it covers the span alone and follows it across a wrap, one rect per line. `underlineColor` gives it a colour of its own and `underlineStyle` a shape; either implies it. |
|
|
127
|
+
| `underlineColor` | `underline_color` | `KuiTextStyle.underline_color`; `KuiSpan.underline_color` | `Text_Style.underline_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | The underline's own colour — a diagnostic's red under keyword-coloured text (backlog K4). Implies `underline`. On a `<span>` the span's; a span with no colour of its own takes the text's. |
|
|
128
|
+
| `underlineStyle` | `underline_style` | `KuiTextStyle.underline_style` (`KUI_UNDERLINE_*`); `KuiSpan.underline_style` | `Text_Style.underline_style` | `solid` \\| `wavy` \\| `dotted` | The underline's shape (backlog K4): `solid` (the face's line), `wavy` (three strokes tall around the line, a six-stroke period — a diagnostic's squiggle, a terminal's undercurl) or `dotted` (dots two strokes across, four apart). Implies `underline`. A wave or dots are runs of the segment primitive a `line` draws, so no backend learns a kind; the cost is two quads per period. |
|
|
129
|
+
| `wrap` | `wrap` | `KuiTextStyle.wrap` (`KUI_WRAP_*`) | `Text_Style.wrap` | `word` \\| `glyph` \\| `none` \\| `break-spaces` | Line breaking at the node's width: between words (default), anywhere, never (one line per paragraph, clipped to the node), or between words with whitespace taking its room (`break-spaces`: a space that does not fit starts the next row rather than hanging past the edge — an editor's wrapped line). On a single-line `edit` — a field, which otherwise takes one line and scrolls it — declaring it is what makes the field fold to its width like a document, by this mode, while Enter still submits (see `edit`). |
|
|
129
130
|
|
|
130
131
|
## Composite props (hand-written per binding)
|
|
131
132
|
|
|
132
|
-
| JSX | Lua | C | description |
|
|
133
|
-
|
|
134
|
-
| `alwaysOnTop` (root box only) | `always_on_top = true` (root table) | `kui_set_always_on_top` | Declares that this frame wants the window kept above every other app's — a floating palette, a picture-in-picture player, a timer (backlog C30). Frame state the way `title` is, applied by the driver on change and free on the frames it does not change, but with a default of false rather than "leave as-is": a frame that stops declaring it lowers the window again, so a pin button is a toggle on the app's own state and nothing has to remember to undo it. Whether the platform has a level to set is `env.window.always_on_top`, which is what the pin button should draw its state from — Wayland has no call for it at all, so there the window never moves and the reading says so; it is the driver's record of what it set, not a query, so a level the OS dropped afterwards (a fullscreen space, a tiling manager) is not reported. A popup keeps its own level whatever its owner declares. |
|
|
135
|
-
| `borderW`, `borderColor` | `border = { w=, color= }` | `border_w`, `border_color` | Border width and color (drawn inside the rect). |
|
|
136
|
-
| `dir="row" \| "column" \| "table"` | `row { }` / `column { }` / `grid { }` | `dir` (`KUI_ROW` / `KUI_COLUMN` / `KUI_TABLE`) | Main axis; column is the default. `table` is a column whose rows' children line up in columns (the `table` element). |
|
|
137
|
-
| `float="below" \| "above" \| "parent" \| "viewport"` or `{ anchor, at, self, dx, dy, fit, clip }` | `float = "below"` or `float = { anchor=, at=, self=, dx=, dy=, fit=, clip= }` | `float_mode`, `float_anchor_x/y`, `float_self_x/y`, `float_dx/dy`, `float_fit`, `float_clip`; `kui_spec_float_preset` fills them from a preset name | Out-of-flow positioning against the parent or the viewport; `fit` flips/clamps to stay on screen. A float escapes every ancestor's clip — a tooltip is not cut by the scroller it hangs from — unless it declares `clip` and is anchored to its parent (`parent`, `below`, `above`): then the parent's clip holds it as it holds a child, so a node on a `clip` canvas panned past the canvas's edge is cut there and cannot be hit past it; it still paints as a layer over its in-flow siblings. A `line`, `polygon` or `path` in its parent's box is always clipped this way. The four preset names resolve in `FloatConfig::preset`, and `anchor` takes any of them — an override left out keeps the preset's own value, so `{ anchor: "below", dx: 4 }` still hangs below with its 6px gap. A float is a layer of its own: above the in-flow tree and every float that opened before it, under every float that opened after, and hit-tested in the same order — so a tooltip that appears over an open menu is over it, and a popover over a scroller's bar takes the press there. A scroller's bars and the focus ring belong to the layer that owns them. There is no z-index; a float declared under a fresh key reopens on top (`docs/adr/0023-layers-stack-in-the-order-they-open.md`). |
|
|
138
|
-
| `imeOff` (root box only) | `ime_off = true` (root table) | `kui_set_ime_off` | Declares that this window takes the keyboard as keys, with the platform's input method off (backlog F125): no composition and no candidate window, and on a Mac no dead key waiting for the next and no press-and-hold — an input method too, so a held letter repeats instead of opening the accent picker, whatever the user's `ApplePressAndHoldEnabled` says. A `key` event's `text` is still the layout's character; what goes is everything the OS would have composed from it. What a modal editor's normal mode wants — `jjjj` is how one moves, and an IME left on eats the keymap — while its insert mode stops declaring it and gets accents, dead keys and the IME back. Frame state the way `alwaysOnTop` is, default false: declare it on every frame the mode wants it, and the frame that stops gives the input method back; the runner applies it to the window on change, never per frame, and a composition in progress when it turns off ends without a commit, as an empty `preedit`. The window's, not a node's: a stock editor focused under it composes nothing either. A popup's keys arrive through its owner, so the owner's declaration is the one they are read under. On Windows and Linux the window's IME is disabled the same way, and only that: their dead keys are the layout's, and still compose. A C host with its own loop reads the ask with `kui_ime_off_get` and applies it itself. |
|
|
139
|
-
| `index` | `index` | `kui_open_indexed` | Stable identity by *data* index rather than by name: the key auto-keying would have given this node as the `i`th child, given to it wherever it actually sits. What a virtualised list is for — a view that builds rows 900..930 of ten thousand opens each with its own row number, so the row keeps its hover, focus, edit buffer and tweens as the built range slides over it, and a list that builds every row agrees with one that builds a screenful. Wherever `key` names a node this numbers it (a box, a `line`, a `cells`, a `fragment`); declared beside `key` the index wins. Indices and names are separate namespaces, so a spacer keyed `"lead"` cannot collide with row 0 — but two rows on one index do, exactly as two on one name would. |
|
|
140
|
-
| `key` | `key` | `kui_open_keyed` label | Stable identity for retained state (scroll offsets, editors, transitions; keys are hashes of the path from the root). Retained state outlives the key's absence, under a budget on the states nobody declares (see `<edit>` and the overflow props). |
|
|
141
|
-
| `keyFocus` | `key_focus` | `kui_set_key_focus` | Focuses this node (an `onKey` sink, an editor, any focusable node) when it starts being declared: declared every frame it takes focus once, so a later Tab press is not clobbered. Declaring it on the frame a `modal` stops being declared is how a view says where focus lands on the way out — the edge stands, and the focus the modal displaced is not handed back over it (`docs/adr/0003-modal-surfaces.md`, decision 4). To move focus at any time call the binding's focus verb (`ctx.focus`, `kui_focus`, `env.set_focus`). |
|
|
142
|
-
| `optionAsAlt="left"` — `"none"`, `"left"`, `"right"`, `"both"` (root box only) | `option_as_alt = "left"` (root table) | `kui_set_option_as_alt` (`KUI_OPTION_AS_ALT_*`) | Declares which Option keys act as Alt in this window on macOS (backlog F113). On a Mac, Option composes: ⌥m types `µ`, and ⌥u, ⌥e, ⌥i, ⌥n and ⌥` are dead keys that start an accent and wait for the next key, so the press never reaches the app as a key and a keymap binding `<A-u>` never hears it. An Option key named here is Alt instead: it composes nothing and types nothing, and a key under it arrives as a chord of the key the layout prints unmodified — a terminal's "Option as Meta", an editor's Alt bindings. `"left"` or `"right"` leaves the other side composing, so a user keeps `ü` on one Option; `"both"` takes both; `"none"`, the default, is the Mac's own behaviour. Frame state the way `alwaysOnTop` is: declare it on every frame, and the frame that stops gives the Option keys back to the layout; the runner applies it to the window on change, never per frame. A popup's keys arrive through its owner, so the owner's declaration is the one they are read under. Nothing on Windows or Linux, whose Alt composes nothing. A C host with its own loop reads the ask with `kui_option_as_alt_get` and applies it itself. |
|
|
143
|
-
| `clip`, `scrollX`, `scrollY` | `clip`, `scroll_x`, `scroll_y` (`scroll` = `scroll_y`) | `overflow` bits `KUI_CLIP` \| `KUI_SCROLL_X` \| `KUI_SCROLL_Y` | Clip children; scroll (implies clip) with retained offsets and live scrollbars. A `radius` on the same node rounds the clip, so a rounded card does not show square corners poking out of it; nesting two rounded clippers keeps only the corners neither of them moved, and hit-testing stays rectangular. Every frontend ORs the same bits and hands them to `NodeSpec::overflow_bits`. The wheel goes to the scroller under the pointer on the axes it scrolls, and the rest of the notch to the one around it: a `scrollY` list inside a `scrollX` strip moves the strip on a sideways swipe (backlog DX13). A scroll gesture — a swipe and its glide, a wheel spun without a pause — picks that scroller when it starts, skipping one already at its limit that way for the one around it (unless it says `overscroll: contain`), and keeps it until it ends, so content moving under a still pointer does not hand the rest of a swipe to what came under it (backlog F107). An offset is kept while the key is declared; an undeclared one is kept until the budget needs the room (1024 undeclared entries, longest-undeclared evicted first). |
|
|
144
|
-
| `pad`, `padX`, `padY`, `padL`, `padR`, `padT`, `padB` | `pad = n` or `pad = { all=, x=, y=, l=, r=, t=, b= }` | `pad_l`, `pad_r`, `pad_t`, `pad_b` | Padding; a frontend reports the names it saw and `PadShorthand::resolve` turns them into four edges — an edge falls back to its axis, an axis to the all-round `pad`, and the specific one always wins. |
|
|
145
|
-
| `rowCount` | `row_count` | `kui_row_count` | How many `index`ed rows this node's virtual list has, built or not. `uniformList` / `uniform_list` / `widgets::uniform_list` and `widgets::list` declare it on their container; a list composed by hand says it beside `scrollY`. What it buys: Select All (Cmd/Ctrl-A, the menu's row) inside a `selectable` virtual list selects the *data*, rows `0..rowCount`, rather than the rows the frame built, and the copy is a `selectionrange` ask whose `to.byte` is past the last row's length when that row is not built — cut it to the row. Without it Select All is the built rows, which is all the core can see. |
|
|
146
|
-
| `secureInput` (root box only) | `secure_input = true` (root table) | `kui_set_secure_input` | Declares that this frame wants the keyboard to this window kept from every other process while the window has it — macOS's Secure Keyboard Entry, what a terminal turns on at a password prompt (backlog F85). Frame state the way `alwaysOnTop` is, default false: declare it on every frame the prompt is up, and the frame that stops is what turns it off, so nothing has to remember to undo it. The runner owns the platform call and its balance: `EnableSecureEventInput` is process-wide and counted, and the runner holds one count while a window whose frame asked has the keyboard, giving it back when that window loses the keyboard, closes or stops asking, and at exit — Apple's rule, since while it is on no other process can read the keyboard at all (a launcher's hotkey, a text expander, an accessibility tool). Nothing on Windows or Linux, which have no such switch. A C host with its own loop reads the ask with `kui_secure_input_get` and makes the call itself. |
|
|
147
|
-
| `size` (text) | `size` | `KuiTextStyle.size` | Font size in logical px; the text style is constructed from it, so declare it for the other style props to apply at that size. |
|
|
148
|
-
| `title` (root box only) | `window_title` (root table) | `kui_window_title` | Declares the window title for this frame; the driver diffs and applies. |
|
|
149
|
-
| `tooltip="hint"` | `tooltip = "hint"` | `KuiSpec.tooltip` (`kui_tooltip` / `kui_tooltip_with` draw a hint that is not hover-gated) | Floats a hint below the node while hovered. All three effects — hover tracking, the accessible description, and the float itself — come from `PropsOut::apply_tooltip`, so no frontend can implement two of them; a Rust view has all three in `NodeSpec::tooltip` (`NodeSpec::apply_tooltip` is the spec half, for a caller that floats the hint itself). The `description` row is that middle effect on its own, for a hint that is spoken and never drawn. On a box or a `fragment` the float is the node's last child; a leaf holds no children — a `line`, `polygon`, `path`, `cells` grid, `image` or `edit` — and its hint floats beside it instead, anchored to it, and lands below its box the same way, out of every clip and flipping above near the window's bottom (backlog RG113; `PropsOut::for_leaf`). A leaf draws its description, which is the hint unless a `description` applied after it overwrote the slot. A `line`, `polygon` or `path` is hovered by its shape, so its hint shows while the pointer is on the stroke or inside the outline, not anywhere in its box. |
|
|
150
|
-
| `windows={[{ name, kind?, anchor?, width?, height?, activates? }]}` (root box only; `windows: (model) => [...]` in the loop config) | `windows = { { name=, kind=, anchor=, width=, height=, activates= } }` (root table) | `kui_window_declare` | Declares which windows exist this frame, by stable name (`docs/adr/0004-multi-window.md`). A window opens on the first frame any window's frame declares it — its config is read then and never again, since the user owns its geometry once it exists — and closes on the first frame none does. The driver drains the `Open` / `Close` that result, and the app sees `{kind:"window", phase, name, id}`. A window the user closed does not reopen while it is still declared: stop declaring it, then declare it again. `kind: "popup"` makes it a menu surface instead: borderless, off the taskbar, owned by the window that declared it and closed with it, placed in screen coordinates against `anchor` — the `{x, y, w, h}` an `onLayout` node reported — and non-activating unless `activates` says otherwise, so the field that opened it keeps the focus ring while the arrows walk the list. A press outside it or Escape raises `{kind:"dismiss", reason, name, id}` and closes nothing, exactly as a `modal` node's does: stop declaring the window. Reach for a popup only for the placements a float cannot make — a list taller than the window, a menu with nowhere in-window to go, a panel beside the app; everything else stays `fit` plus a `modal` float, which costs one tree instead of an OS surface. |
|
|
133
|
+
| JSX | Lua | C | Odin | description |
|
|
134
|
+
|---|---|---|---|---|
|
|
135
|
+
| `alwaysOnTop` (root box only) | `always_on_top = true` (root table) | `kui_set_always_on_top` | `kui.set_always_on_top` | Declares that this frame wants the window kept above every other app's — a floating palette, a picture-in-picture player, a timer (backlog C30). Frame state the way `title` is, applied by the driver on change and free on the frames it does not change, but with a default of false rather than "leave as-is": a frame that stops declaring it lowers the window again, so a pin button is a toggle on the app's own state and nothing has to remember to undo it. Whether the platform has a level to set is `env.window.always_on_top`, which is what the pin button should draw its state from — Wayland has no call for it at all, so there the window never moves and the reading says so; it is the driver's record of what it set, not a query, so a level the OS dropped afterwards (a fullscreen space, a tiling manager) is not reported. A popup keeps its own level whatever its owner declares. |
|
|
136
|
+
| `borderW`, `borderColor` | `border = { w=, color= }` | `border_w`, `border_color` | `Spec.border_w`, `Spec.border_color` | Border width and color (drawn inside the rect). |
|
|
137
|
+
| `dir="row" \| "column" \| "table"` | `row { }` / `column { }` / `grid { }` | `dir` (`KUI_ROW` / `KUI_COLUMN` / `KUI_TABLE`) | `Spec.dir` (`.Column` / `.Row` / `.Table`); `kui.row` and `kui.column` set it | Main axis; column is the default. `table` is a column whose rows' children line up in columns (the `table` element). |
|
|
138
|
+
| `float="below" \| "above" \| "parent" \| "viewport"` or `{ anchor, at, self, dx, dy, fit, clip }` | `float = "below"` or `float = { anchor=, at=, self=, dx=, dy=, fit=, clip= }` | `float_mode`, `float_anchor_x/y`, `float_self_x/y`, `float_dx/dy`, `float_fit`, `float_clip`; `kui_spec_float_preset` fills them from a preset name | `Spec.float`, a `Float` (`mode`, `anchor_x/y`, `self_x/y`, `dx/dy`, `fit`, `clip`); `kui.float_preset` fills one from a preset name | Out-of-flow positioning against the parent or the viewport; `fit` flips/clamps to stay on screen. A float escapes every ancestor's clip — a tooltip is not cut by the scroller it hangs from — unless it declares `clip` and is anchored to its parent (`parent`, `below`, `above`): then the parent's clip holds it as it holds a child, so a node on a `clip` canvas panned past the canvas's edge is cut there and cannot be hit past it; it still paints as a layer over its in-flow siblings. A `line`, `polygon` or `path` in its parent's box is always clipped this way. The four preset names resolve in `FloatConfig::preset`, and `anchor` takes any of them — an override left out keeps the preset's own value, so `{ anchor: "below", dx: 4 }` still hangs below with its 6px gap. A float is a layer of its own: above the in-flow tree and every float that opened before it, under every float that opened after, and hit-tested in the same order — so a tooltip that appears over an open menu is over it, and a popover over a scroller's bar takes the press there. A scroller's bars and the focus ring belong to the layer that owns them. There is no z-index; a float declared under a fresh key reopens on top (`docs/adr/0023-layers-stack-in-the-order-they-open.md`). |
|
|
139
|
+
| `imeOff` (root box only) | `ime_off = true` (root table) | `kui_set_ime_off` | `kui.set_ime_off` | Declares that this window takes the keyboard as keys, with the platform's input method off (backlog F125): no composition and no candidate window, and on a Mac no dead key waiting for the next and no press-and-hold — an input method too, so a held letter repeats instead of opening the accent picker, whatever the user's `ApplePressAndHoldEnabled` says. A `key` event's `text` is still the layout's character; what goes is everything the OS would have composed from it. What a modal editor's normal mode wants — `jjjj` is how one moves, and an IME left on eats the keymap — while its insert mode stops declaring it and gets accents, dead keys and the IME back. Frame state the way `alwaysOnTop` is, default false: declare it on every frame the mode wants it, and the frame that stops gives the input method back; the runner applies it to the window on change, never per frame, and a composition in progress when it turns off ends without a commit, as an empty `preedit`. The window's, not a node's: a stock editor focused under it composes nothing either. A popup's keys arrive through its owner, so the owner's declaration is the one they are read under. On Windows and Linux the window's IME is disabled the same way, and only that: their dead keys are the layout's, and still compose. A C host with its own loop reads the ask with `kui_ime_off_get` and applies it itself. |
|
|
140
|
+
| `index` | `index` | `kui_open_indexed` | `Spec.index`, a `Maybe(u64)`, which opens the node indexed | Stable identity by *data* index rather than by name: the key auto-keying would have given this node as the `i`th child, given to it wherever it actually sits. What a virtualised list is for — a view that builds rows 900..930 of ten thousand opens each with its own row number, so the row keeps its hover, focus, edit buffer and tweens as the built range slides over it, and a list that builds every row agrees with one that builds a screenful. Wherever `key` names a node this numbers it (a box, a `line`, a `cells`, a `fragment`); declared beside `key` the index wins. Indices and names are separate namespaces, so a spacer keyed `"lead"` cannot collide with row 0 — but two rows on one index do, exactly as two on one name would. |
|
|
141
|
+
| `key` | `key` | `kui_open_keyed` label | `Spec.key`, which opens the node keyed | Stable identity for retained state (scroll offsets, editors, transitions; keys are hashes of the path from the root). Retained state outlives the key's absence, under a budget on the states nobody declares (see `<edit>` and the overflow props). |
|
|
142
|
+
| `keyFocus` | `key_focus` | `kui_set_key_focus` | `Spec.key_focus`, which calls `kui.set_key_focus` on the node | Focuses this node (an `onKey` sink, an editor, any focusable node) when it starts being declared: declared every frame it takes focus once, so a later Tab press is not clobbered. Declaring it on the frame a `modal` stops being declared is how a view says where focus lands on the way out — the edge stands, and the focus the modal displaced is not handed back over it (`docs/adr/0003-modal-surfaces.md`, decision 4). To move focus at any time call the binding's focus verb (`ctx.focus`, `kui_focus`, `env.set_focus`). |
|
|
143
|
+
| `optionAsAlt="left"` — `"none"`, `"left"`, `"right"`, `"both"` (root box only) | `option_as_alt = "left"` (root table) | `kui_set_option_as_alt` (`KUI_OPTION_AS_ALT_*`) | `kui.set_option_as_alt` (an `Option_As_Alt`) | Declares which Option keys act as Alt in this window on macOS (backlog F113). On a Mac, Option composes: ⌥m types `µ`, and ⌥u, ⌥e, ⌥i, ⌥n and ⌥` are dead keys that start an accent and wait for the next key, so the press never reaches the app as a key and a keymap binding `<A-u>` never hears it. An Option key named here is Alt instead: it composes nothing and types nothing, and a key under it arrives as a chord of the key the layout prints unmodified — a terminal's "Option as Meta", an editor's Alt bindings. `"left"` or `"right"` leaves the other side composing, so a user keeps `ü` on one Option; `"both"` takes both; `"none"`, the default, is the Mac's own behaviour. Frame state the way `alwaysOnTop` is: declare it on every frame, and the frame that stops gives the Option keys back to the layout; the runner applies it to the window on change, never per frame. A popup's keys arrive through its owner, so the owner's declaration is the one they are read under. Nothing on Windows or Linux, whose Alt composes nothing. A C host with its own loop reads the ask with `kui_option_as_alt_get` and applies it itself. |
|
|
144
|
+
| `clip`, `scrollX`, `scrollY` | `clip`, `scroll_x`, `scroll_y` (`scroll` = `scroll_y`) | `overflow` bits `KUI_CLIP` \| `KUI_SCROLL_X` \| `KUI_SCROLL_Y` | `Spec.overflow`: `{.Clip}`, `{.Scroll_X}`, `{.Scroll_Y}` | Clip children; scroll (implies clip) with retained offsets and live scrollbars. A `radius` on the same node rounds the clip, so a rounded card does not show square corners poking out of it; nesting two rounded clippers keeps only the corners neither of them moved, and hit-testing stays rectangular. Every frontend ORs the same bits and hands them to `NodeSpec::overflow_bits`. The wheel goes to the scroller under the pointer on the axes it scrolls, and the rest of the notch to the one around it: a `scrollY` list inside a `scrollX` strip moves the strip on a sideways swipe (backlog DX13). A scroll gesture — a swipe and its glide, a wheel spun without a pause — picks that scroller when it starts, skipping one already at its limit that way for the one around it (unless it says `overscroll: contain`), and keeps it until it ends, so content moving under a still pointer does not hand the rest of a swipe to what came under it (backlog F107). An offset is kept while the key is declared; an undeclared one is kept until the budget needs the room (1024 undeclared entries, longest-undeclared evicted first). |
|
|
145
|
+
| `pad`, `padX`, `padY`, `padL`, `padR`, `padT`, `padB` | `pad = n` or `pad = { all=, x=, y=, l=, r=, t=, b= }` | `pad_l`, `pad_r`, `pad_t`, `pad_b` | `Spec.pad`: `kui.pad(16)`, `kui.pad(16, 8)`, or `{l = .., r = .., t = .., b = ..}` | Padding; a frontend reports the names it saw and `PadShorthand::resolve` turns them into four edges — an edge falls back to its axis, an axis to the all-round `pad`, and the specific one always wins. |
|
|
146
|
+
| `rowCount` | `row_count` | `kui_row_count` | `Spec.row_count`, a `Maybe(u64)`, which calls `kui.row_count` on the node | How many `index`ed rows this node's virtual list has, built or not. `uniformList` / `uniform_list` / `widgets::uniform_list` and `widgets::list` declare it on their container; a list composed by hand says it beside `scrollY`. What it buys: Select All (Cmd/Ctrl-A, the menu's row) inside a `selectable` virtual list selects the *data*, rows `0..rowCount`, rather than the rows the frame built, and the copy is a `selectionrange` ask whose `to.byte` is past the last row's length when that row is not built — cut it to the row. Without it Select All is the built rows, which is all the core can see. |
|
|
147
|
+
| `secureInput` (root box only) | `secure_input = true` (root table) | `kui_set_secure_input` | `kui.set_secure_input` | Declares that this frame wants the keyboard to this window kept from every other process while the window has it — macOS's Secure Keyboard Entry, what a terminal turns on at a password prompt (backlog F85). Frame state the way `alwaysOnTop` is, default false: declare it on every frame the prompt is up, and the frame that stops is what turns it off, so nothing has to remember to undo it. The runner owns the platform call and its balance: `EnableSecureEventInput` is process-wide and counted, and the runner holds one count while a window whose frame asked has the keyboard, giving it back when that window loses the keyboard, closes or stops asking, and at exit — Apple's rule, since while it is on no other process can read the keyboard at all (a launcher's hotkey, a text expander, an accessibility tool). Nothing on Windows or Linux, which have no such switch. A C host with its own loop reads the ask with `kui_secure_input_get` and makes the call itself. |
|
|
148
|
+
| `size` (text) | `size` | `KuiTextStyle.size` | `Text_Style.size` | Font size in logical px; the text style is constructed from it, so declare it for the other style props to apply at that size. |
|
|
149
|
+
| `title` (root box only) | `window_title` (root table) | `kui_window_title` | `kui.window_title` | Declares the window title for this frame; the driver diffs and applies. |
|
|
150
|
+
| `tooltip="hint"` | `tooltip = "hint"` | `KuiSpec.tooltip` (`kui_tooltip` / `kui_tooltip_with` draw a hint that is not hover-gated) | `Spec.tooltip` (`kui.tooltip` / `kui.tooltip_with` draw a hint that is not hover-gated) | Floats a hint below the node while hovered. All three effects — hover tracking, the accessible description, and the float itself — come from `PropsOut::apply_tooltip`, so no frontend can implement two of them; a Rust view has all three in `NodeSpec::tooltip` (`NodeSpec::apply_tooltip` is the spec half, for a caller that floats the hint itself). The `description` row is that middle effect on its own, for a hint that is spoken and never drawn. On a box or a `fragment` the float is the node's last child; a leaf holds no children — a `line`, `polygon`, `path`, `cells` grid, `image` or `edit` — and its hint floats beside it instead, anchored to it, and lands below its box the same way, out of every clip and flipping above near the window's bottom (backlog RG113; `PropsOut::for_leaf`). A leaf draws its description, which is the hint unless a `description` applied after it overwrote the slot. A `line`, `polygon` or `path` is hovered by its shape, so its hint shows while the pointer is on the stroke or inside the outline, not anywhere in its box. |
|
|
151
|
+
| `windows={[{ name, kind?, anchor?, width?, height?, activates? }]}` (root box only; `windows: (model) => [...]` in the loop config) | `windows = { { name=, kind=, anchor=, width=, height=, activates= } }` (root table) | `kui_window_declare` | `kui.window_declare` | Declares which windows exist this frame, by stable name (`docs/adr/0004-multi-window.md`). A window opens on the first frame any window's frame declares it — its config is read then and never again, since the user owns its geometry once it exists — and closes on the first frame none does. The driver drains the `Open` / `Close` that result, and the app sees `{kind:"window", phase, name, id}`. A window the user closed does not reopen while it is still declared: stop declaring it, then declare it again. `kind: "popup"` makes it a menu surface instead: borderless, off the taskbar, owned by the window that declared it and closed with it, placed in screen coordinates against `anchor` — the `{x, y, w, h}` an `onLayout` node reported — and non-activating unless `activates` says otherwise, so the field that opened it keeps the focus ring while the arrows walk the list. A press outside it or Escape raises `{kind:"dismiss", reason, name, id}` and closes nothing, exactly as a `modal` node's does: stop declaring the window. Reach for a popup only for the placements a float cannot make — a list taller than the window, a menu with nowhere in-window to go, a panel beside the app; everything else stays `fit` plus a `modal` float, which costs one tree instead of an OS surface. |
|
|
151
152
|
|
|
152
153
|
## Elements
|
|
153
154
|
|
|
154
|
-
| JSX | Lua | C | notes |
|
|
155
|
-
|
|
156
|
-
| `<box>` | `row { }`, `column { }` | `kui_open*` … `kui_close` | A container: every container prop applies. |
|
|
157
|
-
| `<box dir="table">` | `grid { }` | `kui_open*` with `dir = KUI_TABLE` | A column whose rows' children line up in columns (`docs/adr/0033-a-table-is-a-column-whose-cells-align.md`): its children are the rows, each row's in-flow children its cells, the nth cell of every row column n, and a column as wide as its widest cell — so a label column sits at its longest label with nothing measured and no width picked by hand, in every binding, since the alignment is the layout's and not a widget's. A cell's `width` says how its column sizes: `fit` (the default) and a fixed number are content the column's fit width is the max of; `grow` makes the whole column grow with the table, `grow` factors splitting the room the fit columns leave; a percent takes its cut of the row; and a column's `minWidth` / `maxWidth` are the strictest its cells declared. Fit columns that overflow the row are compressed toward their floors largest first, as a row's children are, unless the table scrolls x; a fixed column never is. A bare text is a cell too, held at its column's width, so a text straight inside a row is a column; an image straight in a row is a cell the same way, its box the column wide and its own aspect tall, the pixels meeting the box by its `fit` row (wrap an icon in a box to keep its own width). The rows are the table's `row` children, ordinary rows — give them `width="grow"` for the columns to grow into (a `fit` row sits at the columns' width) — with their own `gap` between cells, their own padding, background, click, hover and access rows; a row of a table never wraps (`wrap-ignored`). Anything else straight under the table — a text, a `column` section, another table — is a child with its own width and no cells. The table's own `fit` width is its columns', whatever its rows' sizing, so a table with no width is the aligned list; a `scrollX` table's rows are at least as wide as its columns, and it scrolls to them. Everything else is a column's: `gap` is the space between rows, `scrollY` scrolls them, a float in a row is not a cell. Spelled `grid { }` in Lua, since `table` is Lua's own. |
|
|
158
|
-
| `<text>` with `<span bold italic underline strikethrough bg bgRadius color>` children | `text("s", {…})`, `text({ "a", { "b", bold = true, underline = true, bg = 0x.., bg_radius = 4 } })` | `kui_text`, `kui_rich_text` | Plain or rich text; spans shape as one paragraph, so wrapping crosses style boundaries. A text is content plus a style and no box of its own, so the rows it reads are the style rows (`size`, `lineHeight`, `color`, `family`, `font`, `wrap`, `maxLines`, `ellipsis`, `underline`, `strikethrough`, `features`) and nothing else: a container row, an access row (`label`, `role`, `live`), `key` or `onClick` on a text is dropped with an `unknown-prop` warning naming the rows it does take — put them on the box around it. `wrap`, `maxLines` and `ellipsis` control line breaking. A span's `bg` is a background behind its glyphs alone, one rect per line it spans, so it follows the span across a wrap the way a box around a run cannot. With `bgRadius` (`bg_radius` in Lua, `KuiSpan.bg_radius` in C, `Span::bg_radius` in Rust; logical px) the background is rounded and joined into one shape with every rounded background of the same colour and radius it meets: a piece whose edge touches it exactly on the line above or below and overlaps it sideways, or that meets it end to end on its own line, in this text or another. Its corners are then convex where a line reaches past its neighbour, a fillet where it falls short, and round where nothing meets it — a selection over many rows, or over the wrapped lines of a paragraph, is one rounded outline, joined after every text of the frame is laid out and painted, so it is never a frame behind. Nothing names the shape: two that touch are one; `underline` and `strikethrough` on a span or on the whole text are lines where the face puts them. A text with no line breaks that is 4096 bytes or longer (and no `maxLines` or `ellipsis`), plain or spans alike, is shaped in ~1 KB chunks as they come on screen, so a minified bundle or a log line with a blob in it costs the screenful it shows and a keystroke into it — or a span moving along it, an editor's caret — costs the chunk it lands in; wrapped, the rows are broken from the chunks' positions, so a 100k-character paragraph costs the rows it shows. Its size is estimated from the first chunk until the rest shape (exact under monospace), and the access tree carries its value without its runs. |
|
|
159
|
-
| `<button onClick key\|index label description tooltip disabled accent>` | `button { label=, on_click=, key= \| index=, text=, description=, tooltip=, disabled=, accent= }` | `kui_button`, `kui_button_with` | The stock button: `widgets::button_spec(&theme, &metrics)` — the theme's accent trio as its three backgrounds, declared on the node and resolved by the core — keyed by its text (`key` overrides). It paints from the palette like every stock widget (backlog AR41): the OS's accent where the host reports one, the app's where it set or pinned one, kui's blue otherwise; the label goes black or white by the background's luminance. Its look is its spec, so the layout and paint rows are closed — declared, they are dropped with an `unknown-prop` warning naming the rows it does read — and those are the access rows: `label` when the text is not the name, `description`, `tooltip`, and `disabled` (inert, and dimmed to half). The one paint row it takes is `accent`, which on a button changes nothing (it is the accent already) and is kept for the box's sake. In Lua `label` is the name and the text both unless `text` says otherwise; in C the rows ride a `KuiSpec` whose other fields `kui_button_with` ignores. A button that needs any other row is a box with `role="button"` and the same rows spelled out. |
|
|
160
|
-
| `<edit key initial multiline autofocus>`, `<input label initial>` | `edit { key=, initial=, … }`, `input { label=, initial= }` | `kui_text_edit`, `kui_text_input` | Retained editor state by key; read it back with `editText(key)` after a `changed` event. `initial` seeds a new editor only — a key declared again keeps the draft the user typed, and `setEditText(name, text)` is what resets one (it leaves the caret at the end). Name it by the label its `key` prop declares — `setEditText('note', text)` — or by the hex key an event carried. It reaches an editor that does not exist yet: the text is held for the frame that declares that name and seeds it there, over `initial`, so the `update` that opens a rename field can fill it in the same turn, which is what the label spelling is for — the hex key comes from an event an editor being opened has not fired. A name nothing declares on that frame drops its text with an `edit-text-without-editor` warning. A single-line editor is a field and a `multiline` one a document, which decides how each is laid out as well as how it reads: a field takes one line whatever its box, sizes to the text it holds when its width is `fit`, and scrolls that line under the caret when it is not, while a document wraps to its box. The one exception is a field with `wrap` declared (`wrap="word"` or `"glyph"`): it folds to its width the way a document does and keeps a field's keyboard — Enter still submits, a newline is still never admitted, the caret still opens at the end — so a rename field breaks where the label it renames breaks, and with `width="fit"` plus `maxWidth` it sizes to its wrapped draft on the keystroke frame. A single-line editor opens with the caret after its seeded text, as a native field does; a multiline one is a document and opens at its top — a held `setEditText` is the call, not a seed, so it opens at the end either way. State is kept while the key is declared; an undeclared one is kept until the budget needs the room (256 undeclared editors, longest-undeclared evicted first). `autofocus` asks once: the editor takes focus on the frame the flag starts being declared — a new editor, or one whose flag just turned on — and only while nothing holds focus, so a blur afterwards stands and a focused control is never robbed (`docs/adr/0022-focus-regions.md`, decision 9); `focus(key)` is the call for taking it at any other time. |
|
|
161
|
-
| `<select label options={[…]} current>` | `dropdown { label=, options={…}, current= }` | `kui_select` | The stock select (`widgets::select_items`, backlog F72): a field showing the choice in force that, clicked, opens the core's own menu of the options under it with the current one checked — the menu a right-click opens, drawn in the frame or the platform's where the host shows menus itself, dismissed by Escape or a press outside, its rows walked by the arrows and read as a menu. `label` is the key and the accessible name both; `options` is a list whose entries are strings (an option by its label, posting it) or menu-item objects `{ label, id, enabled }` (posting `id`), and a `{ role: "separator" }` is a separator; `current` is the index in force, counted from 0 in JSX and C and from 1 in Lua, or none — one past the options or on a separator is none, with a `select-current-ignored` warning on the field; an empty `options` is refused, and a key of an option object no row reads (`disabled`, where the key is `enabled`) is an `unknown-prop` warning. The app holds no open state: the choice arrives as the `menu` event a menu row posts, on the field's key — `{kind: "menu", role: "custom", item: <the option>}` — and drawing the field again with the new `current` is the whole loop. A reader hears a button named by the field, described by its choice, expanded while the menu is open. Its look is its spec, so it reads no other row: a layout, paint or access row on it is dropped with an `unknown-prop` warning. Lua spells it `dropdown`, since `select` is Lua's own. |
|
|
162
|
-
| `<checkbox checked mixed onClick key label description tooltip disabled>text</checkbox>` | `checkbox { label=, checked=, mixed=, on_click=, key=, text=, description=, tooltip=, disabled= }` | `kui_checkbox` | The stock checkbox (`widgets::toggle_with`, ADR 0034): a box drawn from the state the view declares — `checked`, or `mixed` for the select-all box over a list some of whose rows are selected, drawn as a dash and read as mixed — and its label beside it, keyed by its text (`key` overrides). The state is the app's: a press by the pointer, Space, Enter or assistive technology posts `onClick`, and the view flips its model and draws it again. Its look is its spec, so the layout and paint rows are closed and dropped with an `unknown-prop` warning; the rows it reads are its state and the access rows. In Lua `label` is the name and the text both unless `text` says otherwise. The box is the metrics' control text plus one (16 px comfortable), so `compact` and `scaled` move it with the stock button. |
|
|
163
|
-
| `<radio checked onClick key label description tooltip disabled>text</radio>` | `radio { label=, checked=, on_click=, key=, text=, description=, tooltip=, disabled= }` | `kui_radio` | The stock radio (`widgets::toggle_with`, ADR 0034): a circle drawn from `checked`, and its label, keyed by its text. Put radios in a `radioGroup`, which makes them one Tab stop whose arrows, Home and End move the choice and press the radio they land on (ADR 0007), so radios whose `onClick` each set the choice answer the keyboard with no more code. The state is the app's, as a checkbox's is; the rows are the checkbox's, `mixed` aside. |
|
|
164
|
-
| `<radioGroup label>…radios…</radioGroup>` | `radio_group { label=, … }` | `kui_radio_group_open` … `kui_close` | A container of radios (`widgets::radio_group_with`, ADR 0034): the `radioGroup` role, named by its `label`, laid out as a column with the stock gap — a `dir="row"` lays the radios across, and its arrows run across with it. It reads every box row; the role and the name are its own whatever the rows say. |
|
|
165
|
-
| `<switch checked onClick key label description tooltip disabled>text</switch>` | `switch { label=, checked=, on_click=, key=, text=, description=, tooltip=, disabled= }` | `kui_switch` | The stock switch (`widgets::toggle_with`, ADR 0034): a track and a knob drawn from `checked`, the knob sliding across when it changes, and its label; read as a switch, on or off. The state is the app's, as a checkbox's is; the rows are the checkbox's, `mixed` aside. |
|
|
166
|
-
| `<slider label valueNow valueMin valueMax valueStep valueText onChange width description tooltip disabled/>` | `slider { label=, value_now=, value_min=, value_max=, value_step=, value_text=, on_change=, width=, … }` | `kui_slider` | The stock slider (`widgets::slider_with`, ADR 0034): a track, a fill to `valueNow` and a thumb, as wide as a menu (`width` sizes it), keyed by its `label`, which is also its accessible name. With `onChange` the core does the arithmetic: a press proposes the value under the pointer, a drag each new step, the arrows one `valueStep`, PageUp / PageDown ten, Home / End the ends, all clamped to `valueMin`..`valueMax` (0..100 unset) and snapped to the step, as `{kind:"change", value, phase:"move"\|"end", tag}`. The value is proposed, never applied: the view stores it and declares it as `valueNow`. Its look is its spec, so the rows it reads are the value rows, the access rows and its width. |
|
|
167
|
-
| `<image src={id} sampling fit>` | `image { id=, sampling=, fit= }` | `kui_image`, `kui_image_with` | A registered RGBA image. Sizing: `width="fit"` takes the pixel size, a fit height against a resolved width keeps the aspect, `radius` rounds it. Two rows say how the pixels meet the box (`docs/adr/0025-the-image-is-the-canvas.md`): `sampling` is `linear` (the default) or `nearest` — pixel art, an emulator, a data grid that must stay square under zoom; `fit` is `fill` (the default: the pixels stretch to the box), `contain` (the largest rect of the image's aspect that fits, centred, the rest of the box showing what is behind) or `cover` (the box filled and the pixels that do not fit cropped, centred). The box — its layout, its hit region, its access rect — is the same in every mode. The pixels come from the atlas, or from a texture of the image's own once `updateImage` has replaced them or when no atlas page could hold them; the node cannot tell and need not. |
|
|
168
|
-
| `<polygon points={[[x,y],…]} bg/>` | `polygon { points={{x,y},…}, bg= }` | `kui_polygon` | A filled polygon through up to eight `points`, the fill in `bg` (`docs/adr/0025-the-image-is-the-canvas.md`, decision 6): an arrowhead, a pie slice, the area under a curve. Placed as a `line` is — always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box a pixel out on each side, so it takes no room in a row or column, and painted in the parent's layer at its place in the tree, over the siblings before it and under those after (backlog F123). A polygon in its parent's box space is held by the parent's clip as a child is, its hit region with it, so it is cut at a scroller's edge with the row it is drawn in; a declared float is held that way only when it declares `clip` with a parent anchor, and a `float="viewport"` polygon escapes (backlog F78, `docs/adr/0010-a-segment-primitive.md` decision 5). `transition` eases the fill and, with `slide`, its position. The outline may be concave; a self-intersecting one fills even-odd, its overlaps unfilled. Hit by its outline (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press inside the outline hits it and one in its box past the outline falls through, so a pie's wedges need no hit boxes; with none it takes no input and has no access row, and with input it derives one as a box would (a clickable wedge is a button), so name it. A ninth point and later are dropped with `polygon-points-truncated`; fewer than three draw nothing; no `bg`, no fill. On the wire it is one `fragment` quad painted by a WGSL function the core registers itself, so a host that draws the list gets its source from `kui_fragment_source` like any other; what it costs is that quad and one pipeline switch per run of polygons. A stroked outline is a closed `line` over it. |
|
|
169
|
-
| `<path d="M … Z" bg width color fillRule rotate pivot dash dashOffset/>` | `path { d = "M … Z", bg=, width=, color=, fill_rule=, rotate=, pivot=, dash=, dash_offset= }` | `kui_path` | Any outline — SVG's `d`, a pie wedge with a round arc, a map's region, an icon — filled with `bg` by `fillRule` (`nonzero`, the default, or `evenodd`) and stroked `width` wide in `color` when `width` is given, the stroke over the fill (`docs/adr/0040-a-path-is-a-mask-in-the-atlas.md`). `d` is SVG path data (`M L H V C S Q T A Z`, absolute or relative), parsed by one parser in the core, so every binding draws the same shape; one that does not parse raises `path-malformed` and draws nothing. JSX also takes `d` as a flat number array of op codes and operands, Lua the same as `ops`, and C only that form (`kui_path_parse` turns a string into it). Placed as a `line` is — always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box two pixels out on each side (half the stroke's width further), so it takes no room in a row or column, held by the parent's clip as a child is and painted in the parent's layer at its place in the tree (backlog F123). `transition` eases the fill and, with `slide`, its position; the stroke's colour does not tween, as a box's border does not. Hit by its outline under the fill rule (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press inside hits it and one in its box past the outline falls through, so a pie's wedges need no hit boxes; a stroke with no fill is hit by its stroke as a line is; with input it derives an access row as a box would (a clickable wedge is a button), so name it. On the wire it is one glyph-mask quad per paint, fill and stroke: the outline is rasterized once per shape, scale and quarter-pixel position into the glyph atlas and tinted like a glyph, so a host that draws text draws paths, and nothing is re-rasterized for a colour tween, a hover or a slide. The fill bleeds half a pixel, so two paths sharing an edge meet without the background showing through; a chart that wants separators gaps its own geometry. A mask a quarter of the biggest atlas page or more, or a path whose ops change twice within a few frames, draws from a texture of its own instead (a `texture` quad), and one past 8192 px on a side draws nothing, with `path-too-large`. `rotate` turns the path, in turns clockwise, about `pivot` — a point in the path's own coordinates, the centre of its box without one (`docs/adr/0041-a-mask-turns-about-its-centre.md`): the turn is the quad's and not the mask's, so a path that only turns — a spinner's arc about its circle's centre — is rasterized once and stays in the atlas at every angle. A path with `rotate` or `pivot` is boxed by the square the turn sweeps, its mask centred on the pivot on a whole pixel, and it is hit where it is drawn; `rotate` does not tween. `dash` and `dashOffset` cut the stroke into marks and gaps as a `line`'s do (backlog V2) — lengths as seen, round-capped marks, a gap the dots overlap closed — restarting at every subpath as SVG's do; the pattern is part of the stroke's mask, so a dashed stroke costs what a solid one does, a stroke with no fill is still hit along its gaps, and a `dashOffset` that changes every frame is a shape that changes every frame: the path leaves the atlas for a texture of its own while it marches. |
|
|
170
|
-
| `<fragment src={id} image={id} params={[…]} animate>` | `fragment { id=, image=, params={…}, animate= }` | `kui_fragment`, `kui_fragment_with` | A box a registered WGSL function paints (`docs/adr/0015-a-fragment-element-and-the-painter-it-is-not.md`): a conic or a moving gradient, rings, noise, shimmer — anything the paint vocabulary has no prop for. An ordinary node otherwise — it lays out, rounds, clips, fades, takes input and holds children, which paint over it — but with **no intrinsic size**, so give it a `width`/`height` or `fill` or it is zero by zero. `src` is a handle from `add_fragment`, which validates the source and warns rather than minting one that cannot compile. `params` is up to sixteen numbers the shader reads as four `vec4<f32>`; more are dropped with a warning. `image` is a registered image the function reads — `kui_sample(uv)` (bilinear) and `kui_sample_nearest(uv)` return its texels at `uv` in `[0,1]²`, and `in.image` is its texel rect, `zw` the size — which is what makes a replaced image a waveform, a heatmap, a 50k-point line or an image effect from one quad (`docs/adr/0025-the-image-is-the-canvas.md`, decision 7); the core binds the atlas or the image's own texture, whichever holds it, and a fragment whose image is not live draws nothing, as one whose `src` is not does. `animate` asks for a frame every frame, which is what a fragment that reads `time` needs and what a still one must not declare. |
|
|
171
|
-
| `<cells rows cols cells={Uint32Array} cursorAt={[row, col]} cursorShape cursorColor size family lineHeight/>` | `cells { rows=, cols=, lines={"row text", …}, runs={{row, col, len, fg, bg, flags}, …}, cursor_at={row, col}, cursor_shape=, cursor_color=, size=, family= }` | `kui_cells` | A terminal's screen as one node (backlog C20): `rows × cols` cells, each a character, a foreground and background as `0xRRGGBBAA` (0 = no background), and attribute bits — 1 bold, 2 italic, 4 underline, 8 strikethrough, 16 wide (the glyph spans this cell and the next, which the app leaves blank), 32 the underline is a wave (a terminal's undercurl, SGR 4:3) and 64 dotted (SGR 4:4), either implying it — plus, optionally, the underline's own colour (SGR 58), 0 for the foreground (backlog K4). A glyph is shaped once per character and style variant and thereafter placed at `col × cell_w` without shaping, so a screen whose every cell is new each frame costs what a still one costs (~60 µs for 200 × 50). The cell width is `M`'s advance in the style's font snapped to whole pixels, the height its `lineHeight`; a cell is a cell, so ligatures never form. A character the family has no glyph for is asked of a monospaced face before the platform's fallback list, shaped smaller where it is still wider than its cells (two under wide), and drawn in their middle (backlog F120); the private use area's icons are left as they fall. Box drawing and block elements (U+2500–U+259F) and the Powerline separators (U+E0B0–U+E0BF: the arrows, and the Powerline Extra half circles and wedges) are not shaped at all but drawn from the cell box — a font's are its own line box tall, a cell is `lineHeight` tall, and through the font every `│` was a dash with a gap under it (backlog F66) and a rounded cap a fallback font's squiggle (F112) — so a TUI's frames and rounded rows are seamless in any font, and bold does not thicken a light line (the set has its heavy variants). JSX passes the cells as a `Uint32Array` (or number array) of four entries per cell — codepoint, fg, bg, flags — or five, with the underline colour, in row-major order; Lua a string per row in `lines` plus `runs` of `{row, col, len, fg, bg, flags, ul}` over them (a run's fg, bg or ul of 0 keeps the default: the style's colour, no background, the foreground); C a `KuiCell` array with `ul`. `cursorAt` (`cursor_at`) names a cell to paint under its glyph in `cursorColor` as a `block` (default), `bar` or `underline` — its own name, since `cursor` is the pointer shape. `originLine` (`origin_line`) is the absolute line number of row 0: a grid is one screenful of the app's own history, so a row number means a different line after every scroll, and stamping where the screen sits is what lets a selection keep its ends across one (`docs/adr/0017-selection-as-a-scope.md`). Saying nothing is 0, and a selection then holds only while the screen does not move. The node's own rows apply — an `onKey` makes it the terminal's sink, an `onClick` or `onDrag` carries `cell: {row, col}` on its events — and its access row is `terminal`, the rows joined as its value. |
|
|
172
|
-
| `<line from={[x,y]} to={[x,y]} width color/>`, `<line points={[[x,y],…]} curve dash={[6, 4]} dashOffset/>` | `line { from={x,y}, to={x,y}, width=, color= }`, `line { points={{x,y},…}, curve=true, dash={6, 4}, dash_offset= }` | `kui_line`, `kui_polyline` | A round-capped stroke: one segment, a polyline through `points`, or a smooth curve through them with `curve`. Always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box, so it takes no room in a row or column — but a float for the room alone: in its parent's box space it paints in the parent's layer at its place in the tree, over the siblings declared before it and under those after, as a child does, and opens no layer of its own (backlog F123; a connector meant to sit under two cards is declared before them). A stroke in its parent's box space is held by the parent's clip as a child is, its hit region with it, so it is cut at a scroller's edge with the row it is drawn in; a declared float is held that way only when it declares `clip` with a parent anchor, and a `float="viewport"` stroke escapes, and is a layer of its own (backlog F78, `docs/adr/0010-a-segment-primitive.md` decision 5). `width` is the stroke width (default 1) and `color` the stroke colour; `transition` eases the colour, and with `slide` beside it the stroke's position too — the points ride its box, so a stroke whose ends all move together slides with them, while one whose ends move apart resizes at once (a canvas of floats eases everything or nothing, connectors included). Hit by its shape (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press within half the width of any piece hits it — at least 4 px of grab, so a hairline is a target — and a press elsewhere in its bounding box falls through to what is under; with none it takes no input and has no access row, and with input it derives one as a box would (a clickable connector is a button), so name it. What it costs: one quad per segment, and a curve is flattened in the core at one piece per 6 logical px of chord (at most 32 per span) — fixed rather than tolerance-driven so every binding gets the same pieces and the corpus can pin them — so a nine-point curve over ~50 px spans is ~60 quads, and a `quadCount` budget should expect it. `dash` cuts the stroke into marks and gaps (backlog V2): one length (marks and gaps alike), a mark and a gap, or four lengths for a dash-dot, in px **as seen** — every mark is a short stroke with the stroke's round caps, so `dash` 6, 4 is 6 px of ink and 4 px of nothing at any width up to 6 (SVG's `stroke-dasharray` measures the centre line instead, so with round caps its `4 4` at a width of 4 is solid; this pattern is SVG's `mark − width, gap + width`). A mark no longer than the stroke is wide is a dot as wide as the stroke, in the same period, so its gap is that much shorter; where a mark and its gap together come to no more than the width the dots meet and the gap closes — the marks either side of it are one, and a pattern with no gap left, `dash` 2, 2 at a width of 8, draws solid (backlog RG118). The pattern runs along the stroke's whole length, so it keeps its phase round the corners of a polyline and the pieces of a curve, and `dashOffset` starts that far into it — growing it moves the marks towards the first point, a marquee's marching ants; neither tweens. A pattern with no gap, a mark and gap under a physical pixel together, or more than 16384 marks draws solid. A dashed stroke is hit along its whole length, gaps included, and costs a quad per mark per piece the mark lies on. |
|
|
173
|
-
| `<titlebar title>` or `<titlebar>…</titlebar>` | `titlebar { title= }` / `titlebar { … }` | `kui_titlebar`, `kui_titlebar_with` | Adaptive titlebar for custom chrome: drag strip, native-control inset, window buttons. |
|
|
174
|
-
| `<menuBar menu={[{ label, items: [{ label, id?, role?, accel?, enabled?, checked? }] }]}/>` | `menu_bar { menu = { { label=, items= { { label=, id=, role=, accel=, enabled=, checked= } } } } }` | `kui_menu_bar` | The application menu (`docs/adr/0018-a-menu-bar-the-app-declares.md`): `menu` is what it *is*, and where this element sits is where its titles go when they have to be drawn in the window. One call and not two, because declaring the menu and placing the strip are one decision. It draws **nothing** where the platform owns the bar — macOS, where the driver hands the same declaration to the OS — so the frame has still said what the app's menu is and the strip simply is not there; that is the contract `windowButtons` has under native decorations, and it is what makes one view portable. Its rows are the rows a context menu has: the same `role`s the core performs itself (`copy`, `paste`, `selectAll`, `cut`, `lookUp`), the same `id` payload, the same `accel` text, plus `checked` for a setting — and choosing one posts the same `{kind:"menu", role, item}` event, so an app handles one thing whichever menu it came from. Declared every frame and diffed: an unchanged menu costs a comparison, and an empty list takes it away. An accelerator kui can parse is rewritten into the platform's spelling, so `"mod+s"` reads as `⌘S` on macOS and `Ctrl+S` elsewhere and binds that key in the platform's own bar. On macOS the first menu is the application menu, which the OS titles with the app's own name whatever the label says. While a menu is open the bar is the frame's modal scope, so hovering across the titles moves the open menu, a press on the open title closes it, and Escape or a press in the app below closes it and reaches nothing else. |
|
|
175
|
-
| `<windowButtons/>` | `window_buttons()` | `kui_window_buttons` | Just the min/max/close buttons, for fully custom titlebars. |
|
|
176
|
-
| `<tooltip value="hint"/>` / `<tooltip>…</tooltip>` nodes, or the `tooltip="hint"` prop (see composites) | `tooltip("hint")` / `tooltip { … }` nodes, or the prop | `kui_tooltip`, `kui_tooltip_with` | A float hanging below the parent; the node form always draws, the prop form is hover-gated. |
|
|
177
|
-
| `<latencyGraph/>`, `<latencyHud at/>` | `latency_graph()`, `latency_hud { at= }` | `kui_latency_graph`, `kui_latency_hud` | Per-phase frame timing (windowed drivers fill it; headless shows the chrome empty). |
|
|
178
|
-
| `<audio src={id} loop volume paused finish tag/>` | `audio { src=, loop=, volume=, paused=, finish=, tag= }` | `kui_audio` | A playback retained by node key: present = playing (once, or looped), gone = stopped; `volume` / `paused` apply live, a changed `src` restarts; a `tag` brings back `{kind:"sound", phase:"ended", tag}`. Draws nothing. `finish` changes what *gone* means: the node's removal releases the playback rather than stopping it, so a one-shot plays to its end and the view need not know the asset's length to declare the node for it (a loop still stops on removal — there is no end to reach — and a paused playback released has nothing to finish). Without it, the way to play a sound whole is to hold the node declared until the `tag`'s `ended` message arrives. A released playback is not free: it holds one of the device's 128 voices until its file ends, and the 129th play is refused — reported as a `playback-refused` warning and, for a `tag`, `phase: "refused"` rather than a wait that never returns. In the app's units, voices held = sound length × release rate: a 1.4 s chime released four times a second holds 6 of the 128 at any moment, a 10 s ambience released once a second holds 10, and every playback still declared (a loop included) counts beside them — a `refused` `sound` event is what arriving at 128 sounds like. |
|
|
155
|
+
| JSX | Lua | C | Odin | notes |
|
|
156
|
+
|---|---|---|---|---|
|
|
157
|
+
| `<box>` | `row { }`, `column { }` | `kui_open*` … `kui_close` | `kui.box` / `kui.row` / `kui.column` in an `if`, closed at its end; `kui.open` … `kui.close` unscoped | A container: every container prop applies. |
|
|
158
|
+
| `<box dir="table">` | `grid { }` | `kui_open*` with `dir = KUI_TABLE` | `kui.box` with `dir = .Table` | A column whose rows' children line up in columns (`docs/adr/0033-a-table-is-a-column-whose-cells-align.md`): its children are the rows, each row's in-flow children its cells, the nth cell of every row column n, and a column as wide as its widest cell — so a label column sits at its longest label with nothing measured and no width picked by hand, in every binding, since the alignment is the layout's and not a widget's. A cell's `width` says how its column sizes: `fit` (the default) and a fixed number are content the column's fit width is the max of; `grow` makes the whole column grow with the table, `grow` factors splitting the room the fit columns leave; a percent takes its cut of the row; and a column's `minWidth` / `maxWidth` are the strictest its cells declared. Fit columns that overflow the row are compressed toward their floors largest first, as a row's children are, unless the table scrolls x; a fixed column never is. A bare text is a cell too, held at its column's width, so a text straight inside a row is a column; an image straight in a row is a cell the same way, its box the column wide and its own aspect tall, the pixels meeting the box by its `fit` row (wrap an icon in a box to keep its own width). The rows are the table's `row` children, ordinary rows — give them `width="grow"` for the columns to grow into (a `fit` row sits at the columns' width) — with their own `gap` between cells, their own padding, background, click, hover and access rows; a row of a table never wraps (`wrap-ignored`). Anything else straight under the table — a text, a `column` section, another table — is a child with its own width and no cells. The table's own `fit` width is its columns', whatever its rows' sizing, so a table with no width is the aligned list; a `scrollX` table's rows are at least as wide as its columns, and it scrolls to them. Everything else is a column's: `gap` is the space between rows, `scrollY` scrolls them, a float in a row is not a cell. Spelled `grid { }` in Lua, since `table` is Lua's own. |
|
|
159
|
+
| `<text>` with `<span bold italic underline strikethrough bg bgRadius color>` children | `text("s", {…})`, `text({ "a", { "b", bold = true, underline = true, bg = 0x.., bg_radius = 4 } })` | `kui_text`, `kui_rich_text` | `kui.text`, `kui.rich_text` | Plain or rich text; spans shape as one paragraph, so wrapping crosses style boundaries. A text is content plus a style and no box of its own, so the rows it reads are the style rows (`size`, `lineHeight`, `color`, `family`, `font`, `wrap`, `maxLines`, `ellipsis`, `underline`, `strikethrough`, `features`) and nothing else: a container row, an access row (`label`, `role`, `live`), `key` or `onClick` on a text is dropped with an `unknown-prop` warning naming the rows it does take — put them on the box around it. `wrap`, `maxLines` and `ellipsis` control line breaking. A span's `bg` is a background behind its glyphs alone, one rect per line it spans, so it follows the span across a wrap the way a box around a run cannot. With `bgRadius` (`bg_radius` in Lua, `KuiSpan.bg_radius` in C, `Span::bg_radius` in Rust; logical px) the background is rounded and joined into one shape with every rounded background of the same colour and radius it meets: a piece whose edge touches it exactly on the line above or below and overlaps it sideways, or that meets it end to end on its own line, in this text or another. Its corners are then convex where a line reaches past its neighbour, a fillet where it falls short, and round where nothing meets it — a selection over many rows, or over the wrapped lines of a paragraph, is one rounded outline, joined after every text of the frame is laid out and painted, so it is never a frame behind. Nothing names the shape: two that touch are one; `underline` and `strikethrough` on a span or on the whole text are lines where the face puts them. A text with no line breaks that is 4096 bytes or longer (and no `maxLines` or `ellipsis`), plain or spans alike, is shaped in ~1 KB chunks as they come on screen, so a minified bundle or a log line with a blob in it costs the screenful it shows and a keystroke into it — or a span moving along it, an editor's caret — costs the chunk it lands in; wrapped, the rows are broken from the chunks' positions, so a 100k-character paragraph costs the rows it shows. Its size is estimated from the first chunk until the rest shape (exact under monospace), and the access tree carries its value without its runs. |
|
|
160
|
+
| `<button onClick key\|index label description tooltip disabled accent>` | `button { label=, on_click=, key= \| index=, text=, description=, tooltip=, disabled=, accent= }` | `kui_button`, `kui_button_with` | `kui.button` | The stock button: `widgets::button_spec(&theme, &metrics)` — the theme's accent trio as its three backgrounds, declared on the node and resolved by the core — keyed by its text (`key` overrides). It paints from the palette like every stock widget (backlog AR41): the OS's accent where the host reports one, the app's where it set or pinned one, kui's blue otherwise; the label goes black or white by the background's luminance. Its look is its spec, so the layout and paint rows are closed — declared, they are dropped with an `unknown-prop` warning naming the rows it does read — and those are the access rows: `label` when the text is not the name, `description`, `tooltip`, and `disabled` (inert, and dimmed to half). The one paint row it takes is `accent`, which on a button changes nothing (it is the accent already) and is kept for the box's sake. In Lua `label` is the name and the text both unless `text` says otherwise; in C the rows ride a `KuiSpec` whose other fields `kui_button_with` ignores. A button that needs any other row is a box with `role="button"` and the same rows spelled out. |
|
|
161
|
+
| `<edit key initial multiline autofocus>`, `<input label initial>` | `edit { key=, initial=, … }`, `input { label=, initial= }` | `kui_text_edit`, `kui_text_input` | `kui.text_edit`, `kui.text_input` | Retained editor state by key; read it back with `editText(key)` after a `changed` event. `initial` seeds a new editor only — a key declared again keeps the draft the user typed, and `setEditText(name, text)` is what resets one (it leaves the caret at the end). Name it by the label its `key` prop declares — `setEditText('note', text)` — or by the hex key an event carried. It reaches an editor that does not exist yet: the text is held for the frame that declares that name and seeds it there, over `initial`, so the `update` that opens a rename field can fill it in the same turn, which is what the label spelling is for — the hex key comes from an event an editor being opened has not fired. A name nothing declares on that frame drops its text with an `edit-text-without-editor` warning. A single-line editor is a field and a `multiline` one a document, which decides how each is laid out as well as how it reads: a field takes one line whatever its box, sizes to the text it holds when its width is `fit`, and scrolls that line under the caret when it is not, while a document wraps to its box. The one exception is a field with `wrap` declared (`wrap="word"` or `"glyph"`): it folds to its width the way a document does and keeps a field's keyboard — Enter still submits, a newline is still never admitted, the caret still opens at the end — so a rename field breaks where the label it renames breaks, and with `width="fit"` plus `maxWidth` it sizes to its wrapped draft on the keystroke frame. A single-line editor opens with the caret after its seeded text, as a native field does; a multiline one is a document and opens at its top — a held `setEditText` is the call, not a seed, so it opens at the end either way. State is kept while the key is declared; an undeclared one is kept until the budget needs the room (256 undeclared editors, longest-undeclared evicted first). `autofocus` asks once: the editor takes focus on the frame the flag starts being declared — a new editor, or one whose flag just turned on — and only while nothing holds focus, so a blur afterwards stands and a focused control is never robbed (`docs/adr/0022-focus-regions.md`, decision 9); `focus(key)` is the call for taking it at any other time. |
|
|
162
|
+
| `<select label options={[…]} current>` | `dropdown { label=, options={…}, current= }` | `kui_select` | `kui.select` | The stock select (`widgets::select_items`, backlog F72): a field showing the choice in force that, clicked, opens the core's own menu of the options under it with the current one checked — the menu a right-click opens, drawn in the frame or the platform's where the host shows menus itself, dismissed by Escape or a press outside, its rows walked by the arrows and read as a menu. `label` is the key and the accessible name both; `options` is a list whose entries are strings (an option by its label, posting it) or menu-item objects `{ label, id, enabled }` (posting `id`), and a `{ role: "separator" }` is a separator; `current` is the index in force, counted from 0 in JSX and C and from 1 in Lua, or none — one past the options or on a separator is none, with a `select-current-ignored` warning on the field; an empty `options` is refused, and a key of an option object no row reads (`disabled`, where the key is `enabled`) is an `unknown-prop` warning. The app holds no open state: the choice arrives as the `menu` event a menu row posts, on the field's key — `{kind: "menu", role: "custom", item: <the option>}` — and drawing the field again with the new `current` is the whole loop. A reader hears a button named by the field, described by its choice, expanded while the menu is open. Its look is its spec, so it reads no other row: a layout, paint or access row on it is dropped with an `unknown-prop` warning. Lua spells it `dropdown`, since `select` is Lua's own. |
|
|
163
|
+
| `<checkbox checked mixed onClick key label description tooltip disabled>text</checkbox>` | `checkbox { label=, checked=, mixed=, on_click=, key=, text=, description=, tooltip=, disabled= }` | `kui_checkbox` | `kui.checkbox` | The stock checkbox (`widgets::toggle_with`, ADR 0034): a box drawn from the state the view declares — `checked`, or `mixed` for the select-all box over a list some of whose rows are selected, drawn as a dash and read as mixed — and its label beside it, keyed by its text (`key` overrides). The state is the app's: a press by the pointer, Space, Enter or assistive technology posts `onClick`, and the view flips its model and draws it again. Its look is its spec, so the layout and paint rows are closed and dropped with an `unknown-prop` warning; the rows it reads are its state and the access rows. In Lua `label` is the name and the text both unless `text` says otherwise. The box is the metrics' control text plus one (16 px comfortable), so `compact` and `scaled` move it with the stock button. |
|
|
164
|
+
| `<radio checked onClick key label description tooltip disabled>text</radio>` | `radio { label=, checked=, on_click=, key=, text=, description=, tooltip=, disabled= }` | `kui_radio` | `kui.radio` | The stock radio (`widgets::toggle_with`, ADR 0034): a circle drawn from `checked`, and its label, keyed by its text. Put radios in a `radioGroup`, which makes them one Tab stop whose arrows, Home and End move the choice and press the radio they land on (ADR 0007), so radios whose `onClick` each set the choice answer the keyboard with no more code. The state is the app's, as a checkbox's is; the rows are the checkbox's, `mixed` aside. |
|
|
165
|
+
| `<radioGroup label>…radios…</radioGroup>` | `radio_group { label=, … }` | `kui_radio_group_open` … `kui_close` | `kui.radio_group` in an `if`, closed at its end | A container of radios (`widgets::radio_group_with`, ADR 0034): the `radioGroup` role, named by its `label`, laid out as a column with the stock gap — a `dir="row"` lays the radios across, and its arrows run across with it. It reads every box row; the role and the name are its own whatever the rows say. |
|
|
166
|
+
| `<switch checked onClick key label description tooltip disabled>text</switch>` | `switch { label=, checked=, on_click=, key=, text=, description=, tooltip=, disabled= }` | `kui_switch` | `kui.toggle` | The stock switch (`widgets::toggle_with`, ADR 0034): a track and a knob drawn from `checked`, the knob sliding across when it changes, and its label; read as a switch, on or off. The state is the app's, as a checkbox's is; the rows are the checkbox's, `mixed` aside. |
|
|
167
|
+
| `<slider label valueNow valueMin valueMax valueStep valueText onChange width description tooltip disabled/>` | `slider { label=, value_now=, value_min=, value_max=, value_step=, value_text=, on_change=, width=, … }` | `kui_slider` | `kui.slider` | The stock slider (`widgets::slider_with`, ADR 0034): a track, a fill to `valueNow` and a thumb, as wide as a menu (`width` sizes it), keyed by its `label`, which is also its accessible name. With `onChange` the core does the arithmetic: a press proposes the value under the pointer, a drag each new step, the arrows one `valueStep`, PageUp / PageDown ten, Home / End the ends, all clamped to `valueMin`..`valueMax` (0..100 unset) and snapped to the step, as `{kind:"change", value, phase:"move"\|"end", tag}`. The value is proposed, never applied: the view stores it and declares it as `valueNow`. Its look is its spec, so the rows it reads are the value rows, the access rows and its width. |
|
|
168
|
+
| `<image src={id} sampling fit>` | `image { id=, sampling=, fit= }` | `kui_image`, `kui_image_with` | `kui.image`, `kui.image_with` | A registered RGBA image. Sizing: `width="fit"` takes the pixel size, a fit height against a resolved width keeps the aspect, `radius` rounds it. Two rows say how the pixels meet the box (`docs/adr/0025-the-image-is-the-canvas.md`): `sampling` is `linear` (the default) or `nearest` — pixel art, an emulator, a data grid that must stay square under zoom; `fit` is `fill` (the default: the pixels stretch to the box), `contain` (the largest rect of the image's aspect that fits, centred, the rest of the box showing what is behind) or `cover` (the box filled and the pixels that do not fit cropped, centred). The box — its layout, its hit region, its access rect — is the same in every mode. The pixels come from the atlas, or from a texture of the image's own once `updateImage` has replaced them or when no atlas page could hold them; the node cannot tell and need not. |
|
|
169
|
+
| `<polygon points={[[x,y],…]} bg/>` | `polygon { points={{x,y},…}, bg= }` | `kui_polygon` | `kui.polygon`, its points a `[][2]f32` | A filled polygon through up to eight `points`, the fill in `bg` (`docs/adr/0025-the-image-is-the-canvas.md`, decision 6): an arrowhead, a pie slice, the area under a curve. Placed as a `line` is — always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box a pixel out on each side, so it takes no room in a row or column, and painted in the parent's layer at its place in the tree, over the siblings before it and under those after (backlog F123). A polygon in its parent's box space is held by the parent's clip as a child is, its hit region with it, so it is cut at a scroller's edge with the row it is drawn in; a declared float is held that way only when it declares `clip` with a parent anchor, and a `float="viewport"` polygon escapes (backlog F78, `docs/adr/0010-a-segment-primitive.md` decision 5). `transition` eases the fill and, with `slide`, its position. The outline may be concave; a self-intersecting one fills even-odd, its overlaps unfilled. Hit by its outline (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press inside the outline hits it and one in its box past the outline falls through, so a pie's wedges need no hit boxes; with none it takes no input and has no access row, and with input it derives one as a box would (a clickable wedge is a button), so name it. A ninth point and later are dropped with `polygon-points-truncated`; fewer than three draw nothing; no `bg`, no fill. On the wire it is one `fragment` quad painted by a WGSL function the core registers itself, so a host that draws the list gets its source from `kui_fragment_source` like any other; what it costs is that quad and one pipeline switch per run of polygons. A stroked outline is a closed `line` over it. |
|
|
170
|
+
| `<path d="M … Z" bg width color fillRule rotate pivot dash dashOffset/>` | `path { d = "M … Z", bg=, width=, color=, fill_rule=, rotate=, pivot=, dash=, dash_offset= }` | `kui_path` | `kui.path`, `kui.path_d` | Any outline — SVG's `d`, a pie wedge with a round arc, a map's region, an icon — filled with `bg` by `fillRule` (`nonzero`, the default, or `evenodd`) and stroked `width` wide in `color` when `width` is given, the stroke over the fill (`docs/adr/0040-a-path-is-a-mask-in-the-atlas.md`). `d` is SVG path data (`M L H V C S Q T A Z`, absolute or relative), parsed by one parser in the core, so every binding draws the same shape; one that does not parse raises `path-malformed` and draws nothing. JSX also takes `d` as a flat number array of op codes and operands, Lua the same as `ops`, and C only that form (`kui_path_parse` turns a string into it). Placed as a `line` is — always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box two pixels out on each side (half the stroke's width further), so it takes no room in a row or column, held by the parent's clip as a child is and painted in the parent's layer at its place in the tree (backlog F123). `transition` eases the fill and, with `slide`, its position; the stroke's colour does not tween, as a box's border does not. Hit by its outline under the fill rule (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press inside hits it and one in its box past the outline falls through, so a pie's wedges need no hit boxes; a stroke with no fill is hit by its stroke as a line is; with input it derives an access row as a box would (a clickable wedge is a button), so name it. On the wire it is one glyph-mask quad per paint, fill and stroke: the outline is rasterized once per shape, scale and quarter-pixel position into the glyph atlas and tinted like a glyph, so a host that draws text draws paths, and nothing is re-rasterized for a colour tween, a hover or a slide. The fill bleeds half a pixel, so two paths sharing an edge meet without the background showing through; a chart that wants separators gaps its own geometry. A mask a quarter of the biggest atlas page or more, or a path whose ops change twice within a few frames, draws from a texture of its own instead (a `texture` quad), and one past 8192 px on a side draws nothing, with `path-too-large`. `rotate` turns the path, in turns clockwise, about `pivot` — a point in the path's own coordinates, the centre of its box without one (`docs/adr/0041-a-mask-turns-about-its-centre.md`): the turn is the quad's and not the mask's, so a path that only turns — a spinner's arc about its circle's centre — is rasterized once and stays in the atlas at every angle. A path with `rotate` or `pivot` is boxed by the square the turn sweeps, its mask centred on the pivot on a whole pixel, and it is hit where it is drawn; `rotate` does not tween. `dash` and `dashOffset` cut the stroke into marks and gaps as a `line`'s do (backlog V2) — lengths as seen, round-capped marks, a gap the dots overlap closed — restarting at every subpath as SVG's do; the pattern is part of the stroke's mask, so a dashed stroke costs what a solid one does, a stroke with no fill is still hit along its gaps, and a `dashOffset` that changes every frame is a shape that changes every frame: the path leaves the atlas for a texture of its own while it marches. |
|
|
171
|
+
| `<fragment src={id} image={id} params={[…]} animate>` | `fragment { id=, image=, params={…}, animate= }` | `kui_fragment`, `kui_fragment_with` | `kui.fragment`, `kui.fragment_with`; `kui.fragment_open` in an `if` holds children | A box a registered WGSL function paints (`docs/adr/0015-a-fragment-element-and-the-painter-it-is-not.md`): a conic or a moving gradient, rings, noise, shimmer — anything the paint vocabulary has no prop for. An ordinary node otherwise — it lays out, rounds, clips, fades, takes input and holds children, which paint over it — but with **no intrinsic size**, so give it a `width`/`height` or `fill` or it is zero by zero. `src` is a handle from `add_fragment`, which validates the source and warns rather than minting one that cannot compile. `params` is up to sixteen numbers the shader reads as four `vec4<f32>`; more are dropped with a warning. `image` is a registered image the function reads — `kui_sample(uv)` (bilinear) and `kui_sample_nearest(uv)` return its texels at `uv` in `[0,1]²`, and `in.image` is its texel rect, `zw` the size — which is what makes a replaced image a waveform, a heatmap, a 50k-point line or an image effect from one quad (`docs/adr/0025-the-image-is-the-canvas.md`, decision 7); the core binds the atlas or the image's own texture, whichever holds it, and a fragment whose image is not live draws nothing, as one whose `src` is not does. `animate` asks for a frame every frame, which is what a fragment that reads `time` needs and what a still one must not declare. |
|
|
172
|
+
| `<cells rows cols cells={Uint32Array} cursorAt={[row, col]} cursorShape cursorColor size family lineHeight/>` | `cells { rows=, cols=, lines={"row text", …}, runs={{row, col, len, fg, bg, flags}, …}, cursor_at={row, col}, cursor_shape=, cursor_color=, size=, family= }` | `kui_cells` | `kui.cells` | A terminal's screen as one node (backlog C20): `rows × cols` cells, each a character, a foreground and background as `0xRRGGBBAA` (0 = no background), and attribute bits — 1 bold, 2 italic, 4 underline, 8 strikethrough, 16 wide (the glyph spans this cell and the next, which the app leaves blank), 32 the underline is a wave (a terminal's undercurl, SGR 4:3) and 64 dotted (SGR 4:4), either implying it — plus, optionally, the underline's own colour (SGR 58), 0 for the foreground (backlog K4). A glyph is shaped once per character and style variant and thereafter placed at `col × cell_w` without shaping, so a screen whose every cell is new each frame costs what a still one costs (~60 µs for 200 × 50). The cell width is `M`'s advance in the style's font snapped to whole pixels, the height its `lineHeight`; a cell is a cell, so ligatures never form. A character the family has no glyph for is asked of a monospaced face before the platform's fallback list, shaped smaller where it is still wider than its cells (two under wide), and drawn in their middle (backlog F120); the private use area's icons are left as they fall. Box drawing and block elements (U+2500–U+259F) and the Powerline separators (U+E0B0–U+E0BF: the arrows, and the Powerline Extra half circles and wedges) are not shaped at all but drawn from the cell box — a font's are its own line box tall, a cell is `lineHeight` tall, and through the font every `│` was a dash with a gap under it (backlog F66) and a rounded cap a fallback font's squiggle (F112) — so a TUI's frames and rounded rows are seamless in any font, and bold does not thicken a light line (the set has its heavy variants). JSX passes the cells as a `Uint32Array` (or number array) of four entries per cell — codepoint, fg, bg, flags — or five, with the underline colour, in row-major order; Lua a string per row in `lines` plus `runs` of `{row, col, len, fg, bg, flags, ul}` over them (a run's fg, bg or ul of 0 keeps the default: the style's colour, no background, the foreground); C a `KuiCell` array with `ul`. `cursorAt` (`cursor_at`) names a cell to paint under its glyph in `cursorColor` as a `block` (default), `bar` or `underline` — its own name, since `cursor` is the pointer shape. `originLine` (`origin_line`) is the absolute line number of row 0: a grid is one screenful of the app's own history, so a row number means a different line after every scroll, and stamping where the screen sits is what lets a selection keep its ends across one (`docs/adr/0017-selection-as-a-scope.md`). Saying nothing is 0, and a selection then holds only while the screen does not move. The node's own rows apply — an `onKey` makes it the terminal's sink, an `onClick` or `onDrag` carries `cell: {row, col}` on its events — and its access row is `terminal`, the rows joined as its value. |
|
|
173
|
+
| `<line from={[x,y]} to={[x,y]} width color/>`, `<line points={[[x,y],…]} curve dash={[6, 4]} dashOffset/>` | `line { from={x,y}, to={x,y}, width=, color= }`, `line { points={{x,y},…}, curve=true, dash={6, 4}, dash_offset= }` | `kui_line`, `kui_polyline` | `kui.line`, `kui.polyline` (points, `[][2]f32`) | A round-capped stroke: one segment, a polyline through `points`, or a smooth curve through them with `curve`. Always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box, so it takes no room in a row or column — but a float for the room alone: in its parent's box space it paints in the parent's layer at its place in the tree, over the siblings declared before it and under those after, as a child does, and opens no layer of its own (backlog F123; a connector meant to sit under two cards is declared before them). A stroke in its parent's box space is held by the parent's clip as a child is, its hit region with it, so it is cut at a scroller's edge with the row it is drawn in; a declared float is held that way only when it declares `clip` with a parent anchor, and a `float="viewport"` stroke escapes, and is a layer of its own (backlog F78, `docs/adr/0010-a-segment-primitive.md` decision 5). `width` is the stroke width (default 1) and `color` the stroke colour; `transition` eases the colour, and with `slide` beside it the stroke's position too — the points ride its box, so a stroke whose ends all move together slides with them, while one whose ends move apart resizes at once (a canvas of floats eases everything or nothing, connectors included). Hit by its shape (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press within half the width of any piece hits it — at least 4 px of grab, so a hairline is a target — and a press elsewhere in its bounding box falls through to what is under; with none it takes no input and has no access row, and with input it derives one as a box would (a clickable connector is a button), so name it. What it costs: one quad per segment, and a curve is flattened in the core at one piece per 6 logical px of chord (at most 32 per span) — fixed rather than tolerance-driven so every binding gets the same pieces and the corpus can pin them — so a nine-point curve over ~50 px spans is ~60 quads, and a `quadCount` budget should expect it. `dash` cuts the stroke into marks and gaps (backlog V2): one length (marks and gaps alike), a mark and a gap, or four lengths for a dash-dot, in px **as seen** — every mark is a short stroke with the stroke's round caps, so `dash` 6, 4 is 6 px of ink and 4 px of nothing at any width up to 6 (SVG's `stroke-dasharray` measures the centre line instead, so with round caps its `4 4` at a width of 4 is solid; this pattern is SVG's `mark − width, gap + width`). A mark no longer than the stroke is wide is a dot as wide as the stroke, in the same period, so its gap is that much shorter; where a mark and its gap together come to no more than the width the dots meet and the gap closes — the marks either side of it are one, and a pattern with no gap left, `dash` 2, 2 at a width of 8, draws solid (backlog RG118). The pattern runs along the stroke's whole length, so it keeps its phase round the corners of a polyline and the pieces of a curve, and `dashOffset` starts that far into it — growing it moves the marks towards the first point, a marquee's marching ants; neither tweens. A pattern with no gap, a mark and gap under a physical pixel together, or more than 16384 marks draws solid. A dashed stroke is hit along its whole length, gaps included, and costs a quad per mark per piece the mark lies on. |
|
|
174
|
+
| `<titlebar title>` or `<titlebar>…</titlebar>` | `titlebar { title= }` / `titlebar { … }` | `kui_titlebar`, `kui_titlebar_with` | `kui.titlebar`, `kui.titlebar_with` | Adaptive titlebar for custom chrome: drag strip, native-control inset, window buttons. |
|
|
175
|
+
| `<menuBar menu={[{ label, items: [{ label, id?, role?, accel?, enabled?, checked? }] }]}/>` | `menu_bar { menu = { { label=, items= { { label=, id=, role=, accel=, enabled=, checked= } } } } }` | `kui_menu_bar` | `kui.menu_bar` | The application menu (`docs/adr/0018-a-menu-bar-the-app-declares.md`): `menu` is what it *is*, and where this element sits is where its titles go when they have to be drawn in the window. One call and not two, because declaring the menu and placing the strip are one decision. It draws **nothing** where the platform owns the bar — macOS, where the driver hands the same declaration to the OS — so the frame has still said what the app's menu is and the strip simply is not there; that is the contract `windowButtons` has under native decorations, and it is what makes one view portable. Its rows are the rows a context menu has: the same `role`s the core performs itself (`copy`, `paste`, `selectAll`, `cut`, `lookUp`), the same `id` payload, the same `accel` text, plus `checked` for a setting — and choosing one posts the same `{kind:"menu", role, item}` event, so an app handles one thing whichever menu it came from. Declared every frame and diffed: an unchanged menu costs a comparison, and an empty list takes it away. An accelerator kui can parse is rewritten into the platform's spelling, so `"mod+s"` reads as `⌘S` on macOS and `Ctrl+S` elsewhere and binds that key in the platform's own bar. On macOS the first menu is the application menu, which the OS titles with the app's own name whatever the label says. While a menu is open the bar is the frame's modal scope, so hovering across the titles moves the open menu, a press on the open title closes it, and Escape or a press in the app below closes it and reaches nothing else. |
|
|
176
|
+
| `<windowButtons/>` | `window_buttons()` | `kui_window_buttons` | `kui.window_buttons` | Just the min/max/close buttons, for fully custom titlebars. |
|
|
177
|
+
| `<tooltip value="hint"/>` / `<tooltip>…</tooltip>` nodes, or the `tooltip="hint"` prop (see composites) | `tooltip("hint")` / `tooltip { … }` nodes, or the prop | `kui_tooltip`, `kui_tooltip_with` | `kui.tooltip`, `kui.tooltip_with` | A float hanging below the parent; the node form always draws, the prop form is hover-gated. |
|
|
178
|
+
| `<latencyGraph/>`, `<latencyHud at/>` | `latency_graph()`, `latency_hud { at= }` | `kui_latency_graph`, `kui_latency_hud` | `kui.latency_graph`, `kui.latency_hud` | Per-phase frame timing (windowed drivers fill it; headless shows the chrome empty). |
|
|
179
|
+
| `<audio src={id} loop volume paused finish tag/>` | `audio { src=, loop=, volume=, paused=, finish=, tag= }` | `kui_audio` | `kui.audio` | A playback retained by node key: present = playing (once, or looped), gone = stopped; `volume` / `paused` apply live, a changed `src` restarts; a `tag` brings back `{kind:"sound", phase:"ended", tag}`. Draws nothing. `finish` changes what *gone* means: the node's removal releases the playback rather than stopping it, so a one-shot plays to its end and the view need not know the asset's length to declare the node for it (a loop still stops on removal — there is no end to reach — and a paused playback released has nothing to finish). Without it, the way to play a sound whole is to hold the node declared until the `tag`'s `ended` message arrives. A released playback is not free: it holds one of the device's 128 voices until its file ends, and the 129th play is refused — reported as a `playback-refused` warning and, for a `tag`, `phase: "refused"` rather than a wait that never returns. In the app's units, voices held = sound length × release rate: a 1.4 s chime released four times a second holds 6 of the 128 at any moment, a 10 s ambience released once a second holds 10, and every playback still declared (a loop included) counts beside them — a `refused` `sound` event is what arriving at 128 sounds like. |
|
|
179
180
|
|
|
180
181
|
## Events
|
|
181
182
|
|
|
@@ -219,15 +220,15 @@ one field. The payload shapes:
|
|
|
219
220
|
|
|
220
221
|
## Resources
|
|
221
222
|
|
|
222
|
-
| what | Node | Lua | C |
|
|
223
|
-
|
|
224
|
-
| image | `ctx.addImage(w, h, rgba)` → id for `<image src>` | the host registers; `image { id }` | `kui_image_add` → `kui_image` |
|
|
225
|
-
| fragment (WGSL) | `ctx.addFragment(src)` → id for `<fragment src>` | the host registers; `fragment { id }` | `kui_fragment_add` → `kui_fragment` |
|
|
226
|
-
| font from bytes | `ctx.addFont(buffer)` → id for `font` | the host registers; `font = id` | `kui_font_add` → `KuiTextStyle.font` |
|
|
227
|
-
| font file by path | `ctx.loadFontFile("fonts/Antonio.ttf")` → id for `font` | the host registers; `font = id` | `kui_font_load_file` |
|
|
228
|
-
| a folder of fonts | `ctx.loadFontsDir("fonts")`, then pick by name | the host loads | `kui_font_load_dir` |
|
|
229
|
-
| font by family name (installed or loaded) | `ctx.addSystemFont("Antonio")` (see `systemFontFamilies()`) | the host registers; `font = id` | `kui_font_add_system` (see `kui_font_families`) |
|
|
230
|
-
| sound (wav / ogg / mp3 / flac bytes) | `ctx.addSound(buffer)` → id for `<audio src>`, `clickSound`, `play(id)` | the host registers; `audio { src = id }`, `click_sound = id` | `kui_sound_add` → `kui_audio`, `KuiSpec.click_sound`, `kui_play` |
|
|
223
|
+
| what | Node | Lua | C | Odin |
|
|
224
|
+
|---|---|---|---|---|
|
|
225
|
+
| image | `ctx.addImage(w, h, rgba)` → id for `<image src>` | the host registers; `image { id }` | `kui_image_add` → `kui_image` | `kui.image_add` → `kui.image` |
|
|
226
|
+
| fragment (WGSL) | `ctx.addFragment(src)` → id for `<fragment src>` | the host registers; `fragment { id }` | `kui_fragment_add` → `kui_fragment` | `kui.fragment_add` → `kui.fragment` |
|
|
227
|
+
| font from bytes | `ctx.addFont(buffer)` → id for `font` | the host registers; `font = id` | `kui_font_add` → `KuiTextStyle.font` | `kui.font_add` → `Text_Style.font` |
|
|
228
|
+
| font file by path | `ctx.loadFontFile("fonts/Antonio.ttf")` → id for `font` | the host registers; `font = id` | `kui_font_load_file` | `kui.font_load_file` |
|
|
229
|
+
| a folder of fonts | `ctx.loadFontsDir("fonts")`, then pick by name | the host loads | `kui_font_load_dir` | `kui.font_load_dir` |
|
|
230
|
+
| font by family name (installed or loaded) | `ctx.addSystemFont("Antonio")` (see `systemFontFamilies()`) | the host registers; `font = id` | `kui_font_add_system` (see `kui_font_families`) | `kui.font_add_system` (see `kui.font_families`) |
|
|
231
|
+
| sound (wav / ogg / mp3 / flac bytes) | `ctx.addSound(buffer)` → id for `<audio src>`, `clickSound`, `play(id)` | the host registers; `audio { src = id }`, `click_sound = id` | `kui_sound_add` → `kui_audio`, `KuiSpec.click_sound`, `kui_play` | `kui.sound_add` → `kui.audio`, `Spec.click_sound`, `kui.play` |
|
|
231
232
|
|
|
232
233
|
Handles are slotmap keys with a generation: a removed resource's handle is
|
|
233
234
|
rejected (an image draws nothing, a font shapes as sans) rather than
|
|
@@ -297,7 +298,8 @@ The host facts a view reads: `ui.env()` in Rust, `view(env)` in Lua,
|
|
|
297
298
|
`ctx.env()` / `win.env()` in Node. C is the host, so it *writes* them
|
|
298
299
|
(`kui_env_set`, `kui_env_set_system`, `kui_env_set_window`,
|
|
299
300
|
`kui_env_set_audio`, `kui_env_set_assistive`) and has no
|
|
300
|
-
reading; its column names the argument
|
|
301
|
+
reading; its column names the argument, and so does Odin's, whose setters
|
|
302
|
+
are C's. A real window's runner refreshes every fact each frame;
|
|
301
303
|
headless, `ctx.setEnv` in Node and the C setters are the writers, and
|
|
302
304
|
the conformance corpus drives its two chrome scenes through them. The
|
|
303
305
|
`from` column says which Rust struct holds the fact, or that it is derived
|
|
@@ -329,31 +331,31 @@ of the same shape: whether an accessibility client has asked for the tree,
|
|
|
329
331
|
written by the runner's bridge rather than by a settings query, and the
|
|
330
332
|
one reading that changes what a view *says* rather than what it draws.
|
|
331
333
|
|
|
332
|
-
| field | from | Node | Lua | C | description |
|
|
333
|
-
|
|
334
|
-
| `refresh_hz` | `Env::refresh_hz` | `refreshHz` | `refresh_hz` | `kui_env_set(refresh_hz)` | Display refresh rate in Hz. "The host cannot tell" is `null` in Node (a stable shape to destructure, typed `number \| null`), an absent key in Lua, and a rate at or below zero in C. |
|
|
335
|
-
| `frame_budget_ms` | `Env::frame_budget_ms()`, derived | `frameBudgetMs` | `frame_budget_ms` | — | One vsync interval at `refresh_hz`, or at 120 Hz when the host cannot tell: the per-frame time budget, and what the latency HUD draws its line at. Derived, and in the reading anyway, so no view restates the fallback. A C host is the one that knows the rate and computes its own. |
|
|
336
|
-
| `focused` | `Env::focused` | `focused` | `focused` | `kui_env_set(focused)` | Whether the *window* has the keyboard at all. Not the focused node — that is the `focus` row. |
|
|
337
|
-
| `system.appearance` | `SystemEnv::appearance` | `system.appearance` | `system.appearance` | `kui_env_set_system(appearance)` | The OS light/dark setting: `"light"`, `"dark"`, or `"unknown"` when the host has no way to ask (`KUI_APPEARANCE_*` in C, where unknown is 0). Unknown is a real answer and the default — a view picks its own palette for it rather than being handed a guess. The core acts on it in one way: the theme is derived from it (ADR 0019), so the stock widgets and a `<text>` with no colour follow the setting — and nothing of the app's own repaints, because only the view knows which of its colours is the background. |
|
|
338
|
-
| `system.accent` | `SystemEnv::accent` | `system.accent` | `system.accent` | `kui_env_set_system(accent)` | The OS accent/highlight colour as `0xRRGGBBAA`, ready to pass straight back as a `bg` or `color`. "The host cannot tell" is `null` in Node, an absent key in Lua, and 0 in C — a fully transparent accent is not a colour anyone was given, the way a refresh rate of zero is not a rate. Node's `setEnv` also takes the `"#rrggbb"` spelling a prop takes. |
|
|
339
|
-
| `system.motion` | `SystemEnv::motion` | `system.motion` | `system.motion` | `kui_env_set_system(motion)` | The OS reduce-motion setting: `"reduced"` when the user asked for less animation, `"full"` when they did not, `"unknown"` when nobody asked the OS (`KUI_MOTION_*` in C, unknown 0). Spelled as what the user wants rather than as a `reduceMotion` boolean, because the third reading has no place in a boolean. Nothing in the core shortens an animation for it — a view that honours it does so where it declares one. |
|
|
340
|
-
| `system.locale` | `SystemEnv::locale` | `system.locale` | `system.locale` | `kui_env_set_system(locale)` | The UI language as a BCP-47 tag (`"en"`, `"en-US"`, `"zh-Hant-HK"`), for whatever the view formats dates and numbers with; kui does not parse it. Carried inline (31 ASCII bytes, `Locale`) so the reading stays `Copy`, and anything that does not fit reads back as unknown: `null` in Node, an absent key in Lua, an empty `KuiStr` in C. |
|
|
341
|
-
| `system.assistive` | `SystemEnv::assistive` | `system.assistive` | `system.assistive` | `kui_env_set_assistive(assistive)` | Whether assistive technology is listening: `"listening"` once an accessibility client has asked this window for its tree, `"none"` while the bridge is up and nobody has, `"unknown"` where there is no bridge — a headless `Ctx`, a runner built without `accesskit`, a C host that never called the setter (`KUI_ASSISTIVE_*`, unknown 0). The reading that changes what a view *says* rather than what it draws: an alert that announces when something is listening and blinks when nothing is. Reported through the `system` event when it changes, like the other four. Two limits are the platform's, not kui's. *Any* client counts — a probe, an accessibility inspector, a test driving the AX API and VoiceOver alike all ask for the tree, and nothing tells them apart — so it says something is listening, not that a person is. And it falls back to `"none"` only where the adapter reports deactivation, which in the pinned AccessKit is AT-SPI alone (the session's accessibility bus going away); on macOS and Windows nothing reports a client leaving, so once it has risen it stays `"listening"` for the window's life. |
|
|
342
|
-
| `window.id` | `WindowEnv::id` | `window.id` | `window.id` | `kui_env_set_window(window)`, read back by `kui_ctx_window` | Which window this frame draws, assigned by the driver: 0 for the window the app starts in. Every event from it carries the same number. |
|
|
343
|
-
| `window.custom_chrome` | `WindowEnv::custom_chrome` | `window.customChrome` | `window.custom_chrome` | `kui_env_set_window(custom_chrome)` | The host asked the app to draw its own chrome, so there is no native titlebar to sit under. `<titlebar>` and `<windowButtons>` build nothing when this is false. |
|
|
344
|
-
| `window.maximized` | `WindowEnv::maximized` | `window.maximized` | `window.maximized` | `kui_env_set_window(maximized)` | The window is maximized — what picks the restore glyph over the maximize one. |
|
|
345
|
-
| `window.fullscreen` | `WindowEnv::fullscreen` | `window.fullscreen` | `window.fullscreen` | `kui_env_set_window(fullscreen)` | The window is fullscreen. |
|
|
346
|
-
| `window.always_on_top` | `WindowEnv::always_on_top` | `window.alwaysOnTop` | `window.always_on_top` | `kui_env_set_always_on_top(always_on_top)` | The window is above every other app's: the level the driver set after the frame asked for it (`alwaysOnTop` / `always_on_top` / `kui_set_always_on_top`, backlog C30), on a platform that has one. On Wayland winit has no call for it, so a driver there reports false however often the app asks — which is why a pin button draws its state from this and not from the app's own flag. It is the driver's record of what it set and not a query (winit has no level getter), so a level the OS dropped afterwards — a fullscreen space, a tiling manager — is not seen here. A C host reports it through its own setter rather than an argument on `kui_env_set_window`, the way `kui_env_set_assistive` is, so an older host that never applies a level has nothing to recompile. |
|
|
347
|
-
| `window.native_controls` | `WindowEnv::native_controls` | `window.nativeControls` | `window.controls_w` / `window.controls_h` | `kui_env_set_window(controls_w, controls_h)` | Area (logical px, window coordinates) covered by controls the OS still draws over our content — the macOS traffic lights under custom chrome. Keep out of it. Node hands back the `Rect` the core holds (`{x, y, w, h}`, or `null` for none); Lua and C flatten it to a width and height anchored at the window origin (absent in Lua, `0` in C, for none), which is the shape C's two numbers can express and where the one real instance sits. |
|
|
348
|
-
| `audio.device` | `AudioEnv::device` | `audio.device` | `audio.device` | `kui_env_set_audio(device)` | What the driver's output device is doing: `"closed"` (the default, and a headless driver's answer), `"opening"` (the ~90 ms open, on its own thread), `"open"`, or `"failed"` (it refused, and commands are dropped) — `KUI_AUDIO_DEVICE_*` in C, closed 0. A fact and not a verb: nothing lets a view close it, the driver does that itself once it has been idle a while. Worth reading because an open stream is a real-time thread whether or not anything plays, which is the whole of an idle app's CPU once a session has held a sound. |
|
|
349
|
-
| `audio.live` | `AudioEnv::live` | `audio.live` | `audio.live` | `kui_env_set_audio(live)` | Playbacks started and not yet ended, plus any waiting on the device to open. Zero with the device still `"open"` is the idle stream the row above is about. A play that arrives while the device is `"opening"` counts here from the frame it was asked, until the open answers: if the device refuses, the play is refused on the next apply — `{kind:"sound", phase:"refused"}` for a tagged one — and leaves the count with it, so what a machine with no output device shows is `opening`/1 then `failed`/0 with the refusal between (backlog F63). |
|
|
350
|
-
| `viewport.w` | `Core::viewport()`, the frame's | `viewport.width` | `viewport_w` | `kui_frame_begin(w)` | The logical width of the current (or last) frame's viewport — the window less the devtools' dock while the panel is docked (`docs/adr/0024`), the same number a `resize` reports and Node's `KuiWindow.size()` answers — the other host fact a view wants at the same moment, so it rides in the same reading. Zero before the first frame, since the frame establishes it (backlog F43: this row once read the window instead, so an app under `KUI_DEVTOOLS` sized itself to a viewport it did not have). Node's `viewport` is the `WindowSize` shape `runWindowed` already uses. |
|
|
351
|
-
| `viewport.h` | `Core::viewport()`, the frame's | `viewport.height` | `viewport_h` | `kui_frame_begin(h)` | Its logical height. |
|
|
352
|
-
| `scale` | `Core::scale()`, the frame's | `viewport.scale` | — | `kui_frame_begin(scale)` | Device pixels per logical px. Lua has no reading: a script sees logical px only. |
|
|
353
|
-
| `focus` | `Core::focus()`, the frame's | — | `focus` | `kui_focused()` | The focused *node*'s key, as events carry it (absent for none). A value the host wrote before the view ran, so it lags a same-frame verb by one frame; `env.is_focused(key)` is the live query. Node spells it as the call `focused()` on the context, and C as `kui_focused`, rather than a key on `env`. |
|
|
354
|
-
| `focus_visible` | `Core::focus_visible()`, the frame's | — | `focus_visible` | `kui_focus_visible()` | Whether focus shows — the keyboard or assistive technology put it where it is, or acted on it there; a click alone does not. Node: `focusVisible()` on the context. |
|
|
355
|
-
| `caret_visible` | `Core::caret_visible()`, the frame's | — | `caret_visible` | `kui_caret_visible()` | The caret's blink phase — `true` draws it (backlog C35). The driver's clock sets it while there is a caret to blink: a focused `edit`'s, or the `caret` a `line` under a focused `onKey` sink declares; a custom editor draws its caret node on the on phase and skips it on the off, keeping the `caret` row on its `line` either way, so it blinks in step with the stock editor and, in a window without the keyboard, not at all. Always `true` headless. Node: `caretVisible()` on the context (`setCaretVisible` is the driver's half, for a test that drives the phase). |
|
|
356
|
-
| `region` | `Core::region()`, the frame's | — | `region` | `kui_region()` | The focus region in effect — the key of the `focusRegion` node whose ring Tab walks (absent for the main ring; `docs/adr/0022-focus-regions.md`). What a chord that toggles between a dock and the app reads to know which way it is going. Node spells it as the call `region()` on the context, and C as `kui_region`. |
|
|
334
|
+
| field | from | Node | Lua | C | Odin | description |
|
|
335
|
+
|---|---|---|---|---|---|---|
|
|
336
|
+
| `refresh_hz` | `Env::refresh_hz` | `refreshHz` | `refresh_hz` | `kui_env_set(refresh_hz)` | `kui.env_set(refresh_hz)` | Display refresh rate in Hz. "The host cannot tell" is `null` in Node (a stable shape to destructure, typed `number \| null`), an absent key in Lua, and a rate at or below zero in C. |
|
|
337
|
+
| `frame_budget_ms` | `Env::frame_budget_ms()`, derived | `frameBudgetMs` | `frame_budget_ms` | — | — | One vsync interval at `refresh_hz`, or at 120 Hz when the host cannot tell: the per-frame time budget, and what the latency HUD draws its line at. Derived, and in the reading anyway, so no view restates the fallback. A C host is the one that knows the rate and computes its own. |
|
|
338
|
+
| `focused` | `Env::focused` | `focused` | `focused` | `kui_env_set(focused)` | `kui.env_set(focused)` | Whether the *window* has the keyboard at all. Not the focused node — that is the `focus` row. |
|
|
339
|
+
| `system.appearance` | `SystemEnv::appearance` | `system.appearance` | `system.appearance` | `kui_env_set_system(appearance)` | `kui.env_set_system(appearance)` | The OS light/dark setting: `"light"`, `"dark"`, or `"unknown"` when the host has no way to ask (`KUI_APPEARANCE_*` in C, where unknown is 0). Unknown is a real answer and the default — a view picks its own palette for it rather than being handed a guess. The core acts on it in one way: the theme is derived from it (ADR 0019), so the stock widgets and a `<text>` with no colour follow the setting — and nothing of the app's own repaints, because only the view knows which of its colours is the background. |
|
|
340
|
+
| `system.accent` | `SystemEnv::accent` | `system.accent` | `system.accent` | `kui_env_set_system(accent)` | `kui.env_set_system(accent)` | The OS accent/highlight colour as `0xRRGGBBAA`, ready to pass straight back as a `bg` or `color`. "The host cannot tell" is `null` in Node, an absent key in Lua, and 0 in C — a fully transparent accent is not a colour anyone was given, the way a refresh rate of zero is not a rate. Node's `setEnv` also takes the `"#rrggbb"` spelling a prop takes. |
|
|
341
|
+
| `system.motion` | `SystemEnv::motion` | `system.motion` | `system.motion` | `kui_env_set_system(motion)` | `kui.env_set_system(motion)` | The OS reduce-motion setting: `"reduced"` when the user asked for less animation, `"full"` when they did not, `"unknown"` when nobody asked the OS (`KUI_MOTION_*` in C, unknown 0). Spelled as what the user wants rather than as a `reduceMotion` boolean, because the third reading has no place in a boolean. Nothing in the core shortens an animation for it — a view that honours it does so where it declares one. |
|
|
342
|
+
| `system.locale` | `SystemEnv::locale` | `system.locale` | `system.locale` | `kui_env_set_system(locale)` | `kui.env_set_system(locale)` | The UI language as a BCP-47 tag (`"en"`, `"en-US"`, `"zh-Hant-HK"`), for whatever the view formats dates and numbers with; kui does not parse it. Carried inline (31 ASCII bytes, `Locale`) so the reading stays `Copy`, and anything that does not fit reads back as unknown: `null` in Node, an absent key in Lua, an empty `KuiStr` in C. |
|
|
343
|
+
| `system.assistive` | `SystemEnv::assistive` | `system.assistive` | `system.assistive` | `kui_env_set_assistive(assistive)` | `kui.env_set_assistive(assistive)` | Whether assistive technology is listening: `"listening"` once an accessibility client has asked this window for its tree, `"none"` while the bridge is up and nobody has, `"unknown"` where there is no bridge — a headless `Ctx`, a runner built without `accesskit`, a C host that never called the setter (`KUI_ASSISTIVE_*`, unknown 0). The reading that changes what a view *says* rather than what it draws: an alert that announces when something is listening and blinks when nothing is. Reported through the `system` event when it changes, like the other four. Two limits are the platform's, not kui's. *Any* client counts — a probe, an accessibility inspector, a test driving the AX API and VoiceOver alike all ask for the tree, and nothing tells them apart — so it says something is listening, not that a person is. And it falls back to `"none"` only where the adapter reports deactivation, which in the pinned AccessKit is AT-SPI alone (the session's accessibility bus going away); on macOS and Windows nothing reports a client leaving, so once it has risen it stays `"listening"` for the window's life. |
|
|
344
|
+
| `window.id` | `WindowEnv::id` | `window.id` | `window.id` | `kui_env_set_window(window)`, read back by `kui_ctx_window` | `kui.env_set_window(window)`, read back by `kui.ctx_window` | Which window this frame draws, assigned by the driver: 0 for the window the app starts in. Every event from it carries the same number. |
|
|
345
|
+
| `window.custom_chrome` | `WindowEnv::custom_chrome` | `window.customChrome` | `window.custom_chrome` | `kui_env_set_window(custom_chrome)` | `kui.env_set_window(custom_chrome)` | The host asked the app to draw its own chrome, so there is no native titlebar to sit under. `<titlebar>` and `<windowButtons>` build nothing when this is false. |
|
|
346
|
+
| `window.maximized` | `WindowEnv::maximized` | `window.maximized` | `window.maximized` | `kui_env_set_window(maximized)` | `kui.env_set_window(maximized)` | The window is maximized — what picks the restore glyph over the maximize one. |
|
|
347
|
+
| `window.fullscreen` | `WindowEnv::fullscreen` | `window.fullscreen` | `window.fullscreen` | `kui_env_set_window(fullscreen)` | `kui.env_set_window(fullscreen)` | The window is fullscreen. |
|
|
348
|
+
| `window.always_on_top` | `WindowEnv::always_on_top` | `window.alwaysOnTop` | `window.always_on_top` | `kui_env_set_always_on_top(always_on_top)` | `kui.env_set_always_on_top(always_on_top)` | The window is above every other app's: the level the driver set after the frame asked for it (`alwaysOnTop` / `always_on_top` / `kui_set_always_on_top`, backlog C30), on a platform that has one. On Wayland winit has no call for it, so a driver there reports false however often the app asks — which is why a pin button draws its state from this and not from the app's own flag. It is the driver's record of what it set and not a query (winit has no level getter), so a level the OS dropped afterwards — a fullscreen space, a tiling manager — is not seen here. A C host reports it through its own setter rather than an argument on `kui_env_set_window`, the way `kui_env_set_assistive` is, so an older host that never applies a level has nothing to recompile. |
|
|
349
|
+
| `window.native_controls` | `WindowEnv::native_controls` | `window.nativeControls` | `window.controls_w` / `window.controls_h` | `kui_env_set_window(controls_w, controls_h)` | `kui.env_set_window(controls_w, controls_h)` | Area (logical px, window coordinates) covered by controls the OS still draws over our content — the macOS traffic lights under custom chrome. Keep out of it. Node hands back the `Rect` the core holds (`{x, y, w, h}`, or `null` for none); Lua and C flatten it to a width and height anchored at the window origin (absent in Lua, `0` in C, for none), which is the shape C's two numbers can express and where the one real instance sits. |
|
|
350
|
+
| `audio.device` | `AudioEnv::device` | `audio.device` | `audio.device` | `kui_env_set_audio(device)` | `kui.env_set_audio(device)` | What the driver's output device is doing: `"closed"` (the default, and a headless driver's answer), `"opening"` (the ~90 ms open, on its own thread), `"open"`, or `"failed"` (it refused, and commands are dropped) — `KUI_AUDIO_DEVICE_*` in C, closed 0. A fact and not a verb: nothing lets a view close it, the driver does that itself once it has been idle a while. Worth reading because an open stream is a real-time thread whether or not anything plays, which is the whole of an idle app's CPU once a session has held a sound. |
|
|
351
|
+
| `audio.live` | `AudioEnv::live` | `audio.live` | `audio.live` | `kui_env_set_audio(live)` | `kui.env_set_audio(live)` | Playbacks started and not yet ended, plus any waiting on the device to open. Zero with the device still `"open"` is the idle stream the row above is about. A play that arrives while the device is `"opening"` counts here from the frame it was asked, until the open answers: if the device refuses, the play is refused on the next apply — `{kind:"sound", phase:"refused"}` for a tagged one — and leaves the count with it, so what a machine with no output device shows is `opening`/1 then `failed`/0 with the refusal between (backlog F63). |
|
|
352
|
+
| `viewport.w` | `Core::viewport()`, the frame's | `viewport.width` | `viewport_w` | `kui_frame_begin(w)` | `kui.frame_begin(w)` | The logical width of the current (or last) frame's viewport — the window less the devtools' dock while the panel is docked (`docs/adr/0024`), the same number a `resize` reports and Node's `KuiWindow.size()` answers — the other host fact a view wants at the same moment, so it rides in the same reading. Zero before the first frame, since the frame establishes it (backlog F43: this row once read the window instead, so an app under `KUI_DEVTOOLS` sized itself to a viewport it did not have). Node's `viewport` is the `WindowSize` shape `runWindowed` already uses. |
|
|
353
|
+
| `viewport.h` | `Core::viewport()`, the frame's | `viewport.height` | `viewport_h` | `kui_frame_begin(h)` | `kui.frame_begin(h)` | Its logical height. |
|
|
354
|
+
| `scale` | `Core::scale()`, the frame's | `viewport.scale` | — | `kui_frame_begin(scale)` | `kui.frame_begin(scale)` | Device pixels per logical px. Lua has no reading: a script sees logical px only. |
|
|
355
|
+
| `focus` | `Core::focus()`, the frame's | — | `focus` | `kui_focused()` | `kui.focused()` | The focused *node*'s key, as events carry it (absent for none). A value the host wrote before the view ran, so it lags a same-frame verb by one frame; `env.is_focused(key)` is the live query. Node spells it as the call `focused()` on the context, and C as `kui_focused`, rather than a key on `env`. |
|
|
356
|
+
| `focus_visible` | `Core::focus_visible()`, the frame's | — | `focus_visible` | `kui_focus_visible()` | `kui.focus_visible()` | Whether focus shows — the keyboard or assistive technology put it where it is, or acted on it there; a click alone does not. Node: `focusVisible()` on the context. |
|
|
357
|
+
| `caret_visible` | `Core::caret_visible()`, the frame's | — | `caret_visible` | `kui_caret_visible()` | `kui.caret_visible()` | The caret's blink phase — `true` draws it (backlog C35). The driver's clock sets it while there is a caret to blink: a focused `edit`'s, or the `caret` a `line` under a focused `onKey` sink declares; a custom editor draws its caret node on the on phase and skips it on the off, keeping the `caret` row on its `line` either way, so it blinks in step with the stock editor and, in a window without the keyboard, not at all. Always `true` headless. Node: `caretVisible()` on the context (`setCaretVisible` is the driver's half, for a test that drives the phase). |
|
|
358
|
+
| `region` | `Core::region()`, the frame's | — | `region` | `kui_region()` | `kui.region()` | The focus region in effect — the key of the `focusRegion` node whose ring Tab walks (absent for the main ring; `docs/adr/0022-focus-regions.md`). What a chord that toggles between a dock and the app reads to know which way it is going. Node spells it as the call `region()` on the context, and C as `kui_region`. |
|
|
357
359
|
|
|
358
360
|
## Theme
|
|
359
361
|
|
|
@@ -453,10 +455,11 @@ everywhere else — and `compact` leaves it alone.
|
|
|
453
455
|
## Doors
|
|
454
456
|
|
|
455
457
|
The verbs — what an app or a host *calls* on its context, as against what
|
|
456
|
-
it declares in the tree above — one row per verb across the
|
|
458
|
+
it declares in the tree above — one row per verb across the five bindings
|
|
457
459
|
(`schema::DOORS`, backlog B1a). A cell is the binding's spelling (a
|
|
458
|
-
`kui_*` function; a
|
|
459
|
-
|
|
460
|
+
`kui_*` function; a procedure of Odin's package `kui`; a method on
|
|
461
|
+
both of Node's classes, or on the one it is prefixed with; a function on
|
|
462
|
+
Lua's `env`), the same thing in another
|
|
460
463
|
form (a prop, a reading, a callback, a constructor option), or — in
|
|
461
464
|
italics — the reason the binding has none. The reasons are the point: the
|
|
462
465
|
bindings are not one surface. A Lua script is a guest in the host's frame
|
|
@@ -464,185 +467,188 @@ bindings are not one surface. A Lua script is a guest in the host's frame
|
|
|
464
467
|
a reading, so registering, driving, pacing and reading back are the
|
|
465
468
|
host's; Node's `Ctx` drives a headless core and its `KuiWindow` is
|
|
466
469
|
driven by the runner, so the driver's half is on `Ctx` alone; and a
|
|
467
|
-
Node host never paints, so the renderer's rows are C's.
|
|
470
|
+
Node host never paints, so the renderer's rows are C's. Odin's doors are
|
|
471
|
+
kui.h's, so its column follows C's: the same function without its
|
|
472
|
+
`kui_`, and no door where C has none.
|
|
468
473
|
|
|
469
474
|
Each binding's own test pins its column both ways: every spelling here is
|
|
470
475
|
a door there, and every door there is a row here — so a verb added to one
|
|
471
|
-
binding is a row with its
|
|
476
|
+
binding is a row with its other cells, or a red test. Odin's is pinned by
|
|
477
|
+
its generator (`nu scripts/odin.nu gen --check`).
|
|
472
478
|
|
|
473
|
-
| verb | C | Node | Lua | description |
|
|
474
|
-
|
|
475
|
-
| `SharedResources::add_image` | `kui_image_add` | `addImage` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers RGBA pixels and mints an id for `<image src>`. |
|
|
476
|
-
| `Core::update_image` | `kui_image_update` | `updateImage` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Replaces the pixels behind a live id, keeping the id (ADR 0025). |
|
|
477
|
-
| `Core::remove_image` | `kui_image_remove` | `removeImage` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Drops an image; every window's atlas lets it go (backlog AR8). |
|
|
478
|
-
| `Core::image_pixels` | `kui_image_pixels` | *none: a Node host never paints: the renderer behind `KuiWindow` is the runner's, and a headless `Ctx` has none* | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The pixels behind a handle, for a renderer meeting a texture quad. |
|
|
479
|
-
| `Core::parse_path` | `kui_path_parse` | `d` on `<path>` is the string; the addon hands it to this parser | `d` on `path { }` is the string; the host hands it to this parser | SVG path data to the flat op form a `path` draws (ADR 0040): one parser, so every binding draws the same shape. |
|
|
480
|
-
| `Core::add_fragment` | `kui_fragment_add` | `addFragment` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers a WGSL function and mints an id for `<fragment src>` (ADR 0015). |
|
|
481
|
-
| `Core::remove_fragment` | `kui_fragment_remove` | `removeFragment` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Drops a fragment; the renderer drops its pipelines (backlog AR8). |
|
|
482
|
-
| `Core::fragment_module_source` | `kui_fragment_source` | *none: a Node host never paints: the renderer behind `KuiWindow` is the runner's, and a headless `Ctx` has none* | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The whole WGSL module behind a handle, which is what a renderer compiles. |
|
|
483
|
-
| `Core::add_font_data` | `kui_font_add` | `addFont` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers a font's bytes and mints an id for `font`. |
|
|
484
|
-
| `Core::add_system_font` | `kui_font_add_system` | `addSystemFont` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers an installed family by name. |
|
|
485
|
-
| `Core::load_font_file` | `kui_font_load_file` | `loadFontFile` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers a font file by path. |
|
|
486
|
-
| `Core::load_fonts_dir` | `kui_font_load_dir` | `loadFontsDir` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers every font file in a directory. |
|
|
487
|
-
| `Core::reload_system_fonts` | `kui_font_reload_system` | `reloadSystemFonts` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Scans the system's fonts again, so a font installed while the app runs is found (the scan is otherwise once a process); returns how many faces came and went. |
|
|
488
|
-
| `Core::set_fallback_fonts` | `kui_font_set_fallback` | `setFallbackFonts` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The fonts asked, in order, for a character the text's own family lacks, before the platform's fallback list (backlog F121). |
|
|
489
|
-
| `Core::fallback_fonts` | *none: the list is the one the host set* | *none: the list is the one the host set* | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The families `set_fallback_fonts` named, in order. |
|
|
490
|
-
| `Core::remove_font` | `kui_font_remove` | `removeFont` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Drops a font. |
|
|
491
|
-
| `Core::system_font_families` | `kui_font_families` | `systemFontFamilies` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The installed family names `add_system_font` accepts. |
|
|
492
|
-
| `Core::system_fonts` | `kui_system_fonts` | `systemFonts` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The same families, each with what the font database read off its faces: `monospaced` (every face fixed-pitch), `weights`, `italic` (backlog F97) — a font picker's monospaced-first list without a file loaded or a glyph shaped. |
|
|
493
|
-
| `Core::add_sound` | `kui_sound_add` | `addSound` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers a sound's bytes and mints an id for `<audio src>`, `clickSound` and `play`. |
|
|
494
|
-
| `Core::remove_sound` | `kui_sound_remove` | `removeSound` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Drops a sound. |
|
|
495
|
-
| `Core::set_text_cache_budget` | `kui_set_text_cache_budget` | `setTextCacheBudget` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The shaped-text cache's byte budget (backlog C16). |
|
|
496
|
-
| `Core::text_cache_bytes` | `kui_text_cache_bytes` | `textCacheBytes` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | What the shaped-text cache holds. |
|
|
497
|
-
| `Ui::play` | `kui_play` | `play` | *none: a script owns no sound handle, and its env is the view's: a playback started there would start again every frame — `audio { src = id }` is the declarative form, and what a script has* | Starts a playback of a registered sound, outside any node; answers the playback id. |
|
|
498
|
-
| `Core::stop` | `kui_stop` | `stop` | *none: as `play`: a script declares `audio { }` and stops it by not declaring it* | Stops a playback, with an optional fade. |
|
|
499
|
-
| `Core::set_volume` | `kui_set_volume` | `setVolume` | `audio { volume = }` applies live | A playback's volume, with an optional tween. |
|
|
500
|
-
| `Core::pause` | `kui_pause` | `pause` | `audio { paused = true }` applies live | Pauses a playback. |
|
|
501
|
-
| `Core::resume` | `kui_resume` | `resume` | `audio { paused = false }` | Resumes a paused playback. |
|
|
502
|
-
| `Core::set_master_volume` | `kui_set_master_volume` | `setMasterVolume` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The device's master volume, with an optional tween. |
|
|
503
|
-
| `Ui::announce` | `kui_announce` | `announce` | `announce` | Says something once with no node behind it (ADR 0001). |
|
|
504
|
-
| `Core::take_announcements` | `kui_take_announcements` | `Ctx.announcements` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Drains what was announced, for a host bridging assistive technology; a `KuiWindow`'s bridge is the runner's. |
|
|
505
|
-
| `Core::access_tree` | `kui_access_tree` | `accessTree` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The access tree of the last finished frame (ADR 0001). |
|
|
506
|
-
| `Ui::focus` | `kui_focus` | `focus` | `set_focus` | Moves focus to a node now; an app's move stands over a modal's restore (backlog AR17). `keyFocus` is the declarative, edge-triggered form. |
|
|
507
|
-
| `Ui::blur` | `kui_focus(ctx, 0)` | `blur` | `blur` | Drops focus. |
|
|
508
|
-
| `Ui::focus_next` | `kui_focus_next` | `focusNext` | `focus_next` | Steps the Tab ring forward (ADR 0002). |
|
|
509
|
-
| `Ui::focus_prev` | `kui_focus_next(ctx, false)` | `focusPrev` | `focus_prev` | Steps the Tab ring backward. |
|
|
510
|
-
| `Ui::focus_region` | `kui_focus_region` | `focusRegion` | `focus_region` | Enters a `focusRegion`'s ring, or leaves it for the main one (ADR 0022). |
|
|
511
|
-
| `Ui::region` | `kui_region` | `region` | `env.region`, a reading | The region in effect. |
|
|
512
|
-
| `Ui::focused` | `kui_focused` | `focused` | `env.focus`, a reading | The focused node's key. |
|
|
513
|
-
| `Ui::is_focused` | `kui_is_focused` | `isFocused` | `is_focused` | Whether a node has focus. |
|
|
514
|
-
| `Ui::focus_visible` | `kui_focus_visible` | `focusVisible` | `env.focus_visible`, a reading | Whether focus came from the keyboard and the ring should show. |
|
|
515
|
-
| `Ui::key_of` | `kui_key_of` | `keyOf` | every query and verb takes the label itself (`key_query`) | The key a label names this frame. |
|
|
516
|
-
| `Core::label_of` | *none: the label is the app's own word for the node, and every door names a node by it or by the key an event carried; the one reader is the devtools' inspector, in the core* | *none: the same reason as C's* | *none: the same reason as C's* | The label a key was opened under. |
|
|
517
|
-
| `Ui::caret_visible` | `kui_caret_visible` | `caretVisible` | `env.caret_visible`, a reading | The blink phase a custom editor draws its caret on. |
|
|
518
|
-
| `Core::set_caret_visible` | `kui_set_caret_visible` | `setCaretVisible` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The host's blink clock writes the phase. |
|
|
519
|
-
| `Core::has_caret` | `kui_has_caret` | `hasCaret` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether anything focused draws a caret to blink — a `caretSolid` line's is not one — which arms a host's blink clock. |
|
|
520
|
-
| `Core::caret_stamp` | `kui_caret_stamp` | the loop in `index.js` runs the blink from `nextDeadlineMs`; a headless `Ctx` never blinks | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Changes when the caret moves or focus does, which re-arms the clock solid. |
|
|
521
|
-
| `Ui::is_hovered` | `kui_is_hovered` | `isHovered` | `is_hovered` | Whether the pointer is over a node. |
|
|
522
|
-
| `Ui::is_pressed` | `kui_is_pressed` | `isPressed` | `is_pressed` | Whether a press started on a node and the pointer is still over it. |
|
|
523
|
-
| `Ui::is_drop_target` | `kui_is_drop_target` | `isDropTarget` | `is_drop_target` | Whether files dragged in from the OS are over a node (ADR 0031) — for drop-dependent layout; the colour is `drop_bg`. |
|
|
524
|
-
| `Core::drop_target` | `kui_drop_target` | `dropTarget` | `drop_target` | The drop zone the dragged files are over, if any — what a driver answers the OS with, and what a test reads to say a zone was found (ADR 0031, decision 5). |
|
|
525
|
-
| `Ui::is_group_hovered` | `hoverBg` / `pressedBg` on a `hoverGroup` member paint it; the reader is what the Rust widgets ask when they paint by hand | the same form as C's | the same form as C's | Whether any member of a hover group is hovered (`is_group_pressed` the same for a press). |
|
|
526
|
-
| `Core::cursor` | *none: the pointer's position is the driver's own fact — it injected it* | *none: the same reason as C's* | *none: the same reason as C's, one step removed* | Where the pointer is, in logical viewport px. |
|
|
527
|
-
| `Core::cursor_shape` | `kui_cursor_shape` | `cursorShape` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The pointer shape the frame asks for, which the host sets on its window. |
|
|
528
|
-
| `Ui::layout_of` | `kui_layout_of` | `layoutOf` | `layout_of` | Where layout put a node last frame (backlog C26). |
|
|
529
|
-
| `Ui::scroll_offset` | `kui_scroll_offset` | `scrollOffset` | `scroll_offset` | A scrolling node's offset. |
|
|
530
|
-
| `Ui::scroll_geometry` | `kui_scroll_geometry` | `scrollGeometry` | `scroll_geometry` | A scrolling node's viewport and content sizes. |
|
|
531
|
-
| `Ui::set_scroll` | `kui_set_scroll` | `setScroll` | `set_scroll` | Scrolls a node to an offset. |
|
|
532
|
-
| `Ui::shift_scroll` | `kui_shift_scroll` | `shiftScroll` | `shift_scroll` | Moves a node's scroll by content that moved under it, with no ease: a variable-height list's anchor (backlog C46). |
|
|
533
|
-
| `Ui::reveal` | `kui_reveal` | `reveal` | `reveal` | Scrolls whatever encloses a node until it is in view. |
|
|
534
|
-
| `Ui::text_hit` | `kui_text_hit` | `textHit` | `text_hit` | The byte and line under a point in a node's text (backlog C18). |
|
|
535
|
-
| `Ui::caret_rect` | `kui_caret_rect` | `caretRect` | `caret_rect` | The caret rect for a byte offset in a node's text. |
|
|
536
|
-
| `Core::ime_rect` | `kui_ime_rect` | `imeRect` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Where the OS candidate window goes, which the host hands to the platform (backlog C17). |
|
|
537
|
-
| `Ui::measure_text` | `kui_measure_text` | `measureText` | `measure_text` | Shapes text in a style at a width and answers its size and line count. |
|
|
538
|
-
| `Ui::measure_rich_text` | `kui_measure_rich_text` | `measureText` takes spans too | `measure_text` takes spans too | The same for spans, shaped as one paragraph. |
|
|
539
|
-
| `Ui::edit_text` | `kui_edit_text` | `editText` | `edit_text` | An editor's text, by key or by label. |
|
|
540
|
-
| `Ui::set_edit_text` | `kui_edit_set_text` | `setEditText` | `set_edit_text` | Replaces an editor's text, caret at the end. |
|
|
541
|
-
| `Ui::set_edit_text_by_label` | `kui_edit_set_text_label` | `setEditText` takes the label too | `set_edit_text` takes the label too | The same by the label an editor's `key` declares, which reaches one the frame is about to declare (backlog AR26). |
|
|
542
|
-
| `Core::animating` | `kui_animating` | `animating` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the last frame left a transition mid-flight, so the host draws another without waiting for input. |
|
|
543
|
-
| `Core::owed` | `kui_owed` | `owed` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The same by kind — a finite transition, a keyframe cycle, a departing ghost, a requested frame, an autoscroll — so a test can wait for the transitions to run out under a cycle that never ends; Node's loop has `quiet()` for that wait (backlog F64). |
|
|
544
|
-
| `Core::set_frame_trace` | `kui_set_frame_trace` | `setFrameTrace` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Turns on the trace of why frames run: who holds an owed frame, and whether a frame changed what is drawn (backlog F111). |
|
|
545
|
-
| `Core::frame_cause` | `kui_frame_cause` | `frameCause` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Why the frame being built runs: the input it answers by kind, what the driver noted, and `owed` after a frame that owed one (backlog F111). |
|
|
546
|
-
| `Core::begin_frame_cause` | *none: a C host's view runs between `kui_frame_begin` and `kui_frame_finish`, inside the frame it builds, so it reads that frame already* | `Ctx.beginFrameCause` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Starts the next frame's record ahead of its `begin_frame`, for a driver whose view runs before the frame it is for — Node's loop — so `frame_cause` and `owed_by` read from the view answer that frame (backlog RG81). |
|
|
547
|
-
| `Core::note_frame_cause` | `kui_note_frame_cause` | *none: the drivers that note a reason are kui-native's, which a `KuiWindow` runs on; a `Ctx` driven by hand has nothing but the input the core already records* | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | A driver adds a reason the core cannot see — a wake, a resize, a blink, a retry — to the next frame's (backlog F111). |
|
|
548
|
-
| `Core::owed_by` | *none: lists of named holders are strings the library would own across calls, an [out-array] struct and an ABI bump for a reading that is a debugging aid; a C host reads the kinds from `kui_owed`* | `owedBy` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Who holds the frame the last one left owed: `owed` with the nodes, slots and calling lines named (backlog F111). |
|
|
549
|
-
| `Core::frame_unchanged` | `kui_frame_unchanged` | `frameUnchanged` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the last finished frame drew exactly what the one before drew, traced (backlog F111). |
|
|
550
|
-
| `Ui::request_frame` | `animate` on a node, and `kui_animating` for the driver to read | the same form as C's | the same form as C's | Asks for a frame after this one; the driver paces off `animating()`. |
|
|
551
|
-
| `Ui::modifiers` | *none: the held modifiers ride on every key and pointer event's `mods`; the reader is what the stock editor's Shift-drag asks, inside the core* | *none: the same reason as C's* | *none: the same reason as C's* | The modifier keys held now. |
|
|
552
|
-
| `Ui::selection_text` | `kui_selection_text` | `selectionText` | `selection_text` | The window's selected text — a scope's, a grid's or the focused editor's. |
|
|
553
|
-
| `Ui::selection_html` | `kui_selection_html` | `selectionHtml` | `selection_html` | The same with the formatting the text declared. |
|
|
554
|
-
| `Ui::selection_ends` | `kui_selection_ends` | `selectionEnds` | `selection_ends` | A text selection's anchor and focus as row indices and bytes (ADR 0029). |
|
|
555
|
-
| `Ui::cell_selection` | `kui_cell_selection` | `cellSelection` | `cell_selection` | A `cells` grid's selection: its ends as absolute lines and columns, and whether it is a block (ADR 0017 §4). |
|
|
556
|
-
| `Ui::select_all_in` | `kui_select_all_in` | `selectAllIn` | `select_all_in` | Select All, scoped to a `selectable` node or a grid. |
|
|
557
|
-
| `Ui::clear_selection` | `kui_clear_selection` | `clearSelection` | `clear_selection` | Drops the window's selection. |
|
|
558
|
-
| `Ui::request_copy` | `kui_request_copy` | `requestCopy` | `request_copy` | Asks for the selection as a copy, which may come back as a `selectionrange` question. |
|
|
559
|
-
| `Ui::answer_selection_range` | `kui_answer_selection_range` | `answerSelectionRange` | `answer_selection_range` | The app's answer to that question. |
|
|
560
|
-
| `Ui::set_clipboard` | `kui_set_clipboard` | `setClipboard` | `set_clipboard` | A key sink's own Ctrl-C: posts a clipboard action for the host (backlog C33). |
|
|
561
|
-
| `Ui::set_clipboard_secret` | `kui_set_clipboard_secret` | `setClipboardSecret` | `set_clipboard_secret` | Posts a secret for the clipboard, which the host writes marked concealed and transient the way a password manager does, so no clipboard manager shows or keeps it (backlog F84). |
|
|
562
|
-
| `Ui::request_paste` | `kui_request_paste` | `requestPaste` | `request_paste` | A key sink's own Ctrl-V: the clipboard comes back as a commit, marked `concealed` / `transient` when the pasteboard said so (backlog F84). One ask at a time — a second while one is unanswered is dropped. |
|
|
563
|
-
| `Ui::awaiting_paste` | `kui_awaiting_paste` | `awaitingPaste` | `awaiting_paste` | Whether a paste asked for is still unanswered (backlog AR34). |
|
|
564
|
-
| `Ui::request_files` | `kui_request_files` | `requestFiles` | `request_files` | Asks for the platform's Open, Save or folder dialog; the answer is a `files` event to whoever asked. One at a time — a second while one is out is dropped (backlog C51). |
|
|
565
|
-
| `Ui::awaiting_files` | `kui_awaiting_files` | `awaitingFiles` | `awaiting_files` | Whether a file dialog asked for is still unanswered. |
|
|
566
|
-
| `Core::take_file_requests` | `kui_take_file_request`, then `kui_file_request_filter` per filter | `takeFileRequests` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Drains the dialog asked for, for a host that shows it itself; the runner does. The answer goes back as input (`Ctx.answerFiles`, `kui_input_files`). |
|
|
567
|
-
| `Core::set_lookup_available` | `kui_set_lookup_available` | `setLookupAvailable` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the host can show the platform's definition panel, which decides whether Look Up is offered. |
|
|
568
|
-
| `Ui::open_menu` | `kui_open_menu` | `openMenu` | `open_menu` | Opens a context menu on a node at a point. |
|
|
569
|
-
| `Ui::close_menu` | `kui_close_menu` | `closeMenu` | `close_menu` | Closes it. |
|
|
570
|
-
| `Core::take_menu_actions` | `kui_take_menu_action` | `takeMenuActions` | a chosen row comes back as a `menu` event on the node; the clipboard actions are the host's | Drains what a menu (or a chord, or the standard bar) asked of the host: a clipboard write, a paste, a Look Up. |
|
|
571
|
-
| `Core::menu` | `kui_menu_item_count` / `kui_menu_item`, one row at a time | `menu` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The open menu, for a host showing it natively. |
|
|
572
|
-
| `Core::set_native_menus` | `kui_set_native_menus` | `setNativeMenus` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the host shows menus itself; the core then draws none. |
|
|
573
|
-
| `Core::activate_menu_item` | `kui_activate_menu_item` | `activateMenuItem` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Reports that the host's own menu chose a row; a row that cannot be chosen (disabled, a separator) is refused and the menu stays open. |
|
|
574
|
-
| `Core::menu_bar` | `kui_menu_bar_menu_count` / `kui_menu_bar_menu` / `kui_menu_bar_item`, one row at a time | `menuBar` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The declared menu bar, for a host handing it to the OS. |
|
|
575
|
-
| `Core::set_native_menu_bar` | `kui_set_native_menu_bar` | `setNativeMenuBar` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the host owns the bar; the core then draws no strip. |
|
|
576
|
-
| `Core::activate_menu_bar_item` | `kui_activate_menu_bar_item` | `activateMenuBarItem` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Reports that the OS bar chose a row. |
|
|
577
|
-
| `Ui::window` | `kui_window_declare` | the root's `windows` prop | the root's `windows` field | Declares that a named window exists this frame (ADR 0003 step 3). |
|
|
578
|
-
| `Core::windows` | the ids arrive on `KUI_CMD_OPEN`; a host keeps the list it opened | `windows` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The names of the windows open now. |
|
|
579
|
-
| `Ui::window_name` | `kui_ctx_window_name` | `windowName` | `env.window.name`, a reading | The name of the window this context draws. |
|
|
580
|
-
| `Ui::set_window_size` | `kui_set_window_size` | `setWindowSize` | `set_window_size` | Asks the driver to resize a window. |
|
|
581
|
-
| `Ui::focus_window` | `kui_focus_window` | `focusWindow` | `focus_window` | Asks the driver to bring a window to the front. |
|
|
582
|
-
| `Ui::window_title` | `kui_window_title` | the root's `title` prop | the root's `title` field | Declares the window's title this frame. |
|
|
583
|
-
| `Ui::always_on_top` | `kui_set_always_on_top` | the root's `alwaysOnTop` prop | the root's `always_on_top` field | Declares that the window sits above every other app's this frame (backlog C30). |
|
|
584
|
-
| `Ui::secure_input` | `kui_set_secure_input` | the root's `secureInput` prop | the root's `secure_input` field | Declares that this frame wants secure keyboard entry while the window has the keyboard — a password prompt (backlog F85). |
|
|
585
|
-
| `Ui::option_as_alt` | `kui_set_option_as_alt` | the root's `optionAsAlt` prop | the root's `option_as_alt` field | Declares which Option keys act as Alt in this window on macOS, so a dead key like ⌥u arrives as `<A-u>` (backlog F113). |
|
|
586
|
-
| `Ui::ime_off` | `kui_set_ime_off` | the root's `imeOff` prop | the root's `ime_off` field | Declares that this window takes the keyboard as keys, with the input method off — no composition, and on a Mac no dead keys and no press-and-hold, so a held letter repeats (backlog F125). |
|
|
587
|
-
| `Ui::window_command` | the chrome roles (`KuiSpec.window_role`) are the door; the verb is what `widgets::window_buttons` lowers to | `KuiWindow.close()` for the one command the runner takes from outside a frame; the rest are `windowRole` | `window_role` | Minimize, toggle-maximize, start-drag, close — what a chrome node asks for on a press. |
|
|
588
|
-
| `Core::window_title` | `kui_window_title_get` | `Ctx.windowTitle` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | What the frame declared, for a driver applying it; a `KuiWindow` applies its own. |
|
|
589
|
-
| `Core::always_on_top` | `kui_always_on_top_get` | `Ctx.alwaysOnTop` | `env.window.always_on_top`, a reading | The same for the level. |
|
|
590
|
-
| `Core::secure_input` | `kui_secure_input_get` | `Ctx.secureInput` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The same for the secure-input ask: what a driver with its own loop reads to make the platform call; the runner makes it for a `KuiWindow` and `kui_run`. |
|
|
591
|
-
| `Core::option_as_alt` | `kui_option_as_alt_get` | `Ctx.optionAsAlt` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The same for the Option-as-Alt ask: what a driver with its own loop reads to apply it to its window; the runner applies it for a `KuiWindow` and `kui_run`. |
|
|
592
|
-
| `Core::ime_off` | `kui_ime_off_get` | `Ctx.imeOff` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The same for the input-method ask: what a driver with its own loop reads to apply it to its window; the runner applies it for a `KuiWindow` and `kui_run`. |
|
|
593
|
-
| `Core::take_window_commands` | `kui_take_window_command` | `Ctx.windowCommands` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Drains what the frame asked of the driver: open, close, resize, focus, redraw. |
|
|
594
|
-
| `Core::window_closed` | `kui_window_closed` | `Ctx.windowClosed` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The driver reports a window gone. |
|
|
595
|
-
| `Core::dismiss_window` | `kui_window_dismissed` | `Ctx.windowDismissed` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The driver reports a popup dismissed, with why (ADR 0003 step 4). |
|
|
596
|
-
| `Core::set_theme` | `kui_theme_set` | `setTheme` | *none: read-only: the palette is the host's (ADR 0019)* | Pins a whole palette. |
|
|
597
|
-
| `Core::set_accent` | `kui_theme_set_accent` | `setAccent` | *none: as `set_theme`* | Pins an accent and keeps the OS's base. |
|
|
598
|
-
| `Ui::theme` | `kui_theme` | `theme` | `env.theme`, a reading | The palette in effect (`THEME_ROLES`). |
|
|
599
|
-
| `Core::set_metrics` | `kui_metrics_set` | `setMetrics` | *none: as `set_theme` (backlog T2)* | Pins the stock widgets' sizes. |
|
|
600
|
-
| `Ui::metrics` | `kui_metrics` | `metrics` | `env.metrics`, a reading | The sizes in effect (`METRIC_ROLES`). |
|
|
601
|
-
| `Ui::set_tokens` | `kui_tokens_set` | `setTokens` | `set_tokens` | Declares the origin's colour and length tokens (ADR 0027). |
|
|
602
|
-
| `Tokens::derive` | `kui_tokens_derive` | a colour with `from` in `setTokens` | a colour with `from` in `set_tokens` | Adds derived colours to the declared ones (ADR 0028). |
|
|
603
|
-
| `Ui::tokens` | `kui_token_color` / `kui_token_length`, one name at a time | `tokens` | `env.tokens`, a reading | The tokens in effect, resolved for the appearance. |
|
|
604
|
-
| `Core::tokens_declared` | *none: a plugin declares in every `kui_ext_view` and pays the parse; a reader that lets it skip the second is one line, once a plugin asks for it* | *none: an app declares once, before its loop* | *none: the `tokens` global is declared once, at load* | Whether an origin declared tokens. |
|
|
605
|
-
| `Core::set_diagnostics` | `kui_set_diagnostics` | `setDiagnostics` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Turns the per-frame checks on. |
|
|
606
|
-
| `Core::take_warnings` | `kui_take_warnings` | `warnings` | *none: the host drains and the Lua runner prints* | Drains the warnings raised since the last call. |
|
|
607
|
-
| `Core::warnings_raised` | *none: the C smoke round drains `kui_take_warnings` after each frame; a non-draining reader waits for a C harness that needs one* | `warningsRaised` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The warnings raised so far, undrained, which is what an example's self-check reads (ADR 0021). |
|
|
608
|
-
| `Core::set_devtools` | `kui_set_devtools` | `setDevtools` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Turns the devtools panel on. |
|
|
609
|
-
| `Core::devtools` | `kui_devtools` | `devtools` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether it is on. |
|
|
610
|
-
| `Core::set_devtools_dock` | `kui_set_devtools_dock` | `setDevtoolsDock` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Where it sits. |
|
|
611
|
-
| `Core::devtools_dock` | `kui_devtools_dock` | `devtoolsDock` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Where it sits, read back. |
|
|
612
|
-
| `Core::host_rect` | `kui_host_rect` | `hostArea` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Where the frame laid the host out in the window, logical px: the viewport with its origin, which is what tells the app's quads from the dock's (backlog F92). |
|
|
613
|
-
| `Core::set_devtools_theme` | `kui_set_devtools_theme` | `setDevtoolsTheme` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Seeds the panel's theme override. |
|
|
614
|
-
| `Core::set_devtools_key` | `kui_set_devtools_key` | `setDevtoolsKey` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Respells the chord that moves the keyboard into the panel (`Ctrl+Shift+I` by default). |
|
|
615
|
-
| `Core::devtools_key` | `kui_devtools_key` | `devtoolsKey` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | That chord, read back in its portable spelling. |
|
|
616
|
-
| `Ui::devtools_tab` | `kui_devtools_tab` | `<devtoolsTab name label slot/>` | `devtools_tab { name=, label=, slot= }` | Declares a devtools tab an extension fills through the slot named (ADR 0032). |
|
|
617
|
-
| `Ui::devtools_tab_with` | `kui_devtools_tab_open` | `<devtoolsTab name label>{() => …}</devtoolsTab>`, the function child called only while the tab is on show | `devtools_tab { name=, label=, view = function(env) … end }`, called only while the tab is on show | Declares a devtools tab the host draws itself, and draws it only while it is on show. |
|
|
618
|
-
| `Core::devtools_shown_tab` | *none: a C host's open answers whether the tab is on show (`kui_devtools_tab_open`); nothing encodes ahead of the core there* | `devtoolsShownTab` | *none: the runner's converter reads it for the script (ADR 0032, decision 3)* | The declared devtools tab on show, which a data binding reads once a frame to call the tab's function. |
|
|
619
|
-
| `Core::devtools_selected` | `kui_devtools_selected` | `devtoolsSelected` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The node the panel's tree tab has selected (ADR 0032, decision 4). |
|
|
620
|
-
| `Core::devtools_hovered` | `kui_devtools_hovered` | `devtoolsHovered` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The tree row under the pointer. |
|
|
621
|
-
| `Core::devtools_picked` | `kui_devtools_picked` | `devtoolsPicked` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The node the picker is over. |
|
|
622
|
-
| `Core::set_devtools_pick` | `kui_set_devtools_pick` | `setDevtoolsPick` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Raises the panel's picker from outside it, or puts it away; raised from a declared tab, the pick lands in `devtools_selected` and the tab stays up. |
|
|
623
|
-
| `Core::devtools_picking` | `kui_devtools_picking` | `devtoolsPicking` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the picker is up. |
|
|
624
|
-
| `Core::set_devtools_selected` | `kui_set_devtools_selected` | `setDevtoolsSelected` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Selects and reveals a node in the tree tab from outside the panel. |
|
|
625
|
-
| `Core::set_devtools_tab` | `kui_set_devtools_tab` | `setDevtoolsTab` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Shows the panel's tab named — one of its own, in any case, or a declared one, as declared — from outside the panel, as the strip's click does; a hidden panel comes back docked. |
|
|
626
|
-
| `Core::devtools_current_tab` | `kui_devtools_current_tab` | `devtoolsCurrentTab` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The tab the panel is on, by name. |
|
|
627
|
-
| `Core::set_devtools_legend` | `kui_set_devtools_legend` | `setDevtoolsLegend` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The key legend the panel's facts tab shows. |
|
|
628
|
-
| `Core::set_inspect` | `kui_set_inspect` | `setInspect` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Turns the per-frame node snapshot behind `nodes` on. |
|
|
629
|
-
| `Core::nodes` | `kui_nodes` | `nodes` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The last frame's nodes with what layout and the declarations made of them — a tree view's and an inspector's data. |
|
|
630
|
-
| `Ui::add_extension` | `kui_ctx_add_extension` | `Ctx.addExtension` | `add_extension` | Loads a plugin under a namespace; a `KuiWindow` takes its list at construction (`extensions`). |
|
|
631
|
-
| `Launcher::extensions` | `kui_ctx_extension_count` / `kui_ctx_extension_namespace`, one at a time | `Ctx.extensionNamespaces` | `extension_namespaces` | The namespaces loaded. |
|
|
632
|
-
| `Core::frame` | `kui_frame_begin` … `kui_frame_finish` | `Ctx.frame` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Runs one frame: the view, layout, the draw list; `KuiWindow.setView` is the windowed form, the runner calling it. |
|
|
633
|
-
| `Core::output` | `kui_draw_data` | `quads` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The draw list: quads, clips, fragment and texture draws (`clips`, `fragmentDraws`, `textureDraws` beside `quads` in Node) and the frame's stats. |
|
|
634
|
-
| `Core::take_pending_events` | `kui_poll_event` | `pollEvents` | `on_event(ev)`, pushed after each frame | What the frame and the input since produced, for `update`. |
|
|
635
|
-
| `Core::set_time` | `kui_set_time` | `Ctx.setTime` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The clock the tweens read; a window's runner sets it from the display. |
|
|
636
|
-
| `Core::env` | `kui_env_set` and its four siblings, `ENV_FIELDS`' C column | `Ctx.setEnv` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The host facts written in (the `env` field); a `KuiWindow`'s runner writes its own. |
|
|
637
|
-
| `Ui::env` | *none: C is the host, so it writes the facts and has no reading (`ENV_FIELDS`)* | `env` | `env`, the view's argument | The facts read back, `ENV_FIELDS` row for row. |
|
|
638
|
-
| `Core::handle_input` | `kui_input_cursor` … `kui_input_access`, one per `InputEvent` | `Ctx.cursor` … `Ctx.access`, one per `InputEvent`; a `KuiWindow` refuses injection | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Pointer, wheel, key, text, IME, assistive and OS file-drag input; a wheel gesture's latching is `scroll_gesture` (`kui_input_scroll_gesture`, `Ctx.scrollGesture`, backlog F107); `press` / `release` are a click by label (`kui_input_press`, `Ctx.press`); the file drag is `drag_files` / `drop_files` / `drag_cancel` (ADR 0031); a file dialog's answer is `answer_files` (`kui_input_files`, `Ctx.answerFiles`, backlog C51); the documents the OS asked the app to open are `InputEvent::Open` (`kui_input_open`, `Ctx.openDocuments`, backlog F124). |
|
|
639
|
-
| `Core::modifiers` | `kui_input_modifiers` | `Ctx.modifiers` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The modifier state, reported on its own when the OS does (backlog AR22). |
|
|
640
|
-
| `Core::release_held_keys` | `kui_release_held_keys` | `Ctx.setEnv({focused: false})` releases, as losing the keyboard does for every driver (ADR 0020) | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Lets go of every key the focused sink holds. |
|
|
641
|
-
| `Core::set_subpixel_text` | `kui_set_subpixel_text` | *none: a Node host never paints: the renderer behind `KuiWindow` is the runner's, and a headless `Ctx` has none* | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | LCD subpixel coverage for outline glyphs, for a renderer that blends per channel. |
|
|
642
|
-
| `Core::take_audio_commands` | `kui_take_audio_commands` | `Ctx.audioCommands` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Drains what the frame asked of the audio device; a `KuiWindow`'s device is the runner's. |
|
|
643
|
-
| `Core::audio_ended` | `kui_audio_ended` | `Ctx.audioEnded` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The device reports a playback over. |
|
|
644
|
-
| `Core::audio_truncated` | `kui_audio_truncated` | `Ctx.audioTruncated` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The device reports a stop that cut a playback short — a one-shot node's removal becomes `truncated-playback`. |
|
|
645
|
-
| `Core::audio_refused` | `kui_audio_refused` | `Ctx.audioRefused` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The device reports a play it would not take — a `refused` sound event and `playback-refused`. |
|
|
646
|
-
| `Launcher::size` | `width` / `height` in the `KuiRunConfig` `kui_run_with` takes | `width` / `height` in `WindowOptions` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The window's opening size; `min_size` / `max_size` / `chrome` / `text_aa` / `diagnostics` / `frame_latency` are the rest of the set, and each binding's form carries them all (`min_w`, `chrome`, `text_aa`, `diagnostics`, `frame_latency` in C; `minWidth`, `chrome`, `textAa`, `diagnostics`, `frameLatency` in Node). `Launcher::devtools` and `Launcher::core` are the two the others reach another way: `kui_set_devtools` / `setDevtools` on the context, and the context handed to `kui_run_with` *is* the core. |
|
|
647
|
-
| `Launcher::icon` | `kui_set_icon` | `icon` in `WindowOptions` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The icon every window of the app is created with — RGBA pixels and their size — shown by Windows in the title bar, Alt-Tab and the taskbar and by X11's window manager; macOS (the bundle's `.icns`) and Wayland (the `.desktop` file's) have no window icon (backlog F86). `Launcher::icon_resource` is the Windows executable's own icon resource, which wins there — C's `resource` argument, Node's `icon.resource`. C's is a free function called before `kui_run`, for `kui_on_teardown`'s reason. |
|
|
648
|
-
| `App::teardown` | `kui_on_teardown` | `KuiWindow.onTeardown` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The window going for good — its close button, Quit from the menu or the dock, a close command on it, a pumped runner ended — heard once, before `run` returns or the process exits, with nothing drawing: the place to keep what the app would lose with the window (backlog F74, the other two hosts under RG1). On macOS a Quit ends the process from inside the loop, so this is the only thing an app runs on ⌘Q — nothing after `run`, `kui_run` or `await runWindowed(...)` does, not even `process.on('exit')`. C's is a free function called before `kui_run`, with the run's `user`, since `kui_run`'s app is three arguments and not a struct. Node's is the window's door, called from inside the pump that saw the window go; `runWindowed` registers its config's `teardown(model)` there, and `createApp`'s `app.teardown()` runs the same one for a headless drive. |
|
|
479
|
+
| verb | C | Odin | Node | Lua | description |
|
|
480
|
+
|---|---|---|---|---|---|
|
|
481
|
+
| `SharedResources::add_image` | `kui_image_add` | `image_add` | `addImage` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers RGBA pixels and mints an id for `<image src>`. |
|
|
482
|
+
| `Core::update_image` | `kui_image_update` | `image_update` | `updateImage` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Replaces the pixels behind a live id, keeping the id (ADR 0025). |
|
|
483
|
+
| `Core::remove_image` | `kui_image_remove` | `image_remove` | `removeImage` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Drops an image; every window's atlas lets it go (backlog AR8). |
|
|
484
|
+
| `Core::image_pixels` | `kui_image_pixels` | `image_pixels` | *none: a Node host never paints: the renderer behind `KuiWindow` is the runner's, and a headless `Ctx` has none* | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The pixels behind a handle, for a renderer meeting a texture quad. |
|
|
485
|
+
| `Core::parse_path` | `kui_path_parse` | `path_parse` | `d` on `<path>` is the string; the addon hands it to this parser | `d` on `path { }` is the string; the host hands it to this parser | SVG path data to the flat op form a `path` draws (ADR 0040): one parser, so every binding draws the same shape. |
|
|
486
|
+
| `Core::add_fragment` | `kui_fragment_add` | `fragment_add` | `addFragment` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers a WGSL function and mints an id for `<fragment src>` (ADR 0015). |
|
|
487
|
+
| `Core::remove_fragment` | `kui_fragment_remove` | `fragment_remove` | `removeFragment` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Drops a fragment; the renderer drops its pipelines (backlog AR8). |
|
|
488
|
+
| `Core::fragment_module_source` | `kui_fragment_source` | `fragment_source` | *none: a Node host never paints: the renderer behind `KuiWindow` is the runner's, and a headless `Ctx` has none* | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The whole WGSL module behind a handle, which is what a renderer compiles. |
|
|
489
|
+
| `Core::add_font_data` | `kui_font_add` | `font_add` | `addFont` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers a font's bytes and mints an id for `font`. |
|
|
490
|
+
| `Core::add_system_font` | `kui_font_add_system` | `font_add_system` | `addSystemFont` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers an installed family by name. |
|
|
491
|
+
| `Core::load_font_file` | `kui_font_load_file` | `font_load_file` | `loadFontFile` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers a font file by path. |
|
|
492
|
+
| `Core::load_fonts_dir` | `kui_font_load_dir` | `font_load_dir` | `loadFontsDir` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers every font file in a directory. |
|
|
493
|
+
| `Core::reload_system_fonts` | `kui_font_reload_system` | `font_reload_system` | `reloadSystemFonts` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Scans the system's fonts again, so a font installed while the app runs is found (the scan is otherwise once a process); returns how many faces came and went. |
|
|
494
|
+
| `Core::set_fallback_fonts` | `kui_font_set_fallback` | `font_set_fallback` | `setFallbackFonts` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The fonts asked, in order, for a character the text's own family lacks, before the platform's fallback list (backlog F121). |
|
|
495
|
+
| `Core::fallback_fonts` | *none: the list is the one the host set* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | *none: the list is the one the host set* | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The families `set_fallback_fonts` named, in order. |
|
|
496
|
+
| `Core::remove_font` | `kui_font_remove` | `font_remove` | `removeFont` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Drops a font. |
|
|
497
|
+
| `Core::system_font_families` | `kui_font_families` | `font_families` | `systemFontFamilies` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The installed family names `add_system_font` accepts. |
|
|
498
|
+
| `Core::system_fonts` | `kui_system_fonts` | `system_fonts` | `systemFonts` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | The same families, each with what the font database read off its faces: `monospaced` (every face fixed-pitch), `weights`, `italic` (backlog F97) — a font picker's monospaced-first list without a file loaded or a glyph shaped. |
|
|
499
|
+
| `Core::add_sound` | `kui_sound_add` | `sound_add` | `addSound` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Registers a sound's bytes and mints an id for `<audio src>`, `clickSound` and `play`. |
|
|
500
|
+
| `Core::remove_sound` | `kui_sound_remove` | `sound_remove` | `removeSound` | *none: a script owns no handle: the host registers and the script names the id it was given (`image { id = }`, `font = id`, `audio { src = id }`)* | Drops a sound. |
|
|
501
|
+
| `Core::set_text_cache_budget` | `kui_set_text_cache_budget` | `set_text_cache_budget` | `setTextCacheBudget` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The shaped-text cache's byte budget (backlog C16). |
|
|
502
|
+
| `Core::text_cache_bytes` | `kui_text_cache_bytes` | `text_cache_bytes` | `textCacheBytes` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | What the shaped-text cache holds. |
|
|
503
|
+
| `Ui::play` | `kui_play` | `play` | `play` | *none: a script owns no sound handle, and its env is the view's: a playback started there would start again every frame — `audio { src = id }` is the declarative form, and what a script has* | Starts a playback of a registered sound, outside any node; answers the playback id. |
|
|
504
|
+
| `Core::stop` | `kui_stop` | `stop` | `stop` | *none: as `play`: a script declares `audio { }` and stops it by not declaring it* | Stops a playback, with an optional fade. |
|
|
505
|
+
| `Core::set_volume` | `kui_set_volume` | `set_volume` | `setVolume` | `audio { volume = }` applies live | A playback's volume, with an optional tween. |
|
|
506
|
+
| `Core::pause` | `kui_pause` | `pause` | `pause` | `audio { paused = true }` applies live | Pauses a playback. |
|
|
507
|
+
| `Core::resume` | `kui_resume` | `resume` | `resume` | `audio { paused = false }` | Resumes a paused playback. |
|
|
508
|
+
| `Core::set_master_volume` | `kui_set_master_volume` | `set_master_volume` | `setMasterVolume` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The device's master volume, with an optional tween. |
|
|
509
|
+
| `Ui::announce` | `kui_announce` | `announce` | `announce` | `announce` | Says something once with no node behind it (ADR 0001). |
|
|
510
|
+
| `Core::take_announcements` | `kui_take_announcements` | `take_announcements` | `Ctx.announcements` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Drains what was announced, for a host bridging assistive technology; a `KuiWindow`'s bridge is the runner's. |
|
|
511
|
+
| `Core::access_tree` | `kui_access_tree` | `access_tree` | `accessTree` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The access tree of the last finished frame (ADR 0001). |
|
|
512
|
+
| `Ui::focus` | `kui_focus` | `focus` | `focus` | `set_focus` | Moves focus to a node now; an app's move stands over a modal's restore (backlog AR17). `keyFocus` is the declarative, edge-triggered form. |
|
|
513
|
+
| `Ui::blur` | `kui_focus(ctx, 0)` | `focus(ui, 0)` | `blur` | `blur` | Drops focus. |
|
|
514
|
+
| `Ui::focus_next` | `kui_focus_next` | `focus_next` | `focusNext` | `focus_next` | Steps the Tab ring forward (ADR 0002). |
|
|
515
|
+
| `Ui::focus_prev` | `kui_focus_next(ctx, false)` | `focus_next(ui, false)` | `focusPrev` | `focus_prev` | Steps the Tab ring backward. |
|
|
516
|
+
| `Ui::focus_region` | `kui_focus_region` | `focus_region` | `focusRegion` | `focus_region` | Enters a `focusRegion`'s ring, or leaves it for the main one (ADR 0022). |
|
|
517
|
+
| `Ui::region` | `kui_region` | `region` | `region` | `env.region`, a reading | The region in effect. |
|
|
518
|
+
| `Ui::focused` | `kui_focused` | `focused` | `focused` | `env.focus`, a reading | The focused node's key. |
|
|
519
|
+
| `Ui::is_focused` | `kui_is_focused` | `is_focused` | `isFocused` | `is_focused` | Whether a node has focus. |
|
|
520
|
+
| `Ui::focus_visible` | `kui_focus_visible` | `focus_visible` | `focusVisible` | `env.focus_visible`, a reading | Whether focus came from the keyboard and the ring should show. |
|
|
521
|
+
| `Ui::key_of` | `kui_key_of` | `key_of` | `keyOf` | every query and verb takes the label itself (`key_query`) | The key a label names this frame. |
|
|
522
|
+
| `Core::label_of` | *none: the label is the app's own word for the node, and every door names a node by it or by the key an event carried; the one reader is the devtools' inspector, in the core* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | *none: the same reason as C's* | *none: the same reason as C's* | The label a key was opened under. |
|
|
523
|
+
| `Ui::caret_visible` | `kui_caret_visible` | `caret_visible` | `caretVisible` | `env.caret_visible`, a reading | The blink phase a custom editor draws its caret on. |
|
|
524
|
+
| `Core::set_caret_visible` | `kui_set_caret_visible` | `set_caret_visible` | `setCaretVisible` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The host's blink clock writes the phase. |
|
|
525
|
+
| `Core::has_caret` | `kui_has_caret` | `has_caret` | `hasCaret` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether anything focused draws a caret to blink — a `caretSolid` line's is not one — which arms a host's blink clock. |
|
|
526
|
+
| `Core::caret_stamp` | `kui_caret_stamp` | `caret_stamp` | the loop in `index.js` runs the blink from `nextDeadlineMs`; a headless `Ctx` never blinks | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Changes when the caret moves or focus does, which re-arms the clock solid. |
|
|
527
|
+
| `Ui::is_hovered` | `kui_is_hovered` | `is_hovered` | `isHovered` | `is_hovered` | Whether the pointer is over a node. |
|
|
528
|
+
| `Ui::is_pressed` | `kui_is_pressed` | `is_pressed` | `isPressed` | `is_pressed` | Whether a press started on a node and the pointer is still over it. |
|
|
529
|
+
| `Ui::is_drop_target` | `kui_is_drop_target` | `is_drop_target` | `isDropTarget` | `is_drop_target` | Whether files dragged in from the OS are over a node (ADR 0031) — for drop-dependent layout; the colour is `drop_bg`. |
|
|
530
|
+
| `Core::drop_target` | `kui_drop_target` | `drop_target` | `dropTarget` | `drop_target` | The drop zone the dragged files are over, if any — what a driver answers the OS with, and what a test reads to say a zone was found (ADR 0031, decision 5). |
|
|
531
|
+
| `Ui::is_group_hovered` | `hoverBg` / `pressedBg` on a `hoverGroup` member paint it; the reader is what the Rust widgets ask when they paint by hand | `hover_bg` / `pressed_bg` on a `hover_group` member paint it | the same form as C's | the same form as C's | Whether any member of a hover group is hovered (`is_group_pressed` the same for a press). |
|
|
532
|
+
| `Core::cursor` | *none: the pointer's position is the driver's own fact — it injected it* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | *none: the same reason as C's* | *none: the same reason as C's, one step removed* | Where the pointer is, in logical viewport px. |
|
|
533
|
+
| `Core::cursor_shape` | `kui_cursor_shape` | `cursor_shape` | `cursorShape` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The pointer shape the frame asks for, which the host sets on its window. |
|
|
534
|
+
| `Ui::layout_of` | `kui_layout_of` | `layout_of` | `layoutOf` | `layout_of` | Where layout put a node last frame (backlog C26). |
|
|
535
|
+
| `Ui::scroll_offset` | `kui_scroll_offset` | `scroll_offset` | `scrollOffset` | `scroll_offset` | A scrolling node's offset. |
|
|
536
|
+
| `Ui::scroll_geometry` | `kui_scroll_geometry` | `scroll_geometry` | `scrollGeometry` | `scroll_geometry` | A scrolling node's viewport and content sizes. |
|
|
537
|
+
| `Ui::set_scroll` | `kui_set_scroll` | `set_scroll` | `setScroll` | `set_scroll` | Scrolls a node to an offset. |
|
|
538
|
+
| `Ui::shift_scroll` | `kui_shift_scroll` | `shift_scroll` | `shiftScroll` | `shift_scroll` | Moves a node's scroll by content that moved under it, with no ease: a variable-height list's anchor (backlog C46). |
|
|
539
|
+
| `Ui::reveal` | `kui_reveal` | `reveal` | `reveal` | `reveal` | Scrolls whatever encloses a node until it is in view. |
|
|
540
|
+
| `Ui::text_hit` | `kui_text_hit` | `text_hit` | `textHit` | `text_hit` | The byte and line under a point in a node's text (backlog C18). |
|
|
541
|
+
| `Ui::caret_rect` | `kui_caret_rect` | `caret_rect` | `caretRect` | `caret_rect` | The caret rect for a byte offset in a node's text. |
|
|
542
|
+
| `Core::ime_rect` | `kui_ime_rect` | `ime_rect` | `imeRect` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Where the OS candidate window goes, which the host hands to the platform (backlog C17). |
|
|
543
|
+
| `Ui::measure_text` | `kui_measure_text` | `measure_text` | `measureText` | `measure_text` | Shapes text in a style at a width and answers its size and line count. |
|
|
544
|
+
| `Ui::measure_rich_text` | `kui_measure_rich_text` | `measure_rich_text` | `measureText` takes spans too | `measure_text` takes spans too | The same for spans, shaped as one paragraph. |
|
|
545
|
+
| `Ui::edit_text` | `kui_edit_text` | `edit_text` | `editText` | `edit_text` | An editor's text, by key or by label. |
|
|
546
|
+
| `Ui::set_edit_text` | `kui_edit_set_text` | `edit_set_text` | `setEditText` | `set_edit_text` | Replaces an editor's text, caret at the end. |
|
|
547
|
+
| `Ui::set_edit_text_by_label` | `kui_edit_set_text_label` | `edit_set_text_label` | `setEditText` takes the label too | `set_edit_text` takes the label too | The same by the label an editor's `key` declares, which reaches one the frame is about to declare (backlog AR26). |
|
|
548
|
+
| `Core::animating` | `kui_animating` | `animating` | `animating` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the last frame left a transition mid-flight, so the host draws another without waiting for input. |
|
|
549
|
+
| `Core::owed` | `kui_owed` | `owed` | `owed` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The same by kind — a finite transition, a keyframe cycle, a departing ghost, a requested frame, an autoscroll — so a test can wait for the transitions to run out under a cycle that never ends; Node's loop has `quiet()` for that wait (backlog F64). |
|
|
550
|
+
| `Core::set_frame_trace` | `kui_set_frame_trace` | `set_frame_trace` | `setFrameTrace` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Turns on the trace of why frames run: who holds an owed frame, and whether a frame changed what is drawn (backlog F111). |
|
|
551
|
+
| `Core::frame_cause` | `kui_frame_cause` | `frame_cause` | `frameCause` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Why the frame being built runs: the input it answers by kind, what the driver noted, and `owed` after a frame that owed one (backlog F111). |
|
|
552
|
+
| `Core::begin_frame_cause` | *none: a C host's view runs between `kui_frame_begin` and `kui_frame_finish`, inside the frame it builds, so it reads that frame already* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | `Ctx.beginFrameCause` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Starts the next frame's record ahead of its `begin_frame`, for a driver whose view runs before the frame it is for — Node's loop — so `frame_cause` and `owed_by` read from the view answer that frame (backlog RG81). |
|
|
553
|
+
| `Core::note_frame_cause` | `kui_note_frame_cause` | `note_frame_cause` | *none: the drivers that note a reason are kui-native's, which a `KuiWindow` runs on; a `Ctx` driven by hand has nothing but the input the core already records* | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | A driver adds a reason the core cannot see — a wake, a resize, a blink, a retry — to the next frame's (backlog F111). |
|
|
554
|
+
| `Core::owed_by` | *none: lists of named holders are strings the library would own across calls, an [out-array] struct and an ABI bump for a reading that is a debugging aid; a C host reads the kinds from `kui_owed`* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | `owedBy` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Who holds the frame the last one left owed: `owed` with the nodes, slots and calling lines named (backlog F111). |
|
|
555
|
+
| `Core::frame_unchanged` | `kui_frame_unchanged` | `frame_unchanged` | `frameUnchanged` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the last finished frame drew exactly what the one before drew, traced (backlog F111). |
|
|
556
|
+
| `Ui::request_frame` | `animate` on a node, and `kui_animating` for the driver to read | `animate` on a node, and `animating` for the driver to read | the same form as C's | the same form as C's | Asks for a frame after this one; the driver paces off `animating()`. |
|
|
557
|
+
| `Ui::modifiers` | *none: the held modifiers ride on every key and pointer event's `mods`; the reader is what the stock editor's Shift-drag asks, inside the core* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | *none: the same reason as C's* | *none: the same reason as C's* | The modifier keys held now. |
|
|
558
|
+
| `Ui::selection_text` | `kui_selection_text` | `selection_text` | `selectionText` | `selection_text` | The window's selected text — a scope's, a grid's or the focused editor's. |
|
|
559
|
+
| `Ui::selection_html` | `kui_selection_html` | `selection_html` | `selectionHtml` | `selection_html` | The same with the formatting the text declared. |
|
|
560
|
+
| `Ui::selection_ends` | `kui_selection_ends` | `selection_ends` | `selectionEnds` | `selection_ends` | A text selection's anchor and focus as row indices and bytes (ADR 0029). |
|
|
561
|
+
| `Ui::cell_selection` | `kui_cell_selection` | `cell_selection` | `cellSelection` | `cell_selection` | A `cells` grid's selection: its ends as absolute lines and columns, and whether it is a block (ADR 0017 §4). |
|
|
562
|
+
| `Ui::select_all_in` | `kui_select_all_in` | `select_all_in` | `selectAllIn` | `select_all_in` | Select All, scoped to a `selectable` node or a grid. |
|
|
563
|
+
| `Ui::clear_selection` | `kui_clear_selection` | `clear_selection` | `clearSelection` | `clear_selection` | Drops the window's selection. |
|
|
564
|
+
| `Ui::request_copy` | `kui_request_copy` | `request_copy` | `requestCopy` | `request_copy` | Asks for the selection as a copy, which may come back as a `selectionrange` question. |
|
|
565
|
+
| `Ui::answer_selection_range` | `kui_answer_selection_range` | `answer_selection_range` | `answerSelectionRange` | `answer_selection_range` | The app's answer to that question. |
|
|
566
|
+
| `Ui::set_clipboard` | `kui_set_clipboard` | `set_clipboard` | `setClipboard` | `set_clipboard` | A key sink's own Ctrl-C: posts a clipboard action for the host (backlog C33). |
|
|
567
|
+
| `Ui::set_clipboard_secret` | `kui_set_clipboard_secret` | `set_clipboard_secret` | `setClipboardSecret` | `set_clipboard_secret` | Posts a secret for the clipboard, which the host writes marked concealed and transient the way a password manager does, so no clipboard manager shows or keeps it (backlog F84). |
|
|
568
|
+
| `Ui::request_paste` | `kui_request_paste` | `request_paste` | `requestPaste` | `request_paste` | A key sink's own Ctrl-V: the clipboard comes back as a commit, marked `concealed` / `transient` when the pasteboard said so (backlog F84). One ask at a time — a second while one is unanswered is dropped. |
|
|
569
|
+
| `Ui::awaiting_paste` | `kui_awaiting_paste` | `awaiting_paste` | `awaitingPaste` | `awaiting_paste` | Whether a paste asked for is still unanswered (backlog AR34). |
|
|
570
|
+
| `Ui::request_files` | `kui_request_files` | `request_files` | `requestFiles` | `request_files` | Asks for the platform's Open, Save or folder dialog; the answer is a `files` event to whoever asked. One at a time — a second while one is out is dropped (backlog C51). |
|
|
571
|
+
| `Ui::awaiting_files` | `kui_awaiting_files` | `awaiting_files` | `awaitingFiles` | `awaiting_files` | Whether a file dialog asked for is still unanswered. |
|
|
572
|
+
| `Core::take_file_requests` | `kui_take_file_request`, then `kui_file_request_filter` per filter | `take_file_request`, then `file_request_filter` per filter | `takeFileRequests` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Drains the dialog asked for, for a host that shows it itself; the runner does. The answer goes back as input (`Ctx.answerFiles`, `kui_input_files`). |
|
|
573
|
+
| `Core::set_lookup_available` | `kui_set_lookup_available` | `set_lookup_available` | `setLookupAvailable` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the host can show the platform's definition panel, which decides whether Look Up is offered. |
|
|
574
|
+
| `Ui::open_menu` | `kui_open_menu` | `open_menu` | `openMenu` | `open_menu` | Opens a context menu on a node at a point. |
|
|
575
|
+
| `Ui::close_menu` | `kui_close_menu` | `close_menu` | `closeMenu` | `close_menu` | Closes it. |
|
|
576
|
+
| `Core::take_menu_actions` | `kui_take_menu_action` | `take_menu_action` | `takeMenuActions` | a chosen row comes back as a `menu` event on the node; the clipboard actions are the host's | Drains what a menu (or a chord, or the standard bar) asked of the host: a clipboard write, a paste, a Look Up. |
|
|
577
|
+
| `Core::menu` | `kui_menu_item_count` / `kui_menu_item`, one row at a time | `menu_item_count` / `menu_item`, one row at a time | `menu` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The open menu, for a host showing it natively. |
|
|
578
|
+
| `Core::set_native_menus` | `kui_set_native_menus` | `set_native_menus` | `setNativeMenus` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the host shows menus itself; the core then draws none. |
|
|
579
|
+
| `Core::activate_menu_item` | `kui_activate_menu_item` | `activate_menu_item` | `activateMenuItem` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Reports that the host's own menu chose a row; a row that cannot be chosen (disabled, a separator) is refused and the menu stays open. |
|
|
580
|
+
| `Core::menu_bar` | `kui_menu_bar_menu_count` / `kui_menu_bar_menu` / `kui_menu_bar_item`, one row at a time | `menu_bar_menu_count` / `menu_bar_menu` / `menu_bar_item`, one row at a time | `menuBar` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The declared menu bar, for a host handing it to the OS. |
|
|
581
|
+
| `Core::set_native_menu_bar` | `kui_set_native_menu_bar` | `set_native_menu_bar` | `setNativeMenuBar` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the host owns the bar; the core then draws no strip. |
|
|
582
|
+
| `Core::activate_menu_bar_item` | `kui_activate_menu_bar_item` | `activate_menu_bar_item` | `activateMenuBarItem` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Reports that the OS bar chose a row. |
|
|
583
|
+
| `Ui::window` | `kui_window_declare` | `window_declare` | the root's `windows` prop | the root's `windows` field | Declares that a named window exists this frame (ADR 0003 step 3). |
|
|
584
|
+
| `Core::windows` | the ids arrive on `KUI_CMD_OPEN`; a host keeps the list it opened | the ids arrive on `take_window_command`'s `.Open`; a host keeps the list it opened | `windows` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The names of the windows open now. |
|
|
585
|
+
| `Ui::window_name` | `kui_ctx_window_name` | `ctx_window_name` | `windowName` | `env.window.name`, a reading | The name of the window this context draws. |
|
|
586
|
+
| `Ui::set_window_size` | `kui_set_window_size` | `set_window_size` | `setWindowSize` | `set_window_size` | Asks the driver to resize a window. |
|
|
587
|
+
| `Ui::focus_window` | `kui_focus_window` | `focus_window` | `focusWindow` | `focus_window` | Asks the driver to bring a window to the front. |
|
|
588
|
+
| `Ui::window_title` | `kui_window_title` | `window_title` | the root's `title` prop | the root's `title` field | Declares the window's title this frame. |
|
|
589
|
+
| `Ui::always_on_top` | `kui_set_always_on_top` | `set_always_on_top` | the root's `alwaysOnTop` prop | the root's `always_on_top` field | Declares that the window sits above every other app's this frame (backlog C30). |
|
|
590
|
+
| `Ui::secure_input` | `kui_set_secure_input` | `set_secure_input` | the root's `secureInput` prop | the root's `secure_input` field | Declares that this frame wants secure keyboard entry while the window has the keyboard — a password prompt (backlog F85). |
|
|
591
|
+
| `Ui::option_as_alt` | `kui_set_option_as_alt` | `set_option_as_alt` | the root's `optionAsAlt` prop | the root's `option_as_alt` field | Declares which Option keys act as Alt in this window on macOS, so a dead key like ⌥u arrives as `<A-u>` (backlog F113). |
|
|
592
|
+
| `Ui::ime_off` | `kui_set_ime_off` | `set_ime_off` | the root's `imeOff` prop | the root's `ime_off` field | Declares that this window takes the keyboard as keys, with the input method off — no composition, and on a Mac no dead keys and no press-and-hold, so a held letter repeats (backlog F125). |
|
|
593
|
+
| `Ui::window_command` | the chrome roles (`KuiSpec.window_role`) are the door; the verb is what `widgets::window_buttons` lowers to | `window` on a node (`Spec.window`, the chrome roles) is the door | `KuiWindow.close()` for the one command the runner takes from outside a frame; the rest are `windowRole` | `window_role` | Minimize, toggle-maximize, start-drag, close — what a chrome node asks for on a press. |
|
|
594
|
+
| `Core::window_title` | `kui_window_title_get` | `window_title_get` | `Ctx.windowTitle` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | What the frame declared, for a driver applying it; a `KuiWindow` applies its own. |
|
|
595
|
+
| `Core::always_on_top` | `kui_always_on_top_get` | `always_on_top_get` | `Ctx.alwaysOnTop` | `env.window.always_on_top`, a reading | The same for the level. |
|
|
596
|
+
| `Core::secure_input` | `kui_secure_input_get` | `secure_input_get` | `Ctx.secureInput` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The same for the secure-input ask: what a driver with its own loop reads to make the platform call; the runner makes it for a `KuiWindow` and `kui_run`. |
|
|
597
|
+
| `Core::option_as_alt` | `kui_option_as_alt_get` | `option_as_alt_get` | `Ctx.optionAsAlt` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The same for the Option-as-Alt ask: what a driver with its own loop reads to apply it to its window; the runner applies it for a `KuiWindow` and `kui_run`. |
|
|
598
|
+
| `Core::ime_off` | `kui_ime_off_get` | `ime_off_get` | `Ctx.imeOff` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The same for the input-method ask: what a driver with its own loop reads to apply it to its window; the runner applies it for a `KuiWindow` and `kui_run`. |
|
|
599
|
+
| `Core::take_window_commands` | `kui_take_window_command` | `take_window_command` | `Ctx.windowCommands` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Drains what the frame asked of the driver: open, close, resize, focus, redraw. |
|
|
600
|
+
| `Core::window_closed` | `kui_window_closed` | `window_closed` | `Ctx.windowClosed` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The driver reports a window gone. |
|
|
601
|
+
| `Core::dismiss_window` | `kui_window_dismissed` | `window_dismissed` | `Ctx.windowDismissed` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The driver reports a popup dismissed, with why (ADR 0003 step 4). |
|
|
602
|
+
| `Core::set_theme` | `kui_theme_set` | `theme_set` | `setTheme` | *none: read-only: the palette is the host's (ADR 0019)* | Pins a whole palette. |
|
|
603
|
+
| `Core::set_accent` | `kui_theme_set_accent` | `theme_set_accent` | `setAccent` | *none: as `set_theme`* | Pins an accent and keeps the OS's base. |
|
|
604
|
+
| `Ui::theme` | `kui_theme` | `theme` | `theme` | `env.theme`, a reading | The palette in effect (`THEME_ROLES`). |
|
|
605
|
+
| `Core::set_metrics` | `kui_metrics_set` | `metrics_set` | `setMetrics` | *none: as `set_theme` (backlog T2)* | Pins the stock widgets' sizes. |
|
|
606
|
+
| `Ui::metrics` | `kui_metrics` | `metrics` | `metrics` | `env.metrics`, a reading | The sizes in effect (`METRIC_ROLES`). |
|
|
607
|
+
| `Ui::set_tokens` | `kui_tokens_set` | `tokens_set` | `setTokens` | `set_tokens` | Declares the origin's colour and length tokens (ADR 0027). |
|
|
608
|
+
| `Tokens::derive` | `kui_tokens_derive` | `tokens_derive` | a colour with `from` in `setTokens` | a colour with `from` in `set_tokens` | Adds derived colours to the declared ones (ADR 0028). |
|
|
609
|
+
| `Ui::tokens` | `kui_token_color` / `kui_token_length`, one name at a time | `token_color` / `token_length`, one name at a time | `tokens` | `env.tokens`, a reading | The tokens in effect, resolved for the appearance. |
|
|
610
|
+
| `Core::tokens_declared` | *none: a plugin declares in every `kui_ext_view` and pays the parse; a reader that lets it skip the second is one line, once a plugin asks for it* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | *none: an app declares once, before its loop* | *none: the `tokens` global is declared once, at load* | Whether an origin declared tokens. |
|
|
611
|
+
| `Core::set_diagnostics` | `kui_set_diagnostics` | `set_diagnostics` | `setDiagnostics` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Turns the per-frame checks on. |
|
|
612
|
+
| `Core::take_warnings` | `kui_take_warnings` | `take_warnings` | `warnings` | *none: the host drains and the Lua runner prints* | Drains the warnings raised since the last call. |
|
|
613
|
+
| `Core::warnings_raised` | *none: the C smoke round drains `kui_take_warnings` after each frame; a non-draining reader waits for a C harness that needs one* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | `warningsRaised` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The warnings raised so far, undrained, which is what an example's self-check reads (ADR 0021). |
|
|
614
|
+
| `Core::set_devtools` | `kui_set_devtools` | `set_devtools` | `setDevtools` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Turns the devtools panel on. |
|
|
615
|
+
| `Core::devtools` | `kui_devtools` | `devtools` | `devtools` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether it is on. |
|
|
616
|
+
| `Core::set_devtools_dock` | `kui_set_devtools_dock` | `set_devtools_dock` | `setDevtoolsDock` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Where it sits. |
|
|
617
|
+
| `Core::devtools_dock` | `kui_devtools_dock` | `devtools_dock` | `devtoolsDock` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Where it sits, read back. |
|
|
618
|
+
| `Core::host_rect` | `kui_host_rect` | `host_rect` | `hostArea` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Where the frame laid the host out in the window, logical px: the viewport with its origin, which is what tells the app's quads from the dock's (backlog F92). |
|
|
619
|
+
| `Core::set_devtools_theme` | `kui_set_devtools_theme` | `set_devtools_theme` | `setDevtoolsTheme` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Seeds the panel's theme override. |
|
|
620
|
+
| `Core::set_devtools_key` | `kui_set_devtools_key` | `set_devtools_key` | `setDevtoolsKey` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Respells the chord that moves the keyboard into the panel (`Ctrl+Shift+I` by default). |
|
|
621
|
+
| `Core::devtools_key` | `kui_devtools_key` | `devtools_key` | `devtoolsKey` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | That chord, read back in its portable spelling. |
|
|
622
|
+
| `Ui::devtools_tab` | `kui_devtools_tab` | `devtools_tab` | `<devtoolsTab name label slot/>` | `devtools_tab { name=, label=, slot= }` | Declares a devtools tab an extension fills through the slot named (ADR 0032). |
|
|
623
|
+
| `Ui::devtools_tab_with` | `kui_devtools_tab_open` | `devtools_tab_open` | `<devtoolsTab name label>{() => …}</devtoolsTab>`, the function child called only while the tab is on show | `devtools_tab { name=, label=, view = function(env) … end }`, called only while the tab is on show | Declares a devtools tab the host draws itself, and draws it only while it is on show. |
|
|
624
|
+
| `Core::devtools_shown_tab` | *none: a C host's open answers whether the tab is on show (`kui_devtools_tab_open`); nothing encodes ahead of the core there* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | `devtoolsShownTab` | *none: the runner's converter reads it for the script (ADR 0032, decision 3)* | The declared devtools tab on show, which a data binding reads once a frame to call the tab's function. |
|
|
625
|
+
| `Core::devtools_selected` | `kui_devtools_selected` | `devtools_selected` | `devtoolsSelected` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The node the panel's tree tab has selected (ADR 0032, decision 4). |
|
|
626
|
+
| `Core::devtools_hovered` | `kui_devtools_hovered` | `devtools_hovered` | `devtoolsHovered` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The tree row under the pointer. |
|
|
627
|
+
| `Core::devtools_picked` | `kui_devtools_picked` | `devtools_picked` | `devtoolsPicked` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The node the picker is over. |
|
|
628
|
+
| `Core::set_devtools_pick` | `kui_set_devtools_pick` | `set_devtools_pick` | `setDevtoolsPick` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Raises the panel's picker from outside it, or puts it away; raised from a declared tab, the pick lands in `devtools_selected` and the tab stays up. |
|
|
629
|
+
| `Core::devtools_picking` | `kui_devtools_picking` | `devtools_picking` | `devtoolsPicking` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Whether the picker is up. |
|
|
630
|
+
| `Core::set_devtools_selected` | `kui_set_devtools_selected` | `set_devtools_selected` | `setDevtoolsSelected` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Selects and reveals a node in the tree tab from outside the panel. |
|
|
631
|
+
| `Core::set_devtools_tab` | `kui_set_devtools_tab` | `set_devtools_tab` | `setDevtoolsTab` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Shows the panel's tab named — one of its own, in any case, or a declared one, as declared — from outside the panel, as the strip's click does; a hidden panel comes back docked. |
|
|
632
|
+
| `Core::devtools_current_tab` | `kui_devtools_current_tab` | `devtools_current_tab` | `devtoolsCurrentTab` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The tab the panel is on, by name. |
|
|
633
|
+
| `Core::set_devtools_legend` | `kui_set_devtools_legend` | `set_devtools_legend` | `setDevtoolsLegend` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The key legend the panel's facts tab shows. |
|
|
634
|
+
| `Core::set_inspect` | `kui_set_inspect` | `set_inspect` | `setInspect` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Turns the per-frame node snapshot behind `nodes` on. |
|
|
635
|
+
| `Core::nodes` | `kui_nodes` | `nodes` | `nodes` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The last frame's nodes with what layout and the declarations made of them — a tree view's and an inspector's data. |
|
|
636
|
+
| `Ui::add_extension` | `kui_ctx_add_extension` | `ctx_add_extension` | `Ctx.addExtension` | `add_extension` | Loads a plugin under a namespace; a `KuiWindow` takes its list at construction (`extensions`). |
|
|
637
|
+
| `Launcher::extensions` | `kui_ctx_extension_count` / `kui_ctx_extension_namespace`, one at a time | `ctx_extension_count` / `ctx_extension_namespace`, one at a time | `Ctx.extensionNamespaces` | `extension_namespaces` | The namespaces loaded. |
|
|
638
|
+
| `Core::frame` | `kui_frame_begin` … `kui_frame_finish` | `frame_begin` … `frame_finish`, or `kui.frame` around a view | `Ctx.frame` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Runs one frame: the view, layout, the draw list; `KuiWindow.setView` is the windowed form, the runner calling it. |
|
|
639
|
+
| `Core::output` | `kui_draw_data` | `draw_data` | `quads` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The draw list: quads, clips, fragment and texture draws (`clips`, `fragmentDraws`, `textureDraws` beside `quads` in Node) and the frame's stats. |
|
|
640
|
+
| `Core::take_pending_events` | `kui_poll_event` | `poll_event` | `pollEvents` | `on_event(ev)`, pushed after each frame | What the frame and the input since produced, for `update`. |
|
|
641
|
+
| `Core::set_time` | `kui_set_time` | `set_time` | `Ctx.setTime` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The clock the tweens read; a window's runner sets it from the display. |
|
|
642
|
+
| `Core::env` | `kui_env_set` and its four siblings, `ENV_FIELDS`' C column | `env_set` and its four siblings | `Ctx.setEnv` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The host facts written in (the `env` field); a `KuiWindow`'s runner writes its own. |
|
|
643
|
+
| `Ui::env` | *none: C is the host, so it writes the facts and has no reading (`ENV_FIELDS`)* | *none: Odin's doors are kui.h's (packages/odin): C has none, for the reason in C's cell* | `env` | `env`, the view's argument | The facts read back, `ENV_FIELDS` row for row. |
|
|
644
|
+
| `Core::handle_input` | `kui_input_cursor` … `kui_input_access`, one per `InputEvent` | `input_cursor` … `input_access`, one per input event; `click` and `press` for a drive | `Ctx.cursor` … `Ctx.access`, one per `InputEvent`; a `KuiWindow` refuses injection | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Pointer, wheel, key, text, IME, assistive and OS file-drag input; a wheel gesture's latching is `scroll_gesture` (`kui_input_scroll_gesture`, `Ctx.scrollGesture`, backlog F107); `press` / `release` are a click by label (`kui_input_press`, `Ctx.press`); the file drag is `drag_files` / `drop_files` / `drag_cancel` (ADR 0031); a file dialog's answer is `answer_files` (`kui_input_files`, `Ctx.answerFiles`, backlog C51); the documents the OS asked the app to open are `InputEvent::Open` (`kui_input_open`, `Ctx.openDocuments`, backlog F124). |
|
|
645
|
+
| `Core::modifiers` | `kui_input_modifiers` | `input_modifiers` | `Ctx.modifiers` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The modifier state, reported on its own when the OS does (backlog AR22). |
|
|
646
|
+
| `Core::release_held_keys` | `kui_release_held_keys` | `release_held_keys` | `Ctx.setEnv({focused: false})` releases, as losing the keyboard does for every driver (ADR 0020) | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Lets go of every key the focused sink holds. |
|
|
647
|
+
| `Core::set_subpixel_text` | `kui_set_subpixel_text` | `set_subpixel_text` | *none: a Node host never paints: the renderer behind `KuiWindow` is the runner's, and a headless `Ctx` has none* | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | LCD subpixel coverage for outline glyphs, for a renderer that blends per channel. |
|
|
648
|
+
| `Core::take_audio_commands` | `kui_take_audio_commands` | `take_audio_commands` | `Ctx.audioCommands` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Drains what the frame asked of the audio device; a `KuiWindow`'s device is the runner's. |
|
|
649
|
+
| `Core::audio_ended` | `kui_audio_ended` | `audio_ended` | `Ctx.audioEnded` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The device reports a playback over. |
|
|
650
|
+
| `Core::audio_truncated` | `kui_audio_truncated` | `audio_truncated` | `Ctx.audioTruncated` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The device reports a stop that cut a playback short — a one-shot node's removal becomes `truncated-playback`. |
|
|
651
|
+
| `Core::audio_refused` | `kui_audio_refused` | `audio_refused` | `Ctx.audioRefused` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The device reports a play it would not take — a `refused` sound event and `playback-refused`. |
|
|
652
|
+
| `Launcher::size` | `width` / `height` in the `KuiRunConfig` `kui_run_with` takes | `width` / `height` in the `Run_Config` `kui.run` takes | `width` / `height` in `WindowOptions` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The window's opening size; `min_size` / `max_size` / `chrome` / `text_aa` / `diagnostics` / `frame_latency` are the rest of the set, and each binding's form carries them all (`min_w`, `chrome`, `text_aa`, `diagnostics`, `frame_latency` in C; `minWidth`, `chrome`, `textAa`, `diagnostics`, `frameLatency` in Node). `Launcher::devtools` and `Launcher::core` are the two the others reach another way: `kui_set_devtools` / `setDevtools` on the context, and the context handed to `kui_run_with` *is* the core. |
|
|
653
|
+
| `Launcher::icon` | `kui_set_icon` | `set_icon` | `icon` in `WindowOptions` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The icon every window of the app is created with — RGBA pixels and their size — shown by Windows in the title bar, Alt-Tab and the taskbar and by X11's window manager; macOS (the bundle's `.icns`) and Wayland (the `.desktop` file's) have no window icon (backlog F86). `Launcher::icon_resource` is the Windows executable's own icon resource, which wins there — C's `resource` argument, Node's `icon.resource`. C's is a free function called before `kui_run`, for `kui_on_teardown`'s reason. |
|
|
654
|
+
| `App::teardown` | `kui_on_teardown` | the `teardown` procedure `kui.run` takes | `KuiWindow.onTeardown` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The window going for good — its close button, Quit from the menu or the dock, a close command on it, a pumped runner ended — heard once, before `run` returns or the process exits, with nothing drawing: the place to keep what the app would lose with the window (backlog F74, the other two hosts under RG1). On macOS a Quit ends the process from inside the loop, so this is the only thing an app runs on ⌘Q — nothing after `run`, `kui_run` or `await runWindowed(...)` does, not even `process.on('exit')`. C's is a free function called before `kui_run`, with the run's `user`, since `kui_run`'s app is three arguments and not a struct. Node's is the window's door, called from inside the pump that saw the window go; `runWindowed` registers its config's `teardown(model)` there, and `createApp`'s `app.teardown()` runs the same one for a headless drive. |
|