@qxuken/kui 0.1.0-alpha.41 → 0.1.0-alpha.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +449 -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/encoder.js +21 -4
- package/howto.md +83 -0
- package/index.d.ts +63 -1
- package/index.js +8 -4
- 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 +385 -374
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,455 @@ listed under both (backlog F61, from the alpha.12 field reports: the list
|
|
|
21
21
|
is what the release knows it broke, and a fix it did not think of as one
|
|
22
22
|
was the first bare bump to break an app in five releases).
|
|
23
23
|
|
|
24
|
+
## 0.1.0-alpha.43 (2026-10-08)
|
|
25
|
+
|
|
26
|
+
**What breaks.**
|
|
27
|
+
|
|
28
|
+
- The drawn menu — a context menu, a select's list, a drawn menu bar's
|
|
29
|
+
dropdown — is as wide as its widest row needs, where it was always
|
|
30
|
+
`Metrics::menu_width` (now its floor), and no row's label or
|
|
31
|
+
accelerator wraps (under Fixed, F127).
|
|
32
|
+
- A menu's accelerator kui can parse is drawn, and read back through
|
|
33
|
+
`Core::menu` / Node's `menu()` / C's `kui_menu_item`, in the platform's
|
|
34
|
+
spelling: `"mod+shift+n"` is `⇧⌘N` or `Ctrl+Shift+N`, where it was the
|
|
35
|
+
string as declared — as the menu bar's already was (under Fixed,
|
|
36
|
+
F127).
|
|
37
|
+
- Rust: `MenuItem` gains `submenu` (under Added, F128), so a struct
|
|
38
|
+
literal of one needs the field; `MenuItem::KEYS` is seven keys, with
|
|
39
|
+
`items`, where it was six.
|
|
40
|
+
- Rust: `WindowEnv` gains `backdrop` (under Added, F126), so a struct
|
|
41
|
+
literal of one needs the field or `..Default::default()`; so does
|
|
42
|
+
`kui_devtools::Window`, in the examples' harness.
|
|
43
|
+
- Rust: `QuadKind` gains `Backdrop` (under Added, F129), so a `match` on
|
|
44
|
+
a quad's kind needs the arm — a renderer of its own draws nothing for
|
|
45
|
+
it; `InteractSpec` and `NodeInfo` gain `backdrop_blur`, for a struct
|
|
46
|
+
literal of either. The conformance report's `kinds` line has a tenth
|
|
47
|
+
column, `backdrop`.
|
|
48
|
+
- C: ABI 26. `KuiSpec` appends `backdrop_blur` (into the tail padding:
|
|
49
|
+
the 64-bit size stays 704), `KuiMenuItem` appends `submenu` and
|
|
50
|
+
`submenu_count` (an array element, so its stride moved, to 72), and
|
|
51
|
+
`KuiRunConfig` appends `backdrop` (44). `KUI_QUAD_BACKDROP` is a new
|
|
52
|
+
quad kind. `KuiSpan` appends `family`, `size` and `font` (an array
|
|
53
|
+
element, so its stride moved, to 56), with the `KUI_SPAN_FAMILY` flag
|
|
54
|
+
(under Added, F130). Recompile; a zeroed tail is what every spec, row,
|
|
55
|
+
span and config had.
|
|
56
|
+
- Rust: `text::Span` gains `family` and `size` (under Added, F130), for a
|
|
57
|
+
struct literal of one; `Span::new` and the builders are unchanged.
|
|
58
|
+
- Node: the wire is v22 — a span carries its face and size after its
|
|
59
|
+
background's radius — so the addon and the package go together, as
|
|
60
|
+
every wire step has.
|
|
61
|
+
- `Accel::parse` reads a named key's label as that key: `"⌘⌫"` is ⌘
|
|
62
|
+
Backspace and `"Ctrl+Page Up"` parses, where the first was the
|
|
63
|
+
character `⌫` and the second nothing (under Fixed, RG146).
|
|
64
|
+
- A host's report of a dead menu row — `Core::activate_menu_bar_item` /
|
|
65
|
+
`_path`, Node's `activateMenuBarItem`, C's `kui_activate_menu_bar_*`,
|
|
66
|
+
or a row under a dead submenu row through `activate_menu_path` — is
|
|
67
|
+
refused and posts nothing, where it was performed (under Fixed,
|
|
68
|
+
RG147).
|
|
69
|
+
|
|
70
|
+
`backdropBlur` is a number row like any other, and a row's `items` rides
|
|
71
|
+
in the JSON a row already was.
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **A window with the desktop behind it** (backlog F126, from Noticon,
|
|
76
|
+
for a sidebar like an Obsidian theme's on a Mac).
|
|
77
|
+
`Launcher::backdrop(Backdrop)` — `backdrop` in Node's window options —
|
|
78
|
+
asks for what shows through the app's windows, named for the effect:
|
|
79
|
+
`Transparent` (the desktop as it is), `Blur` (a live blur of what is
|
|
80
|
+
behind the window) or `Tinted` (the desktop's colour, steady). Which
|
|
81
|
+
regions show it is the app's, by painting them with alpha and the rest
|
|
82
|
+
opaque. macOS: an `NSVisualEffectView` under the content view, blending
|
|
83
|
+
behind the window and dimmed with it in the background — the sidebar
|
|
84
|
+
material for `Blur`, the window-background one for `Tinted`. Windows 11
|
|
85
|
+
22H2 and later: Acrylic for `Blur`, Mica for `Tinted`
|
|
86
|
+
(`DWMWA_SYSTEMBACKDROP_TYPE`, the frame extended under the client
|
|
87
|
+
area), the window without a GDI surface and the device presenting D3D12
|
|
88
|
+
through DirectComposition (`kui_wgpu::GpuOptions::transparent`,
|
|
89
|
+
`Renderer::new_with` / `new_in_with`). Linux: `Blur` asked of the
|
|
90
|
+
compositor — `ext-background-effect-v1` where it is advertised with
|
|
91
|
+
blur, KWin's `org_kde_kwin_blur`, `_KDE_NET_WM_BLUR_BEHIND_REGION` under
|
|
92
|
+
X11. Where the OS has no effect to give — GNOME, other Linux, Windows
|
|
93
|
+
10 — `Blur` and `Tinted` draw the desktop's wallpaper (GNOME's
|
|
94
|
+
`picture-uri`, Plasma's config, `SPI_GETDESKWALLPAPER`), read on a
|
|
95
|
+
thread, scaled down and blurred once and cached by path and modification
|
|
96
|
+
time, as the window's ground under the frame
|
|
97
|
+
(`Renderer::set_ground` / `set_ground_uv`), aligned to where the window
|
|
98
|
+
sits on its monitor (centred on Wayland), and report `Tinted`; with no
|
|
99
|
+
wallpaper, `Opaque`. Where the OS draws the effect the frame is cleared
|
|
100
|
+
to nothing rather than to the theme's `bg`, and glyphs are grayscale
|
|
101
|
+
under `TextAa::Auto`. What the window got is `env.window.backdrop`
|
|
102
|
+
(`window.backdrop` in Node and Lua, `kui_env_set_backdrop` for a C host
|
|
103
|
+
that makes its own), so a view paints opaque when it says `Opaque`.
|
|
104
|
+
Every window of the app takes it but a popup and the devtools' own; an
|
|
105
|
+
app that does not ask opens exactly the window and the swapchain it
|
|
106
|
+
always did. `KUI_BACKDROP_EMULATE=1` draws the wallpaper for `Blur` and
|
|
107
|
+
`Tinted` on Windows and Linux, to look at it (macOS reads no wallpaper,
|
|
108
|
+
so a window there reads `Opaque`). Seen in windows on Windows 11:
|
|
109
|
+
Acrylic blurring a red window behind the translucent region, Mica, the
|
|
110
|
+
bare desktop through `Transparent`, the wallpaper drawn by kui under
|
|
111
|
+
`KUI_BACKDROP_EMULATE`, and an opaque devtools window beside them; on
|
|
112
|
+
WSLg (Wayland, no blur protocol, no gsettings) a `Blur` reading
|
|
113
|
+
`Opaque`. On a Mac, built through Noticon, the library column shows the
|
|
114
|
+
vibrancy. No KDE or GNOME session was at hand: the Linux half compiles
|
|
115
|
+
and passes clippy there, untried on either desktop. C asks with
|
|
116
|
+
`KuiRunConfig.backdrop` (a `KUI_BACKDROP_*`) and a view reads what the
|
|
117
|
+
window got with `kui_ctx_backdrop`, the answer and not the ask; Odin's
|
|
118
|
+
`Run_Config.backdrop` and `kui.ctx_backdrop`. The `backdrop` example
|
|
119
|
+
paints a translucent library and an opaque page from the reading.
|
|
120
|
+
*What you can delete:* nothing — this is new.
|
|
121
|
+
- **A node blurs what is drawn beneath it** (backlog F129, from Noticon,
|
|
122
|
+
for a frosted toolbar over a scrolling note): CSS's `backdrop-filter:
|
|
123
|
+
blur()`. `NodeSpec::backdrop_blur(radius)` — `backdropBlur` in JSX,
|
|
124
|
+
`backdrop_blur` in Lua and Odin, `KuiSpec.backdrop_blur` in C — blurs
|
|
125
|
+
everything painted before the node, inside its rounded box, by that
|
|
126
|
+
radius in logical px (the Gaussian's standard deviation, as CSS's):
|
|
127
|
+
the ancestors' backgrounds, the siblings and the content scrolling
|
|
128
|
+
under it, and the window's backdrop where it has one. The node's own
|
|
129
|
+
`bg`, border and children paint over the blur, so a translucent `bg`
|
|
130
|
+
is frosted glass; it is clipped as the node is and faded by its
|
|
131
|
+
`opacity`. The core emits a `QuadKind::Backdrop` quad
|
|
132
|
+
(`KUI_QUAD_BACKDROP`) just before the node's own paint, carrying the
|
|
133
|
+
shape, the clip, the radius in physical px and the group opacity.
|
|
134
|
+
kui-wgpu draws a frame that has one into an offscreen copy of the
|
|
135
|
+
surface, breaks the pass at each, copies out the region under the node
|
|
136
|
+
plus three radii around it, averages it down by a power of two that
|
|
137
|
+
leaves a kernel of two to four texels, blurs it along each axis, and
|
|
138
|
+
writes it back inside the node's rounded rect and its clip, mixing by
|
|
139
|
+
coverage and opacity with blending off so a transparent window stays
|
|
140
|
+
premultiplied; then blits the frame to the surface. A frame without
|
|
141
|
+
one is drawn exactly as before, and the offscreen textures go after
|
|
142
|
+
120 frames without one. A renderer that cannot read back what it drew
|
|
143
|
+
draws nothing for the quad, which leaves the node over an unblurred
|
|
144
|
+
backdrop; a headless core has the quad in its display list and no
|
|
145
|
+
pixels. `diag::BACKDROP_BLUR_HIDDEN` warns of one under an opaque `bg`
|
|
146
|
+
of its own, which hides all of it. The devtools inspector and
|
|
147
|
+
`Core::nodes` (`backdropBlur` in Node) read the radius. Seen on Windows
|
|
148
|
+
11 in the new `backdrop_blur` example, in an opaque window and over
|
|
149
|
+
Acrylic: the cards under the toolbar smeared, the edge below it sharp,
|
|
150
|
+
the badge's blur inside its corners. *What you can delete:* a
|
|
151
|
+
toolbar's opaque fill over content that scrolls under it.
|
|
152
|
+
- **Submenus** (backlog F128, from Noticon, for "Move to ▸" and "Sort by
|
|
153
|
+
▸"). `MenuItem::submenu(label, rows)` — `items` on a row in Node and
|
|
154
|
+
Lua, read by the one row parser — is a row with a chevron that opens
|
|
155
|
+
its rows in a menu beside it: when the pointer rests on it, on a click,
|
|
156
|
+
on Enter or the Right arrow (focus on its first row). Left or Escape
|
|
157
|
+
closes it, focus back on its row, and Escape again closes the menu.
|
|
158
|
+
They nest, in the context menu, a select's list and the drawn menu
|
|
159
|
+
bar's menus alike, and a chosen row inside posts its own `{kind:"menu",
|
|
160
|
+
role, item}` on the node the menu is about, as any row does; the row
|
|
161
|
+
that opens one is never chosen. On macOS the context menu and the menu
|
|
162
|
+
bar build an `NSMenu` submenu, which AppKit opens itself, and report a
|
|
163
|
+
row inside by its path. A host showing menus itself reports one with
|
|
164
|
+
`Core::activate_menu_path` / `activate_menu_bar_path` (Node
|
|
165
|
+
`activateMenuPath` / `activateMenuBarPath`); `menu()` reads a row's
|
|
166
|
+
`items` back. `Core::menu_submenus` / `menu_bar_submenus` say what is
|
|
167
|
+
open. The pointer opens and closes on a change of row, with no timer,
|
|
168
|
+
so a pointer resting on one row does not undo what the keyboard opened.
|
|
169
|
+
In C a row's `submenu` / `submenu_count` (ABI 26) nest the same
|
|
170
|
+
`KuiMenuItem`s, in `kui_open_menu`, `kui_select` and `kui_menu_bar`; a
|
|
171
|
+
row's flags carry `KUI_MENU_ITEM_SUBMENU`, and a host showing its own
|
|
172
|
+
menus reads and reports a row inside by its path —
|
|
173
|
+
`kui_menu_submenu_count` / `kui_menu_item_path` /
|
|
174
|
+
`kui_activate_menu_path`, and the bar's `kui_menu_bar_submenu_count` /
|
|
175
|
+
`kui_menu_bar_item_path` / `kui_activate_menu_bar_path`; Odin's
|
|
176
|
+
`Menu_Item.submenu` and the same doors. A C menu nested past 32 levels
|
|
177
|
+
— a row that is its own submenu — is refused, as an unknown role is.
|
|
178
|
+
*What you can delete:* a list cut short because a menu could not
|
|
179
|
+
nest — Noticon's `MOVE_TARGETS` cap on the folders a note can move to.
|
|
180
|
+
|
|
181
|
+
- **A span in a face and a size of its own** (backlog F130, from
|
|
182
|
+
Noticon, whose inline `code` was drawn in the body face with a wash).
|
|
183
|
+
`Span::family(FontFamily)` / `Span::mono()` and `Span::size(px)` —
|
|
184
|
+
`family`, `font` and `size` on a JSX `<span>` and in a Lua span table,
|
|
185
|
+
`KuiSpan.family` (with `KUI_SPAN_FAMILY`), `.font` and `.size` in C,
|
|
186
|
+
the same fields on Odin's `Span`. The paragraph still shapes as one
|
|
187
|
+
flow; each span's glyphs are shaped in its face, at that family's
|
|
188
|
+
weights, so a caret, a hit, a selection and the measurement read the
|
|
189
|
+
glyphs that are drawn and byte positions stay exact across a change of
|
|
190
|
+
face mid-line. A sized span's line height scales at the paragraph's
|
|
191
|
+
ratio, a line is as tall as its tallest span (every span then carries
|
|
192
|
+
its metrics, so a line of only smaller ones never comes out shorter),
|
|
193
|
+
measurement sums the lines' own heights, a selection and a caret are
|
|
194
|
+
their line's height, and a span's background is its own height around
|
|
195
|
+
its glyphs rather than the line's. A paragraph with a sized span is
|
|
196
|
+
shaped whole, never chunked as a long line. Pinned by
|
|
197
|
+
`tests/span_face.rs` (the mono width, carets and hits across the
|
|
198
|
+
change, line heights, the caret, the wash), Node's `a span takes a face
|
|
199
|
+
and a size of its own`, and the corpus's first scene, which gained a
|
|
200
|
+
mono span and a sized one in all five adapters; the `text` example
|
|
201
|
+
shows inline code and a larger word. *What you can delete:* inline code
|
|
202
|
+
drawn in the body face, or as a box beside the text.
|
|
203
|
+
|
|
204
|
+
### Fixed
|
|
205
|
+
|
|
206
|
+
- **A menu's accelerator reads as the platform writes it** (backlog
|
|
207
|
+
F127, from Noticon). `MenuItem::accel` drew its string verbatim in a
|
|
208
|
+
context menu, so a row declared with the portable `"mod+shift+n"` —
|
|
209
|
+
the spelling the docs give for a menu bar, where it was already
|
|
210
|
+
rewritten — showed those eleven characters. `open_menu` now rewrites
|
|
211
|
+
an accelerator it can parse into `Accel::display`'s spelling, as
|
|
212
|
+
`declare_menu_bar` does, and the drawn rows read every accelerator
|
|
213
|
+
through `Accel::label`, so a menu an app draws itself with
|
|
214
|
+
`widgets::context_menu` gets the same. A spelling kui cannot parse
|
|
215
|
+
(`"gd"`) is still drawn exactly as written. `MenuItem::accel_label`
|
|
216
|
+
is the drawn string; `accel_text` stays the declared one. *What you
|
|
217
|
+
can delete:* the `Accel::parse(..).display()` an app ran over its own
|
|
218
|
+
accelerators before handing them to a menu.
|
|
219
|
+
- **A menu is as wide as its rows** (backlog F127). The panel was the
|
|
220
|
+
metric's 200 px whatever it held, so a long label beside a long
|
|
221
|
+
accelerator wrapped one of them onto a second line. It is now as wide
|
|
222
|
+
as its widest label plus its widest accelerator, with at least
|
|
223
|
+
`widgets::MENU_ACCEL_GAP` between them, and never narrower than
|
|
224
|
+
`Metrics::menu_width`; labels and accelerators are one line each.
|
|
225
|
+
- **`scripts/npm-approve.nu` takes the version as npm spells it.**
|
|
226
|
+
Approving alpha.42 as `nu scripts/npm-approve.nu
|
|
227
|
+
@qxuken/kui@0.1.0-alpha.42` looked for a stage whose version was the
|
|
228
|
+
whole spec, found none, and said "nothing staged for
|
|
229
|
+
@qxuken/kui@@qxuken/kui@0.1.0-alpha.42" while the stage sat there. The
|
|
230
|
+
package prefix is cut off now, and a spec naming another package is
|
|
231
|
+
refused by name. A release script, so nothing an app sees.
|
|
232
|
+
- **A macOS menu shortcut on a named key works** (backlog RG146). The
|
|
233
|
+
bar (since alpha.11) and now the context menu build each item's key
|
|
234
|
+
equivalent from the accelerator in the platform's spelling, and
|
|
235
|
+
`Accel::parse` read `⌘⌫`'s `⌫` as a character: AppKit drew ⌘⌫ beside
|
|
236
|
+
the row and the chord did nothing, as with ⌘↩, ⌘← and every named
|
|
237
|
+
key. `parse` now reads a key as `Accel::display` writes it, in either
|
|
238
|
+
platform's spelling.
|
|
239
|
+
- **A dead menu row cannot be chosen by reporting it** (backlog RG147).
|
|
240
|
+
The bar's activate doors performed a disabled row, or one in a
|
|
241
|
+
disabled menu — kui.h said they refused it — and `activate_menu_path`
|
|
242
|
+
a row under a disabled submenu row. Every row on the path must be
|
|
243
|
+
enabled now, as the drawn menus need.
|
|
244
|
+
|
|
245
|
+
**What you can delete.**
|
|
246
|
+
|
|
247
|
+
- The `Accel::parse(..).display()` an app ran over its accelerators
|
|
248
|
+
before handing them to a context menu (F127).
|
|
249
|
+
- A menu cut short, or flattened, because menus could not nest: "Move to
|
|
250
|
+
…" rows one per folder up to a cap (F128).
|
|
251
|
+
- A toolbar's opaque fill over content that scrolls under it (F129).
|
|
252
|
+
- Inline code drawn in the body face, or as a box beside the text (F130).
|
|
253
|
+
- A toolbar's opaque fill over content that scrolls under it (F129).
|
|
254
|
+
- Inline code drawn in the body face, or as a box beside the text (F130).
|
|
255
|
+
|
|
256
|
+
### Native verification
|
|
257
|
+
|
|
258
|
+
The by-hand round alpha.6 introduced (backlog R4), on 2026-10-08, over
|
|
259
|
+
F126–F130 from the Noticon wish list, with alpha.43's pre-tag pass over
|
|
260
|
+
them on the Mac: the mechanical round, then three read-only reviews of
|
|
261
|
+
the diff since alpha.42 (the window backdrop and the blur; the menus and
|
|
262
|
+
the span's face; the docs), each claim probed. They filed RG146–RG149,
|
|
263
|
+
built before the tag (a macOS menu shortcut on a named key binding no
|
|
264
|
+
key, a host's report of a dead menu row performed, an Escape spent on a
|
|
265
|
+
submenu an app-drawn menu had noted, a Node span's family losing to an
|
|
266
|
+
inherited font handle), and RG150, open.
|
|
267
|
+
|
|
268
|
+
**macOS**, the pre-tag pass. fmt and clippy are clean; `nu
|
|
269
|
+
scripts/test.nu`: **1911 tests over 142 suites**, 0 failed. The C round
|
|
270
|
+
passes, and so do the **57 scenes**; Node's tests under
|
|
271
|
+
`KUI_CONFORMANCE_REQUIRED=1` (**218 of 218**), `npm run gen` with no
|
|
272
|
+
diff, the examples' typecheck, the headless round (with `backdrop_blur`'s
|
|
273
|
+
drive, which the roster had left out) and the book. The windowed round
|
|
274
|
+
with Node's: **128 windows** (one `transition` window hung at the
|
|
275
|
+
timeout on a first run four at a time and drew on eight runs alone and a
|
|
276
|
+
whole second round). The AX audit: **106/106**. The bench guard against the alpha.42 tag:
|
|
277
|
+
**green**, the guarded rows −0.4 to +3.1% (`deep_nesting_64_levels`,
|
|
278
|
+
−2.1% on a second run); the stream, long-line and cell grid rows flat.
|
|
279
|
+
The Odin
|
|
280
|
+
binding's four steps did not run here — the machine's `odin` links an
|
|
281
|
+
`llvm@22` no longer installed — so CI's check on the tag is their run.
|
|
282
|
+
F126's macOS half and F128's macOS menus were compiled and run on a Mac
|
|
283
|
+
the day they were built, through Noticon; F126 has not met KDE or GNOME.
|
|
284
|
+
|
|
285
|
+
## 0.1.0-alpha.42 (2026-10-07)
|
|
286
|
+
|
|
287
|
+
**What breaks.**
|
|
288
|
+
|
|
289
|
+
- A press after a dead key it does not combine with carries what the
|
|
290
|
+
platform typed in its `text`: German's `^ space` is a Space whose
|
|
291
|
+
`text` is `^`, and `^ z` a `z` whose `text` is `^z`, where they said `" "`
|
|
292
|
+
and `z` — in a key sink's payload and in what an editor inserts (under
|
|
293
|
+
Fixed, RG127). A keymap that binds Space by its `text` rather than its
|
|
294
|
+
`code` sees the accent there.
|
|
295
|
+
- Node: a whole number past 2^53 in a message comes back a `number`,
|
|
296
|
+
where it came back a `BigInt` (RG138).
|
|
297
|
+
- C: `kui_take_menu_action` with a NULL or too-small `out` leaves the
|
|
298
|
+
action queued, where it dropped it; `kui_take_warnings` keeps what does
|
|
299
|
+
not fit `cap` for the next call, where it dropped it; `kui_draw_data`'s
|
|
300
|
+
`fragments` and `textures` are NULL on a frame that draws none, where
|
|
301
|
+
they were a non-NULL pointer to nothing (RG133, RG134, RG131).
|
|
302
|
+
- A text style whose size or line height is under a pixel — 0, negative,
|
|
303
|
+
NaN — shapes at a pixel, where Node and Lua aborted or hung and Rust
|
|
304
|
+
panicked (RG136); a cell grid's rows are a pixel apart at an infinite
|
|
305
|
+
one, where they went to the layout's limit (RG144).
|
|
306
|
+
- Rust: `schema::Door`, `schema::CustomProp` and `schema::ElementDef` gain
|
|
307
|
+
`odin` (under Added, the Odin binding), so a struct literal of one needs
|
|
308
|
+
the field.
|
|
309
|
+
|
|
310
|
+
C stays at ABI 25 (kui.h gains no function and no field; four doors now
|
|
311
|
+
keep what it always promised) and the Node wire at v21 (no frame version).
|
|
312
|
+
|
|
313
|
+
### Added
|
|
314
|
+
|
|
315
|
+
- **An Odin binding, experimental** (`packages/odin`). `kui/c` mirrors
|
|
316
|
+
kui.h declaration by declaration, pinned to the C compiler's layout by
|
|
317
|
+
848 generated `#assert`s. `kui` is the typed layer, generated from
|
|
318
|
+
kui.h and the prop schema:
|
|
319
|
+
- `Spec` and `Text_Style`, one field per schema row;
|
|
320
|
+
- an enum or bit set per family of constants;
|
|
321
|
+
- a door per C function: out-params as results, arrays as slices,
|
|
322
|
+
messages as plain Odin values with `#[derive(Message)]`'s `kind`
|
|
323
|
+
rule.
|
|
324
|
+
|
|
325
|
+
The elements are written by hand: containers close at the end of their
|
|
326
|
+
`if` through `@(deferred_in)`. A C function the hand-written files do
|
|
327
|
+
not call gets a generated door, so the binding covers kui.h whole: 219
|
|
328
|
+
generated, 51 by hand, 2 skipped with their reason.
|
|
329
|
+
`examples/odin/tools/surface.odin` is `surface.c` through it, and calls
|
|
330
|
+
every door, and `examples/odin/tools/conformance.odin` rebuilds the
|
|
331
|
+
scene corpus through `Spec` and the doors, matching the reference report
|
|
332
|
+
byte for byte, as the Rust, Lua, C and Node adapters do.
|
|
333
|
+
`polyline` and `polygon` take points (`[][2]f32`), the count kui.h
|
|
334
|
+
means; a dash or a pivot is a fixed array in a `Maybe` (`[5]f32`,
|
|
335
|
+
`[2]f32`), so a short one cannot be written; and pixels are checked
|
|
336
|
+
against the size beside them.
|
|
337
|
+
|
|
338
|
+
It covers extensions both ways. An Odin host loads a plugin with
|
|
339
|
+
`ctx_add_extension` and `slot`. An Odin plugin is a shared library whose
|
|
340
|
+
seven `kui_ext_*` exports are one line each over `extension.odin`.
|
|
341
|
+
Built with `KUI_PLUGIN` on macOS and Linux, it links no kui and loads
|
|
342
|
+
into any host (on Windows it imports `kui_ffi.dll`, as a C plugin does):
|
|
343
|
+
`nu scripts/odin.nu slots` drives the Odin panel in the C and Rust
|
|
344
|
+
hosts and the C panel in the Odin host.
|
|
345
|
+
|
|
346
|
+
Run it with `nu scripts/odin.nu gen | test | slots | run counter`. It is
|
|
347
|
+
not published. CI's `check` installs a pinned Odin release (`ODIN_VERSION`)
|
|
348
|
+
and runs `gen --check`, `check`, `test` and `slots`, so a header or schema
|
|
349
|
+
change that was not regenerated goes red there. All four also run green
|
|
350
|
+
by hand on Windows x64 and Linux x64. On Windows the binding links
|
|
351
|
+
`kui_ffi.dll.lib`, odin.nu puts `kui_ffi.dll` beside the programs, and
|
|
352
|
+
`gen --check` reads a CRLF checkout as current.
|
|
353
|
+
- `scripts/pack-ffi.nu`: libkui_ffi for every platform kui ships, dynamic
|
|
354
|
+
and static, with `kui.h`, rustc's `native-static-libs` as `link.txt`, a
|
|
355
|
+
tarball each and `SHA256SUMS`. It is for a C or Odin host that links a
|
|
356
|
+
library instead of building the workspace. The machine's own platform
|
|
357
|
+
is built natively. Linux x64 and arm64 (glibc 2.28) and Windows x64 are
|
|
358
|
+
built in a pinned Docker image, with cargo-zigbuild at the release
|
|
359
|
+
workflow's pins and with cargo-xwin. Windows also asks for
|
|
360
|
+
`--accept-msvc-license`, for the CRT and SDK xwin downloads. macOS is
|
|
361
|
+
built on a Mac only. A Windows pack carries windows-targets' import
|
|
362
|
+
libraries (`windows.0.53.0.lib`, `windows.0.52.0.lib`), which its
|
|
363
|
+
`link.txt` names and no SDK has, so a static link finds them. Every
|
|
364
|
+
leg has been run, and a C host linked against each x64 pack alone,
|
|
365
|
+
dynamically and statically, and run; the arm64 pack was read, with no
|
|
366
|
+
arm64 machine to run it on. `kui.h` ships with LF line ends from any
|
|
367
|
+
checkout.
|
|
368
|
+
- `examples/rust/tools/schema-dump.rs`: the prop schema (props,
|
|
369
|
+
composites, elements, events, the verb table, the name lists) as JSON,
|
|
370
|
+
for a binding generated outside Rust.
|
|
371
|
+
- The verb table (`schema::DOORS`) and `docs/props.md` have an Odin
|
|
372
|
+
column beside C's, each spelling held by the Odin generator both ways;
|
|
373
|
+
Node's `protocol()` carries it (`Door.odin`).
|
|
374
|
+
|
|
375
|
+
### Fixed
|
|
376
|
+
|
|
377
|
+
- **A dead key types its accent before a key it does not combine with**
|
|
378
|
+
(backlog RG127, from the Windows and Linux round after alpha.41). With
|
|
379
|
+
German, `^ space` typed a space where it types `^`, `´ space` a space
|
|
380
|
+
where it types `´`, and `^ z` a `z` where it types `^z` — on Windows
|
|
381
|
+
always, and under X11 with `imeOff` on; on US-International, where `'`
|
|
382
|
+
and `"` are dead, a quote could not be typed at all. winit composed it
|
|
383
|
+
right: the runner read a press's text off the logical key, which is
|
|
384
|
+
the key the accent fell back to, and Space's insert was a space
|
|
385
|
+
whatever the press carried. A press's text is now what the platform
|
|
386
|
+
composed, the layout's character where it composed nothing, and Space
|
|
387
|
+
inserts its own. `^ e` was right and is unchanged; with the input
|
|
388
|
+
method on, X11 composes through XIM as before.
|
|
389
|
+
- The docs of F103 (alpha.41) said an animating Windows window behind
|
|
390
|
+
other windows skips its frames. It does not: DWM composes a covered
|
|
391
|
+
window, and it draws at the display's rate, 241 frames a second here,
|
|
392
|
+
as an uncovered one does; minimized, it asks for none (RG45). F103's
|
|
393
|
+
wait holds there only for an acquire that times out. `retry.rs` says so
|
|
394
|
+
now (backlog RG128).
|
|
395
|
+
- **Text is read aloud on Windows and Linux** (backlog RG129, from the
|
|
396
|
+
regression and smoke round after RG127). Every static text — a line of
|
|
397
|
+
text, a list row's content, a drawn title — reached UI Automation with
|
|
398
|
+
an empty Name and AT-SPI with an empty name: AccessKit names a label by
|
|
399
|
+
its value, and kui set only its label. macOS read it all along.
|
|
400
|
+
- **A disclosure or a select opens from Narrator** (RG130). UI
|
|
401
|
+
Automation gives a node with `expanded` its ExpandCollapse pattern in
|
|
402
|
+
place of Invoke, and its Expand and Collapse reached nothing. They are
|
|
403
|
+
the node's click.
|
|
404
|
+
- **A text size or line height of 0 no longer ends the process** (RG136).
|
|
405
|
+
`lineHeight: 0`, `size: 0`, or a size under 0.4 (whose line height
|
|
406
|
+
rounds to 0) aborted a Node or Lua process — cosmic-text asserts a line
|
|
407
|
+
height is not 0 — and a negative one spun its layout; on an `edit`, a
|
|
408
|
+
`cells` and in `measureText` too. Every buffer is shaped at a pixel at
|
|
409
|
+
least. C's door had always read them as unset.
|
|
410
|
+
- C: what kui.h says a door hands out lives as long as it says (RG131–
|
|
411
|
+
RG134). A second `kui_draw_data` in a frame freed the arrays the first
|
|
412
|
+
handed out; `kui_access_runs` freed the strings `kui_access_tree`
|
|
413
|
+
handed out; reading a menu row or asking for a copy freed a taken menu
|
|
414
|
+
action's text; and `kui_take_warnings` dropped what did not fit `cap`,
|
|
415
|
+
where kui.h says the rest wait.
|
|
416
|
+
- A custom editor whose `caret` or `selection_anchor` falls inside a
|
|
417
|
+
character has an access tree (RG135): the run was sliced there and
|
|
418
|
+
panicked, which under C emptied the tree every frame; the position is
|
|
419
|
+
the character's start.
|
|
420
|
+
- Node: `measureText` inside a `<devtoolsTab>` function child no longer
|
|
421
|
+
breaks the frame being encoded (RG137); a message, a `<select>` option
|
|
422
|
+
or a `menuBar` row holding a string cut through an emoji draws its lone
|
|
423
|
+
surrogate as U+FFFD rather than failing the frame, and `dir: null` is
|
|
424
|
+
absent like every other null prop (RG138, RG140).
|
|
425
|
+
|
|
426
|
+
### Native verification
|
|
427
|
+
|
|
428
|
+
The by-hand round alpha.6 introduced (backlog R4), on 2026-10-07, over
|
|
429
|
+
the rounds after the alpha.41 tag — the Odin binding and `pack-ffi.nu`,
|
|
430
|
+
RG127 and RG128 from the Windows and Linux round after alpha.41, and the
|
|
431
|
+
regression and smoke round after RG127 (RG129–RG138) — with alpha.42's
|
|
432
|
+
pre-tag pass over them on the Mac: the mechanical round, then four
|
|
433
|
+
read-only reviews of the diff since alpha.41 (the Odin binding; the C
|
|
434
|
+
doors and `pack-ffi.nu`; the runner's keys, the access bridge, the text
|
|
435
|
+
floor and Node; the docs), each claim probed. They filed RG140–RG144,
|
|
436
|
+
built before the tag (a `<select>` option or menu bar row cut through an
|
|
437
|
+
emoji failing the Node frame, `kui_draw_data` while a frame built losing
|
|
438
|
+
the finished frame's fragment draws, the Odin drains freeing the strings
|
|
439
|
+
they returned, Odin's popups, unchecked slices and acronym kinds, a cell
|
|
440
|
+
grid's infinite line height), and RG145, open.
|
|
441
|
+
|
|
442
|
+
**macOS**, the pre-tag pass. fmt and clippy are clean; `cargo test
|
|
443
|
+
--workspace --features kui-core/conformance`: **1856 tests over 139
|
|
444
|
+
suites**. The C round passes, and so do the **57 scenes**, the Odin
|
|
445
|
+
binding's `gen --check`, `check`, `test` and `slots`, Node's tests under
|
|
446
|
+
`KUI_CONFORMANCE_REQUIRED=1` (**215 of 215**), `npm run gen` with no
|
|
447
|
+
diff, the headless round and the book. The windowed round with Node's:
|
|
448
|
+
**124 windows**. The AX audit: **106/106**. RG127's dead keys, which the
|
|
449
|
+
Windows and Linux round could not reach on a Mac: twelve sequences typed
|
|
450
|
+
into the `edit` example through `CGEventPostToPid` and read back through
|
|
451
|
+
AX, each as AppKit composes it (`⌥e e` is `é`, `⌥e space` `´`, `⌥e z`
|
|
452
|
+
`´z`). The bench guard against the alpha.41 tag: **green**, the guarded
|
|
453
|
+
rows −1.2 to −0.3%, RG126's warm cell grid −0.4%.
|
|
454
|
+
|
|
455
|
+
**Windows 11 x64 (MSVC) and Linux x64 (WSL Ubuntu 24.04), rustc
|
|
456
|
+
1.99.0**, on the tree with the pre-tag fixes and `pack-ffi.nu`'s. fmt
|
|
457
|
+
and `cargo clippy --workspace --all-targets --features
|
|
458
|
+
kui-core/conformance -- -D warnings` are clean on both. `cargo test
|
|
459
|
+
--workspace --features kui-core/conformance`: **1857 tests over 140
|
|
460
|
+
suites** on Windows (5 ignored), **1853 over 139** on Linux (4
|
|
461
|
+
ignored), **0 failed**. `cargo audit --deny warnings` is clean over
|
|
462
|
+
`Cargo.lock`'s 444 crates. `pack-ffi.nu` ran every leg but macOS's: native
|
|
463
|
+
on both, and linux-x64, linux-arm64 and win32-x64 (cargo-xwin) in its
|
|
464
|
+
container. A C host linked against each x64 pack alone, dynamically and
|
|
465
|
+
statically, ran; the arm64 pack was read, not run. It found three things,
|
|
466
|
+
fixed before the tag (RG145): a CRLF `kui.h` from a CRLF checkout, the
|
|
467
|
+
closing table lost off a terminal, and a `docker` that could not be
|
|
468
|
+
spawned stopping the script. Earlier the same day, before the pre-tag
|
|
469
|
+
fixes, the Odin binding's four steps passed on both, and the regression
|
|
470
|
+
and smoke round after RG127 ran the windowed smoke and walked the
|
|
471
|
+
accessibility tree through UI Automation and AT-SPI.
|
|
472
|
+
|
|
24
473
|
## 0.1.0-alpha.41 (2026-10-07)
|
|
25
474
|
|
|
26
475
|
**What breaks.**
|
|
@@ -331,7 +331,14 @@ derived orientation are the whole surface.
|
|
|
331
331
|
decision 5's warning is where an app feels the missing semantic half.
|
|
332
332
|
- **Submenus.** Left / Right in a `menu` should close and open them. kui
|
|
333
333
|
has no submenu relation (a `menu` inside a `menuItem`), so today those
|
|
334
|
-
arrows step like any other menu's.
|
|
334
|
+
arrows step like any other menu's. *Built 2026-10-08 for the core's own
|
|
335
|
+
menus (backlog F128):* Right on a row with a submenu opens it with focus
|
|
336
|
+
on its first row, Left inside one closes it with focus back on its row,
|
|
337
|
+
and Escape closes the innermost before the menu. The relation is the
|
|
338
|
+
core's state rather than a derived one — a submenu's panel is a `menu`
|
|
339
|
+
beside its row, not inside the `menuItem` (which is named from its
|
|
340
|
+
content), and the item walk already stops at a nested `menu` — so an
|
|
341
|
+
app's own `menu` composites step on Left / Right as before.
|
|
335
342
|
- **`radio-without-group`.** A lone `radio` is invalid ARIA and now also
|
|
336
343
|
means "no arrows here". A warning in the shape of `control-without-name`
|
|
337
344
|
once something in the repo declares radios — the fixture will, so this
|
|
@@ -189,6 +189,16 @@ does. An app that has to write the menu twice has not been given a menu.
|
|
|
189
189
|
something wants one, it wants it in the context menu too, and that is one
|
|
190
190
|
change to `MenuItem` rather than two features.
|
|
191
191
|
|
|
192
|
+
*Amended 2026-10-08 (backlog F128).* Something wanted one — Noticon's
|
|
193
|
+
"Move to ▸" and "Sort by ▸", in the context menu first — and it was that
|
|
194
|
+
one change: `MenuItem::submenu` (`items` in plain data, so every
|
|
195
|
+
binding's one parser reads it), drawn by the same `menu_panel` beside
|
|
196
|
+
its row, in the context menu and the drawn bar alike, and an `NSMenu`
|
|
197
|
+
submenu where the platform draws them. `BarMenu` is still one level;
|
|
198
|
+
its rows nest. There is no hover timer: the core opens or closes on a
|
|
199
|
+
*change* of hovered row, which is what keeps a pointer resting on one
|
|
200
|
+
row from undoing what the keyboard just opened.
|
|
201
|
+
|
|
192
202
|
- **Standard application-menu roles** (`about`, `quit`, `services`, and
|
|
193
203
|
macOS's `NSApplication` responders). They change what an item *is* — one
|
|
194
204
|
the platform performs rather than one the app hears — so they need their
|
package/encoder.js
CHANGED
|
@@ -402,7 +402,8 @@ export function createEncoder(P) {
|
|
|
402
402
|
const np = fi++;
|
|
403
403
|
let n = 0;
|
|
404
404
|
// dir and size first: the decoder constructs spec/style from them.
|
|
405
|
-
|
|
405
|
+
// `!= null`: a null dir is absent, as every other null prop is.
|
|
406
|
+
if (p.dir != null && p.dir !== 'column') {
|
|
406
407
|
// `table` is a column whose rows' cells line up (ADR 0033).
|
|
407
408
|
const dir = p.dir === 'row' ? 1 : p.dir === 'table' ? 2 : undefined;
|
|
408
409
|
if (dir === undefined) throw new Error(`bad dir ${JSON.stringify(p.dir)} (row | column | table)`);
|
|
@@ -734,6 +735,8 @@ export function createEncoder(P) {
|
|
|
734
735
|
// dotted (v13, backlog K4). A span's own underline colour and style
|
|
735
736
|
// beat the enclosing span's; either implies the underline. Then the
|
|
736
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
|
|
737
740
|
// own beats the enclosing span's.
|
|
738
741
|
function collectSpans(node, st, out) {
|
|
739
742
|
if (node == null || typeof node === 'boolean') return;
|
|
@@ -751,7 +754,7 @@ export function createEncoder(P) {
|
|
|
751
754
|
(st.ul?.ref ? 512 : 0) |
|
|
752
755
|
(st.ulStyle === 'wavy' ? 1024 : 0) |
|
|
753
756
|
(st.ulStyle === 'dotted' ? 2048 : 0);
|
|
754
|
-
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]);
|
|
755
758
|
return;
|
|
756
759
|
}
|
|
757
760
|
if (Array.isArray(node)) {
|
|
@@ -766,6 +769,12 @@ export function createEncoder(P) {
|
|
|
766
769
|
if (p.bgRadius != null && !(typeof p.bgRadius === 'number' && p.bgRadius >= 0)) {
|
|
767
770
|
throw new Error(`bad bgRadius ${JSON.stringify(p.bgRadius)} on <span> (a number of logical px, 0 or more)`);
|
|
768
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
|
+
}
|
|
769
778
|
collectSpans(
|
|
770
779
|
node.children,
|
|
771
780
|
{
|
|
@@ -778,6 +787,11 @@ export function createEncoder(P) {
|
|
|
778
787
|
ul: spanColor(p.underlineColor, st.ul),
|
|
779
788
|
ulStyle: p.underlineStyle ?? st.ulStyle,
|
|
780
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,
|
|
781
795
|
},
|
|
782
796
|
out,
|
|
783
797
|
);
|
|
@@ -828,15 +842,18 @@ export function createEncoder(P) {
|
|
|
828
842
|
collectSpans(el.children, {}, spans);
|
|
829
843
|
f[fi++] = OP.richText;
|
|
830
844
|
props(p, null, false);
|
|
831
|
-
reserve(8 + spans.length *
|
|
845
|
+
reserve(8 + spans.length * 14);
|
|
832
846
|
f[fi++] = spans.length;
|
|
833
|
-
for (const [text, flags, c, bg, ul, radius] of spans) {
|
|
847
|
+
for (const [text, flags, c, bg, ul, radius, family, font, size] of spans) {
|
|
834
848
|
strRef(text);
|
|
835
849
|
f[fi++] = flags;
|
|
836
850
|
f[fi++] = c;
|
|
837
851
|
f[fi++] = bg;
|
|
838
852
|
f[fi++] = ul;
|
|
839
853
|
f[fi++] = radius;
|
|
854
|
+
strRef(family);
|
|
855
|
+
strRef(font);
|
|
856
|
+
f[fi++] = size;
|
|
840
857
|
}
|
|
841
858
|
} else {
|
|
842
859
|
f[fi++] = OP.text;
|