@qxuken/kui 0.1.0-alpha.44 → 0.1.0-alpha.46
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 +275 -0
- package/docs/adr/0041-a-mask-turns-about-its-centre.md +3 -1
- package/docs/adr/0043-a-node-turns-about-its-pivot.md +370 -0
- package/encoder.js +5 -1
- package/howto.md +143 -4
- package/index.d.ts +159 -4
- package/index.js +12 -1
- package/jsx-runtime.d.ts +26 -4
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/kui_node.node +0 -0
- package/prebuilds/linux-arm64/kui_node.node +0 -0
- package/prebuilds/linux-x64/kui_node.node +0 -0
- package/prebuilds/win32-x64/kui_node.node +0 -0
- package/props.md +21 -5
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,281 @@ 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.46 (2026-10-09)
|
|
25
|
+
|
|
26
|
+
**What breaks.**
|
|
27
|
+
|
|
28
|
+
- C ABI 27 (under Added, F131): `KuiClip` grows from eight words to
|
|
29
|
+
twenty — `transform`, `inner` and `inner_radius` — so a host that
|
|
30
|
+
strides `KuiDrawData.clips` reads the new stride and the conformance
|
|
31
|
+
digest hashes the whole entry; `KuiSpec` appends `rotate`, `scale`,
|
|
32
|
+
`pivot_set`, `pivot_x`, `pivot_y` and `iterations` (F133; 64-bit size
|
|
33
|
+
744), `KuiKeyframe` and `KuiEnter` append `rotate` and `scale`, and
|
|
34
|
+
`KuiKeyframe` `dx` and `dy` with `KUI_KF_OFFSET` (strides 52 and 48;
|
|
35
|
+
F132). `KuiSpec` holds `KuiEnter` twice by value, as `enter` and
|
|
36
|
+
`exit`, so its fields after `enter` move by 8 bytes and those after
|
|
37
|
+
`exit` by 16: recompile, and re-derive the offsets of a hand-written
|
|
38
|
+
mirror (ctypes, Zig, an Odin of its own) rather than append to it. A
|
|
39
|
+
zeroed field is what every spec, stop and entrance had before. Node's
|
|
40
|
+
`clips()` buffer has the same stride, and `decodeClips` reads
|
|
41
|
+
`transform`, `inner` and `innerRadii`. A renderer of its own that
|
|
42
|
+
ignores the new words draws a turned subtree upright.
|
|
43
|
+
- Rust struct literals: none of these is `#[non_exhaustive]`, so a
|
|
44
|
+
literal that names every field gains the new ones or a `..` base —
|
|
45
|
+
`Clip` (`transform`, `inner`, `inner_radius`; it has no `Default`,
|
|
46
|
+
so `..Clip::NONE`), `Keyframe` (`dx`, `dy`), `Slots` and `Enter`
|
|
47
|
+
(`rotate`, `scale`), `AnimSpec::iterations`,
|
|
48
|
+
`InteractSpec::transform`, `NodeInfo` (`rotate`, `scale`),
|
|
49
|
+
`HitRegion::turn`, `ScrollRegion::turn` and `EnvFacts::now`.
|
|
50
|
+
- Node's `WarningCode` union gains `'ambiguous-name'` (F137): a
|
|
51
|
+
`switch` over it that TypeScript checks for exhaustiveness names it.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **A node turns about its pivot** (backlog F131, from berainder's
|
|
56
|
+
review; [ADR 0043](docs/adr/0043-a-node-turns-about-its-pivot.md)).
|
|
57
|
+
`rotate` (turns, clockwise), `scale` (a uniform factor) and
|
|
58
|
+
`pivotX` / `pivotY` (fractions of the box, the centre by default) on
|
|
59
|
+
any node — `NodeSpec::rotate` / `scale` / `pivot` in Rust, the rows in
|
|
60
|
+
JSX and Lua, `KuiSpec.rotate` / `scale` / `pivot_set` with
|
|
61
|
+
`KUI_PIVOT_X` / `KUI_PIVOT_Y` in C (ABI 27), the generated `Spec`
|
|
62
|
+
fields in Odin. Paint-only: the node takes the room its upright self
|
|
63
|
+
takes and `onLayout` reports the layout rect; everything the subtree
|
|
64
|
+
draws turns with it — backgrounds, borders, shadows, text, images,
|
|
65
|
+
strokes, fragments — and so does what it clips, so a photo stays
|
|
66
|
+
inside a tilted card's rounded corners. Hit where drawn: a tilted
|
|
67
|
+
card is grabbed on its tilted edge, a press in its box past its edge
|
|
68
|
+
falls through, a scroller inside a turn takes the wheel where it is
|
|
69
|
+
drawn; the access rect is the bounding box. Turns nest by
|
|
70
|
+
composition; a float anchored to its parent turns with it, a viewport
|
|
71
|
+
float does not. A turn and a scale are one slot that tweens with
|
|
72
|
+
`transition` — a card follows the pointer while a drag holds the
|
|
73
|
+
transition off and springs back when it is on — and an entrance, an
|
|
74
|
+
exit and a keyframe stop name `rotate` and `scale` (`enter: { scale:
|
|
75
|
+
0.8 }`, `keyframes: [{ rotate: 0 }, { rotate: 1 }]` spins a box); a
|
|
76
|
+
departing subtree keeps its turn. On the wire the clip entry carries
|
|
77
|
+
the space (`Clip::transform`, `inner`, `inner_radius`); the quad does
|
|
78
|
+
not change. A `path`'s own `rotate` keeps ADR 0041's meaning and
|
|
79
|
+
composes under the node's. The `transform` example.
|
|
80
|
+
- `Transform`, the similarity a clip entry carries, with `about`,
|
|
81
|
+
`apply`, `unapply`, `then`, `bounds`; `Clip::turned`, `turned_by`,
|
|
82
|
+
`visible`, `shown`; `HitTurn` on a hit region and a scroll region;
|
|
83
|
+
`NodeInfo::rotate` / `scale` in the devtools' facts (F131).
|
|
84
|
+
- **A keyframe stop names a position** (backlog F132, from berainder's
|
|
85
|
+
review). `dx` / `dy` on a stop — `Keyframe::dx` / `dy` / `offset` in
|
|
86
|
+
Rust, the stop's fields in JSX and Lua, `KuiKeyframe.dx` / `dy` with
|
|
87
|
+
`KUI_KF_OFFSET` in C (ABI 27), `Keyframe.dx` / `dy` with `.Offset` in
|
|
88
|
+
Odin — are logical px from where layout put the node, as an entrance's
|
|
89
|
+
are: the node and its subtree are drawn, hit and read by assistive
|
|
90
|
+
technology that far away at the stop, a lane a stop leaves out is 0,
|
|
91
|
+
and the offset adds to a `slide`'s. `[{ dy: 0 }, { dy: -6 }]` with
|
|
92
|
+
`repeat: 'alternate'` bobs a box; three stops drift a sparkle up as it
|
|
93
|
+
fades. Paint, hit and access only: the room the node takes is its
|
|
94
|
+
place's, and an `onLayout` node reports its layout rect, not a cycle
|
|
95
|
+
that would post an event every frame it ran. The `transition` example
|
|
96
|
+
has a bob and a sparkle.
|
|
97
|
+
- **A keyframe cycle plays so many times** (backlog F133, from
|
|
98
|
+
berainder's review). `iterations` — CSS's `animation-iteration-count`:
|
|
99
|
+
`NodeSpec::iterations` in Rust, the row in JSX and Lua, `KuiSpec.iterations`
|
|
100
|
+
in C (0 is for ever; ABI 27), the generated `Spec.iterations` in Odin.
|
|
101
|
+
Left out, a cycle runs for ever and reads the clock as it always did,
|
|
102
|
+
so siblings stay in phase. A finite one plays from the first frame its
|
|
103
|
+
node is declared with it, holds its first stop through its `delay`,
|
|
104
|
+
and rests where its last iteration ended (CSS's fill `both`); then it
|
|
105
|
+
owes no frame, so `animating()`, `owed()` and the frame trace go quiet
|
|
106
|
+
as a settled transition's do. `1` plays a burst or a shake once, `2`
|
|
107
|
+
alternating goes out and back, `0.5` stops halfway; `delay` plus
|
|
108
|
+
`iterations: 1` staggers one-shots. A node that leaves and comes back
|
|
109
|
+
plays again, and so does one whose stops the view drops for a frame
|
|
110
|
+
and declares again: that is how a view replays it.
|
|
111
|
+
- **The view reads the frame clock** (backlog F134). `Ui::now` /
|
|
112
|
+
`Core::now` in Rust, `env.now` in Lua, `kui_now` in C, `now` in Odin,
|
|
113
|
+
`ctx.now()` in Node: the driver's seconds that `transition` and
|
|
114
|
+
`keyframes` read this frame, 0 before a driver sets one. A deadline
|
|
115
|
+
read off it — a toast's expiry, a sequence's beats — agrees with the
|
|
116
|
+
core's easing, and a test's `advance` moves both.
|
|
117
|
+
- **A frame asked for at a time** (backlog F135). `Ui::request_frame_at`
|
|
118
|
+
/ `Core::request_frame_at` and `Core::next_frame_at` in Rust,
|
|
119
|
+
`env.request_frame_at` in Lua, `kui_request_frame_at` /
|
|
120
|
+
`kui_next_frame_at` in C (`INFINITY` for none), the same doors in Odin,
|
|
121
|
+
`ctx.requestFrameAt` / `nextFrameAt` in Node; `Waker::wake_at(Instant)`
|
|
122
|
+
for a thread. On the frame clock: the runner sleeps to the earliest
|
|
123
|
+
time asked for and runs the view, with nothing owed until then, so
|
|
124
|
+
`animating()` stays false. A time already past is a frame now; the
|
|
125
|
+
time is kept until a frame reaches it.
|
|
126
|
+
- **The exit named at the removal** (backlog F136). `Ui::exit_with` /
|
|
127
|
+
`Core::set_exit` in Rust, `env.exit_with` in Lua, `kui_exit_with` in
|
|
128
|
+
C, `exit_with` in Odin, `ctx.exitWith` in Node: the exit a node leaves
|
|
129
|
+
by if it leaves in the frame that finishes next, over the one its
|
|
130
|
+
last frame declared, so the handler that removes a card says which way
|
|
131
|
+
it goes. It lapses when that frame finishes; it aims a node that
|
|
132
|
+
declares an `exit`, with that node's `transition`.
|
|
133
|
+
- **A lookup by accessible name** (backlog F137). `Core::key_named` /
|
|
134
|
+
`Drive::key_named` in Rust, `kui_key_named` in C, `key_named` in Odin,
|
|
135
|
+
`ctx.keyNamed` in Node: the first node in the last frame whose
|
|
136
|
+
accessible name — its `label`, else a control's text — is the one
|
|
137
|
+
asked for, so a test presses "the button named Like" as a reader would.
|
|
138
|
+
Two with one name raise the new `ambiguous-name` warning. The docs now
|
|
139
|
+
say "key label" for what `key_of` and `texts_under` read, the name the
|
|
140
|
+
view opened a node under, and "accessible name" for the `label` row.
|
|
141
|
+
- **The runner decodes images, GIFs included** (backlog F138).
|
|
142
|
+
`kui_native::decode_image(bytes)` turns PNG, JPEG, WebP or GIF bytes
|
|
143
|
+
into straight RGBA and a size, the shape `add_image` takes;
|
|
144
|
+
`decode_animation` keeps every frame of an animated GIF, APNG or WebP,
|
|
145
|
+
composited to the whole canvas, with its delay and the loop count, and
|
|
146
|
+
`Animation::at(elapsed)` says which frame shows and when the next is
|
|
147
|
+
due, so a GIF plays on the frame clock through `update_image_with` and
|
|
148
|
+
`request_frame_at` with nothing owed between steps.
|
|
149
|
+
`Launcher::icon_bytes` takes an icon from a PNG
|
|
150
|
+
(`Launcher::try_icon_bytes` for bytes from outside the program). C: `kui_decode_image`,
|
|
151
|
+
`kui_decode_animation`, `kui_animation_at` and `kui_pixels_free`, with
|
|
152
|
+
`runner`; Odin: `decode_image`, `decode_animation`, `animation_at`,
|
|
153
|
+
copied into the context allocator; Node: `decodeImage`,
|
|
154
|
+
`decodeAnimation` and `animationAt` on the package. The runner's
|
|
155
|
+
`image` gains its `gif` feature. The `decode` example plays a shipped
|
|
156
|
+
spinner.
|
|
157
|
+
|
|
158
|
+
**What you can delete.**
|
|
159
|
+
|
|
160
|
+
- A width and a height tweened against each other to fake a card's
|
|
161
|
+
tilt, and a `path` drawn in a box's place so that it could turn
|
|
162
|
+
(F131).
|
|
163
|
+
- A float declared where a drifting thing ends, entering from where it
|
|
164
|
+
starts over a transition as long as its life, to move it along a path
|
|
165
|
+
(F132): its stops say the path.
|
|
166
|
+
- A clock of the app's own and `request_frame` on every frame to time a
|
|
167
|
+
sequence that plays once — a burst, a pop, a row of stars — and the
|
|
168
|
+
frame owed for good by a cycle the app meant to stop (F133).
|
|
169
|
+
- A clock of the app's own, beside the core's, that a test had to push
|
|
170
|
+
forward by hand (F134): read `now()`.
|
|
171
|
+
- A thread per deadline that sleeps and then calls `Waker::wake` (F135):
|
|
172
|
+
`request_frame_at`, or `Waker::wake_at` from off the view.
|
|
173
|
+
- A frame drawn with the exit aimed before the frame that removes the
|
|
174
|
+
card, and the model field and `request_frame` that schedule it
|
|
175
|
+
(F136): `exit_with` in the handler.
|
|
176
|
+
- The `image` dependency an app added to decode its photos, its version
|
|
177
|
+
pin and the `[profile.dev.package."*"]` line that made it fast, and a
|
|
178
|
+
GIF crate beside it (F138): `decode_image` and `decode_animation`.
|
|
179
|
+
|
|
180
|
+
### Native verification
|
|
181
|
+
|
|
182
|
+
The by-hand round alpha.6 introduced (backlog R4), on 2026-10-09, over
|
|
183
|
+
F131–F138 from the berainder review, with alpha.46's pre-tag pass over
|
|
184
|
+
it on the Mac: the mechanical round as CI runs it, the windowed round,
|
|
185
|
+
the accessibility audit and the bench guard, then three read-only
|
|
186
|
+
reviewers of the diff since alpha.45 — the core and the renderer, the
|
|
187
|
+
bindings, the docs — each claim probed. What they found was corrected
|
|
188
|
+
before the tag (the backlog's alpha.46 pre-tag section lists it), and
|
|
189
|
+
one entry, RG155 — the caret inside a turned node — is open.
|
|
190
|
+
|
|
191
|
+
**macOS**, the pre-tag pass. fmt and clippy are clean; `nu
|
|
192
|
+
scripts/test.nu`: **1997 tests over 149 suites**, 0 failed. The C round
|
|
193
|
+
passes (5 checks), and so do the **58 scenes** through Rust, Lua, C and
|
|
194
|
+
Node; Node's tests under `KUI_CONFORMANCE_REQUIRED=1`, **224 of 224**;
|
|
195
|
+
`npm run gen` with no diff, the examples' typecheck, the headless round,
|
|
196
|
+
the book, and `cargo audit` over the lockfile the decoder grew. The
|
|
197
|
+
windowed round with Node's: **55 examples on both bases**, clean on a
|
|
198
|
+
first run, twice. The accessibility audit: **106 of 106**. The bench
|
|
199
|
+
guard against the alpha.45 tag: **green**, the eight guarded rows −0.9%
|
|
200
|
+
to +0.9%; the unguarded exit rows read +2% to +4%, the 16 bytes an
|
|
201
|
+
entrance and an exit grew by. The Odin steps did not run: this
|
|
202
|
+
machine's Odin links an LLVM that is gone, so the hand-written Odin
|
|
203
|
+
layer this round needed is checked by CI's `odin.nu` steps on the tag,
|
|
204
|
+
and its layout was compared by script against the C side's asserts. No
|
|
205
|
+
Windows or Linux machine ran this round.
|
|
206
|
+
|
|
207
|
+
## 0.1.0-alpha.45 (2026-10-08)
|
|
208
|
+
|
|
209
|
+
**What breaks.**
|
|
210
|
+
|
|
211
|
+
- A menu row whose accelerator would leave its label under 48 px — a
|
|
212
|
+
window narrower than the accelerator with its gaps — draws the
|
|
213
|
+
accelerator cut short with "…" and the label's first glyphs, where it
|
|
214
|
+
drew the whole accelerator past the panel's edge and no label (under
|
|
215
|
+
Fixed, RG154).
|
|
216
|
+
- A `menu_panel` floating in the viewport whose caller declared its
|
|
217
|
+
ceiling as a size expression (`max_width(Bound::Calc(..))`, `"50%"`)
|
|
218
|
+
bounds its labels by that ceiling read against the window, where it
|
|
219
|
+
bounded them by the window alone (under Fixed, RG154). A panel
|
|
220
|
+
anchored elsewhere keeps the window as its labels' bound.
|
|
221
|
+
- A menu bar the app leaves out of a frame while a submenu switch waits
|
|
222
|
+
its 0.3 s starts the wait again when the bar is back, where the switch
|
|
223
|
+
went through at once on the next build (under Fixed, RG154).
|
|
224
|
+
- Windows and Plasma: a `Blur` or `Tinted` window on a desktop with no
|
|
225
|
+
wallpaper kui can read is `Opaque` from its first frame again, as it
|
|
226
|
+
was in alpha.43; alpha.44's `Tinted`-then-`Opaque` is GNOME's alone
|
|
227
|
+
now (under Fixed, RG154).
|
|
228
|
+
|
|
229
|
+
### Fixed
|
|
230
|
+
|
|
231
|
+
- **The accelerator is bounded too** (backlog RG154, from the alpha.44
|
|
232
|
+
pre-tag pass). A row's label was what the panel's width bounded, and
|
|
233
|
+
its accelerator was drawn whole: in a window narrower than the
|
|
234
|
+
accelerator the label shrank to nothing and the accelerator ran past
|
|
235
|
+
the panel, which is what RG150 set out to stop. The label keeps a
|
|
236
|
+
floor of 48 px (`widgets::MENU_LABEL_MIN`) and the accelerator takes
|
|
237
|
+
what that leaves, ending in "…". And a ceiling declared as a size
|
|
238
|
+
expression on a panel floating in the viewport now caps the labels as
|
|
239
|
+
a px one does, read against the window; a panel anchored to a node
|
|
240
|
+
keeps the window's ceiling for its labels, since the room layout will
|
|
241
|
+
read the expression against is not placed when the rows are built.
|
|
242
|
+
- **A menu bar the app stops drawing owes no frames** (backlog RG154).
|
|
243
|
+
A submenu switch waiting its 0.3 s was cleared by the next build of the
|
|
244
|
+
bar's menu; an app that stopped drawing the bar inside the wait (or
|
|
245
|
+
handed it to the platform) kept the switch, and the frames it owed,
|
|
246
|
+
for good. A frame that builds no menu on a surface drops the switch
|
|
247
|
+
waiting there.
|
|
248
|
+
- **Windows and Plasma: no wallpaper, opaque from the first frame**
|
|
249
|
+
(backlog RG154). RG150 moved finding the wallpaper's path to the
|
|
250
|
+
loader's thread for GNOME's sake, where it is up to three `gsettings`
|
|
251
|
+
processes; on Windows it is one call and on Plasma one file read, so
|
|
252
|
+
the loop asks there and a window with none to draw never reads
|
|
253
|
+
`Tinted` first. GNOME keeps the thread. Windows run on screen;
|
|
254
|
+
Plasma compiled and read.
|
|
255
|
+
|
|
256
|
+
**What you can delete.**
|
|
257
|
+
|
|
258
|
+
- A shorter accelerator chosen for a menu that has to fit a narrow
|
|
259
|
+
window (RG154).
|
|
260
|
+
- A frame an app requested itself after stopping its menu bar, to let
|
|
261
|
+
the core settle (RG154).
|
|
262
|
+
|
|
263
|
+
### Native verification
|
|
264
|
+
|
|
265
|
+
The by-hand round alpha.6 introduced (backlog R4), on 2026-10-08, over
|
|
266
|
+
RG154 — what alpha.44's pre-tag pass left — with alpha.45's pre-tag pass
|
|
267
|
+
over it on the Windows machine and under WSLg: the mechanical round on
|
|
268
|
+
both, then a read-only review of the diff since alpha.44, each claim
|
|
269
|
+
probed. It filed nothing; its one finding, a `Calc` ceiling read against
|
|
270
|
+
the window for a panel anchored elsewhere, was corrected inside RG154
|
|
271
|
+
before the tag.
|
|
272
|
+
|
|
273
|
+
**Windows**, the pre-tag pass. fmt and clippy are clean; `nu
|
|
274
|
+
scripts/test.nu --node`: **2148 tests over 143 suites**, 0 failed. The
|
|
275
|
+
C round passes (6 checks), and so do the **57 scenes** through Rust,
|
|
276
|
+
Lua, C, Node and Odin; the Odin binding's four steps with CI's pinned
|
|
277
|
+
`dev-2026-09`; Node's tests under `KUI_CONFORMANCE_REQUIRED=1` (**218 of
|
|
278
|
+
219**, the one skip Windows'), `npm run gen` with no diff, the
|
|
279
|
+
examples' typecheck and the headless round. The windowed round with
|
|
280
|
+
Node's: **53 examples on both bases**, clean on a first run; `counter`
|
|
281
|
+
and `host` opened by hand after `cbuild`, and the backdrop examples
|
|
282
|
+
opened with and without `KUI_BACKDROP_EMULATE`. The bench guard against
|
|
283
|
+
the alpha.44 tag, on a quiet machine: **green**, the eight guarded rows
|
|
284
|
+
−0.7% to +2.1% (`frame_10k_rects_with_access_tree` the high one), the
|
|
285
|
+
worst run-to-run spread 3.5%. A first run beside the WSL round read two
|
|
286
|
+
rows unreadable at 13 and 21% spread, which is why it was run again.
|
|
287
|
+
|
|
288
|
+
**Linux**, under WSLg (llvmpipe), the same commit: fmt and clippy
|
|
289
|
+
clean; `cargo test --workspace`: **1926 tests over 142 suites**, 0
|
|
290
|
+
failed; the C round (5 checks) and the 57 scenes through every adapter,
|
|
291
|
+
Node **219 of 219** with the corpus required, gen clean, the typecheck,
|
|
292
|
+
the headless round. The windowed round under X11 with Node's: **53
|
|
293
|
+
examples on both bases**, clean on a first run, two at a time. The
|
|
294
|
+
backdrop examples under X11 read no wallpaper (WSLg has none to name)
|
|
295
|
+
and went `Opaque`, as they should; Plasma's on-the-loop read is
|
|
296
|
+
compiled and read, not run. No Mac ran this round: the AX audit is CI's
|
|
297
|
+
and the next Mac round's.
|
|
298
|
+
|
|
24
299
|
## 0.1.0-alpha.44 (2026-10-08)
|
|
25
300
|
|
|
26
301
|
**What breaks.**
|
|
@@ -143,7 +143,9 @@ date: 2026-10-05
|
|
|
143
143
|
framebuffer space, a scroller inside a turned box scrolls along a
|
|
144
144
|
tilted axis, hit-testing inverts a matrix per ancestor, and the access
|
|
145
145
|
tree's rects become bounding boxes. Not built here, and this does not
|
|
146
|
-
rule it out: a path's own `rotate` would compose under it.
|
|
146
|
+
rule it out: a path's own `rotate` would compose under it. *Built as
|
|
147
|
+
[ADR 0043](0043-a-node-turns-about-its-pivot.md) on 2026-10-08, with
|
|
148
|
+
the path's turn composing under the node's as said.*
|
|
147
149
|
- **A stock arc fragment** (an SDF sector, angles in its params, as
|
|
148
150
|
`polygon` is a stock fragment). A turn is then two floats a frame and
|
|
149
151
|
no raster, exact at any angle. ADR 0040 declined a `sector` element
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-10-08
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A node turns about its pivot: `rotate` and `scale` on any node, paint-only, carried by the clip entry
|
|
7
|
+
|
|
8
|
+
> **Accepted and built 2026-10-08**, the day it was proposed; the
|
|
9
|
+
> *Amendment* at the end records what the building changed. Asked by
|
|
10
|
+
> berainder (backlog F131): a card that tilts a few degrees towards the
|
|
11
|
+
> side it is being dragged to is the swipe's whole look, and the app
|
|
12
|
+
> tweened `width` and `height` instead and called it depth. [ADR
|
|
13
|
+
> 0041](0041-a-mask-turns-about-its-centre.md) gave `rotate` to a
|
|
14
|
+
> `path` alone and listed "a transform on any node" under *Considered
|
|
15
|
+
> options* as "the general thing, and a different document" — every
|
|
16
|
+
> quad kind turns, glyphs leave the pixel grid, the clip stops being a
|
|
17
|
+
> rect in framebuffer space, a scroller inside a turned box scrolls
|
|
18
|
+
> along a tilted axis, hit-testing inverts a matrix per ancestor, the
|
|
19
|
+
> access tree's rects become bounding boxes. This is that document. It
|
|
20
|
+
> takes each of those costs, decides which to pay and which to decline,
|
|
21
|
+
> and lands on a turn and a uniform scale about a pivot, **paint-only**:
|
|
22
|
+
> layout is what it was, and everything after layout — the quads, the
|
|
23
|
+
> clip, the hits, the access rect — is drawn, cut, hit and read through
|
|
24
|
+
> the transform. [ADR 0005](0005-the-paint-vocabulary.md)'s paint
|
|
25
|
+
> vocabulary gains two slots and no quad kind; ADR 0041's path turn
|
|
26
|
+
> composes under this one and is otherwise untouched.
|
|
27
|
+
|
|
28
|
+
## Context
|
|
29
|
+
|
|
30
|
+
- **What a turning box costs today: everything.** No node has a
|
|
31
|
+
transform. `slide`, `enter` and `exit` move and fade; ADR 0025 gave
|
|
32
|
+
layout a scale factor (the DPI), not a rotation; backlog V7 declined
|
|
33
|
+
a layout `zoom` — a subtree laid out in its own logical space — until
|
|
34
|
+
a second app asks for it, and berainder did not: it asked for the
|
|
35
|
+
*look* of a tilt, with the card taking the room its upright self
|
|
36
|
+
takes. A view that wants one today fakes it (width and height eased
|
|
37
|
+
against each other), or draws the card as a `path` (which cannot
|
|
38
|
+
hold a photo or text), or goes to a framework with CSS transforms.
|
|
39
|
+
- **The vertex stage already turns.** ADR 0041 put a path's turn in the
|
|
40
|
+
quad: the `blur` slot carries radians for kinds 1 and 3, and
|
|
41
|
+
`vs_main` turns the four corners about the quad's centre
|
|
42
|
+
(`shader.wgsl`). `local` and `uv` stay the quad's own, so the fragment
|
|
43
|
+
stage's SDF, border, radii and image sampling come out right on a
|
|
44
|
+
turned quad without knowing it turned. The one thing that does not
|
|
45
|
+
turn is the clip, tested against the framebuffer position.
|
|
46
|
+
- **The clip is already a table.** Since ABI 11 a quad names a
|
|
47
|
+
[`Clip`] entry in `DisplayList::clips` rather than carrying its rect
|
|
48
|
+
(`display.rs`): one entry per *distinct* clip a frame reaches, a
|
|
49
|
+
handful, interned by run length as emission walks paint order. Every
|
|
50
|
+
quad under one card names the same entry. An entry is the space a
|
|
51
|
+
quad is painted in; today that space is "framebuffer, cut to this
|
|
52
|
+
rounded rect".
|
|
53
|
+
- **Hits are by shape.** ADR 0026 made a region's hit test the rect and
|
|
54
|
+
then a shape inside it — rounded corners, a stroke's pieces, a
|
|
55
|
+
polygon, a path's outline — in the region's own coordinates
|
|
56
|
+
(`input.rs`, `contains`). A region's test is local already; what it
|
|
57
|
+
lacks is the step from the pointer to local.
|
|
58
|
+
- **The slots are a list.** `anim::Slot` names nine things a transition
|
|
59
|
+
tweens; an entrance and a keyframe stop name five of them through
|
|
60
|
+
`Slots` (`slots.rs`), parsed in one place for every binding. A tenth
|
|
61
|
+
slot is a row in that enum, a lane in that struct and a branch in
|
|
62
|
+
`ease_transitioning`.
|
|
63
|
+
- **What a turn is for, in the apps this repo has seen.** A card that
|
|
64
|
+
tilts with a drag and springs back; a pulse (`scale` 1 → 1.05 → 1 on
|
|
65
|
+
a cycle); a wobble (`rotate` ±0.01 on a cycle); a spinner that is a
|
|
66
|
+
box and not an arc; a chip that enters at `scale: 0.8` and settles;
|
|
67
|
+
an icon that flips. Every one is a turn or a uniform scale about a
|
|
68
|
+
point in the box, and every one wants to **tween**. None is a skew,
|
|
69
|
+
a matrix or a 3D flip.
|
|
70
|
+
|
|
71
|
+
## Decisions
|
|
72
|
+
|
|
73
|
+
1. **`rotate`, `scale`, `pivotX`, `pivotY` on any node.** `rotate` in
|
|
74
|
+
turns, clockwise with y down, 0 for none — the unit a `path`'s
|
|
75
|
+
`rotate` and a gradient's `angle` already take. `scale` a uniform
|
|
76
|
+
factor, 1 for none. `pivotX`/`pivotY` fractions of the node's box,
|
|
77
|
+
`0.5, 0.5` — the centre — by default, the point the turn and the
|
|
78
|
+
scale are about. In Rust `NodeSpec::rotate(turns)`, `scale(f)`,
|
|
79
|
+
`pivot(fx, fy)`; JSX `rotate`, `scale`, `pivotX`, `pivotY`; Lua
|
|
80
|
+
their snake case; C `rotate`, `scale` (0 is 1), `pivot_set`,
|
|
81
|
+
`pivot_x`, `pivot_y` on `KuiSpec`; Odin the generated `Spec` fields.
|
|
82
|
+
Container rows — a box, an image, an edit, a `line`, a `polygon`;
|
|
83
|
+
on a `text` they are dropped with the `unknown-prop` warning every
|
|
84
|
+
container row on a text raises (put them on the box around it). On
|
|
85
|
+
a `path`, `rotate` and `pivot` keep ADR 0041's meaning — the path's
|
|
86
|
+
own turn about a point in `d`'s coordinates, which does not tween —
|
|
87
|
+
and `scale` is the node's; a path that wants a tweened turn is put
|
|
88
|
+
in a box. The two should become one; see *Open questions*.
|
|
89
|
+
2. **Paint-only.** Layout does not see the transform: the node takes
|
|
90
|
+
the room its upright self takes, its siblings do not move, a scroller
|
|
91
|
+
around it scrolls the untransformed content size, `onLayout` reports
|
|
92
|
+
the layout rect, `rect_of` and `layout_of` answer it. A transform is
|
|
93
|
+
`opacity`'s kind of row — something applied after layout to
|
|
94
|
+
everything the subtree draws — and not `width`'s. V7's layout zoom
|
|
95
|
+
stays declined on its own terms; nothing here is it.
|
|
96
|
+
3. **A node's whole subtree turns with it, about the node's pivot.**
|
|
97
|
+
Every quad the subtree emits — backgrounds, borders, shadows,
|
|
98
|
+
glyphs, images, fragments, segments, masks — is drawn through the
|
|
99
|
+
same similarity: `pixel = R(angle) · scale · p + t`, with `t` chosen
|
|
100
|
+
so the pivot stays put. Transforms **nest** by composition: a node
|
|
101
|
+
turned inside a turned node turns about its own pivot in its
|
|
102
|
+
parent's turned space. A float anchored to its parent turns with the
|
|
103
|
+
parent as its content does; a viewport float does not; a float
|
|
104
|
+
anchored to a node by key (the devtools' tabs) does not.
|
|
105
|
+
4. **The clip entry carries the space.** [`Clip`] gains three fields:
|
|
106
|
+
`transform` — angle in radians, scale, `tx`, `ty`, the similarity
|
|
107
|
+
every quad naming the entry is drawn through — and `inner` with
|
|
108
|
+
`inner_radius`, a second clip in the quad's own space, before the
|
|
109
|
+
transform. The entry's `rect` and `radius` stay what they were: the
|
|
110
|
+
clip in framebuffer space. A clipping node *outside* any turn
|
|
111
|
+
narrows `rect`; one *inside* a turn narrows `inner`, since its box is
|
|
112
|
+
in the turned space. Between two nested turns a clip is approximated
|
|
113
|
+
by its bounding box in the inner space (its corners drop to square);
|
|
114
|
+
a tilted card inside a tilted card inside a scroller is the one
|
|
115
|
+
shape that pays, by a sliver at a corner. A frame with no transform
|
|
116
|
+
interns exactly the entries it interned before, with the identity
|
|
117
|
+
transform and no inner clip, so the run-length intern and every
|
|
118
|
+
backend that ignores the new fields are unchanged.
|
|
119
|
+
5. **The shader turns the corners and tests two clips.** `vs_main`
|
|
120
|
+
applies the entry's transform after a path's own turn (decision 1 of
|
|
121
|
+
ADR 0041 composes under it, as that document said it would), and
|
|
122
|
+
passes the pre-transform position down as a varying. The fragment
|
|
123
|
+
stage tests the framebuffer clip against the framebuffer position,
|
|
124
|
+
as before, and the inner clip against the pre-transform position —
|
|
125
|
+
an affine function of position, so the interpolated varying is
|
|
126
|
+
exact, and no inverse is computed per pixel. A segment's capsule SDF
|
|
127
|
+
reads the pre-transform position too, so a turned stroke is a
|
|
128
|
+
stroke. The instance grows by three `vec4`s (48 bytes); the quad on
|
|
129
|
+
the wire does not change. A fragment pipeline's epilogue
|
|
130
|
+
(`fragment::EPILOGUE`) tests the inner clip the same way, so a
|
|
131
|
+
`fragment` inside a turned card is cut by the card.
|
|
132
|
+
6. **Hit where drawn.** A region under a transform carries it and the
|
|
133
|
+
inner clip; `contains` unprojects the pointer into the node's space
|
|
134
|
+
and tests the rect, the shape and the inner clip there, and the
|
|
135
|
+
framebuffer clip with the pointer as it is. A tilted card is grabbed
|
|
136
|
+
on its tilted edge; its rounded corners still miss; a press in its
|
|
137
|
+
box off its outline falls through. Drag payloads stay in viewport
|
|
138
|
+
px — what a drag means to the app that moves the card is where the
|
|
139
|
+
pointer is on screen. A scroll region inside a turned node is hit
|
|
140
|
+
the same way, so the wheel over a tilted list scrolls it; what moves
|
|
141
|
+
is the content along the node's own axes, since the offset is
|
|
142
|
+
applied in layout and the turn after.
|
|
143
|
+
7. **The access rect is the bounding box.** Assistive technology gets
|
|
144
|
+
an axis-aligned rect, so a turned node's is the bounding box of its
|
|
145
|
+
turned layout rect, cut to the framebuffer clip as every rect is.
|
|
146
|
+
Reading order and actions are untouched.
|
|
147
|
+
8. **A turn and a scale are one slot, and it tweens.** `Slot::Transform`
|
|
148
|
+
carries `[rotate, scale, 0, 0]`: with a `transition` a card follows
|
|
149
|
+
the pointer while a drag holds the transition off and springs back
|
|
150
|
+
when it is on, as `slide` does for position. An entrance names
|
|
151
|
+
`rotate` and `scale` (`enter: { scale: 0.8 }` settles a chip in), an
|
|
152
|
+
exit names them (`exit: { rotate: 0.1, scale: 0 }` spins a card
|
|
153
|
+
away), a keyframe stop names them (`keyframes: [{ scale: 1.05, at:
|
|
154
|
+
0.5 }]` pulses; `[{ rotate: 0 }, { rotate: 1 }]` spins a box) —
|
|
155
|
+
through `Slots`, so every binding gets them from the one parser. A
|
|
156
|
+
lane a stop or an entrance leaves out is the node's own value,
|
|
157
|
+
which for `scale` is 1 and not 0: a stop that names only `rotate`
|
|
158
|
+
does not shrink the box.
|
|
159
|
+
9. **A ghost keeps its turn.** A departing subtree is replayed with the
|
|
160
|
+
transform its nodes declared, the root's eased toward the exit's;
|
|
161
|
+
the turns of ancestors outside the picture — which may be gone — are
|
|
162
|
+
not replayed, as their clips are not.
|
|
163
|
+
10. **What is declined.** A matrix, skew, 3D, a per-axis scale, a
|
|
164
|
+
transform origin outside the box (`pivot` past 0..1 is allowed and
|
|
165
|
+
means what it says; it is not clamped). CSS's `transform` is the
|
|
166
|
+
general thing; a turn and a uniform scale are what every app has
|
|
167
|
+
asked for, and each is one float that tweens. A `backdropBlur`
|
|
168
|
+
under a turn blurs its upright box (the blur reads the framebuffer
|
|
169
|
+
region); `pixelSnap` snaps the layout rect before the turn, so a
|
|
170
|
+
turned snapped box is a turned box. Both are said in `props.md`.
|
|
171
|
+
|
|
172
|
+
## Considered options
|
|
173
|
+
|
|
174
|
+
- **The transform on the quad.** Four floats on every quad of every
|
|
175
|
+
frame, for a feature almost no quad uses; and the inner clip has no
|
|
176
|
+
room there at all. The clip table exists for exactly this reason
|
|
177
|
+
(ABI 11), and a quad under a turned card already names the entry its
|
|
178
|
+
siblings do. Not taken.
|
|
179
|
+
- **Rasterize the subtree offscreen and draw the texture turned.** One
|
|
180
|
+
quad, exact clipping, and the way a browser composites a transformed
|
|
181
|
+
layer. It costs a render target per turned subtree per frame, a copy
|
|
182
|
+
of every pixel under it, and text rendered to a texture and
|
|
183
|
+
resampled — blurry at any angle, which is the one thing a glyph
|
|
184
|
+
turned by its own quad is not. The backdrop pass (F129) does this
|
|
185
|
+
for a blur because a blur needs the pixels; a turn does not.
|
|
186
|
+
- **A transform on the clip only, hits and access left upright.** Half
|
|
187
|
+
the build and a lie: a tilted card hit on its upright rect is grabbed
|
|
188
|
+
in the air beside its corner. ADR 0026's shapes make the honest
|
|
189
|
+
version cheap.
|
|
190
|
+
- **Unify the path's `rotate` with this one now.** The path's turn
|
|
191
|
+
would become a node transform, its `pivot` a point in the box, the
|
|
192
|
+
square box ADR 0041 sweeps unnecessary, and its turn would tween for
|
|
193
|
+
free. It is the right end state and the wrong day: `path` is
|
|
194
|
+
released, its `pivot` is in `d`'s coordinates by a decision this
|
|
195
|
+
repo argued, and `path_coverage.rs` pins the sweep. Left open below.
|
|
196
|
+
- **A per-axis scale (`scaleX`/`scaleY`).** A flip (`scaleX: -1`) is
|
|
197
|
+
the one use, and a flip of a box with text in it mirrors the text.
|
|
198
|
+
The slot's lanes have room for it if an app asks.
|
|
199
|
+
|
|
200
|
+
## Consequences
|
|
201
|
+
|
|
202
|
+
- **A tilt is one row and it tweens.** berainder's card is
|
|
203
|
+
`rotate(lean * 0.03)` with its transition held off while dragging,
|
|
204
|
+
and the spring back is the transition it already has.
|
|
205
|
+
- **The instance grows.** Three `vec4`s per quad in `kui-wgpu`: half a
|
|
206
|
+
megabyte more upload on a 10,000-quad frame, a few microseconds.
|
|
207
|
+
Nothing on the core's side of the wire grows per quad; the clip
|
|
208
|
+
table's entries grow from 32 to 80 bytes, and a frame has a handful.
|
|
209
|
+
- **The C ABI bumps to 27.** `KuiClip` grows (a `[lib]` struct a host
|
|
210
|
+
strides with its own `sizeof`), `KuiSpec`, `KuiEnter` and
|
|
211
|
+
`KuiKeyframe` gain fields, and the conformance digest hashes the
|
|
212
|
+
whole clip entry. The Node `clips()` buffer's stride grows with it;
|
|
213
|
+
`decodeClips` reads the new fields.
|
|
214
|
+
- **Every binding gets the four rows and the two slot lanes** from the
|
|
215
|
+
schema and the one slot parser; the Odin layer regenerates its
|
|
216
|
+
`Spec`, `Enter`, `Keyframe` and `Clip`.
|
|
217
|
+
- **Glyphs leave the pixel grid under a turn**, as a turned path's
|
|
218
|
+
mask does: a turned text is resampled at its angle and reads
|
|
219
|
+
softer. A text that must stay crisp is not turned. Under a scale, the
|
|
220
|
+
SDF ramps scale with the box, so a box scaled up has an edge
|
|
221
|
+
`scale` times as soft; a UI scale stays near 1.
|
|
222
|
+
- **The devtools' facts show `rotate` and `scale`** beside `opacity`.
|
|
223
|
+
|
|
224
|
+
## Open questions
|
|
225
|
+
|
|
226
|
+
- **The path's own turn.** When a release can break `path`, its
|
|
227
|
+
`rotate` should be this one — tweened, composed, about a pivot in
|
|
228
|
+
the box — and `pivot` should take the box's fractions as every other
|
|
229
|
+
node's does; ADR 0041's square sweep goes with it. Condition: the
|
|
230
|
+
first app that wants a path's turn to tween.
|
|
231
|
+
- **Text sharpness under a turn.** A glyph mask is bilinear-sampled at
|
|
232
|
+
its angle. If a turned paragraph is ever asked for — not a card with
|
|
233
|
+
a word on it — the answer is a supersampled mask or a rasterization
|
|
234
|
+
at the angle, both of which the atlas's turn-bin rejection in ADR
|
|
235
|
+
0041 already priced.
|
|
236
|
+
|
|
237
|
+
## Measurements to take
|
|
238
|
+
|
|
239
|
+
- `frame_10k_rects` and the guarded `frame` rows: the transform walk is
|
|
240
|
+
behind `Tree::any_transform`, so a frame with none should read as it
|
|
241
|
+
did (the guard allows the noise floor).
|
|
242
|
+
- A frame of 1,000 turned boxes, each its own clip entry, against the
|
|
243
|
+
same frame upright: the cost of the per-node compose and the
|
|
244
|
+
bounding-box cull. Not yet taken (RG155).
|
|
245
|
+
|
|
246
|
+
## Amendment: what the building changed (2026-10-08)
|
|
247
|
+
|
|
248
|
+
- **`KuiSpec` is 744 bytes, not 728.** The five appended fields land
|
|
249
|
+
after the 8-byte-aligned tail, and the parity test said so.
|
|
250
|
+
- **`pivot_set` is two bits.** A C host setting one axis — the parity
|
|
251
|
+
test's one-row-at-a-time check was the first — needs to say which, so
|
|
252
|
+
`KUI_PIVOT_X` and `KUI_PIVOT_Y` are bits as `KUI_VALUE_*` are, and the
|
|
253
|
+
Odin lowering ORs them in. `scale` stays "0 is 1".
|
|
254
|
+
- **The slot bits are `KUI_KF_ROTATE` 64, `KUI_KF_SCALE` 128,
|
|
255
|
+
`KUI_ENTER_ROTATE` 64, `KUI_ENTER_SCALE` 128.**
|
|
256
|
+
- **A glyph cull reads the clip in the quad's space.** The text, cell
|
|
257
|
+
and editor painters compared glyph positions against the entry's
|
|
258
|
+
framebuffer rect; under a turn that is the wrong space. They read
|
|
259
|
+
`Clip::visible` — the inner clip narrowed by the outer one pulled
|
|
260
|
+
back through the transform — which is the rect itself when nothing
|
|
261
|
+
turns.
|
|
262
|
+
- **The pre-transform position is a varying, not an inverse.** `pre` at
|
|
263
|
+
location 10 of `VsOut`; the fragment epilogue takes it at the same
|
|
264
|
+
location, with `inner` and `inner_radii` at 11 and 12. A fragment
|
|
265
|
+
entry point lists a subset of the vertex outputs, so an app's
|
|
266
|
+
fragment compiled against the old prelude still links.
|
|
267
|
+
- **A scrollbar's thumb under a turn is hit upright.** The bar is drawn
|
|
268
|
+
turned (its quads name the scroller's entry) but its track is tested
|
|
269
|
+
as an upright rect. A turned scroller is a demonstration shape, and
|
|
270
|
+
the wheel over it is right; the thumb waits for an app that drags one.
|
|
271
|
+
- **A caret placed inside a turn reads the pointer upright.** A press
|
|
272
|
+
finds an editor in a tilted card, but where its caret lands, the drag
|
|
273
|
+
that extends it, a static text's selection drag and `text_hit` take
|
|
274
|
+
the pointer less the content origin without pulling it back through
|
|
275
|
+
the turn, so under a turn or a scale the caret lands on the wrong
|
|
276
|
+
character. Found by alpha.46's pre-tag pass; open as RG155, with the
|
|
277
|
+
inner clip's unsmoothed edge and the second measurement below.
|
|
278
|
+
- **The Odin layer was regenerated by hand.** No Odin ran on the
|
|
279
|
+
building machine (its binary wants an LLVM the machine does not have),
|
|
280
|
+
so `kui_c.odin`, `layout.odin`, `generated.odin`, `types.odin` and the
|
|
281
|
+
generator's policy were written as the generator writes them, and
|
|
282
|
+
CI's `odin.nu gen --check` is what says whether they match.
|
|
283
|
+
It did not, at first: `KuiSpec` embeds `KuiEnter` twice (`enter`,
|
|
284
|
+
`exit`), whose eight new bytes moved every field after them by 16,
|
|
285
|
+
and the hand-written `layout.odin` moved only the tail. Found while
|
|
286
|
+
building F133 (2026-10-09) and rewritten from the C mirror's own
|
|
287
|
+
asserts (`target/kui-abi-assert.c`), all 77 offsets, with every field
|
|
288
|
+
of every mirrored struct checked to have one.
|
|
289
|
+
- **Lua's `path` strips the row.** Lua reads every key of a table as a
|
|
290
|
+
schema row before an element reads its own, so `path { rotate = }`
|
|
291
|
+
turned the node as well as the mask and the `path` scene's digest
|
|
292
|
+
moved; the arm now drops the node transform the generic walk read.
|
|
293
|
+
Node's encoder had the same fault and the same fix (its `path` case
|
|
294
|
+
writes the rows without `rotate`): the full run that cleared this ADR
|
|
295
|
+
loaded a stale addon and passed, and the next one, building F133,
|
|
296
|
+
caught it. C and Odin take a path's turn as an argument.
|
|
297
|
+
- **The conformance digest hashes twenty words a clip** in all five
|
|
298
|
+
adapters, and the `transform` scene is the first whose digest moves
|
|
299
|
+
if a turn, a scale or an inner clip does.
|
|
300
|
+
- **Measured, and given back (2026-10-09).** The first build passed
|
|
301
|
+
the bench guard against `v0.1.0-alpha.44` with every frame that turns
|
|
302
|
+
nothing 4% to 9% slower. Investigated before merging, against the
|
|
303
|
+
build's own parent (`acbbbb4d`, RG154 — which reads as alpha.44 does,
|
|
304
|
+
so all of it was this ADR's), by alternating the two bench binaries
|
|
305
|
+
and profiling with `sample`. Six costs, each found by an experiment
|
|
306
|
+
before it was fixed:
|
|
307
|
+
|
|
308
|
+
1. **The paint path carried the whole clip.** `Paint` held the
|
|
309
|
+
80-byte `Clip` by value into `emit_node` and `paint_box`, which
|
|
310
|
+
rescaled all twenty floats for every node. It holds the clip's rect
|
|
311
|
+
and id now; a leaf that culls — a text, a grid, an editor — reads
|
|
312
|
+
the scaled entry from the display list, and the table rules look
|
|
313
|
+
the logical clip up by node.
|
|
314
|
+
2. **The walk carried it too.** `emit_frame` built an 80-byte value
|
|
315
|
+
per node and read its rect. It carries the rect, the cull rect and
|
|
316
|
+
the id; the clip stays in `self.clips`, which is pushed in tree
|
|
317
|
+
order instead of filled and overwritten, and children of one
|
|
318
|
+
clipper share one intersect and one intern through a two-index
|
|
319
|
+
cache (a list's rows were interning the same 80 bytes each).
|
|
320
|
+
3. **A tenth tween slot on every transitioning node.** 96 bytes a
|
|
321
|
+
node whether it turned or not. The turn's tween lives in
|
|
322
|
+
`AnimStore::turns`, for the nodes that turn.
|
|
323
|
+
4. **The test for a turn inside `ease_transitioning`.** It is eased
|
|
324
|
+
after the other slots, in `ease_transform`, out of line, behind a
|
|
325
|
+
two-box check in `ease_spec`; `ease_transitioning` is alpha.44's.
|
|
326
|
+
5. **An inlining cliff.** The drive body shared by the array slots and
|
|
327
|
+
the turn was force-inlined into both callers, so `Tween::eased_at`
|
|
328
|
+
and `spring_step` gained a second caller and stopped being inlined —
|
|
329
|
+
4% on the transitioning row by itself. The shared body is the
|
|
330
|
+
out-of-line function now, as `NodeAnim::drive` was, and the
|
|
331
|
+
helpers inline into it again.
|
|
332
|
+
6. **A test that stopped `prepare_spec` inlining.** The check for a
|
|
333
|
+
turn in `ease_spec` scanned the keyframe stops inline, which made
|
|
334
|
+
`prepare_spec` too big to inline into `open_content`, and every node
|
|
335
|
+
paid a call (+2% on 10,000 gradient cells). The scan is out of line.
|
|
336
|
+
|
|
337
|
+
Smaller: the hit region's turn is boxed (36 bytes inline was 1% on a
|
|
338
|
+
thousand buttons), the transform inside the interact group is boxed
|
|
339
|
+
(so a gradient or a hover allocates the group in the class it did),
|
|
340
|
+
the leaves that do not cull by the clip never copy it, and the
|
|
341
|
+
turned-space cull is asked only on a frame that turns. Medians against
|
|
342
|
+
RG154, on mains power:
|
|
343
|
+
|
|
344
|
+
| row | first build | now (median) | now (fastest) |
|
|
345
|
+
| --- | --- | --- | --- |
|
|
346
|
+
| `frame_10k_rects` | +5.9% | +0.5% | +0.5% |
|
|
347
|
+
| `frame_1k_typical` | +4.7% | −0.3% | −0.1% |
|
|
348
|
+
| `frame_10k_rects_with_text_and_hits` | +4.5% | −0.3% | −0.6% |
|
|
349
|
+
| `frame_10k_segments` | +5.2% | +0.2% | +0.6% |
|
|
350
|
+
| `frame_10k_rects_with_access_tree` | +4.1% | −0.1% | −0.5% |
|
|
351
|
+
| `list_10k_rows_virtual` | +8.1% | +0.4% | +2.1% |
|
|
352
|
+
| `frame_1k_curves` | +1.1% | +0.1% | +0.3% |
|
|
353
|
+
| `frame_10k_rects_all_transitioning` | +11.0% | −2.7% | −3.9% |
|
|
354
|
+
| `deep_nesting_64_levels` | +5.7% | +3.0% | +0.5% |
|
|
355
|
+
| `frame_10k_rects_with_gradient` | +6.2% | +1.2% | +1.6% |
|
|
356
|
+
| `frame_10k_rects_all_declaring_exit` | +13.6% | +4.6% | +0.6% |
|
|
357
|
+
| `frame_10k_rects_rounded_clip` | +17.4% | −6.1% | −6.0% |
|
|
358
|
+
|
|
359
|
+
`deep_nesting_64_levels` never clips or turns, and what moved in it is
|
|
360
|
+
layout, whose code is unchanged and the same size: placement, not
|
|
361
|
+
cost. The exit row's medians span 15% on this machine, and its fastest
|
|
362
|
+
samples are level; what it keeps is its entrance and exit, 16 bytes
|
|
363
|
+
bigger each for `rotate` and `scale`, which every node declaring one
|
|
364
|
+
allocates. The rounded clip is faster than before ADR 0043 because a
|
|
365
|
+
clipper's children now share one intersect. Two
|
|
366
|
+
traps for the next one: the Mac on battery, and another session's
|
|
367
|
+
builds, made the transitioning row unreadable (medians spanning 2×)
|
|
368
|
+
until the race waited out `rustc` and read the fastest sample beside
|
|
369
|
+
the median; and the guard's base was alpha.44, which hid that RG154
|
|
370
|
+
sat between it and this build.
|