@qxuken/kui 0.1.0-alpha.43 → 0.1.0-alpha.45

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 CHANGED
@@ -21,6 +21,259 @@ listed under both (backlog F61, from the alpha.12 field reports: the list
21
21
  is what the release knows it broke, and a fix it did not think of as one
22
22
  was the first bare bump to break an app in five releases).
23
23
 
24
+ ## 0.1.0-alpha.45 (2026-10-08)
25
+
26
+ **What breaks.**
27
+
28
+ - A menu row whose accelerator would leave its label under 48 px — a
29
+ window narrower than the accelerator with its gaps — draws the
30
+ accelerator cut short with "…" and the label's first glyphs, where it
31
+ drew the whole accelerator past the panel's edge and no label (under
32
+ Fixed, RG154).
33
+ - A `menu_panel` floating in the viewport whose caller declared its
34
+ ceiling as a size expression (`max_width(Bound::Calc(..))`, `"50%"`)
35
+ bounds its labels by that ceiling read against the window, where it
36
+ bounded them by the window alone (under Fixed, RG154). A panel
37
+ anchored elsewhere keeps the window as its labels' bound.
38
+ - A menu bar the app leaves out of a frame while a submenu switch waits
39
+ its 0.3 s starts the wait again when the bar is back, where the switch
40
+ went through at once on the next build (under Fixed, RG154).
41
+ - Windows and Plasma: a `Blur` or `Tinted` window on a desktop with no
42
+ wallpaper kui can read is `Opaque` from its first frame again, as it
43
+ was in alpha.43; alpha.44's `Tinted`-then-`Opaque` is GNOME's alone
44
+ now (under Fixed, RG154).
45
+
46
+ ### Fixed
47
+
48
+ - **The accelerator is bounded too** (backlog RG154, from the alpha.44
49
+ pre-tag pass). A row's label was what the panel's width bounded, and
50
+ its accelerator was drawn whole: in a window narrower than the
51
+ accelerator the label shrank to nothing and the accelerator ran past
52
+ the panel, which is what RG150 set out to stop. The label keeps a
53
+ floor of 48 px (`widgets::MENU_LABEL_MIN`) and the accelerator takes
54
+ what that leaves, ending in "…". And a ceiling declared as a size
55
+ expression on a panel floating in the viewport now caps the labels as
56
+ a px one does, read against the window; a panel anchored to a node
57
+ keeps the window's ceiling for its labels, since the room layout will
58
+ read the expression against is not placed when the rows are built.
59
+ - **A menu bar the app stops drawing owes no frames** (backlog RG154).
60
+ A submenu switch waiting its 0.3 s was cleared by the next build of the
61
+ bar's menu; an app that stopped drawing the bar inside the wait (or
62
+ handed it to the platform) kept the switch, and the frames it owed,
63
+ for good. A frame that builds no menu on a surface drops the switch
64
+ waiting there.
65
+ - **Windows and Plasma: no wallpaper, opaque from the first frame**
66
+ (backlog RG154). RG150 moved finding the wallpaper's path to the
67
+ loader's thread for GNOME's sake, where it is up to three `gsettings`
68
+ processes; on Windows it is one call and on Plasma one file read, so
69
+ the loop asks there and a window with none to draw never reads
70
+ `Tinted` first. GNOME keeps the thread. Windows run on screen;
71
+ Plasma compiled and read.
72
+
73
+ **What you can delete.**
74
+
75
+ - A shorter accelerator chosen for a menu that has to fit a narrow
76
+ window (RG154).
77
+ - A frame an app requested itself after stopping its menu bar, to let
78
+ the core settle (RG154).
79
+
80
+ ### Native verification
81
+
82
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-08, over
83
+ RG154 — what alpha.44's pre-tag pass left — with alpha.45's pre-tag pass
84
+ over it on the Windows machine and under WSLg: the mechanical round on
85
+ both, then a read-only review of the diff since alpha.44, each claim
86
+ probed. It filed nothing; its one finding, a `Calc` ceiling read against
87
+ the window for a panel anchored elsewhere, was corrected inside RG154
88
+ before the tag.
89
+
90
+ **Windows**, the pre-tag pass. fmt and clippy are clean; `nu
91
+ scripts/test.nu --node`: **2148 tests over 143 suites**, 0 failed. The
92
+ C round passes (6 checks), and so do the **57 scenes** through Rust,
93
+ Lua, C, Node and Odin; the Odin binding's four steps with CI's pinned
94
+ `dev-2026-09`; Node's tests under `KUI_CONFORMANCE_REQUIRED=1` (**218 of
95
+ 219**, the one skip Windows'), `npm run gen` with no diff, the
96
+ examples' typecheck and the headless round. The windowed round with
97
+ Node's: **53 examples on both bases**, clean on a first run; `counter`
98
+ and `host` opened by hand after `cbuild`, and the backdrop examples
99
+ opened with and without `KUI_BACKDROP_EMULATE`. The bench guard against
100
+ the alpha.44 tag, on a quiet machine: **green**, the eight guarded rows
101
+ −0.7% to +2.1% (`frame_10k_rects_with_access_tree` the high one), the
102
+ worst run-to-run spread 3.5%. A first run beside the WSL round read two
103
+ rows unreadable at 13 and 21% spread, which is why it was run again.
104
+
105
+ **Linux**, under WSLg (llvmpipe), the same commit: fmt and clippy
106
+ clean; `cargo test --workspace`: **1926 tests over 142 suites**, 0
107
+ failed; the C round (5 checks) and the 57 scenes through every adapter,
108
+ Node **219 of 219** with the corpus required, gen clean, the typecheck,
109
+ the headless round. The windowed round under X11 with Node's: **53
110
+ examples on both bases**, clean on a first run, two at a time. The
111
+ backdrop examples under X11 read no wallpaper (WSLg has none to name)
112
+ and went `Opaque`, as they should; Plasma's on-the-loop read is
113
+ compiled and read, not run. No Mac ran this round: the AX audit is CI's
114
+ and the next Mac round's.
115
+
116
+ ## 0.1.0-alpha.44 (2026-10-08)
117
+
118
+ **What breaks.**
119
+
120
+ - A menu row wider than the window — a recent file's path, a long
121
+ `<select>` option — draws its label cut short with "…" in a menu as
122
+ wide as the window less 8 px a side (and never narrower than the
123
+ metric's menu width, 200 px), where the menu ran off the edge (under
124
+ Fixed, RG150).
125
+ - With a frame clock (every runner sets one), moving the pointer from an
126
+ open submenu's row to another row of its menu switches after 0.3 s of
127
+ rest there, not at once (under Fixed, RG150). A driver that sets no
128
+ clock switches at once, as before.
129
+ - A `<select>` option given `items` (or a C `KuiMenuItem` with
130
+ `submenu`) opens no submenu: the rows are dropped, with an
131
+ `unknown-prop` warning in Node and Lua (under Fixed, RG150).
132
+ - Windows: a `Blur` or `Tinted` window whose environment presents through
133
+ the window's handle (`WGPU_DX12_PRESENTATION_SYSTEM=hwnd`) or names no
134
+ D3D12 in `WGPU_BACKEND` draws the wallpaper kui reads and reports
135
+ `Tinted` (or `Opaque`), where it was translucent over black and
136
+ reported `Blur`; a transparent window whose `WGPU_BACKEND` lists D3D12
137
+ among others opens D3D12 alone (under Fixed, RG150).
138
+ - Windows and Linux: a `Blur` or `Tinted` window on a desktop with no
139
+ wallpaper kui can read (a solid colour, a file it cannot decode) reads
140
+ `Tinted` for its first frame or frames and `Opaque` once the loader's
141
+ thread has answered, where alpha.43 read the path on the event loop
142
+ and said `Opaque` from the first frame (under Fixed, RG150). A view
143
+ that branches on the backdrop sees it change once, early.
144
+ - An image or a fragment that declares `border` draws it, as a ring over
145
+ the content, where it drew none (under Fixed, RG152): an app that
146
+ kept the `border` and drew its own ring around the picture draws two.
147
+
148
+ ### Added
149
+
150
+ - `MenuItem::stray_keys`, `MenuItem::stray_option_keys` and
151
+ `MenuBar::stray_keys`: the keys of plain-data menu rows no row reads,
152
+ a submenu's rows' included, for a binding to warn about (backlog
153
+ RG150).
154
+ - `kui_wgpu::see_through_by_visual` (and `_with`, its pure form): whether
155
+ a Windows window can be seen through, by the environment wgpu reads —
156
+ the one answer the window's surface and the swapchain both take
157
+ (backlog RG150).
158
+
159
+ ### Fixed
160
+
161
+ - **A menu has a width ceiling** (backlog RG150, from alpha.43's pre-tag
162
+ pass). F127 made a menu as wide as its widest row, with nothing above
163
+ it: a long path or `<select>` option ran the panel and its
164
+ accelerators off a narrow window. A menu is now never wider than the
165
+ window less 8 px a side (a narrower ceiling the caller declared in px
166
+ stands; the metric's menu width is the floor), and a row's label is
167
+ bounded by what its accelerator leaves it and ends in "…".
168
+ - **A submenu survives the pointer passing over a row on its way in**
169
+ (backlog RG150). A diagonal path from a row to a lower row of its
170
+ submenu crosses the rows below it, and each one closed the submenu. A
171
+ row that would close one now waits 0.3 s of rest on the frame clock,
172
+ and moving into the submenu cancels it; the frames for the wait are
173
+ owed. And a submenu the keyboard closed opens again when the pointer
174
+ leaves the menu and comes back to its row.
175
+ - **A stray key inside a submenu warns** (backlog RG150). Only a select's
176
+ top-level options were checked, so `{ label, disabled: true }` inside a
177
+ submenu was silently an enabled row; Node's `openMenu` and `<menuBar>`
178
+ and Lua's `open_menu` and `menu_bar` now warn for every level.
179
+ - **The backdrop blur's scratch is the size of what it blurs** (backlog
180
+ RG150). Four surface-sized textures, cleared and stored by every pass,
181
+ were ~236 MB at 5K and three full-surface stores per blurred node, and
182
+ were kept by a window gone idle. The scratch is now the largest
183
+ region's, its passes load rather than clear, the composite rides in the
184
+ pass that follows, and the first frame without a blur drops it all:
185
+ 65.5 MB → 16.8 MB and ~230 → ~150 µs a frame at 2560×1600 for one
186
+ toolbar, the output bit-identical.
187
+ - **Windows: one answer for the window's surface and the swapchain**
188
+ (backlog RG150). `WGPU_BACKEND` decided the first and
189
+ `WGPU_DX12_PRESENTATION_SYSTEM` the second, so `WGPU_BACKEND=dx12` drew
190
+ translucency over black while the window reported `Blur`. Compiled and
191
+ read, not run.
192
+ - **Linux: the wallpaper is found off the event loop** (backlog RG150):
193
+ up to three `gsettings` runs at window creation on GNOME before the
194
+ window showed; and Wayland's blur managers are bound once per process,
195
+ not once per window and never released. A window with no wallpaper to
196
+ read is `Tinted` until the thread answers, within its first frames,
197
+ and `Opaque` from then on (see What breaks). Compiled and read, not
198
+ run; the Linux half built and smoked under WSLg's X11 in the pre-tag
199
+ pass.
200
+
201
+ - **A box that becomes a float keeps a float it held above it** (backlog
202
+ RG151, from berainder). The float stack kept last frame's floats in
203
+ their order and put a new one on top, so a box that kept its key while
204
+ turning into a float went over the float inside it: a debug build
205
+ panicked on the stack's own assertion, a release build painted and hit
206
+ the inner float under its parent. A float found below the one it is in
207
+ now moves to just above it.
208
+ - **`border` on an image draws** (backlog RG152, from berainder). The
209
+ border was painted with the background, under the picture, which
210
+ covered it; on an image or a fragment it is now a ring over the
211
+ content, as over a gradient. And an image with a `gradient` and a
212
+ `border` no longer carries an empty quad where the gradient's own
213
+ ring went (from the alpha.44 pre-tag pass).
214
+ - **The float stack keeps the order of two floats a box held, and
215
+ sorts a float moved into one** (backlog RG153, from the alpha.44
216
+ pre-tag pass). RG151's move put each held float just above the box in
217
+ turn, so of two — a name panel and a tip opened over it — the first
218
+ came out on top; and a float that moves into another float under a
219
+ key the app keeps (`open_key`, `leaf_key`) changed no rank, so the
220
+ steady path kept it under the float it is now in and the debug
221
+ assertion tripped by RG151's other road. A held float now waits for
222
+ the float it is in and goes just above it, in the order it had; and
223
+ the steady order is checked for nesting and rebuilt when it fails.
224
+
225
+ **What you can delete.**
226
+
227
+ - An app's own truncation of menu labels to keep a menu on screen
228
+ (RG150).
229
+ - A key of its own for a box on each side of becoming a float — the
230
+ card behind and the card on top — kept only to stop a float inside it
231
+ falling under it (RG151), or to keep two floats inside it in the order
232
+ they opened (RG153).
233
+ - A padded box around an image to draw its border (RG152).
234
+
235
+ ### Native verification
236
+
237
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-08, over
238
+ RG150 from alpha.43's pre-tag pass and berainder's RG151 and RG152, with
239
+ alpha.44's pre-tag pass over them on the Windows machine and under WSLg:
240
+ the mechanical round on both, then three read-only reviews of the diff
241
+ since alpha.43 (the menus; the backdrop and the blur, read against
242
+ wgpu's and wayland-client's sources; the floats, the image border and
243
+ the docs), each claim probed. They filed RG153, built before the tag
244
+ (the float stack reversing two floats a box held, and keeping a float
245
+ moved into another under a kept key below it), and RG154, open; and the
246
+ round itself caught F127's width test failing on both platforms under
247
+ RG150's ceiling (its accelerator is a word each there, wider than the
248
+ test's window).
249
+
250
+ **Windows**, the pre-tag pass. fmt and clippy are clean; `nu
251
+ scripts/test.nu --node`: **2144 tests over 143 suites**, 0 failed. The C round passes (6 checks), and so do the **57 scenes**
252
+ through Rust, Lua, C, Node and Odin; the Odin binding's four steps with
253
+ CI's pinned `dev-2026-09`; Node's tests under
254
+ `KUI_CONFORMANCE_REQUIRED=1` (**218 of 219**, the one skip Windows'),
255
+ `npm run gen` with no diff, the examples' typecheck, the headless round
256
+ (37 drives) and the book's listing. The windowed round with Node's:
257
+ **53 examples on both bases**, every one clean on a first run; `counter`
258
+ and `host` opened by hand after `cbuild`. The bench guard against the
259
+ alpha.43 tag: **green**, the guarded rows −5.9% to +3.6%
260
+ (`frame_10k_rects_with_access_tree`; `frame_10k_rects` itself −5.9%),
261
+ the worst run-to-run spread on a guarded row 5.4%.
262
+
263
+ **Linux**, under WSLg (llvmpipe), the same commit with the pass's
264
+ fixes: fmt and clippy clean; `cargo test --workspace`: **1921 tests
265
+ over 142 suites**, 0 failed; the C round (5 checks) and the 57 scenes
266
+ through every adapter, the Odin binding's four steps, Node **219 of
267
+ 219** with the corpus required, gen clean, the typecheck, the headless
268
+ round. The windowed round under X11 with Node's: 53 examples on both
269
+ bases; eight windows of a first run four at a time died with
270
+ "X connection to :0 broken" — XWayland's, twice — and the four
271
+ examples drew every frame run again one at a time. Nothing opened a
272
+ Wayland window (Weston's decorations crash under winit here) and
273
+ nothing read a wallpaper: F126's Linux half is still compiled and read,
274
+ not met on KDE or GNOME. No Mac ran this round: the AX audit and the
275
+ macOS halves of RG150 are CI's check and the next Mac round's.
276
+
24
277
  ## 0.1.0-alpha.43 (2026-10-08)
25
278
 
26
279
  **What breaks.**
@@ -250,8 +503,6 @@ in the JSON a row already was.
250
503
  …" rows one per folder up to a cap (F128).
251
504
  - A toolbar's opaque fill over content that scrolls under it (F129).
252
505
  - Inline code drawn in the body face, or as a box beside the text (F130).
253
- - A toolbar's opaque fill over content that scrolls under it (F129).
254
- - Inline code drawn in the body face, or as a box beside the text (F130).
255
506
 
256
507
  ### Native verification
257
508
 
@@ -180,10 +180,17 @@ and none of which the tree order gives:
180
180
  declares it under a fresh key. This is the only "raise" and it is not a
181
181
  row; see *What was declined*.
182
182
 
183
- A nested float is above its enclosing float without a rule: it opened
184
- the same frame (appended after its parent, in tree order) or a later one
185
- (appended above). The key scheme means the reverse cannot happen, and a
186
- `debug_assert` says so where the stack is rebuilt.
183
+ A nested float is above its enclosing float, and almost always without a
184
+ rule: it opened the same frame (appended after its parent, in tree
185
+ order) or a later one (appended above), and a key derived from its
186
+ parent's cannot have been opened first. Two roads reach the reverse,
187
+ both under a key the app keeps across the change (backlog RG151 and
188
+ RG153, 2026-10-08): a box that becomes a float around a float it already
189
+ held — the box is new to the stack and the held float is not — and a
190
+ float that moves into another float under `open_key`, which changes no
191
+ rank. So the rebuild places each float after the float it is in, the
192
+ held ones in the order they had, and the steady order is checked for the
193
+ same and rebuilt when it fails. A `debug_assert` says the result holds.
187
194
 
188
195
  The steady state — the same float roots as last frame — is one
189
196
  comparison of two short key lists and costs nothing more; the first
package/encoder.js CHANGED
@@ -1026,7 +1026,9 @@ export function createEncoder(P) {
1026
1026
  for (const o of p.options) {
1027
1027
  const ok = (typeof o === 'string' && o.length > 0) || (o !== null && typeof o === 'object' && !Array.isArray(o));
1028
1028
  if (!ok) throw new Error('<select> options are non-empty strings or menu item objects { label, id, enabled }');
1029
- if (typeof o === 'object') for (const k in o) if (!MENU_ITEM_KEYS.has(k)) unknown.push([MENU_ITEM, k]);
1029
+ // An option is chosen, never opened: its `items` are dropped,
1030
+ // so they are reported as any key no option reads (RG150).
1031
+ if (typeof o === 'object') for (const k in o) if (!MENU_ITEM_KEYS.has(k) || k === 'items') unknown.push([MENU_ITEM, k]);
1030
1032
  }
1031
1033
  const current = p.current;
1032
1034
  if (current != null && (!Number.isInteger(current) || current < 0)) {
package/howto.md CHANGED
@@ -805,14 +805,21 @@ clicked, or Enter or the Right arrow is pressed; Left or Escape closes
805
805
  it, and Escape again closes the menu. It nests, in a context menu and in
806
806
  a menu bar, drawn or the platform's. A row inside is chosen like any
807
807
  row: one `{kind:"menu", role, item}` with its own `id`, on the node the
808
- menu is about. A host that shows menus itself reports one with
808
+ menu is about. Give the rows inside an `id`: a row without one posts its
809
+ label, and "Name" under "Sort by ▸" and "Name" under "Group by ▸" would
810
+ post the same `item`. A host that shows menus itself reports one with
809
811
  `Core::activate_menu_path(&[1, 0])` (Node `activateMenuPath`, C
810
812
  `kui_activate_menu_path` with a path of `size_t`s). In C a row's
811
813
  `submenu` / `submenu_count` nest the same `KuiMenuItem`s, and
812
814
  `KUI_MENU_ITEM_SUBMENU` in a row's flags says it has rows, read with
813
815
  `kui_menu_item_path`. An
814
816
  `accel` in the portable spelling (`"mod+shift+n"`) is drawn the
815
- platform's way, and the menu widens to its longest row.
817
+ platform's way, and the menu widens to its longest row, up to the window
818
+ less a margin (and never below the metric's menu width), where a longer
819
+ label ends in an ellipsis — and past the label's 48 px floor, the
820
+ accelerator does. On the frame
821
+ clock, an open submenu waits 0.3 s before giving way to a row the
822
+ pointer crosses, so a diagonal path into it does not close it.
816
823
 
817
824
  [ADR 0018](docs/adr/0018-a-menu-bar-the-app-declares.md) ·
818
825
  [`tests/submenu.rs`](../crates/kui-core/tests/submenu.rs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qxuken/kui",
3
- "version": "0.1.0-alpha.43",
3
+ "version": "0.1.0-alpha.45",
4
4
  "description": "kui for Node: JSX views lowered into the kui IR, Elm-style messages as data",
5
5
  "license": "MIT",
6
6
  "repository": {
Binary file
Binary file
Binary file
package/props.md CHANGED
@@ -118,9 +118,9 @@ where they make sense); text props apply to `<text>` and `<edit>`.
118
118
  |---|---|---|---|---|---|
119
119
  | `color` | `color` | `KuiTextStyle.color` | `Text_Style.color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Text color; default foreground when omitted. |
120
120
  | `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. |
121
- | `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
+ | `family` | `family` | `KuiTextStyle.family` (`KUI_FONT_*`); `KuiSpan.family` (with `KUI_SPAN_FAMILY`) | `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. |
122
122
  | `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. |
123
- | `font` | `font` | `KuiTextStyle.font` (from `kui_font_add*`) | `Text_Style.font` | resource handle | A registered font handle (addFont / addSystemFont); overrides `family`. |
123
+ | `font` | `font` | `KuiTextStyle.font` (from `kui_font_add*`); `KuiSpan.font` | `Text_Style.font` | resource handle | A registered font handle (addFont / addSystemFont); overrides `family`. |
124
124
  | `lineHeight` | `line_height` | `KuiTextStyle.line_height` | `Text_Style.line_height` | number, or a `"$length"` token | Line height (logical px); default size * 1.35. |
125
125
  | `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. |
126
126
  | `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. |
@@ -146,7 +146,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
146
146
  | `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. |
147
147
  | `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. |
148
148
  | `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. |
149
- | `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
+ | `size` (text) | `size` | `KuiTextStyle.size`; `KuiSpan.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. |
150
150
  | `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. |
151
151
  | `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. |
152
152
  | `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. |
@@ -654,6 +654,6 @@ its generator (`nu scripts/odin.nu gen --check`).
654
654
  | `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. |
655
655
  | `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`. |
656
656
  | `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`. |
657
- | `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. |
657
+ | `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` / `backdrop` are the rest of the set, and each binding's form carries them all (`min_w`, `chrome`, `text_aa`, `diagnostics`, `frame_latency`, `backdrop` in C and Odin; `minWidth`, `chrome`, `textAa`, `diagnostics`, `frameLatency`, `backdrop` 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. |
658
658
  | `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. |
659
659
  | `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. |