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

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,267 @@ 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.43 (2026-10-08)
25
+
26
+ **What breaks.**
27
+
28
+ - The drawn menu — a context menu, a select's list, a drawn menu bar's
29
+ dropdown — is as wide as its widest row needs, where it was always
30
+ `Metrics::menu_width` (now its floor), and no row's label or
31
+ accelerator wraps (under Fixed, F127).
32
+ - A menu's accelerator kui can parse is drawn, and read back through
33
+ `Core::menu` / Node's `menu()` / C's `kui_menu_item`, in the platform's
34
+ spelling: `"mod+shift+n"` is `⇧⌘N` or `Ctrl+Shift+N`, where it was the
35
+ string as declared — as the menu bar's already was (under Fixed,
36
+ F127).
37
+ - Rust: `MenuItem` gains `submenu` (under Added, F128), so a struct
38
+ literal of one needs the field; `MenuItem::KEYS` is seven keys, with
39
+ `items`, where it was six.
40
+ - Rust: `WindowEnv` gains `backdrop` (under Added, F126), so a struct
41
+ literal of one needs the field or `..Default::default()`; so does
42
+ `kui_devtools::Window`, in the examples' harness.
43
+ - Rust: `QuadKind` gains `Backdrop` (under Added, F129), so a `match` on
44
+ a quad's kind needs the arm — a renderer of its own draws nothing for
45
+ it; `InteractSpec` and `NodeInfo` gain `backdrop_blur`, for a struct
46
+ literal of either. The conformance report's `kinds` line has a tenth
47
+ column, `backdrop`.
48
+ - C: ABI 26. `KuiSpec` appends `backdrop_blur` (into the tail padding:
49
+ the 64-bit size stays 704), `KuiMenuItem` appends `submenu` and
50
+ `submenu_count` (an array element, so its stride moved, to 72), and
51
+ `KuiRunConfig` appends `backdrop` (44). `KUI_QUAD_BACKDROP` is a new
52
+ quad kind. `KuiSpan` appends `family`, `size` and `font` (an array
53
+ element, so its stride moved, to 56), with the `KUI_SPAN_FAMILY` flag
54
+ (under Added, F130). Recompile; a zeroed tail is what every spec, row,
55
+ span and config had.
56
+ - Rust: `text::Span` gains `family` and `size` (under Added, F130), for a
57
+ struct literal of one; `Span::new` and the builders are unchanged.
58
+ - Node: the wire is v22 — a span carries its face and size after its
59
+ background's radius — so the addon and the package go together, as
60
+ every wire step has.
61
+ - `Accel::parse` reads a named key's label as that key: `"⌘⌫"` is ⌘
62
+ Backspace and `"Ctrl+Page Up"` parses, where the first was the
63
+ character `⌫` and the second nothing (under Fixed, RG146).
64
+ - A host's report of a dead menu row — `Core::activate_menu_bar_item` /
65
+ `_path`, Node's `activateMenuBarItem`, C's `kui_activate_menu_bar_*`,
66
+ or a row under a dead submenu row through `activate_menu_path` — is
67
+ refused and posts nothing, where it was performed (under Fixed,
68
+ RG147).
69
+
70
+ `backdropBlur` is a number row like any other, and a row's `items` rides
71
+ in the JSON a row already was.
72
+
73
+ ### Added
74
+
75
+ - **A window with the desktop behind it** (backlog F126, from Noticon,
76
+ for a sidebar like an Obsidian theme's on a Mac).
77
+ `Launcher::backdrop(Backdrop)` — `backdrop` in Node's window options —
78
+ asks for what shows through the app's windows, named for the effect:
79
+ `Transparent` (the desktop as it is), `Blur` (a live blur of what is
80
+ behind the window) or `Tinted` (the desktop's colour, steady). Which
81
+ regions show it is the app's, by painting them with alpha and the rest
82
+ opaque. macOS: an `NSVisualEffectView` under the content view, blending
83
+ behind the window and dimmed with it in the background — the sidebar
84
+ material for `Blur`, the window-background one for `Tinted`. Windows 11
85
+ 22H2 and later: Acrylic for `Blur`, Mica for `Tinted`
86
+ (`DWMWA_SYSTEMBACKDROP_TYPE`, the frame extended under the client
87
+ area), the window without a GDI surface and the device presenting D3D12
88
+ through DirectComposition (`kui_wgpu::GpuOptions::transparent`,
89
+ `Renderer::new_with` / `new_in_with`). Linux: `Blur` asked of the
90
+ compositor — `ext-background-effect-v1` where it is advertised with
91
+ blur, KWin's `org_kde_kwin_blur`, `_KDE_NET_WM_BLUR_BEHIND_REGION` under
92
+ X11. Where the OS has no effect to give — GNOME, other Linux, Windows
93
+ 10 — `Blur` and `Tinted` draw the desktop's wallpaper (GNOME's
94
+ `picture-uri`, Plasma's config, `SPI_GETDESKWALLPAPER`), read on a
95
+ thread, scaled down and blurred once and cached by path and modification
96
+ time, as the window's ground under the frame
97
+ (`Renderer::set_ground` / `set_ground_uv`), aligned to where the window
98
+ sits on its monitor (centred on Wayland), and report `Tinted`; with no
99
+ wallpaper, `Opaque`. Where the OS draws the effect the frame is cleared
100
+ to nothing rather than to the theme's `bg`, and glyphs are grayscale
101
+ under `TextAa::Auto`. What the window got is `env.window.backdrop`
102
+ (`window.backdrop` in Node and Lua, `kui_env_set_backdrop` for a C host
103
+ that makes its own), so a view paints opaque when it says `Opaque`.
104
+ Every window of the app takes it but a popup and the devtools' own; an
105
+ app that does not ask opens exactly the window and the swapchain it
106
+ always did. `KUI_BACKDROP_EMULATE=1` draws the wallpaper for `Blur` and
107
+ `Tinted` on Windows and Linux, to look at it (macOS reads no wallpaper,
108
+ so a window there reads `Opaque`). Seen in windows on Windows 11:
109
+ Acrylic blurring a red window behind the translucent region, Mica, the
110
+ bare desktop through `Transparent`, the wallpaper drawn by kui under
111
+ `KUI_BACKDROP_EMULATE`, and an opaque devtools window beside them; on
112
+ WSLg (Wayland, no blur protocol, no gsettings) a `Blur` reading
113
+ `Opaque`. On a Mac, built through Noticon, the library column shows the
114
+ vibrancy. No KDE or GNOME session was at hand: the Linux half compiles
115
+ and passes clippy there, untried on either desktop. C asks with
116
+ `KuiRunConfig.backdrop` (a `KUI_BACKDROP_*`) and a view reads what the
117
+ window got with `kui_ctx_backdrop`, the answer and not the ask; Odin's
118
+ `Run_Config.backdrop` and `kui.ctx_backdrop`. The `backdrop` example
119
+ paints a translucent library and an opaque page from the reading.
120
+ *What you can delete:* nothing — this is new.
121
+ - **A node blurs what is drawn beneath it** (backlog F129, from Noticon,
122
+ for a frosted toolbar over a scrolling note): CSS's `backdrop-filter:
123
+ blur()`. `NodeSpec::backdrop_blur(radius)` — `backdropBlur` in JSX,
124
+ `backdrop_blur` in Lua and Odin, `KuiSpec.backdrop_blur` in C — blurs
125
+ everything painted before the node, inside its rounded box, by that
126
+ radius in logical px (the Gaussian's standard deviation, as CSS's):
127
+ the ancestors' backgrounds, the siblings and the content scrolling
128
+ under it, and the window's backdrop where it has one. The node's own
129
+ `bg`, border and children paint over the blur, so a translucent `bg`
130
+ is frosted glass; it is clipped as the node is and faded by its
131
+ `opacity`. The core emits a `QuadKind::Backdrop` quad
132
+ (`KUI_QUAD_BACKDROP`) just before the node's own paint, carrying the
133
+ shape, the clip, the radius in physical px and the group opacity.
134
+ kui-wgpu draws a frame that has one into an offscreen copy of the
135
+ surface, breaks the pass at each, copies out the region under the node
136
+ plus three radii around it, averages it down by a power of two that
137
+ leaves a kernel of two to four texels, blurs it along each axis, and
138
+ writes it back inside the node's rounded rect and its clip, mixing by
139
+ coverage and opacity with blending off so a transparent window stays
140
+ premultiplied; then blits the frame to the surface. A frame without
141
+ one is drawn exactly as before, and the offscreen textures go after
142
+ 120 frames without one. A renderer that cannot read back what it drew
143
+ draws nothing for the quad, which leaves the node over an unblurred
144
+ backdrop; a headless core has the quad in its display list and no
145
+ pixels. `diag::BACKDROP_BLUR_HIDDEN` warns of one under an opaque `bg`
146
+ of its own, which hides all of it. The devtools inspector and
147
+ `Core::nodes` (`backdropBlur` in Node) read the radius. Seen on Windows
148
+ 11 in the new `backdrop_blur` example, in an opaque window and over
149
+ Acrylic: the cards under the toolbar smeared, the edge below it sharp,
150
+ the badge's blur inside its corners. *What you can delete:* a
151
+ toolbar's opaque fill over content that scrolls under it.
152
+ - **Submenus** (backlog F128, from Noticon, for "Move to ▸" and "Sort by
153
+ ▸"). `MenuItem::submenu(label, rows)` — `items` on a row in Node and
154
+ Lua, read by the one row parser — is a row with a chevron that opens
155
+ its rows in a menu beside it: when the pointer rests on it, on a click,
156
+ on Enter or the Right arrow (focus on its first row). Left or Escape
157
+ closes it, focus back on its row, and Escape again closes the menu.
158
+ They nest, in the context menu, a select's list and the drawn menu
159
+ bar's menus alike, and a chosen row inside posts its own `{kind:"menu",
160
+ role, item}` on the node the menu is about, as any row does; the row
161
+ that opens one is never chosen. On macOS the context menu and the menu
162
+ bar build an `NSMenu` submenu, which AppKit opens itself, and report a
163
+ row inside by its path. A host showing menus itself reports one with
164
+ `Core::activate_menu_path` / `activate_menu_bar_path` (Node
165
+ `activateMenuPath` / `activateMenuBarPath`); `menu()` reads a row's
166
+ `items` back. `Core::menu_submenus` / `menu_bar_submenus` say what is
167
+ open. The pointer opens and closes on a change of row, with no timer,
168
+ so a pointer resting on one row does not undo what the keyboard opened.
169
+ In C a row's `submenu` / `submenu_count` (ABI 26) nest the same
170
+ `KuiMenuItem`s, in `kui_open_menu`, `kui_select` and `kui_menu_bar`; a
171
+ row's flags carry `KUI_MENU_ITEM_SUBMENU`, and a host showing its own
172
+ menus reads and reports a row inside by its path —
173
+ `kui_menu_submenu_count` / `kui_menu_item_path` /
174
+ `kui_activate_menu_path`, and the bar's `kui_menu_bar_submenu_count` /
175
+ `kui_menu_bar_item_path` / `kui_activate_menu_bar_path`; Odin's
176
+ `Menu_Item.submenu` and the same doors. A C menu nested past 32 levels
177
+ — a row that is its own submenu — is refused, as an unknown role is.
178
+ *What you can delete:* a list cut short because a menu could not
179
+ nest — Noticon's `MOVE_TARGETS` cap on the folders a note can move to.
180
+
181
+ - **A span in a face and a size of its own** (backlog F130, from
182
+ Noticon, whose inline `code` was drawn in the body face with a wash).
183
+ `Span::family(FontFamily)` / `Span::mono()` and `Span::size(px)` —
184
+ `family`, `font` and `size` on a JSX `<span>` and in a Lua span table,
185
+ `KuiSpan.family` (with `KUI_SPAN_FAMILY`), `.font` and `.size` in C,
186
+ the same fields on Odin's `Span`. The paragraph still shapes as one
187
+ flow; each span's glyphs are shaped in its face, at that family's
188
+ weights, so a caret, a hit, a selection and the measurement read the
189
+ glyphs that are drawn and byte positions stay exact across a change of
190
+ face mid-line. A sized span's line height scales at the paragraph's
191
+ ratio, a line is as tall as its tallest span (every span then carries
192
+ its metrics, so a line of only smaller ones never comes out shorter),
193
+ measurement sums the lines' own heights, a selection and a caret are
194
+ their line's height, and a span's background is its own height around
195
+ its glyphs rather than the line's. A paragraph with a sized span is
196
+ shaped whole, never chunked as a long line. Pinned by
197
+ `tests/span_face.rs` (the mono width, carets and hits across the
198
+ change, line heights, the caret, the wash), Node's `a span takes a face
199
+ and a size of its own`, and the corpus's first scene, which gained a
200
+ mono span and a sized one in all five adapters; the `text` example
201
+ shows inline code and a larger word. *What you can delete:* inline code
202
+ drawn in the body face, or as a box beside the text.
203
+
204
+ ### Fixed
205
+
206
+ - **A menu's accelerator reads as the platform writes it** (backlog
207
+ F127, from Noticon). `MenuItem::accel` drew its string verbatim in a
208
+ context menu, so a row declared with the portable `"mod+shift+n"` —
209
+ the spelling the docs give for a menu bar, where it was already
210
+ rewritten — showed those eleven characters. `open_menu` now rewrites
211
+ an accelerator it can parse into `Accel::display`'s spelling, as
212
+ `declare_menu_bar` does, and the drawn rows read every accelerator
213
+ through `Accel::label`, so a menu an app draws itself with
214
+ `widgets::context_menu` gets the same. A spelling kui cannot parse
215
+ (`"gd"`) is still drawn exactly as written. `MenuItem::accel_label`
216
+ is the drawn string; `accel_text` stays the declared one. *What you
217
+ can delete:* the `Accel::parse(..).display()` an app ran over its own
218
+ accelerators before handing them to a menu.
219
+ - **A menu is as wide as its rows** (backlog F127). The panel was the
220
+ metric's 200 px whatever it held, so a long label beside a long
221
+ accelerator wrapped one of them onto a second line. It is now as wide
222
+ as its widest label plus its widest accelerator, with at least
223
+ `widgets::MENU_ACCEL_GAP` between them, and never narrower than
224
+ `Metrics::menu_width`; labels and accelerators are one line each.
225
+ - **`scripts/npm-approve.nu` takes the version as npm spells it.**
226
+ Approving alpha.42 as `nu scripts/npm-approve.nu
227
+ @qxuken/kui@0.1.0-alpha.42` looked for a stage whose version was the
228
+ whole spec, found none, and said "nothing staged for
229
+ @qxuken/kui@@qxuken/kui@0.1.0-alpha.42" while the stage sat there. The
230
+ package prefix is cut off now, and a spec naming another package is
231
+ refused by name. A release script, so nothing an app sees.
232
+ - **A macOS menu shortcut on a named key works** (backlog RG146). The
233
+ bar (since alpha.11) and now the context menu build each item's key
234
+ equivalent from the accelerator in the platform's spelling, and
235
+ `Accel::parse` read `⌘⌫`'s `⌫` as a character: AppKit drew ⌘⌫ beside
236
+ the row and the chord did nothing, as with ⌘↩, ⌘← and every named
237
+ key. `parse` now reads a key as `Accel::display` writes it, in either
238
+ platform's spelling.
239
+ - **A dead menu row cannot be chosen by reporting it** (backlog RG147).
240
+ The bar's activate doors performed a disabled row, or one in a
241
+ disabled menu — kui.h said they refused it — and `activate_menu_path`
242
+ a row under a disabled submenu row. Every row on the path must be
243
+ enabled now, as the drawn menus need.
244
+
245
+ **What you can delete.**
246
+
247
+ - The `Accel::parse(..).display()` an app ran over its accelerators
248
+ before handing them to a context menu (F127).
249
+ - A menu cut short, or flattened, because menus could not nest: "Move to
250
+ …" rows one per folder up to a cap (F128).
251
+ - A toolbar's opaque fill over content that scrolls under it (F129).
252
+ - Inline code drawn in the body face, or as a box beside the text (F130).
253
+ - A toolbar's opaque fill over content that scrolls under it (F129).
254
+ - Inline code drawn in the body face, or as a box beside the text (F130).
255
+
256
+ ### Native verification
257
+
258
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-08, over
259
+ F126–F130 from the Noticon wish list, with alpha.43's pre-tag pass over
260
+ them on the Mac: the mechanical round, then three read-only reviews of
261
+ the diff since alpha.42 (the window backdrop and the blur; the menus and
262
+ the span's face; the docs), each claim probed. They filed RG146–RG149,
263
+ built before the tag (a macOS menu shortcut on a named key binding no
264
+ key, a host's report of a dead menu row performed, an Escape spent on a
265
+ submenu an app-drawn menu had noted, a Node span's family losing to an
266
+ inherited font handle), and RG150, open.
267
+
268
+ **macOS**, the pre-tag pass. fmt and clippy are clean; `nu
269
+ scripts/test.nu`: **1911 tests over 142 suites**, 0 failed. The C round
270
+ passes, and so do the **57 scenes**; Node's tests under
271
+ `KUI_CONFORMANCE_REQUIRED=1` (**218 of 218**), `npm run gen` with no
272
+ diff, the examples' typecheck, the headless round (with `backdrop_blur`'s
273
+ drive, which the roster had left out) and the book. The windowed round
274
+ with Node's: **128 windows** (one `transition` window hung at the
275
+ timeout on a first run four at a time and drew on eight runs alone and a
276
+ whole second round). The AX audit: **106/106**. The bench guard against the alpha.42 tag:
277
+ **green**, the guarded rows −0.4 to +3.1% (`deep_nesting_64_levels`,
278
+ −2.1% on a second run); the stream, long-line and cell grid rows flat.
279
+ The Odin
280
+ binding's four steps did not run here — the machine's `odin` links an
281
+ `llvm@22` no longer installed — so CI's check on the tag is their run.
282
+ F126's macOS half and F128's macOS menus were compiled and run on a Mac
283
+ the day they were built, through Noticon; F126 has not met KDE or GNOME.
284
+
24
285
  ## 0.1.0-alpha.42 (2026-10-07)
25
286
 
26
287
  **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
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;
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,27 @@ 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. A host that shows menus itself reports one with
809
+ `Core::activate_menu_path(&[1, 0])` (Node `activateMenuPath`, C
810
+ `kui_activate_menu_path` with a path of `size_t`s). In C a row's
811
+ `submenu` / `submenu_count` nest the same `KuiMenuItem`s, and
812
+ `KUI_MENU_ITEM_SUBMENU` in a row's flags says it has rows, read with
813
+ `kui_menu_item_path`. An
814
+ `accel` in the portable spelling (`"mod+shift+n"`) is drawn the
815
+ platform's way, and the menu widens to its longest row.
816
+
817
+ [ADR 0018](docs/adr/0018-a-menu-bar-the-app-declares.md) ·
818
+ [`tests/submenu.rs`](../crates/kui-core/tests/submenu.rs)
819
+
737
820
  ### How do I take files dropped from the Finder?
738
821
 
739
822
  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.43",
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`. |
@@ -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. |