@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 +420 -0
- package/docs/adr/0007-composite-keyboard-patterns.md +8 -1
- package/docs/adr/0018-a-menu-bar-the-app-declares.md +10 -0
- package/docs/adr/0023-layers-stack-in-the-order-they-open.md +11 -4
- package/encoder.js +22 -4
- package/howto.md +89 -0
- package/index.d.ts +61 -1
- package/index.js +2 -2
- package/jsx-runtime.d.ts +19 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/kui_node.node +0 -0
- package/prebuilds/linux-arm64/kui_node.node +0 -0
- package/prebuilds/linux-x64/kui_node.node +0 -0
- package/prebuilds/win32-x64/kui_node.node +0 -0
- package/props.md +13 -8
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
|
|
184
|
-
the same frame (appended after its parent, in tree
|
|
185
|
-
|
|
186
|
-
|
|
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 *
|
|
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
|
-
|
|
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
|
Binary file
|
|
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::
|
|
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. |
|