@qxuken/kui 0.1.0-alpha.47 → 0.1.0-alpha.49
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 +211 -0
- package/docs/adr/0016-caching-against-the-last-frame.md +17 -0
- package/docs/adr/0025-the-image-is-the-canvas.md +10 -2
- package/docs/adr/0044-an-image-drawn-smaller-is-drawn-from-a-level.md +229 -0
- package/docs/adr/0045-a-slot-replayed-by-its-host.md +237 -0
- package/encoder.js +11 -3
- package/howto.md +76 -2
- package/index.d.ts +43 -7
- package/index.js +2 -2
- package/jsx-runtime.d.ts +14 -6
- 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 +10 -7
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,217 @@ 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.49 (2026-10-09)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **A Lua view lowers in about two-thirds of the time** (backlog F143).
|
|
29
|
+
kui-lua reads each table in one pass: `Table::for_each` with a key
|
|
30
|
+
type that copies a string key's bytes off the Lua stack instead of
|
|
31
|
+
making a registry reference for it (a `pairs` step made one for every
|
|
32
|
+
key and every string or table value, and dropped it again), the keys
|
|
33
|
+
matched as bytes, the schema rows looked up in a hash map rather than
|
|
34
|
+
scanned, `size` and `radius` read in the same pass and still applied
|
|
35
|
+
before the rows they underlie, a text's `value` and `spans` taken from
|
|
36
|
+
it, and a span's twelve style keys, a `pad` table's seven edges and
|
|
37
|
+
the unknown-prop check — a second walk over every table while the
|
|
38
|
+
core's diagnostics are on — folded into it. Nothing a view writes
|
|
39
|
+
changes, and a table in an unusual shape (a number for a text's
|
|
40
|
+
`value`, a span's `size` as a string) is read through `get` as before.
|
|
41
|
+
Measured over a settings pane's worth of tables (`cargo bench -p
|
|
42
|
+
kui-lua --bench walk`, medians): **2.55 → 1.64 ms** a frame with the
|
|
43
|
+
diagnostics off, as a release build of the windowed runner has them,
|
|
44
|
+
and **3.42 → 1.75 ms** with them on; in kawoosh's settings, themes,
|
|
45
|
+
theme lab and grammars panes the walk went 3.45 → 2.27, 3.51 → 2.13,
|
|
46
|
+
2.12 → 1.42 and 1.46 → 1.06 ms on the frames a pane is rebuilt in.
|
|
47
|
+
The rest of the walk is mlua's own reference for each string and
|
|
48
|
+
table value, which its safe API does not let a binding skip.
|
|
49
|
+
|
|
50
|
+
**What you can delete.**
|
|
51
|
+
|
|
52
|
+
- Hand-flattening a Lua view's tree, or building it in fewer, larger
|
|
53
|
+
tables, to keep the lowering off the frame: kawoosh's settings pane
|
|
54
|
+
lowers at about 1.4 µs a table where it took 2.2.
|
|
55
|
+
|
|
56
|
+
### Native verification
|
|
57
|
+
|
|
58
|
+
The by-hand round on 2026-10-09, over F143 — kui-lua's walk from a
|
|
59
|
+
view's tables to the tree — on the Mac: the mechanical round as CI runs
|
|
60
|
+
it, the Odin binding checked with CI's pinned Odin in its Debian image,
|
|
61
|
+
the windowed round, the accessibility audit and the bench guard.
|
|
62
|
+
|
|
63
|
+
**macOS**, the pre-tag pass. fmt and clippy are clean; `nu
|
|
64
|
+
scripts/test.nu`: **2034 tests over 152 suites**, 0 failed. The C round
|
|
65
|
+
passes (5 checks), and so do the **58 scenes** through Rust, Lua, C and
|
|
66
|
+
Node; Node's tests under `KUI_CONFORMANCE_REQUIRED=1`, **225 of 225**;
|
|
67
|
+
`npm run gen` with no diff, the examples' typecheck, the headless round
|
|
68
|
+
(4.0 s). `nu scripts/odin.nu gen --check` says the binding is current
|
|
69
|
+
and `odin.nu check` vets both packages and every example. The windowed
|
|
70
|
+
round with Node's: **55 examples on both bases**, clean on a first run,
|
|
71
|
+
and `counter`, `host`, `c_panel` and `lua_panel` for 120 frames each.
|
|
72
|
+
The accessibility audit: **106 of 106**. The bench guard against the
|
|
73
|
+
alpha.48 tag: **green**, the eight guarded rows within tolerance; the
|
|
74
|
+
new `walk` rows are in docs/performance.md. No Windows or Linux machine
|
|
75
|
+
ran this round.
|
|
76
|
+
|
|
77
|
+
## 0.1.0-alpha.48 (2026-10-09)
|
|
78
|
+
|
|
79
|
+
**What breaks.**
|
|
80
|
+
|
|
81
|
+
- C ABI 28 (under Added, W22): `KuiRunConfig` appends `titlebar`, a
|
|
82
|
+
`KUI_TITLEBAR_*` (64-bit size 48). Recompile; zeroed, the window is
|
|
83
|
+
what it was. A hand-written mirror (ctypes, Zig) appends one `u32`.
|
|
84
|
+
- `ui.metrics().titlebar_h` under macOS custom chrome (under Added,
|
|
85
|
+
W22) reads the measured titlebar, 32 on macOS 27, where it read the
|
|
86
|
+
stock 34; the strip itself was already drawn at 32. A view that laid
|
|
87
|
+
something out against the metric beside the strip moves up 2 px.
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- **The traffic lights sit lower in a taller titlebar** (backlog W22).
|
|
92
|
+
`Launcher::titlebar(Titlebar::Medium | Tall)` under `Chrome::Custom`
|
|
93
|
+
makes macOS's own titlebar a compact toolbar's height (40 pt, the
|
|
94
|
+
lights at 12,13 on macOS 27) or a full toolbar's (52 pt about the
|
|
95
|
+
lights, at 19,19), against the plain one's 32 at 9,9 — the look of a
|
|
96
|
+
Mac app with a toolbar, with the app's own strip in it. kui gives the
|
|
97
|
+
window an empty `NSToolbar` in that style and leaves the buttons to
|
|
98
|
+
AppKit, which keeps them in place through resizing, fullscreen and
|
|
99
|
+
focus; nothing runs per frame. `env.window.native_controls` measures
|
|
100
|
+
where they landed, so `widgets::titlebar` grows and insets with them,
|
|
101
|
+
and the toolbar takes no clicks: tabs in the strip press and the empty
|
|
102
|
+
strip drags as before. `titlebar: 'standard' | 'medium' | 'tall'` in
|
|
103
|
+
Node's window options, `KuiRunConfig.titlebar` in C,
|
|
104
|
+
`Run_Config.titlebar` in Odin. Under macOS custom chrome the
|
|
105
|
+
window's `titlebar_h` metric is now the strip AppKit drew, as
|
|
106
|
+
measured — 32, 40 or 52 on macOS 27 — so `$titlebar_h` says the
|
|
107
|
+
strip's height. On Windows and Linux, where the strip
|
|
108
|
+
is the app's, the same ask makes the window's `titlebar_h` metric 40
|
|
109
|
+
or 52 in place of the caption's 32 or 34, so one setting draws one
|
|
110
|
+
strip on all three: `ui.metrics().titlebar_h` and `$titlebar_h` read
|
|
111
|
+
it, the drawn buttons grow to it, and an app's own metrics keep it
|
|
112
|
+
while their `titlebar_h` is the stock number
|
|
113
|
+
(`Core::set_platform_titlebar_h`). The `titlebar` example takes
|
|
114
|
+
`--titlebar tall`.
|
|
115
|
+
|
|
116
|
+
||||||| parent of 2ba22215 (A slot replayed by its host: the extension spared when the host says nothing it feeds it changed, and the core checks everything else (backlog F142, ADR 0045 amending ADR 0016; F143 filed))
|
|
117
|
+
- **A slot replayed by its host** (backlog F142, ADR 0045, amending ADR
|
|
118
|
+
0016). `Ui::slot_kept` fills a slot as `slot_with` does and keeps what
|
|
119
|
+
the fill built — every node as its door saw it, before hover, accent
|
|
120
|
+
and easing touched the spec; the labels, indices and hints beside
|
|
121
|
+
them; the slots it declared inside; and every fact of the frame it
|
|
122
|
+
read while it ran. `Ui::slot_replay` is the host's claim that nothing
|
|
123
|
+
*it* feeds the extension has changed; the core checks everything it
|
|
124
|
+
can see — the params, each fact read against its value now, that the
|
|
125
|
+
slot is where it was — and pushes the kept nodes again through the
|
|
126
|
+
same doors without asking the extension (`SlotFill::Replayed`), or
|
|
127
|
+
runs it as `slot_kept` would and says why (`NotKept`, `Params`,
|
|
128
|
+
`Reads`, `NotReplayable`, `Moved`); `Core::slot_fill(name)` reads the
|
|
129
|
+
answer back. A nested slot is declared again and filled fresh, so an
|
|
130
|
+
editor's field blinks inside a pane that is not rebuilt. A fill that
|
|
131
|
+
declared something of the frame (a title, a window, a frame it wants,
|
|
132
|
+
a devtools tab, audio, a loaded extension), drew a `cells` grid, or
|
|
133
|
+
failed is kept as not replayable, and so is one that pushed a node
|
|
134
|
+
through a door the journal does not know — by count, so a door added
|
|
135
|
+
later is a fresh fill and never a wrong one. Nothing of the frame is
|
|
136
|
+
cached: layout and emission run as always; what is skipped is the
|
|
137
|
+
extension's `view` and, for a Lua or C fill, the binding's walk from
|
|
138
|
+
its tables to the tree, which is where such a fill spends most of its
|
|
139
|
+
time (about 2 µs a node, sixty times the push). C: `kui_slot_kept`,
|
|
140
|
+
`kui_slot_replay`, `kui_slot_fill` and the `KUI_SLOT_*` codes (new
|
|
141
|
+
symbols; the ABI version stays). Lua: `fill { keep = true }`, `fill {
|
|
142
|
+
replay = true }`, `env.slot_fill(name)`. Node: `<slot keep>`, `<slot
|
|
143
|
+
replay>`, `ctx.slotFill(name)` (stream v23: `slot` carries a flags
|
|
144
|
+
word). A reading handed out whole (Lua's `env`) cannot say whether a
|
|
145
|
+
script used its clock or caret phase, so those two are left out of
|
|
146
|
+
the comparison and a view that draws from them is one its host must
|
|
147
|
+
not replay; the explicit doors still note the read. Tests:
|
|
148
|
+
`tests/slot_replay.rs` (nine), kui-lua's `slots.rs`, the C slots
|
|
149
|
+
host's `--headless`, Node's `test.mjs` with the C plugin; the bench
|
|
150
|
+
`slot_1500_nodes_fresh` against `slot_1500_nodes_replayed` (287 against
|
|
151
|
+
214 µs a frame for a Rust fill, the gap its own pushes; a Lua fill's
|
|
152
|
+
gap is its whole walk).
|
|
153
|
+
- **An image drawn smaller is drawn from a level the core halves**
|
|
154
|
+
(backlog V6, ADR 0044). An `image` drawn at less than half its
|
|
155
|
+
texels a pixel — a photo on a card, a thumbnail — samples a level
|
|
156
|
+
of the image halved on the CPU, 2×2 means in linear light with the
|
|
157
|
+
alpha premultiplied, instead of skipping texels: no shimmer as it
|
|
158
|
+
moves, no false patterns in fine detail. The level is the deepest
|
|
159
|
+
with at least a texel a pixel, counting the display's scale and any
|
|
160
|
+
`scale` the node is drawn through; it is made once per session at
|
|
161
|
+
the first draw that wants it (about 10 ms for a 12-megapixel photo's
|
|
162
|
+
first level) and kept in the atlas in place of the whole image, so a
|
|
163
|
+
photo only ever shown small never puts its full size in the page.
|
|
164
|
+
`sampling: nearest` and an image ever updated (a stream) draw the
|
|
165
|
+
whole image, as before. Nothing to declare and no binding changed:
|
|
166
|
+
a C host drawing `KuiDrawData` finds the level in the page it already
|
|
167
|
+
uploads. The `image` example shows a zone plate both ways.
|
|
168
|
+
|
|
169
|
+
### Fixed
|
|
170
|
+
|
|
171
|
+
- **A handler reads the clock at the time it runs** (backlog F139).
|
|
172
|
+
The windowed runner set the frame clock only when it drew, and the
|
|
173
|
+
loop parks between frames, so after an idle stretch `now()` in an
|
|
174
|
+
event handler read the last frame's time: a toast stamped in a click
|
|
175
|
+
handler counted from a frame drawn seconds before. The runner now
|
|
176
|
+
stamps every window's clock before it handles input and before
|
|
177
|
+
events reach the app, and `Drive` does the same after `advance`. A
|
|
178
|
+
composite's type-ahead ages at the keystroke as well as at the frame,
|
|
179
|
+
so a letter typed after a quiet second starts a new search, and a
|
|
180
|
+
Space after the pause presses the item rather than extending the old
|
|
181
|
+
one.
|
|
182
|
+
- **`ambiguous-name` points a caption at `role="none"`** (backlog
|
|
183
|
+
F140). When the nodes sharing a name are a control and the text
|
|
184
|
+
beside it that repeats it — a round "like" button over the word
|
|
185
|
+
"like" — the warning says the caption is decorative and that
|
|
186
|
+
`role="none"` on the box around it takes it out of the tree, or that
|
|
187
|
+
the control's `label` can say what it does. The `role` row calls
|
|
188
|
+
`none` the decorative door.
|
|
189
|
+
- **The docs say what a spring does between keyframe stops** (backlog
|
|
190
|
+
F141): it is drawn as `easeOut`, and `bounce` does nothing there,
|
|
191
|
+
since a cycle is sampled off the clock; an overshoot in a cycle is a
|
|
192
|
+
stop past the target. Unchanged behaviour, now on the `keyframes`,
|
|
193
|
+
`easing` and `bounce` rows and pinned by a test.
|
|
194
|
+
|
|
195
|
+
**What you can delete.**
|
|
196
|
+
|
|
197
|
+
- Throttling or skipping a plugin pane's frames on the host's side —
|
|
198
|
+
drawing it every other frame, or only on its own events — to keep a
|
|
199
|
+
Lua or C extension's `view` off the frames it draws nothing new in:
|
|
200
|
+
`slot_replay` with what the pane reads from the host as the condition.
|
|
201
|
+
- Stamping deadlines in `view` because a handler's `now()` was stale:
|
|
202
|
+
`core.now()` read in `on_event_with` is the time of the event.
|
|
203
|
+
- Resampling a photo to the size it is shown at before `add_image`:
|
|
204
|
+
register it as decoded and declare the box.
|
|
205
|
+
|
|
206
|
+
### Native verification
|
|
207
|
+
|
|
208
|
+
The by-hand round on 2026-10-09, over F142 and W22 — the slot replayed
|
|
209
|
+
by its host, and the taller macOS titlebar with the metric that follows
|
|
210
|
+
it — on the Mac: the mechanical round as CI runs it, the Odin binding
|
|
211
|
+
regenerated and type-checked with CI's pinned Odin (`dev-2026-09`, in a
|
|
212
|
+
Debian image with the checkout mounted at its own path, since the Mac
|
|
213
|
+
has no `odin`), the windowed round, the accessibility audit and the
|
|
214
|
+
bench guard. The first attempt of the round filled the disk — 118 MB
|
|
215
|
+
left under three build trees — and was run again whole once 61 GB were
|
|
216
|
+
cleared; nothing of it is read from the partial run.
|
|
217
|
+
|
|
218
|
+
**macOS**, the pre-tag pass. fmt and clippy are clean; `nu
|
|
219
|
+
scripts/test.nu`: **2032 tests over 152 suites**, 0 failed. The C round
|
|
220
|
+
passes (5 checks), and so do the **58 scenes** through Rust, Lua, C and
|
|
221
|
+
Node; Node's tests under `KUI_CONFORMANCE_REQUIRED=1`, **225 of 225**;
|
|
222
|
+
`npm run gen` with no diff beyond the three `DOORS` rows of this
|
|
223
|
+
release, the examples' typecheck, the headless round (3.4 s). `nu
|
|
224
|
+
scripts/odin.nu gen --check` says the binding is current and `odin.nu
|
|
225
|
+
check` vets both packages and every example; its `test` and `slots`
|
|
226
|
+
rounds run Linux binaries and are CI's. The windowed round with Node's:
|
|
227
|
+
**55 examples on both bases**, clean on a first run, and `counter`,
|
|
228
|
+
`host`, `c_panel` and `lua_panel` for 120 frames each. The accessibility audit:
|
|
229
|
+
**106 of 106**. The bench guard against the alpha.47 tag: **green**, the
|
|
230
|
+
eight guarded rows within tolerance, and the two new rows
|
|
231
|
+
`slot_1500_nodes_fresh` / `slot_1500_nodes_replayed` at 277 and 208 µs
|
|
232
|
+
(now in docs/performance.md). No Windows or Linux machine ran this
|
|
233
|
+
round.
|
|
234
|
+
|
|
24
235
|
## 0.1.0-alpha.47 (2026-10-09)
|
|
25
236
|
|
|
26
237
|
### Fixed
|
|
@@ -5,6 +5,23 @@ date: 2026-09-09
|
|
|
5
5
|
|
|
6
6
|
# Caching against the last frame: the access tree yes, the frame no
|
|
7
7
|
|
|
8
|
+
> **Amended 2026-10-09 by [ADR 0045](0045-a-slot-replayed-by-its-host.md).**
|
|
9
|
+
> Decision 2 below refuses a `memo` element in the form that asks the
|
|
10
|
+
> *view* to declare its own dependencies, and it stands as written. ADR
|
|
11
|
+
> 0045 admits a different form: the *host* declares an extension's slot
|
|
12
|
+
> unchanged (`Ui::slot_replay`), vouching only for what the extension
|
|
13
|
+
> reads from the host itself, and the core checks everything it can see
|
|
14
|
+
> — the params, every fact of the frame the kept fill read, with the
|
|
15
|
+
> value read compared against the value now — before pushing the kept
|
|
16
|
+
> fill's nodes again through the doors they came in by. Nothing of the
|
|
17
|
+
> frame is cached: layout and emission run as they always did, and
|
|
18
|
+
> decision 1 is untouched. What moved the line is a measurement that
|
|
19
|
+
> this document did not have: a Lua extension's fill costs about 2 µs a
|
|
20
|
+
> node in the binding's walk from its tables to the tree, sixty times the
|
|
21
|
+
> push, on frames where the pane did nothing — the half of the bill
|
|
22
|
+
> option C alone addressed, which is why "it will keep being proposed"
|
|
23
|
+
> was written here and why its proposal now has a document.
|
|
24
|
+
|
|
8
25
|
> **Accepted (2026-09-09), and its one yes is built.** Out of the
|
|
9
26
|
> performance round that shipped the quad shrink and closed C24. It asks
|
|
10
27
|
> one question — may a frame reuse work from the frame before it, and
|
|
@@ -249,7 +249,11 @@ date: 2026-09-11
|
|
|
249
249
|
one at another; it is the node's to say, like `radius`.
|
|
250
250
|
- **Mipmaps.** Deferred: `contain`/`cover` plus an app that renders at
|
|
251
251
|
`w × scale` pixels needs none; a photo viewer minifying a 12-megapixel
|
|
252
|
-
texture does, and that is the view that files it.
|
|
252
|
+
texture does, and that is the view that files it. Filed by a card game
|
|
253
|
+
instead (backlog V6, 2026-10-09) and built as [ADR
|
|
254
|
+
0044](0044-an-image-drawn-smaller-is-drawn-from-a-level.md): levels the
|
|
255
|
+
core halves on the CPU and keeps in the atlas, for every image but a
|
|
256
|
+
stream.
|
|
253
257
|
- **A core `zoom` row** — a per-subtree scale as scroll's sibling, layout
|
|
254
258
|
in the subtree's own logical space, `env.scale × zoom` composed at emit
|
|
255
259
|
(`runtime/emit.rs:418` is the one seam), local-space payloads and
|
|
@@ -346,7 +350,11 @@ the one to read carefully: at a hundred boxes the texture case is slower
|
|
|
346
350
|
than the fragment case not because of the split but because each box
|
|
347
351
|
samples a 1920×1080 texture minified into 320×180 pixels with no mips —
|
|
348
352
|
that is the fill's bill, and the mipmap deferral (V6) is what it argues
|
|
349
|
-
for once a view minifies a large stream.
|
|
353
|
+
for once a view minifies a large stream. (Measured again on 2026-10-09
|
|
354
|
+
for ADR 0044 on an M-series Mac, with texels a GPU cannot compress:
|
|
355
|
+
the same hundred boxes drawn from the texture halved twice saved 0.03
|
|
356
|
+
ms of the 0.79 — minification is a small part of that bill there, and
|
|
357
|
+
the rest of the gap to the fragments' 0.46 is not minification.) At the sizes an app draws a
|
|
350
358
|
stream (one box, near its own size) the row that matters is 1: within
|
|
351
359
|
noise of no split at all.
|
|
352
360
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-10-09
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# An image drawn smaller is drawn from a level the core halves
|
|
7
|
+
|
|
8
|
+
> **Accepted and built 2026-10-09**, the day it was proposed; the
|
|
9
|
+
> *Amendment* at the end records what the building measured and
|
|
10
|
+
> changed. Backlog V6's second half, its condition met
|
|
11
|
+
> by berainder's alpha.47 upgrade: the app shows its bear photos on
|
|
12
|
+
> cards smaller than the files, and smaller again on the 0.9-scaled
|
|
13
|
+
> card underneath, and keeps a 25-line crop-and-resample in `bears.rs`
|
|
14
|
+
> to bring each photo down to card size before `add_image`, because the
|
|
15
|
+
> renderer samples the whole image with no mip chain. [ADR
|
|
16
|
+
> 0025](0025-the-image-is-the-canvas.md) deferred mipmaps until "a
|
|
17
|
+
> photo viewer minifying a 12-megapixel texture" filed them; this is
|
|
18
|
+
> that, from a card game. V6's first half, dirty rects on
|
|
19
|
+
> `update_image`, keeps its own condition and is not here.
|
|
20
|
+
|
|
21
|
+
## Context
|
|
22
|
+
|
|
23
|
+
- **What a minified image costs today.** An `image` node's quad samples
|
|
24
|
+
the image's texel rect bilinearly — four texels around each pixel
|
|
25
|
+
centre — whatever the ratio of texels to pixels. At 1:1 to 2:1 that
|
|
26
|
+
is every texel read. At 4:1 three texels in four are skipped and the
|
|
27
|
+
ones read are an accident of where the pixel centres land: a photo
|
|
28
|
+
shrunk to a card shimmers as it moves, and its fine lines break up.
|
|
29
|
+
ADR 0025's split bench shows the GPU bill as well: a hundred 320×180
|
|
30
|
+
boxes each sampling a 1920×1080 texture ran at 0.59 ms against 0.36
|
|
31
|
+
for fragments of the same size, the difference being cache misses
|
|
32
|
+
from minified reads with no mip chain.
|
|
33
|
+
- **Where an image lives.** An image that fits a `MAX_ATLAS_SIZE`
|
|
34
|
+
(4096) page and was never updated is blitted into the window's glyph
|
|
35
|
+
atlas at first draw, whole, and drawn as an `Image` quad whose `uv` is
|
|
36
|
+
its texel rect in the page (`atlas.rs`, `get_or_insert_image`). One
|
|
37
|
+
past a page, or one ever updated, is texture-backed: its own texture,
|
|
38
|
+
a `Texture` quad and an entry in `DisplayList::textures`
|
|
39
|
+
(`ImageBacking`). A 4032×3024 phone photo is atlas-backed, and takes
|
|
40
|
+
48 MB of a page that also holds every glyph the window draws.
|
|
41
|
+
- **Who draws.** `kui-wgpu` is the renderer this repo ships; a C host
|
|
42
|
+
that draws `KuiDrawData` itself (ADR 0012's contract) uploads the
|
|
43
|
+
atlas page and the texture side list and samples what the quads
|
|
44
|
+
name. Whatever answers V6 has to reach that host too
|
|
45
|
+
([`feedback: bindings first`] — a capability goes where every binding
|
|
46
|
+
and every backend gets it).
|
|
47
|
+
- **What the core knows at emission.** The image's size, the texel rect
|
|
48
|
+
`fit` picked, the rect the quad is drawn at in physical px, and —
|
|
49
|
+
since ADR 0043 — the clip entry's composed `Transform`, whose `scale`
|
|
50
|
+
is the factor every ancestor's `scale` multiplied into. That is the
|
|
51
|
+
whole input to "how many texels per pixel", per quad, on the CPU.
|
|
52
|
+
|
|
53
|
+
## Decisions
|
|
54
|
+
|
|
55
|
+
1. **A level is the image halved, and halved again.** Level 0 is the
|
|
56
|
+
image; level *n* + 1 is level *n* with each 2×2 block averaged into
|
|
57
|
+
one texel, `⌈w/2⌉ × ⌈h/2⌉`, the last row and column of an odd size
|
|
58
|
+
repeated. The average is taken in linear light with the alpha
|
|
59
|
+
premultiplied — sRGB decoded through a 256-entry table, the result
|
|
60
|
+
encoded through a 4096-entry one — so a level is neither darker
|
|
61
|
+
than its source nor fringed where a transparent edge meets colour.
|
|
62
|
+
The chain stops at 1×1.
|
|
63
|
+
2. **The core picks the level per quad.** For an image drawn with
|
|
64
|
+
`sampling: linear` (the default), the ratio is
|
|
65
|
+
`r = min(texels_w / px_w, texels_h / px_h)`: the texel rect `fit`
|
|
66
|
+
picked over the drawn rect in physical px, times the clip entry's
|
|
67
|
+
composed `scale`. The level is `⌊log₂ r⌋`, at least 0, at most the
|
|
68
|
+
chain's last — the deepest level still at least as many texels as
|
|
69
|
+
pixels on both axes, so a level never magnifies and the bilinear
|
|
70
|
+
sample inside it minifies by less than two. The smaller ratio of the
|
|
71
|
+
two axes decides, so a `fill` stretched in one direction is never
|
|
72
|
+
blurred in the other. `sampling: nearest` always draws level 0 — it
|
|
73
|
+
asked for the texels as they are. Nothing new in any binding: no
|
|
74
|
+
row, no door, no ABI. The level is a fact the core derives, as
|
|
75
|
+
`ImageBacking` is.
|
|
76
|
+
3. **A level lives where level 0 would, in the atlas.** The page keys
|
|
77
|
+
image slots by `(image, level)`; a level is blitted at the first
|
|
78
|
+
draw that wants it and its slot named by the quad, an `Image` quad
|
|
79
|
+
as before. An image only ever drawn small never puts level 0 in the
|
|
80
|
+
page at all — the 48 MB photo drawn on a 400-point card at 2× takes
|
|
81
|
+
the 1008×756 level's 3 MB. `fit`'s crop is computed on the level's own
|
|
82
|
+
size, so `cover` crops whole texels of the level. A host that draws
|
|
83
|
+
`KuiDrawData` gets the level in the page it already uploads, and
|
|
84
|
+
changes nothing.
|
|
85
|
+
4. **A texture-backed image is levelled until it is updated.** One that
|
|
86
|
+
is texture-backed because it is larger than a page, and was never
|
|
87
|
+
updated, draws a level that fits a page from the atlas like any
|
|
88
|
+
other, and level 0 from its texture as before. One ever updated —
|
|
89
|
+
a stream — draws level 0, as today: halving a 1080p frame on every
|
|
90
|
+
update is milliseconds a frame, and a stream minified on screen is
|
|
91
|
+
V6's other half's condition, still unmet.
|
|
92
|
+
5. **Levels are made once per session, lazily, and shared.** The
|
|
93
|
+
halved pixels are kept on the image's entry in the session's
|
|
94
|
+
resources, so a second window, or the same window after its atlas
|
|
95
|
+
page was reset, blits them again without halving again. Level *n* is
|
|
96
|
+
made from level *n* − 1, which is kept too, so a chain costs a third
|
|
97
|
+
of the image again at most, and only the levels some draw asked for
|
|
98
|
+
(and those above them) are ever made. An `update_image` drops them;
|
|
99
|
+
`remove_image` drops them with the entry.
|
|
100
|
+
6. **Fragments sample level 0.** A `fragment`'s `image` input is
|
|
101
|
+
sampled at coordinates the shader computes, and the core does not
|
|
102
|
+
know the ratio there; it keeps the whole image. A fragment that
|
|
103
|
+
wants a smaller one is handed a smaller one.
|
|
104
|
+
|
|
105
|
+
## Consequences
|
|
106
|
+
|
|
107
|
+
- berainder deletes its resampler: a photo is registered as decoded and
|
|
108
|
+
declared at the size it is shown.
|
|
109
|
+
- A photo that animates its size across a power of two switches level
|
|
110
|
+
at the crossing — the image sharpens or softens by a step there,
|
|
111
|
+
where trilinear sampling would blend the two. At the ratios a card's
|
|
112
|
+
hover or its 0.9 under-scale move through, it does not cross; a
|
|
113
|
+
pinch-zoom would, and is where trilinear comes back (see *Open
|
|
114
|
+
questions*).
|
|
115
|
+
- The first draw of a large photo at a small size pays the halving on
|
|
116
|
+
the frame that draws it: about a quarter of the image's pixels a
|
|
117
|
+
level, so a 12-megapixel photo's first level is three million output
|
|
118
|
+
texels. Measured below. berainder paid the same in its own
|
|
119
|
+
resampler, before the first frame.
|
|
120
|
+
- A window's atlas holds less for a photo drawn small, and holds a
|
|
121
|
+
second slot for a photo drawn at two sizes at once (a card and its
|
|
122
|
+
thumbnail).
|
|
123
|
+
- Hit-testing, layout, `image_size`, access and the texture side list
|
|
124
|
+
are unchanged: a level changes which texels a quad names and nothing
|
|
125
|
+
else.
|
|
126
|
+
|
|
127
|
+
## Considered options
|
|
128
|
+
|
|
129
|
+
- **A mip chain on the GPU, made by the renderer.** The usual answer:
|
|
130
|
+
`mip_level_count` on the texture, a blit pass per level at upload, a
|
|
131
|
+
sampler with a mip filter, the hardware choosing per pixel from the
|
|
132
|
+
derivatives. Declined as the answer for atlas images: mip levels of
|
|
133
|
+
a page that packs glyphs, paths and images side by side bleed every
|
|
134
|
+
neighbour into every item from the second level down, unless every
|
|
135
|
+
item is padded to its level's alignment — and the C host that draws
|
|
136
|
+
`KuiDrawData` would need the same chain built its own way. For a
|
|
137
|
+
texture-backed *stream* it is still the right shape, and is what V6's
|
|
138
|
+
stream half would build; it composes with this.
|
|
139
|
+
- **Trilinear, two levels blended in the shader.** Declined for now:
|
|
140
|
+
the quad would carry two texel rects (the instance grows) and the
|
|
141
|
+
fragment stage would sample twice for every image, to smooth a
|
|
142
|
+
switch that only a size animated across a power of two shows.
|
|
143
|
+
- **A row, `mipmap` or `minify`, that the app sets.** Declined: there
|
|
144
|
+
is no image an app wants aliased when it is drawn smaller, and
|
|
145
|
+
`sampling: nearest` already says "these texels, as they are".
|
|
146
|
+
- **The app resamples, as berainder does.** The status quo. It works,
|
|
147
|
+
and every app that shows a photo writes it, picks a filter, and gets
|
|
148
|
+
sRGB wrong.
|
|
149
|
+
|
|
150
|
+
## Measurements to take
|
|
151
|
+
|
|
152
|
+
- The halving: a 4032×3024 level 0 to its first level, and the whole
|
|
153
|
+
chain, in release, on the Mac.
|
|
154
|
+
- The frame: a frame drawing a 1920×1080 photo in a hundred 320×180
|
|
155
|
+
boxes, before (level 0, every box) and after (level 2), CPU and GPU —
|
|
156
|
+
the row of ADR 0025's split bench that argued for this.
|
|
157
|
+
- The frame bench guard, which draws no minified image, unchanged.
|
|
158
|
+
|
|
159
|
+
All three are taken; see the *Amendment*.
|
|
160
|
+
|
|
161
|
+
## Open questions
|
|
162
|
+
|
|
163
|
+
- Trilinear for a size that animates through a power of two, when an
|
|
164
|
+
app pinch-zooms a photo.
|
|
165
|
+
- Making levels off the frame thread for a photo large enough that its
|
|
166
|
+
first draw is a dropped frame.
|
|
167
|
+
|
|
168
|
+
## Amendment (2026-10-09, built)
|
|
169
|
+
|
|
170
|
+
Built the day it was proposed, as decided, in `kui-core` alone:
|
|
171
|
+
`mip.rs` (`halve`, `depth`, `level_for` and the two sRGB tables),
|
|
172
|
+
`ImageEntry::level(n)` holding the chain behind a mutex on the
|
|
173
|
+
session's entry (`levels_allowed` is `rev == 0`; both updates drop it),
|
|
174
|
+
the page's image slots keyed `(ImageId, level)` with
|
|
175
|
+
`get_or_insert_image_level` taking the pixels as a closure so a level
|
|
176
|
+
already in the page is never made, and `Emit::image_level` choosing per
|
|
177
|
+
quad from the `fit` rect, the display's scale and the clip entry's
|
|
178
|
+
composed `scale`. No binding, no door and no ABI moved; `kui-wgpu` did
|
|
179
|
+
not change. What the building measured and changed:
|
|
180
|
+
|
|
181
|
+
- **The halving has an opaque fast path.** Written first with the
|
|
182
|
+
premultiplied weights for every block, a 4032×3024 photo's first
|
|
183
|
+
level took 13.4 ms; a block whose four alphas are 255 now takes the
|
|
184
|
+
plain mean, and the same level takes **10.1 ms**
|
|
185
|
+
(`benches/levels.rs`, `halve_4032x3024`). The first frame that draws
|
|
186
|
+
the photo on a 400×300 card at 2× — levels 1 to 3 made and level 3
|
|
187
|
+
blitted — is **13.2 ms**, once per photo per session; every frame
|
|
188
|
+
after reads **0.33 µs** for the whole one-image frame. That first
|
|
189
|
+
frame drops at 120 Hz; *Open questions* keeps making levels off the
|
|
190
|
+
frame thread.
|
|
191
|
+
- **The GPU half of the argument was smaller than ADR 0025 read it.**
|
|
192
|
+
`benches/split.rs` had not run since ADR 0043 grew the instance (its
|
|
193
|
+
mirror of `Instance` was 128 bytes against the renderer's 176, so the
|
|
194
|
+
pipeline refused to build); it is mirrored again, with `LEVEL=n`
|
|
195
|
+
(the 1080p texture halved `n` times) and `NOISE=1` (texels a GPU
|
|
196
|
+
cannot compress — the flat orange one read the same at level 0 and
|
|
197
|
+
level 2, 0.757 ms against 0.756, because a flat texture is free
|
|
198
|
+
however it is minified). With noise, a hundred 320×180 boxes
|
|
199
|
+
saturated at **0.786 ms from level 0 and 0.758 ms from level 2**:
|
|
200
|
+
about 0.3 µs a box on an M-series GPU. The texture boxes' gap to
|
|
201
|
+
fragments of the same size (0.46 ms) is mostly not minification. The
|
|
202
|
+
case for this ADR is what is drawn, and the page's memory, not the
|
|
203
|
+
GPU's time.
|
|
204
|
+
- **What is drawn.** The `image` example gained a 768-px zone plate
|
|
205
|
+
drawn at 128 px, from a level beside `nearest`. Drawn whole and
|
|
206
|
+
bilinear (levels switched off for the comparison), its outer rings
|
|
207
|
+
alias into false rings as strong as `nearest`'s; from the level they
|
|
208
|
+
fade to the grey they average to, with faint ghost rings left at the
|
|
209
|
+
corners. Those are decision 2's choice showing: at 2× the plate is
|
|
210
|
+
three texels a pixel, so it is drawn from level 1 at 1.5 texels a
|
|
211
|
+
pixel, and bilinear sampling at 1.5 still folds the top half-octave
|
|
212
|
+
of a pattern made of nothing else. Rounding the level instead, as a
|
|
213
|
+
GPU's nearest-mip filter does, would draw level 2 magnified by 1.33
|
|
214
|
+
— no ghosts, and every icon drawn a little smaller than its file
|
|
215
|
+
blurred by the same factor. A UI has more icons than zone plates;
|
|
216
|
+
the rule stays "never magnify", and trilinear is what removes the
|
|
217
|
+
ghosts without the blur.
|
|
218
|
+
- **The guard.** `nu scripts/bench-check.nu` against alpha.47: the eight
|
|
219
|
+
guarded rows within −0.6% to +0.8% (run-to-run ±2.8%). The two 1080p
|
|
220
|
+
`update_image` rows read +4.4% and +6.9% in that run and −0.8% and
|
|
221
|
+
−1.6% run alone; memcpy-bound rows, noise.
|
|
222
|
+
- **Tests.** `mip.rs`'s six (a flat image halves to itself, the linear
|
|
223
|
+
light mean of black and white is sRGB 188, red beside clear stays red
|
|
224
|
+
at half alpha, an odd side repeats its edge, the chain's depth, the
|
|
225
|
+
level never magnifies) and `tests/image_levels.rs`'s six (the deepest
|
|
226
|
+
level that covers, the display's scale and a node's `scale`, `cover`
|
|
227
|
+
on the level, `nearest` and a stream at level 0, an image past a page
|
|
228
|
+
drawn from the page, the page holding the halved texel). The `image`
|
|
229
|
+
example's headless drive checks the zone plate's two quads.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-10-09
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A slot replayed by its host: the extension is spared when the host says nothing it feeds it changed, and the core checks everything else
|
|
7
|
+
|
|
8
|
+
> **Accepted and built (2026-10-09), for alpha.48; backlog F142.** This
|
|
9
|
+
> amends [ADR 0016](0016-caching-against-the-last-frame.md) — decision 2
|
|
10
|
+
> there refuses a `memo` element "in the form that asks the view to
|
|
11
|
+
> declare its own dependencies", and this is not that form, but it is the
|
|
12
|
+
> same move one level up and the refusal's reasons are answered here one
|
|
13
|
+
> by one rather than waved at. What changed between the two documents is
|
|
14
|
+
> a measurement: ADR 0016 priced the app's own building at 40% of a
|
|
15
|
+
> 717 µs frame of Rust pushes, about 0.03 µs a node; a Lua extension's
|
|
16
|
+
> fill costs about 2 µs a node *before* it reaches `Tree::push`, in the
|
|
17
|
+
> binding's walk from its tables to the tree, and that is the half of the
|
|
18
|
+
> bill no primitive of kui's can reduce, because the nodes are not the
|
|
19
|
+
> cost — the conversion is.
|
|
20
|
+
|
|
21
|
+
A host that puts a plugin's pane beside its own content runs the plugin's
|
|
22
|
+
`view` every frame the window draws, whatever brought the frame: a key in
|
|
23
|
+
the host's own editor, a caret blinking in a field, a pointer moving over
|
|
24
|
+
a title bar. For a Rust extension that is the same tenth of a microsecond
|
|
25
|
+
a node the host pays for its own tree. For a Lua one it is not: kawoosh's
|
|
26
|
+
settings pane — 1,561 tables for a dozen settings on show — spends
|
|
27
|
+
1.25 ms running the view and **3.0 ms** in kui-lua turning the tables it
|
|
28
|
+
returned into nodes, on frames where the pane did nothing, against 0.3 ms
|
|
29
|
+
for kui's whole layout and 0.25 ms for the editor beside it; a frame after
|
|
30
|
+
a pause runs cold and the same pane costs 14–22 ms at the caret's 2 Hz.
|
|
31
|
+
(The kawoosh perf log of 2026-10-09, release build, median over forty
|
|
32
|
+
frames each: settings 5.9 ms a frame against 1.1 ms with no pane; themes
|
|
33
|
+
6.2; theme lab 4.6; a native pane would pay the C ABI's per-node calls the
|
|
34
|
+
same way, smaller.) We decided that **the host may declare a slot as
|
|
35
|
+
unchanged** (`Ui::slot_replay`), that **the core then checks everything
|
|
36
|
+
it can see itself** — the slot's params, every fact of the frame the kept
|
|
37
|
+
fill read, where the slot sits — and **pushes the kept fill's nodes again
|
|
38
|
+
through the doors they came in by** without asking the extension, or
|
|
39
|
+
runs the extension and says why; that **a fill is kept only when the host
|
|
40
|
+
asked** (`Ui::slot_kept`) and forgotten the frame it is not declared;
|
|
41
|
+
that **a nested slot is declared again and filled fresh on a replay**;
|
|
42
|
+
and that **a fill the journal cannot vouch for is refused whole**, by a
|
|
43
|
+
count of the nodes it pushed against the nodes it journaled.
|
|
44
|
+
|
|
45
|
+
## Context
|
|
46
|
+
|
|
47
|
+
- **ADR 0016's decision 2 and its reasons.** "An element that means
|
|
48
|
+
'trust me, nothing under here changed' moves the correctness obligation
|
|
49
|
+
from the core to every app, and the class of bug it admits — a view that
|
|
50
|
+
forgot a dependency, so the screen shows something that stopped being
|
|
51
|
+
true — is exactly the class immediate mode exists to make impossible."
|
|
52
|
+
And: "kui's answer to 'my view is too expensive to run every frame'
|
|
53
|
+
stays *build less*." Both reasons stand and both are addressed below;
|
|
54
|
+
neither is overruled.
|
|
55
|
+
- **What a slot already is.** A position the host declares with params
|
|
56
|
+
"declared every frame and never retained" (ADR 0014), filled then and
|
|
57
|
+
there by an extension the host loaded, its nodes keyed under the slot's
|
|
58
|
+
own key and tagged with the extension's origin, its events routed back
|
|
59
|
+
by that origin. The host is already the one who says what the extension
|
|
60
|
+
reads from it; the core already trusts the host for every other
|
|
61
|
+
per-frame fact (the title, the focus, the declared windows).
|
|
62
|
+
- **What the core can see of a fill's inputs, and what it cannot.** A
|
|
63
|
+
fill reads two kinds of things: facts of the frame, through the core's
|
|
64
|
+
own doors — `is_hovered`, `focus`, `scroll_offset`, `layout_of`,
|
|
65
|
+
`caret_visible`, `now`, `measure_text`, the env reading — and facts of
|
|
66
|
+
the host, through the host's own API (an editor's buffers, a settings
|
|
67
|
+
table), which the core never sees. The first kind can be *recorded with
|
|
68
|
+
the value read* and compared next frame, exactly; the second kind is
|
|
69
|
+
the host's, and only the host can say whether it moved.
|
|
70
|
+
- **What a replay can and cannot re-issue.** A fill pushes nodes, and may
|
|
71
|
+
also declare things of the frame — a window title, a frame it wants
|
|
72
|
+
next, a devtools tab, a loaded extension — or draw through a door that
|
|
73
|
+
carries a picture rather than a spec (`cells`). The nodes come back from
|
|
74
|
+
a journal; the declarations do not, and a fill that makes them is one
|
|
75
|
+
whose next frame is its own business.
|
|
76
|
+
- **Keys are the slot's, not the frame's.** Every node under a fill is
|
|
77
|
+
keyed from the slot's key (`fill_within` sets the namespace), so a
|
|
78
|
+
replay of the same ops under the same slot key yields the same keys —
|
|
79
|
+
which is what keeps hover, focus, scroll offsets, transitions, edit
|
|
80
|
+
buffers and exit diffs where they were, since all of those are keyed
|
|
81
|
+
state the core already retains outside the spec.
|
|
82
|
+
- **Hover, accent and easing are resolved at push, not in the view.**
|
|
83
|
+
`prepare_spec` folds the declared `hover_bg`, the OS accent and a
|
|
84
|
+
transition's eased values into the spec on the way into the tree. A
|
|
85
|
+
journal that records the spec *before* that step and replays it through
|
|
86
|
+
the same door resolves all three for the frame being built, so a
|
|
87
|
+
replayed button lights under the pointer and a replayed card keeps
|
|
88
|
+
easing, with no refill and nothing stale.
|
|
89
|
+
|
|
90
|
+
## Decision
|
|
91
|
+
|
|
92
|
+
1. **Two doors on `Ui`, the second a claim.** `slot_kept(name, params)`
|
|
93
|
+
is `slot_with` and keeps what the fill built. `slot_replay(name,
|
|
94
|
+
params)` is the host's claim that nothing *it* feeds the extension has
|
|
95
|
+
changed since; it answers [`SlotFill`]: `Replayed`, or why it ran the
|
|
96
|
+
extension instead — `NotKept`, `Params`, `Reads`, `NotReplayable`,
|
|
97
|
+
`Moved` — having filled and kept it either way, so the next frame may
|
|
98
|
+
replay. `None` when the slot was not declared. The C side is
|
|
99
|
+
`kui_slot_kept` / `kui_slot_replay` / `kui_slot_fill` with
|
|
100
|
+
`KUI_SLOT_*` codes; Lua's `fill { keep = true }` / `fill { replay =
|
|
101
|
+
true }` and `env.slot_fill(name)`; Node's `<slot keep>` / `<slot
|
|
102
|
+
replay>` and `ctx.slotFill(name)`. New symbols only: the C ABI version
|
|
103
|
+
stays, the Node stream version moves (v23) because `slot` gained a
|
|
104
|
+
word.
|
|
105
|
+
|
|
106
|
+
2. **The host vouches for its side only; the core checks the rest.**
|
|
107
|
+
Before a replay the core compares the params (`Value` equality), every
|
|
108
|
+
fact of the frame the kept fill read against its value now — a hover,
|
|
109
|
+
a press, a focus, the caret phase, a scroll offset or geometry, a
|
|
110
|
+
layout rect, a text hit, an editor's text, the modifiers, the pointer,
|
|
111
|
+
the fonts' revision behind a measurement, the env reading less its
|
|
112
|
+
clock and caret phase — and that the slot's key is the kept one. The
|
|
113
|
+
clock is never the same twice, and the selection's text is not
|
|
114
|
+
compared: a fill that read either runs every frame. The read is noted
|
|
115
|
+
where the door is, in `Core`, so a stock widget's `is_hovered` inside
|
|
116
|
+
the fill is caught as surely as the script's `env.is_hovered`.
|
|
117
|
+
|
|
118
|
+
3. **What is kept is the fill's ops, as the doors saw them.** Every node
|
|
119
|
+
push records the key, the spec *as declared* (before `prepare_spec`),
|
|
120
|
+
and what the door needs to push it again — a text's string and style,
|
|
121
|
+
a stroke's points, a path's ops, an editor's label and initial, a
|
|
122
|
+
fragment's handle and params, an image's handle and fit — beside the
|
|
123
|
+
labels, data indices, row counts, hints and the `key_focus` asks the
|
|
124
|
+
fill made, and each nested slot it declared with its params. A replay
|
|
125
|
+
pushes these through the same `*_with_key` doors under the kept
|
|
126
|
+
origin, inside `fill_within` as a fresh fill would be, so the tree's
|
|
127
|
+
fill ranges (`ev.slot`), the key labels, the hints and the event
|
|
128
|
+
routing come out as they would have.
|
|
129
|
+
|
|
130
|
+
4. **A nested slot is declared again, and filled fresh.** `Op::Slot`
|
|
131
|
+
replays as `slot_with`, so whoever fills it runs. This is what makes
|
|
132
|
+
the engine's field inside a script's pane blink its caret at the
|
|
133
|
+
field's cost while the pane around it is not rebuilt. A `slot_kept` or
|
|
134
|
+
`slot_replay` *inside* a fill being kept is a plain slot: the outer
|
|
135
|
+
journal holds the slot, not the inner's nodes.
|
|
136
|
+
|
|
137
|
+
5. **A fill the journal cannot vouch for is refused whole, by count.**
|
|
138
|
+
Every node a fill pushes is journaled by its door, pushed by a nested
|
|
139
|
+
fill, or pushed by the core on its own behalf (a hover hint) while the
|
|
140
|
+
journal is paused; at the end the nodes the tree gained less the
|
|
141
|
+
nested and the core's own must equal the nodes journaled, or the kept
|
|
142
|
+
fill is `NotReplayable`. So is one that declared something of the frame
|
|
143
|
+
— a title, always-on-top, secure input, option-as-alt, the input
|
|
144
|
+
method, a window, a devtools tab, a frame now or at a time, a widget's
|
|
145
|
+
owed frame, audio, a loaded extension — or drew a `cells` grid, or
|
|
146
|
+
whose view failed. The valve is what lets the journal start narrow:
|
|
147
|
+
a door it does not know is a fresh fill, never a wrong one.
|
|
148
|
+
|
|
149
|
+
6. **Nothing of the frame is cached.** The tree is built from scratch,
|
|
150
|
+
laid out and emitted as it always was; a replayed node is a node, hit,
|
|
151
|
+
read and departed like any other. What is skipped is the extension's
|
|
152
|
+
`view` and the binding's work before the first push. ADR 0016's
|
|
153
|
+
decision 1 — no subtree cache of layout and emission — is untouched,
|
|
154
|
+
and its list of what a digest cannot see does not apply, because
|
|
155
|
+
nothing downstream of the push is reused.
|
|
156
|
+
|
|
157
|
+
## How this answers ADR 0016
|
|
158
|
+
|
|
159
|
+
- **"Moves the correctness obligation to every app."** It moves *half* of
|
|
160
|
+
it, and the half it moves is the half only the app has: what the
|
|
161
|
+
extension reads from the host. The other half — everything read through
|
|
162
|
+
kui — the core carries, with values compared rather than inputs
|
|
163
|
+
enumerated, which is the form ADR 0016's own measurement section
|
|
164
|
+
recommends ("compare, do not hash"). An app that vouches wrongly draws
|
|
165
|
+
last frame's pane until the next frame it vouches rightly; that is a
|
|
166
|
+
real bug and a quiet one, and the answers `slot_replay` gives and
|
|
167
|
+
`slot_fill` reads back are what make it a line in a ledger rather than a
|
|
168
|
+
feeling. A host that cannot track what its extension reads has
|
|
169
|
+
`slot_with`, and nothing changed for it.
|
|
170
|
+
- **"Build less."** The host did: the settings pane is a dozen rows on
|
|
171
|
+
show. The cost is not the rows, it is that a Lua table becomes a node
|
|
172
|
+
at 2 µs each, and `virtual_column` does not make a table cheaper to
|
|
173
|
+
read. The binding's walk can be made cheaper (backlog F143), by a
|
|
174
|
+
factor; a replay removes it, on the frames where the view would have
|
|
175
|
+
built the same thing.
|
|
176
|
+
- **"The frame stays honest."** It does: no part of the core keeps a copy
|
|
177
|
+
of a frame it might serve instead. It keeps what a fill *declared*,
|
|
178
|
+
which is what the host would have declared again, and declares it
|
|
179
|
+
again on the host's word — the same word the core takes for the title
|
|
180
|
+
and the focus.
|
|
181
|
+
|
|
182
|
+
## Consequences
|
|
183
|
+
|
|
184
|
+
- A host with a plugin pane gets the pane's steady frames for the price
|
|
185
|
+
of its own, and a view that reads nothing of the frame is never asked
|
|
186
|
+
twice for the same picture. Measured in the bench
|
|
187
|
+
(`slot_1500_nodes_fresh` against `slot_1500_nodes_replayed`): the
|
|
188
|
+
Rust stand-in's fill of 1,500 nodes is 287 µs a frame fresh and 214 µs
|
|
189
|
+
replayed (medians, M3 Pro), the 73 µs between them the extension's own
|
|
190
|
+
pushes and nothing else — the two are close, which is the point: the
|
|
191
|
+
saving is the extension's own time and the binding's, and a Rust
|
|
192
|
+
extension has little of either. kawoosh's numbers are in its own log.
|
|
193
|
+
- **The hole, written down.** A binding that hands the env reading out
|
|
194
|
+
whole (Lua's `env` table, Node's `ctx.env()`) cannot say which fields
|
|
195
|
+
the script used, so the reading's clock and caret phase are left out of
|
|
196
|
+
the comparison; a script that draws from `env.now` or
|
|
197
|
+
`env.caret_visible` is one its host must not replay. The explicit doors
|
|
198
|
+
(`Ui::now`, `Ui::caret_visible`, Lua's functions, C's) still note the
|
|
199
|
+
read. A host that lends the script a proxy over `env` can close the
|
|
200
|
+
hole on its own side.
|
|
201
|
+
- **What a kept fill costs.** One clone of each declared spec into the
|
|
202
|
+
journal (a `NodeSpec` is 224 bytes plus its boxed parts) and a second
|
|
203
|
+
into the tree on replay; a `Value` clone of the params; and a `Read`
|
|
204
|
+
per fact read. The journal is dropped the frame its slot is not
|
|
205
|
+
declared, so a pane closed costs nothing after.
|
|
206
|
+
- **A door added later must journal or taint.** A new way to push a node
|
|
207
|
+
that does neither is caught by the count — the fill is refused, never
|
|
208
|
+
wrong — but it is caught as a slot that stopped replaying, which a
|
|
209
|
+
host will report as a regression. The doors are listed in
|
|
210
|
+
`runtime/replay.rs`'s `Op`.
|
|
211
|
+
- The devtools do not yet say which slots were replayed; `slot_fill` does,
|
|
212
|
+
`slot_fill_why` names the fact that moved or what the fill declared, and
|
|
213
|
+
a host's own ledger can carry both.
|
|
214
|
+
|
|
215
|
+
## Considered options
|
|
216
|
+
|
|
217
|
+
**A. Make the binding's walk cheaper and leave the contract alone.**
|
|
218
|
+
Honest, bounded, and worth doing (F143); it does not reach the frames
|
|
219
|
+
where the view would have produced the same tree, which are most frames.
|
|
220
|
+
|
|
221
|
+
**B. A `memo` element the view declares with its dependencies.** ADR
|
|
222
|
+
0016's decision 2, refused there and refused here for the same reason:
|
|
223
|
+
the view is the party least able to enumerate what it read, and the core
|
|
224
|
+
can enumerate most of it.
|
|
225
|
+
|
|
226
|
+
**C. A core-side digest of the extension's output.** The extension would
|
|
227
|
+
have run to produce the thing digested; nothing is saved.
|
|
228
|
+
|
|
229
|
+
**D. The host declares the slot unchanged; the core checks what it can.
|
|
230
|
+
(Chosen.)** The claim sits with the party that owns the facts the core
|
|
231
|
+
cannot see, and nowhere else.
|
|
232
|
+
|
|
233
|
+
## Amendment to ADR 0016
|
|
234
|
+
|
|
235
|
+
A note at the head of ADR 0016 points here. Decision 2 there stands as
|
|
236
|
+
written for the view-declared form; the host-declared form, with the
|
|
237
|
+
core's own checks, is this document.
|
package/encoder.js
CHANGED
|
@@ -875,14 +875,22 @@ export function createEncoder(P) {
|
|
|
875
875
|
throw new Error(`bad slot name ${JSON.stringify(p.name)} (a full "namespace/slot")`);
|
|
876
876
|
}
|
|
877
877
|
for (const k of Object.keys(p)) {
|
|
878
|
-
if (k !== 'name' && k !== 'params' && k !== 'children') {
|
|
879
|
-
throw new Error(`<slot> takes name and
|
|
878
|
+
if (k !== 'name' && k !== 'params' && k !== 'keep' && k !== 'replay' && k !== 'children') {
|
|
879
|
+
throw new Error(`<slot> takes name, params, keep and replay, not ${JSON.stringify(k)} — it is a position, not a box`);
|
|
880
880
|
}
|
|
881
881
|
}
|
|
882
|
-
|
|
882
|
+
// A slot replayed by its host (ADR 0045): `keep` keeps what the
|
|
883
|
+
// fill builds, `replay` asks for last frame's back when the view's
|
|
884
|
+
// own side of it is unchanged; `ctx.slotFill(name)` says which it
|
|
885
|
+
// got. One or the other.
|
|
886
|
+
if (p.keep && p.replay) {
|
|
887
|
+
throw new Error('<slot> takes keep or replay, not both');
|
|
888
|
+
}
|
|
889
|
+
reserve(7);
|
|
883
890
|
f[fi++] = OP.slot;
|
|
884
891
|
strRef(p.name);
|
|
885
892
|
strRef(p.params != null ? JSON.stringify(p.params) : null);
|
|
893
|
+
f[fi++] = p.replay ? 2 : p.keep ? 1 : 0;
|
|
886
894
|
return;
|
|
887
895
|
}
|
|
888
896
|
case 'devtoolsTab': {
|
package/howto.md
CHANGED
|
@@ -116,7 +116,11 @@ Decode it with the runner, which links the decoder it draws its
|
|
|
116
116
|
wallpaper with: `kui_native::decode_image(bytes)` turns PNG, JPEG, WebP
|
|
117
117
|
or GIF bytes into straight RGBA and a size, which `add_image` takes as it
|
|
118
118
|
is (`decodeImage` in Node, `kui_decode_image` in C, freed with
|
|
119
|
-
`kui_pixels_free`). No `image` dependency of the app's own
|
|
119
|
+
`kui_pixels_free`). No `image` dependency of the app's own, and no
|
|
120
|
+
resampling to the size it is shown at: register the photo as decoded
|
|
121
|
+
and declare the box, and a photo drawn smaller is drawn from a level the
|
|
122
|
+
core halves it to (ADR 0044), the page holding that level and not the
|
|
123
|
+
whole photo. For an
|
|
120
124
|
animated GIF, APNG or WebP, `decode_animation` keeps every frame — the
|
|
121
125
|
whole canvas each, as a browser composites it — with the seconds each
|
|
122
126
|
shows. Play it on the frame clock: keep when it started, ask
|
|
@@ -696,6 +700,38 @@ backdrop; `KUI_BACKDROP_EMULATE=1` shows the wallpaper path on Windows and Linux
|
|
|
696
700
|
In C, `KuiRunConfig.backdrop` asks (`KUI_BACKDROP_BLUR`) and
|
|
697
701
|
`kui_ctx_backdrop(ctx)` in the view reads what the window got.
|
|
698
702
|
|
|
703
|
+
### How do I move the traffic lights down, like a Mac app with a toolbar?
|
|
704
|
+
|
|
705
|
+
Ask for a taller titlebar: `kui_native::app("Notes").custom_titlebar()
|
|
706
|
+
.titlebar(Titlebar::Tall)`, or `titlebar: 'tall'` beside `chrome:
|
|
707
|
+
'custom'` in Node's window options. `Medium` is a compact toolbar's
|
|
708
|
+
titlebar (40 pt, the lights at 12,13 on macOS 27) and `Tall` a full
|
|
709
|
+
toolbar's (52 pt about the lights, at 19,19), against `Standard`'s 32 pt
|
|
710
|
+
at 9,9. kui does not move the buttons: it gives the window an empty
|
|
711
|
+
`NSToolbar` in the style that makes AppKit's own titlebar that tall, so
|
|
712
|
+
AppKit keeps the lights where they belong through resizing, fullscreen
|
|
713
|
+
and focus. The runner measures where they landed into
|
|
714
|
+
`env.window.native_controls`, and `widgets::titlebar` takes its height
|
|
715
|
+
and inset from it, so a strip of tabs or a search field centres on the
|
|
716
|
+
lights with nothing else to change. The toolbar takes no clicks: a
|
|
717
|
+
button in the strip is pressed as before, and the empty strip still
|
|
718
|
+
drags. The window's `titlebar_h` metric is the measured height, so
|
|
719
|
+
`ui.metrics().titlebar_h` and `$titlebar_h` say how tall the strip is
|
|
720
|
+
(32 for `Standard` on macOS 27, where the stock number is 34). On
|
|
721
|
+
Windows and Linux the same ask draws the same strip: there it
|
|
722
|
+
is the app's, as tall as the `titlebar_h` metric, and `Medium` and
|
|
723
|
+
`Tall` make that window's metric 40 and 52 in place of the caption's 32
|
|
724
|
+
or 34 — `ui.metrics().titlebar_h` and `$titlebar_h` read it, and the
|
|
725
|
+
drawn buttons grow to it. An app that sets its own metrics keeps it as
|
|
726
|
+
long as their `titlebar_h` is the stock number (`Metrics::compact()`,
|
|
727
|
+
Node's `base`); a number of its own wins.
|
|
728
|
+
|
|
729
|
+
[`titlebar.rs`](../examples/rust/widgets/titlebar.rs) (`-- --titlebar tall`) ·
|
|
730
|
+
[`window.native_controls` row](props.md#env)
|
|
731
|
+
|
|
732
|
+
In C, `KuiRunConfig.titlebar` asks (`KUI_TITLEBAR_TALL`); in Odin,
|
|
733
|
+
`Run_Config.titlebar`.
|
|
734
|
+
|
|
699
735
|
### How do I frost a toolbar over content that scrolls under it?
|
|
700
736
|
|
|
701
737
|
Give the toolbar a `backdropBlur` and a translucent `bg`:
|
|
@@ -1736,6 +1772,37 @@ extension stamping its payloads.
|
|
|
1736
1772
|
[alpha.9](CHANGELOG.md#010-alpha9-2026-09-08) ·
|
|
1737
1773
|
[alpha.13](CHANGELOG.md#010-alpha13-2026-09-15)
|
|
1738
1774
|
|
|
1775
|
+
### My plugin's pane costs every frame, even when nothing in it moved — how do I stop asking it?
|
|
1776
|
+
|
|
1777
|
+
Declare the slot with `ui.slot_kept("fs/panel", ¶ms)` the frame you
|
|
1778
|
+
first show it, and `ui.slot_replay("fs/panel", ¶ms)` after, for as
|
|
1779
|
+
long as nothing *you* feed the plugin has changed — the buffers, the
|
|
1780
|
+
settings, whatever it reads from you outside kui. That is all you vouch
|
|
1781
|
+
for. The core checks the rest itself before it replays: the params are
|
|
1782
|
+
the same, every fact of the frame the kept fill read is the same (which
|
|
1783
|
+
node was hovered, where its scroller stood, the theme, the focus), and
|
|
1784
|
+
the slot is where it was. When all of that holds it pushes last frame's
|
|
1785
|
+
nodes again through the doors they came in by, resolving hover and a
|
|
1786
|
+
transition's easing for this frame, and never calls the plugin's `view`;
|
|
1787
|
+
when any of it does not, it runs the plugin as `slot_kept` would and tells
|
|
1788
|
+
you why in the answer (`SlotFill::Params`, `Reads`, `Moved`, `NotKept`,
|
|
1789
|
+
`NotReplayable`) and in `core.slot_fill(name)` after. A slot inside the
|
|
1790
|
+
plugin's tree — an editor's field, a legend — is declared again and
|
|
1791
|
+
filled fresh on a replay, so a caret still blinks inside a pane that is
|
|
1792
|
+
not rebuilt. A fill that declares something of the frame beyond its
|
|
1793
|
+
nodes (a title, a window, a frame it wants next, a devtools tab), draws
|
|
1794
|
+
a `cells` grid, or fails, is kept as not replayable and runs every frame
|
|
1795
|
+
as before. In Lua, `fill { name =, params =, keep = true }` then
|
|
1796
|
+
`replay = true` and `env.slot_fill(name)`; in C, `kui_slot_kept`,
|
|
1797
|
+
`kui_slot_replay` and `kui_slot_fill`; in Node, `<slot keep>` then
|
|
1798
|
+
`<slot replay>` and `ctx.slotFill(name)`. A view that reads the clock or
|
|
1799
|
+
the caret phase off `env` as a plain field is one you must not replay —
|
|
1800
|
+
a reading handed out whole cannot say which fields were used — and the
|
|
1801
|
+
explicit doors (`ui.now()`, `env.caret_visible()` as a call) still count.
|
|
1802
|
+
|
|
1803
|
+
[ADR 0045](docs/adr/0045-a-slot-replayed-by-its-host.md) ·
|
|
1804
|
+
`crates/kui-core/tests/slot_replay.rs`
|
|
1805
|
+
|
|
1739
1806
|
### How do I redraw when a thread has new data?
|
|
1740
1807
|
|
|
1741
1808
|
Take the `Waker` the loop hands `App::setup` and clone it into the thread —
|
|
@@ -1834,9 +1901,16 @@ that a screen reader can. `key_of` asks a different question: the *key
|
|
|
1834
1901
|
label*, the name the view opened the node under (`with_keyed("like",
|
|
1835
1902
|
..)`, a `key` prop), which a reader never hears and `texts_under` reads
|
|
1836
1903
|
too. Two nodes with one name resolve to the first in tree order and raise
|
|
1837
|
-
`ambiguous-name` — the same two a reader cannot tell apart.
|
|
1904
|
+
`ambiguous-name` — the same two a reader cannot tell apart. When one of
|
|
1905
|
+
them is a caption under the control it repeats (a round "like" button
|
|
1906
|
+
over the word "like"), either the caption is decorative — `role="none"`
|
|
1907
|
+
on the box around it, `NodeSpec::role(Role::None)` in Rust, takes it out
|
|
1908
|
+
of the tree — or the control's `label` says what it does ("like this
|
|
1909
|
+
bear"), which a reader is better off with when the caption is all the
|
|
1910
|
+
button says.
|
|
1838
1911
|
|
|
1839
1912
|
[`label` row](props.md#container-props) ·
|
|
1913
|
+
[`role` row](props.md#container-props) ·
|
|
1840
1914
|
[`ambiguous-name`](props.md#warnings)
|
|
1841
1915
|
|
|
1842
1916
|
### How do I test the real window, not a headless core?
|
package/index.d.ts
CHANGED
|
@@ -810,7 +810,10 @@ export type WarningCode =
|
|
|
810
810
|
* `kui_key_named`) found more than one node with that name in the last frame
|
|
811
811
|
* — two buttons both read as "Delete" — and used the first in tree order. A
|
|
812
812
|
* reader hears the same name twice too: give each a `label` that says which,
|
|
813
|
-
* or look the one meant up by its key label.
|
|
813
|
+
* or look the one meant up by its key label. When one of them is static text
|
|
814
|
+
* and another is not — a caption under the button it repeats — the message
|
|
815
|
+
* says so: a caption a reader need not hear is decorative, and `role="none"`
|
|
816
|
+
* on the box around it takes it out of the tree (backlog F140). */
|
|
814
817
|
| 'ambiguous-name'
|
|
815
818
|
/** A `focusRegion(name)` (`Core::focus_region`, `env.focus_region`,
|
|
816
819
|
* `kui_focus_region`) named a node the frame after it did not declare as a
|
|
@@ -1589,11 +1592,15 @@ export interface Metrics {
|
|
|
1589
1592
|
menuWidth: number;
|
|
1590
1593
|
/** The drawn menu bar's height. */
|
|
1591
1594
|
menuBarH: number;
|
|
1592
|
-
/** The titlebar's height
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1595
|
+
/** The titlebar strip's height: the platform's caption height, 32 on
|
|
1596
|
+
* Windows and 34 elsewhere, or the window's own where the runner knows it
|
|
1597
|
+
* (backlog W22) — 40 and 52 off macOS for a launcher's `medium` or `tall`
|
|
1598
|
+
* titlebar under custom chrome, and under macOS custom chrome the OS's own
|
|
1599
|
+
* titlebar as `window.native_controls` measures it (32, 40 or 52 on macOS
|
|
1600
|
+
* 27). The stock number stands for that window's height, so a set of the
|
|
1601
|
+
* app's keeps it unless it names a number of its own. Under macOS custom
|
|
1602
|
+
* chrome the strip is drawn at the measured height whatever this row says
|
|
1603
|
+
* (`widgets::titlebar_height`). */
|
|
1597
1604
|
titlebarH: number;
|
|
1598
1605
|
// -- end generated --
|
|
1599
1606
|
}
|
|
@@ -1864,6 +1871,18 @@ export interface WindowOptions {
|
|
|
1864
1871
|
maxWidth?: number;
|
|
1865
1872
|
maxHeight?: number;
|
|
1866
1873
|
chrome?: 'native' | 'custom' | 'borderless';
|
|
1874
|
+
/** How tall the titlebar strip is under `chrome: 'custom'` (the
|
|
1875
|
+
* launcher's `titlebar`, backlog W22): `'standard'` (the default),
|
|
1876
|
+
* `'medium'` or `'tall'`. On macOS it is AppKit's own titlebar, made
|
|
1877
|
+
* with an empty toolbar, and so where the traffic lights sit: 32 px
|
|
1878
|
+
* with them at 9,9 on macOS 27, 40 at 12,13, or 52 about them at
|
|
1879
|
+
* 19,19; what the window got is `env().window.nativeControls`, and
|
|
1880
|
+
* the window's `titlebarH` metric is that height. On Windows and Linux
|
|
1881
|
+
* the strip is the app's: `'medium'` and `'tall'` make the window's
|
|
1882
|
+
* `titlebarH` metric 40 and 52 in place of the caption's 32 or 34.
|
|
1883
|
+
* Either way a `setMetrics` keeps it unless it names a `titlebarH` of
|
|
1884
|
+
* its own, and `titlebar` lays out against it. */
|
|
1885
|
+
titlebar?: 'standard' | 'medium' | 'tall';
|
|
1867
1886
|
/** What shows through the window where a frame paints nothing or paints
|
|
1868
1887
|
* with alpha (the launcher's `backdrop`), by effect: `'opaque'` (the
|
|
1869
1888
|
* default), `'transparent'` (the desktop as it is), `'blur'` (macOS
|
|
@@ -3133,6 +3152,14 @@ export declare class Ctx {
|
|
|
3133
3152
|
* plugin filling a slot is answered from its own nodes only.
|
|
3134
3153
|
*/
|
|
3135
3154
|
keyOf(label: string): string | null
|
|
3155
|
+
/**
|
|
3156
|
+
* What a `<slot replay>` of `name` got this frame, or the
|
|
3157
|
+
* frame before while this one is being built (ADR 0045):
|
|
3158
|
+
* `'replayed'`, or why it was filled fresh — `'not-kept'`,
|
|
3159
|
+
* `'params'`, `'reads'`, `'not-replayable'`, `'moved'` — or
|
|
3160
|
+
* null when nothing asked.
|
|
3161
|
+
*/
|
|
3162
|
+
slotFill(name: string): string | null
|
|
3136
3163
|
/**
|
|
3137
3164
|
* The hex key of the first node in the last finished frame
|
|
3138
3165
|
* whose accessible name is `name` (backlog F137) — its
|
|
@@ -3641,7 +3668,8 @@ export declare class KuiWindow {
|
|
|
3641
3668
|
/**
|
|
3642
3669
|
* Options: `{width, height, minWidth, minHeight, maxWidth, maxHeight,
|
|
3643
3670
|
* chrome: "native" | "custom" | "borderless", textAa: "auto" | "gray"
|
|
3644
|
-
* | "subpixel", frameLatency, system, icon, backdrop
|
|
3671
|
+
* | "subpixel", frameLatency, system, icon, backdrop, titlebar:
|
|
3672
|
+
* "standard" | "medium" | "tall"}`. The min/max pairs bound what the user can
|
|
3645
3673
|
* resize the window to; either half may stand alone. `system` pins part of `env.system` over what the OS
|
|
3646
3674
|
* says, for the life of the window — `{motion: 'reduced'}` is what a
|
|
3647
3675
|
* user who asked for less motion would get, on a machine whose owner
|
|
@@ -4432,6 +4460,14 @@ export declare class KuiWindow {
|
|
|
4432
4460
|
* plugin filling a slot is answered from its own nodes only.
|
|
4433
4461
|
*/
|
|
4434
4462
|
keyOf(label: string): string | null
|
|
4463
|
+
/**
|
|
4464
|
+
* What a `<slot replay>` of `name` got this frame, or the
|
|
4465
|
+
* frame before while this one is being built (ADR 0045):
|
|
4466
|
+
* `'replayed'`, or why it was filled fresh — `'not-kept'`,
|
|
4467
|
+
* `'params'`, `'reads'`, `'not-replayable'`, `'moved'` — or
|
|
4468
|
+
* null when nothing asked.
|
|
4469
|
+
*/
|
|
4470
|
+
slotFill(name: string): string | null
|
|
4435
4471
|
/**
|
|
4436
4472
|
* The hex key of the first node in the last finished frame
|
|
4437
4473
|
* whose accessible name is `name` (backlog F137) — its
|
package/index.js
CHANGED
|
@@ -1047,8 +1047,8 @@ runWindowed[PACE] = pacer;
|
|
|
1047
1047
|
* is opened as a user who asked for less motion would see it.
|
|
1048
1048
|
*/
|
|
1049
1049
|
export function windowOptions(opts = {}) {
|
|
1050
|
-
const { width, height, minWidth, minHeight, maxWidth, maxHeight, chrome, textAa, frameLatency, system, icon, backdrop } = opts;
|
|
1051
|
-
return { width, height, minWidth, minHeight, maxWidth, maxHeight, chrome, textAa, frameLatency, system, icon, backdrop };
|
|
1050
|
+
const { width, height, minWidth, minHeight, maxWidth, maxHeight, chrome, textAa, frameLatency, system, icon, backdrop, titlebar } = opts;
|
|
1051
|
+
return { width, height, minWidth, minHeight, maxWidth, maxHeight, chrome, textAa, frameLatency, system, icon, backdrop, titlebar };
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
1054
|
/**
|
package/jsx-runtime.d.ts
CHANGED
|
@@ -278,7 +278,7 @@ export interface GeneratedSpecProps {
|
|
|
278
278
|
backdropBlur?: LengthProp;
|
|
279
279
|
/** Background fill. */
|
|
280
280
|
bg?: ColorProp;
|
|
281
|
-
/** How far a spring overshoots its target: 0 glides in with none, 0.5 bounces visibly, and values past 0.9 are held there (a spring at 1 would never settle). It replaces a spring `easing`'s own bounce (`smooth` 0, `snappy` 0.15, `spring` 0.25, `bouncy` 0.5), and on a timed easing makes the transition a spring — so `transition` plus `bounce` is a spring of that length and bounce. */
|
|
281
|
+
/** How far a spring overshoots its target: 0 glides in with none, 0.5 bounces visibly, and values past 0.9 are held there (a spring at 1 would never settle). It replaces a spring `easing`'s own bounce (`smooth` 0, `snappy` 0.15, `spring` 0.25, `bouncy` 0.5), and on a timed easing makes the transition a spring — so `transition` plus `bounce` is a spring of that length and bounce. It does nothing between `keyframes` stops, which are sampled off the clock (see that row). */
|
|
282
282
|
bounce?: LengthProp;
|
|
283
283
|
/** Which non-primary buttons `onButton` claims (backlog F105): `"secondary"`, `"middle"` and `"other"` (every button past those), separated by spaces or commas — `"middle"`, `"secondary middle"`. Unset, all three: a node that wants the middle button and leaves the secondary one to its context menu says `"middle"`. A word that is none of the three is skipped, so a string of none of them claims nothing, and a typo never takes the secondary button from a context menu. Meaningless without `onButton`. */
|
|
284
284
|
buttons?: string;
|
|
@@ -306,7 +306,7 @@ export interface GeneratedSpecProps {
|
|
|
306
306
|
disabled?: boolean;
|
|
307
307
|
/** Background while files dragged in from the OS are over this node (ADR 0031); wins over pressedBg, focusBg and hoverBg, clears when they leave, land or the drag is cancelled. Implies hover tracking, eases with `transition`. */
|
|
308
308
|
dropBg?: ColorProp;
|
|
309
|
-
/** Easing for `transition` (default easeOut). The springs — `smooth` (no overshoot), `snappy`, `spring` and `bouncy` (the most), each a `bounce` of its own — integrate with momentum, so a value retargeted mid-flight keeps moving the way it was; `transition` is then about how long one takes to get there. */
|
|
309
|
+
/** Easing for `transition` (default easeOut). The springs — `smooth` (no overshoot), `snappy`, `spring` and `bouncy` (the most), each a `bounce` of its own — integrate with momentum, so a value retargeted mid-flight keeps moving the way it was; `transition` is then about how long one takes to get there. Between `keyframes` stops a spring is drawn as `easeOut` (see that row). */
|
|
310
310
|
easing?: 'easeOut' | 'linear' | 'easeIn' | 'easeInOut' | 'spring' | 'bouncy' | 'smooth' | 'snappy';
|
|
311
311
|
/** Where the node starts the first frame it is seen `{ dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }`: those slots ease in from there over `transition` ms instead of snapping (`dx`/`dy` slide it in from that far away, `opacity: 0` fades the whole subtree in, `scale: 0.8` settles it in). */
|
|
312
312
|
enter?: EnterProp;
|
|
@@ -342,7 +342,7 @@ export interface GeneratedSpecProps {
|
|
|
342
342
|
keepFocus?: boolean;
|
|
343
343
|
/** With `onKey`: releases arrive too, as the same payload with phase:"up" (`text` null, `repeat` false) — for a held-key interaction (WASD, press-and-hold, a key that arms a mode while it is down). A key only comes up where it went down: a release whose press the sink never got is dropped, and focus leaving while a key is held delivers the `up` first, so nothing is left stuck down. Without it a sink hears presses only, which is what a keymap wants — one that heard both halves would run every binding twice. */
|
|
344
344
|
keyUp?: boolean;
|
|
345
|
-
/** CSS-style stops `[{ at?, dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }, …]`: the slots they name cycle through them over `transition` ms, for ever unless `iterations` says how many times, without the view redrawing; `at` is 0..1 and spreads evenly when omitted. `dx` / `dy` are logical px from where layout put the node (backlog F132), as an entrance's are: the node and its subtree are drawn and hit that far away at the stop, a lane a stop leaves out is 0, and the offset adds to a `slide`'s, so `[{ dy: 0 }, { dy: -6 }]` with `repeat: 'alternate'` bobs a box and a sparkle drifts up its stops. Paint, hit and access only: layout and the room the node takes are its own place's, and an `onLayout` node reports its layout rect, not the cycle, which would post an event every frame it runs. */
|
|
345
|
+
/** CSS-style stops `[{ at?, dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }, …]`: the slots they name cycle through them over `transition` ms, for ever unless `iterations` says how many times, without the view redrawing; `at` is 0..1 and spreads evenly when omitted. `dx` / `dy` are logical px from where layout put the node (backlog F132), as an entrance's are: the node and its subtree are drawn and hit that far away at the stop, a lane a stop leaves out is 0, and the offset adds to a `slide`'s, so `[{ dy: 0 }, { dy: -6 }]` with `repeat: 'alternate'` bobs a box and a sparkle drifts up its stops. Paint, hit and access only: layout and the room the node takes are its own place's, and an `onLayout` node reports its layout rect, not the cycle, which would post an event every frame it runs. The `easing` applies to each step between two stops, as CSS applies its timing function per keyframe; a spring easing there is drawn as `easeOut` and `bounce` does nothing, since a cycle is sampled off the clock and a spring has to be integrated (backlog F141) — an overshoot in a cycle is a stop past the target, `[{ scale: 1 }, { scale: 1.15, at: 0.6 }, { scale: 1 }]`. */
|
|
346
346
|
keyframes?: KeyframeProp[];
|
|
347
347
|
/** The accessible name. Without one a button, link, tab or heading is named by the text inside it; an image, an icon-only button and a `modal` dialog have none, and the core warns (`image-without-label`, `control-without-name`, `modal-without-name`). Not the key label a `key` prop or `with_keyed` declares, which `key_of` looks up and a reader never hears; `key_named` looks a node up by this name. */
|
|
348
348
|
label?: string;
|
|
@@ -412,7 +412,7 @@ export interface GeneratedSpecProps {
|
|
|
412
412
|
radiusTR?: LengthProp;
|
|
413
413
|
/** How `keyframes` cycle (CSS `animation-direction`, default normal). Lua: `direction`, since `repeat` is a keyword. */
|
|
414
414
|
repeat?: 'normal' | 'reverse' | 'alternate' | 'alternateReverse';
|
|
415
|
-
/** What the node is to assistive technology. Unset, the core derives one (an `onClick` node is a button, an editor a text input, a scrolling box a scroll view, a plain box nothing); `none` hides the node and its subtree from the access tree. A `radio` belongs inside a `radioGroup` and a `tab` inside a `tabList`, labelled with what the choice is: the pair is a composite (`docs/adr/0007-composite-keyboard-patterns.md`) — one Tab stop for the set, the arrows, Home and End moving the choice inside it (each step is the item's click, so the choice follows focus), and a screen reader reading "2 of 3". A `radio` or `tab` with no container above it is a Tab stop of its own that no arrow moves, and the core warns (`item-outside-container`). `menu` holds `menuItem`s and `list` holds `listItem`s the same way. */
|
|
415
|
+
/** What the node is to assistive technology. Unset, the core derives one (an `onClick` node is a button, an editor a text input, a scrolling box a scroll view, a plain box nothing); `none` hides the node and its subtree from the access tree — the decorative door, for what a reader need not hear: an icon beside the text that says the same, or a caption under the button it repeats. A text takes no `role` (it has no box), so `none` goes on the box around it; `ambiguous-name` points at it when a caption and its control share a name (backlog F140). Where the caption is all a control says, a `label` on the control that says what it does is the better answer. A `radio` belongs inside a `radioGroup` and a `tab` inside a `tabList`, labelled with what the choice is: the pair is a composite (`docs/adr/0007-composite-keyboard-patterns.md`) — one Tab stop for the set, the arrows, Home and End moving the choice inside it (each step is the item's click, so the choice follows focus), and a screen reader reading "2 of 3". A `radio` or `tab` with no container above it is a Tab stop of its own that no arrow moves, and the core warns (`item-outside-container`). `menu` holds `menuItem`s and `list` holds `listItem`s the same way. */
|
|
416
416
|
role?: 'none' | 'button' | 'checkbox' | 'radio' | 'switch' | 'slider' | 'tab' | 'tabList' | 'link' | 'heading' | 'list' | 'listItem' | 'image' | 'dialog' | 'group' | 'textInput' | 'multilineTextInput' | 'line' | 'radioGroup' | 'menu' | 'menuItem' | 'terminal';
|
|
417
417
|
/** Turns this node and everything under it, in turns clockwise (0.25 is a quarter turn right), about its pivot — the centre unless `pivotX` / `pivotY` say — after layout (`docs/adr/0043-a-node-turns-about-its-pivot.md`). Paint-only: the node takes the room its upright self takes, nothing around it moves, `onLayout` reports the layout rect. Everything the subtree draws turns with it — backgrounds, borders, shadows, text, images, strokes, fragments — and so does what it clips: a child cut by a turned card's rounded corners stays inside them. Hit where drawn: a tilted card is grabbed on its tilted edge and a press in its box past its edge falls through; drag payloads stay in viewport px. The access rect is the bounding box. Nests by composition. Tweens with `transition` as one slot with `scale`, and an entrance, an exit or a keyframe stop may name it (`enter: { rotate: -0.02 }`, `keyframes: [{ rotate: 0 }, { rotate: 1 }]` spins a box). A float anchored to the parent turns with it; a viewport float does not. On a `path` this is the path's own turn (ADR 0041), which does not tween — wrap it in a box for one that does. Text under a turn leaves the pixel grid, as a turned mask does. `backdropBlur` under a turn blurs the upright box. */
|
|
418
418
|
rotate?: LengthProp;
|
|
@@ -838,8 +838,16 @@ export declare namespace JSX {
|
|
|
838
838
|
*
|
|
839
839
|
* A position, not a box: it takes no other props, and with nothing
|
|
840
840
|
* loaded under that namespace it places an empty node so a view can
|
|
841
|
-
* declare its layout before it has a plugin to put in it.
|
|
842
|
-
|
|
841
|
+
* declare its layout before it has a plugin to put in it.
|
|
842
|
+
*
|
|
843
|
+
* `keep` keeps what the fill builds, and `replay` is the view's claim
|
|
844
|
+
* that nothing it feeds the plugin has changed since: the core checks
|
|
845
|
+
* what it can see — the params, every fact of the frame the kept fill
|
|
846
|
+
* read, that the slot is where it was — and pushes last frame's nodes
|
|
847
|
+
* again without asking the plugin, or fills and keeps it and says why
|
|
848
|
+
* in `ctx.slotFill(name)` (docs/adr/0045-a-slot-replayed-by-its-host.md).
|
|
849
|
+
* One or the other. */
|
|
850
|
+
slot: { name: string; params?: unknown; keep?: boolean; replay?: boolean };
|
|
843
851
|
/** A tab in the core's devtools panel, beside facts, events and tree
|
|
844
852
|
* (docs/adr/0032-a-devtools-tab-mounts-a-slot.md). `name` is the tab's
|
|
845
853
|
* identity, `label` what the strip shows (the name when left out).
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/props.md
CHANGED
|
@@ -20,7 +20,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
|
|
|
20
20
|
| `aspectRatio` | `aspect_ratio` | `aspect_ratio` | `Spec.aspect_ratio` | number, or a `"$length"` token | Width over height — `16/9`, `1` for a square — CSS's `aspect-ratio`. It sizes the axis left `fit`: a fit height is the final width over the ratio (so `width: grow` and a ratio is a box that keeps its shape as the window resizes), and a fit width under a fixed height is that height times it. With both axes declared, or a fit width under a `grow` or percent height, it has nothing it can set and warns. The derived axis is neither shrunk nor fitted to the children, which overflow it; `minHeight: 'fit'` floors it at them. On an image it wins over the pixels' own aspect. |
|
|
21
21
|
| `backdropBlur` | `backdrop_blur` | `backdrop_blur` | `Spec.backdrop_blur` | number, or a `"$length"` token | Blur what was drawn beneath the node, inside its rounded box, by this radius in logical px — CSS's `backdrop-filter: blur()`, the radius its standard deviation (backlog F129). What blurs is everything painted before the node: its ancestors' backgrounds, the siblings under it, content scrolling beneath it, the window's `backdrop` where the window has one. The node's own `bg`, border and children paint over the blur, so a translucent `bg` (`#ffffff40`) makes frosted glass and an opaque one hides it. Clipped as the node is, faded by its `opacity`; 0 is none. The GPU renderer reads back only the box (and a margin of three radii around it) and blurs it at reduced resolution, so it costs a copy and three small passes per blurred node on a frame that has one and nothing on a frame that does not. A renderer that cannot read back what it drew — a host's own, or anything older — leaves the node over an unblurred backdrop; the display list carries it as a `backdrop` quad (`KUI_QUAD_BACKDROP` in C) either way. |
|
|
22
22
|
| `bg` | `bg` | `bg` | `Spec.bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background fill. |
|
|
23
|
-
| `bounce` | `bounce` | `bounce` | `Spec.bounce` | number, or a `"$length"` token | How far a spring overshoots its target: 0 glides in with none, 0.5 bounces visibly, and values past 0.9 are held there (a spring at 1 would never settle). It replaces a spring `easing`'s own bounce (`smooth` 0, `snappy` 0.15, `spring` 0.25, `bouncy` 0.5), and on a timed easing makes the transition a spring — so `transition` plus `bounce` is a spring of that length and bounce. |
|
|
23
|
+
| `bounce` | `bounce` | `bounce` | `Spec.bounce` | number, or a `"$length"` token | How far a spring overshoots its target: 0 glides in with none, 0.5 bounces visibly, and values past 0.9 are held there (a spring at 1 would never settle). It replaces a spring `easing`'s own bounce (`smooth` 0, `snappy` 0.15, `spring` 0.25, `bouncy` 0.5), and on a timed easing makes the transition a spring — so `transition` plus `bounce` is a spring of that length and bounce. It does nothing between `keyframes` stops, which are sampled off the clock (see that row). |
|
|
24
24
|
| `buttons` | `buttons` | `buttons` (`KUI_BUTTONS_*` bits; zeroed, all three) | `Spec.buttons` | string | Which non-primary buttons `onButton` claims (backlog F105): `"secondary"`, `"middle"` and `"other"` (every button past those), separated by spaces or commas — `"middle"`, `"secondary middle"`. Unset, all three: a node that wants the middle button and leaves the secondary one to its context menu says `"middle"`. A word that is none of the three is skipped, so a string of none of them claims nothing, and a typo never takes the secondary button from a context menu. Meaningless without `onButton`. |
|
|
25
25
|
| `caret` | `caret` | `caret` with `KUI_VALUE_CARET` in `value_set` | `Spec.caret` | number, or a `"$length"` token | On a `line` of a custom editor (a `textInput` / `multilineTextInput` role drawn by the app): the caret's byte offset into that line's text. |
|
|
26
26
|
| `caretSolid` | `caret_solid` | `caret_solid` | `Spec.caret_solid` | boolean | On a `line` declaring `caret`: the caret is solid — a block caret in a modal editor's normal mode — so the driver's blink clock is not armed on it and `caretVisible` stays true, while the offset still anchors the IME and reads to assistive technology. Without it a declared `caret` is a caret to blink, and the one thing that asks an idle app for a frame twice a second; an editor whose caret only blinks while typing declares this on every other mode's line. |
|
|
@@ -34,7 +34,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
|
|
|
34
34
|
| `description` | `description` | `description` | `Spec.description` | string | The accessible description: the extra sentence a reader says after the name, for what the name cannot say on its own — what a button will do, why a control is disabled, what format a field wants. `tooltip` is the shorthand that also draws the string and hover-tracks the node; this is the description alone, for a hint that is spoken and never drawn. Both write the one slot, so a node declaring both keeps whichever its binding applied last. It reads only on a node that reaches the access tree — a role, a label, a control — since a plain box is elided and takes its description with it. |
|
|
35
35
|
| `disabled` | `disabled` | `disabled` | `Spec.disabled` | boolean | Inert: no click, drag or key sink, no hover / pressed / focus background, skipped by Tab, reported disabled to assistive technology; hover tracking stays so a `tooltip` can say why. |
|
|
36
36
|
| `dropBg` | `drop_bg` | `drop_bg` | `Spec.drop_bg` | color (`#hex` or `0xRRGGBBAA`), or a `"$color"` token | Background while files dragged in from the OS are over this node (ADR 0031); wins over pressedBg, focusBg and hoverBg, clears when they leave, land or the drag is cancelled. Implies hover tracking, eases with `transition`. |
|
|
37
|
-
| `easing` | `easing` | `easing` (`KUI_EASE_*`) | `Spec.easing` | `easeOut` \\| `linear` \\| `easeIn` \\| `easeInOut` \\| `spring` \\| `bouncy` \\| `smooth` \\| `snappy` | Easing for `transition` (default easeOut). The springs — `smooth` (no overshoot), `snappy`, `spring` and `bouncy` (the most), each a `bounce` of its own — integrate with momentum, so a value retargeted mid-flight keeps moving the way it was; `transition` is then about how long one takes to get there. |
|
|
37
|
+
| `easing` | `easing` | `easing` (`KUI_EASE_*`) | `Spec.easing` | `easeOut` \\| `linear` \\| `easeIn` \\| `easeInOut` \\| `spring` \\| `bouncy` \\| `smooth` \\| `snappy` | Easing for `transition` (default easeOut). The springs — `smooth` (no overshoot), `snappy`, `spring` and `bouncy` (the most), each a `bounce` of its own — integrate with momentum, so a value retargeted mid-flight keeps moving the way it was; `transition` is then about how long one takes to get there. Between `keyframes` stops a spring is drawn as `easeOut` (see that row). |
|
|
38
38
|
| `enter` | `enter` | `enter` (`KuiEnter`, with `set` bits) | `Spec.enter` | entrance (`{ dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }`) | Where the node starts the first frame it is seen `{ dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }`: those slots ease in from there over `transition` ms instead of snapping (`dx`/`dy` slide it in from that far away, `opacity: 0` fades the whole subtree in, `scale: 0.8` settles it in). |
|
|
39
39
|
| `exit` | `exit` | `exit` (`KuiEnter`, with `set` bits) | `Spec.exit` | entrance (`{ dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }`) | Where the node ends the frame after the view stops declaring it `{ dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }` — an `enter` read the other way. It plays when the node itself is removed, its parent still declared; a node that goes because an ancestor went — a tab switched away, a panel closed around it — goes at once with it, unless that ancestor has an `exit` of its own, whose picture carries it (backlog DX19; React's `AnimatePresence` rule). With a `transition`, the departing subtree is copied out of the last frame that had it and replayed frozen, in its place (the pass it painted in, just under the node that painted after it — a panel under a HUD leaves under it) and inert (no clicks, no Tab stop, no access row) while those slots ease from where they were, then dropped; without one it vanishes at once as it always did. `width`/`height` resize the departing node's own box only — the subtree inside it is a picture and is not laid out again. The exit read is the one the last frame that had the node declared, unless `exit_with` named another for the frame it went in: a card a button throws left or right is aimed by the handler that removes it, with no frame drawn first to point it. Needs a stable key across frames. |
|
|
40
40
|
| `expanded` | `expanded` | `expanded` (`KUI_EXPANDED_*`) | `Spec.expanded` | `collapsed` \\| `expanded` | A disclosure's state: what a node that shows and hides something (a twisty, an accordion header, a menu button) reads as. Unset, the node does not expand at all — which is why this names its state instead of being a flag. |
|
|
@@ -52,7 +52,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
|
|
|
52
52
|
| `iterations` | `iterations` | `iterations` (0 is for ever) | `Spec.iterations` | number, or a `"$length"` token | How many times the `keyframes` cycle runs (CSS `animation-iteration-count`, backlog F133); left out, for ever. A finite cycle plays from the first frame the node is declared with it — a node that leaves and comes back plays again — holds its first stop through its `delay`, and rests where its last iteration ended (CSS's fill `both`): `1` plays a burst or a shake once, `2` with `repeat: "alternate"` goes out and back and ends where it began, `0.5` stops halfway. Once it is over the node owes no frame, so `animating()` and `owed()` go quiet as a settled transition's do; `delay` plus `iterations: 1` staggers one-shots. A count that is not a positive number is for ever. C: 0 is for ever. |
|
|
53
53
|
| `keepFocus` | `keep_focus` | `keep_focus` | `Spec.keep_focus` | boolean | A press on this node, or anywhere inside it, leaves keyboard focus where it was: a toolbar button, a tab or a divider that acts without taking the keyboard from the editor or key sink that had it. Without it a press on an `onClick` node focuses the node, and the app's keys stop reaching the sink until it takes focus back. The press also leaves a text or cell selection and the Tab ring where they were, so a Copy button copies what was selected. An `<edit>` inside still takes its caret and focus, as the keyboard's own owner. The click, drag and hover are unchanged, and Tab and assistive technology still reach the node. |
|
|
54
54
|
| `keyUp` | `key_up` | `key_up` | `Spec.key_up` | boolean | With `onKey`: releases arrive too, as the same payload with phase:"up" (`text` null, `repeat` false) — for a held-key interaction (WASD, press-and-hold, a key that arms a mode while it is down). A key only comes up where it went down: a release whose press the sink never got is dropped, and focus leaving while a key is held delivers the `up` first, so nothing is left stuck down. Without it a sink hears presses only, which is what a keymap wants — one that heard both halves would run every binding twice. |
|
|
55
|
-
| `keyframes` | `keyframes` | `keyframes` + `keyframes_len` (`KuiKeyframe[]`) | `Spec.keyframes` | keyframe list (`[{ at?, dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }, …]`) | CSS-style stops `[{ at?, dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }, …]`: the slots they name cycle through them over `transition` ms, for ever unless `iterations` says how many times, without the view redrawing; `at` is 0..1 and spreads evenly when omitted. `dx` / `dy` are logical px from where layout put the node (backlog F132), as an entrance's are: the node and its subtree are drawn and hit that far away at the stop, a lane a stop leaves out is 0, and the offset adds to a `slide`'s, so `[{ dy: 0 }, { dy: -6 }]` with `repeat: 'alternate'` bobs a box and a sparkle drifts up its stops. Paint, hit and access only: layout and the room the node takes are its own place's, and an `onLayout` node reports its layout rect, not the cycle, which would post an event every frame it runs. |
|
|
55
|
+
| `keyframes` | `keyframes` | `keyframes` + `keyframes_len` (`KuiKeyframe[]`) | `Spec.keyframes` | keyframe list (`[{ at?, dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }, …]`) | CSS-style stops `[{ at?, dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }, …]`: the slots they name cycle through them over `transition` ms, for ever unless `iterations` says how many times, without the view redrawing; `at` is 0..1 and spreads evenly when omitted. `dx` / `dy` are logical px from where layout put the node (backlog F132), as an entrance's are: the node and its subtree are drawn and hit that far away at the stop, a lane a stop leaves out is 0, and the offset adds to a `slide`'s, so `[{ dy: 0 }, { dy: -6 }]` with `repeat: 'alternate'` bobs a box and a sparkle drifts up its stops. Paint, hit and access only: layout and the room the node takes are its own place's, and an `onLayout` node reports its layout rect, not the cycle, which would post an event every frame it runs. The `easing` applies to each step between two stops, as CSS applies its timing function per keyframe; a spring easing there is drawn as `easeOut` and `bounce` does nothing, since a cycle is sampled off the clock and a spring has to be integrated (backlog F141) — an overshoot in a cycle is a stop past the target, `[{ scale: 1 }, { scale: 1.15, at: 0.6 }, { scale: 1 }]`. |
|
|
56
56
|
| `label` | `label` | `label` (KuiStr) | `Spec.label` | string | The accessible name. Without one a button, link, tab or heading is named by the text inside it; an image, an icon-only button and a `modal` dialog have none, and the core warns (`image-without-label`, `control-without-name`, `modal-without-name`). Not the key label a `key` prop or `with_keyed` declares, which `key_of` looks up and a reader never hears; `key_named` looks a node up by this name. |
|
|
57
57
|
| `live` | `live` | `live` (`KUI_LIVE_*`) | `Spec.live` | `off` \\| `polite` \\| `assertive` | Marks this node a live region: when the text inside it changes, a screen reader reads the change without being asked — `polite` at the next pause, `assertive` interrupting. Put it on the smallest node that holds the message, since everything inside a live node is live. For a one-off with no node behind it ("Saved") the binding's `announce` verb is the other half. |
|
|
58
58
|
| `mainAlign` | `main_align` | `main_align` | `Spec.main_align` | `start` \\| `center` \\| `end` \\| `spaceBetween` \\| `spaceAround` \\| `spaceEvenly` \\| `baseline` | Child alignment along the main axis. `start`, `center` and `end` put the children together; `spaceBetween` deals the free space out between them (none at the ends), `spaceAround` gives each child an equal share split to its two sides, and `spaceEvenly` makes every gap and both ends equal — CSS's `justify-content`. The spread is added to `gap`, and there is none when nothing is free: a `grow` child takes it all, and an overflowing run keeps its gaps. `baseline` means nothing here and lays out as `start`, with a warning. |
|
|
@@ -87,7 +87,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
|
|
|
87
87
|
| `radiusTL` | `radius_tl` | `radius_tl` with `per_corner` | `Spec.radius_tl` | number, or a `"$length"` token | Top-left corner radius (logical px). |
|
|
88
88
|
| `radiusTR` | `radius_tr` | `radius_tr` with `per_corner` | `Spec.radius_tr` | number, or a `"$length"` token | Top-right corner radius (logical px). |
|
|
89
89
|
| `repeat` | `repeat` | `repeat` (`KUI_REPEAT_*`) | `Spec.repeat` | `normal` \\| `reverse` \\| `alternate` \\| `alternateReverse` | How `keyframes` cycle (CSS `animation-direction`, default normal). Lua: `direction`, since `repeat` is a keyword. |
|
|
90
|
-
| `role` | `role` | `role` (`KUI_ROLE_*`) | `Spec.role` | `none` \\| `button` \\| `checkbox` \\| `radio` \\| `switch` \\| `slider` \\| `tab` \\| `tabList` \\| `link` \\| `heading` \\| `list` \\| `listItem` \\| `image` \\| `dialog` \\| `group` \\| `textInput` \\| `multilineTextInput` \\| `line` \\| `radioGroup` \\| `menu` \\| `menuItem` \\| `terminal` | What the node is to assistive technology. Unset, the core derives one (an `onClick` node is a button, an editor a text input, a scrolling box a scroll view, a plain box nothing); `none` hides the node and its subtree from the access tree. A `radio` belongs inside a `radioGroup` and a `tab` inside a `tabList`, labelled with what the choice is: the pair is a composite (`docs/adr/0007-composite-keyboard-patterns.md`) — one Tab stop for the set, the arrows, Home and End moving the choice inside it (each step is the item's click, so the choice follows focus), and a screen reader reading "2 of 3". A `radio` or `tab` with no container above it is a Tab stop of its own that no arrow moves, and the core warns (`item-outside-container`). `menu` holds `menuItem`s and `list` holds `listItem`s the same way. |
|
|
90
|
+
| `role` | `role` | `role` (`KUI_ROLE_*`) | `Spec.role` | `none` \\| `button` \\| `checkbox` \\| `radio` \\| `switch` \\| `slider` \\| `tab` \\| `tabList` \\| `link` \\| `heading` \\| `list` \\| `listItem` \\| `image` \\| `dialog` \\| `group` \\| `textInput` \\| `multilineTextInput` \\| `line` \\| `radioGroup` \\| `menu` \\| `menuItem` \\| `terminal` | What the node is to assistive technology. Unset, the core derives one (an `onClick` node is a button, an editor a text input, a scrolling box a scroll view, a plain box nothing); `none` hides the node and its subtree from the access tree — the decorative door, for what a reader need not hear: an icon beside the text that says the same, or a caption under the button it repeats. A text takes no `role` (it has no box), so `none` goes on the box around it; `ambiguous-name` points at it when a caption and its control share a name (backlog F140). Where the caption is all a control says, a `label` on the control that says what it does is the better answer. A `radio` belongs inside a `radioGroup` and a `tab` inside a `tabList`, labelled with what the choice is: the pair is a composite (`docs/adr/0007-composite-keyboard-patterns.md`) — one Tab stop for the set, the arrows, Home and End moving the choice inside it (each step is the item's click, so the choice follows focus), and a screen reader reading "2 of 3". A `radio` or `tab` with no container above it is a Tab stop of its own that no arrow moves, and the core warns (`item-outside-container`). `menu` holds `menuItem`s and `list` holds `listItem`s the same way. |
|
|
91
91
|
| `rotate` | `rotate` | `rotate` | `Spec.rotate` | number, or a `"$length"` token | Turns this node and everything under it, in turns clockwise (0.25 is a quarter turn right), about its pivot — the centre unless `pivotX` / `pivotY` say — after layout (`docs/adr/0043-a-node-turns-about-its-pivot.md`). Paint-only: the node takes the room its upright self takes, nothing around it moves, `onLayout` reports the layout rect. Everything the subtree draws turns with it — backgrounds, borders, shadows, text, images, strokes, fragments — and so does what it clips: a child cut by a turned card's rounded corners stays inside them. Hit where drawn: a tilted card is grabbed on its tilted edge and a press in its box past its edge falls through; drag payloads stay in viewport px. The access rect is the bounding box. Nests by composition. Tweens with `transition` as one slot with `scale`, and an entrance, an exit or a keyframe stop may name it (`enter: { rotate: -0.02 }`, `keyframes: [{ rotate: 0 }, { rotate: 1 }]` spins a box). A float anchored to the parent turns with it; a viewport float does not. On a `path` this is the path's own turn (ADR 0041), which does not tween — wrap it in a box for one that does. Text under a turn leaves the pixel grid, as a turned mask does. `backdropBlur` under a turn blurs the upright box. |
|
|
92
92
|
| `ruleWidth` | `rule_width` | `rule_w` | `Spec.rule_width` | number, or a `"$length"` token | The width of a table's `rules` in logical px; 1 when unset. |
|
|
93
93
|
| `rules` | `rules` | `rules` | `Spec.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. |
|
|
@@ -171,7 +171,7 @@ where they make sense); text props apply to `<text>` and `<edit>`.
|
|
|
171
171
|
| `<radioGroup label>…radios…</radioGroup>` | `radio_group { label=, … }` | `kui_radio_group_open` … `kui_close` | `kui.radio_group` in an `if`, closed at its end | A container of radios (`widgets::radio_group_with`, ADR 0034): the `radioGroup` role, named by its `label`, laid out as a column with the stock gap — a `dir="row"` lays the radios across, and its arrows run across with it. It reads every box row; the role and the name are its own whatever the rows say. |
|
|
172
172
|
| `<switch checked onClick key label description tooltip disabled>text</switch>` | `switch { label=, checked=, on_click=, key=, text=, description=, tooltip=, disabled= }` | `kui_switch` | `kui.toggle` | 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. |
|
|
173
173
|
| `<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` | `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. |
|
|
174
|
-
| `<image src={id} sampling fit>` | `image { id=, sampling=, fit= }` | `kui_image`, `kui_image_with` | `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. |
|
|
174
|
+
| `<image src={id} sampling fit>` | `image { id=, sampling=, fit= }` | `kui_image`, `kui_image_with` | `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. Drawn at less than half its texels a pixel, a `linear` image is drawn from a level the core halved it to (`docs/adr/0044-an-image-drawn-smaller-is-drawn-from-a-level.md`): the deepest level with at least a texel a pixel, the 2×2 means taken in linear light, made once per session at the first draw that wants it and kept in the atlas in place of the whole image — so a photo registered as decoded and shown on a card is neither resampled by the app nor aliased on screen. The display's scale and any `scale` the node is drawn through count. `nearest` and an image ever updated (a stream) draw the whole image. |
|
|
175
175
|
| `<polygon points={[[x,y],…]} bg/>` | `polygon { points={{x,y},…}, bg= }` | `kui_polygon` | `kui.polygon`, its points a `[][2]f32` | 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. |
|
|
176
176
|
| `<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` | `kui.path`, `kui.path_d` | Any outline — SVG's `d`, a pie wedge with a round arc, a map's region, an icon — filled with `bg` by `fillRule` (`nonzero`, the default, or `evenodd`) and stroked `width` wide in `color` when `width` is given, the stroke over the fill (`docs/adr/0040-a-path-is-a-mask-in-the-atlas.md`). `d` is SVG path data (`M L H V C S Q T A Z`, absolute or relative), parsed by one parser in the core, so every binding draws the same shape; one that does not parse raises `path-malformed` and draws nothing. JSX also takes `d` as a flat number array of op codes and operands, Lua the same as `ops`, and C only that form (`kui_path_parse` turns a string into it). Placed as a `line` is — always a float in its parent's box space (`float="viewport"` for viewport space), sized to its own bounding box two pixels out on each side (half the stroke's width further), so it takes no room in a row or column, held by the parent's clip as a child is and painted in the parent's layer at its place in the tree (backlog F123). `transition` eases the fill and, with `slide`, its position; the stroke's colour does not tween, as a box's border does not. Hit by its outline under the fill rule (`docs/adr/0026-hit-testing-by-shape.md`): with `onClick`, `onDrag`, `onHover` or `hoverable` a press inside hits it and one in its box past the outline falls through, so a pie's wedges need no hit boxes; a stroke with no fill is hit by its stroke as a line is; with input it derives an access row as a box would (a clickable wedge is a button), so name it. On the wire it is one glyph-mask quad per paint, fill and stroke: the outline is rasterized once per shape, scale and quarter-pixel position into the glyph atlas and tinted like a glyph, so a host that draws text draws paths, and nothing is re-rasterized for a colour tween, a hover or a slide. The fill bleeds half a pixel, so two paths sharing an edge meet without the background showing through; a chart that wants separators gaps its own geometry. A mask a quarter of the biggest atlas page or more, or a path whose ops change twice within a few frames, draws from a texture of its own instead (a `texture` quad), and one past 8192 px on a side draws nothing, with `path-too-large`. `rotate` turns the path, in turns clockwise, about `pivot` — a point in the path's own coordinates, the centre of its box without one (`docs/adr/0041-a-mask-turns-about-its-centre.md`): the turn is the quad's and not the mask's, so a path that only turns — a spinner's arc about its circle's centre — is rasterized once and stays in the atlas at every angle. A path with `rotate` or `pivot` is boxed by the square the turn sweeps, its mask centred on the pivot on a whole pixel, and it is hit where it is drawn; `rotate` does not tween. `dash` and `dashOffset` cut the stroke into marks and gaps as a `line`'s do (backlog V2) — lengths as seen, round-capped marks, a gap the dots overlap closed — restarting at every subpath as SVG's do; the pattern is part of the stroke's mask, so a dashed stroke costs what a solid one does, a stroke with no fill is still hit along its gaps, and a `dashOffset` that changes every frame is a shape that changes every frame: the path leaves the atlas for a texture of its own while it marches. |
|
|
177
177
|
| `<fragment src={id} image={id} params={[…]} animate>` | `fragment { id=, image=, params={…}, animate= }` | `kui_fragment`, `kui_fragment_with` | `kui.fragment`, `kui.fragment_with`; `kui.fragment_open` in an `if` holds children | 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. |
|
|
@@ -256,7 +256,7 @@ people. In Node the codes are the `WarningCode` union.
|
|
|
256
256
|
| `transition-auto-key` | The child count of a node changed while one of its children carries a transition under an auto-assigned key. Auto keys are sibling positions, so the children that shifted became new nodes and snapped instead of easing. Give list items a key. |
|
|
257
257
|
| `duplicate-key` | Two nodes in one frame share a key: everything retained per key (transitions, scroll offsets, editors, layout events, hover state) is mixed between them. Siblings need distinct keys. |
|
|
258
258
|
| `ambiguous-key` | A label resolved by name (`focus("beta")` in Node, `env.set_focus("beta")` in Lua, `kui_key_of` in C) is declared by more than one node in the frame, under different parents, so they have distinct keys and the name picked the first in tree order. Labels are unique among siblings, not across a tree. An extension asking from inside its fill is answered from the nodes it opened and no one else's, and the host from its own first — so this is a clash among the asker's own. Give the node meant a label nothing else declares, or pass the hex key an event carried. Two nodes with the *same* key are `duplicate-key`. |
|
|
259
|
-
| `ambiguous-name` | A lookup by accessible name (`Core::key_named`, `ctx.keyNamed`, `kui_key_named`) found more than one node with that name in the last frame — two buttons both read as "Delete" — and used the first in tree order. A reader hears the same name twice too: give each a `label` that says which, or look the one meant up by its key label. |
|
|
259
|
+
| `ambiguous-name` | A lookup by accessible name (`Core::key_named`, `ctx.keyNamed`, `kui_key_named`) found more than one node with that name in the last frame — two buttons both read as "Delete" — and used the first in tree order. A reader hears the same name twice too: give each a `label` that says which, or look the one meant up by its key label. When one of them is static text and another is not — a caption under the button it repeats — the message says so: a caption a reader need not hear is decorative, and `role="none"` on the box around it takes it out of the tree (backlog F140). |
|
|
260
260
|
| `focus-region-without-node` | A `focusRegion(name)` (`Core::focus_region`, `env.focus_region`, `kui_focus_region`) named a node the frame after it did not declare as a `focusRegion` — no node under the label, or a node without the row — so nothing was entered and focus stayed where it was. The call is resolved against the frame it lands on, so an `update` that toggles a dock on and enters it in one go is fine; this is that call with the view half missing, with a name the view spells differently, or naming a node that is not a region. |
|
|
261
261
|
| `label-without-node` | A `reveal` or `setScroll` by label (`env.reveal("rows")`, `win.reveal("rows")`, `Core::reveal_label`) named a label the frame it resolved against did not declare, so nothing moved. A label is resolved when the frame finishes, so a view may name a node it is declaring right now, or one the next frame declares; this is the name spelled differently from the `key` that declares it, or the node not declared at all. |
|
|
262
262
|
| `unknown-family` | A text's `family` named a family no installed or loaded font has, so it shaped as sans. `sans`, `serif` and `mono` are kui's own; any other name is matched as `addSystemFont` matches it, and `systemFonts()` lists the names a machine has. |
|
|
@@ -460,7 +460,7 @@ everywhere else — and `compact` leaves it alone.
|
|
|
460
460
|
| `menu_pad_y` | `menuPadY` | 5 | 3 | A menu row's vertical padding; a menu-bar title's is two px less. |
|
|
461
461
|
| `menu_width` | `menuWidth` | 200 | 180 | A menu panel's width. |
|
|
462
462
|
| `menu_bar_h` | `menuBarH` | 26 | 22 | The drawn menu bar's height. |
|
|
463
|
-
| `titlebar_h` | `titlebarH` | 32 / 34 | 32 / 34 | The titlebar
|
|
463
|
+
| `titlebar_h` | `titlebarH` | 32 / 34 | 32 / 34 | The titlebar strip's height: the platform's caption height, 32 on Windows and 34 elsewhere, or the window's own where the runner knows it (backlog W22) — 40 and 52 off macOS for a launcher's `medium` or `tall` titlebar under custom chrome, and under macOS custom chrome the OS's own titlebar as `window.native_controls` measures it (32, 40 or 52 on macOS 27). The stock number stands for that window's height, so a set of the app's keeps it unless it names a number of its own. Under macOS custom chrome the strip is drawn at the measured height whatever this row says (`widgets::titlebar_height`). |
|
|
464
464
|
|
|
465
465
|
## Doors
|
|
466
466
|
|
|
@@ -647,6 +647,9 @@ its generator (`nu scripts/odin.nu gen --check`).
|
|
|
647
647
|
| `Core::set_inspect` | `kui_set_inspect` | `set_inspect` | `setInspect` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Turns the per-frame node snapshot behind `nodes` on. |
|
|
648
648
|
| `Core::nodes` | `kui_nodes` | `nodes` | `nodes` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The last frame's nodes with what layout and the declarations made of them — a tree view's and an inspector's data. |
|
|
649
649
|
| `Ui::add_extension` | `kui_ctx_add_extension` | `ctx_add_extension` | `Ctx.addExtension` | `add_extension` | Loads a plugin under a namespace; a `KuiWindow` takes its list at construction (`extensions`). |
|
|
650
|
+
| `Ui::slot_kept` | `kui_slot_kept` | `slot_kept` | `<slot name params keep/>` | `fill { name=, params=, keep = true }` | Declares a slot as `slot` does and keeps what the fill built — every node as its door saw it, the slots inside, every fact of the frame it read — for `slot_replay` to push again (ADR 0045). |
|
|
651
|
+
| `Ui::slot_replay` | `kui_slot_replay` | `slot_replay` | `<slot name params replay/>` | `fill { name=, params=, replay = true }` | The host's claim that nothing it feeds the extension changed: the core checks the params, every fact the kept fill read and the slot's place, and pushes the kept nodes again without the extension, or fills and keeps and says why (ADR 0045). |
|
|
652
|
+
| `Core::slot_fill` | `kui_slot_fill` | `slot_fill` | `slotFill` | `slot_fill` | What the last `slot_replay` of a name answered this frame, or the frame before while this one is being built: replayed, or why it was filled fresh (ADR 0045). |
|
|
650
653
|
| `Launcher::extensions` | `kui_ctx_extension_count` / `kui_ctx_extension_namespace`, one at a time | `ctx_extension_count` / `ctx_extension_namespace`, one at a time | `Ctx.extensionNamespaces` | `extension_namespaces` | The namespaces loaded. |
|
|
651
654
|
| `Core::frame` | `kui_frame_begin` … `kui_frame_finish` | `frame_begin` … `frame_finish`, or `kui.frame` around a view | `Ctx.frame` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | Runs one frame: the view, layout, the draw list; `KuiWindow.setView` is the windowed form, the runner calling it. |
|
|
652
655
|
| `Core::output` | `kui_draw_data` | `draw_data` | `quads` | *none: a script is a guest in the host's frame (ADR 0014): its env is the view's reading, and registering, driving, pacing and reading back are the host's* | The draw list: quads, clips, fragment and texture draws (`clips`, `fragmentDraws`, `textureDraws` beside `quads` in Node) and the frame's stats. |
|