Finish the transparent-frames handoff

This commit is contained in:
iris-ai committed 2026-09-18 13:49:38 -04:00
1 parent bdddb610c0
commit 280fad7472
3 files changed
+781 -1617

No files matched your search

+315 -1595
View File
File diff suppressed because it is too large. Load diff
+217 -22
View File
@@ -4,11 +4,12 @@ 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, §2 and §3 have landed in Iris (#16 and #18) and the notes below have been
brought to what shipped rather than what was proposed; §4 to §6 describe the
same design as it stands, and name types that have since been replaced where
they were written before it. `docs/HANDOFF.md` has the invariants
the code now rests on and what is still to do.
§1, §2 and §3 have landed in Iris (#16 and #18). §4 to §6 and the density
section retain the rationale of the design but still name types that have
since been replaced; they are not an API reference. `docs/HANDOFF.md` is the
current transparent-frames work and its checks; `docs/LAYOUT_LOG.md` is what
the sessions doing that work found, kept until it lands. The sections at the
end of this file are durable design moved out of the handoff on 2026-09-18.
## Design
@@ -143,14 +144,14 @@ Drawing that ancestor consumes the marks of every dirty descendant it
reaches; the loop then takes whatever remains. Drawing never synchronously
invalidates or invokes a parent, so there is no layout recursion.
Dirty widgets settle deepest-first, and `dirty_size_under` stops a reader
taking a retained answer while something below that answer is still dirty --
an optimisation against laying out twice rather than a second validity
mechanism. An exact `size_hint` stops propagation when both axes still equal
the retained size; otherwise propagation is deliberately conservative, since
only a dependent ancestor can assign the final boxes. This is a generic
constraint rule, not a text exception. Wrapped text is merely the common
example: it reads width, so changing only height leaves its answer valid.
Dirty widgets settle deepest-first. `dirty_size_under` has been deleted;
settling consumes descendant marks bottom-up, so no clean retained answer can
hide an unsettled size dependency. An exact `size_hint` stops propagation when
both axes still equal the retained size; otherwise propagation is deliberately
conservative, since only a dependent ancestor can assign the final boxes.
This is a generic constraint rule, not a text exception. Wrapped text is
merely the common example: it reads width, so changing only height leaves its
answer valid.
### 4. Wrapped text, and "needs child height before choosing width"
@@ -312,14 +313,208 @@ fragment stage cannot make. Rendering and hit-testing both traverse the full
mask chain and use the same rounded-rectangle coverage; `iris/tests/mask_sdf.rs`
checks the WGSL implementation against the CPU SDF.
## Offered boxes
## Frames, decided boxes and padding
`Pad` must work in every container: it offers an inset region to its child and
reports the child's used size plus padding. In a generous parent it behaves as
an inset; in a tight parent it grows the result outward.
Containers that only divide room are transparent to fractions. A child frame
is narrowed by a length its parent decided: a declared `px` or `rel` length,
or the resolved slot of a `leftover` child. A box a widget reports for itself
does not narrow its descendants' frame.
When a widget does not fit its offered box, it is redrawn at the box implied by
its reported size in the same frame. Deferring would leave ordinary
`.background(rect(..))` surfaces one frame behind their content. The settling
draw occurs only when the widget's own size changes. Widgets whose size varies
with every offered box are therefore unsuitable as `LazySpan` rows.
`Pad` is an outset: it forwards its frame less the padding, draws the child
inside that area, and reports the child's used size plus padding. A
`rel(1.0)` child inside padding inside a share is a fraction of the resolved
share less that padding. The mixed "outset pixels, inset fractions and
shares" interpretation is rejected.
The current experiment still redraws some widgets in boxes derived from their
own answers. That is the open protocol defect, not a design invariant. The
target in `docs/HANDOFF.md` evaluates container bodies only in boxes a parent
offered or decided; placing an answer reuses or translates its drawing rather
than running the body in an answer-derived box.
## Layout decisions and invariants (2026-09-15 to 2026-09-17)
Moved here from the handoff on 2026-09-18. These are settled unless a
subsection explicitly says it is pending.
### Fixed point
Decided with Bryan on 2026-09-15. Layout decides on a grid rather than in
floats.
- **`Fixed<SHIFT>` is an `i32` counting `1 / 2^SHIFT`.** Adding and
subtracting are exact; `mul` drops to the step below (Bryan, 2026-09-16:
truncation is preferable); `div`, `div_int` and `ratio` round to nearest;
`to_scale` takes the nearest step. Two routes to one place that land on
one number are the same place, so everything downstream compares for
equality.
- **`Px` is `1/1024` px, `Rel` is `1/2^24` of a box, `Weight` is `1/65536`
of a share.** `PX_SHIFT` and `REL_SHIFT` are the only statement of the
first two; the shader's copy is prepended from them by
`render::module_source`. `Px` was `1/64` first, where one rounding's
residue was 0.016 px and enough to move a box. Range is +/-2.1M px and
conversion to `f32` is exact to 16,384 px.
- A weight is not a fraction: a list divides its room by the total of its
weights, and `Rel::ratio` turns two weights into a share on the finer
grid.
- **Arithmetic wraps** (`4febabf`, Bryan: a coordinate past the range will
not draw reasonably anyway, so wrap and break clearly). Saturating cost a
twelfth of layout's instructions. `MIN` and `MAX` stand in for an
unbounded end and are only ever compared against; `from_f32` is the one
operation that clamps, and `Holds` keeps a saturating `narrow`.
- A pointer, a wheel notch, a shaped glyph advance and a window size arrive
as floats and go on the grid where they arrive. `Vec2` is what the GPU
and the platform speak; `PxVec2` is what layout decides in.
- **Do not widen the grid to chase a residue.** Every failure seen was one
value reached by two expressions, sitting on a boundary defined by the
same value coming back the other way. No precision shrinks a residue that
is the whole distance.
- **A value that comes back as a box is rounded away from the measurement,
not to the nearest step.** `Fixed::ceil_from_f32` exists for that and is
the only rounding on the grid that is not to nearest. Rounding to nearest
is right for a value being carried and wrong for a bound; a text reporting
`ceil` of its longest line is what keeps the box it is handed back one its
line fits in (`4bd8607`).
- **A structural decision may not be taken on a hair's breadth.** A
boundary that decides which children exist (a span's leftover split) is
derived through the inverse of the expression that draws, never by a
second expression for the same length: `mul` floors while `div` rounds,
so a boundary derived with a division guards a drawing made with a
multiply (`53b00c6`).
### A box in pixels is one multiply from its parent's
A draw threads pixel lengths down: the box a parent gave a widget, then the
part of that box its own answer placed its drawing in. `Painter::px_size`
and `px_len` read that value, and a local redraw takes the same steps back
up the parent chain (`asked_px`). Neither chain has a coordinate frame in it,
so a region node cannot break either, and warm and cold reach every length
by the same expression.
- **`Holds::through` is the exact preimage of `px + floor(rel * box)`**:
`floor(rel * B) >= lo - px` is `rel * B >= (lo - px) << REL` and
`floor(rel * B) <= hi - px` is `rel * B < (hi - px + 1) << REL`, two
`div_toward`s once the sign of `rel` has said which bound is which. The
answer is an interval even for a single length, because a floor is not
invertible. The range has to contain the box a drawing was made in (the
`Holds` assertion in `draw_at`, debug only) and must not contain a box
the drawing does not hold for (the oracle); being the preimage makes
those one statement rather than a trade-off.
- **Symbolic regions are for the GPU, hit testing and remaps alone.**
`Moves::resolve` is the only walk left and it is the vertex shader's.
Nothing layout decides is composed back up the move chain.
- **`px` is not stored on `ActiveData`, deliberately.** A resize every
widget's `Holds` admits redraws nothing, so a stored pixel length would
be stale on every widget in the tree with nothing to say so. `asked_px`
walks up only where a widget is already being redrawn; the mean chain is
2.8 levels.
- **The window is not a move entry** (`5b78002`). A chain bottoms out in
`MoveIdx::NONE`; the window is applied where a fraction becomes pixels,
`to_px(output_size)` on the CPU and the uniform in the shader. A resize
rewrites no retained entry and re-uploads nothing but the uniform; its
cost is whatever `Holds` redraws.
- **A move that keeps a box's length is a translation, and exact.** A box
that changed length re-expresses each part as a fraction of the new one,
which rounds. `tests/cases/drift.rs` pins that the grid does not drift
either way. A length given in pixels is that many pixels wherever it ends
up (`Len::within` adds a part's own pixels rather than scaling them);
equal shares come out one or two steps apart because positions, not
lengths, are what gets rounded, so the row fills and no two children
leave a seam.
### Retained-layout invariants
- `Holds` is the interval of box lengths for which a widget's drawing and
reported size remain valid. Reading `Painter::px_len` or `px_size` narrows
it; `Painter::holds` widens it. The contract is trusted rather than checked
defensively on every use.
- A retained drawing is reusable only when its `Holds` contains the new box
on both axes, its parent and region-node choice match, it is on the layer it
was drawn on, and the widget is clean. A valid ordinary subtree moves by
recursive remap; a region node moves by one entry. A container that draws a
child to learn its size uses `Painter::child_layer_at`, the layer the child
will actually occupy.
- An answer's validity and its final drawing's validity are independent. A
parent may reuse an answer while redrawing the placed output. Translate the
drawing contract back through its placement; do not intersect it into the
answer contract.
- A fraction resolves once against its frame. A report returns raw and is
composed only where a parent narrowed that frame. A part's own pixel length
is added rather than scaled, so a pixel length remains that many pixels at
every nesting depth.
- An asked-but-undrawn size dependency belongs to the widget that asked. Keep
it recorded so a later child change reaches the parent that decided not to
draw it. Dirty size dependencies settle deepest-first.
- A widget that creates a mask clips to and reports its box. Its own mask and
its inherited mask are distinct retained state: the former says which mask
a move rewrites, while a local redraw receives the latter.
- A span's leftover/no-leftover boundary is a strict structural decision, not
a tolerance. Derive the boundary through the inverse of the expression that
places children. A cap may not contain `leftover`, because feeding the
span's own room division back into a cap admits multiple fixed points.
- Text shaping is retained separately from line breaking. A greedy break
holds from its longest produced line through the width at which it was
made, expressed with `Painter::holds`.
- A region node stores one whole `UiRegion` in its parent node's coordinates;
`FULL` is the identity. Changing node ownership redraws the subtree once,
and a removed node's move entry remains alive until every descendant has
migrated.
- Alignment is one value per axis and defaults to the middle because neither
edge is neutral without a direction. One widget has one length per axis; a
second length requires a second widget through `.wrapper()`.
### What the fuzzers tolerate
Warm and cold pixel regions must compare exactly; there is no step
allowance. When a row's grid-step count is not divisible by the number of
children, individual share widths differ, but every rerun of that layout
must still agree exactly.
### Rendering the grid (pending)
`snap_floor` in `prelude.wgsl` adds half a layout step before flooring,
which absorbs float error and not a layout step, so a third of 900 px
(299.999 on the grid) lands at 299 on screen. Bryan approved on 2026-09-17
rounding to the nearest pixel in the shader together with round-to-nearest
in `Fixed::mul` on the CPU, as one change with one verification; neither has
landed. The reason for the CPU half: a `Rel` is off by at most `2^-25` of
its box, so with round-to-nearest every product whose true value is a whole
number of steps is exact for boxes under about 8,000 px, where truncation
leaves half of them one step short and layout then decides "does not fit"
on a container the user meant to fit exactly. Use the branchless
round-half-up form, `(a * b + (1 << (BY - 1))) >> BY`; re-derive
`Holds::through` for it; check with `nm` that `UiSpan::within` still
inlines.
## Measuring layout cost on this machine
- **Check the work counters before comparing two commits' times.**
`tests/layout_diagnostics.rs` prints drawn widgets, widget draws and
primitive writes; a comparison is only worth reading when they match.
`random.rs`'s `Branch` picks a subtree by a measured pixel length, so the
fixture's shape moves with the thing measured; `Edits::fixed_branches`
pins it for timing and the oracle keeps measured branches on purpose. A
3x once reported was that artifact.
- **`perf stat` in this VM returns garbage readings** for both
`instructions:u` and `cycles:u`, roughly a quarter of the time, off by a
factor of five to fifteen. Take medians of nine or more and report how
many readings a filter kept. Instruction counts hold to 0.02% within a
binary and move 0.5% across a rebuild, so build the baseline beside the
thing measured and quote a delta.
- **What moves cycles is whether `UiSpan::within` inlines.** It is the
hottest line in layout; `nm` shows it as a symbol when it does not.
Shrinking its body until the inliner takes it won; `#[inline]` on the
body it had lost 1.5% cycles. Shrink it, do not annotate it.
- `Holds::through` divides twice per call and accounts for essentially all
of a run's `i64` divisions: 2.8% of a 500-frame `many`.
- Tried and rejected, with numbers: a float reciprocal for the remap
division, +6% cycles; branchless `shift_round`, +6.7%; removing the
per-child hash lookup in `remap_subtree`, 0.0%; short-circuiting
`apply_scalar` where the fraction is nought or one, +17%. Short-circuits
guarding a saturating multiply stopped paying once the multiply wrapped;
re-price a short-circuit before keeping it. Rust does not contract
`a + b * c`. Wrapping (`4febabf`) was -8.6% instructions; truncating
(`08c9d5a`) costs a share a thousandth of a pixel of its row.
- Threading the pixel box down the draw (2026-09-17) was free on cold
layout and 9-13% of instructions off the retained paths, measured against
`5b78002` at seed 1, depth 8, medians of 21.
+249
View File
@@ -0,0 +1,249 @@
# Layout findings log
What the sessions working on Iris's retained layout found, planner and
worker alike, kept so that nothing here is rediscovered. Each entry says who
found it and when. **Delete this file when transparent frames lands**; what
must outlive it (settled design, the measurement method) is already in
`docs/LAYOUT.md`, and the current plan is in `docs/HANDOFF.md`. Commit ids
are in `/home/bob/repos/iris-layout-experiment` unless said otherwise.
## The two open shrinker seeds on `wip/transparent-frames` (planner, 2026-09-18)
Both are the rule in `draw_inner` that decides which answer places a widget's
box: `measured = if info.offer() { fresh } else { retained }` with
`offer() := place == offer_place`. That bit is meant to say "this ask is a
measurement" and has no consistent value once a container's body runs in
more than one box.
- **Seed 2, `repaint`, depth 5** shrinks to `Stack{sized by child 0}[x:
leftover] > [OneLine, Pad{0} > Span{DOWN}[Rect, OneLine], Branch]`. The
stack's box is one text line tall. The span measured in the stack's room
has leftover room, draws the rect and reports `leftover` across; in the
stack's box it has none, undraws the rect and reports the text's width.
Cold kept the first because the stack's placing evaluation reused the
pad's retained answer, which looked valid only because `in_parent`'s
`Part::Of` arm drops the span's `extent_len` pin. Warm re-asked the span in
the box and got the second. The correct layout is the second: the stack's
non-sizing children belong in the stack's box, and the measuring pass drew
them in a box the stack never has.
- **Seed 108, `reorder`, depth 5** shrinks to `Span{LEFT} > [Span{LEFT} >
[Span{DOWN} > [Branch{probe Rect, wide Rect, narrow Wrapped, 483},
Wrapped], Rect], Rect]`. A draw trace shows the `DOWN` span evaluated at
900, 450, 600 and 300 px across in one cold layout (room of the outer
span, slot inside the inner span's measuring pass, slot of the inner span,
slot inside that), the wrapping text re-shaped at each. In the last one
the branch's narrow text is drawn in its real 300 px box and answers 286
px; `draw_inner` discards that for the retained 438.9 px answer from the
450 px evaluation because the branch's `below` place embeds
`extent_len(Y)`, which moved when the sibling text's height changed, so
`place != offer_place`; the text is redrawn 438.9 px wide in a 300 px box.
Warm does the same with a different stale answer (286 px from the first
frame). The same shape is `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`.
Two one-line experiments, both reverted, run at `49cec82` with the fast
oracle, the debug suite and the shrinker at 400 seeds of depth 5:
| change | seed 2 | seed 108 | else |
| --- | --- | --- | --- |
| compose the pin through `Part::Of` (`len.px - part_len.px` where `part_len.rel == ONE`) | fixed | fails | seed 220 `reorder` and one suite test fail |
| `measured = answer.0` always, and every ask records `answer`/`offer_part` | fails | fixed | seeds 184, 246, 292, 372 and two suite tests fail, all under `Scroll` |
The second experiment's failures are the case the gate was secretly holding
up: a container re-drawn in a box its own answer derived. The specific one
is `Scroll`, whose `apply_leftover` content length adds a whole viewport on
top of the content's pixels (a row reporting `600 px + leftover` in a 900 px
viewport gets a 1500 px content box), so the wrapping text inside is offered
900 px more room than it was measured in and re-wraps; the layout drifts one
iteration per evaluation, and warm and cold differ by how many they ran.
Bryan: that content length was always wrong; a share is the room left in the
viewport, so `content = max(viewport, px + rel*viewport)`. With it the
content box never re-offers room and `Scroll` needs no own-answer
evaluation.
The `many`/`resize` cost is the same mechanism in miniature: four
evaluations of a ten-widget subtree in one cold layout, from `Within(cursor..
far)` measuring rooms followed by `Fill(slot)` placing, with the span's
`extent_len` pin forcing a redraw at every box length.
## What the plan of 2026-09-18 got wrong (planner, same day)
The plan claimed the retained model rests on the frame's length being the
same on every ask. Answers depend on the *part* (the box offered), not the
frame, for every widget that reads its box, so transparent frames fixed
fraction resolution and moved the second geometry from the frame to the
extent rather than removing it. Step 6 ("redraw without the deferral; both
seeds must pass now because the frame no longer changes under the widget")
followed from that claim and was false for the same reason. The plan also
carried `offer_place` ("from the first ask of the parent's draw at the
offer") forward from the old protocol without noticing it is undefinable
once a container is evaluated in several boxes, and its `Inset` sketch
assumed a narrowed frame and a placed extent could coexist, which
`frame_and_extent` collapses ("a narrowed frame is its own extent") and which
the extent-in-frame-coordinates representation cannot express in general
(`(0.5*B - 20)/(B - 20)` is not `rel + px`). The plan also wrote "a frame is
narrowed only by a declared length, an inset or the root" and the code's
`declared_lens` filters `leftover` out; Bryan: a share was always meant to
narrow the frame like the other two.
The worker executed the plan as written, found exactly this ("whether a
given draw is a measurement cannot be recovered from the ask"), tried four
bookkeeping rules (first-ness alone, place matching the retained offer place,
box matching the measured box, a retained flag on the drawing), each fixing
some seeds and breaking others, restored the deferral and stopped. That was
the right call. The plan as handed over is at ai-app-2 `152bed7` if the
wording is ever needed.
## What the worker measured on `wip/transparent-frames` (worker, 2026-09-18)
Widget draws / distinct widgets / update ms from `tests/layout_diagnostics.rs`
at depth 8; draw counts are deterministic so these are single runs.
`e44dea3` is #18's head, `34cafb6` the commit the branch starts from,
`49cec82` its head.
| seed 1 | e44dea3 | 34cafb6 | 49cec82 |
| --- | --- | --- | --- |
| cold | 369/261/10.6 | 463/274/13.3 | 516/288/12.0 |
| repaint | 1 | 1 | 1 |
| many | 157/95/0.33 | 263/108/0.59 | 187/119/0.52 |
| size | 16/12/0.018 | 3/3 | 3/3/0.010 |
| scroll | 2/0.002 | 1 | 1/0.004 |
| resize | 13/13/0.019 | 22/15/0.032 | 24/76/0.090 |
| seed 13 | e44dea3 | 49cec82 |
| --- | --- | --- |
| cold | 1330/707/20.3 | 2940/982/28.3 |
| many | 524/159/1.09 | 1091/423/2.39 |
| resize | nothing drawn | 2215/510/6.56 |
Three findings, each applied on the branch:
- **Lazy `Within` placement costs more than it saves.** Leaving a child's
answer to be placed at the end of the parent's draw puts the drawing in the
part first and in the answer's box after; where it does not hold for both
that is two drawings. Seed 1's resize went from 391 widget draws to 29
with it removed.
- **An inset said in frame lengths makes a container read its own box.**
"Less eleven pixels at the end" needs the length, and a container whose
box is its own answer then depends on its own answer: `Pad` drew
sixty-four times in one resize frame at seed 13. `Part::Of` says it as a
part of the box and composes without a length.
- **Pin one axis at a time.** `extent_len` pinning both axes made a span
dividing one hold for one length of the other.
`Part::Of`'s `in_parent` arm drops a child's `extent_len` pin (only `All`
composes one), which is what let seed 2's pad reuse an answer for a box its
span had never been measured in.
Reference renders at `49cec82` against `34cafb6`: `view` and `minimal`
byte-identical; `tabs` 2,332 pixels; `text` shows `wtext(..).width(rel(1.0))`
inside `.pad(16)` at the window's edges. Under the decided share rule that
example is correct as written: once the share narrows the frame, `rel(1.0)`
is the padded share and fits. The clipped render records the current bug.
`random` moves where nested spans do.
## Defects landed before transparent frames, and their lessons (2026-09-17 review)
- **A report is a fraction of the containing widget** (`ffd79f3`). A
report used to come back composed through the box it was offered, and a
span offers each child the room from its cursor, so a nested span taking
half of what it was given took a quarter of a row whose first half was
spoken for. Consequence: a span can overflow itself without bound, so its
`fixed <= 0` branches are ordinary and boxes of negative length reach
children; nothing yet states what a widget may assume about one.
- **An answer is not an answer while anything under it is dirty**
(`0e0d4af`, superseded by `a0693ac`). `dirty_size_under` was what made an
answer an answer until the settling walk made the state it guarded against
unreachable. Found at seed 564, depth 6, `shuffle-every-other`.
- **A frame settles strictly bottom-up** (`a92c6ac`, `a0693ac`). A widget
that cannot settle defers to its parent rather than drawing the parent
from inside itself; `update` marks the root for a resize instead of
drawing it top-down, and only where the new output falls outside what the
root's answer holds for. Sound by induction on depth. Bryan: "then that
entire category of issue can't even occur."
- **A text is handed back a box its own line fits in** (`4bd8607`): report
`ceil` of the shaped size; the two tolerances that were holding it
together (`BREAK_EPSILON_PX`, a nearest-step `Holds` start) both went.
- **A subtree that changes hands is recorded on both sides** (`e44dea3`):
the old parent's child list is repaired and the subtree's depths re-walked
where its top moved. The fuzzer never re-parents, which is why nothing
generated reached it.
- **A span's leftover boundary is its own inverse** (`53b00c6`): forty
lines became twelve and one `div` left layout.
- **`wip/stack-fraction-twice`** (a stack sized by a child reporting
`rel(0.5)` applied it twice) is closed by transparent frames:
`placed_extent` takes the reported length *from* the part rather than
composing it into the part. No oracle could see it; the branch's test pins
it. **`wip/padding-outset-and-inset`** is superseded: padding goes outside
what it pads, and its `rel(0.5)`-under-inset failure was the same second
application.
- Four findings from the frame/extent prototype still shape the code: an
answer and a drawing each retain their dependencies (`answer_under` and
`under`); a wider contract does not invalidate an existing guarantee; no
measurement is different from a measured zero (`ActiveData::answer` is
optional); the settling walk takes the deepest mark from a `BTreeSet`
keyed by depth and what ends it is the mark set.
## Failed hypotheses worth not repeating
- **The placement pin was blamed for the `many` gap and is not the cause**
(2026-09-17). Disabling it still redrew 487 distinct widgets a frame at
seed 13 against 159; the per-widget trace showed local redraws deferring
to their parents in chains to the root because a span handed its children
its own placement as their frame. Transparent frames fixed that part.
- **`wip/local-reask`** re-asked a dirty widget at its offer instead of
deferring, under the old protocol, and diverged at seeds 532 and 398 of
depth 6. Superseded; the branch can be deleted.
- **An offer composed back up the move chain** fell back to `FULL` under a
region node and was resolved against that node's *placed* box, so
everything under a `Scroll` was re-asked at the content's width. Pinned by
`unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`.
- **Four bookkeeping rules for "which draw is a measurement"** (worker) and
**the two experiments above** (planner): each fixes some seeds and breaks
others. The bit is undefinable; stop trying to define it.
- **Choosing between a fixed and a relative child in pixels at the span's
current width** admits multiple self-sizing fixed points; seed 13 settled
differently warm and cold under it. The same circularity is what a cap
containing `leftover` would put into `SizeRule::Max`.
- **A tolerant endpoint on the span's leftover split** retained zero-height
children at seed 16. Pinned by
`unsettled::a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over`.
- **`Scroll` returning the final placed answer** rather than the first
box's advanced one fixed-point iteration (seed 86). Moot once the content
box never re-offers room, but keep the test until that is measured.
- **A `git bisect`** once named a commit that could not be the cause; read
the tree rather than the bisect when that happens.
## Fuzzer coverage
- Seeds 1121 and 1839 at depth 4 failed on every commit before `4bd8607`
and nothing routine reached them: the fast oracle takes ten seeds, the
shrinker 400 at depth 5, the long oracle 1000 at depth 6. The scan that
found them (2000 seeds at depth 4 over all fifteen cases, 261 s) should be
run after any layout change. `Rng::new` is `seed | 1`, so an even seed and
the odd one above it are one tree.
- The shrinker panics per thread at the first failing seed, so a run
reports at most one seed per chunk of about 58; a seed listed as new after
a change may have been hidden behind another in the same chunk. Check a
single seed against the unpatched code before calling it new (seed 220
was checked this way and is new under the pin experiment).
- Depth finds things and so does breadth; widen one axis at a time and
record which.
## Smaller open items (carried from the old handoff)
- An undrawn `leftover` child still contributes its gap, so a vanished
child leaves a double gap.
- Nested spans pass `leftover` weight up, so three leftover children in one
inner span beside one in another get three quarters to one quarter. No
other layout system does that; confirm it is wanted.
- `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`;
both are caller bugs under `debug_assert`, but the fallbacks differ.
- The comment on the `local == UiRegion::FULL` shortcut in `widget_at` says
composing through `FULL` "is not quite the identity in f32"; on the grid
it is exact and the shortcut is performance only.
- `docs/LAYOUT.md` §4, §5 and the density section name `Painter::place`,
`SetSize`, `desired_width`, `apply_rest`, `Len::dp`, `Aligned` and
`MaxSize`, none of which exist. Do not restore `OnResize::Translate` or
`OrthoSize`.
- `tabs` changed twice across `d3b0ebf` with nobody looking; take the oracle
as the reference and the five renders as a spot check.