@qxuken/kui 0.1.0-alpha.36 → 0.1.0-alpha.38
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 +352 -0
- package/docs/adr/0005-the-paint-vocabulary.md +5 -0
- package/docs/adr/0010-a-segment-primitive.md +46 -0
- 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 +44 -2
- package/docs/adr/0042-a-gradient-is-an-image-the-core-paints.md +455 -0
- package/encoder.js +26 -4
- package/howto.md +79 -1
- package/index.d.ts +23 -0
- package/jsx-runtime.d.ts +58 -3
- 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 +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,358 @@ 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.38 (2026-10-05)
|
|
25
|
+
|
|
26
|
+
**What breaks.**
|
|
27
|
+
|
|
28
|
+
- C: `KuiSpec` gains `scroll_mods` at its end (64-bit size 704), so
|
|
29
|
+
`KUI_ABI_VERSION` is 25; recompile. A zeroed spec is what it was.
|
|
30
|
+
- Rust: `event::Scroll` gains a `mods` field and `EventSpec` a
|
|
31
|
+
`scroll_mods` one (under Added), so a struct literal of either needs
|
|
32
|
+
them.
|
|
33
|
+
|
|
34
|
+
The Node wire stays v21: `scrollMods` is one more string row.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **An `onScroll` node can be for a modified wheel** (backlog F122,
|
|
39
|
+
from kawoosh). `scrollMods` — `"ctrl super"`, any of `shift`, `ctrl`,
|
|
40
|
+
`alt`, `super`; Rust `NodeSpec::scroll_mods(KeyMods)`, C
|
|
41
|
+
`KuiSpec.scroll_mods` in `KUI_KMOD_*` bits, Lua `scroll_mods` — and
|
|
42
|
+
the node hears only a scroll gesture that began with one of them
|
|
43
|
+
held, and hears it first: ahead of every scroll container and every
|
|
44
|
+
`onScroll` that names none, wherever under the pointer the gesture
|
|
45
|
+
began, the innermost such node winning. A Ctrl-wheel zoom declared
|
|
46
|
+
on the window's root is heard over a list, and the list does not
|
|
47
|
+
scroll; a canvas inside can name the same key and take it for
|
|
48
|
+
itself. A wheel with none of them held passes the node by — and
|
|
49
|
+
scrolls it, if it is a scroll container too: a list can name a key
|
|
50
|
+
for its own zoom and scroll for every other wheel (backlog RG119,
|
|
51
|
+
from this release's pre-tag pass; as merged, such a list stood still
|
|
52
|
+
for a plain wheel). Its
|
|
53
|
+
`scroll` events carry `mods` (`Scroll::mods`), the modifiers held
|
|
54
|
+
when the gesture began: a gesture stays what it began as to the end
|
|
55
|
+
of its glide, whatever is let go or pressed meanwhile.
|
|
56
|
+
|
|
57
|
+
**What you can delete.** An `onScroll` handler that read the modifiers
|
|
58
|
+
to tell a zoom from a scroll, and the scroll it then had to do itself
|
|
59
|
+
for the plain wheel — and the knowledge that it only ever worked over
|
|
60
|
+
that handler's own node, every scroll container inside it taking the
|
|
61
|
+
same wheel first.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **`scripts/npm-approve.nu` no longer fails a release it has just
|
|
66
|
+
made.** Approving alpha.37 went through, and the script then waited
|
|
67
|
+
a minute for `npm view` to list the version, gave up with "approved,
|
|
68
|
+
but npmjs does not list it yet; run this again", and on the second
|
|
69
|
+
run — no stage left, the cached packument still without the
|
|
70
|
+
version — said nothing was staged and asked whether the release
|
|
71
|
+
workflow had run. `latest` was set by hand. What is live is now read
|
|
72
|
+
off the dist-tags, which a version takes the moment it is approved,
|
|
73
|
+
and `latest` follows the approval at once. A release script, so
|
|
74
|
+
nothing an app sees.
|
|
75
|
+
|
|
76
|
+
**What you can delete.** Nothing.
|
|
77
|
+
|
|
78
|
+
### Native verification
|
|
79
|
+
|
|
80
|
+
The by-hand round alpha.6 introduced (backlog R4), on 2026-10-05 — the
|
|
81
|
+
two commits after the alpha.37 tag: `scrollMods` (F122) and the
|
|
82
|
+
approval script — with a regression pass over them first: the diff
|
|
83
|
+
read whole, each claim probed with a test before anything changed.
|
|
84
|
+
RG119 came of it and is in this release. Before any of it the last
|
|
85
|
+
`check` on main was read on both hosts, green at `3d4fff6` — the step
|
|
86
|
+
alpha.37's first tag went without. This round ran on the Mac alone;
|
|
87
|
+
Windows and Linux did not run it for this tag.
|
|
88
|
+
|
|
89
|
+
**macOS 27.0.1 on an M3 Pro MacBook Pro, rustc 1.99.0 (the toolchain
|
|
90
|
+
CI runs), Node 26.10.0, nu 0.116.0**, on the release commit's tree,
|
|
91
|
+
the workspace's own artifacts pruned and rebuilt. `cargo fmt --all
|
|
92
|
+
--check` and `cargo clippy --workspace --all-targets -- -D warnings`
|
|
93
|
+
are clean. `scripts/test.nu`, the workspace's tests with the
|
|
94
|
+
conformance feature: **1808 tests over 136 suites, 0 failed** (4
|
|
95
|
+
ignored). The C round, `cbuild --run`, passes its five checks; the
|
|
96
|
+
corpus passes its **57 scenes** in four adapters, `scroll-gestures`
|
|
97
|
+
now holding a Ctrl-wheel over a contained list at its limit; the ABI
|
|
98
|
+
is **25**. Node's `node --test test.mjs` under
|
|
99
|
+
`KUI_CONFORMANCE_REQUIRED=1`: **210 of 210**. `npm run gen` leaves no
|
|
100
|
+
diff, the examples typecheck and their lockfile installs, the headless
|
|
101
|
+
round passes all **35 drives**, the book builds and
|
|
102
|
+
`scripts/book-examples.nu --check`
|
|
103
|
+
passes.
|
|
104
|
+
|
|
105
|
+
**The windowed round**, `smoke -- --node`, twice over: **51 Rust
|
|
106
|
+
examples and the eleven Node examples, each on both bases, 120 frames
|
|
107
|
+
each, every one exiting 0** — 124 windows, eight at a time, in 34 and
|
|
108
|
+
35 s — and `counter`, `host`, `c_panel` and `lua_panel` by hand under
|
|
109
|
+
`KUI_SMOKE_FRAMES=120`, each exiting 0 with nothing on stderr: **128
|
|
110
|
+
windows over five hosts.** The AX audit: **106/106**, the audited
|
|
111
|
+
window raised to the front by its pid first, and no warning on the
|
|
112
|
+
fixture's stderr. `scrollMods` itself was not driven in a window in
|
|
113
|
+
this round: it was tried in kawoosh's before the merge, and RG119's
|
|
114
|
+
case is pinned in the core's tests alone.
|
|
115
|
+
|
|
116
|
+
**The bench guard** against the alpha.37 tag, on the release commit's
|
|
117
|
+
tree: **green**, none of the 8 guarded rows more than 10% slower —
|
|
118
|
+
every one between −4.1% and +0.6% (the worst guarded run-to-run spread
|
|
119
|
+
4.3%), and no row of the run more than 5% slower. `README.md`'s table
|
|
120
|
+
is kept as it was.
|
|
121
|
+
|
|
122
|
+
## 0.1.0-alpha.37 (2026-10-05)
|
|
123
|
+
|
|
124
|
+
**What breaks.**
|
|
125
|
+
|
|
126
|
+
- Rust: `HitShape::Path` gains a `stroke` field and `Content` a
|
|
127
|
+
`PathFlat` variant (under Fixed), so a pattern over either needs
|
|
128
|
+
them; `HitShapes::path` takes the stroke's width after the rule.
|
|
129
|
+
- A `path` with a stroke over a fill is hit out to the stroke's edge,
|
|
130
|
+
where the half of the stroke outside the outline fell through; and a
|
|
131
|
+
stroked path with no `bg` but a `hover_bg`, `pressed_bg` or
|
|
132
|
+
`focus_bg` is hit inside, where it was hit along its stroke only
|
|
133
|
+
(under Fixed).
|
|
134
|
+
- Lua: `path { ops = }` whose numbers are not the flat form draws
|
|
135
|
+
nothing with a `path-malformed` warning, where it was an error that
|
|
136
|
+
failed the view (under Fixed).
|
|
137
|
+
- A `path` that animated and then held still for 120 frames draws from
|
|
138
|
+
the atlas again — a `GlyphMask` quad where it stayed a `Texture`
|
|
139
|
+
(under Fixed). Nothing an app sees; a host that counts quads by kind
|
|
140
|
+
does.
|
|
141
|
+
- C: `kui_polyline`, `kui_path` and `kui_path_d` take a `dash` before
|
|
142
|
+
`spec` — five floats, or NULL for the solid stroke they drew (under
|
|
143
|
+
Added). `KUI_ABI_VERSION` is 24; pass NULL and recompile. The same
|
|
144
|
+
step appends `gradient` to `KuiSpec` (64-bit size 696).
|
|
145
|
+
- Rust: `schema::Kind`, `Apply` and `Parsed` gain a `Gradient` variant
|
|
146
|
+
and `InteractSpec` a `gradient` field (under Added).
|
|
147
|
+
- Rust: `Stroke` gains a `dash` field, so a struct literal needs it
|
|
148
|
+
(`Stroke::new` does not); `path::MaskPaint` gains `Dashed`.
|
|
149
|
+
- A cell grid's glyph for a character its family lacks is drawn from a
|
|
150
|
+
monospaced face where one has it, in the middle of its cell, and no
|
|
151
|
+
wider than it (F120, under Fixed), where it was the platform's first
|
|
152
|
+
fallback at its own width from the cell's left edge.
|
|
153
|
+
- Rust: `Tree` gains an `any_gradient` field.
|
|
154
|
+
|
|
155
|
+
The Node wire moves to v21 for the dash's five floats on a line and a
|
|
156
|
+
path, which an encoder and addon of one release never see apart.
|
|
157
|
+
|
|
158
|
+
### Added
|
|
159
|
+
|
|
160
|
+
- **`gradient` on a box, in every binding**
|
|
161
|
+
([ADR 0042](docs/adr/0042-a-gradient-is-an-image-the-core-paints.md),
|
|
162
|
+
which supersedes ADR 0005's "no gradients"): `gradient={{ to:
|
|
163
|
+
'bottom', stops: ['#1e2030', '#14161e'] }}`, `{ angle: 0.125, stops }`
|
|
164
|
+
in turns clockwise from east, `{ radial: true, at: [0.5, 0], stops }`;
|
|
165
|
+
the same table in Lua; `NodeSpec::gradient(Gradient::to(Side::Bottom,
|
|
166
|
+
[...]))` with `Gradient::angle` and `Gradient::radial_at`; and a
|
|
167
|
+
`const KuiGradient *` on `KuiSpec`. A stop is a colour — a `$token`
|
|
168
|
+
too — or a colour and a position.
|
|
169
|
+
- **Over `bg`, under the border and the children.** `bg` stays a
|
|
170
|
+
colour and keeps its tweens, tokens and state backgrounds; a
|
|
171
|
+
transparent stop shows it through.
|
|
172
|
+
- **An image the core paints.** Each distinct gradient is rasterized
|
|
173
|
+
once into the glyph atlas — a 256-texel strip along an axis, a
|
|
174
|
+
128-texel square otherwise, each with a gutter of the gradient
|
|
175
|
+
carried a texel past its edges, since a stretched quad samples
|
|
176
|
+
there — and drawn as one `Image` quad, so no
|
|
177
|
+
renderer changes and a host that draws an image draws a gradient.
|
|
178
|
+
The key is the gradient and not the box: a resize, another scale
|
|
179
|
+
and a thousand boxes sharing one rasterize nothing.
|
|
180
|
+
- **On the box's unit square.** A side or a corner is CSS's; any
|
|
181
|
+
other `angle` runs corner to corner at an eighth of a turn at any
|
|
182
|
+
aspect, where CSS's pixel-measured `45deg` does not. Stops mix in
|
|
183
|
+
straight sRGB with the alpha premultiplied.
|
|
184
|
+
- **What it does not do.** It does not tween and the state
|
|
185
|
+
backgrounds do not replace it; a hard stop is as soft as the raster
|
|
186
|
+
stretched (a 256th of the box along a strip); there is no conic
|
|
187
|
+
one, and none on a `path`, a `line`, a border or a text. Those, and
|
|
188
|
+
anything animated, stay a `fragment`'s.
|
|
189
|
+
- Measured (the ADR's *Measured*): a linear gradient is within half
|
|
190
|
+
an 8-bit level of the mix computed per pixel at every pixel of the
|
|
191
|
+
box, a radial within 1.2; a gradient box costs about 52 ns over a
|
|
192
|
+
flat one, half of it what any `hoverBg` pays; a raster is 4 µs for
|
|
193
|
+
a strip and 25 to 42 for a square.
|
|
194
|
+
- The corpus gains a `gradients` scene; `examples/rust/apps/loaders.rs`
|
|
195
|
+
gains a rainbow — one gradient two tracks long slid under a clip,
|
|
196
|
+
one strip in the atlas for good.
|
|
197
|
+
|
|
198
|
+
- **`dash` on a `line` and on a `path`'s stroke, in every binding**
|
|
199
|
+
(backlog V2, the amendment in
|
|
200
|
+
[ADR 0010](docs/adr/0010-a-segment-primitive.md)): `<line dash={[6, 4]}
|
|
201
|
+
dashOffset/>`, `line { dash = {6, 4}, dash_offset = }`,
|
|
202
|
+
`Stroke::new(w, c).dash(6.0, 4.0).dash_offset(n)` (`Dash::of` for a
|
|
203
|
+
pattern read from data), and a `const float *dash` on `kui_polyline`,
|
|
204
|
+
`kui_path` and `kui_path_d`. One length is marks and gaps alike, two
|
|
205
|
+
are a mark and a gap, four a dash-dot.
|
|
206
|
+
- **The lengths are the ones seen.** Every mark is a short stroke
|
|
207
|
+
with the stroke's own round caps, so `6, 4` is 6 px of ink and 4 px
|
|
208
|
+
of nothing at any width and a mark no longer than the stroke is
|
|
209
|
+
wide is a dot. SVG's `stroke-dasharray` measures the centre line,
|
|
210
|
+
which with round caps makes `4 4` at a width of 4 a solid line;
|
|
211
|
+
this pattern is SVG's `mark − width, gap + width`.
|
|
212
|
+
- **The pattern keeps its phase** along the whole stroke — round the
|
|
213
|
+
corners of a polyline and across the pieces of a curve, which is
|
|
214
|
+
what ADR 0010 would not ship without — and restarts at each subpath
|
|
215
|
+
of a `path`, as SVG's does. `dashOffset` starts that far into it;
|
|
216
|
+
growing it moves the marks towards the first point, a marquee's
|
|
217
|
+
marching ants. Neither tweens.
|
|
218
|
+
- **No renderer changes.** A dashed line is one `Segment` quad per
|
|
219
|
+
mark per piece the mark lies on, cut in the core, so a host that
|
|
220
|
+
draws a stroke draws a dashed one; a dashed path stroke is cut by
|
|
221
|
+
the rasterizer into the mask it already was. A path whose offset
|
|
222
|
+
changes every frame is a shape that changes every frame, and
|
|
223
|
+
leaves the atlas for a texture of its own while it marches.
|
|
224
|
+
- A pattern with no gap, a mark and gap under a physical pixel
|
|
225
|
+
together, or more than 16384 marks draws solid. A dashed stroke is
|
|
226
|
+
hit along its whole length, gaps included.
|
|
227
|
+
- The corpus's `lines` and `paths` scenes carry one each, so the
|
|
228
|
+
segment count pins the cut in four bindings;
|
|
229
|
+
`examples/rust/widgets/line.rs` hangs its planned cards off dashed
|
|
230
|
+
links that march under the pointer.
|
|
231
|
+
|
|
232
|
+
- **The fallback fonts are the app's to name** (backlog F121, from
|
|
233
|
+
kawoosh). `Core::set_fallback_fonts(&[FontId])` — C
|
|
234
|
+
`kui_font_set_fallback`, Node `ctx.setFallbackFonts(ids)` — lists the
|
|
235
|
+
fonts asked, in order, for a character the text's own family has no
|
|
236
|
+
glyph for, before the platform's list, whose first choice on macOS is
|
|
237
|
+
the system's proportional face. For every text in the session — an
|
|
238
|
+
editor already open and a cell grid too — and kept across
|
|
239
|
+
`reload_system_fonts`; an empty list is the platform's alone.
|
|
240
|
+
`Core::fallback_fonts` reads the families back.
|
|
241
|
+
|
|
242
|
+
### Fixed
|
|
243
|
+
|
|
244
|
+
- **A cell grid's fallback glyph stays in its cell** (backlog F120, from
|
|
245
|
+
kawoosh: Russian text in a terminal whose family has no Cyrillic, `Ю`
|
|
246
|
+
drawn across the letter after it). A character the grid's family has
|
|
247
|
+
no glyph for is asked of a monospaced face before the platform's
|
|
248
|
+
fallback list — on macOS that list opens with the system's
|
|
249
|
+
proportional face; a glyph still wider than its cells, two under
|
|
250
|
+
`wide`, is shaped at the size it fits at; and the room a narrower one
|
|
251
|
+
leaves is shared either side. The family's own glyphs and the private
|
|
252
|
+
use area's icons draw as they did.
|
|
253
|
+
|
|
254
|
+
- **What the `path` reviews left** (backlog RG112, the second review in
|
|
255
|
+
[ADR 0040](docs/adr/0040-a-path-is-a-mask-in-the-atlas.md)):
|
|
256
|
+
- A window closed while it showed an animating or a big path left the
|
|
257
|
+
path's texture with the renderer for the life of the process; the
|
|
258
|
+
core now hands it to the session as it goes, and the next frame any
|
|
259
|
+
window draws drops it. A frame built and never read keeps its
|
|
260
|
+
`dropped_textures` and `dropped_fragments` for the next one.
|
|
261
|
+
- A path that stopped moving stops costing a texture and a draw of
|
|
262
|
+
its own: after 120 still frames its mask is the atlas's again. A
|
|
263
|
+
row of icons keyed by position, whose neighbours came and went
|
|
264
|
+
twice within eight frames, read as animating and stayed so for
|
|
265
|
+
good.
|
|
266
|
+
- A stroke over a fill is hit as far as it is painted
|
|
267
|
+
(`HitShape::Path`'s `stroke`).
|
|
268
|
+
- Ops that are not the flat form raise `path-malformed` under the
|
|
269
|
+
node's key in every binding (`Core::path_flat_node`,
|
|
270
|
+
`Content::PathFlat`); C and Node drew nothing in silence.
|
|
271
|
+
- JSX types `width` on `line` and `path` as a number or a `$length`,
|
|
272
|
+
as the encoder reads it; `Core::image_pixels` answers for the
|
|
273
|
+
handle a path's `Texture` quad names.
|
|
274
|
+
|
|
275
|
+
- **What this release's own pre-tag pass found** (backlog RG114–RG117;
|
|
276
|
+
RG118 holds what it read and left), none of it in a release before
|
|
277
|
+
this one:
|
|
278
|
+
- An editor open when `set_fallback_fonts` was called kept the faces
|
|
279
|
+
its lines were shaped in until they were edited (RG114).
|
|
280
|
+
- The root's `gradient` was dropped while the devtools panel was
|
|
281
|
+
docked (RG115).
|
|
282
|
+
- A dashed `line` whose points coincide drew nothing and was still
|
|
283
|
+
hit; it is the dot its solid one is (RG116).
|
|
284
|
+
- A gradient's `radial` that is not a boolean is an error, where it
|
|
285
|
+
read as linear; its row says that fewer than two stops fail the
|
|
286
|
+
view in JSX and Lua and draw nothing in Rust and C; `kui.h` says
|
|
287
|
+
what a zeroed `KuiGradient` is (RG117).
|
|
288
|
+
|
|
289
|
+
**What you can delete.** A family chosen only because it covers a
|
|
290
|
+
script the preferred one lacks, which can give way to the preferred
|
|
291
|
+
one with the fallback list named; the key an app gave a still icon only so a
|
|
292
|
+
neighbour's coming and going would not move it to a texture; the
|
|
293
|
+
invisible wider path laid under a thick-stroked one to catch the press
|
|
294
|
+
on its edge; the WGSL a card's two-colour fade was written in, and the
|
|
295
|
+
`fragment` that held its children; the loop that walked a connector's points and declared a
|
|
296
|
+
`line` per dash, and the arithmetic that kept its phase round a corner.
|
|
297
|
+
|
|
298
|
+
### Native verification
|
|
299
|
+
|
|
300
|
+
The by-hand round alpha.6 introduced (backlog R4), on 2026-10-05 — the
|
|
301
|
+
nine commits after the alpha.36 tag: `dash`, the `gradient` row, F120,
|
|
302
|
+
F121 and RG112 — with a regression pass over them first: three
|
|
303
|
+
read-only reviews (the dash, the gradient, the fonts with this
|
|
304
|
+
release's docs), each claim probed with a test before anything
|
|
305
|
+
changed. RG114–RG117 came of it and are in this release; RG118 holds
|
|
306
|
+
what was read and left. This round ran on the Mac alone; Windows and
|
|
307
|
+
Linux did not run it for this tag.
|
|
308
|
+
|
|
309
|
+
**macOS 27.0.1 on an M3 Pro MacBook Pro, rustc 1.99.0 (the toolchain
|
|
310
|
+
CI runs), Node 26.10.0, nu 0.116.0**, on the release commit's tree,
|
|
311
|
+
the workspace's own artifacts pruned and rebuilt. `cargo fmt --all
|
|
312
|
+
--check` and `cargo clippy --workspace --all-targets -- -D warnings`
|
|
313
|
+
are clean. `scripts/test.nu`, the workspace's tests with the
|
|
314
|
+
conformance feature: **1802 tests over 135 suites, 0 failed** (4
|
|
315
|
+
ignored). The C round, `cbuild --run`, passes its five checks; the
|
|
316
|
+
corpus passes its **57 scenes** in four adapters, `gradients` the new
|
|
317
|
+
one; the ABI is **24**. Node's `node --test test.mjs` under
|
|
318
|
+
`KUI_CONFORMANCE_REQUIRED=1`: **209 of 209**. `npm run gen` leaves no
|
|
319
|
+
diff, the examples typecheck and their lockfile installs, the headless
|
|
320
|
+
round passes all **35 drives**, the book builds and
|
|
321
|
+
`scripts/book-examples.nu --check` passes.
|
|
322
|
+
|
|
323
|
+
**The windowed round**, `smoke -- --node`, three times over: **51 Rust
|
|
324
|
+
examples and the eleven Node examples, each on both bases, 120 frames
|
|
325
|
+
each, every one exiting 0** — 124 windows, eight at a time, in 28 to
|
|
326
|
+
32 s — and `counter`, `host`, `c_panel` and `lua_panel` by hand under
|
|
327
|
+
`KUI_SMOKE_FRAMES=120`, each exiting 0 with nothing on stderr: **128
|
|
328
|
+
windows over five hosts.** In each of the three runs one window of the
|
|
329
|
+
first eight took the whole round to draw its frames — `cells`, then
|
|
330
|
+
`audio`, then `accessibility`, each a second and a half when run
|
|
331
|
+
alone — which reads as a window covered by the seven launched over it
|
|
332
|
+
and not drawn until they had gone; not compared against alpha.36's
|
|
333
|
+
build. The AX audit: **106/106**, the audited window raised to the
|
|
334
|
+
front by its pid first, and no warning on the fixture's stderr.
|
|
335
|
+
|
|
336
|
+
**The bench guard** against the alpha.36 tag, on the release commit's
|
|
337
|
+
tree: **green**, none of the 8 guarded rows more than 10% slower —
|
|
338
|
+
every one between −0.9% and +2.8% (the worst guarded run-to-run spread
|
|
339
|
+
2.1%). It was not so at first, twice over, and both are fixed in this
|
|
340
|
+
tree:
|
|
341
|
+
|
|
342
|
+
- `frame_10k_segments` read **+7.7%** (863 → 929 µs, ±1.0%) on solid
|
|
343
|
+
lines, and that was the dash (backlog C52): bisected to V2's commit
|
|
344
|
+
by building the bench at each one. The `Stroke`, twenty bytes wider
|
|
345
|
+
with its `Dash`, was copied at each call from `Ui::line` down to the
|
|
346
|
+
line store, and the dash's cut ran its whole test on every line. The
|
|
347
|
+
stroke is lent and a solid pattern is told by its zeroes; the row
|
|
348
|
+
reads +2.5% in the guard and 875 → 880 µs with the two binaries
|
|
349
|
+
alternated.
|
|
350
|
+
- `frame_10k_rects` read +9.3% and every row built on the bench's grid
|
|
351
|
+
5 to 9% slower, and that was the bench: bisected to the commit that
|
|
352
|
+
gave its grid the gradient rows' arms in one `match` per cell, with
|
|
353
|
+
no line of the core between it and the commit before. The gradient
|
|
354
|
+
cell is a function of its own now, the plain rows read as
|
|
355
|
+
alpha.36's, and the gradient rows were taken again — 52 ns a node
|
|
356
|
+
over a flat box where the first reading said 55
|
|
357
|
+
([ADR 0042](docs/adr/0042-a-gradient-is-an-image-the-core-paints.md),
|
|
358
|
+
*Measured*; [docs/performance.md](docs/performance.md) carries the
|
|
359
|
+
four rows from that run, the rest of its table kept as it was).
|
|
360
|
+
|
|
361
|
+
**This tag was cut twice.** The first, at `c7e9793`, published
|
|
362
|
+
nothing: both hosts' `check` failed on F121's own test, which asserted
|
|
363
|
+
that the platform's list draws `字` differently from the app's — and
|
|
364
|
+
on an image with DejaVu alone, both pipelines', the fixture is the one
|
|
365
|
+
face that has it, list or no list. Forgejo's `check` had been red on
|
|
366
|
+
it since F121 was merged, which this round did not read before
|
|
367
|
+
tagging; cargo stops at the first failing suite, so the suites after
|
|
368
|
+
`fonts` had not run there either. The test now tells which machine it
|
|
369
|
+
is on and asserts the platform's half only where there is one, and
|
|
370
|
+
`cargo test -p kui-core` passes its 105 suites in `rust:1-bookworm`
|
|
371
|
+
with `fonts-dejavu-core` alone (the whole workspace would not fit that
|
|
372
|
+
container). With no crate and no package carrying the version, the tag
|
|
373
|
+
was moved to the commit that has the fix and C52's; the mechanical
|
|
374
|
+
round, the windowed round and the guard above are of that commit.
|
|
375
|
+
|
|
24
376
|
## 0.1.0-alpha.36 (2026-10-05)
|
|
25
377
|
|
|
26
378
|
**What breaks.**
|
|
@@ -127,6 +127,11 @@ silent surprise.
|
|
|
127
127
|
|
|
128
128
|
### Gradients: out of scope for v0
|
|
129
129
|
|
|
130
|
+
> **Superseded by [ADR 0042](0042-a-gradient-is-an-image-the-core-paints.md)
|
|
131
|
+
> (2026-10-05):** a box takes a `gradient`, linear or radial, rasterized
|
|
132
|
+
> once into the atlas and drawn as an image quad. What follows is why it
|
|
133
|
+
> was not in v0.
|
|
134
|
+
|
|
130
135
|
Two colors and a direction sound like one more `PROPS` row and are not. A
|
|
131
136
|
gradient needs a stop list (so: a `Keyframes`-shaped parse, in five
|
|
132
137
|
bindings), a type (linear, radial, conic), a geometry (angle or two points,
|
|
@@ -408,3 +408,49 @@ binding: two nodes on a clipping canvas are panned half past its top edge,
|
|
|
408
408
|
one clipped and one not. A press over the toolbar where the clipped node's
|
|
409
409
|
cut half would be reaches the toolbar, and the same press on the other
|
|
410
410
|
node reaches that node.
|
|
411
|
+
|
|
412
|
+
## Amendment: a dash is cut in the core
|
|
413
|
+
|
|
414
|
+
*2026-10-05, backlog V2.* Decision 9 left dashes out, and the options
|
|
415
|
+
said why: a `dash` that restarted at every join of a curve would read as
|
|
416
|
+
a bug. A view asked, so it is built, and not the way the option sketched.
|
|
417
|
+
|
|
418
|
+
**The sketch was a phase per quad.** `params.w` would carry how far
|
|
419
|
+
along the stroke each segment starts, and the backend would cut the
|
|
420
|
+
capsule by the pattern. That is one quad per piece whatever the pattern,
|
|
421
|
+
and it is a change to every renderer: the wgpu shader, and each host
|
|
422
|
+
that draws the list itself, which would draw a dashed stroke solid until
|
|
423
|
+
it caught up — with nothing to tell it so.
|
|
424
|
+
|
|
425
|
+
**What is built is a cut in the core.** A dashed stroke's run is walked
|
|
426
|
+
along its arc length (`line::Cut::marks`) and each mark is emitted as
|
|
427
|
+
what it is, a short round-capped stroke: one `Segment` quad per mark per
|
|
428
|
+
piece the mark lies on, two meeting at the corner for a mark that turns
|
|
429
|
+
one. No backend changed, the corpus pins the cut by its segment count,
|
|
430
|
+
and the phase is kept by construction, since the walk does not know
|
|
431
|
+
where the pieces end. It costs quads in proportion to the marks rather
|
|
432
|
+
than the pieces — a 1000 px line at a 10 px period is 100 — and a
|
|
433
|
+
stroke that would be more than 16384 marks, or whose mark and gap come
|
|
434
|
+
to under a physical pixel, draws solid.
|
|
435
|
+
|
|
436
|
+
**The lengths are the ones seen.** Caps are round (decision 2), so a
|
|
437
|
+
mark `on` long is a capsule with a centre line of `on − width`, and the
|
|
438
|
+
gap's centre line takes the difference; a mark no longer than the stroke
|
|
439
|
+
is wide is a dot. SVG's `stroke-dasharray` measures the centre line,
|
|
440
|
+
and with a round cap its `4 4` at a width of 4 is a solid line, which
|
|
441
|
+
is the first thing anyone writes. The first mark's cap sits where the
|
|
442
|
+
solid stroke's would.
|
|
443
|
+
|
|
444
|
+
**The same pattern on a `path`.** A path's stroke is a mask
|
|
445
|
+
([ADR 0040](0040-a-path-is-a-mask-in-the-atlas.md), whose decision 3
|
|
446
|
+
left `dash` to V2), so the centre lengths go to the rasterizer and the
|
|
447
|
+
mask is keyed by them. The pattern restarts at each subpath, as SVG's
|
|
448
|
+
does. An offset that changes every frame is read as the path's shape
|
|
449
|
+
changing, so a marching outline takes a texture of its own instead of
|
|
450
|
+
an atlas slot a frame.
|
|
451
|
+
|
|
452
|
+
**What it does not do.** Neither `dash` nor `dashOffset` tweens; a
|
|
453
|
+
translucent dashed polyline double-blends where two halves of one mark
|
|
454
|
+
meet at a corner, as a solid one does at every join; and the hit region
|
|
455
|
+
is the whole stroke, gaps included — the target is the stroke, not the
|
|
456
|
+
ink.
|
|
@@ -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`.
|
|
@@ -127,7 +127,9 @@ date: 2026-10-04
|
|
|
127
127
|
wedge; the stroke's colour tweens as a line's does. `fillRule` is
|
|
128
128
|
`nonzero` (SVG's default; a self-intersecting outline fills its
|
|
129
129
|
overlaps) or `evenodd` (the `polygon`'s rule, kept there unchanged).
|
|
130
|
-
Joins and caps are round, as a `line`'s are; `dash` stays V2's
|
|
130
|
+
Joins and caps are round, as a `line`'s are; `dash` stays V2's
|
|
131
|
+
(built 2026-10-05: the stroke's mask is cut by the pattern, see
|
|
132
|
+
[ADR 0010's amendment](0010-a-segment-primitive.md#amendment-a-dash-is-cut-in-the-core)).
|
|
131
133
|
4. **Rasterized on the CPU, through zeno, at the node's physical scale.**
|
|
132
134
|
The ops are transformed to physical px and rasterized into an 8-bit
|
|
133
135
|
coverage mask the size of the outline's bounding box plus a pixel on
|
|
@@ -181,7 +183,8 @@ date: 2026-10-04
|
|
|
181
183
|
that is different each frame; a shape that must move cheaply every
|
|
182
184
|
frame at any size stays a `polygon`, whose SDF costs the quad and
|
|
183
185
|
nothing else. A path still for two frames after animating stays
|
|
184
|
-
texture-backed; the rule is one-way, as the image's is.
|
|
186
|
+
texture-backed; the rule is one-way, as the image's is. *(Amended:
|
|
187
|
+
it returns after 120 still frames — see the second review below.)*
|
|
185
188
|
9. **Hit by its outline, nonzero or even-odd as it fills.** Emission
|
|
186
189
|
flattens the ops once — the same flattening the rasterizer gets — into
|
|
187
190
|
`Interaction::shape_points`, and `HitShape::Polygon` gains the fill
|
|
@@ -399,6 +402,45 @@ allocation, not the core, was the difference, and the bench now builds
|
|
|
399
402
|
its geometry once, as the polygon bench's points are. An app keeps its
|
|
400
403
|
geometry too.
|
|
401
404
|
|
|
405
|
+
### Second review (2026-10-05, before the alpha.36 tag and after it)
|
|
406
|
+
|
|
407
|
+
The pre-tag regression pass read the built element again (backlog
|
|
408
|
+
RG107–RG112). What it changed in the decisions above:
|
|
409
|
+
|
|
410
|
+
- **Decision 8 is not one-way.** An animating key whose shape has held
|
|
411
|
+
for 120 frames (`path::SETTLED_AFTER`, a second at 120 Hz) is a shape
|
|
412
|
+
again: its mask goes back to the atlas and its texture is dropped.
|
|
413
|
+
The latch was for what moves; held for good it also caught a key from
|
|
414
|
+
the tree position whose siblings came and went twice within the
|
|
415
|
+
window, and a spinner that had stopped — each a texture, a bind group
|
|
416
|
+
and a draw of its own for the life of the node. A pause shorter than
|
|
417
|
+
that stays out, so a spinner that stutters does not churn the page,
|
|
418
|
+
and moving again takes two changes, as the first time. The image's
|
|
419
|
+
rule is unchanged: an image's backing is declared by its updates, a
|
|
420
|
+
path's is inferred.
|
|
421
|
+
- **Decision 9, with a stroke over a fill.** A path that may paint a
|
|
422
|
+
fill — a `bg`, or one a hover, a press or the focus brings — and has
|
|
423
|
+
a stroke is hit by the fill *or* within half the stroke's width of
|
|
424
|
+
the stroke's pieces (`HitShape::Path`'s `stroke`), so the outer half
|
|
425
|
+
of a thick stroke is hit where it is painted. A stroke with no fill
|
|
426
|
+
at all is still `HitShape::Segments`, with its minimum grab.
|
|
427
|
+
- **A texture of its own is the session's to drop when its window
|
|
428
|
+
goes.** A core's `PathTextures` hands what it still holds to the
|
|
429
|
+
session as it is dropped, so the next list any window builds carries
|
|
430
|
+
the drop, as a removed image's does; and a frame whose list nobody
|
|
431
|
+
read (`Core::output`) hands its drops to the next, since an animating
|
|
432
|
+
path sweeps a texture every frame and a frame built twice before a
|
|
433
|
+
render lost one each time.
|
|
434
|
+
- **The flat form is checked in the core.** `Core::path_flat_node`
|
|
435
|
+
(and `Content::PathFlat` for a binding that lowers props) reads the
|
|
436
|
+
floats and raises `path-malformed` under the node's key when they are
|
|
437
|
+
not the form — what C and Node drew nothing for in silence and Lua
|
|
438
|
+
failed the view for. A number that is not finite, in either form or
|
|
439
|
+
in the turn, is `path-malformed` too (RG107).
|
|
440
|
+
- **The limit is the device's.** `kui-wgpu` opens its device with
|
|
441
|
+
wgpu's default limits, so 8192 is what every device it draws on
|
|
442
|
+
holds; the gate counts the mask's own margin (RG111).
|
|
443
|
+
|
|
402
444
|
## Action items — all done 2026-10-05
|
|
403
445
|
|
|
404
446
|
- [x] `crates/kui-core`: `path.rs` (ops, store, `Path` builder,
|