Redesign span layout around retained placement

This commit is contained in:
iris committed 2026-09-09 15:11:57 -04:00
1 parent ae0af8f5e3
commit 0aa03cf621
30 files changed
+501 -1321

No files matched your search

+18 -20
View File
@@ -21,8 +21,8 @@ controller back, and gets `scroll`, `fling`, `drag`, `amt`,
Two widgets have one, and they differ only in how they spend a delta:
- **`ScrollArea`** (`scroll_area.rs`) — a fixed child, measured whole and
then slid about as a lump, which is what makes a scroll tick an O(1)
- **`ScrollArea`** (`scroll_area.rs`) — a fixed child, drawn and then
slid about as a lump, which is what makes a scroll tick an O(1)
move of one subtree. `.scrollable(axis, pin)` wraps anything in one.
- **`LazySpan`** (`lazy_span.rs`) — lays its own rows out from an anchor,
so it cannot be a lump and is not wrapped in anything. Its own
@@ -126,21 +126,18 @@ The same direction for both owners, and a different origin:
A scrollbar needs a real content length before it can use either, and a
lazy span has none. Do not invent one.
## `ScrollArea::draw` — measure, then place
## `ScrollArea::draw` — draw, then place
1. `take_delta`, and move to where it asks.
2. Draw the child in a box as long as **last frame's** length, to measure
it. This is free in the common case: the same region as last frame
means `draw_inner` returns immediately.
3. Apply the pin and clamp against the length just measured.
4. Draw the child again, at that length and position.
2. Offer the child **last frame's** length and read the size it reports.
An unchanged child returns from `draw_inner` without running `draw`.
3. Apply the pin and clamp against that size.
4. Place the retained drawing at its exact length and position. It is
redrawn only if its reported size does not fit that box.
Only the second draw decides anything, and a frame on which the content
did change pays one real extra draw — a frame on which it was being
redrawn anyway. Placing against the hint and letting the next frame fix it
is what hung the composer's text half a line outside its box on Iris's
phone: **layout is a pure function of the state, not of how many frames
have been drawn**, and there may be no next frame.
There is no measurement mode and no discarded drawing. Placing against
the old length and letting the next frame correct it is not valid: layout
must finish from the current state even if no later frame arrives.
The pin only re-pins on a frame with **no delta of its own**: the pin
means "stay flush with the end as the content grows", and a reader who has
@@ -157,11 +154,10 @@ can say how far it may go, so nothing above it is in a position to.
Measured 2026-09-08, and worth not re-deriving:
- A `Span` is skipped entirely in the steady state, but **when it is
redrawn it costs two draws per child** (21 draws for 10 children):
phase 1 offers each child the ambient region to learn its length,
phase 2 offers it its real share. So any mutation of a `Span` redraws
all of it — 24 draws for 11 children after one prepend.
- A `Span` is skipped entirely in the steady state. When redrawn, it uses
exact hints first, draws unknown fixed children forward from the cursor,
and places retained drawings after flexible allocation. A child is
redrawn only when its final box changes size.
- A `ScrollArea`'s efficiency and virtualisation pull opposite ways: a
scroll tick offers a same-size moved region, `draw_inner` takes the
`mov` path, and the child's `draw` never runs. A virtualising child
@@ -282,7 +278,9 @@ cannot pan; there is a `debug_assert` in `drag` naming that.
even enter the widget. This is the number any "store the edges and only
recompute what changed" optimisation would have to beat, and it is why
the walk was left alone.
- `Span`, redrawn: two draws per child (see above).
- A fully hinted `Span` draws each child once. Unknown fixed children draw
provisionally and move; region-dependent children redraw if their final
box has a different size.
- The one design that would collapse those 31 moves into a single delta
write is moving the content as a unit, which needs a content length —
which a lazy layout cannot supply.