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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -21,6 +21,421 @@ 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.42 (2026-10-07)
25
+
26
+ **What breaks.**
27
+
28
+ - A press after a dead key it does not combine with carries what the
29
+ platform typed in its `text`: German's `^ space` is a Space whose
30
+ `text` is `^`, and `^ z` a `z` whose `text` is `^z`, where they said `" "`
31
+ and `z` — in a key sink's payload and in what an editor inserts (under
32
+ Fixed, RG127). A keymap that binds Space by its `text` rather than its
33
+ `code` sees the accent there.
34
+ - Node: a whole number past 2^53 in a message comes back a `number`,
35
+ where it came back a `BigInt` (RG138).
36
+ - C: `kui_take_menu_action` with a NULL or too-small `out` leaves the
37
+ action queued, where it dropped it; `kui_take_warnings` keeps what does
38
+ not fit `cap` for the next call, where it dropped it; `kui_draw_data`'s
39
+ `fragments` and `textures` are NULL on a frame that draws none, where
40
+ they were a non-NULL pointer to nothing (RG133, RG134, RG131).
41
+ - A text style whose size or line height is under a pixel — 0, negative,
42
+ NaN — shapes at a pixel, where Node and Lua aborted or hung and Rust
43
+ panicked (RG136); a cell grid's rows are a pixel apart at an infinite
44
+ one, where they went to the layout's limit (RG144).
45
+ - Rust: `schema::Door`, `schema::CustomProp` and `schema::ElementDef` gain
46
+ `odin` (under Added, the Odin binding), so a struct literal of one needs
47
+ the field.
48
+
49
+ C stays at ABI 25 (kui.h gains no function and no field; four doors now
50
+ keep what it always promised) and the Node wire at v21 (no frame version).
51
+
52
+ ### Added
53
+
54
+ - **An Odin binding, experimental** (`packages/odin`). `kui/c` mirrors
55
+ kui.h declaration by declaration, pinned to the C compiler's layout by
56
+ 848 generated `#assert`s. `kui` is the typed layer, generated from
57
+ kui.h and the prop schema:
58
+ - `Spec` and `Text_Style`, one field per schema row;
59
+ - an enum or bit set per family of constants;
60
+ - a door per C function: out-params as results, arrays as slices,
61
+ messages as plain Odin values with `#[derive(Message)]`'s `kind`
62
+ rule.
63
+
64
+ The elements are written by hand: containers close at the end of their
65
+ `if` through `@(deferred_in)`. A C function the hand-written files do
66
+ not call gets a generated door, so the binding covers kui.h whole: 219
67
+ generated, 51 by hand, 2 skipped with their reason.
68
+ `examples/odin/tools/surface.odin` is `surface.c` through it, and calls
69
+ every door, and `examples/odin/tools/conformance.odin` rebuilds the
70
+ scene corpus through `Spec` and the doors, matching the reference report
71
+ byte for byte, as the Rust, Lua, C and Node adapters do.
72
+ `polyline` and `polygon` take points (`[][2]f32`), the count kui.h
73
+ means; a dash or a pivot is a fixed array in a `Maybe` (`[5]f32`,
74
+ `[2]f32`), so a short one cannot be written; and pixels are checked
75
+ against the size beside them.
76
+
77
+ It covers extensions both ways. An Odin host loads a plugin with
78
+ `ctx_add_extension` and `slot`. An Odin plugin is a shared library whose
79
+ seven `kui_ext_*` exports are one line each over `extension.odin`.
80
+ Built with `KUI_PLUGIN` on macOS and Linux, it links no kui and loads
81
+ into any host (on Windows it imports `kui_ffi.dll`, as a C plugin does):
82
+ `nu scripts/odin.nu slots` drives the Odin panel in the C and Rust
83
+ hosts and the C panel in the Odin host.
84
+
85
+ Run it with `nu scripts/odin.nu gen | test | slots | run counter`. It is
86
+ not published. CI's `check` installs a pinned Odin release (`ODIN_VERSION`)
87
+ and runs `gen --check`, `check`, `test` and `slots`, so a header or schema
88
+ change that was not regenerated goes red there. All four also run green
89
+ by hand on Windows x64 and Linux x64. On Windows the binding links
90
+ `kui_ffi.dll.lib`, odin.nu puts `kui_ffi.dll` beside the programs, and
91
+ `gen --check` reads a CRLF checkout as current.
92
+ - `scripts/pack-ffi.nu`: libkui_ffi for every platform kui ships, dynamic
93
+ and static, with `kui.h`, rustc's `native-static-libs` as `link.txt`, a
94
+ tarball each and `SHA256SUMS`. It is for a C or Odin host that links a
95
+ library instead of building the workspace. The machine's own platform
96
+ is built natively. Linux x64 and arm64 (glibc 2.28) and Windows x64 are
97
+ built in a pinned Docker image, with cargo-zigbuild at the release
98
+ workflow's pins and with cargo-xwin. Windows also asks for
99
+ `--accept-msvc-license`, for the CRT and SDK xwin downloads. macOS is
100
+ built on a Mac only. A Windows pack carries windows-targets' import
101
+ libraries (`windows.0.53.0.lib`, `windows.0.52.0.lib`), which its
102
+ `link.txt` names and no SDK has, so a static link finds them. Every
103
+ leg has been run, and a C host linked against each x64 pack alone,
104
+ dynamically and statically, and run; the arm64 pack was read, with no
105
+ arm64 machine to run it on. `kui.h` ships with LF line ends from any
106
+ checkout.
107
+ - `examples/rust/tools/schema-dump.rs`: the prop schema (props,
108
+ composites, elements, events, the verb table, the name lists) as JSON,
109
+ for a binding generated outside Rust.
110
+ - The verb table (`schema::DOORS`) and `docs/props.md` have an Odin
111
+ column beside C's, each spelling held by the Odin generator both ways;
112
+ Node's `protocol()` carries it (`Door.odin`).
113
+
114
+ ### Fixed
115
+
116
+ - **A dead key types its accent before a key it does not combine with**
117
+ (backlog RG127, from the Windows and Linux round after alpha.41). With
118
+ German, `^ space` typed a space where it types `^`, `´ space` a space
119
+ where it types `´`, and `^ z` a `z` where it types `^z` — on Windows
120
+ always, and under X11 with `imeOff` on; on US-International, where `'`
121
+ and `"` are dead, a quote could not be typed at all. winit composed it
122
+ right: the runner read a press's text off the logical key, which is
123
+ the key the accent fell back to, and Space's insert was a space
124
+ whatever the press carried. A press's text is now what the platform
125
+ composed, the layout's character where it composed nothing, and Space
126
+ inserts its own. `^ e` was right and is unchanged; with the input
127
+ method on, X11 composes through XIM as before.
128
+ - The docs of F103 (alpha.41) said an animating Windows window behind
129
+ other windows skips its frames. It does not: DWM composes a covered
130
+ window, and it draws at the display's rate, 241 frames a second here,
131
+ as an uncovered one does; minimized, it asks for none (RG45). F103's
132
+ wait holds there only for an acquire that times out. `retry.rs` says so
133
+ now (backlog RG128).
134
+ - **Text is read aloud on Windows and Linux** (backlog RG129, from the
135
+ regression and smoke round after RG127). Every static text — a line of
136
+ text, a list row's content, a drawn title — reached UI Automation with
137
+ an empty Name and AT-SPI with an empty name: AccessKit names a label by
138
+ its value, and kui set only its label. macOS read it all along.
139
+ - **A disclosure or a select opens from Narrator** (RG130). UI
140
+ Automation gives a node with `expanded` its ExpandCollapse pattern in
141
+ place of Invoke, and its Expand and Collapse reached nothing. They are
142
+ the node's click.
143
+ - **A text size or line height of 0 no longer ends the process** (RG136).
144
+ `lineHeight: 0`, `size: 0`, or a size under 0.4 (whose line height
145
+ rounds to 0) aborted a Node or Lua process — cosmic-text asserts a line
146
+ height is not 0 — and a negative one spun its layout; on an `edit`, a
147
+ `cells` and in `measureText` too. Every buffer is shaped at a pixel at
148
+ least. C's door had always read them as unset.
149
+ - C: what kui.h says a door hands out lives as long as it says (RG131–
150
+ RG134). A second `kui_draw_data` in a frame freed the arrays the first
151
+ handed out; `kui_access_runs` freed the strings `kui_access_tree`
152
+ handed out; reading a menu row or asking for a copy freed a taken menu
153
+ action's text; and `kui_take_warnings` dropped what did not fit `cap`,
154
+ where kui.h says the rest wait.
155
+ - A custom editor whose `caret` or `selection_anchor` falls inside a
156
+ character has an access tree (RG135): the run was sliced there and
157
+ panicked, which under C emptied the tree every frame; the position is
158
+ the character's start.
159
+ - Node: `measureText` inside a `<devtoolsTab>` function child no longer
160
+ breaks the frame being encoded (RG137); a message, a `<select>` option
161
+ or a `menuBar` row holding a string cut through an emoji draws its lone
162
+ surrogate as U+FFFD rather than failing the frame, and `dir: null` is
163
+ absent like every other null prop (RG138, RG140).
164
+
165
+ ### Native verification
166
+
167
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-07, over
168
+ the rounds after the alpha.41 tag — the Odin binding and `pack-ffi.nu`,
169
+ RG127 and RG128 from the Windows and Linux round after alpha.41, and the
170
+ regression and smoke round after RG127 (RG129–RG138) — with alpha.42's
171
+ pre-tag pass over them on the Mac: the mechanical round, then four
172
+ read-only reviews of the diff since alpha.41 (the Odin binding; the C
173
+ doors and `pack-ffi.nu`; the runner's keys, the access bridge, the text
174
+ floor and Node; the docs), each claim probed. They filed RG140–RG144,
175
+ built before the tag (a `<select>` option or menu bar row cut through an
176
+ emoji failing the Node frame, `kui_draw_data` while a frame built losing
177
+ the finished frame's fragment draws, the Odin drains freeing the strings
178
+ they returned, Odin's popups, unchecked slices and acronym kinds, a cell
179
+ grid's infinite line height), and RG145, open.
180
+
181
+ **macOS**, the pre-tag pass. fmt and clippy are clean; `cargo test
182
+ --workspace --features kui-core/conformance`: **1856 tests over 139
183
+ suites**. The C round passes, and so do the **57 scenes**, the Odin
184
+ binding's `gen --check`, `check`, `test` and `slots`, Node's tests under
185
+ `KUI_CONFORMANCE_REQUIRED=1` (**215 of 215**), `npm run gen` with no
186
+ diff, the headless round and the book. The windowed round with Node's:
187
+ **124 windows**. The AX audit: **106/106**. RG127's dead keys, which the
188
+ Windows and Linux round could not reach on a Mac: twelve sequences typed
189
+ into the `edit` example through `CGEventPostToPid` and read back through
190
+ AX, each as AppKit composes it (`⌥e e` is `é`, `⌥e space` `´`, `⌥e z`
191
+ `´z`). The bench guard against the alpha.41 tag: **green**, the guarded
192
+ rows −1.2 to −0.3%, RG126's warm cell grid −0.4%.
193
+
194
+ **Windows 11 x64 (MSVC) and Linux x64 (WSL Ubuntu 24.04), rustc
195
+ 1.99.0**, on the tree with the pre-tag fixes and `pack-ffi.nu`'s. fmt
196
+ and `cargo clippy --workspace --all-targets --features
197
+ kui-core/conformance -- -D warnings` are clean on both. `cargo test
198
+ --workspace --features kui-core/conformance`: **1857 tests over 140
199
+ suites** on Windows (5 ignored), **1853 over 139** on Linux (4
200
+ ignored), **0 failed**. `cargo audit --deny warnings` is clean over
201
+ `Cargo.lock`'s 444 crates. `pack-ffi.nu` ran every leg but macOS's: native
202
+ on both, and linux-x64, linux-arm64 and win32-x64 (cargo-xwin) in its
203
+ container. A C host linked against each x64 pack alone, dynamically and
204
+ statically, ran; the arm64 pack was read, not run. It found three things,
205
+ fixed before the tag (RG145): a CRLF `kui.h` from a CRLF checkout, the
206
+ closing table lost off a terminal, and a `docker` that could not be
207
+ spawned stopping the script. Earlier the same day, before the pre-tag
208
+ fixes, the Odin binding's four steps passed on both, and the regression
209
+ and smoke round after RG127 ran the windowed smoke and walked the
210
+ accessibility tree through UI Automation and AT-SPI.
211
+
212
+ ## 0.1.0-alpha.41 (2026-10-07)
213
+
214
+ **What breaks.**
215
+
216
+ - A leaf that declares `tooltip` — `line`, `polygon`, `path`, `cells`,
217
+ `image`, `edit`, in any binding, or a Rust leaf whose spec says
218
+ `NodeSpec::tooltip` — draws its hint while hovered, where it drew
219
+ nothing; so does a C `kui_fragment*` node with `KuiSpec.tooltip`.
220
+ - A `dash` gap the round-capped marks overlap closes: `dash = {2, 2}` at
221
+ a width of 8 draws a solid line where it drew 8 px dots every 4 px.
222
+ - With a fallback list set, `Mono` text asks the app's fallback faces
223
+ before the machine's other monospaced ones, so a character may draw in
224
+ another face, and a single-weight monospaced face draws its bold
225
+ synthesized instead of in the platform's proportional face.
226
+ - A cell grid whose family has no `M` draws its own glyphs where its face
227
+ puts them, no longer centred as a fallback's.
228
+ - On a line past 4 KB, a tab after more than ~512 bytes with none moves
229
+ onto the line's tab stops, up to a tab's width, and the rest of the
230
+ line with it.
231
+ - Rust: `schema::PropsOut` gains `ime_off` (under Added, F125), so a
232
+ struct literal of it needs the field or `..Default::default()`; so do
233
+ `conformance::Expect` and `conformance::Output`, behind the
234
+ `conformance` feature.
235
+ - npm: `@qxuken/kui` bundles no darwin-x64 prebuild. On an Intel Mac (or
236
+ an x64 Node under Rosetta),
237
+ build the addon (`cargo build -p kui-node --release`) and point
238
+ `KUI_NODE_LIB` at it; the Rust crates are unchanged there.
239
+
240
+ C stays at ABI 25 (`kui_set_ime_off` / `kui_ime_off_get` are new
241
+ functions) and the Node wire at v21 (a root prop, no frame version).
242
+
243
+ A leaf's hint is drawn now, and the four paint changes after it are each
244
+ what the code always meant to draw: a hint the docs told you to put on a
245
+ box around the leaf, a dotted line that was a lumpy solid one at a quad a
246
+ dot, a fallback list that `Mono` text never asked, a grid's own glyphs
247
+ treated as a stranger's, and a tab measured from where its chunk began
248
+ instead of where the line did. Each moves pixels an app may have written
249
+ to, so each is listed.
250
+
251
+ ### Added
252
+
253
+ - **A window can take the keyboard as keys, with the input method off**
254
+ (backlog F125, from kawoosh's wish list). `ui.ime_off(true)` / a root
255
+ `imeOff` / `ime_off = true` / `kui_set_ime_off(ctx, true)` turns the
256
+ platform's input method off in the window: no composition and no
257
+ candidate window, and on a Mac no dead key waiting for the next and no
258
+ press-and-hold — which is an input method too, so a held `j` repeats
259
+ and a held `e` opens no accent picker, whatever the user's
260
+ `ApplePressAndHoldEnabled` says. A `key` event's `text` is still the
261
+ layout's character. What a modal editor's normal mode wants; its
262
+ insert mode stops declaring it and gets accents, dead keys and the IME
263
+ back. Frame state in `optionAsAlt`'s shape, default off; the runner
264
+ applies it on change through winit's `set_ime_allowed`, so winit's
265
+ `keyDown:` stops calling `interpretKeyEvents:`, where macOS composes. A
266
+ key pressed while it was off keeps it off until it comes up, so the
267
+ `i` that switches to insert mode, held, keeps typing `iiii` rather
268
+ than opening the picker over its own repeats; the next fresh press in
269
+ insert mode gets the picker. A composition open when it turns off is
270
+ dropped, and ends in the view as an empty `preedit` — as one does now
271
+ when the user switches input source mid-composition, which used to
272
+ leave the preedit drawn. On Windows and Linux the window's IME is
273
+ disabled the same way, and only that: a dead key there is the
274
+ layout's, which winit composes whatever the IME says, and it still
275
+ composes. `Ctx.imeOff()` / `kui_ime_off_get` read the ask
276
+ back; `modal_editor` declares it in normal mode. *What you can
277
+ delete:* the README line telling a Mac user of a modal editor to run
278
+ `defaults write -g ApplePressAndHoldEnabled -bool false`, and any
279
+ per-mode IME switching an app did by hand.
280
+
281
+ ### Changed
282
+
283
+ - **Intel Macs lose their Node prebuild.** The package ships the addon
284
+ for linux-x64, linux-arm64, darwin-arm64 and win32-x64; the release
285
+ workflow's darwin-x64 leg and `release-local.nu`'s are gone. On an
286
+ Intel Mac `require('@qxuken/kui')` fails with the loader's not-found
287
+ error, which now says the prebuild was dropped and how to build one.
288
+ `kui-native`, `kui-ffi` and the rest still build and run there; only
289
+ the bundled `.node` is gone.
290
+
291
+ ### Fixed
292
+
293
+ - **A leaf draws its tooltip** (backlog RG113). The hint floats as its
294
+ node's last child, and a leaf has none, so a hovered wedge, path, line,
295
+ grid, image or editor with a `tooltip` showed nothing — it was tracked
296
+ and spoken, not drawn. It now floats beside the leaf, anchored to it
297
+ (`FloatAnchor::Node`), and lands below the leaf's box as a box's hint
298
+ lands below the box: outside every clip, outside layout, flipped above
299
+ near the window's bottom, and only while the pointer is on the stroke
300
+ or inside the outline. In all four bindings — `NodeSpec::tooltip`, the
301
+ JSX and Lua prop, `KuiSpec.tooltip` on C's leaf doors. A float anchored
302
+ to a node now honours `fit` as a parent-anchored one does. On a leaf
303
+ the drawn string is its description, so a leaf that declares a
304
+ `description` after its `tooltip` draws that.
305
+ - **An animating window whose surface skips its frames waits between
306
+ tries** (backlog F103). A skipped frame returns at once, with no
307
+ drawable to wait for and no vsync behind it, so where the platform
308
+ never says a window is covered — Windows behind other windows, Wayland,
309
+ an acquire that times out — an animation asked for its next frame on
310
+ the loop's very next turn: 82,000 views a second at a core, measured on
311
+ a Mac with every frame forced to skip and no display link. It waits a
312
+ retry (16 ms) after a skip now — 60 a second at 2% — and runs at the
313
+ display's rate again from the first frame that lands. A window macOS
314
+ calls covered still asks for nothing at all. On Windows the timer that
315
+ keeps an animation going through a title-bar drag paces the same tries
316
+ at its own interval, 10 to 16 ms.
317
+ - **A tab on a long line stops where the line's stops are** (backlog
318
+ RG76). On a line past 4 KB, a tab that followed more than half a chunk
319
+ without one measured from its chunk's start. Such a chunk now ends
320
+ after that tab, and the line gives the tab the advance that reaches its
321
+ next stop — exact once the text since the previous tab has been on
322
+ screen, and as near as the rest of the line's estimate before that.
323
+ Wrapped rows carry the same advance.
324
+ - **`measure_text` leaves a drawn text's rows alone** (backlog RG76).
325
+ Measuring, between frames, a text a node drew at another width
326
+ re-wrapped the run they share, so that node's `caret_rect` and
327
+ `text_hit` answered from the measured rows until the next frame wrapped
328
+ it back. The measure lays out a copy at its own width, kept for the
329
+ next measure there.
330
+ - **`Mono` text asks the app's fallback fonts straight after its own
331
+ face** (backlog RG118). cosmic-text's generic monospace family walks
332
+ every installed monospaced face that has the character before the
333
+ lists `set_fallback_fonts` joins — on a Mac, Hebrew in `Mono` came out
334
+ in Courier New *Italic* with Arial named. While a list is set, `Mono`
335
+ is shaped as the face it is pinned to, by name, so the order is that
336
+ face, the app's names, then the platform's. With no list nothing
337
+ changes.
338
+ - **A grid tells its family's glyphs by the family's name** (backlog
339
+ RG118). It read the family's face off the face `M` shaped with, and a
340
+ symbols-only or CJK-only family has no `M`, so its own glyphs were
341
+ taken for a fallback's — centred, and asked of a monospaced face first.
342
+ - **A new fallback list or a font rescan draws every window of the
343
+ session** (backlog RG118). `set_fallback_fonts` and
344
+ `reload_system_fonts` asked for a frame of the window they were called
345
+ through alone; another window shaped again whenever something else drew
346
+ it. Each window that has drawn now owes a frame (`animating()`) while
347
+ the session's fonts are newer than the text it shaped.
348
+ - **Dash dots that overlap make one mark** (backlog RG118). A gap no
349
+ wider than the stroke is closed by the round caps either side of it,
350
+ and `{2, 2}` at a width of 8 was 8 px dots every 4 px — a solid line
351
+ drawn lumpy, at a quad a dot. Such a gap now closes and the marks
352
+ either side are one mark; a pattern with no gap left draws solid. A
353
+ pattern whose gaps show draws exactly as before.
354
+ - **A long line's placed tab answers the caret and the click where it
355
+ is drawn** (backlog RG122, from the pre-tag pass). A line ending in such
356
+ a tab has its end caret, and a selection's end, at the line's width —
357
+ it fell short of the width in a monospaced face and past it in a
358
+ proportional one, and wrapped rows used the chunk's own narrower tab —
359
+ and a click answers the tab before the middle of the drawn tab and the
360
+ byte after it from there, where both halves answered the byte after.
361
+ - **A pinned `Mono` keeps to its face at every weight, and follows the
362
+ fonts the app loads and removes** (backlog RG123, from the pre-tag
363
+ pass). A single-weight monospaced face was passed over for bold and set
364
+ in the platform's proportional face; it now draws in itself, with bold
365
+ synthesized. A pinned family whose only face the app removed left
366
+ `Mono` naming nothing, and one loaded after the list was set was never
367
+ pinned; both now move the pin, and every window shapes again.
368
+ - **A C fragment draws its tooltip** (backlog RG124, from the pre-tag
369
+ pass). The four `kui_fragment*` doors tracked and spoke
370
+ `KuiSpec.tooltip` and floated nothing, where JSX and Lua drew it.
371
+ - The docs of a `gradient` stop whose `$token` misses say what it does:
372
+ `unknown-token` is raised and the stop is left out, so a gradient left
373
+ with fewer than two stops draws nothing over its `bg` — no error, and
374
+ no fade nobody wrote. ADR 0042's amendment says why it stays (backlog
375
+ RG118).
376
+
377
+ **What you can delete.** A box wrapped around a `line`, `polygon` or
378
+ `path` only to carry its `tooltip`; a pattern stretched by hand so a
379
+ thick stroke's dots would not touch; a second `set_fallback_fonts` (or a
380
+ redraw) sent through every other window of the session.
381
+
382
+ ### Native verification
383
+
384
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-07, over
385
+ the rounds after the alpha.40 tag — `imeOff` (F125), F103's skipped
386
+ half, RG76, RG113 and RG118, and the darwin-x64 prebuild dropped — with
387
+ a regression pass over them first: the mechanical round, then three
388
+ read-only reviews in worktrees (text and fonts; core paint, layout and
389
+ bindings; the runner, the release machinery and the docs), each claim
390
+ probed with a test before anything changed. They filed five: RG122–RG125,
391
+ built before the tag (a long line's placed tab answering the caret and
392
+ the click where its run had it, a pinned `Mono` asked at weights its face
393
+ lacks and not re-pinned when the app's fonts move, a C fragment's
394
+ tooltip never drawn, and `imeOff`'s docs promising no dead keys on
395
+ Windows and Linux, where winit composes them whatever the IME says), and
396
+ RG126, open: `cells_200x50_warm`, unguarded, 3 to 5% slower, bisected to
397
+ the grid's family-by-name check and not yet explained. F103 itself was
398
+ measured in a window first: on macOS a hidden animating window already
399
+ built nothing, and with every frame forced to skip and no display link
400
+ the probe went from 82,000 frames a second at a core to 60 at 2%. This
401
+ round ran on the Mac alone; Windows and Linux did not run it for this
402
+ tag, so F125's `set_ime_allowed` there and F103 under Windows' drag
403
+ timer are by reading.
404
+
405
+ **macOS 27.0.1 on an M3 Pro MacBook Pro, rustc 1.99.0 (the toolchain
406
+ CI runs), Node 26.10.0, nu 0.116.1**, on the release commit's tree.
407
+ `cargo fmt --all --check` and `cargo clippy --workspace --all-targets
408
+ --features kui-core/conformance -- -D warnings` are clean, and so is
409
+ `cargo audit --deny warnings`. `cargo test --workspace --features
410
+ kui-core/conformance`: **1844 tests over 138 suites, 0 failed** (4
411
+ ignored). The C round, `cbuild --run`, passes its five checks; the
412
+ corpus passes its **57 scenes** in four adapters, the `path` scene now
413
+ resting on a tooltip wedge; the ABI is **25**. Node's `node --test
414
+ test.mjs` under `KUI_CONFORMANCE_REQUIRED=1`: **212 of 212**. `npm run
415
+ gen` leaves no diff, the examples typecheck and their lockfile installs,
416
+ the headless round passes all **35 drives**, the book builds and
417
+ `scripts/book-examples.nu --check` passes.
418
+
419
+ **The windowed round**, `smoke -- --node`, twice — before the fixes and
420
+ on the release tree: **51 Rust examples and the eleven Node examples,
421
+ each on both bases, 120 frames each, every one exiting 0** — 124
422
+ windows, eight at a time, in 34.2 and 52.3 s, the second beside another
423
+ session's builds — and `counter`, `host`, `c_panel` and `lua_panel` by
424
+ hand under `KUI_SMOKE_FRAMES=120`, each exiting 0 with nothing on
425
+ stderr: **128 windows over five hosts.** `target/debug/examples` was
426
+ pruned first (12,280 files), the alpha.36 trap. The AX audit:
427
+ **106/106** on both trees, the audited window raised to the front by its
428
+ pid first, and no warning on the fixture's stderr.
429
+
430
+ **The bench guard** against the alpha.40 tag: **green**, none of the 8
431
+ guarded rows more than 10% slower — every one between −1.0% and +0.6%
432
+ (the worst guarded run-to-run spread 2.8%), `frame_10k_segments` +0.3%
433
+ with RG118's dash — beside other processes using ~500% CPU, so the
434
+ medians are not the README's and its table is kept as it was. Of the
435
+ unguarded rows the frame bench's moved by at most +1.7%; the `cells`
436
+ bench's `warm` row read +6.0% (±1.4%) against the tag, +3.8% against
437
+ alpha.40 with F125 merged, and is RG126.
438
+
24
439
  ## 0.1.0-alpha.40 (2026-10-07)
25
440
 
26
441
  **What breaks.**
package/README.md CHANGED
@@ -24,9 +24,10 @@ absent. Ranges do not pin a prerelease — `^0.1.0-alpha.8` and `~0.1.0-alpha.8`
24
24
  both admit every later alpha of the same `0.1.0` — so an app that wants the
25
25
  version it tested writes that version exactly and commits its lockfile.
26
26
 
27
- The tarball bundles the native addon for linux-x64, linux-arm64, darwin-arm64,
28
- darwin-x64 and win32-x64 under `prebuilds/`; `native.cjs` picks the one matching
29
- `process.platform`-`process.arch`. On any other platform build it from the
27
+ The tarball bundles the native addon for linux-x64, linux-arm64, darwin-arm64
28
+ and win32-x64 under `prebuilds/`; `native.cjs` picks the one matching
29
+ `process.platform`-`process.arch`. On any other platform — an Intel Mac
30
+ among them, since 0.1.0-alpha.41 — build it from the
30
31
  repo (`cargo build -p kui-node --release`) and set `KUI_NODE_LIB` to the
31
32
  resulting library.
32
33
 
@@ -456,6 +456,16 @@ and with a round cap its `4 4` at a width of 4 is a solid line, which
456
456
  is the first thing anyone writes. The first mark's cap sits where the
457
457
  solid stroke's would.
458
458
 
459
+ **A gap the dots overlap closes** (2026-10-07, backlog RG118). A dot is
460
+ as wide as the stroke whatever its mark says, in the same period, so
461
+ where a mark and its gap together come to no more than the width the
462
+ dots meet: as first built, `{2, 2}` at a width of 8 was 8 px dots every
463
+ 4 px, a lumpy solid line at a quad a dot. Clamping the dot to its mark
464
+ cannot be drawn, and growing the period to keep the gap moves every
465
+ dotted line whose dots do not touch. So the gap closes and the marks
466
+ either side of it are one (`Dash::cut`); a pattern with no gap left is
467
+ solid. A gap that is seen is untouched.
468
+
459
469
  **The same pattern on a `path`.** A path's stroke is a mask
460
470
  ([ADR 0040](0040-a-path-is-a-mask-in-the-atlas.md), whose decision 3
461
471
  left `dash` to V2), so the centre lengths go to the rasterizer and the
@@ -318,9 +318,21 @@ options*, and decisions 1–4 and 6–11 stand as written.
318
318
  nobody spells, an unknown field, fewer than two stops in the list or
319
319
  a number that is not one is an error from `gradient::parse_with`,
320
320
  with the field named. What still draws nothing in silence is a
321
- gradient that *parsed* and has nothing to paint — every stop a token
322
- that missed (each raised as `unknown-token`), or one built in Rust or
323
- C with one stop or a NaN. No warning code was added.
321
+ gradient that *parsed* and has nothing to paint — fewer than two
322
+ stops left once those whose token missed are taken out (each raised
323
+ as `unknown-token`), or one built in Rust or C with one stop or a
324
+ NaN. No warning code was added.
325
+
326
+ *Weighed again (2026-10-07, backlog RG118).* The count is of the
327
+ list, so `[$peach, $peech]` parses and draws nothing. A missed stop
328
+ could instead keep its place painted transparent, so the gradient
329
+ still draws; it stays left out. A missed token leaves any other slot
330
+ as if it were not declared (a `bg`, a keyframe's stop, an
331
+ entrance), and a stand-in here would paint a colour nobody wrote — a
332
+ fade to nothing that reads as intended, where a box with no gradient
333
+ over its `bg` reads as the mistake it is beside the `unknown-token`
334
+ that names it. Tokens have no fallback of their own to paint instead.
335
+ The docs of the row say so.
324
336
  - **The atlas holds straight alpha** (open question 1). An `Image`
325
337
  quad's shader multiplies the texel's rgb by the tint and its alpha by
326
338
  the coverage separately, so the texels are straight. The raster mixes
package/encoder.js CHANGED
@@ -395,14 +395,15 @@ export function createEncoder(P) {
395
395
  // protocol kind (no names or shapes hardcoded here); only composites (the
396
396
  // pad family, border, overflow bits, float) and the constructor-ordering
397
397
  // specials (dir, size) have hand-written stanzas, mirroring binary.rs.
398
- // `key` rides along as P_KEY; `isRoot` admits `title`, `alwaysOnTop`, `secureInput`, `optionAsAlt` and `windows` (and drops `key`);
398
+ // `key` rides along as P_KEY; `isRoot` admits `title`, `alwaysOnTop`, `secureInput`, `optionAsAlt`, `imeOff` and `windows` (and drops `key`);
399
399
  // `admit`, when given, is the only names written (a closed composite's
400
400
  // rows — the rest were already reported by checkProps).
401
401
  function props(p, key, isRoot, admit) {
402
402
  const np = fi++;
403
403
  let n = 0;
404
404
  // dir and size first: the decoder constructs spec/style from them.
405
- if (p.dir !== undefined && p.dir !== 'column') {
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)`);
@@ -527,6 +528,13 @@ export function createEncoder(P) {
527
528
  n++;
528
529
  }
529
530
  break;
531
+ case 'imeOff':
532
+ // And again (backlog F125).
533
+ if (isRoot && v) {
534
+ f[fi++] = PR.imeOff.id;
535
+ n++;
536
+ }
537
+ break;
530
538
  case 'optionAsAlt': {
531
539
  // Root only; a side by its number, `OptionAsAlt::index` (backlog
532
540
  // F113). "none" is the default and writes nothing, as `false`
package/howto.md CHANGED
@@ -61,7 +61,9 @@ a nine-point curve over ~50 px spans is ~60 quads.
61
61
  marks with 4 px gaps, `dash={4}` the same length for both, and four
62
62
  lengths are a dash-dot. The lengths are what you see — every mark has the
63
63
  stroke's round caps — so a mark no longer than the stroke is wide is a
64
- dot: `width={3} dash={[3, 5]}` is a dotted line. The pattern runs along
64
+ dot: `width={3} dash={[3, 5]}` is a dotted line. A dot is as wide as the
65
+ stroke, so a gap the dots overlap closes: `width={8} dash={[2, 2]}` is a
66
+ solid line. The pattern runs along
65
67
  the whole stroke, round corners and along a `curve`. For marching ants,
66
68
  grow `dashOffset` from a tick; the marks move towards the first point.
67
69
  A dashed line costs a quad per mark, and is still hit in its gaps.
@@ -305,7 +307,9 @@ text's own family has no glyph for goes to the first of them that has
305
307
  it, and only then to the platform's list — whose first choice on macOS
306
308
  is the system's interface face, so Cyrillic in a Latin-only monospaced
307
309
  family comes out proportional. The list is the session's, for every
308
- family and every kind of text; `[]` is the platform's alone. Set it
310
+ family and every kind of text; `[]` is the platform's alone. `mono`
311
+ text asks it straight after its own face, ahead of the machine's other
312
+ monospaced faces, which it walks first when there is no list. Set it
309
313
  when the choice changes, not every frame with a new list: a new list
310
314
  shapes every text again (the same one twice costs nothing). A cell grid
311
315
  goes one step further by itself: with no fallback of yours that has the
@@ -1134,20 +1138,40 @@ macOS's press-and-hold: holding a letter offers its accents (`e` → `é è
1134
1138
  ê`) instead of repeating it, and a letter with no accents does nothing
1135
1139
  at all — on by default, and off only on a machine whose owner turned it
1136
1140
  off, which is why it works on one Mac and not the next. Every other
1137
- platform repeats. It is the **user's setting, not the app's**: the read
1138
- that decides is HIToolbox's, of the user's global preference by name,
1139
- and no per-process default reaches it — not the argument domain, not a
1140
- registered default, not the app's own domain (F69 built a door that
1141
- pinned one; RG15 found it inert and RG16 removed it — the backlog
1142
- archive has the measurements). What works is
1141
+ platform repeats. The setting is the user's — the read that decides is
1142
+ HIToolbox's, of the user's global preference by name, and no
1143
+ per-process default reaches it (F69 built a door that pinned one; RG15
1144
+ found it inert and RG16 removed it — the backlog archive has the
1145
+ measurements) — but press-and-hold is an *input method*, and a window
1146
+ can take the keyboard with its input method off. Declare `imeOff` on the
1147
+ root while your keys are commands (Rust `ui.ime_off(true)`, Lua
1148
+ `ime_off = true` on the root table, C `kui_set_ime_off(ctx, true)`):
1149
+
1150
+ ```rust
1151
+ // Normal mode is a keymap; insert mode types text.
1152
+ ui.ime_off(self.mode == Mode::Normal);
1153
+ ```
1154
+
1155
+ A held `j` then repeats, a held `e` opens nothing, and an IME left on
1156
+ no longer eats the keymap — nor, on a Mac, a dead key; each key still
1157
+ carries the layout's character as its `text`. On Windows and Linux a
1158
+ dead key is the layout's and still composes: winit composes it whatever
1159
+ the IME says. Stop declaring it and insert mode has
1160
+ accents, dead keys and the IME back. A key held across the switch — the
1161
+ `i` that enters insert mode — keeps repeating until it comes up, so the
1162
+ picker comes from the next fresh press. It is frame state: declare it on
1163
+ every frame the mode wants it.
1164
+
1165
+ The user can still turn press-and-hold off for every app:
1143
1166
 
1144
1167
  ```
1145
1168
  defaults write -g ApplePressAndHoldEnabled -bool false
1146
1169
  ```
1147
1170
 
1148
- and a relaunch — what the owner of a modal editor, where `j` held is a
1149
- motion, has usually done already. An app whose keys are commands can say
1150
- so in its README; kui has nothing to offer it beyond that.
1171
+ and a relaunch.
1172
+
1173
+ [`imeOff`](props.md#composite-props-hand-written-per-binding) ·
1174
+ [`examples/rust/apps/modal_editor.rs`](../examples/rust/apps/modal_editor.rs)
1151
1175
 
1152
1176
  ### How do I tell the keypad from the main keys, or hear a lone Shift?
1153
1177
 
@@ -1420,8 +1444,10 @@ them sharing an edge show a hairline of the background through it.
1420
1444
  `gradient` on the box: `gradient={{ to: 'bottom', stops: ['#1e2030',
1421
1445
  '#14161e'] }}` runs to a side or a corner, `{ angle: 0.125, stops }` along
1422
1446
  a direction in turns clockwise from east, and `{ radial: true, at: [0.5,
1423
- 0], stops }` out from a centre. A stop is a colour or `[colour, position]`.
1424
- It paints over `bg` and under the border and the children, so a scrim is
1447
+ 0], stops }` out from a centre. A stop is a colour or `[colour, position]`,
1448
+ the colour a `$token` too; a token that misses is raised as
1449
+ `unknown-token` and its stop left out, so a two-stop gradient with a
1450
+ typo draws nothing over its `bg`. It paints over `bg` and under the border and the children, so a scrim is
1425
1451
  a gradient with a transparent stop over whatever is beneath. The geometry
1426
1452
  is the box's unit square stretched to the box — a corner is CSS's corner,
1427
1453
  and an `angle` runs corner to corner at an eighth of a turn whatever the
package/index.d.ts CHANGED
@@ -1890,6 +1890,8 @@ export type DoorCell = { is: string } | { as: string } | { no: string };
1890
1890
  export interface Door {
1891
1891
  rust: string;
1892
1892
  c: DoorCell;
1893
+ /** The Odin binding's (packages/odin), pinned by its generator. */
1894
+ odin: DoorCell;
1893
1895
  node: DoorCell;
1894
1896
  lua: DoorCell;
1895
1897
  doc: string;
@@ -2401,6 +2403,14 @@ export declare class Ctx {
2401
2403
  * so a test can assert on it.
2402
2404
  */
2403
2405
  optionAsAlt(): 'none' | 'left' | 'right' | 'both'
2406
+ /**
2407
+ * Whether the last frame asked for the platform's input method off
2408
+ * in its window (a root `<box imeOff>`) — no composition, and on a
2409
+ * Mac no dead keys and no press-and-hold; false when it did not.
2410
+ * `runWindowed` applies it to the window on change; a bare `Ctx`
2411
+ * hands the ask back so a test can assert on it.
2412
+ */
2413
+ imeOff(): boolean
2404
2414
  /**
2405
2415
  * A headless context is one window, the main: this answers whether
2406
2416
  * `window` names it (`"main"`, `0`, or left out) and addresses
@@ -2471,8 +2481,10 @@ export declare class Ctx {
2471
2481
  * — whose first choice on macOS is the system's proportional
2472
2482
  * face. Ids from `addFont` / `addSystemFont` / `loadFontFile`;
2473
2483
  * one that names no font is left out, and `[]` is the
2474
- * platform's list alone. A new list shapes every text again;
2475
- * the same list twice is nothing.
2484
+ * platform's list alone. `mono` text asks them straight after
2485
+ * its own face, ahead of the machine's other monospaced faces.
2486
+ * A new list shapes every text again; the same list twice is
2487
+ * nothing.
2476
2488
  */
2477
2489
  setFallbackFonts(ids: Array<string>): void
2478
2490
  removeFont(id: string): void
@@ -3653,8 +3665,10 @@ export declare class KuiWindow {
3653
3665
  * — whose first choice on macOS is the system's proportional
3654
3666
  * face. Ids from `addFont` / `addSystemFont` / `loadFontFile`;
3655
3667
  * one that names no font is left out, and `[]` is the
3656
- * platform's list alone. A new list shapes every text again;
3657
- * the same list twice is nothing.
3668
+ * platform's list alone. `mono` text asks them straight after
3669
+ * its own face, ahead of the machine's other monospaced faces.
3670
+ * A new list shapes every text again; the same list twice is
3671
+ * nothing.
3658
3672
  */
3659
3673
  setFallbackFonts(ids: Array<string>): void
3660
3674
  removeFont(id: string): void