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

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,199 @@ 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.40 (2026-10-07)
25
+
26
+ **What breaks.**
27
+
28
+ - Rust: `InputEvent` gains `Open(Vec<String>)`, so an exhaustive `match`
29
+ on it needs an arm.
30
+
31
+ C stays at ABI 25 (`kui_input_open` is a new function) and the Node wire
32
+ at v21.
33
+
34
+ ### Added
35
+
36
+ - **The documents the OS asks the app to open reach it** (backlog F124,
37
+ from kawoosh). The Finder's Open With, a file dropped on the Dock icon,
38
+ `open -a YourApp file`, a double-click on a document type the bundle's
39
+ `Info.plist` declares (`CFBundleDocumentTypes`): AppKit hands every one
40
+ to the application delegate's `application:openURLs:`, which winit's
41
+ delegate does not answer, so they went nowhere. The macOS runner adds
42
+ it to winit's delegate class once per process, and the app hears
43
+ `{kind:"open", paths}` on the root — file-system paths, one event per
44
+ request, asked for or not. A launch's documents, which AppKit sends
45
+ before any window exists and leaves out of the arguments, wait for the
46
+ main window and arrive on its first turn; while the app runs they
47
+ arrive at once. A delegate class that already answers documents (an
48
+ app's own override) is left alone. Headless and for a host with its
49
+ own window: `InputEvent::Open(paths)`, C `kui_input_open`, Node
50
+ `ctx.openDocuments(paths)` (`OpenMsg` in `CoreMsg`). Windows and Linux
51
+ hand documents over in the process's arguments, and nothing sends it
52
+ there.
53
+
54
+ **What you can delete.** An `NSApplicationDelegate` method added or
55
+ swizzled by hand onto winit's delegate to hear a Finder open, and the
56
+ queue that held a launch's paths until a window existed to show them.
57
+
58
+ ### Native verification
59
+
60
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-07 — the
61
+ one round after the alpha.39 tag: the documents the OS asks the app to
62
+ open (F124) — with a regression pass over it first: the diff read whole,
63
+ each claim probed before anything changed. The probes ran in a window,
64
+ since the half that can break is AppKit's: a scratch app on the
65
+ release tree's `kui-native`, bare and in an ad-hoc-signed bundle
66
+ declaring `public.data` (`LSHandlerRank` `Alternate`). `open -a` at
67
+ launch handed over both documents in one `open` event, `argv` the
68
+ executable alone; two `open -a` calls into the running instance arrived
69
+ as two events, in order, each with its request's paths in their order.
70
+ And the one thing an added `application:openURLs:` could have started
71
+ that F124's own checks did not look at: AppKit taking the process's
72
+ arguments for documents once the delegate answers them
73
+ (`NSTreatUnknownArgumentsAsOpen`). A binary run from a terminal with a
74
+ path, a subcommand, a missing file and a flag, bare and as the bundle's
75
+ own executable, even with `-NSTreatUnknownArgumentsAsOpen YES`, heard
76
+ no `open` — so an app that reads its arguments and hears `open` too
77
+ does not open a file twice. The non-macOS side (`pump_open_documents`
78
+ without `macos_open`) is clean under clippy for `x86_64-pc-windows-msvc`.
79
+ Nothing was filed. This round ran on the Mac alone; Windows and Linux
80
+ did not run it for this tag.
81
+
82
+ **macOS 27.0.1 on an M3 Pro MacBook Pro, rustc 1.99.0 (the toolchain
83
+ CI runs), Node 25.6.0, nu 0.116.1**, on the release commit's tree.
84
+ `cargo fmt --all --check` and `cargo clippy --workspace --all-targets
85
+ --features kui-core/conformance -- -D warnings` are clean, and so is
86
+ `cargo audit --deny warnings`. `cargo test --workspace --features
87
+ kui-core/conformance`: **1822 tests over 137 suites, 0 failed** (4
88
+ ignored). The C round, `cbuild --run`, passes its five checks; the
89
+ corpus passes its **57 scenes** in four adapters, the `drop` scene now
90
+ ending with two documents opened; the ABI is **25**. Node's `node --test
91
+ test.mjs` under `KUI_CONFORMANCE_REQUIRED=1`: **211 of 211**. `npm run
92
+ gen` leaves no diff, the examples typecheck and their lockfile installs,
93
+ the headless round passes all **35 drives**, the book builds and
94
+ `scripts/book-examples.nu --check` passes.
95
+
96
+ **The windowed round**, `smoke -- --node`, three times over — twice
97
+ before the version bump, once on the release tree: **51 Rust examples
98
+ and the eleven Node examples, each on both bases, 120 frames each, every
99
+ one exiting 0** — 124 windows, eight at a time, in 32.3, 32.5 and
100
+ 38.8 s — and `counter`, `host`, `c_panel` and `lua_panel` by hand under
101
+ `KUI_SMOKE_FRAMES=120`, each exiting 0 with nothing on stderr: **128
102
+ windows over five hosts.** The AX audit: **106/106**, the audited window
103
+ raised to the front by its pid first, and no warning on the fixture's
104
+ stderr. F124 itself was seen in kawoosh's window before the merge, built
105
+ against the branch by path.
106
+
107
+ **The bench guard** against the alpha.39 tag: **green**, none of the 8
108
+ guarded rows more than 10% slower — every one between −3.2% and +3.5%
109
+ (the worst guarded run-to-run spread 1.3%) — and no row of the run more
110
+ than 4.7% slower (`frame_1k_paths_animating`, a frame F124 does not
111
+ reach). `README.md`'s table is kept as it was.
112
+
113
+ ## 0.1.0-alpha.39 (2026-10-06)
114
+
115
+ **What breaks.**
116
+
117
+ - A `line`, `polygon` or `path` in its parent's box space paints in its
118
+ parent's layer at its place in the tree — over the siblings declared
119
+ before it, under those declared after — where it was a float layer of
120
+ its own, above every in-flow node and every float that had opened
121
+ before it (under Fixed). A stroke declared before a sibling it
122
+ overlaps is under that sibling now; declare it after to keep it on
123
+ top. One anchored `float="viewport"` is placed as it was.
124
+
125
+ ### Fixed
126
+
127
+ - **A stroke is its parent's content, not a layer over the window**
128
+ (backlog F123, from kawoosh). The core floats every `line`, `polygon`
129
+ and `path` so it takes no room in a row or column, and since ADR 0023
130
+ every float is a layer stacked by when it opened — so a glyph drawn as
131
+ polylines into a pane's title bar, in a pane opened after the first
132
+ frame, painted over a toast the app had floated on every frame since
133
+ the window opened: kawoosh's "a new Kawoosh is installed: relaunch to
134
+ run it" with an `⌥` key cap through its border. A stroke in its
135
+ parent's box space now paints in the parent's layer, at its place in
136
+ the tree, as a child that takes no room does, and is held by the
137
+ parent's clip as before; a stroke anchored to the viewport keeps a
138
+ layer of its own. `Tree::opens_layer` is the one reading, for the live
139
+ pass and for a departing stroke's ghost.
140
+ - **`modal-behind-content` reads the paint order, not the float bit**
141
+ (backlog RG120, from this release's pre-tag pass). With a stroke in its
142
+ parent's layer, the check for content over a modal the view did not
143
+ float still took any node under a `float` for a layer over it — and
144
+ every `line`, `polygon` and `path` is one for the room alone — so an
145
+ icon drawn with strokes before the modal's declaration raised the
146
+ warning on a modal nothing painted over. It reads `Tree::opens_layer`
147
+ now; a viewport-anchored stroke, a layer of its own, still warns.
148
+
149
+ **What you can delete.** A toast or popover redeclared under a fresh key
150
+ so it stacks over the icons an app draws with `line` or `polygon`, and
151
+ a stroke moved out of its row into a float of its own so a card could
152
+ cover it.
153
+
154
+ - **`scripts/npm-approve.nu` asks npmjs whatever the npm it runs under
155
+ is configured with.** An npm with `@qxuken:registry` pointing at the
156
+ Forgejo copy asks that registry for a scoped package whatever
157
+ `--registry` says. alpha.38 was published by hand during a GitHub
158
+ Actions outage, Forgejo's npm copy first, and the script then read
159
+ Forgejo's dist-tags and said "live on npmjs already" and "latest is
160
+ 0.1.0-alpha.38 already" while npmjs held the version staged. Every
161
+ call names npmjs for the scope as well now. A release script, so
162
+ nothing an app sees.
163
+
164
+ **What you can delete.** Nothing.
165
+
166
+ ### Native verification
167
+
168
+ The by-hand round alpha.6 introduced (backlog R4), on 2026-10-06 — the
169
+ two commits after the alpha.38 tag: a stroke painting in its parent's
170
+ layer (F123) and the approval script naming npmjs for the scope — with a
171
+ regression pass over them first: the diff read whole, each claim probed
172
+ with a test before anything changed. Six probes were written; five
173
+ passed and are kept in `tests/layers.rs` for what F123's own tests did
174
+ not pin (a `polygon` and a `path` under the same rule, a departing
175
+ stroke's ghost in its parent's layer, a declared float with `clip` still
176
+ a layer, a viewport-anchored stroke still escaping its clip, a clickable
177
+ stroke hit at its place), and the sixth found RG120, which is in this
178
+ release. This round ran on the Mac alone; Windows and Linux did not run
179
+ it for this tag.
180
+
181
+ **macOS 27.0.1 on an M3 Pro MacBook Pro, rustc 1.99.0 (the toolchain
182
+ CI runs), Node 25.6.0, nu 0.116.0**, on the release commit's tree, the
183
+ workspace's own artifacts pruned and rebuilt. `cargo fmt --all --check`
184
+ and `cargo clippy --workspace --all-targets --features
185
+ kui-core/conformance -- -D warnings` are clean. `cargo test --workspace
186
+ --features kui-core/conformance`: **1816 tests over 136 suites, 0
187
+ failed** (4 ignored). The C round, `cbuild --run`, passes its five
188
+ checks; the corpus passes its **57 scenes** in four adapters; the ABI
189
+ is **25**. Node's `node --test test.mjs` under
190
+ `KUI_CONFORMANCE_REQUIRED=1`: **210 of 210**. `npm run gen` leaves no
191
+ diff, the examples typecheck and their lockfile installs, the headless
192
+ round passes all **35 drives**, the book builds and
193
+ `scripts/book-examples.nu --check` passes.
194
+
195
+ **The windowed round**, `smoke -- --node`, twice over: **51 Rust
196
+ examples and the eleven Node examples, each on both bases, 120 frames
197
+ each, every one exiting 0** — 124 windows, eight at a time, in
198
+ 37.6 and 33.4 s — and `counter`, `host`, `c_panel` and `lua_panel` by
199
+ hand under `KUI_SMOKE_FRAMES=120`, each exiting 0 with nothing on
200
+ stderr: **128 windows over five hosts.** The AX audit: **106/106**, the
201
+ audited window raised to the front by its pid first, and no warning on
202
+ the fixture's stderr. F123 itself was seen in kawoosh's window before
203
+ the merge, built against the branch by path; RG120's case is pinned in
204
+ the core's tests alone.
205
+
206
+ **The bench guard** against the alpha.38 tag, on the tree the release
207
+ commit was cut from: **green**, none of the 8 guarded rows more than
208
+ 10% slower — every one between −5.3% and +1.4% (the worst guarded
209
+ run-to-run spread 2.7%), and no row of the run more than 2.2% slower.
210
+ Three rows got faster by more than the noise, and that is F123:
211
+ `frame_10k_segments` 881 → 834 µs (−5.3%), `frame_1k_closed_lines`
212
+ 103 → 97.1 µs (−5.9%) and `frame_1k_polygons` 102 → 97.2 µs (−4.5%) —
213
+ ten thousand strokes that were ten thousand layers for the float stack
214
+ to sort are their parents' content now. `README.md`'s table is kept as
215
+ it was.
216
+
24
217
  ## 0.1.0-alpha.38 (2026-10-05)
25
218
 
26
219
  **What breaks.**
@@ -136,6 +136,11 @@ test — is declined or deferred below, each with the reason.
136
136
  ignored. Because it is a float it paints in the float pass, on top of
137
137
  its parent's in-flow content and in tree order among the other floats:
138
138
  a connector meant to sit under two cards is declared before them.
139
+ *Amended 2026-10-06 (backlog F123):* it paints in its parent's layer
140
+ at its place in the tree — over the parent's box and the siblings
141
+ before it, under those after — and opens no layer of its own; the
142
+ connector under two cards is still declared before them. See the
143
+ amendment at the end.
139
144
  `slide`, `enter` and `exit` offsets move it as they move any float.
140
145
  *Amended 2026-09-22 (backlog F78):* it is clipped as a child of its
141
146
  parent is — by the parent's own box when the parent clips or
@@ -403,7 +408,17 @@ for a stroke that escapes its parent.
403
408
  Paint order does not change. A clipped float is still its own layer, drawn
404
409
  above its in-flow siblings in the float pass and hit in the same order
405
410
  ([ADR 0023](0023-layers-stack-in-the-order-they-open.md)). Only the clip
406
- comes from the parent. The corpus's `clip-float` scene pins it in every
411
+ comes from the parent. *Amended 2026-10-06 (backlog F123):* that holds
412
+ for a float a view declared with the bit. A `line`, `polygon` or `path`
413
+ in its parent's box space, whose float the core made, is not a layer at
414
+ all: it paints in its parent's layer at its place in the tree, as a child
415
+ that takes no room — `Tree::opens_layer` is the one reading, for the live
416
+ pass and the ghost pass. Under ADR 0023's stack, where a layer is above
417
+ every layer that opened before it, a stroke's own layer put a key cap
418
+ drawn into a title bar over a toast that had been open since the window's
419
+ first frame; a stroke that is its parent's content stacks with the
420
+ parent. One anchored `float="viewport"` escapes and keeps a layer of its
421
+ own. The corpus's `clip-float` scene pins it in every
407
422
  binding: two nodes on a clipping canvas are panned half past its top edge,
408
423
  one clipped and one not. A press over the toolbar where the clipped node's
409
424
  cut half would be reaches the toolbar, and the same press on the other
@@ -374,3 +374,22 @@ paints over the page's bar in a real window, checked by screenshot.
374
374
  "draws on top of in-flow content" becomes "a layer of its own, above
375
375
  the in-flow tree and every float that opened before it"; the `float`
376
376
  schema row (and so `docs/props.md`) gains the same sentence.
377
+
378
+ ## Amendment — a stroke is not a layer (2026-10-06, backlog F123)
379
+
380
+ Decision 1 made every float root a layer, and the core makes a float of
381
+ every `line`, `polygon` and `path` so it takes no room in a row or column
382
+ ([ADR 0010](0010-a-segment-primitive.md) decision 5). Together, under
383
+ decision 3, a stroke drawn a frame after some float opened was above that
384
+ float: kawoosh's update toast, a viewport float declared on every frame
385
+ so it stays under every confirm, had the `⌥` key cap of a pane's title
386
+ bar — polylines — painted over it, since the pane opened after the
387
+ window's first frame. The stroke's float is the core's, for the room
388
+ alone; the stroke is its parent's content, which F78 already said for its
389
+ clip. So a stroke in its parent's box space now paints in its parent's
390
+ layer at its place in the tree, as a child does — `Tree::opens_layer`
391
+ decides what is a layer root, in `emit_frame` and in `PaintOrder::of`
392
+ for a departing one — and only a float a view declared, or a stroke
393
+ anchored to the viewport, opens a layer. `tests/layers.rs` pins the
394
+ stroke under a toast opened before it, a stroke inside a float in that
395
+ float's layer, and the viewport-anchored one above.
@@ -170,7 +170,9 @@ date: 2026-09-11
170
170
  decision 5): always a float, sized to its bounding box inflated by one
171
171
  logical px for the antialiasing ramp, points in the parent's box space
172
172
  (`float="viewport"` for viewport space), no room taken in a row or
173
- column; `slide`, `enter`, `exit` move it as a float. Like a line it
173
+ column; `slide`, `enter`, `exit` move it as a float — and, since
174
+ backlog F123, painted in the parent's layer at its place in the tree
175
+ as a line is, not in a layer of its own. Like a line it
174
176
  takes **no input** and has **no access row** (`polygon-ignores-input`
175
177
  for the same six keys, a `role` and `label` honoured if declared) —
176
178
  *superseded the same day by [ADR 0026](0026-hit-testing-by-shape.md):
@@ -105,7 +105,9 @@ date: 2026-10-04
105
105
  parent's box space, `float="viewport"` for viewport space, sized to its
106
106
  own bounding box inflated by one logical px, no room taken in a row or
107
107
  column; `slide`, `enter` and `exit` move and fade it as a float; a
108
- declared `clip` holds it as it holds a polygon.
108
+ declared `clip` holds it as it holds a polygon; and, since backlog
109
+ F123, it paints in the parent's layer at its place in the tree as a
110
+ line and a polygon do, not in a layer of its own.
109
111
  2. **One wire form, and SVG's `d` parsed in the core.** On the wire a
110
112
  path is a flat `f32` list, an op code followed by its operands, every
111
113
  coordinate absolute and in the parent's box space: `M x y`, `L x y`,
package/howto.md CHANGED
@@ -42,7 +42,10 @@ because the removal is judged whole rather than half-animated, and the
42
42
  `<line from={[x, y]} to={[x, y]} width color/>` is one round-capped stroke,
43
43
  `<line points={[[x, y], …]} curve/>` a polyline or a smooth curve through
44
44
  the points; a line is always a float in its parent's box space, sized to its
45
- own bounding box, so it takes no room in a row or column. With `onClick`,
45
+ own bounding box, so it takes no room in a row or column — and it paints
46
+ where a child declared there would, in its parent's layer, over the siblings
47
+ before it and under those after (a connector meant to sit under two cards is
48
+ declared before them; one meant to sit over them, after). With `onClick`,
46
49
  `onDrag` or `hoverable` it is hit by its stroke, at least 4 px wide
47
50
  ([ADR 0026](docs/adr/0026-hit-testing-by-shape.md)). Budget its quads: one per segment, and a curve is flattened
48
51
  in the core at one piece per 6 logical px of chord, at most 32 per span — so
@@ -747,6 +750,35 @@ drive, and `dropTarget()` is what a driver answers the OS with.
747
750
  [`drop` payload](props.md#events) ·
748
751
  [`examples/rust/features/drop.rs`](../examples/rust/features/drop.rs)
749
752
 
753
+ ### How do I hear the documents the OS opens: Finder's Open With, the Dock icon?
754
+
755
+ Handle `{kind:"open", paths}` in `update` (Rust `on_event`, C's event
756
+ callback): it arrives on the root, asked for or not, with the file-system
757
+ paths of the documents the Finder's Open With, a file dropped on the Dock
758
+ icon, `open -a YourApp file.rs` or a double-click handed the app. The
759
+ macOS runner hears them from the application delegate's
760
+ `application:openURLs:` — at launch, where AppKit sends them before the
761
+ window exists and puts none in the arguments, the paths wait for the main
762
+ window and arrive on its first turn; while the app runs, at once. One
763
+ `open` per request, several paths when the user opened several.
764
+
765
+ The app's bundle has to say it opens documents, or the Finder will not
766
+ offer it: an `Info.plist` with `CFBundleDocumentTypes`, one entry per
767
+ kind — `LSItemContentTypes` the UTIs (`public.plain-text`,
768
+ `public.source-code`, `public.data` for anything), `CFBundleTypeRole`
769
+ `Editor` or `Viewer`, and `LSHandlerRank` `Alternate` to be listed in Open
770
+ With without taking the type's double-click from its owner (`Default` or
771
+ `Owner` to take it). Re-register a bundle you rebuilt in place with
772
+ `lsregister -f YourApp.app` if the Finder still has the old list.
773
+
774
+ On Windows and Linux the documents are in the process's arguments (a
775
+ file association, a `.desktop` file's `%F`), and nothing sends `open`.
776
+ Headless, `ctx.openDocuments(paths)` (C `kui_input_open`, Rust
777
+ `InputEvent::Open`) is the drive.
778
+
779
+ [`open` payload](props.md#events) ·
780
+ [alpha.40 `### Added`](CHANGELOG.md#010-alpha40-2026-10-07)
781
+
750
782
  ### How do I have global shortcuts and a Tab ring at once?
751
783
 
752
784
  Put the keymap on an `onKey` sink that encloses the controls: a focused
package/index.d.ts CHANGED
@@ -368,6 +368,18 @@ export type SystemMsg = {
368
368
  * A font the app loads itself raises none. */
369
369
  export type FontsMsg = { kind: 'fonts' };
370
370
 
371
+ /** The OS asked the app to open these documents (backlog F124): the
372
+ * Finder's Open With, a file dropped on the Dock icon, `open -a`, a
373
+ * double-click on a document type the app's Info.plist declares
374
+ * (`CFBundleDocumentTypes`). Delivered on the root whether or not the app
375
+ * asked — at launch, once the window is open, and while it runs. `paths`
376
+ * are file-system paths. macOS only: Windows and Linux hand documents over
377
+ * in `process.argv`. `ctx.openDocuments(paths)` is the headless drive. */
378
+ export type OpenMsg = {
379
+ kind: 'open';
380
+ paths: string[];
381
+ };
382
+
371
383
  /** A declared window opened, or closed — because nothing declares it any
372
384
  * more, or because the user closed it. A window the user closed stays
373
385
  * closed while it is still declared (a declaration reopens a window only
@@ -496,7 +508,8 @@ export type CoreMsg =
496
508
  | SoundMsg
497
509
  | AccessMsg
498
510
  | ChangeMsg
499
- | FilesMsg;
511
+ | FilesMsg
512
+ | OpenMsg;
500
513
 
501
514
  /** A stock slider's proposal (docs/adr/0034-stock-controls-over-the-roles.md):
502
515
  * the core turned a press, a drag, an arrow, a Page key, Home / End or an
@@ -2161,6 +2174,14 @@ export declare class Ctx {
2161
2174
  * asked it is dropped.
2162
2175
  */
2163
2176
  answerFiles(paths: Array<string>): void
2177
+ /**
2178
+ * The OS asked the app to open these documents (backlog F124) — the
2179
+ * Finder's Open With, a file dropped on the Dock icon, `open -a`: the
2180
+ * app hears `{kind:"open", paths}` on the root whether or not it
2181
+ * asked; none is nothing. A `KuiWindow` on macOS hears it from the
2182
+ * runner; this is the headless drive.
2183
+ */
2184
+ openDocuments(paths: Array<string>): void
2164
2185
  /**
2165
2186
  * The dragged files released at (`x`, `y`): the zone there hears
2166
2187
  * `{kind:"drop", phase:"drop", paths, x, y, tag}` and no `leave`
package/jsx-runtime.d.ts CHANGED
@@ -900,7 +900,9 @@ export declare namespace JSX {
900
900
  * Placed as a `line` is: always a float in its parent's box space
901
901
  * (`float="viewport"` for viewport space), sized to its own bounding
902
902
  * box two pixels out on each side, so it takes no room in a row or
903
- * column. `transition` eases the fill and, with `slide`, its position.
903
+ * column, and painted in the parent's layer at its place in the tree
904
+ * (backlog F123). `transition` eases the fill and, with `slide`, its
905
+ * position.
904
906
  * Hit by its outline under the fill rule
905
907
  * (docs/adr/0026-hit-testing-by-shape.md): with `onClick`, `onDrag`,
906
908
  * `onHover` or `hoverable`, a press inside hits it and one in its box
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qxuken/kui",
3
- "version": "0.1.0-alpha.38",
3
+ "version": "0.1.0-alpha.40",
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
Binary file
package/props.md CHANGED
@@ -164,11 +164,11 @@ where they make sense); text props apply to `<text>` and `<edit>`.
164
164
  | `<switch checked onClick key label description tooltip disabled>text</switch>` | `switch { label=, checked=, on_click=, key=, text=, description=, tooltip=, disabled= }` | `kui_switch` | The stock switch (`widgets::toggle_with`, ADR 0034): a track and a knob drawn from `checked`, the knob sliding across when it changes, and its label; read as a switch, on or off. The state is the app's, as a checkbox's is; the rows are the checkbox's, `mixed` aside. |
165
165
  | `<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
166
  | `<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
- | `<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. 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. `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. |
167
+ | `<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
169
  | `<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
170
  | `<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. 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 (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. |
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
172
  | `<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
173
  | `<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
174
  | `<windowButtons/>` | `window_buttons()` | `kui_window_buttons` | Just the min/max/close buttons, for fully custom titlebars. |
@@ -202,6 +202,7 @@ one field. The payload shapes:
202
202
  | hover | `{ kind: "hover", phase: "enter" \| "leave", by: "pointer" \| "content", tag }` | The pointer entered or left an `onHover` node — also when a new frame moved it under a still cursor. `by` says which (backlog DX20): `pointer` when the pointer moved or left the window, `content` when it stayed and what is under it changed — a list scrolled by the wheel or the keys, a row that grew, a float that opened. A picker whose selection follows the pointer ignores `content`, or the rows sliding under a still pointer as the keys scroll the list drag the selection with them. |
203
203
  | drop | `{ kind: "drop", phase: "enter" \| "move" \| "leave" \| "drop", paths: string[], x, y, tag }` | Files dragged in from the OS over an `onDrop` node (`docs/adr/0031-a-drop-zone-is-a-row-and-the-files-are-an-event.md`): `enter` when they come over the zone, `move` while they move over it (never twice for one point), `leave` when they go to another zone, to no zone or out of the window, `drop` when they land — and no `leave` after a `drop`. `paths` are the OS paths as strings; `x`/`y` the pointer in logical viewport coordinates, absent on `leave`. The zone is the topmost one under the pointer by paint order; a node inside it is its, and a node that is no zone is looked past (an overlay shown on `enter` does not end the hover). Nothing is re-resolved when a frame lands: only the driver's next report moves the files, so a zone the view stops declaring hears its `leave` then. |
204
204
  | files | `{ kind: "files", paths: string[], tag }` | A file dialog's answer (backlog C51): what an Open, Save or folder dialog asked for with `requestFiles` / `request_files` / `kui_request_files` picked — the OS paths, as a `drop` carries them — or no paths when the user cancelled. `tag` is the dialog's own. It reaches whoever asked: the host, or the extension whose fill asked; one dialog is out at a time. |
205
+ | open | `{ kind: "open", paths: string[] }` on the root | The OS asked the app to open documents (backlog F124): the Finder's Open With, a file dropped on the Dock icon, `open -a App file`, a double-click on a document of a type the app's `Info.plist` declares under `CFBundleDocumentTypes`. Delivered to the host on the root whether or not anything asked — unlike `files`, which answers an ask. `paths` are file-system paths as strings; a URL of a scheme the app registers is not one, and is not carried (room is left for a `urls` beside `paths`). The macOS runner sends it at launch — held until the main window has opened, since AppKit hands the documents over before it exists and puts none in the arguments — and while the app runs; a host driving its own window sends it as input (`openDocuments`, `kui_input_open`, `InputEvent::Open`). Windows and Linux pass documents in the process's arguments instead, and nothing sends it there. |
205
206
  | layout | `{ kind: "layout", x, y, w, h, parent: { x, y, w, h }, scale, tag }` | The rect layout gave an `onLayout` node (logical px, viewport coords, after scrolling and easing): on its first frame and whenever it changes, never on a frame that left it alone. `scale` is physical px per logical px at the node — `w × scale` by `h × scale` is how many pixels to render for it before `updateImage` (the frame's scale today; where a zoom would compose in). |
206
207
  | resize | `{ kind: "resize", width, height, scale }` | The viewport changed size or DPI (logical px, delivered to the host on the root): the window less the devtools' dock while the panel is docked (`docs/adr/0024`), so a dock coming, going or being dragged is a resize too. `KuiWindow.size()` queries the same numbers, before the first frame as well (backlog F43). The first frame establishes the viewport rather than reporting it, so a dock present at launch posts none. |
207
208
  | window | `{ kind: "window", phase: "opened" \| "closed" \| "focused" \| "blurred", name, id }` | A declared window opened (the diff queued its `Open`) or closed — because nothing declares it any more, or because the user closed it, in which case it stays closed while still declared: stop declaring `name`, then declare it again to reopen. `id` is what its events carry; the event itself is on the root of whichever window's frame noticed. `focused` and `blurred` are this window gaining and losing the keyboard (backlog DX18) — `env.focused` changing, as the driver reports it — so an app that saves on blur or re-reads the clipboard on return hears it once instead of diffing `env.focused` every frame. |
@@ -631,7 +632,7 @@ binding is a row with its three other cells, or a red test.
631
632
  | `Core::set_time` | `kui_set_time` | `Ctx.setTime` | *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 clock the tweens read; a window's runner sets it from the display. |
632
633
  | `Core::env` | `kui_env_set` and its four siblings, `ENV_FIELDS`' C column | `Ctx.setEnv` | *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 host facts written in (the `env` field); a `KuiWindow`'s runner writes its own. |
633
634
  | `Ui::env` | *none: C is the host, so it writes the facts and has no reading (`ENV_FIELDS`)* | `env` | `env`, the view's argument | The facts read back, `ENV_FIELDS` row for row. |
634
- | `Core::handle_input` | `kui_input_cursor` … `kui_input_access`, one per `InputEvent` | `Ctx.cursor` … `Ctx.access`, one per `InputEvent`; a `KuiWindow` refuses injection | *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* | Pointer, wheel, key, text, IME, assistive and OS file-drag input; a wheel gesture's latching is `scroll_gesture` (`kui_input_scroll_gesture`, `Ctx.scrollGesture`, backlog F107); `press` / `release` are a click by label (`kui_input_press`, `Ctx.press`); the file drag is `drag_files` / `drop_files` / `drag_cancel` (ADR 0031); a file dialog's answer is `answer_files` (`kui_input_files`, `Ctx.answerFiles`, backlog C51). |
635
+ | `Core::handle_input` | `kui_input_cursor` … `kui_input_access`, one per `InputEvent` | `Ctx.cursor` … `Ctx.access`, one per `InputEvent`; a `KuiWindow` refuses injection | *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* | Pointer, wheel, key, text, IME, assistive and OS file-drag input; a wheel gesture's latching is `scroll_gesture` (`kui_input_scroll_gesture`, `Ctx.scrollGesture`, backlog F107); `press` / `release` are a click by label (`kui_input_press`, `Ctx.press`); the file drag is `drag_files` / `drop_files` / `drag_cancel` (ADR 0031); a file dialog's answer is `answer_files` (`kui_input_files`, `Ctx.answerFiles`, backlog C51); the documents the OS asked the app to open are `InputEvent::Open` (`kui_input_open`, `Ctx.openDocuments`, backlog F124). |
635
636
  | `Core::modifiers` | `kui_input_modifiers` | `Ctx.modifiers` | *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 modifier state, reported on its own when the OS does (backlog AR22). |
636
637
  | `Core::release_held_keys` | `kui_release_held_keys` | `Ctx.setEnv({focused: false})` releases, as losing the keyboard does for every driver (ADR 0020) | *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* | Lets go of every key the focused sink holds. |
637
638
  | `Core::set_subpixel_text` | `kui_set_subpixel_text` | *none: a Node host never paints: the renderer behind `KuiWindow` is the runner's, and a headless `Ctx` has none* | *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* | LCD subpixel coverage for outline glyphs, for a renderer that blends per channel. |