@qxuken/kui 0.1.0-alpha.42 → 0.1.0-alpha.44

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,426 @@ 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.44 (2026-10-08)
25
+
26
+ **What breaks.**
27
+
28
+ - A menu row wider than the window — a recent file's path, a long
29
+ `<select>` option — draws its label cut short with "…" in a menu as
30
+ wide as the window less 8 px a side (and never narrower than the
31
+ metric's menu width, 200 px), where the menu ran off the edge (under
32
+ Fixed, RG150).
33
+ - With a frame clock (every runner sets one), moving the pointer from an
34
+ open submenu's row to another row of its menu switches after 0.3 s of
35
+ rest there, not at once (under Fixed, RG150). A driver that sets no
36
+ clock switches at once, as before.
37
+ - A `<select>` option given `items` (or a C `KuiMenuItem` with
38
+ `submenu`) opens no submenu: the rows are dropped, with an
39
+ `unknown-prop` warning in Node and Lua (under Fixed, RG150).
40
+ - Windows: a `Blur` or `Tinted` window whose environment presents through
41
+ the window's handle (`WGPU_DX12_PRESENTATION_SYSTEM=hwnd`) or names no
42
+ D3D12 in `WGPU_BACKEND` draws the wallpaper kui reads and reports
43
+ `Tinted` (or `Opaque`), where it was translucent over black and
44
+ reported `Blur`; a transparent window whose `WGPU_BACKEND` lists D3D12
45
+ among others opens D3D12 alone (under Fixed, RG150).
46
+ - Windows and Linux: a `Blur` or `Tinted` window on a desktop with no
47
+ wallpaper kui can read (a solid colour, a file it cannot decode) reads
48
+ `Tinted` for its first frame or frames and `Opaque` once the loader's
49
+ thread has answered, where alpha.43 read the path on the event loop
50
+ and said `Opaque` from the first frame (under Fixed, RG150). A view
51
+ that branches on the backdrop sees it change once, early.
52
+ - An image or a fragment that declares `border` draws it, as a ring over
53
+ the content, where it drew none (under Fixed, RG152): an app that
54
+ kept the `border` and drew its own ring around the picture draws two.
55
+
56
+ ### Added
57
+
58
+ - `MenuItem::stray_keys`, `MenuItem::stray_option_keys` and
59
+ `MenuBar::stray_keys`: the keys of plain-data menu rows no row reads,
60
+ a submenu's rows' included, for a binding to warn about (backlog
61
+ RG150).
62
+ - `kui_wgpu::see_through_by_visual` (and `_with`, its pure form): whether
63
+ a Windows window can be seen through, by the environment wgpu reads —
64
+ the one answer the window's surface and the swapchain both take
65
+ (backlog RG150).
66
+
67
+ ### Fixed
68
+
69
+ - **A menu has a width ceiling** (backlog RG150, from alpha.43's pre-tag
70
+ pass). F127 made a menu as wide as its widest row, with nothing above
71
+ it: a long path or `<select>` option ran the panel and its
72
+ accelerators off a narrow window. A menu is now never wider than the
73
+ window less 8 px a side (a narrower ceiling the caller declared in px
74
+ stands; the metric's menu width is the floor), and a row's label is
75
+ bounded by what its accelerator leaves it and ends in "…".
76
+ - **A submenu survives the pointer passing over a row on its way in**
77
+ (backlog RG150). A diagonal path from a row to a lower row of its
78
+ submenu crosses the rows below it, and each one closed the submenu. A
79
+ row that would close one now waits 0.3 s of rest on the frame clock,
80
+ and moving into the submenu cancels it; the frames for the wait are
81
+ owed. And a submenu the keyboard closed opens again when the pointer
82
+ leaves the menu and comes back to its row.
83
+ - **A stray key inside a submenu warns** (backlog RG150). Only a select's
84
+ top-level options were checked, so `{ label, disabled: true }` inside a
85
+ submenu was silently an enabled row; Node's `openMenu` and `<menuBar>`
86
+ and Lua's `open_menu` and `menu_bar` now warn for every level.
87
+ - **The backdrop blur's scratch is the size of what it blurs** (backlog
88
+ RG150). Four surface-sized textures, cleared and stored by every pass,
89
+ were ~236 MB at 5K and three full-surface stores per blurred node, and
90
+ were kept by a window gone idle. The scratch is now the largest
91
+ region's, its passes load rather than clear, the composite rides in the
92
+ pass that follows, and the first frame without a blur drops it all:
93
+ 65.5 MB → 16.8 MB and ~230 → ~150 µs a frame at 2560×1600 for one
94
+ toolbar, the output bit-identical.
95
+ - **Windows: one answer for the window's surface and the swapchain**
96
+ (backlog RG150). `WGPU_BACKEND` decided the first and
97
+ `WGPU_DX12_PRESENTATION_SYSTEM` the second, so `WGPU_BACKEND=dx12` drew
98
+ translucency over black while the window reported `Blur`. Compiled and
99
+ read, not run.
100
+ - **Linux: the wallpaper is found off the event loop** (backlog RG150):
101
+ up to three `gsettings` runs at window creation on GNOME before the
102
+ window showed; and Wayland's blur managers are bound once per process,
103
+ not once per window and never released. A window with no wallpaper to
104
+ read is `Tinted` until the thread answers, within its first frames,
105
+ and `Opaque` from then on (see What breaks). Compiled and read, not
106
+ run; the Linux half built and smoked under WSLg's X11 in the pre-tag
107
+ pass.
108
+
109
+ - **A box that becomes a float keeps a float it held above it** (backlog
110
+ RG151, from berainder). The float stack kept last frame's floats in
111
+ their order and put a new one on top, so a box that kept its key while
112
+ turning into a float went over the float inside it: a debug build
113
+ panicked on the stack's own assertion, a release build painted and hit
114
+ the inner float under its parent. A float found below the one it is in
115
+ now moves to just above it.
116
+ - **`border` on an image draws** (backlog RG152, from berainder). The
117
+ border was painted with the background, under the picture, which
118
+ covered it; on an image or a fragment it is now a ring over the
119
+ content, as over a gradient. And an image with a `gradient` and a
120
+ `border` no longer carries an empty quad where the gradient's own
121
+ ring went (from the alpha.44 pre-tag pass).
122
+ - **The float stack keeps the order of two floats a box held, and
123
+ sorts a float moved into one** (backlog RG153, from the alpha.44
124
+ pre-tag pass). RG151's move put each held float just above the box in
125
+ turn, so of two — a name panel and a tip opened over it — the first
126
+ came out on top; and a float that moves into another float under a
127
+ key the app keeps (`open_key`, `leaf_key`) changed no rank, so the
128
+ steady path kept it under the float it is now in and the debug
129
+ assertion tripped by RG151's other road. A held float now waits for
130
+ the float it is in and goes just above it, in the order it had; and
131
+ the steady order is checked for nesting and rebuilt when it fails.
132
+
133
+ **What you can delete.**
134
+
135
+ - An app's own truncation of menu labels to keep a menu on screen
136
+ (RG150).
137
+ - A key of its own for a box on each side of becoming a float — the
138
+ card behind and the card on top — kept only to stop a float inside it
139
+ falling under it (RG151), or to keep two floats inside it in the order
140
+ they opened (RG153).
141
+ - A padded box around an image to draw its border (RG152).
142
+
143
+ ### Native verification
144
+
145
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-08, over
146
+ RG150 from alpha.43's pre-tag pass and berainder's RG151 and RG152, with
147
+ alpha.44's pre-tag pass over them on the Windows machine and under WSLg:
148
+ the mechanical round on both, then three read-only reviews of the diff
149
+ since alpha.43 (the menus; the backdrop and the blur, read against
150
+ wgpu's and wayland-client's sources; the floats, the image border and
151
+ the docs), each claim probed. They filed RG153, built before the tag
152
+ (the float stack reversing two floats a box held, and keeping a float
153
+ moved into another under a kept key below it), and RG154, open; and the
154
+ round itself caught F127's width test failing on both platforms under
155
+ RG150's ceiling (its accelerator is a word each there, wider than the
156
+ test's window).
157
+
158
+ **Windows**, the pre-tag pass. fmt and clippy are clean; `nu
159
+ scripts/test.nu --node`: **2144 tests over 143 suites**, 0 failed. The C round passes (6 checks), and so do the **57 scenes**
160
+ through Rust, Lua, C, Node and Odin; the Odin binding's four steps with
161
+ CI's pinned `dev-2026-09`; Node's tests under
162
+ `KUI_CONFORMANCE_REQUIRED=1` (**218 of 219**, the one skip Windows'),
163
+ `npm run gen` with no diff, the examples' typecheck, the headless round
164
+ (37 drives) and the book's listing. The windowed round with Node's:
165
+ **53 examples on both bases**, every one clean on a first run; `counter`
166
+ and `host` opened by hand after `cbuild`. The bench guard against the
167
+ alpha.43 tag: **green**, the guarded rows −5.9% to +3.6%
168
+ (`frame_10k_rects_with_access_tree`; `frame_10k_rects` itself −5.9%),
169
+ the worst run-to-run spread on a guarded row 5.4%.
170
+
171
+ **Linux**, under WSLg (llvmpipe), the same commit with the pass's
172
+ fixes: fmt and clippy clean; `cargo test --workspace`: **1921 tests
173
+ over 142 suites**, 0 failed; the C round (5 checks) and the 57 scenes
174
+ through every adapter, the Odin binding's four steps, Node **219 of
175
+ 219** with the corpus required, gen clean, the typecheck, the headless
176
+ round. The windowed round under X11 with Node's: 53 examples on both
177
+ bases; eight windows of a first run four at a time died with
178
+ "X connection to :0 broken" — XWayland's, twice — and the four
179
+ examples drew every frame run again one at a time. Nothing opened a
180
+ Wayland window (Weston's decorations crash under winit here) and
181
+ nothing read a wallpaper: F126's Linux half is still compiled and read,
182
+ not met on KDE or GNOME. No Mac ran this round: the AX audit and the
183
+ macOS halves of RG150 are CI's check and the next Mac round's.
184
+
185
+ ## 0.1.0-alpha.43 (2026-10-08)
186
+
187
+ **What breaks.**
188
+
189
+ - The drawn menu — a context menu, a select's list, a drawn menu bar's
190
+ dropdown — is as wide as its widest row needs, where it was always
191
+ `Metrics::menu_width` (now its floor), and no row's label or
192
+ accelerator wraps (under Fixed, F127).
193
+ - A menu's accelerator kui can parse is drawn, and read back through
194
+ `Core::menu` / Node's `menu()` / C's `kui_menu_item`, in the platform's
195
+ spelling: `"mod+shift+n"` is `⇧⌘N` or `Ctrl+Shift+N`, where it was the
196
+ string as declared — as the menu bar's already was (under Fixed,
197
+ F127).
198
+ - Rust: `MenuItem` gains `submenu` (under Added, F128), so a struct
199
+ literal of one needs the field; `MenuItem::KEYS` is seven keys, with
200
+ `items`, where it was six.
201
+ - Rust: `WindowEnv` gains `backdrop` (under Added, F126), so a struct
202
+ literal of one needs the field or `..Default::default()`; so does
203
+ `kui_devtools::Window`, in the examples' harness.
204
+ - Rust: `QuadKind` gains `Backdrop` (under Added, F129), so a `match` on
205
+ a quad's kind needs the arm — a renderer of its own draws nothing for
206
+ it; `InteractSpec` and `NodeInfo` gain `backdrop_blur`, for a struct
207
+ literal of either. The conformance report's `kinds` line has a tenth
208
+ column, `backdrop`.
209
+ - C: ABI 26. `KuiSpec` appends `backdrop_blur` (into the tail padding:
210
+ the 64-bit size stays 704), `KuiMenuItem` appends `submenu` and
211
+ `submenu_count` (an array element, so its stride moved, to 72), and
212
+ `KuiRunConfig` appends `backdrop` (44). `KUI_QUAD_BACKDROP` is a new
213
+ quad kind. `KuiSpan` appends `family`, `size` and `font` (an array
214
+ element, so its stride moved, to 56), with the `KUI_SPAN_FAMILY` flag
215
+ (under Added, F130). Recompile; a zeroed tail is what every spec, row,
216
+ span and config had.
217
+ - Rust: `text::Span` gains `family` and `size` (under Added, F130), for a
218
+ struct literal of one; `Span::new` and the builders are unchanged.
219
+ - Node: the wire is v22 — a span carries its face and size after its
220
+ background's radius — so the addon and the package go together, as
221
+ every wire step has.
222
+ - `Accel::parse` reads a named key's label as that key: `"⌘⌫"` is ⌘
223
+ Backspace and `"Ctrl+Page Up"` parses, where the first was the
224
+ character `⌫` and the second nothing (under Fixed, RG146).
225
+ - A host's report of a dead menu row — `Core::activate_menu_bar_item` /
226
+ `_path`, Node's `activateMenuBarItem`, C's `kui_activate_menu_bar_*`,
227
+ or a row under a dead submenu row through `activate_menu_path` — is
228
+ refused and posts nothing, where it was performed (under Fixed,
229
+ RG147).
230
+
231
+ `backdropBlur` is a number row like any other, and a row's `items` rides
232
+ in the JSON a row already was.
233
+
234
+ ### Added
235
+
236
+ - **A window with the desktop behind it** (backlog F126, from Noticon,
237
+ for a sidebar like an Obsidian theme's on a Mac).
238
+ `Launcher::backdrop(Backdrop)` — `backdrop` in Node's window options —
239
+ asks for what shows through the app's windows, named for the effect:
240
+ `Transparent` (the desktop as it is), `Blur` (a live blur of what is
241
+ behind the window) or `Tinted` (the desktop's colour, steady). Which
242
+ regions show it is the app's, by painting them with alpha and the rest
243
+ opaque. macOS: an `NSVisualEffectView` under the content view, blending
244
+ behind the window and dimmed with it in the background — the sidebar
245
+ material for `Blur`, the window-background one for `Tinted`. Windows 11
246
+ 22H2 and later: Acrylic for `Blur`, Mica for `Tinted`
247
+ (`DWMWA_SYSTEMBACKDROP_TYPE`, the frame extended under the client
248
+ area), the window without a GDI surface and the device presenting D3D12
249
+ through DirectComposition (`kui_wgpu::GpuOptions::transparent`,
250
+ `Renderer::new_with` / `new_in_with`). Linux: `Blur` asked of the
251
+ compositor — `ext-background-effect-v1` where it is advertised with
252
+ blur, KWin's `org_kde_kwin_blur`, `_KDE_NET_WM_BLUR_BEHIND_REGION` under
253
+ X11. Where the OS has no effect to give — GNOME, other Linux, Windows
254
+ 10 — `Blur` and `Tinted` draw the desktop's wallpaper (GNOME's
255
+ `picture-uri`, Plasma's config, `SPI_GETDESKWALLPAPER`), read on a
256
+ thread, scaled down and blurred once and cached by path and modification
257
+ time, as the window's ground under the frame
258
+ (`Renderer::set_ground` / `set_ground_uv`), aligned to where the window
259
+ sits on its monitor (centred on Wayland), and report `Tinted`; with no
260
+ wallpaper, `Opaque`. Where the OS draws the effect the frame is cleared
261
+ to nothing rather than to the theme's `bg`, and glyphs are grayscale
262
+ under `TextAa::Auto`. What the window got is `env.window.backdrop`
263
+ (`window.backdrop` in Node and Lua, `kui_env_set_backdrop` for a C host
264
+ that makes its own), so a view paints opaque when it says `Opaque`.
265
+ Every window of the app takes it but a popup and the devtools' own; an
266
+ app that does not ask opens exactly the window and the swapchain it
267
+ always did. `KUI_BACKDROP_EMULATE=1` draws the wallpaper for `Blur` and
268
+ `Tinted` on Windows and Linux, to look at it (macOS reads no wallpaper,
269
+ so a window there reads `Opaque`). Seen in windows on Windows 11:
270
+ Acrylic blurring a red window behind the translucent region, Mica, the
271
+ bare desktop through `Transparent`, the wallpaper drawn by kui under
272
+ `KUI_BACKDROP_EMULATE`, and an opaque devtools window beside them; on
273
+ WSLg (Wayland, no blur protocol, no gsettings) a `Blur` reading
274
+ `Opaque`. On a Mac, built through Noticon, the library column shows the
275
+ vibrancy. No KDE or GNOME session was at hand: the Linux half compiles
276
+ and passes clippy there, untried on either desktop. C asks with
277
+ `KuiRunConfig.backdrop` (a `KUI_BACKDROP_*`) and a view reads what the
278
+ window got with `kui_ctx_backdrop`, the answer and not the ask; Odin's
279
+ `Run_Config.backdrop` and `kui.ctx_backdrop`. The `backdrop` example
280
+ paints a translucent library and an opaque page from the reading.
281
+ *What you can delete:* nothing — this is new.
282
+ - **A node blurs what is drawn beneath it** (backlog F129, from Noticon,
283
+ for a frosted toolbar over a scrolling note): CSS's `backdrop-filter:
284
+ blur()`. `NodeSpec::backdrop_blur(radius)` — `backdropBlur` in JSX,
285
+ `backdrop_blur` in Lua and Odin, `KuiSpec.backdrop_blur` in C — blurs
286
+ everything painted before the node, inside its rounded box, by that
287
+ radius in logical px (the Gaussian's standard deviation, as CSS's):
288
+ the ancestors' backgrounds, the siblings and the content scrolling
289
+ under it, and the window's backdrop where it has one. The node's own
290
+ `bg`, border and children paint over the blur, so a translucent `bg`
291
+ is frosted glass; it is clipped as the node is and faded by its
292
+ `opacity`. The core emits a `QuadKind::Backdrop` quad
293
+ (`KUI_QUAD_BACKDROP`) just before the node's own paint, carrying the
294
+ shape, the clip, the radius in physical px and the group opacity.
295
+ kui-wgpu draws a frame that has one into an offscreen copy of the
296
+ surface, breaks the pass at each, copies out the region under the node
297
+ plus three radii around it, averages it down by a power of two that
298
+ leaves a kernel of two to four texels, blurs it along each axis, and
299
+ writes it back inside the node's rounded rect and its clip, mixing by
300
+ coverage and opacity with blending off so a transparent window stays
301
+ premultiplied; then blits the frame to the surface. A frame without
302
+ one is drawn exactly as before, and the offscreen textures go after
303
+ 120 frames without one. A renderer that cannot read back what it drew
304
+ draws nothing for the quad, which leaves the node over an unblurred
305
+ backdrop; a headless core has the quad in its display list and no
306
+ pixels. `diag::BACKDROP_BLUR_HIDDEN` warns of one under an opaque `bg`
307
+ of its own, which hides all of it. The devtools inspector and
308
+ `Core::nodes` (`backdropBlur` in Node) read the radius. Seen on Windows
309
+ 11 in the new `backdrop_blur` example, in an opaque window and over
310
+ Acrylic: the cards under the toolbar smeared, the edge below it sharp,
311
+ the badge's blur inside its corners. *What you can delete:* a
312
+ toolbar's opaque fill over content that scrolls under it.
313
+ - **Submenus** (backlog F128, from Noticon, for "Move to ▸" and "Sort by
314
+ ▸"). `MenuItem::submenu(label, rows)` — `items` on a row in Node and
315
+ Lua, read by the one row parser — is a row with a chevron that opens
316
+ its rows in a menu beside it: when the pointer rests on it, on a click,
317
+ on Enter or the Right arrow (focus on its first row). Left or Escape
318
+ closes it, focus back on its row, and Escape again closes the menu.
319
+ They nest, in the context menu, a select's list and the drawn menu
320
+ bar's menus alike, and a chosen row inside posts its own `{kind:"menu",
321
+ role, item}` on the node the menu is about, as any row does; the row
322
+ that opens one is never chosen. On macOS the context menu and the menu
323
+ bar build an `NSMenu` submenu, which AppKit opens itself, and report a
324
+ row inside by its path. A host showing menus itself reports one with
325
+ `Core::activate_menu_path` / `activate_menu_bar_path` (Node
326
+ `activateMenuPath` / `activateMenuBarPath`); `menu()` reads a row's
327
+ `items` back. `Core::menu_submenus` / `menu_bar_submenus` say what is
328
+ open. The pointer opens and closes on a change of row, with no timer,
329
+ so a pointer resting on one row does not undo what the keyboard opened.
330
+ In C a row's `submenu` / `submenu_count` (ABI 26) nest the same
331
+ `KuiMenuItem`s, in `kui_open_menu`, `kui_select` and `kui_menu_bar`; a
332
+ row's flags carry `KUI_MENU_ITEM_SUBMENU`, and a host showing its own
333
+ menus reads and reports a row inside by its path —
334
+ `kui_menu_submenu_count` / `kui_menu_item_path` /
335
+ `kui_activate_menu_path`, and the bar's `kui_menu_bar_submenu_count` /
336
+ `kui_menu_bar_item_path` / `kui_activate_menu_bar_path`; Odin's
337
+ `Menu_Item.submenu` and the same doors. A C menu nested past 32 levels
338
+ — a row that is its own submenu — is refused, as an unknown role is.
339
+ *What you can delete:* a list cut short because a menu could not
340
+ nest — Noticon's `MOVE_TARGETS` cap on the folders a note can move to.
341
+
342
+ - **A span in a face and a size of its own** (backlog F130, from
343
+ Noticon, whose inline `code` was drawn in the body face with a wash).
344
+ `Span::family(FontFamily)` / `Span::mono()` and `Span::size(px)` —
345
+ `family`, `font` and `size` on a JSX `<span>` and in a Lua span table,
346
+ `KuiSpan.family` (with `KUI_SPAN_FAMILY`), `.font` and `.size` in C,
347
+ the same fields on Odin's `Span`. The paragraph still shapes as one
348
+ flow; each span's glyphs are shaped in its face, at that family's
349
+ weights, so a caret, a hit, a selection and the measurement read the
350
+ glyphs that are drawn and byte positions stay exact across a change of
351
+ face mid-line. A sized span's line height scales at the paragraph's
352
+ ratio, a line is as tall as its tallest span (every span then carries
353
+ its metrics, so a line of only smaller ones never comes out shorter),
354
+ measurement sums the lines' own heights, a selection and a caret are
355
+ their line's height, and a span's background is its own height around
356
+ its glyphs rather than the line's. A paragraph with a sized span is
357
+ shaped whole, never chunked as a long line. Pinned by
358
+ `tests/span_face.rs` (the mono width, carets and hits across the
359
+ change, line heights, the caret, the wash), Node's `a span takes a face
360
+ and a size of its own`, and the corpus's first scene, which gained a
361
+ mono span and a sized one in all five adapters; the `text` example
362
+ shows inline code and a larger word. *What you can delete:* inline code
363
+ drawn in the body face, or as a box beside the text.
364
+
365
+ ### Fixed
366
+
367
+ - **A menu's accelerator reads as the platform writes it** (backlog
368
+ F127, from Noticon). `MenuItem::accel` drew its string verbatim in a
369
+ context menu, so a row declared with the portable `"mod+shift+n"` —
370
+ the spelling the docs give for a menu bar, where it was already
371
+ rewritten — showed those eleven characters. `open_menu` now rewrites
372
+ an accelerator it can parse into `Accel::display`'s spelling, as
373
+ `declare_menu_bar` does, and the drawn rows read every accelerator
374
+ through `Accel::label`, so a menu an app draws itself with
375
+ `widgets::context_menu` gets the same. A spelling kui cannot parse
376
+ (`"gd"`) is still drawn exactly as written. `MenuItem::accel_label`
377
+ is the drawn string; `accel_text` stays the declared one. *What you
378
+ can delete:* the `Accel::parse(..).display()` an app ran over its own
379
+ accelerators before handing them to a menu.
380
+ - **A menu is as wide as its rows** (backlog F127). The panel was the
381
+ metric's 200 px whatever it held, so a long label beside a long
382
+ accelerator wrapped one of them onto a second line. It is now as wide
383
+ as its widest label plus its widest accelerator, with at least
384
+ `widgets::MENU_ACCEL_GAP` between them, and never narrower than
385
+ `Metrics::menu_width`; labels and accelerators are one line each.
386
+ - **`scripts/npm-approve.nu` takes the version as npm spells it.**
387
+ Approving alpha.42 as `nu scripts/npm-approve.nu
388
+ @qxuken/kui@0.1.0-alpha.42` looked for a stage whose version was the
389
+ whole spec, found none, and said "nothing staged for
390
+ @qxuken/kui@@qxuken/kui@0.1.0-alpha.42" while the stage sat there. The
391
+ package prefix is cut off now, and a spec naming another package is
392
+ refused by name. A release script, so nothing an app sees.
393
+ - **A macOS menu shortcut on a named key works** (backlog RG146). The
394
+ bar (since alpha.11) and now the context menu build each item's key
395
+ equivalent from the accelerator in the platform's spelling, and
396
+ `Accel::parse` read `⌘⌫`'s `⌫` as a character: AppKit drew ⌘⌫ beside
397
+ the row and the chord did nothing, as with ⌘↩, ⌘← and every named
398
+ key. `parse` now reads a key as `Accel::display` writes it, in either
399
+ platform's spelling.
400
+ - **A dead menu row cannot be chosen by reporting it** (backlog RG147).
401
+ The bar's activate doors performed a disabled row, or one in a
402
+ disabled menu — kui.h said they refused it — and `activate_menu_path`
403
+ a row under a disabled submenu row. Every row on the path must be
404
+ enabled now, as the drawn menus need.
405
+
406
+ **What you can delete.**
407
+
408
+ - The `Accel::parse(..).display()` an app ran over its accelerators
409
+ before handing them to a context menu (F127).
410
+ - A menu cut short, or flattened, because menus could not nest: "Move to
411
+ …" rows one per folder up to a cap (F128).
412
+ - A toolbar's opaque fill over content that scrolls under it (F129).
413
+ - Inline code drawn in the body face, or as a box beside the text (F130).
414
+
415
+ ### Native verification
416
+
417
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-08, over
418
+ F126–F130 from the Noticon wish list, with alpha.43's pre-tag pass over
419
+ them on the Mac: the mechanical round, then three read-only reviews of
420
+ the diff since alpha.42 (the window backdrop and the blur; the menus and
421
+ the span's face; the docs), each claim probed. They filed RG146–RG149,
422
+ built before the tag (a macOS menu shortcut on a named key binding no
423
+ key, a host's report of a dead menu row performed, an Escape spent on a
424
+ submenu an app-drawn menu had noted, a Node span's family losing to an
425
+ inherited font handle), and RG150, open.
426
+
427
+ **macOS**, the pre-tag pass. fmt and clippy are clean; `nu
428
+ scripts/test.nu`: **1911 tests over 142 suites**, 0 failed. The C round
429
+ passes, and so do the **57 scenes**; Node's tests under
430
+ `KUI_CONFORMANCE_REQUIRED=1` (**218 of 218**), `npm run gen` with no
431
+ diff, the examples' typecheck, the headless round (with `backdrop_blur`'s
432
+ drive, which the roster had left out) and the book. The windowed round
433
+ with Node's: **128 windows** (one `transition` window hung at the
434
+ timeout on a first run four at a time and drew on eight runs alone and a
435
+ whole second round). The AX audit: **106/106**. The bench guard against the alpha.42 tag:
436
+ **green**, the guarded rows −0.4 to +3.1% (`deep_nesting_64_levels`,
437
+ −2.1% on a second run); the stream, long-line and cell grid rows flat.
438
+ The Odin
439
+ binding's four steps did not run here — the machine's `odin` links an
440
+ `llvm@22` no longer installed — so CI's check on the tag is their run.
441
+ F126's macOS half and F128's macOS menus were compiled and run on a Mac
442
+ the day they were built, through Noticon; F126 has not met KDE or GNOME.
443
+
24
444
  ## 0.1.0-alpha.42 (2026-10-07)
25
445
 
26
446
  **What breaks.**
@@ -331,7 +331,14 @@ derived orientation are the whole surface.
331
331
  decision 5's warning is where an app feels the missing semantic half.
332
332
  - **Submenus.** Left / Right in a `menu` should close and open them. kui
333
333
  has no submenu relation (a `menu` inside a `menuItem`), so today those
334
- arrows step like any other menu's.
334
+ arrows step like any other menu's. *Built 2026-10-08 for the core's own
335
+ menus (backlog F128):* Right on a row with a submenu opens it with focus
336
+ on its first row, Left inside one closes it with focus back on its row,
337
+ and Escape closes the innermost before the menu. The relation is the
338
+ core's state rather than a derived one — a submenu's panel is a `menu`
339
+ beside its row, not inside the `menuItem` (which is named from its
340
+ content), and the item walk already stops at a nested `menu` — so an
341
+ app's own `menu` composites step on Left / Right as before.
335
342
  - **`radio-without-group`.** A lone `radio` is invalid ARIA and now also
336
343
  means "no arrows here". A warning in the shape of `control-without-name`
337
344
  once something in the repo declares radios — the fixture will, so this
@@ -189,6 +189,16 @@ does. An app that has to write the menu twice has not been given a menu.
189
189
  something wants one, it wants it in the context menu too, and that is one
190
190
  change to `MenuItem` rather than two features.
191
191
 
192
+ *Amended 2026-10-08 (backlog F128).* Something wanted one — Noticon's
193
+ "Move to ▸" and "Sort by ▸", in the context menu first — and it was that
194
+ one change: `MenuItem::submenu` (`items` in plain data, so every
195
+ binding's one parser reads it), drawn by the same `menu_panel` beside
196
+ its row, in the context menu and the drawn bar alike, and an `NSMenu`
197
+ submenu where the platform draws them. `BarMenu` is still one level;
198
+ its rows nest. There is no hover timer: the core opens or closes on a
199
+ *change* of hovered row, which is what keeps a pointer resting on one
200
+ row from undoing what the keyboard just opened.
201
+
192
202
  - **Standard application-menu roles** (`about`, `quit`, `services`, and
193
203
  macOS's `NSApplication` responders). They change what an item *is* — one
194
204
  the platform performs rather than one the app hears — so they need their
@@ -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
@@ -735,6 +735,8 @@ export function createEncoder(P) {
735
735
  // dotted (v13, backlog K4). A span's own underline colour and style
736
736
  // beat the enclosing span's; either implies the underline. Then the
737
737
  // background's radius (v17, backlog F101), 0 for a square one; a span's
738
+ // own beats the enclosing span's. Then the span's own face — `family`
739
+ // by name, `font` by handle, which wins — and `size` (v22); a span's
738
740
  // own beats the enclosing span's.
739
741
  function collectSpans(node, st, out) {
740
742
  if (node == null || typeof node === 'boolean') return;
@@ -752,7 +754,7 @@ export function createEncoder(P) {
752
754
  (st.ul?.ref ? 512 : 0) |
753
755
  (st.ulStyle === 'wavy' ? 1024 : 0) |
754
756
  (st.ulStyle === 'dotted' ? 2048 : 0);
755
- out.push([String(node), flags, st.color?.v ?? 0, st.bg?.v ?? 0, st.ul?.v ?? 0, st.bgRadius ?? 0]);
757
+ out.push([String(node), flags, st.color?.v ?? 0, st.bg?.v ?? 0, st.ul?.v ?? 0, st.bgRadius ?? 0, st.family ?? null, st.font ?? null, st.size ?? 0]);
756
758
  return;
757
759
  }
758
760
  if (Array.isArray(node)) {
@@ -767,6 +769,12 @@ export function createEncoder(P) {
767
769
  if (p.bgRadius != null && !(typeof p.bgRadius === 'number' && p.bgRadius >= 0)) {
768
770
  throw new Error(`bad bgRadius ${JSON.stringify(p.bgRadius)} on <span> (a number of logical px, 0 or more)`);
769
771
  }
772
+ if (p.family != null && typeof p.family !== 'string') {
773
+ throw new Error(`bad family ${JSON.stringify(p.family)} on <span> (sans, serif, mono or an installed family's name)`);
774
+ }
775
+ if (p.size != null && !(typeof p.size === 'number' && p.size > 0)) {
776
+ throw new Error(`bad size ${JSON.stringify(p.size)} on <span> (a number of logical px, above 0)`);
777
+ }
770
778
  collectSpans(
771
779
  node.children,
772
780
  {
@@ -779,6 +787,11 @@ export function createEncoder(P) {
779
787
  ul: spanColor(p.underlineColor, st.ul),
780
788
  ulStyle: p.underlineStyle ?? st.ulStyle,
781
789
  bgRadius: p.bgRadius ?? st.bgRadius,
790
+ family: p.family ?? st.family,
791
+ // A span naming a family drops the handle it inherited, which
792
+ // would otherwise win over it.
793
+ font: p.font != null ? String(p.font) : p.family != null ? null : st.font,
794
+ size: p.size ?? st.size,
782
795
  },
783
796
  out,
784
797
  );
@@ -829,15 +842,18 @@ export function createEncoder(P) {
829
842
  collectSpans(el.children, {}, spans);
830
843
  f[fi++] = OP.richText;
831
844
  props(p, null, false);
832
- reserve(8 + spans.length * 8);
845
+ reserve(8 + spans.length * 14);
833
846
  f[fi++] = spans.length;
834
- for (const [text, flags, c, bg, ul, radius] of spans) {
847
+ for (const [text, flags, c, bg, ul, radius, family, font, size] of spans) {
835
848
  strRef(text);
836
849
  f[fi++] = flags;
837
850
  f[fi++] = c;
838
851
  f[fi++] = bg;
839
852
  f[fi++] = ul;
840
853
  f[fi++] = radius;
854
+ strRef(family);
855
+ strRef(font);
856
+ f[fi++] = size;
841
857
  }
842
858
  } else {
843
859
  f[fi++] = OP.text;
@@ -1010,7 +1026,9 @@ export function createEncoder(P) {
1010
1026
  for (const o of p.options) {
1011
1027
  const ok = (typeof o === 'string' && o.length > 0) || (o !== null && typeof o === 'object' && !Array.isArray(o));
1012
1028
  if (!ok) throw new Error('<select> options are non-empty strings or menu item objects { label, id, enabled }');
1013
- 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]);
1014
1032
  }
1015
1033
  const current = p.current;
1016
1034
  if (current != null && (!Number.isInteger(current) || current < 0)) {
package/howto.md CHANGED
@@ -278,6 +278,23 @@ gap, to keep it apart.
278
278
  [ADR 0035](docs/adr/0035-a-rounded-background-is-joined-by-meeting.md) ·
279
279
  [alpha.22 `### Added`](CHANGELOG.md#010-alpha22-2026-09-28)
280
280
 
281
+ ### How do I set inline code in monospace inside a paragraph?
282
+
283
+ Give the span a face of its own: `<span family="mono">cargo test</span>`,
284
+ `Span::new("cargo test").mono()` in Rust, `{ "cargo test", family = "mono" }`
285
+ in Lua, `.flags = KUI_SPAN_FAMILY, .family = KUI_FONT_MONO` on a C
286
+ `KuiSpan` (`.font` for a registered font, which wins). Any family name the
287
+ text's `family` takes works, and `size` gives a span its own size — a
288
+ larger first word, a smaller footnote mark — with the line as tall as its
289
+ tallest span. The paragraph still shapes as one flow, and its carets, hit
290
+ tests and selection are measured in the face each glyph is drawn in, so a
291
+ byte position means the same thing on either side of the change. Add a
292
+ `bg` and a `bgRadius` for the usual wash; it is the span's own height,
293
+ not the line's.
294
+
295
+ [`text` element](props.md#elements) ·
296
+ [`text.rs`](../examples/rust/widgets/text.rs)
297
+
281
298
  ### How do I list the installed fonts, the monospaced ones first?
282
299
 
283
300
  `ctx.systemFonts()` (Rust `Core::system_fonts()`, C `kui_system_fonts`)
@@ -558,6 +575,51 @@ message.
558
575
  [ADR 0019](docs/adr/0019-a-theme-derived-from-appearance-and-accent.md) ·
559
576
  [alpha.10](CHANGELOG.md#010-alpha10-2026-09-09)
560
577
 
578
+ ### How do I show the blurred desktop through my sidebar, like a Mac app?
579
+
580
+ Ask the launcher for the effect — `kui_native::app("Notes")
581
+ .backdrop(Backdrop::Blur)`, or `backdrop: 'blur'` in Node's window options
582
+ — and paint the regions that should show it with alpha:
583
+ `bg(t.raised.with_alpha(0.5))` on the sidebar, an opaque `bg` on the page
584
+ beside it. kui puts the effect behind the whole window and knows nothing of
585
+ sidebars; what you paint decides where it shows. `Blur` is a live blur of
586
+ what is behind the window (macOS vibrancy, Windows 11's Acrylic, KDE's
587
+ compositor blur), `Tinted` the desktop's colour, steady (Mica, macOS's
588
+ window-background material), `Transparent` the desktop as it is. Then read
589
+ what the window got, every frame, from `ui.env().window.backdrop` (Node
590
+ `env().window.backdrop`): where the OS has no effect — GNOME, Windows 10 —
591
+ it reads `Tinted`, the wallpaper kui reads, blurs once and draws under the
592
+ frame, or `Opaque` where no wallpaper could be read; paint the sidebar
593
+ opaque then. Hyprland, SwayFX and picom blur translucent windows
594
+ themselves: ask for `Transparent` there. Glyphs are grayscale under a
595
+ backdrop; `KUI_BACKDROP_EMULATE=1` shows the wallpaper path on Windows and Linux
596
+ (macOS reads no wallpaper, so the window there is `Opaque`).
597
+
598
+ [`window.backdrop` row](props.md#env) ·
599
+ [`backdrop.rs`](../examples/rust/features/backdrop.rs)
600
+
601
+ In C, `KuiRunConfig.backdrop` asks (`KUI_BACKDROP_BLUR`) and
602
+ `kui_ctx_backdrop(ctx)` in the view reads what the window got.
603
+
604
+ ### How do I frost a toolbar over content that scrolls under it?
605
+
606
+ Give the toolbar a `backdropBlur` and a translucent `bg`:
607
+ `NodeSpec::row().backdrop_blur(16.0).bg(Color::WHITE.with_alpha(0.25))`
608
+ in Rust, `backdropBlur: 16` in JSX, `backdrop_blur` in Lua, Odin and C's
609
+ `KuiSpec`. What blurs is everything painted before the node — the page,
610
+ the rows scrolling under it, the window's backdrop where there is one —
611
+ inside the node's rounded box and its clip, by the radius in logical px
612
+ (CSS's `backdrop-filter: blur()`). The node's own `bg`, border and
613
+ children lie on top, so an opaque `bg` hides the blur, and a
614
+ `backdrop-blur-hidden` warning says so. Float the toolbar over the
615
+ scroller, after it in the tree, so the scroller paints first. It costs a
616
+ copy and three small passes per blurred node on a frame that has one, and
617
+ nothing on a frame that does not; a renderer of your own that cannot read
618
+ back its frame draws the node unblurred.
619
+
620
+ [`backdropBlur` row](props.md#container-props) ·
621
+ [`backdrop_blur.rs`](../examples/rust/features/backdrop_blur.rs)
622
+
561
623
  ## Interaction, focus and reading
562
624
 
563
625
  ### How do I open a popup, and when is a modal enough?
@@ -734,6 +796,33 @@ the same palette from the keyboard and types the same way.
734
796
  [ADR 0030](docs/adr/0030-the-standard-menus-the-runner-keeps.md) ·
735
797
  [examples/rust/widgets/menu_bar.rs](../examples/rust/widgets/menu_bar.rs)
736
798
 
799
+ ### How do I put a submenu in a menu — "Move to ▸", "Sort by ▸"?
800
+
801
+ Give the row `items`: `MenuItem::submenu("Move to", rows)` in Rust,
802
+ `{ label: 'Move to', items: [...] }` in Node and Lua. The row draws a
803
+ chevron and opens its rows beside it when the pointer rests on it, it is
804
+ clicked, or Enter or the Right arrow is pressed; Left or Escape closes
805
+ it, and Escape again closes the menu. It nests, in a context menu and in
806
+ a menu bar, drawn or the platform's. A row inside is chosen like any
807
+ row: one `{kind:"menu", role, item}` with its own `id`, on the node the
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
811
+ `Core::activate_menu_path(&[1, 0])` (Node `activateMenuPath`, C
812
+ `kui_activate_menu_path` with a path of `size_t`s). In C a row's
813
+ `submenu` / `submenu_count` nest the same `KuiMenuItem`s, and
814
+ `KUI_MENU_ITEM_SUBMENU` in a row's flags says it has rows, read with
815
+ `kui_menu_item_path`. An
816
+ `accel` in the portable spelling (`"mod+shift+n"`) is drawn the
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. On the frame
820
+ clock, an open submenu waits 0.3 s before giving way to a row the
821
+ pointer crosses, so a diagonal path into it does not close it.
822
+
823
+ [ADR 0018](docs/adr/0018-a-menu-bar-the-app-declares.md) ·
824
+ [`tests/submenu.rs`](../crates/kui-core/tests/submenu.rs)
825
+
737
826
  ### How do I take files dropped from the Finder?
738
827
 
739
828
  Declare `onDrop` (Rust and Lua `on_drop`, C `KuiSpec.on_drop`) on the box
package/index.d.ts CHANGED
@@ -730,6 +730,10 @@ export interface OpenMenuItem {
730
730
  * own, or its role's default (`⌘C` on a `copy` row that declared none);
731
731
  * null where there is neither. */
732
732
  accel: string | null;
733
+ /** The rows of the submenu this row opens, read the same way; absent on
734
+ * a row that opens none. A row with them is never chosen itself: report
735
+ * a row inside with `activateMenuPath`. */
736
+ items?: OpenMenuItem[];
733
737
  }
734
738
 
735
739
  /** The menu a window has open (`Ctx.menu()`): where it opened, the node it
@@ -925,6 +929,12 @@ export type WarningCode =
925
929
  * after every width is. The ratio sizes a fit height from the width, or a
926
930
  * fit width from a fixed height. */
927
931
  | 'aspect-ignored'
932
+ /** A `backdropBlur` under an opaque `bg` of the node's own: the background
933
+ * paints over the whole blur, so nothing of it shows and the copy and the
934
+ * passes are spent for nothing. Give the `bg` some transparency
935
+ * (`#ffffff40`) — frosted glass is a translucent fill over a blur (backlog
936
+ * F129). */
937
+ | 'backdrop-blur-hidden'
928
938
  /** A text node sits more than four levels below the `line` row above it,
929
939
  * which is as far as a text's place remembers its ancestors — so `textHit` /
930
940
  * `caretRect` asked by that row's key cannot find the run, and a press
@@ -1369,6 +1379,8 @@ export interface NodeInfo {
1369
1379
  /** `0xRRGGBBAA`. */
1370
1380
  borderColor: number;
1371
1381
  opacity: number;
1382
+ /** Its `backdropBlur` radius in px, 0 for none (backlog F129). */
1383
+ backdropBlur: number;
1372
1384
  /** A scroller's offset; `null` for a node that does not scroll. */
1373
1385
  scroll: { x: number; y: number } | null;
1374
1386
  /** Every handler it declared with the payload it would post: `click`,
@@ -1647,8 +1659,19 @@ export interface WindowEnv {
1647
1659
  * draws over our content — the macOS traffic lights under custom chrome.
1648
1660
  * Keep out of it. Null means the OS draws nothing over us. */
1649
1661
  nativeControls: Rect | null;
1662
+ /** What is behind the window's transparent pixels, as the runner got it:
1663
+ * `'opaque'` (the default), `'transparent'` (the desktop as it is),
1664
+ * `'blur'` (a live blur of what is behind the window) or `'tinted'`
1665
+ * (the desktop's colour, steady). Less where the platform has less — a
1666
+ * blur asked of GNOME reads `'tinted'`, the wallpaper kui draws, or
1667
+ * `'opaque'` — so a view paints its translucent regions opaque when
1668
+ * this is `'opaque'`. */
1669
+ backdrop: Backdrop;
1650
1670
  }
1651
1671
 
1672
+ /** What is behind a window's transparent pixels (`WindowEnv.backdrop`). */
1673
+ export type Backdrop = 'opaque' | 'transparent' | 'blur' | 'tinted';
1674
+
1652
1675
  /** A box in logical px: position and size. */
1653
1676
  export interface Rect {
1654
1677
  x: number;
@@ -1689,6 +1712,8 @@ export interface EnvInput {
1689
1712
  /** `x` and `y` default to the window origin; a zero-sized rect and null
1690
1713
  * both mean "nothing is drawn over us". */
1691
1714
  nativeControls?: Partial<Rect> | null;
1715
+ /** What a runner would report behind the window's transparent pixels. */
1716
+ backdrop?: Backdrop;
1692
1717
  };
1693
1718
  /** What a driver with a device would report; see `AudioEnv`. */
1694
1719
  audio?: {
@@ -1828,6 +1853,15 @@ export interface WindowOptions {
1828
1853
  maxWidth?: number;
1829
1854
  maxHeight?: number;
1830
1855
  chrome?: 'native' | 'custom' | 'borderless';
1856
+ /** What shows through the window where a frame paints nothing or paints
1857
+ * with alpha (the launcher's `backdrop`), by effect: `'opaque'` (the
1858
+ * default), `'transparent'` (the desktop as it is), `'blur'` (macOS
1859
+ * vibrancy, Windows 11 Acrylic, KDE's compositor blur) or `'tinted'`
1860
+ * (Windows 11 Mica, macOS's window-background material, the wallpaper
1861
+ * kui draws where the OS has neither). Paint the regions that should
1862
+ * show it with alpha and the rest opaque; `env().window.backdrop` says
1863
+ * what the platform gave, which is less where it has less. */
1864
+ backdrop?: Backdrop;
1831
1865
  /** How outline glyphs are antialiased: `'auto'` (the default) is LCD
1832
1866
  * subpixel coverage where the GPU blends per channel and grayscale
1833
1867
  * otherwise. `KUI_TEXT_AA=gray|subpixel` in the environment still
@@ -3280,6 +3314,12 @@ export declare class Ctx {
3280
3314
  * takes. False for a row that is not there.
3281
3315
  */
3282
3316
  activateMenuBarItem(menu: number, item: number): boolean
3317
+ /**
3318
+ * `activateMenuBarItem` for a row inside a submenu of menu
3319
+ * `menu`, by its path through the rows' `items`. False for a
3320
+ * row that is not there or that opens a submenu.
3321
+ */
3322
+ activateMenuBarPath(menu: number, path: Array<number>): boolean
3283
3323
  /**
3284
3324
  * Tells the core this host can show the platform's definition
3285
3325
  * panel. The standard Look Up row is then offered where it
@@ -3295,6 +3335,13 @@ export declare class Ctx {
3295
3335
  * stays open and nothing is posted.
3296
3336
  */
3297
3337
  activateMenuItem(index: number): boolean
3338
+ /**
3339
+ * `activateMenuItem` for a row inside a submenu, by its path
3340
+ * through the rows' `items`: `[2, 0]` is the first row of the
3341
+ * third row's submenu. False where `activateMenuItem` is, and
3342
+ * for a row that opens a submenu, which is never chosen.
3343
+ */
3344
+ activateMenuPath(path: Array<number>): boolean
3298
3345
  /** Closes whatever menu is open; true when there was one. */
3299
3346
  closeMenu(): boolean
3300
3347
  /**
@@ -3481,7 +3528,7 @@ export declare class KuiWindow {
3481
3528
  /**
3482
3529
  * Options: `{width, height, minWidth, minHeight, maxWidth, maxHeight,
3483
3530
  * chrome: "native" | "custom" | "borderless", textAa: "auto" | "gray"
3484
- * | "subpixel", frameLatency, system, icon}`. The min/max pairs bound what the user can
3531
+ * | "subpixel", frameLatency, system, icon, backdrop}`. The min/max pairs bound what the user can
3485
3532
  * resize the window to; either half may stand alone. `system` pins part of `env.system` over what the OS
3486
3533
  * says, for the life of the window — `{motion: 'reduced'}` is what a
3487
3534
  * user who asked for less motion would get, on a machine whose owner
@@ -4464,6 +4511,12 @@ export declare class KuiWindow {
4464
4511
  * takes. False for a row that is not there.
4465
4512
  */
4466
4513
  activateMenuBarItem(menu: number, item: number): boolean
4514
+ /**
4515
+ * `activateMenuBarItem` for a row inside a submenu of menu
4516
+ * `menu`, by its path through the rows' `items`. False for a
4517
+ * row that is not there or that opens a submenu.
4518
+ */
4519
+ activateMenuBarPath(menu: number, path: Array<number>): boolean
4467
4520
  /**
4468
4521
  * Tells the core this host can show the platform's definition
4469
4522
  * panel. The standard Look Up row is then offered where it
@@ -4479,6 +4532,13 @@ export declare class KuiWindow {
4479
4532
  * stays open and nothing is posted.
4480
4533
  */
4481
4534
  activateMenuItem(index: number): boolean
4535
+ /**
4536
+ * `activateMenuItem` for a row inside a submenu, by its path
4537
+ * through the rows' `items`: `[2, 0]` is the first row of the
4538
+ * third row's submenu. False where `activateMenuItem` is, and
4539
+ * for a row that opens a submenu, which is never chosen.
4540
+ */
4541
+ activateMenuPath(path: Array<number>): boolean
4482
4542
  /** Closes whatever menu is open; true when there was one. */
4483
4543
  closeMenu(): boolean
4484
4544
  /**
package/index.js CHANGED
@@ -1044,8 +1044,8 @@ runWindowed[PACE] = pacer;
1044
1044
  * is opened as a user who asked for less motion would see it.
1045
1045
  */
1046
1046
  export function windowOptions(opts = {}) {
1047
- const { width, height, minWidth, minHeight, maxWidth, maxHeight, chrome, textAa, frameLatency, system, icon } = opts;
1048
- return { width, height, minWidth, minHeight, maxWidth, maxHeight, chrome, textAa, frameLatency, system, icon };
1047
+ const { width, height, minWidth, minHeight, maxWidth, maxHeight, chrome, textAa, frameLatency, system, icon, backdrop } = opts;
1048
+ return { width, height, minWidth, minHeight, maxWidth, maxHeight, chrome, textAa, frameLatency, system, icon, backdrop };
1049
1049
  }
1050
1050
 
1051
1051
  /**
package/jsx-runtime.d.ts CHANGED
@@ -219,6 +219,11 @@ export interface MenuItemInput {
219
219
  * A declaration kui can parse is rewritten into the platform's own
220
220
  * spelling, so `'mod+s'` reads as `⌘S` on macOS and `Ctrl+S` elsewhere. */
221
221
  accel?: string;
222
+ /** The rows of a submenu: the row draws a chevron and opens them beside
223
+ * itself — on hover, a click, Enter or the Right arrow; Left or Escape
224
+ * closes it — and is never chosen itself. A chosen row inside posts its
225
+ * own `menu` event, on the node the menu is about. */
226
+ items?: MenuItemInput[];
222
227
  }
223
228
 
224
229
  /** One menu of the application menu bar (the `<menuBar menu={…}/>`
@@ -257,6 +262,8 @@ export interface GeneratedSpecProps {
257
262
  animate?: boolean;
258
263
  /** 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. */
259
264
  aspectRatio?: LengthProp;
265
+ /** Blur what was drawn beneath the node, inside its rounded box, by this radius in logical px — CSS's `backdrop-filter: blur()`, the radius its standard deviation (backlog F129). What blurs is everything painted before the node: its ancestors' backgrounds, the siblings under it, content scrolling beneath it, the window's `backdrop` where the window has one. The node's own `bg`, border and children paint over the blur, so a translucent `bg` (`#ffffff40`) makes frosted glass and an opaque one hides it. Clipped as the node is, faded by its `opacity`; 0 is none. The GPU renderer reads back only the box (and a margin of three radii around it) and blurs it at reduced resolution, so it costs a copy and three small passes per blurred node on a frame that has one and nothing on a frame that does not. A renderer that cannot read back what it drew — a host's own, or anything older — leaves the node over an unblurred backdrop; the display list carries it as a `backdrop` quad (`KUI_QUAD_BACKDROP` in C) either way. */
266
+ backdropBlur?: LengthProp;
260
267
  /** Background fill. */
261
268
  bg?: ColorProp;
262
269
  /** 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. */
@@ -599,6 +606,18 @@ export interface SpanProps extends Keyed {
599
606
  * A selection over rows, or over a paragraph's wrapped lines, is one
600
607
  * outline. Nested spans inherit; 0 (the default) is square. */
601
608
  bgRadius?: number;
609
+ /** The span's own face: `sans`, `serif`, `mono` or an installed family's
610
+ * name, as the text's `family` takes — inline code in `mono` inside a
611
+ * sans paragraph. Carets, hits and selection are measured in the face
612
+ * the glyphs are drawn in. Nested spans inherit. */
613
+ family?: string;
614
+ /** A registered font handle (`addFont` / `addSystemFont`), the span's
615
+ * face; wins over `family`. Nested spans inherit. */
616
+ font?: string;
617
+ /** The span's own size, logical px: its line height scales with it at
618
+ * the paragraph's ratio, and a line is as tall as its tallest span.
619
+ * Nested spans inherit. */
620
+ size?: number;
602
621
  children?: KuiNode;
603
622
  }
604
623
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qxuken/kui",
3
- "version": "0.1.0-alpha.42",
3
+ "version": "0.1.0-alpha.44",
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
@@ -18,6 +18,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
18
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
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
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
+ | `backdropBlur` | `backdrop_blur` | `backdrop_blur` | `Spec.backdrop_blur` | number, or a `"$length"` token | Blur what was drawn beneath the node, inside its rounded box, by this radius in logical px — CSS's `backdrop-filter: blur()`, the radius its standard deviation (backlog F129). What blurs is everything painted before the node: its ancestors' backgrounds, the siblings under it, content scrolling beneath it, the window's `backdrop` where the window has one. The node's own `bg`, border and children paint over the blur, so a translucent `bg` (`#ffffff40`) makes frosted glass and an opaque one hides it. Clipped as the node is, faded by its `opacity`; 0 is none. The GPU renderer reads back only the box (and a margin of three radii around it) and blurs it at reduced resolution, so it costs a copy and three small passes per blurred node on a frame that has one and nothing on a frame that does not. A renderer that cannot read back what it drew — a host's own, or anything older — leaves the node over an unblurred backdrop; the display list carries it as a `backdrop` quad (`KUI_QUAD_BACKDROP` in C) either way. |
21
22
  | `bg` | `bg` | `bg` | `Spec.bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background fill. |
22
23
  | `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
24
  | `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`. |
@@ -117,9 +118,9 @@ where they make sense); text props apply to `<text>` and `<edit>`.
117
118
  |---|---|---|---|---|---|
118
119
  | `color` | `color` | `KuiTextStyle.color` | `Text_Style.color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Text color; default foreground when omitted. |
119
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. |
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
+ | `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. |
121
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. |
122
- | `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`. |
123
124
  | `lineHeight` | `line_height` | `KuiTextStyle.line_height` | `Text_Style.line_height` | number, or a `"$length"` token | Line height (logical px); default size * 1.35. |
124
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. |
125
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. |
@@ -145,7 +146,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
145
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. |
146
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. |
147
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. |
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
+ | `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. |
149
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. |
150
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. |
151
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. |
@@ -156,7 +157,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
156
157
  |---|---|---|---|---|
157
158
  | `<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
159
  | `<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
+ | `<text>` with `<span bold italic underline strikethrough bg bgRadius color family font size>` children | `text("s", {…})`, `text({ "a", { "b", bold = true, underline = true, bg = 0x.., bg_radius = 4 }, { "code", family = "mono", size = 13 } })` | `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 span takes a face and a size of its own: `family` (a stock name or an installed family's, as the text's) or `font` (a handle, which wins) — inline code in `mono` inside a sans paragraph — and `size` in logical px, its line height scaled at the paragraph's ratio (`Span::family` / `Span::mono` / `Span::size` in Rust, `KuiSpan.family` with `KUI_SPAN_FAMILY`, `.font` and `.size` in C). Its glyphs are shaped in that face, so a caret, a hit, a selection and the measurement read the same glyphs and byte positions stay exact across the change; a line is as tall as its tallest span, and a span's background is its own height around its glyphs. A paragraph with a sized span is shaped whole, never in chunks. 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
161
  | `<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
162
  | `<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
163
  | `<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. |
@@ -172,7 +173,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
172
173
  | `<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
174
  | `<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
175
  | `<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
+ | `<menuBar menu={[{ label, items: [{ label, id?, role?, accel?, enabled?, checked?, items? }] }]}/>` | `menu_bar { menu = { { label=, items= { { label=, id=, role=, accel=, enabled=, checked=, items= } } } } }` | `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
177
  | `<windowButtons/>` | `window_buttons()` | `kui_window_buttons` | `kui.window_buttons` | Just the min/max/close buttons, for fully custom titlebars. |
177
178
  | `<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
179
  | `<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). |
@@ -271,6 +272,7 @@ people. In Node the codes are the `WarningCode` union.
271
272
  | `wrap-ignored` | `wrapChildren` on a container that cannot break lines: a column, a row whose main axis scrolls, or a row of a table, whose children are the table's columns. All lay out exactly as if the flag were absent, which reads as "wrapping is broken"; see `LayoutSpec::wrap` for why a column cannot have it. |
272
273
  | `align-ignored` | An alignment declared where it means nothing: a spread (`spaceBetween` / `spaceAround` / `spaceEvenly`) on `crossAlign`, `baseline` on `mainAlign` or on a column's `crossAlign`, or either as a float's attach point. Each lays out as `start` — the two centring spreads as `center` — which reads as "the value is broken" when it is the axis that is wrong. |
273
274
  | `aspect-ignored` | An `aspectRatio` with nothing it can set: both axes are declared, or the width is `fit` under a `grow` or percent height, which is resolved only after every width is. The ratio sizes a fit height from the width, or a fit width from a fixed height. |
275
+ | `backdrop-blur-hidden` | A `backdropBlur` under an opaque `bg` of the node's own: the background paints over the whole blur, so nothing of it shows and the copy and the passes are spent for nothing. Give the `bg` some transparency (`#ffffff40`) — frosted glass is a translucent fill over a blur (backlog F129). |
274
276
  | `text-beyond-line` | A text node sits more than four levels below the `line` row above it, which is as far as a text's place remembers its ancestors — so `textHit` / `caretRect` asked by that row's key cannot find the run, and a press inside it reports `byte: 0`. Flatten the wrappers between the row and its text, or ask by a nearer key. |
275
277
  | `exit-budget` | One frame removed more nodes declaring `exit` than the exit store will hold (4096, `depart::MAX_NODES`), so none of that frame's removal animated: every departing node of it vanished at once, as a node with no `exit` does, rather than some sliding out and the rest blinking. Correct, and invisible from the outside, which is the whole reason it is a line here: a list that drops a thousand rows wants `exit` on the list, not on every row. A removal that fits the budget but finds earlier exits still in flight evicts those, oldest first, and is not this warning. |
276
278
  | `unknown-prop` | A prop name nothing claims: not a schema row, not a composite, not one of the element's own props (see `schema::known_prop`). The binding threw the declaration away — `hoverBg` in a Lua table, `onclick` in JSX — so unlike every other code here this one is raised by the frontend that saw it, through `Core::warn`: by the time a frame is a tree the name is gone. The message names the likely spelling. Also raised for a key a menu row map carried that no row reads — `disabled` on a select's option, where the key is `enabled` — by the binding that read the row. |
@@ -347,6 +349,7 @@ one reading that changes what a view *says* rather than what it draws.
347
349
  | `window.fullscreen` | `WindowEnv::fullscreen` | `window.fullscreen` | `window.fullscreen` | `kui_env_set_window(fullscreen)` | `kui.env_set_window(fullscreen)` | The window is fullscreen. |
348
350
  | `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
351
  | `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. |
352
+ | `window.backdrop` | `WindowEnv::backdrop` | `window.backdrop` | `window.backdrop` | `kui_env_set_backdrop(backdrop)`; read back with `kui_ctx_backdrop()` | `kui.env_set_backdrop(backdrop)`; read back with `kui.ctx_backdrop()` | What is behind the window's transparent pixels, as the driver got it (backlog F126): `"opaque"` (the default, and every headless core's), `"transparent"` (the desktop as it is), `"blur"` (a live blur of what is behind the window) or `"tinted"` (the desktop's colour, not live) — `KUI_BACKDROP_*` in C, opaque 0. The app asks with `Launcher::backdrop` and decides which regions show it by painting them with alpha; this is the answer, which is less where the platform has less — a blur asked of GNOME reads `"tinted"`, the wallpaper kui draws itself, or `"opaque"` where none could be read — so a view paints its translucent regions opaque when this says so. A C host reports it through its own setter, as `always_on_top` is; a C app asks with `KuiRunConfig.backdrop` and its view reads the answer with `kui_ctx_backdrop`. |
350
353
  | `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
354
  | `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
355
  | `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. |
@@ -574,12 +577,14 @@ its generator (`nu scripts/odin.nu gen --check`).
574
577
  | `Ui::open_menu` | `kui_open_menu` | `open_menu` | `openMenu` | `open_menu` | Opens a context menu on a node at a point. |
575
578
  | `Ui::close_menu` | `kui_close_menu` | `close_menu` | `closeMenu` | `close_menu` | Closes it. |
576
579
  | `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. |
580
+ | `Core::menu` | `kui_menu_item_count` / `kui_menu_item`, one row at a time, and a submenu's by path with `kui_menu_submenu_count` / `kui_menu_item_path` | `menu_item_count` / `menu_item`, one row at a time, and a submenu's by path with `menu_submenu_count` / `menu_item_path` | `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
581
  | `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
582
  | `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. |
583
+ | `Core::activate_menu_path` | `kui_activate_menu_path` | `activate_menu_path` | `activateMenuPath` | *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* | `activate_menu_item` for a row inside a submenu, by its path through the rows' submenus (backlog F128); a row that opens a submenu is refused. |
584
+ | `Core::menu_bar` | `kui_menu_bar_menu_count` / `kui_menu_bar_menu` / `kui_menu_bar_item`, one row at a time, and a submenu's by path with `kui_menu_bar_submenu_count` / `kui_menu_bar_item_path` | `menu_bar_menu_count` / `menu_bar_menu` / `menu_bar_item`, one row at a time, and a submenu's by path with `menu_bar_submenu_count` / `menu_bar_item_path` | `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
585
  | `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
586
  | `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. |
587
+ | `Core::activate_menu_bar_path` | `kui_activate_menu_bar_path` | `activate_menu_bar_path` | `activateMenuBarPath` | *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* | `activate_menu_bar_item` for a row inside a submenu, by its path (backlog F128). |
583
588
  | `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
589
  | `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
590
  | `Ui::window_name` | `kui_ctx_window_name` | `ctx_window_name` | `windowName` | `env.window.name`, a reading | The name of the window this context draws. |
@@ -649,6 +654,6 @@ its generator (`nu scripts/odin.nu gen --check`).
649
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. |
650
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`. |
651
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`. |
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. |
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. |
653
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. |
654
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. |