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

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,233 @@ 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.41 (2026-10-07)
25
+
26
+ **What breaks.**
27
+
28
+ - A leaf that declares `tooltip` — `line`, `polygon`, `path`, `cells`,
29
+ `image`, `edit`, in any binding, or a Rust leaf whose spec says
30
+ `NodeSpec::tooltip` — draws its hint while hovered, where it drew
31
+ nothing; so does a C `kui_fragment*` node with `KuiSpec.tooltip`.
32
+ - A `dash` gap the round-capped marks overlap closes: `dash = {2, 2}` at
33
+ a width of 8 draws a solid line where it drew 8 px dots every 4 px.
34
+ - With a fallback list set, `Mono` text asks the app's fallback faces
35
+ before the machine's other monospaced ones, so a character may draw in
36
+ another face, and a single-weight monospaced face draws its bold
37
+ synthesized instead of in the platform's proportional face.
38
+ - A cell grid whose family has no `M` draws its own glyphs where its face
39
+ puts them, no longer centred as a fallback's.
40
+ - On a line past 4 KB, a tab after more than ~512 bytes with none moves
41
+ onto the line's tab stops, up to a tab's width, and the rest of the
42
+ line with it.
43
+ - Rust: `schema::PropsOut` gains `ime_off` (under Added, F125), so a
44
+ struct literal of it needs the field or `..Default::default()`; so do
45
+ `conformance::Expect` and `conformance::Output`, behind the
46
+ `conformance` feature.
47
+ - npm: `@qxuken/kui` bundles no darwin-x64 prebuild. On an Intel Mac (or
48
+ an x64 Node under Rosetta),
49
+ build the addon (`cargo build -p kui-node --release`) and point
50
+ `KUI_NODE_LIB` at it; the Rust crates are unchanged there.
51
+
52
+ C stays at ABI 25 (`kui_set_ime_off` / `kui_ime_off_get` are new
53
+ functions) and the Node wire at v21 (a root prop, no frame version).
54
+
55
+ A leaf's hint is drawn now, and the four paint changes after it are each
56
+ what the code always meant to draw: a hint the docs told you to put on a
57
+ box around the leaf, a dotted line that was a lumpy solid one at a quad a
58
+ dot, a fallback list that `Mono` text never asked, a grid's own glyphs
59
+ treated as a stranger's, and a tab measured from where its chunk began
60
+ instead of where the line did. Each moves pixels an app may have written
61
+ to, so each is listed.
62
+
63
+ ### Added
64
+
65
+ - **A window can take the keyboard as keys, with the input method off**
66
+ (backlog F125, from kawoosh's wish list). `ui.ime_off(true)` / a root
67
+ `imeOff` / `ime_off = true` / `kui_set_ime_off(ctx, true)` turns the
68
+ platform's input method off in the window: no composition and no
69
+ candidate window, and on a Mac no dead key waiting for the next and no
70
+ press-and-hold — which is an input method too, so a held `j` repeats
71
+ and a held `e` opens no accent picker, whatever the user's
72
+ `ApplePressAndHoldEnabled` says. A `key` event's `text` is still the
73
+ layout's character. What a modal editor's normal mode wants; its
74
+ insert mode stops declaring it and gets accents, dead keys and the IME
75
+ back. Frame state in `optionAsAlt`'s shape, default off; the runner
76
+ applies it on change through winit's `set_ime_allowed`, so winit's
77
+ `keyDown:` stops calling `interpretKeyEvents:`, where macOS composes. A
78
+ key pressed while it was off keeps it off until it comes up, so the
79
+ `i` that switches to insert mode, held, keeps typing `iiii` rather
80
+ than opening the picker over its own repeats; the next fresh press in
81
+ insert mode gets the picker. A composition open when it turns off is
82
+ dropped, and ends in the view as an empty `preedit` — as one does now
83
+ when the user switches input source mid-composition, which used to
84
+ leave the preedit drawn. On Windows and Linux the window's IME is
85
+ disabled the same way, and only that: a dead key there is the
86
+ layout's, which winit composes whatever the IME says, and it still
87
+ composes. `Ctx.imeOff()` / `kui_ime_off_get` read the ask
88
+ back; `modal_editor` declares it in normal mode. *What you can
89
+ delete:* the README line telling a Mac user of a modal editor to run
90
+ `defaults write -g ApplePressAndHoldEnabled -bool false`, and any
91
+ per-mode IME switching an app did by hand.
92
+
93
+ ### Changed
94
+
95
+ - **Intel Macs lose their Node prebuild.** The package ships the addon
96
+ for linux-x64, linux-arm64, darwin-arm64 and win32-x64; the release
97
+ workflow's darwin-x64 leg and `release-local.nu`'s are gone. On an
98
+ Intel Mac `require('@qxuken/kui')` fails with the loader's not-found
99
+ error, which now says the prebuild was dropped and how to build one.
100
+ `kui-native`, `kui-ffi` and the rest still build and run there; only
101
+ the bundled `.node` is gone.
102
+
103
+ ### Fixed
104
+
105
+ - **A leaf draws its tooltip** (backlog RG113). The hint floats as its
106
+ node's last child, and a leaf has none, so a hovered wedge, path, line,
107
+ grid, image or editor with a `tooltip` showed nothing — it was tracked
108
+ and spoken, not drawn. It now floats beside the leaf, anchored to it
109
+ (`FloatAnchor::Node`), and lands below the leaf's box as a box's hint
110
+ lands below the box: outside every clip, outside layout, flipped above
111
+ near the window's bottom, and only while the pointer is on the stroke
112
+ or inside the outline. In all four bindings — `NodeSpec::tooltip`, the
113
+ JSX and Lua prop, `KuiSpec.tooltip` on C's leaf doors. A float anchored
114
+ to a node now honours `fit` as a parent-anchored one does. On a leaf
115
+ the drawn string is its description, so a leaf that declares a
116
+ `description` after its `tooltip` draws that.
117
+ - **An animating window whose surface skips its frames waits between
118
+ tries** (backlog F103). A skipped frame returns at once, with no
119
+ drawable to wait for and no vsync behind it, so where the platform
120
+ never says a window is covered — Windows behind other windows, Wayland,
121
+ an acquire that times out — an animation asked for its next frame on
122
+ the loop's very next turn: 82,000 views a second at a core, measured on
123
+ a Mac with every frame forced to skip and no display link. It waits a
124
+ retry (16 ms) after a skip now — 60 a second at 2% — and runs at the
125
+ display's rate again from the first frame that lands. A window macOS
126
+ calls covered still asks for nothing at all. On Windows the timer that
127
+ keeps an animation going through a title-bar drag paces the same tries
128
+ at its own interval, 10 to 16 ms.
129
+ - **A tab on a long line stops where the line's stops are** (backlog
130
+ RG76). On a line past 4 KB, a tab that followed more than half a chunk
131
+ without one measured from its chunk's start. Such a chunk now ends
132
+ after that tab, and the line gives the tab the advance that reaches its
133
+ next stop — exact once the text since the previous tab has been on
134
+ screen, and as near as the rest of the line's estimate before that.
135
+ Wrapped rows carry the same advance.
136
+ - **`measure_text` leaves a drawn text's rows alone** (backlog RG76).
137
+ Measuring, between frames, a text a node drew at another width
138
+ re-wrapped the run they share, so that node's `caret_rect` and
139
+ `text_hit` answered from the measured rows until the next frame wrapped
140
+ it back. The measure lays out a copy at its own width, kept for the
141
+ next measure there.
142
+ - **`Mono` text asks the app's fallback fonts straight after its own
143
+ face** (backlog RG118). cosmic-text's generic monospace family walks
144
+ every installed monospaced face that has the character before the
145
+ lists `set_fallback_fonts` joins — on a Mac, Hebrew in `Mono` came out
146
+ in Courier New *Italic* with Arial named. While a list is set, `Mono`
147
+ is shaped as the face it is pinned to, by name, so the order is that
148
+ face, the app's names, then the platform's. With no list nothing
149
+ changes.
150
+ - **A grid tells its family's glyphs by the family's name** (backlog
151
+ RG118). It read the family's face off the face `M` shaped with, and a
152
+ symbols-only or CJK-only family has no `M`, so its own glyphs were
153
+ taken for a fallback's — centred, and asked of a monospaced face first.
154
+ - **A new fallback list or a font rescan draws every window of the
155
+ session** (backlog RG118). `set_fallback_fonts` and
156
+ `reload_system_fonts` asked for a frame of the window they were called
157
+ through alone; another window shaped again whenever something else drew
158
+ it. Each window that has drawn now owes a frame (`animating()`) while
159
+ the session's fonts are newer than the text it shaped.
160
+ - **Dash dots that overlap make one mark** (backlog RG118). A gap no
161
+ wider than the stroke is closed by the round caps either side of it,
162
+ and `{2, 2}` at a width of 8 was 8 px dots every 4 px — a solid line
163
+ drawn lumpy, at a quad a dot. Such a gap now closes and the marks
164
+ either side are one mark; a pattern with no gap left draws solid. A
165
+ pattern whose gaps show draws exactly as before.
166
+ - **A long line's placed tab answers the caret and the click where it
167
+ is drawn** (backlog RG122, from the pre-tag pass). A line ending in such
168
+ a tab has its end caret, and a selection's end, at the line's width —
169
+ it fell short of the width in a monospaced face and past it in a
170
+ proportional one, and wrapped rows used the chunk's own narrower tab —
171
+ and a click answers the tab before the middle of the drawn tab and the
172
+ byte after it from there, where both halves answered the byte after.
173
+ - **A pinned `Mono` keeps to its face at every weight, and follows the
174
+ fonts the app loads and removes** (backlog RG123, from the pre-tag
175
+ pass). A single-weight monospaced face was passed over for bold and set
176
+ in the platform's proportional face; it now draws in itself, with bold
177
+ synthesized. A pinned family whose only face the app removed left
178
+ `Mono` naming nothing, and one loaded after the list was set was never
179
+ pinned; both now move the pin, and every window shapes again.
180
+ - **A C fragment draws its tooltip** (backlog RG124, from the pre-tag
181
+ pass). The four `kui_fragment*` doors tracked and spoke
182
+ `KuiSpec.tooltip` and floated nothing, where JSX and Lua drew it.
183
+ - The docs of a `gradient` stop whose `$token` misses say what it does:
184
+ `unknown-token` is raised and the stop is left out, so a gradient left
185
+ with fewer than two stops draws nothing over its `bg` — no error, and
186
+ no fade nobody wrote. ADR 0042's amendment says why it stays (backlog
187
+ RG118).
188
+
189
+ **What you can delete.** A box wrapped around a `line`, `polygon` or
190
+ `path` only to carry its `tooltip`; a pattern stretched by hand so a
191
+ thick stroke's dots would not touch; a second `set_fallback_fonts` (or a
192
+ redraw) sent through every other window of the session.
193
+
194
+ ### Native verification
195
+
196
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-07, over
197
+ the rounds after the alpha.40 tag — `imeOff` (F125), F103's skipped
198
+ half, RG76, RG113 and RG118, and the darwin-x64 prebuild dropped — with
199
+ a regression pass over them first: the mechanical round, then three
200
+ read-only reviews in worktrees (text and fonts; core paint, layout and
201
+ bindings; the runner, the release machinery and the docs), each claim
202
+ probed with a test before anything changed. They filed five: RG122–RG125,
203
+ built before the tag (a long line's placed tab answering the caret and
204
+ the click where its run had it, a pinned `Mono` asked at weights its face
205
+ lacks and not re-pinned when the app's fonts move, a C fragment's
206
+ tooltip never drawn, and `imeOff`'s docs promising no dead keys on
207
+ Windows and Linux, where winit composes them whatever the IME says), and
208
+ RG126, open: `cells_200x50_warm`, unguarded, 3 to 5% slower, bisected to
209
+ the grid's family-by-name check and not yet explained. F103 itself was
210
+ measured in a window first: on macOS a hidden animating window already
211
+ built nothing, and with every frame forced to skip and no display link
212
+ the probe went from 82,000 frames a second at a core to 60 at 2%. This
213
+ round ran on the Mac alone; Windows and Linux did not run it for this
214
+ tag, so F125's `set_ime_allowed` there and F103 under Windows' drag
215
+ timer are by reading.
216
+
217
+ **macOS 27.0.1 on an M3 Pro MacBook Pro, rustc 1.99.0 (the toolchain
218
+ CI runs), Node 26.10.0, nu 0.116.1**, on the release commit's tree.
219
+ `cargo fmt --all --check` and `cargo clippy --workspace --all-targets
220
+ --features kui-core/conformance -- -D warnings` are clean, and so is
221
+ `cargo audit --deny warnings`. `cargo test --workspace --features
222
+ kui-core/conformance`: **1844 tests over 138 suites, 0 failed** (4
223
+ ignored). The C round, `cbuild --run`, passes its five checks; the
224
+ corpus passes its **57 scenes** in four adapters, the `path` scene now
225
+ resting on a tooltip wedge; the ABI is **25**. Node's `node --test
226
+ test.mjs` under `KUI_CONFORMANCE_REQUIRED=1`: **212 of 212**. `npm run
227
+ gen` leaves no diff, the examples typecheck and their lockfile installs,
228
+ the headless round passes all **35 drives**, the book builds and
229
+ `scripts/book-examples.nu --check` passes.
230
+
231
+ **The windowed round**, `smoke -- --node`, twice — before the fixes and
232
+ on the release tree: **51 Rust examples and the eleven Node examples,
233
+ each on both bases, 120 frames each, every one exiting 0** — 124
234
+ windows, eight at a time, in 34.2 and 52.3 s, the second beside another
235
+ session's builds — and `counter`, `host`, `c_panel` and `lua_panel` by
236
+ hand under `KUI_SMOKE_FRAMES=120`, each exiting 0 with nothing on
237
+ stderr: **128 windows over five hosts.** `target/debug/examples` was
238
+ pruned first (12,280 files), the alpha.36 trap. The AX audit:
239
+ **106/106** on both trees, the audited window raised to the front by its
240
+ pid first, and no warning on the fixture's stderr.
241
+
242
+ **The bench guard** against the alpha.40 tag: **green**, none of the 8
243
+ guarded rows more than 10% slower — every one between −1.0% and +0.6%
244
+ (the worst guarded run-to-run spread 2.8%), `frame_10k_segments` +0.3%
245
+ with RG118's dash — beside other processes using ~500% CPU, so the
246
+ medians are not the README's and its table is kept as it was. Of the
247
+ unguarded rows the frame bench's moved by at most +1.7%; the `cells`
248
+ bench's `warm` row read +6.0% (±1.4%) against the tag, +3.8% against
249
+ alpha.40 with F125 merged, and is RG126.
250
+
24
251
  ## 0.1.0-alpha.40 (2026-10-07)
25
252
 
26
253
  **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,7 +395,7 @@ 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) {
@@ -527,6 +527,13 @@ export function createEncoder(P) {
527
527
  n++;
528
528
  }
529
529
  break;
530
+ case 'imeOff':
531
+ // And again (backlog F125).
532
+ if (isRoot && v) {
533
+ f[fi++] = PR.imeOff.id;
534
+ n++;
535
+ }
536
+ break;
530
537
  case 'optionAsAlt': {
531
538
  // Root only; a side by its number, `OptionAsAlt::index` (backlog
532
539
  // 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
@@ -2401,6 +2401,14 @@ export declare class Ctx {
2401
2401
  * so a test can assert on it.
2402
2402
  */
2403
2403
  optionAsAlt(): 'none' | 'left' | 'right' | 'both'
2404
+ /**
2405
+ * Whether the last frame asked for the platform's input method off
2406
+ * in its window (a root `<box imeOff>`) — no composition, and on a
2407
+ * Mac no dead keys and no press-and-hold; false when it did not.
2408
+ * `runWindowed` applies it to the window on change; a bare `Ctx`
2409
+ * hands the ask back so a test can assert on it.
2410
+ */
2411
+ imeOff(): boolean
2404
2412
  /**
2405
2413
  * A headless context is one window, the main: this answers whether
2406
2414
  * `window` names it (`"main"`, `0`, or left out) and addresses
@@ -2471,8 +2479,10 @@ export declare class Ctx {
2471
2479
  * — whose first choice on macOS is the system's proportional
2472
2480
  * face. Ids from `addFont` / `addSystemFont` / `loadFontFile`;
2473
2481
  * 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.
2482
+ * platform's list alone. `mono` text asks them straight after
2483
+ * its own face, ahead of the machine's other monospaced faces.
2484
+ * A new list shapes every text again; the same list twice is
2485
+ * nothing.
2476
2486
  */
2477
2487
  setFallbackFonts(ids: Array<string>): void
2478
2488
  removeFont(id: string): void
@@ -3653,8 +3663,10 @@ export declare class KuiWindow {
3653
3663
  * — whose first choice on macOS is the system's proportional
3654
3664
  * face. Ids from `addFont` / `addSystemFont` / `loadFontFile`;
3655
3665
  * 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.
3666
+ * platform's list alone. `mono` text asks them straight after
3667
+ * its own face, ahead of the machine's other monospaced faces.
3668
+ * A new list shapes every text again; the same list twice is
3669
+ * nothing.
3658
3670
  */
3659
3671
  setFallbackFonts(ids: Array<string>): void
3660
3672
  removeFont(id: string): void
package/jsx-runtime.d.ts CHANGED
@@ -164,7 +164,9 @@ export interface GradientProp {
164
164
  at?: [number, number];
165
165
  /** Two or more colours, each alone or as `[colour, position]` with the
166
166
  * position 0 to 1. Stops without one are spaced evenly between those
167
- * with; two at one position are a hard edge. */
167
+ * with; two at one position are a hard edge. A `$token` that misses
168
+ * is raised as `unknown-token` and its stop left out, so a gradient
169
+ * left with fewer than two draws nothing over the `bg`. */
168
170
  stops: (ColorProp | [ColorProp, number])[];
169
171
  }
170
172
 
@@ -301,7 +303,7 @@ export interface GeneratedSpecProps {
301
303
  focusable?: boolean;
302
304
  /** Space between children along the main axis. */
303
305
  gap?: LengthProp;
304
- /** A gradient painted over the node's `bg` and under its border and its children (`docs/adr/0042-a-gradient-is-an-image-the-core-paints.md`): `{ to: 'bottom', stops: [...] }` towards a side or a corner (`right`, `bottom left`, …; the default is `bottom`), `{ angle: 0.125, stops }` in turns clockwise from east, or `{ radial: true, at: [0.5, 0], stops }` out from a centre (fractions of the box, the middle by default) to its farthest corner. A stop is a colour — a `$token` too — or `[colour, position]` with the position 0 to 1; stops without one are spaced evenly between those with. Two stops at one position are a hard edge. The gradient is defined on the box's unit square and stretched to it, so a side or a corner is CSS's and any other `angle` runs corner to corner at an eighth of a turn whatever the box's aspect, where CSS's pixel-measured `45deg` does not. Stops mix in straight sRGB with the alpha premultiplied, as CSS's do. What it costs is one image quad: the core rasterizes each distinct gradient once into the glyph atlas — a 256-texel strip along an axis, a 128-texel square otherwise, within half an 8-bit level of the gradient computed per pixel for a linear one and 1.2 for a radial — keyed by the gradient and not the box, so a box that resizes and a thousand boxes that share one rasterize nothing, and a host that draws an image draws it; a gradient box costs about 55 ns over a flat one, so ten thousand of them are half a millisecond. A hard edge is as soft as the raster stretched to the box (a 256th of its length along a strip); stripes are boxes. It does not tween — `transition` eases the `bg` under it and `opacity` fades it — and `hoverBg` and the other state backgrounds replace `bg`, not the gradient; one that changes every frame is a raster a frame, and a shimmer is a `fragment`'s. Ignored on a `line`, a `polygon` and a `path`. Fewer than two stops are an error in JSX and Lua, as a malformed `keyframes` is, and draw nothing in Rust and C. */
306
+ /** A gradient painted over the node's `bg` and under its border and its children (`docs/adr/0042-a-gradient-is-an-image-the-core-paints.md`): `{ to: 'bottom', stops: [...] }` towards a side or a corner (`right`, `bottom left`, …; the default is `bottom`), `{ angle: 0.125, stops }` in turns clockwise from east, or `{ radial: true, at: [0.5, 0], stops }` out from a centre (fractions of the box, the middle by default) to its farthest corner. A stop is a colour — a `$token` too — or `[colour, position]` with the position 0 to 1; stops without one are spaced evenly between those with. Two stops at one position are a hard edge. The gradient is defined on the box's unit square and stretched to it, so a side or a corner is CSS's and any other `angle` runs corner to corner at an eighth of a turn whatever the box's aspect, where CSS's pixel-measured `45deg` does not. Stops mix in straight sRGB with the alpha premultiplied, as CSS's do. What it costs is one image quad: the core rasterizes each distinct gradient once into the glyph atlas — a 256-texel strip along an axis, a 128-texel square otherwise, within half an 8-bit level of the gradient computed per pixel for a linear one and 1.2 for a radial — keyed by the gradient and not the box, so a box that resizes and a thousand boxes that share one rasterize nothing, and a host that draws an image draws it; a gradient box costs about 55 ns over a flat one, so ten thousand of them are half a millisecond. A hard edge is as soft as the raster stretched to the box (a 256th of its length along a strip); stripes are boxes. It does not tween — `transition` eases the `bg` under it and `opacity` fades it — and `hoverBg` and the other state backgrounds replace `bg`, not the gradient; one that changes every frame is a raster a frame, and a shimmer is a `fragment`'s. Ignored on a `line`, a `polygon` and a `path`. Fewer than two stops are an error in JSX and Lua, as a malformed `keyframes` is, and draw nothing in Rust and C. A stop whose `$token` misses is not an error: it is raised as `unknown-token` and left out, as a miss leaves any slot unset, and the rest are spaced as if it had not been declared — so a gradient left with fewer than two stops, a two-stop one with a typo, draws nothing over its `bg` (backlog RG118). */
305
307
  gradient?: GradientProp;
306
308
  /** Vertical size: px | "fit" | "grow" | "N%" | a size expression (see `width`). */
307
309
  height?: SizingProp;
@@ -500,8 +502,10 @@ export interface CustomSpecProps {
500
502
  * once, so a later Tab press is not clobbered. `ctx.focus(key)` moves
501
503
  * focus at any time. */
502
504
  keyFocus?: boolean;
503
- /** Hover hint: a tooltip floated below this box while it is hovered
504
- * (implies hoverable). */
505
+ /** Hover hint: a tooltip floated below this node while it is hovered
506
+ * (implies hoverable) — as a box's last child, and beside a leaf that
507
+ * holds no children (`line`, `polygon`, `path`, `cells`, `image`,
508
+ * `edit`), anchored to it, below its box (backlog RG113). */
505
509
  tooltip?: string;
506
510
  /** Stable identity by *data* index rather than by name: the key
507
511
  * auto-keying would have given this node as the `i`th child, given to
@@ -546,6 +550,14 @@ export interface BoxProps extends Keyed, GeneratedSpecProps, CustomSpecProps {
546
550
  * Declare it every frame; the frame that stops gives the Option keys
547
551
  * back to the layout. Nothing elsewhere. */
548
552
  optionAsAlt?: 'none' | 'left' | 'right' | 'both';
553
+ /** Root box only: this window takes the keyboard as keys, with the
554
+ * platform's input method off (backlog F125) — no composition, and on
555
+ * a Mac no dead keys and no press-and-hold, so a held letter repeats
556
+ * instead of opening the accent picker. A key's `text` is still the
557
+ * layout's character. A modal editor's normal mode: declare it every
558
+ * frame the mode wants it; the frame that stops gives the IME, dead
559
+ * keys and accents back. */
560
+ imeOff?: boolean;
549
561
  /** Root box only: which windows exist besides the main one (see
550
562
  * `WindowDecl` in `@qxuken/kui`). `runWindowed` / `createApp` write it
551
563
  * from the loop config's `windows(model)`; a view driving a `Ctx` by
@@ -846,9 +858,11 @@ export declare namespace JSX {
846
858
  * gap]` for a dash-dot, in px **as seen** — every mark is
847
859
  * round-capped, so a mark no longer than the stroke is wide is a
848
860
  * dot (SVG's `stroke-dasharray` measures the centre line; this is
849
- * its `mark − width, gap + width`). The pattern runs along the
850
- * whole stroke, corners and curves included. A pattern with no
851
- * gap, or finer than a pixel, draws solid. */
861
+ * its `mark − width, gap + width`). A gap the dots overlap — a
862
+ * mark and its gap together no longer than the width — closes,
863
+ * and the marks either side of it are one. The pattern runs along
864
+ * the whole stroke, corners and curves included. A pattern with
865
+ * no gap left, or finer than a pixel, draws solid. */
852
866
  dash?: number | [number] | [number, number] | [number, number, number, number];
853
867
  /** How far into the pattern the stroke starts, in px: growing it
854
868
  * moves the marks towards the first point — a marquee's marching
@@ -938,9 +952,11 @@ export declare namespace JSX {
938
952
  * gap]` for a dash-dot, in px **as seen** — every mark is
939
953
  * round-capped, so a mark no longer than the stroke is wide is a
940
954
  * dot (SVG's `stroke-dasharray` measures the centre line; this is
941
- * its `mark − width, gap + width`). The pattern runs along the
942
- * outline, restarting at every subpath as SVG's does. A pattern with no
943
- * gap, or finer than a pixel, draws solid. */
955
+ * its `mark − width, gap + width`). A gap the dots overlap — a
956
+ * mark and its gap together no longer than the width — closes,
957
+ * and the marks either side of it are one. The pattern runs along
958
+ * the outline, restarting at every subpath as SVG's does. A
959
+ * pattern with no gap left, or finer than a pixel, draws solid. */
944
960
  dash?: number | [number] | [number, number] | [number, number, number, number];
945
961
  /** How far into the pattern the stroke starts, in px: growing it
946
962
  * moves the marks towards the first point — a marquee's marching
package/native.cjs CHANGED
@@ -107,10 +107,17 @@ if (native) {
107
107
  'KUI_NODE_LIB at a working library.',
108
108
  );
109
109
  } else {
110
+ // An Intel Mac had a prebuild until alpha.40 (as did an x64 Node under
111
+ // Rosetta on Apple silicon, which reads as the same platform), so it is
112
+ // told that, not left to guess whether the install went wrong.
113
+ const dropped =
114
+ process.platform === 'darwin' && process.arch === 'x64'
115
+ ? ' (Intel Macs, and an x64 Node under Rosetta, had one until 0.1.0-alpha.40 and build it from source since)'
116
+ : '';
110
117
  throw new Error(
111
- `kui native library not found for ${process.platform}-${process.arch} - ` +
112
- 'this package ships prebuilds for linux-x64, linux-arm64, darwin-arm64, ' +
113
- 'darwin-x64 and win32-x64; elsewhere run `cargo build -p kui-node --release` in the kui ' +
118
+ `kui native library not found for ${process.platform}-${process.arch}${dropped} - ` +
119
+ 'this package ships prebuilds for linux-x64, linux-arm64, darwin-arm64 ' +
120
+ 'and win32-x64; elsewhere run `cargo build -p kui-node --release` in the kui ' +
114
121
  'repo or point KUI_NODE_LIB at a built library.\nLooked in:\n ' +
115
122
  looked.join('\n '),
116
123
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qxuken/kui",
3
- "version": "0.1.0-alpha.40",
3
+ "version": "0.1.0-alpha.41",
4
4
  "description": "kui for Node: JSX views lowered into the kui IR, Elm-style messages as data",
5
5
  "license": "MIT",
6
6
  "repository": {
Binary file
Binary file
Binary file
package/props.md CHANGED
@@ -40,7 +40,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
40
40
  | `focusRegion` | `focus_region` | `focus_region` | boolean | Makes this node's subtree a focus region: a Tab ring of its own that the ring outside never enters and that never leaves — a devtools dock, an inspector beside the app (`docs/adr/0022-focus-regions.md`). Entered on purpose: `focusRegion(name)` (`Ui::focus_region`, `env.focus_region`, `kui_focus_region`) moves focus in — to the focus the region last held, else its `initialFocus`, else its first stop — and `focusRegion(null)` moves it back to the main ring the same way; a press inside the region, or an explicit focus on a node in it, enters it too. Tab then walks that ring alone, wrapping inside it; with nothing focused, Tab enters the ring of the region in effect (`region()`). A region that stops being declared hands focus back to what the main ring last held. Only the ring is scoped: keys still bubble through the boundary to the sink above (a region that wants its own keymap is an `onKey` sink), the pointer and assistive technology see a plain node, and a `modal` in effect is the ring wherever it sits. Nested regions are skipped by the outer ring the way the main ring skips them. |
41
41
  | `focusable` | `focusable` | `focusable` | boolean | Reachable by Tab (and focused by a click) without a click payload or a control role — a row that opens on Enter. Editors, key sinks, `onClick` boxes and the control roles are focusable already. |
42
42
  | `gap` | `gap` | `gap` | number, or a `"$length"` token | Space between children along the main axis. |
43
- | `gradient` | `gradient` | `gradient` (`const KuiGradient *`) | gradient (`{ to? \\| angle? \\| radial?, at?, stops: [color \\| [color, at], …] }`) | A gradient painted over the node's `bg` and under its border and its children (`docs/adr/0042-a-gradient-is-an-image-the-core-paints.md`): `{ to: 'bottom', stops: [...] }` towards a side or a corner (`right`, `bottom left`, …; the default is `bottom`), `{ angle: 0.125, stops }` in turns clockwise from east, or `{ radial: true, at: [0.5, 0], stops }` out from a centre (fractions of the box, the middle by default) to its farthest corner. A stop is a colour — a `$token` too — or `[colour, position]` with the position 0 to 1; stops without one are spaced evenly between those with. Two stops at one position are a hard edge. The gradient is defined on the box's unit square and stretched to it, so a side or a corner is CSS's and any other `angle` runs corner to corner at an eighth of a turn whatever the box's aspect, where CSS's pixel-measured `45deg` does not. Stops mix in straight sRGB with the alpha premultiplied, as CSS's do. What it costs is one image quad: the core rasterizes each distinct gradient once into the glyph atlas — a 256-texel strip along an axis, a 128-texel square otherwise, within half an 8-bit level of the gradient computed per pixel for a linear one and 1.2 for a radial — keyed by the gradient and not the box, so a box that resizes and a thousand boxes that share one rasterize nothing, and a host that draws an image draws it; a gradient box costs about 55 ns over a flat one, so ten thousand of them are half a millisecond. A hard edge is as soft as the raster stretched to the box (a 256th of its length along a strip); stripes are boxes. It does not tween — `transition` eases the `bg` under it and `opacity` fades it — and `hoverBg` and the other state backgrounds replace `bg`, not the gradient; one that changes every frame is a raster a frame, and a shimmer is a `fragment`'s. Ignored on a `line`, a `polygon` and a `path`. Fewer than two stops are an error in JSX and Lua, as a malformed `keyframes` is, and draw nothing in Rust and C. |
43
+ | `gradient` | `gradient` | `gradient` (`const KuiGradient *`) | gradient (`{ to? \\| angle? \\| radial?, at?, stops: [color \\| [color, at], …] }`) | A gradient painted over the node's `bg` and under its border and its children (`docs/adr/0042-a-gradient-is-an-image-the-core-paints.md`): `{ to: 'bottom', stops: [...] }` towards a side or a corner (`right`, `bottom left`, …; the default is `bottom`), `{ angle: 0.125, stops }` in turns clockwise from east, or `{ radial: true, at: [0.5, 0], stops }` out from a centre (fractions of the box, the middle by default) to its farthest corner. A stop is a colour — a `$token` too — or `[colour, position]` with the position 0 to 1; stops without one are spaced evenly between those with. Two stops at one position are a hard edge. The gradient is defined on the box's unit square and stretched to it, so a side or a corner is CSS's and any other `angle` runs corner to corner at an eighth of a turn whatever the box's aspect, where CSS's pixel-measured `45deg` does not. Stops mix in straight sRGB with the alpha premultiplied, as CSS's do. What it costs is one image quad: the core rasterizes each distinct gradient once into the glyph atlas — a 256-texel strip along an axis, a 128-texel square otherwise, within half an 8-bit level of the gradient computed per pixel for a linear one and 1.2 for a radial — keyed by the gradient and not the box, so a box that resizes and a thousand boxes that share one rasterize nothing, and a host that draws an image draws it; a gradient box costs about 55 ns over a flat one, so ten thousand of them are half a millisecond. A hard edge is as soft as the raster stretched to the box (a 256th of its length along a strip); stripes are boxes. It does not tween — `transition` eases the `bg` under it and `opacity` fades it — and `hoverBg` and the other state backgrounds replace `bg`, not the gradient; one that changes every frame is a raster a frame, and a shimmer is a `fragment`'s. Ignored on a `line`, a `polygon` and a `path`. Fewer than two stops are an error in JSX and Lua, as a malformed `keyframes` is, and draw nothing in Rust and C. A stop whose `$token` misses is not an error: it is raised as `unknown-token` and left out, as a miss leaves any slot unset, and the rest are spaced as if it had not been declared — so a gradient left with fewer than two stops, a two-stop one with a typo, draws nothing over its `bg` (backlog RG118). |
44
44
  | `height` | `height` | `height` (KuiSizing) | sizing (`number` \\| `"fit"` \\| `"grow"` \\| `"N%"` \\| a size expression \\| `"$length"`) | Vertical size: px \| "fit" \| "grow" \| "N%" \| a size expression (see `width`). |
45
45
  | `hoverBg` | `hover_bg` | `hover_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while hovered (or while any node in its hoverGroup is); implies hover tracking, eases with `transition`. |
46
46
  | `hoverGroup` | `hover_group` | `hover_group` (KuiStr) | string | Nodes sharing a group name show hoverBg/pressedBg together (a split button, a multi-piece shape). |
@@ -135,6 +135,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
135
135
  | `borderW`, `borderColor` | `border = { w=, color= }` | `border_w`, `border_color` | Border width and color (drawn inside the rect). |
136
136
  | `dir="row" \| "column" \| "table"` | `row { }` / `column { }` / `grid { }` | `dir` (`KUI_ROW` / `KUI_COLUMN` / `KUI_TABLE`) | Main axis; column is the default. `table` is a column whose rows' children line up in columns (the `table` element). |
137
137
  | `float="below" \| "above" \| "parent" \| "viewport"` or `{ anchor, at, self, dx, dy, fit, clip }` | `float = "below"` or `float = { anchor=, at=, self=, dx=, dy=, fit=, clip= }` | `float_mode`, `float_anchor_x/y`, `float_self_x/y`, `float_dx/dy`, `float_fit`, `float_clip`; `kui_spec_float_preset` fills them from a preset name | Out-of-flow positioning against the parent or the viewport; `fit` flips/clamps to stay on screen. A float escapes every ancestor's clip — a tooltip is not cut by the scroller it hangs from — unless it declares `clip` and is anchored to its parent (`parent`, `below`, `above`): then the parent's clip holds it as it holds a child, so a node on a `clip` canvas panned past the canvas's edge is cut there and cannot be hit past it; it still paints as a layer over its in-flow siblings. A `line`, `polygon` or `path` in its parent's box is always clipped this way. The four preset names resolve in `FloatConfig::preset`, and `anchor` takes any of them — an override left out keeps the preset's own value, so `{ anchor: "below", dx: 4 }` still hangs below with its 6px gap. A float is a layer of its own: above the in-flow tree and every float that opened before it, under every float that opened after, and hit-tested in the same order — so a tooltip that appears over an open menu is over it, and a popover over a scroller's bar takes the press there. A scroller's bars and the focus ring belong to the layer that owns them. There is no z-index; a float declared under a fresh key reopens on top (`docs/adr/0023-layers-stack-in-the-order-they-open.md`). |
138
+ | `imeOff` (root box only) | `ime_off = true` (root table) | `kui_set_ime_off` | Declares that this window takes the keyboard as keys, with the platform's input method off (backlog F125): no composition and no candidate window, and on a Mac no dead key waiting for the next and no press-and-hold — an input method too, so a held letter repeats instead of opening the accent picker, whatever the user's `ApplePressAndHoldEnabled` says. A `key` event's `text` is still the layout's character; what goes is everything the OS would have composed from it. What a modal editor's normal mode wants — `jjjj` is how one moves, and an IME left on eats the keymap — while its insert mode stops declaring it and gets accents, dead keys and the IME back. Frame state the way `alwaysOnTop` is, default false: declare it on every frame the mode wants it, and the frame that stops gives the input method back; the runner applies it to the window on change, never per frame, and a composition in progress when it turns off ends without a commit, as an empty `preedit`. The window's, not a node's: a stock editor focused under it composes nothing either. A popup's keys arrive through its owner, so the owner's declaration is the one they are read under. On Windows and Linux the window's IME is disabled the same way, and only that: their dead keys are the layout's, and still compose. A C host with its own loop reads the ask with `kui_ime_off_get` and applies it itself. |
138
139
  | `index` | `index` | `kui_open_indexed` | Stable identity by *data* index rather than by name: the key auto-keying would have given this node as the `i`th child, given to it wherever it actually sits. What a virtualised list is for — a view that builds rows 900..930 of ten thousand opens each with its own row number, so the row keeps its hover, focus, edit buffer and tweens as the built range slides over it, and a list that builds every row agrees with one that builds a screenful. Wherever `key` names a node this numbers it (a box, a `line`, a `cells`, a `fragment`); declared beside `key` the index wins. Indices and names are separate namespaces, so a spacer keyed `"lead"` cannot collide with row 0 — but two rows on one index do, exactly as two on one name would. |
139
140
  | `key` | `key` | `kui_open_keyed` label | Stable identity for retained state (scroll offsets, editors, transitions; keys are hashes of the path from the root). Retained state outlives the key's absence, under a budget on the states nobody declares (see `<edit>` and the overflow props). |
140
141
  | `keyFocus` | `key_focus` | `kui_set_key_focus` | Focuses this node (an `onKey` sink, an editor, any focusable node) when it starts being declared: declared every frame it takes focus once, so a later Tab press is not clobbered. Declaring it on the frame a `modal` stops being declared is how a view says where focus lands on the way out — the edge stands, and the focus the modal displaced is not handed back over it (`docs/adr/0003-modal-surfaces.md`, decision 4). To move focus at any time call the binding's focus verb (`ctx.focus`, `kui_focus`, `env.set_focus`). |
@@ -145,7 +146,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
145
146
  | `secureInput` (root box only) | `secure_input = true` (root table) | `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. |
146
147
  | `size` (text) | `size` | `KuiTextStyle.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. |
147
148
  | `title` (root box only) | `window_title` (root table) | `kui_window_title` | Declares the window title for this frame; the driver diffs and applies. |
148
- | `tooltip="hint"` | `tooltip = "hint"` | `KuiSpec.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. The float is the node's last child, so it is drawn for a box or a `fragment`; on a leaf that holds no children — an `image`, an `edit`, a `cells` grid — the hint is tracked and spoken but not drawn, so put the tooltip on a box around it (backlog RG75). |
149
+ | `tooltip="hint"` | `tooltip = "hint"` | `KuiSpec.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. |
149
150
  | `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` | 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. |
150
151
 
151
152
  ## Elements
@@ -165,10 +166,10 @@ where they make sense); text props apply to `<text>` and `<edit>`.
165
166
  | `<slider label valueNow valueMin valueMax valueStep valueText onChange width description tooltip disabled/>` | `slider { label=, value_now=, value_min=, value_max=, value_step=, value_text=, on_change=, width=, … }` | `kui_slider` | The stock slider (`widgets::slider_with`, ADR 0034): a track, a fill to `valueNow` and a thumb, as wide as a menu (`width` sizes it), keyed by its `label`, which is also its accessible name. With `onChange` the core does the arithmetic: a press proposes the value under the pointer, a drag each new step, the arrows one `valueStep`, PageUp / PageDown ten, Home / End the ends, all clamped to `valueMin`..`valueMax` (0..100 unset) and snapped to the step, as `{kind:"change", value, phase:"move"\|"end", tag}`. The value is proposed, never applied: the view stores it and declares it as `valueNow`. Its look is its spec, so the rows it reads are the value rows, the access rows and its width. |
166
167
  | `<image src={id} sampling fit>` | `image { id=, sampling=, fit= }` | `kui_image`, `kui_image_with` | A registered RGBA image. Sizing: `width="fit"` takes the pixel size, a fit height against a resolved width keeps the aspect, `radius` rounds it. Two rows say how the pixels meet the box (`docs/adr/0025-the-image-is-the-canvas.md`): `sampling` is `linear` (the default) or `nearest` — pixel art, an emulator, a data grid that must stay square under zoom; `fit` is `fill` (the default: the pixels stretch to the box), `contain` (the largest rect of the image's aspect that fits, centred, the rest of the box showing what is behind) or `cover` (the box filled and the pixels that do not fit cropped, centred). The box — its layout, its hit region, its access rect — is the same in every mode. The pixels come from the atlas, or from a texture of the image's own once `updateImage` has replaced them or when no atlas page could hold them; the node cannot tell and need not. |
167
168
  | `<polygon points={[[x,y],…]} bg/>` | `polygon { points={{x,y},…}, bg= }` | `kui_polygon` | A filled polygon through up to eight `points`, the fill in `bg` (`docs/adr/0025-the-image-is-the-canvas.md`, decision 6): an arrowhead, a pie slice, the area under a curve. Placed as a `line` is — always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box a pixel out on each side, so it takes no room in a row or column, and painted in the parent's layer at its place in the tree, over the siblings before it and under those after (backlog F123). A polygon 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"` polygon escapes (backlog F78, `docs/adr/0010-a-segment-primitive.md` decision 5). `transition` eases the fill and, with `slide`, its position. The outline may be concave; a self-intersecting one fills even-odd, its overlaps unfilled. Hit by its outline (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press inside the outline hits it and one in its box past the outline falls through, so a pie's wedges need no hit boxes; with none it takes no input and has no access row, and with input it derives one as a box would (a clickable wedge is a button), so name it. A ninth point and later are dropped with `polygon-points-truncated`; fewer than three draw nothing; no `bg`, no fill. On the wire it is one `fragment` quad painted by a WGSL function the core registers itself, so a host that draws the list gets its source from `kui_fragment_source` like any other; what it costs is that quad and one pipeline switch per run of polygons. A stroked outline is a closed `line` over it. |
168
- | `<path d="M … Z" bg width color fillRule rotate pivot dash dashOffset/>` | `path { d = "M … Z", bg=, width=, color=, fill_rule=, rotate=, pivot=, dash=, dash_offset= }` | `kui_path` | Any outline — SVG's `d`, a pie wedge with a round arc, a map's region, an icon — filled with `bg` by `fillRule` (`nonzero`, the default, or `evenodd`) and stroked `width` wide in `color` when `width` is given, the stroke over the fill (`docs/adr/0040-a-path-is-a-mask-in-the-atlas.md`). `d` is SVG path data (`M L H V C S Q T A Z`, absolute or relative), parsed by one parser in the core, so every binding draws the same shape; one that does not parse raises `path-malformed` and draws nothing. JSX also takes `d` as a flat number array of op codes and operands, Lua the same as `ops`, and C only that form (`kui_path_parse` turns a string into it). Placed as a `line` is — always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box two pixels out on each side (half the stroke's width further), so it takes no room in a row or column, held by the parent's clip as a child is and painted in the parent's layer at its place in the tree (backlog F123). `transition` eases the fill and, with `slide`, its position; the stroke's colour does not tween, as a box's border does not. Hit by its outline under the fill rule (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press inside hits it and one in its box past the outline falls through, so a pie's wedges need no hit boxes; a stroke with no fill is hit by its stroke as a line is; with input it derives an access row as a box would (a clickable wedge is a button), so name it. On the wire it is one glyph-mask quad per paint, fill and stroke: the outline is rasterized once per shape, scale and quarter-pixel position into the glyph atlas and tinted like a glyph, so a host that draws text draws paths, and nothing is re-rasterized for a colour tween, a hover or a slide. The fill bleeds half a pixel, so two paths sharing an edge meet without the background showing through; a chart that wants separators gaps its own geometry. A mask a quarter of the biggest atlas page or more, or a path whose ops change twice within a few frames, draws from a texture of its own instead (a `texture` quad), and one past 8192 px on a side draws nothing, with `path-too-large`. `rotate` turns the path, in turns clockwise, about `pivot` — a point in the path's own coordinates, the centre of its box without one (`docs/adr/0041-a-mask-turns-about-its-centre.md`): the turn is the quad's and not the mask's, so a path that only turns — a spinner's arc about its circle's centre — is rasterized once and stays in the atlas at every angle. A path with `rotate` or `pivot` is boxed by the square the turn sweeps, its mask centred on the pivot on a whole pixel, and it is hit where it is drawn; `rotate` does not tween. `dash` and `dashOffset` cut the stroke into marks and gaps as a `line`'s do (backlog V2) — lengths as seen, round-capped marks — restarting at every subpath as SVG's do; the pattern is part of the stroke's mask, so a dashed stroke costs what a solid one does, a stroke with no fill is still hit along its gaps, and a `dashOffset` that changes every frame is a shape that changes every frame: the path leaves the atlas for a texture of its own while it marches. |
169
+ | `<path d="M … Z" bg width color fillRule rotate pivot dash dashOffset/>` | `path { d = "M … Z", bg=, width=, color=, fill_rule=, rotate=, pivot=, dash=, dash_offset= }` | `kui_path` | Any outline — SVG's `d`, a pie wedge with a round arc, a map's region, an icon — filled with `bg` by `fillRule` (`nonzero`, the default, or `evenodd`) and stroked `width` wide in `color` when `width` is given, the stroke over the fill (`docs/adr/0040-a-path-is-a-mask-in-the-atlas.md`). `d` is SVG path data (`M L H V C S Q T A Z`, absolute or relative), parsed by one parser in the core, so every binding draws the same shape; one that does not parse raises `path-malformed` and draws nothing. JSX also takes `d` as a flat number array of op codes and operands, Lua the same as `ops`, and C only that form (`kui_path_parse` turns a string into it). Placed as a `line` is — always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box two pixels out on each side (half the stroke's width further), so it takes no room in a row or column, held by the parent's clip as a child is and painted in the parent's layer at its place in the tree (backlog F123). `transition` eases the fill and, with `slide`, its position; the stroke's colour does not tween, as a box's border does not. Hit by its outline under the fill rule (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press inside hits it and one in its box past the outline falls through, so a pie's wedges need no hit boxes; a stroke with no fill is hit by its stroke as a line is; with input it derives an access row as a box would (a clickable wedge is a button), so name it. On the wire it is one glyph-mask quad per paint, fill and stroke: the outline is rasterized once per shape, scale and quarter-pixel position into the glyph atlas and tinted like a glyph, so a host that draws text draws paths, and nothing is re-rasterized for a colour tween, a hover or a slide. The fill bleeds half a pixel, so two paths sharing an edge meet without the background showing through; a chart that wants separators gaps its own geometry. A mask a quarter of the biggest atlas page or more, or a path whose ops change twice within a few frames, draws from a texture of its own instead (a `texture` quad), and one past 8192 px on a side draws nothing, with `path-too-large`. `rotate` turns the path, in turns clockwise, about `pivot` — a point in the path's own coordinates, the centre of its box without one (`docs/adr/0041-a-mask-turns-about-its-centre.md`): the turn is the quad's and not the mask's, so a path that only turns — a spinner's arc about its circle's centre — is rasterized once and stays in the atlas at every angle. A path with `rotate` or `pivot` is boxed by the square the turn sweeps, its mask centred on the pivot on a whole pixel, and it is hit where it is drawn; `rotate` does not tween. `dash` and `dashOffset` cut the stroke into marks and gaps as a `line`'s do (backlog V2) — lengths as seen, round-capped marks, a gap the dots overlap closed — restarting at every subpath as SVG's do; the pattern is part of the stroke's mask, so a dashed stroke costs what a solid one does, a stroke with no fill is still hit along its gaps, and a `dashOffset` that changes every frame is a shape that changes every frame: the path leaves the atlas for a texture of its own while it marches. |
169
170
  | `<fragment src={id} image={id} params={[…]} animate>` | `fragment { id=, image=, params={…}, animate= }` | `kui_fragment`, `kui_fragment_with` | A box a registered WGSL function paints (`docs/adr/0015-a-fragment-element-and-the-painter-it-is-not.md`): a conic or a moving gradient, rings, noise, shimmer — anything the paint vocabulary has no prop for. An ordinary node otherwise — it lays out, rounds, clips, fades, takes input and holds children, which paint over it — but with **no intrinsic size**, so give it a `width`/`height` or `fill` or it is zero by zero. `src` is a handle from `add_fragment`, which validates the source and warns rather than minting one that cannot compile. `params` is up to sixteen numbers the shader reads as four `vec4<f32>`; more are dropped with a warning. `image` is a registered image the function reads — `kui_sample(uv)` (bilinear) and `kui_sample_nearest(uv)` return its texels at `uv` in `[0,1]²`, and `in.image` is its texel rect, `zw` the size — which is what makes a replaced image a waveform, a heatmap, a 50k-point line or an image effect from one quad (`docs/adr/0025-the-image-is-the-canvas.md`, decision 7); the core binds the atlas or the image's own texture, whichever holds it, and a fragment whose image is not live draws nothing, as one whose `src` is not does. `animate` asks for a frame every frame, which is what a fragment that reads `time` needs and what a still one must not declare. |
170
171
  | `<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` | 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. |
171
- | `<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` | 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, and a mark no longer than the stroke is wide is a dot (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`). 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. |
172
+ | `<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` | 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. |
172
173
  | `<titlebar title>` or `<titlebar>…</titlebar>` | `titlebar { title= }` / `titlebar { … }` | `kui_titlebar`, `kui_titlebar_with` | Adaptive titlebar for custom chrome: drag strip, native-control inset, window buttons. |
173
174
  | `<menuBar menu={[{ label, items: [{ label, id?, role?, accel?, enabled?, checked? }] }]}/>` | `menu_bar { menu = { { label=, items= { { label=, id=, role=, accel=, enabled=, checked= } } } } }` | `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. |
174
175
  | `<windowButtons/>` | `window_buttons()` | `kui_window_buttons` | Just the min/max/close buttons, for fully custom titlebars. |
@@ -582,11 +583,13 @@ binding is a row with its three other cells, or a red test.
582
583
  | `Ui::always_on_top` | `kui_set_always_on_top` | the root's `alwaysOnTop` prop | the root's `always_on_top` field | Declares that the window sits above every other app's this frame (backlog C30). |
583
584
  | `Ui::secure_input` | `kui_set_secure_input` | the root's `secureInput` prop | the root's `secure_input` field | Declares that this frame wants secure keyboard entry while the window has the keyboard — a password prompt (backlog F85). |
584
585
  | `Ui::option_as_alt` | `kui_set_option_as_alt` | the root's `optionAsAlt` prop | the root's `option_as_alt` field | Declares which Option keys act as Alt in this window on macOS, so a dead key like ⌥u arrives as `<A-u>` (backlog F113). |
586
+ | `Ui::ime_off` | `kui_set_ime_off` | the root's `imeOff` prop | the root's `ime_off` field | Declares that this window takes the keyboard as keys, with the input method off — no composition, and on a Mac no dead keys and no press-and-hold, so a held letter repeats (backlog F125). |
585
587
  | `Ui::window_command` | the chrome roles (`KuiSpec.window_role`) are the door; the verb is what `widgets::window_buttons` lowers to | `KuiWindow.close()` for the one command the runner takes from outside a frame; the rest are `windowRole` | `window_role` | Minimize, toggle-maximize, start-drag, close — what a chrome node asks for on a press. |
586
588
  | `Core::window_title` | `kui_window_title_get` | `Ctx.windowTitle` | *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* | What the frame declared, for a driver applying it; a `KuiWindow` applies its own. |
587
589
  | `Core::always_on_top` | `kui_always_on_top_get` | `Ctx.alwaysOnTop` | `env.window.always_on_top`, a reading | The same for the level. |
588
590
  | `Core::secure_input` | `kui_secure_input_get` | `Ctx.secureInput` | *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 same for the secure-input ask: what a driver with its own loop reads to make the platform call; the runner makes it for a `KuiWindow` and `kui_run`. |
589
591
  | `Core::option_as_alt` | `kui_option_as_alt_get` | `Ctx.optionAsAlt` | *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 same for the Option-as-Alt ask: what a driver with its own loop reads to apply it to its window; the runner applies it for a `KuiWindow` and `kui_run`. |
592
+ | `Core::ime_off` | `kui_ime_off_get` | `Ctx.imeOff` | *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 same for the input-method ask: what a driver with its own loop reads to apply it to its window; the runner applies it for a `KuiWindow` and `kui_run`. |
590
593
  | `Core::take_window_commands` | `kui_take_window_command` | `Ctx.windowCommands` | *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* | Drains what the frame asked of the driver: open, close, resize, focus, redraw. |
591
594
  | `Core::window_closed` | `kui_window_closed` | `Ctx.windowClosed` | *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 driver reports a window gone. |
592
595
  | `Core::dismiss_window` | `kui_window_dismissed` | `Ctx.windowDismissed` | *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 driver reports a popup dismissed, with why (ADR 0003 step 4). |
Binary file