@qxuken/kui 0.1.0-alpha.37 → 0.1.0-alpha.39
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 +202 -0
- package/docs/adr/0010-a-segment-primitive.md +16 -1
- package/docs/adr/0023-layers-stack-in-the-order-they-open.md +19 -0
- package/docs/adr/0025-the-image-is-the-canvas.md +3 -1
- package/docs/adr/0038-a-scroll-gesture-latches-its-target.md +8 -0
- package/docs/adr/0040-a-path-is-a-mask-in-the-atlas.md +3 -1
- package/howto.md +29 -1
- package/index.d.ts +3 -0
- package/jsx-runtime.d.ts +5 -1
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/kui_node.node +0 -0
- package/prebuilds/darwin-x64/kui_node.node +0 -0
- package/prebuilds/linux-arm64/kui_node.node +0 -0
- package/prebuilds/linux-x64/kui_node.node +0 -0
- package/prebuilds/win32-x64/kui_node.node +0 -0
- package/props.md +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,208 @@ 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.39 (2026-10-06)
|
|
25
|
+
|
|
26
|
+
**What breaks.**
|
|
27
|
+
|
|
28
|
+
- A `line`, `polygon` or `path` in its parent's box space paints in its
|
|
29
|
+
parent's layer at its place in the tree — over the siblings declared
|
|
30
|
+
before it, under those declared after — where it was a float layer of
|
|
31
|
+
its own, above every in-flow node and every float that had opened
|
|
32
|
+
before it (under Fixed). A stroke declared before a sibling it
|
|
33
|
+
overlaps is under that sibling now; declare it after to keep it on
|
|
34
|
+
top. One anchored `float="viewport"` is placed as it was.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **A stroke is its parent's content, not a layer over the window**
|
|
39
|
+
(backlog F123, from kawoosh). The core floats every `line`, `polygon`
|
|
40
|
+
and `path` so it takes no room in a row or column, and since ADR 0023
|
|
41
|
+
every float is a layer stacked by when it opened — so a glyph drawn as
|
|
42
|
+
polylines into a pane's title bar, in a pane opened after the first
|
|
43
|
+
frame, painted over a toast the app had floated on every frame since
|
|
44
|
+
the window opened: kawoosh's "a new Kawoosh is installed: relaunch to
|
|
45
|
+
run it" with an `⌥` key cap through its border. A stroke in its
|
|
46
|
+
parent's box space now paints in the parent's layer, at its place in
|
|
47
|
+
the tree, as a child that takes no room does, and is held by the
|
|
48
|
+
parent's clip as before; a stroke anchored to the viewport keeps a
|
|
49
|
+
layer of its own. `Tree::opens_layer` is the one reading, for the live
|
|
50
|
+
pass and for a departing stroke's ghost.
|
|
51
|
+
- **`modal-behind-content` reads the paint order, not the float bit**
|
|
52
|
+
(backlog RG120, from this release's pre-tag pass). With a stroke in its
|
|
53
|
+
parent's layer, the check for content over a modal the view did not
|
|
54
|
+
float still took any node under a `float` for a layer over it — and
|
|
55
|
+
every `line`, `polygon` and `path` is one for the room alone — so an
|
|
56
|
+
icon drawn with strokes before the modal's declaration raised the
|
|
57
|
+
warning on a modal nothing painted over. It reads `Tree::opens_layer`
|
|
58
|
+
now; a viewport-anchored stroke, a layer of its own, still warns.
|
|
59
|
+
|
|
60
|
+
**What you can delete.** A toast or popover redeclared under a fresh key
|
|
61
|
+
so it stacks over the icons an app draws with `line` or `polygon`, and
|
|
62
|
+
a stroke moved out of its row into a float of its own so a card could
|
|
63
|
+
cover it.
|
|
64
|
+
|
|
65
|
+
- **`scripts/npm-approve.nu` asks npmjs whatever the npm it runs under
|
|
66
|
+
is configured with.** An npm with `@qxuken:registry` pointing at the
|
|
67
|
+
Forgejo copy asks that registry for a scoped package whatever
|
|
68
|
+
`--registry` says. alpha.38 was published by hand during a GitHub
|
|
69
|
+
Actions outage, Forgejo's npm copy first, and the script then read
|
|
70
|
+
Forgejo's dist-tags and said "live on npmjs already" and "latest is
|
|
71
|
+
0.1.0-alpha.38 already" while npmjs held the version staged. Every
|
|
72
|
+
call names npmjs for the scope as well now. A release script, so
|
|
73
|
+
nothing an app sees.
|
|
74
|
+
|
|
75
|
+
**What you can delete.** Nothing.
|
|
76
|
+
|
|
77
|
+
### Native verification
|
|
78
|
+
|
|
79
|
+
The by-hand round alpha.6 introduced (backlog R4), on 2026-10-06 — the
|
|
80
|
+
two commits after the alpha.38 tag: a stroke painting in its parent's
|
|
81
|
+
layer (F123) and the approval script naming npmjs for the scope — with a
|
|
82
|
+
regression pass over them first: the diff read whole, each claim probed
|
|
83
|
+
with a test before anything changed. Six probes were written; five
|
|
84
|
+
passed and are kept in `tests/layers.rs` for what F123's own tests did
|
|
85
|
+
not pin (a `polygon` and a `path` under the same rule, a departing
|
|
86
|
+
stroke's ghost in its parent's layer, a declared float with `clip` still
|
|
87
|
+
a layer, a viewport-anchored stroke still escaping its clip, a clickable
|
|
88
|
+
stroke hit at its place), and the sixth found RG120, which is in this
|
|
89
|
+
release. This round ran on the Mac alone; Windows and Linux did not run
|
|
90
|
+
it for this tag.
|
|
91
|
+
|
|
92
|
+
**macOS 27.0.1 on an M3 Pro MacBook Pro, rustc 1.99.0 (the toolchain
|
|
93
|
+
CI runs), Node 25.6.0, nu 0.116.0**, on the release commit's tree, the
|
|
94
|
+
workspace's own artifacts pruned and rebuilt. `cargo fmt --all --check`
|
|
95
|
+
and `cargo clippy --workspace --all-targets --features
|
|
96
|
+
kui-core/conformance -- -D warnings` are clean. `cargo test --workspace
|
|
97
|
+
--features kui-core/conformance`: **1816 tests over 136 suites, 0
|
|
98
|
+
failed** (4 ignored). The C round, `cbuild --run`, passes its five
|
|
99
|
+
checks; the corpus passes its **57 scenes** in four adapters; the ABI
|
|
100
|
+
is **25**. Node's `node --test test.mjs` under
|
|
101
|
+
`KUI_CONFORMANCE_REQUIRED=1`: **210 of 210**. `npm run gen` leaves no
|
|
102
|
+
diff, the examples typecheck and their lockfile installs, the headless
|
|
103
|
+
round passes all **35 drives**, the book builds and
|
|
104
|
+
`scripts/book-examples.nu --check` passes.
|
|
105
|
+
|
|
106
|
+
**The windowed round**, `smoke -- --node`, twice over: **51 Rust
|
|
107
|
+
examples and the eleven Node examples, each on both bases, 120 frames
|
|
108
|
+
each, every one exiting 0** — 124 windows, eight at a time, in
|
|
109
|
+
37.6 and 33.4 s — and `counter`, `host`, `c_panel` and `lua_panel` by
|
|
110
|
+
hand under `KUI_SMOKE_FRAMES=120`, each exiting 0 with nothing on
|
|
111
|
+
stderr: **128 windows over five hosts.** The AX audit: **106/106**, the
|
|
112
|
+
audited window raised to the front by its pid first, and no warning on
|
|
113
|
+
the fixture's stderr. F123 itself was seen in kawoosh's window before
|
|
114
|
+
the merge, built against the branch by path; RG120's case is pinned in
|
|
115
|
+
the core's tests alone.
|
|
116
|
+
|
|
117
|
+
**The bench guard** against the alpha.38 tag, on the tree the release
|
|
118
|
+
commit was cut from: **green**, none of the 8 guarded rows more than
|
|
119
|
+
10% slower — every one between −5.3% and +1.4% (the worst guarded
|
|
120
|
+
run-to-run spread 2.7%), and no row of the run more than 2.2% slower.
|
|
121
|
+
Three rows got faster by more than the noise, and that is F123:
|
|
122
|
+
`frame_10k_segments` 881 → 834 µs (−5.3%), `frame_1k_closed_lines`
|
|
123
|
+
103 → 97.1 µs (−5.9%) and `frame_1k_polygons` 102 → 97.2 µs (−4.5%) —
|
|
124
|
+
ten thousand strokes that were ten thousand layers for the float stack
|
|
125
|
+
to sort are their parents' content now. `README.md`'s table is kept as
|
|
126
|
+
it was.
|
|
127
|
+
|
|
128
|
+
## 0.1.0-alpha.38 (2026-10-05)
|
|
129
|
+
|
|
130
|
+
**What breaks.**
|
|
131
|
+
|
|
132
|
+
- C: `KuiSpec` gains `scroll_mods` at its end (64-bit size 704), so
|
|
133
|
+
`KUI_ABI_VERSION` is 25; recompile. A zeroed spec is what it was.
|
|
134
|
+
- Rust: `event::Scroll` gains a `mods` field and `EventSpec` a
|
|
135
|
+
`scroll_mods` one (under Added), so a struct literal of either needs
|
|
136
|
+
them.
|
|
137
|
+
|
|
138
|
+
The Node wire stays v21: `scrollMods` is one more string row.
|
|
139
|
+
|
|
140
|
+
### Added
|
|
141
|
+
|
|
142
|
+
- **An `onScroll` node can be for a modified wheel** (backlog F122,
|
|
143
|
+
from kawoosh). `scrollMods` — `"ctrl super"`, any of `shift`, `ctrl`,
|
|
144
|
+
`alt`, `super`; Rust `NodeSpec::scroll_mods(KeyMods)`, C
|
|
145
|
+
`KuiSpec.scroll_mods` in `KUI_KMOD_*` bits, Lua `scroll_mods` — and
|
|
146
|
+
the node hears only a scroll gesture that began with one of them
|
|
147
|
+
held, and hears it first: ahead of every scroll container and every
|
|
148
|
+
`onScroll` that names none, wherever under the pointer the gesture
|
|
149
|
+
began, the innermost such node winning. A Ctrl-wheel zoom declared
|
|
150
|
+
on the window's root is heard over a list, and the list does not
|
|
151
|
+
scroll; a canvas inside can name the same key and take it for
|
|
152
|
+
itself. A wheel with none of them held passes the node by — and
|
|
153
|
+
scrolls it, if it is a scroll container too: a list can name a key
|
|
154
|
+
for its own zoom and scroll for every other wheel (backlog RG119,
|
|
155
|
+
from this release's pre-tag pass; as merged, such a list stood still
|
|
156
|
+
for a plain wheel). Its
|
|
157
|
+
`scroll` events carry `mods` (`Scroll::mods`), the modifiers held
|
|
158
|
+
when the gesture began: a gesture stays what it began as to the end
|
|
159
|
+
of its glide, whatever is let go or pressed meanwhile.
|
|
160
|
+
|
|
161
|
+
**What you can delete.** An `onScroll` handler that read the modifiers
|
|
162
|
+
to tell a zoom from a scroll, and the scroll it then had to do itself
|
|
163
|
+
for the plain wheel — and the knowledge that it only ever worked over
|
|
164
|
+
that handler's own node, every scroll container inside it taking the
|
|
165
|
+
same wheel first.
|
|
166
|
+
|
|
167
|
+
### Fixed
|
|
168
|
+
|
|
169
|
+
- **`scripts/npm-approve.nu` no longer fails a release it has just
|
|
170
|
+
made.** Approving alpha.37 went through, and the script then waited
|
|
171
|
+
a minute for `npm view` to list the version, gave up with "approved,
|
|
172
|
+
but npmjs does not list it yet; run this again", and on the second
|
|
173
|
+
run — no stage left, the cached packument still without the
|
|
174
|
+
version — said nothing was staged and asked whether the release
|
|
175
|
+
workflow had run. `latest` was set by hand. What is live is now read
|
|
176
|
+
off the dist-tags, which a version takes the moment it is approved,
|
|
177
|
+
and `latest` follows the approval at once. A release script, so
|
|
178
|
+
nothing an app sees.
|
|
179
|
+
|
|
180
|
+
**What you can delete.** Nothing.
|
|
181
|
+
|
|
182
|
+
### Native verification
|
|
183
|
+
|
|
184
|
+
The by-hand round alpha.6 introduced (backlog R4), on 2026-10-05 — the
|
|
185
|
+
two commits after the alpha.37 tag: `scrollMods` (F122) and the
|
|
186
|
+
approval script — with a regression pass over them first: the diff
|
|
187
|
+
read whole, each claim probed with a test before anything changed.
|
|
188
|
+
RG119 came of it and is in this release. Before any of it the last
|
|
189
|
+
`check` on main was read on both hosts, green at `3d4fff6` — the step
|
|
190
|
+
alpha.37's first tag went without. This round ran on the Mac alone;
|
|
191
|
+
Windows and Linux did not run it for this tag.
|
|
192
|
+
|
|
193
|
+
**macOS 27.0.1 on an M3 Pro MacBook Pro, rustc 1.99.0 (the toolchain
|
|
194
|
+
CI runs), Node 26.10.0, nu 0.116.0**, on the release commit's tree,
|
|
195
|
+
the workspace's own artifacts pruned and rebuilt. `cargo fmt --all
|
|
196
|
+
--check` and `cargo clippy --workspace --all-targets -- -D warnings`
|
|
197
|
+
are clean. `scripts/test.nu`, the workspace's tests with the
|
|
198
|
+
conformance feature: **1808 tests over 136 suites, 0 failed** (4
|
|
199
|
+
ignored). The C round, `cbuild --run`, passes its five checks; the
|
|
200
|
+
corpus passes its **57 scenes** in four adapters, `scroll-gestures`
|
|
201
|
+
now holding a Ctrl-wheel over a contained list at its limit; the ABI
|
|
202
|
+
is **25**. Node's `node --test test.mjs` under
|
|
203
|
+
`KUI_CONFORMANCE_REQUIRED=1`: **210 of 210**. `npm run gen` leaves no
|
|
204
|
+
diff, the examples typecheck and their lockfile installs, the headless
|
|
205
|
+
round passes all **35 drives**, the book builds and
|
|
206
|
+
`scripts/book-examples.nu --check`
|
|
207
|
+
passes.
|
|
208
|
+
|
|
209
|
+
**The windowed round**, `smoke -- --node`, twice over: **51 Rust
|
|
210
|
+
examples and the eleven Node examples, each on both bases, 120 frames
|
|
211
|
+
each, every one exiting 0** — 124 windows, eight at a time, in 34 and
|
|
212
|
+
35 s — and `counter`, `host`, `c_panel` and `lua_panel` by hand under
|
|
213
|
+
`KUI_SMOKE_FRAMES=120`, each exiting 0 with nothing on stderr: **128
|
|
214
|
+
windows over five hosts.** The AX audit: **106/106**, the audited
|
|
215
|
+
window raised to the front by its pid first, and no warning on the
|
|
216
|
+
fixture's stderr. `scrollMods` itself was not driven in a window in
|
|
217
|
+
this round: it was tried in kawoosh's before the merge, and RG119's
|
|
218
|
+
case is pinned in the core's tests alone.
|
|
219
|
+
|
|
220
|
+
**The bench guard** against the alpha.37 tag, on the release commit's
|
|
221
|
+
tree: **green**, none of the 8 guarded rows more than 10% slower —
|
|
222
|
+
every one between −4.1% and +0.6% (the worst guarded run-to-run spread
|
|
223
|
+
4.3%), and no row of the run more than 5% slower. `README.md`'s table
|
|
224
|
+
is kept as it was.
|
|
225
|
+
|
|
24
226
|
## 0.1.0-alpha.37 (2026-10-05)
|
|
25
227
|
|
|
26
228
|
**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.
|
|
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
|
|
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):
|
|
@@ -186,3 +186,11 @@ date: 2026-09-28
|
|
|
186
186
|
the C door, and by the corpus's `scroll-gestures` scene in four
|
|
187
187
|
adapters. The scene holds a contained list at its limit and a y-only
|
|
188
188
|
handler met by a sideways notch.
|
|
189
|
+
- *Amended (F122, RG119):* a handler that names modifiers
|
|
190
|
+
(`scroll_mods`) is asked before this walk, of the modifiers the
|
|
191
|
+
gesture began with, the innermost such handler taking it whatever
|
|
192
|
+
room or `contain` the regions inside it have. To a gesture it does
|
|
193
|
+
not hear it is what it would be with no `on_scroll`: the container
|
|
194
|
+
it may also be, asked like any other, and otherwise passed. The
|
|
195
|
+
latch keeps the modifiers with the targets. Pinned by
|
|
196
|
+
`tests/scroll_mods.rs`.
|
|
@@ -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
|
|
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
|
|
@@ -775,6 +778,31 @@ rows.
|
|
|
775
778
|
[`transition` row](props.md#container-props) ·
|
|
776
779
|
[alpha.17](CHANGELOG.md#010-alpha17-2026-09-25)
|
|
777
780
|
|
|
781
|
+
### How do I zoom with Ctrl or ⌘ and the wheel?
|
|
782
|
+
|
|
783
|
+
Put an `onScroll` on the node that zooms and say which keys it is for:
|
|
784
|
+
`<box onScroll={{ kind: 'zoom' }} scrollMods="ctrl super">` (Rust
|
|
785
|
+
`.on_scroll(tag).scroll_mods(KeyMods::NONE.with_ctrl().with_super())`,
|
|
786
|
+
C `.scroll_mods = KUI_KMOD_CTRL | KUI_KMOD_SUPER`, Lua `scroll_mods =
|
|
787
|
+
"ctrl super"`). A wheel turned with one of them held is then that
|
|
788
|
+
node's `scroll` event wherever under the pointer it began — over a
|
|
789
|
+
list inside it, and the list does not move — and a plain wheel passes
|
|
790
|
+
the node by, so the lists go on scrolling — the node itself too, if it
|
|
791
|
+
is the list: a scroll container that names a key for its own zoom
|
|
792
|
+
scrolls for every other wheel. On the window's root it is
|
|
793
|
+
the whole window's zoom; a canvas inside can name the same key and take
|
|
794
|
+
the gesture for itself, the innermost winning. Positive `dy` is the
|
|
795
|
+
wheel rolling up, "bigger". A mouse's notch is 40 px and a trackpad's
|
|
796
|
+
swipe comes in small pixel steps, so add `dy` up and step when the sum
|
|
797
|
+
crosses your notch rather than once an event. The event's `mods` are
|
|
798
|
+
the keys held when the gesture began: a swipe begun with the key held
|
|
799
|
+
stays yours to the end of its glide even if the key was let go, and
|
|
800
|
+
one begun without it never becomes yours, so there is nothing to latch
|
|
801
|
+
yourself. Without the row an `onScroll` cannot do this: every scroll
|
|
802
|
+
container inside it takes the wheel first.
|
|
803
|
+
|
|
804
|
+
[`scrollMods` row](props.md#container-props)
|
|
805
|
+
|
|
778
806
|
### Why does a swipe down not move the strip sideways?
|
|
779
807
|
|
|
780
808
|
A trackpad swipe keeps to the axis it started on, so nothing is yours
|
package/index.d.ts
CHANGED
|
@@ -172,6 +172,9 @@ export type ScrollMsg<T = AppMsg> = {
|
|
|
172
172
|
dx: number;
|
|
173
173
|
dy: number;
|
|
174
174
|
lines: number | null;
|
|
175
|
+
/** On a node that names `scrollMods`: the modifiers held when the
|
|
176
|
+
* gesture began. */
|
|
177
|
+
mods?: { shift: boolean; ctrl: boolean; alt: boolean; super: boolean };
|
|
175
178
|
tag?: T;
|
|
176
179
|
};
|
|
177
180
|
|
package/jsx-runtime.d.ts
CHANGED
|
@@ -393,6 +393,8 @@ export interface GeneratedSpecProps {
|
|
|
393
393
|
rules?: ColorProp;
|
|
394
394
|
/** Which axes `onScroll` takes (backlog F107): `both` (the default), `x` or `y`. A scroll gesture on an axis the node does not take passes it by, to the scroller around it, and hears nothing here: a terminal that scrolls its history says `y`, and a sideways swipe that starts over it moves the strip it sits in. (A swipe that started elsewhere is not the node's either way: a gesture keeps the target it started with.) Meaningless without `onScroll`. */
|
|
395
395
|
scrollAxes?: 'both' | 'x' | 'y';
|
|
396
|
+
/** The modifiers `onScroll` is for (backlog F122): `"shift"`, `"ctrl"`, `"alt"` and `"super"` (⌘, the Windows key), separated by spaces or commas — `"ctrl super"`. With any named, the node hears only a scroll gesture that began with one of them held, and hears it first: ahead of every scroll container and every `onScroll` that names none, wherever under the pointer the gesture began, the innermost such node winning — so a Ctrl-wheel zoom declared on the window's root is heard over a list, and the list does not scroll. A wheel with none of them held passes the node by, as if it had no `onScroll`: a node that scrolls as well (`overflow`) scrolls for it as any container does. Its `scroll` events carry `mods`, the modifiers held when the gesture began; the gesture stays the node's to the end of its glide, whatever is let go meanwhile, and one begun without them never becomes its. A word that is none of the four is skipped. Unset, a handler like any other. Meaningless without `onScroll`. */
|
|
397
|
+
scrollMods?: string;
|
|
396
398
|
/** When a scrolling node draws its bars: `visible` (the default — the stock overlay thumb, drawn while the content overflows), `hidden` (no thumb, no track to press; the wheel, the keyboard, `reveal` and the caret still scroll it — for a list that draws its own indicator, or a pane whose bar would sit on a border), or `auto` (shown while the scroll state is changing — the offset or the content's extent moved, the pointer is on the track, a thumb is dragged — and for a second after, then faded out over a quarter of one; a node first seen shows it the same second; what an overlay bar does on macOS). `auto` needs the driver's clock and is `visible` without one. The bars are overlays and take no layout space in any mode. From the last change until it has faded — a second and a quarter — an `auto` bar asks for frames the way a transition of that length would (nothing else could wake the core when the hold ends); while the pointer holds it, it asks for none. */
|
|
397
399
|
scrollbar?: 'visible' | 'hidden' | 'auto';
|
|
398
400
|
/** The thumb under the pointer or while dragged; the default is the theme's `scrollbar_active` role. */
|
|
@@ -898,7 +900,9 @@ export declare namespace JSX {
|
|
|
898
900
|
* Placed as a `line` is: always a float in its parent's box space
|
|
899
901
|
* (`float="viewport"` for viewport space), sized to its own bounding
|
|
900
902
|
* box two pixels out on each side, so it takes no room in a row or
|
|
901
|
-
* column
|
|
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.
|
|
902
906
|
* Hit by its outline under the fill rule
|
|
903
907
|
* (docs/adr/0026-hit-testing-by-shape.md): with `onClick`, `onDrag`,
|
|
904
908
|
* `onHover` or `hoverable`, a press inside hits it and one in its box
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/props.md
CHANGED
|
@@ -86,6 +86,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
|
|
|
86
86
|
| `ruleWidth` | `rule_width` | `rule_w` | number, or a `"$length"` token | The width of a table's `rules` in logical px; 1 when unset. |
|
|
87
87
|
| `rules` | `rules` | `rules` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | On a table (`dir="table"`, ADR 0033): grid lines of this colour between its columns and between its rows (backlog DX21) — down the middle of each gap between the columns of its widest row, from the first row's top to the last row's bottom, and across the middle of each gap between rows, the content box wide. Drawn with the table's box, under its cells and on whole pixels, so give the table and its rows a `gap` at least `ruleWidth` for the lines to show between cells; the outer edge is the table's `border`. Ignored on anything but a table. |
|
|
88
88
|
| `scrollAxes` | `scroll_axes` | `scroll_axes` (`KUI_SCROLL_AXES_*`; zeroed, both) | `both` \\| `x` \\| `y` | Which axes `onScroll` takes (backlog F107): `both` (the default), `x` or `y`. A scroll gesture on an axis the node does not take passes it by, to the scroller around it, and hears nothing here: a terminal that scrolls its history says `y`, and a sideways swipe that starts over it moves the strip it sits in. (A swipe that started elsewhere is not the node's either way: a gesture keeps the target it started with.) Meaningless without `onScroll`. |
|
|
89
|
+
| `scrollMods` | `scroll_mods` | `scroll_mods` (`KUI_KMOD_*` bits; zeroed, none) | string | The modifiers `onScroll` is for (backlog F122): `"shift"`, `"ctrl"`, `"alt"` and `"super"` (⌘, the Windows key), separated by spaces or commas — `"ctrl super"`. With any named, the node hears only a scroll gesture that began with one of them held, and hears it first: ahead of every scroll container and every `onScroll` that names none, wherever under the pointer the gesture began, the innermost such node winning — so a Ctrl-wheel zoom declared on the window's root is heard over a list, and the list does not scroll. A wheel with none of them held passes the node by, as if it had no `onScroll`: a node that scrolls as well (`overflow`) scrolls for it as any container does. Its `scroll` events carry `mods`, the modifiers held when the gesture began; the gesture stays the node's to the end of its glide, whatever is let go meanwhile, and one begun without them never becomes its. A word that is none of the four is skipped. Unset, a handler like any other. Meaningless without `onScroll`. |
|
|
89
90
|
| `scrollbar` | `scrollbar` | `scrollbar` | `visible` \\| `hidden` \\| `auto` | When a scrolling node draws its bars: `visible` (the default — the stock overlay thumb, drawn while the content overflows), `hidden` (no thumb, no track to press; the wheel, the keyboard, `reveal` and the caret still scroll it — for a list that draws its own indicator, or a pane whose bar would sit on a border), or `auto` (shown while the scroll state is changing — the offset or the content's extent moved, the pointer is on the track, a thumb is dragged — and for a second after, then faded out over a quarter of one; a node first seen shows it the same second; what an overlay bar does on macOS). `auto` needs the driver's clock and is `visible` without one. The bars are overlays and take no layout space in any mode. From the last change until it has faded — a second and a quarter — an `auto` bar asks for frames the way a transition of that length would (nothing else could wake the core when the hold ends); while the pointer holds it, it asks for none. |
|
|
90
91
|
| `scrollbarActiveColor` | `scrollbar_active_color` | `scrollbar_active_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | The thumb under the pointer or while dragged; the default is the theme's `scrollbar_active` role. |
|
|
91
92
|
| `scrollbarColor` | `scrollbar_color` | `scrollbar_color` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | The thumb at rest; the default is the theme's `scrollbar` role, a translucent wash over whatever it sits on. |
|
|
@@ -163,11 +164,11 @@ where they make sense); text props apply to `<text>` and `<edit>`.
|
|
|
163
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. |
|
|
164
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. |
|
|
165
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. |
|
|
166
|
-
| `<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. |
|
|
167
|
-
| `<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. |
|
|
168
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. |
|
|
169
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. |
|
|
170
|
-
| `<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. |
|
|
171
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. |
|
|
172
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. |
|
|
173
174
|
| `<windowButtons/>` | `window_buttons()` | `kui_window_buttons` | Just the min/max/close buttons, for fully custom titlebars. |
|
|
@@ -196,7 +197,7 @@ one field. The payload shapes:
|
|
|
196
197
|
| menu | `{ kind: "menu", role, item }` | A row of the core's own context menu was chosen (`openMenu` / `open_menu` / `kui_open_menu`), on the node the menu was about. `item` is the row's `id`, or its label when it declared none; `role` is the row's standard role or `custom`. Every chosen row posts, the standard ones included: a `cut` or `paste` role is carried out by the core (its clipboard work queued for the host) *and* reported, so an app can hear its editor being cut from and is free to ignore it (`docs/adr/0017-selection-as-a-scope.md`, decision 5). |
|
|
197
198
|
| forceclick | `{ kind: "forceclick", x, y, tag }` | A press that deepened past the second stage of a Force Touch trackpad, on an `onForceClick` node, at the logical viewport point it happened at. Routed as a secondary press is — no focus moved, no caret placed, no click — but asked of the topmost node only, and the ordinary click the press is still producing arrives afterwards. Text needs none of this: over an `edit` or a `selectable` scope the core selects the word under it and asks the host for its Look Up panel instead. macOS-only in practice. |
|
|
198
199
|
| button | `{ kind: "button", phase: "press" \| "move" \| "release", button: "secondary" \| "middle" \| number, x, y, clicks, cell?: { row, col }, line?, byte?, tag }` | A non-primary button on an `onButton` node that claims it (`buttons`), backlog F105: `press` where it went down — with the driver's click count, `clicks`, which the native runner keeps for the primary button alone and so always reports as 1 here — then `move` for every pointer move while it is held and `release` where it came up, both on the same node wherever the pointer went, since the press captured the button. `button` is the button's name, or for one past the middle button its number (`3 + n`, as `kui_input_mouse_button` takes it; Node's `ctx.mouse` takes the three names only); `x`/`y` are logical viewport coordinates. On a `cells` grid each carries `cell: {row, col}`, clamped to the grid, and inside an `onKey` sink that draws `role="line"` rows `line` and `byte` as a drag does. A claimed secondary press is this event instead of `contextmenu`; the press moves no focus, caret, selection or scrollbar. |
|
|
199
|
-
| scroll | `{ kind: "scroll", x, y, dx, dy, lines, tag }` | The wheel over an `onScroll` node, or a drag-select held past a `cells` grid's top or bottom edge: `dx`/`dy` the delta in logical px as the driver reported it (positive `dy` is the wheel rolling up, toward earlier content), `x`/`y` the pointer in logical viewport coordinates, `lines` the whole lines a `cells` grid's `dy` covers — positive is later history, the sign `originLine` grows in, the fraction carried to the next notch — and null on any other node. The core scrolls nothing for it: the app re-declares the grid's `originLine`, or zooms its canvas. From the edge drag it comes once a frame while the pointer is held past the edge, with the lines that frame's step covers, and the selection's absolute lines survive the scroll the app answers with (`docs/adr/0029-a-selection-follows-the-pointer-past-the-edge.md`). |
|
|
200
|
+
| scroll | `{ kind: "scroll", x, y, dx, dy, lines, mods?: { shift, ctrl, alt, super }, tag }` | The wheel over an `onScroll` node, or a drag-select held past a `cells` grid's top or bottom edge: `dx`/`dy` the delta in logical px as the driver reported it (positive `dy` is the wheel rolling up, toward earlier content), `x`/`y` the pointer in logical viewport coordinates, `lines` the whole lines a `cells` grid's `dy` covers — positive is later history, the sign `originLine` grows in, the fraction carried to the next notch — and null on any other node. The core scrolls nothing for it: the app re-declares the grid's `originLine`, or zooms its canvas. From the edge drag it comes once a frame while the pointer is held past the edge, with the lines that frame's step covers, and the selection's absolute lines survive the scroll the app answers with (`docs/adr/0029-a-selection-follows-the-pointer-past-the-edge.md`). |
|
|
200
201
|
| focus | `{ kind: "focus", phase: "in" \| "out", by: "pointer" \| "keyboard" \| "assistive" \| "program", tag }` | Keyboard focus entered or left an `onFocus` node's subtree (backlog DX18). `by` is what moved it — a press, a key, a screen reader's request, or the view and the app — so a pane that follows a click into its sink tells that apart from a move the app made itself. |
|
|
201
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. |
|
|
202
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. |
|