Record what the shaping store cost and what it bought

Both gestures, since two widths in turn flatters a store and a drag does
not, and the memory beside the time: the question that decided the shape
of it was whether it is a fixed cost or one per text widget.

Bring LAYOUT.md's §1 and §2 to what shipped. A widget returns its size
rather than recording it, a slot carries a box rather than a
translation, and the per-axis answer is `OnResize` rather than
`is_size_independent` -- and the constraint the position chain replaced
was an agent's, which the text now says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-14 18:51:34 -04:00
1 parent 2acfcf51aa
commit d50e791130
2 files changed
+88 -68

No files matched your search

+30 -27
View File
@@ -4,6 +4,11 @@ A widget draws once and records its size on the `Painter`. Reading a child
`DrawResult::size()` records a retained size dependency; drawing the child
without reading that result does not make the parent's size depend on it.
§1 and §2 have landed in Iris (#16 and #18) and the notes below have been
brought to what shipped rather than what was proposed; §3 to §6 describe the
same design as it stands. `docs/IRIS_EXTRACTION_HANDOFF.md` has the invariants
the code now rests on and what is still to do.
## Design
### UI ownership and frame access
@@ -28,17 +33,15 @@ draw hooks so dispatch never has to scan every active widget.
```rust
pub trait Widget: Any {
fn draw(&mut self, painter: &mut Painter);
fn draw(&mut self, painter: &mut Painter) -> Size;
fn size_hint(&self, axis: Axis) -> Option<Len> { None }
fn is_size_independent(&self) -> bool {
false
}
fn on_resize(&self, axis: Axis) -> OnResize { OnResize::Redraw }
}
```
Every implementation calls `painter.set_size(size)` exactly once. A child
A widget returns what it used of the box it was given. A child
draw returns a `DrawResult` that keeps the painter borrowed; calling `.size()`
on that result reads the child's retained size and records that the current
widget depends on it. Dropping the result without reading it draws the child
@@ -76,11 +79,13 @@ rewriting the row whenever an ancestor moved and would restore the very
O(subtree) work this design removes. Chain depth is bounded in both Rust and
WGSL.
`Painter::set_child_offset` inserts a retained coordinate slot between a
container and its direct children. `LazySpan` uses one so visible row boxes
remain stable while scrolling changes a single shared translation. Ordinary
window-relative positions remain `rel + abs`; move slots carry translation
only, not general remapping.
`Painter::place` draws a child whose box its parent decides and may decide
again, and gives that child a slot of its own; `widget` and `widget_within` do
not, and share the nearest ancestor's. A slot carries a whole **box**, not a
translation: a pixel-space scale and offset would scale a child that has to
keep its pixel length, and the `rel`/`abs` pair is what distinguishes the two.
(That slots carry translation only was an agent's choice on 2026-09-04, never
asked for, and #18 replaced it.)
`UiRenderState::resolved_region` performs the same chain walk on the CPU for
hit-testing, accessibility, and public window-coordinate queries. Masks store
@@ -120,19 +125,17 @@ dirty widgets first, with a changed returned size propagated one reader edge
at a time. Re-reporting the current output size is a no-op.
**(b) A widget's `available` (its parent's offered region) can change
without the widget's *content* changing — this is what
`is_size_independent` (§1) answers.** When a container's own layout shifts
(a sibling grew or shrank, changing this widget's offered box), a widget
that returns `true` from `is_size_independent` is not redrawn: its
primitives are unaffected by size, only by placement, so the parent
either (i) issues a move (§2) if only position changed, or (ii) rewrites
the primitive's `region` fields directly via `region_mut` if the box
changed shape too (still O(primitives owned directly by this widget, not
its subtree, since a size-independent widget by definition has no
size-dependent descendants worth distinguishing — in practice this is
always a leaf: `Rect`, `Image`, a fixed glyph). A widget that returns
`false` (the default) is redrawn in full whenever `available` changes,
which is correct always, just not free.
without the widget's *content* changing — this is what `Widget::on_resize`
answers, per axis.** When a container's own layout shifts (a sibling grew or
shrank, changing this widget's offered box), a widget that says `Scale` on the
axes that changed is not redrawn: everything it drew is a fraction of its own
slot's box, so writing that one box moves and stretches all of it. `Span`,
`Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say `Scale`;
`Scroll` and `MaxSize` read their box in pixels and cannot. `Redraw`, the
default, is correct always and free never. `Translate` — an unchanged drawing
placed somewhere else in a bigger box — is reserved: nothing reads it until a
widget can say where in that box its drawing belongs, which is the alignment
work.
**Size invalidation travels upward before drawing; drawing itself travels only
downward.** Every active widget retains the direct children whose size it read
@@ -180,7 +183,7 @@ length is known, flexible space is allocated and `Painter::place` moves
each retained child into its final box. A child is redrawn only when that
box changes the size it was drawn for.
Hints are optional and affect cost, never correctness. `Sized` can report
Hints are optional and affect cost, never correctness. `SetSize` can report
its declared axis without inspecting its child, which covers the important
`.height(rest())` case. A debug assertion compares every hint with the
eventual `draw` result. Widgets whose answer depends on shaping or on a
@@ -201,11 +204,11 @@ buffer. That check is kept exactly as it is; it is the caching mechanism,
and it already operates at (id, region) granularity, which subsumes "(id,
available size)" once size *is* what a region change means.
`ActiveData::size` stores the value the widget recorded with
`Painter::set_size`. This is what a parent placing the widget for a second
`ActiveData::size` stores the value the widget's `draw` returned.
This is what a parent placing the widget for a second
frame without redrawing it reads instead of recomputing — it replaces
`Cache.size`'s role of "answer a size question without a full draw" with
"read the size of the last actual draw." `ActiveData::size_dependencies`
"read the size of the last actual draw." `ActiveData::size_deps`
stores the direct children whose `DrawResult::size()` or known length the
widget observed during that same draw; the next draw replaces the list, so a
dependency disappears as soon as the widget stops reading it. Both fields