From cf7b33da39aea36b7f5a878b2382e155879b748e Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Tue, 15 Sep 2026 19:27:30 -0400 Subject: [PATCH] Record why a move recomposes, and settle alignment's ownership The measurement is the part worth keeping: adding an offset to a moved subtree's stored regions drifts linearly and without bound, so the fraction path is there for accuracy, not generality. `tests/drift.rs` on the Iris branch pins it. Built-in alignment and size moves onto #18 rather than waiting for it, with ownership agreed -- properties on the `region_node` mechanism, a declared length winning per axis, and a size rule that carries min/max/clamp so a clamp is resolved where the box is decided instead of by a widget reading its own pixels. --- docs/HANDOFF.md | 68 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 898dbcb..7cf8459 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 **`71c9c39`**, forty-seven commits, pushed to the fork. No PR reviews +head is **`0283c9d`**, forty-eight commits, pushed to the fork. No PR reviews were present when checked on 2026-09-15; the verification summary is posted on the PR. @@ -70,11 +70,11 @@ rewrite: `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 `71c9c39` +## Verification at `0283c9d` - `cargo fmt --all --check` - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- `cargo test --workspace --all-features`: 84 passed, 10 ignored +- `cargo test --workspace --all-features`: 85 passed, 10 ignored - `cargo test --release --test generated -- --ignored a_long_run_of_seeds_agrees --exact`: 100 seeds passed in 64.77 s - The `tabs` reference at 900x1200 remained byte-identical to `f437495`: zero @@ -158,6 +158,19 @@ tighter figures. - `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. +- **A move must re-express each part as the same fraction of its new box, not + add an offset to the last answer.** A subtree's stored regions are the only + record of where it is, so an offset integrates its own rounding and nothing + recomputes it. Measured 2026-09-15 on `tests/drift.rs`: offsetting both ends + of a span shortens that fixture's row by 0.071 over 20,000 moves and 0.712 + over 200,000, growing with the count; placing the far end from the near one + leaves 0.069, because the length is re-derived from the endpoints either + way. 20,000 moves is five minutes of scrolling at 60Hz, which is when it + passes the 0.05 physical pixels layout treats as the same place. The + fraction path is exact at 200,000. So `RegionRemap` is load-bearing for + accuracy rather than for generality, and its multiplies are not what is + being paid for. The generated oracle compares within 0.05 and `unsettled.rs` + runs too few frames to reach it, which is why `tests/drift.rs` exists. ## Rigs and reproduction @@ -213,15 +226,48 @@ instruction totals. ## Next -Let PR #18 review. The next small LAYOUT.md §2 item is `LazySpan`. Region nodes -now cover the independently movable-subtree use case; do not restore a -separate child-placement API. +The next small LAYOUT.md §2 item is `LazySpan`. Region nodes now cover the +independently movable-subtree use case; do not restore a separate +child-placement API. `docs/LAYOUT.md` §2 is stale: it still describes +`Painter::place`, which `71c9c39` replaced. -After #18 lands, the next structural design is built-in alignment and size. -Today a declared size and the `Size` returned by `draw` are two sources of -truth, and alignment can disappear behind a wrapper. Agree their ownership -before implementing it. Do not restore `OnResize::Translate`; retained -translation is now expressed by the same `Holds` contract and box chain. +**Built-in alignment and size is next, and goes on #18 rather than after it** +(Bryan, 2026-09-15: #18 is unreviewed and already large enough that most lines +get read anyway). Ownership is agreed: + +- Declared size and alignment become **widget properties on the same mechanism + as `region_node`**, read by `Painter::widget_at`. `SetSize`, `MaxSize` and + `Aligned` are deleted, and `.width()`, `.sized()`, `.max_width()`, `.align()` + and `.center()` stop building widgets. `Painter` then owns measure-then-place, + which `Span`, `Aligned` and `Scroll` each implement separately today. +- A **declared length wins per axis**; the `Size` returned by `draw` answers + only the axes with no declaration. The rule becomes an enum per axis -- + `Exact`, `Min`, `Max`, `Clamp` -- so a clamp is resolved where the box is + decided instead of by a wrapper. `MaxSize::draw` currently calls + `px_size()`, which pins its interval to one exact box on both axes and + redraws its whole subtree on any resize; the parent resolving the clamp + reads px itself and can declare the interval over which the answer holds. +- The clamp boundary is a **hard layout decision, not a tolerance**, the same + shape as `Span`'s leftover split. Its `Holds` range must be exact and split + at the crossover; generated seed 16 is what a tolerant endpoint costs. +- Two things to settle while implementing. `max_width` today does not + constrain anything -- `MaxSize` draws its child in its full box and only + caps the `Size` it reports upward -- so resolving the rule in the box is a + behaviour change. And `declared_len` derives from `size_hint`, so one + channel carries both "my size, for a span to divide around" and "resolve my + box to this length"; those separate. + +Why it comes before anything else that touches layout: a widget's `region` +becomes **its reported size placed by its alignment inside the offer**, rather +than the offer itself. A span's measure-then-place then moves a child between +boxes of the same length, so it is a translation by construction, and the +measuring draw disappears wherever the size is knowable without one. That +changes what `.background()` sits behind, so the reference renders stop being +byte-identical to `ca2b4b2` and the generated cold-layout oracle becomes the +verification. + +Do not restore `OnResize::Translate`; retained translation is now expressed by +the same `Holds` contract and box chain. Other queued work, in dependency order: