diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 410823f..3289430 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -8,7 +8,7 @@ log. Canonical Iris `main` is **`ca2b4b2`** (#17, the headless rig). **#18 `split/18-position-chain`** is open in `/home/bob/repos/iris-pr18`; its local -head is **`d3b0ebf`**, fifty commits. Built-in alignment is complete there; +head is **`5ed9e87`**, fifty-one commits. Built-in alignment is complete there; see "Built-in alignment" below for the retained-layout details. No PR reviews were present when checked on 2026-09-15. @@ -65,20 +65,25 @@ rewrite: - `Span`'s leftover/no-leftover split is a strict layout decision, not a rounding tolerance. Its `Holds` range must use the same exact divided boundary as drawing; a tolerant endpoint retained zero-height children at - the boundary in generated seed 16. + the boundary in generated seed 16. `5ed9e87` moves that boundary rather + than softening it -- see "The leftover boundary" below. `core/src/ui/holds.rs`, the retained tests, and the generated cold-layout oracle pin those rules. Seeds 10 and 86 are now in the ordinary generated set. -## Verification at `d3b0ebf` +## Verification at `5ed9e87` - `cargo fmt --all --check` - `cargo build --workspace --all-features` - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- `cargo test --workspace --all-features`: 88 passed, 10 ignored -- The release generated cold-layout oracle passed 100 seeds in 65.5 s. -- The release shrinker passed both `repaint` and `resize` at 300 seeds each: - 26,001 widgets per case, largest tree 331 widgets. +- `cargo test --workspace --all-features`: 89 passed, 10 ignored +- The release generated cold-layout oracle passed 100 seeds in 68.5 s. +- The release shrinker passed **all five** cases -- `resize`, `repaint`, + `resize-repaint`, `reorder`, `size-change` -- at 1000 seeds of depth 6: + 159,024 widgets per case, largest tree 587 widgets. At the ordinary 300 + seeds of depth 5 it is 26,001 widgets per case, largest tree 331. +- `tabs`, `view`, `minimal`, `text` and `random` render byte-identical at + 1920x1200 against the same worktree without `5ed9e87`. - `tests/drift.rs` passed its 20,000-move exactness check in release mode. - The seeded `random` example, live-resized from 1920x1200 to 1280x800, is byte-identical to a cold 1280x800 render. Both PNGs hash to @@ -175,11 +180,12 @@ tighter figures. - Text shaping is retained separately from line breaking. A greedy line break remains valid from its longest produced line through the width at which it was made, and `TextView` reports that interval through `Painter::holds`. -- `Span`'s decision to distribute `leftover` is a pixel question. Its validity - interval is split exactly at the length where fixed parts fill the box; - pure `leftover` children are undrawn on the no-space side. Exact/open ranges - are for hard widget decisions; the ordinary tolerant ranges remain for - accumulated coordinate rounding. +- `Span`'s decision to distribute `leftover` is a pixel question. The room to + divide is `len * fixed - total.px`, and under `HOLDS_EPSILON_PX` of it is + none; pure `leftover` children are undrawn on the no-room side. The validity + interval is split exactly at that moved boundary. Exact/open ranges are for + hard widget decisions; the ordinary tolerant ranges remain for accumulated + coordinate rounding. - `Scroll` reports its content's first measured size. Its drawing can survive container-length changes only over the interval in which clamping and its current offset do not change. @@ -199,9 +205,7 @@ tighter figures. ## Built-in alignment -Committed as `d3b0ebf` in `/home/bob/repos/iris-pr18`. The shrinker's -`reorder` case remains red at its base `8220a78`, so that failure is -pre-existing and unrelated. +Committed as `d3b0ebf` in `/home/bob/repos/iris-pr18`. What is in it: `align` is a widget property beside `region_node` and the size rule, `Aligned` is deleted, `.align()`/`.center()` set the property, and the @@ -288,6 +292,35 @@ length the box takes and the drawing holds for that length alone. A `debug_assert` that a reported non-leftover size does not exceed the box it drew in would have caught both immediately, and is still worth adding. +## The leftover boundary + +Fixed in `5ed9e87`, which closed the shrinker's `reorder` case. Neither of its +two red seeds was about reordering. + +A span's box in pixels, compared with what its fixed and relative children +fill, is the same number whenever the parent sized that box from the span's own +answer -- and the box comes back through the chain a few bits off. So +`0.00003 px` decided whether a `leftover`-only child existed: warm rounded +under and left it undrawn, cold rounded over and drew it at zero length. Both +layouts are stable and the pixels are identical either way, which is why only +the warm-against-cold oracle could see it. + +**A structural decision may not be taken where boxes structurally land.** The +fix is not a tolerant comparison -- that is what generated seed 16 punished -- +but a boundary moved by `HOLDS_EPSILON_PX` of room, with the validity range +split exactly at the moved boundary. What it gives up is a share of under a +twentieth of a pixel. `tests/unsettled.rs`'s +`a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over` is the +six-widget regression, shrunk from 266; it needs the span above the one that +divides, because without a box composed through it both trees round the same +way. + +`Scroll`'s `content_len <= container_len` sits on the same coincidence but is +continuous there -- it chooses between two `Holds` ranges that both contain the +current length, so a rounding difference costs a redraw rather than a +different layout. Checked while fixing this; nothing else reads a box in +pixels to decide something structural. + ## Rigs and reproduction Ordinary framework verification: @@ -349,15 +382,13 @@ child-placement API. `docs/LAYOUT.md` ยง2 is stale: it still describes **Built-in alignment and size goes on #18 rather than after it** (Bryan, 2026-09-15: #18 is unreviewed and already large enough that most lines get read -anyway). The size half landed as `8220a78`; the alignment half is in the -working tree and described below. +anyway). The size half landed as `8220a78`, the alignment half as `d3b0ebf`. Do not restore `OnResize::Translate`; retained translation is now expressed by the same `Holds` contract and box chain. Queued from this work, in order: -- The shrinker's `repaint` case, via the replacement placement design above. - Delete `OrthoSize`. It is redundant now that size is built in: a span should read its own rule on the orthogonal axis and, where that is fixed, skip reading its children's orthogonal sizes entirely, which is what `Full` does @@ -369,10 +400,10 @@ Queued from this work, in order: clamp resolved where the box is decided also fixes `MaxSize`'s reading of `px_size`, which pinned its interval to one exact box on both axes and redrew its whole subtree on any resize. The clamp boundary is a hard layout - decision, not a tolerance: its `Holds` range must be exact and split at the - crossover, the way generated seed 16 taught for `Span`. -- The shrinker's `reorder` case, which is red at `8220a78` and was not - introduced by any of this. + decision: its `Holds` range must be exact and split at the crossover, the + way generated seed 16 taught for `Span` -- and the crossover itself has to + sit off the length a box structurally lands on, the way `5ed9e87` taught. + See "The leftover boundary". Other queued work, in dependency order: