# Handoff Where the work in flight stands for a session picking it up cold. Keep current invariants, measurements, and failed hypotheses here; this is not a decisions log. ## Where things stand 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 were present when checked on 2026-09-15; the verification summary is posted on the PR. The current head completes LAYOUT.md §2's position chain and the requested `leftover` behavior. A child whose length is only `leftover` is not drawn when nothing is left. A child that also asks for pixels or a relative fraction keeps that part and overflows as before. `29c7881` replaces the parallel resize rules with one retained-layout contract, `Holds`: the interval of box lengths for which a widget's drawing and reported size stay valid. Reading `Painter::px_len` or `px_size` narrows the interval to the length read; `Painter::holds` lets the widget widen it. Parent validity is the intersection of the ranges its children induce. This contract is trusted. A widget that declares an incorrect range is a defective widget; Iris does not add defensive work to recover optimizations from a false declaration. `f437495` restores explicit `Span::ortho(OrthoSize::{Children, Full})` sizing. `Children` remains the default and preserves the old conservative behavior: the largest fixed orthogonal length is reported, while any relative or `leftover` child makes the span report `leftover`. `Full` reports exactly `Len::rel(1.0)`. It does not read the children's orthogonal sizes, but their `Holds` ranges still propagate through final-box drawing, so a resize repositions them without redrawing when their own contracts permit it. `71c9c39` replaces the public `Painter::place` distinction with an opt-in widget property. `.region_node()` gives a widget one independently movable retained region; `Widgets::set_region_node` can change that choice at runtime and causes one structural redraw. Widgets without the property remain at the default shallow chain depth: Iris recursively remaps their retained primitive and mask regions when they move. `.scrollable()` enables a region node on its content once as its convenient default; raw `Scroll::new` respects the caller's choice, and the property can be disabled later without breaking scrolling. `Span` and `Align` do not add nodes to their children. Do not change `Children` to select the pixel-longest arbitrary `Len` at the span's current width. A fixed child and a relative child can create multiple self-sizing fixed points; generated seed 13 settled differently warm and cold under that attempted implementation. A `Holds` interval says where an already chosen answer stays valid, but cannot make that circular choice unique. The implementation also fixes three counterexamples found while finishing the rewrite: - An asked-but-undrawn size dependency must name the widget that asked as its parent. Using the asker's parent skipped a reader and made generated seed 10 settle differently warm and cold. - `Scroll` must return the answer from the first box it asked about, whether that answer came from a retained length or a fresh measurement. Returning the final placed answer only on the retained path advanced one fixed-point iteration and broke seed 86. - A widget retains the layer it was entered on, not the last child layer its painter visited. The old value drifted on local redraw and put a redrawn tab background above its retained text. - `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. `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` - `cargo fmt --all --check` - `cargo clippy --workspace --all-targets --all-features -- -D warnings` - `cargo test --workspace --all-features`: 84 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 differing pixels out of 1,080,000 after the region-node rewrite. - At preceding head `29c7881`, reference renders against `/home/bob/repos/iris-main-cmp` at `ca2b4b2` covered: `tabs`, `view`, `minimal`, and `text` at 1920x1200; `tabs` cold at 900x1200; live resize from 1920x1200 to 900x1200; and the tab interaction before and after replay. Every comparison had zero differing pixels. The live-resize image is also byte-identical to the cold 900x1200 image. The final pre-submit review was run in four passes. The region-node review caught an index-reuse hazard when removing a node; its move entry now remains alive until every descendant has migrated. The generated oracle then exposed the exact `Span` threshold described above. The earlier retained-layout review caught the layer defect above, corrected validity-range inversion for negative relative extents, and removed an impossible-state `unwrap` from `Scroll`. The orthogonal-sizing review caught both the circular longest-child choice and the incompatible visual effect of making `Full` the default. ### Performance The retained rewrite was compared with #18's previous head `691e3eb` using the same release `layout_diagnostics` fixture, seed 1, depth 8, 500 frames, 130 dirty widgets: | phase | `691e3eb` | `29c7881` | | --- | ---: | ---: | | many | 25.17M instructions/frame | 6.14M | | resize | 16.08M | 8.39M | | scroll | 0.720M | 0.691M | | repaint | 0.720M | 0.689M | | size | 0.730M | 0.698M | The final `29c7881` measurements were 3,070,693,965 instructions for 500 `many` frames and 4,192,804,214 for 500 `resize` frames. Re-run before quoting tighter figures. ## Retained-layout invariants - A region node holds a whole `UiRegion` in its parent node's coordinates. `UiRegion::FULL` is the identity. Widgets opt in with `.region_node()` or `Widgets::set_region_node`; ordinary widgets share the nearest ancestor node. Region nodes therefore add chain depth only where moving a whole subtree through one entry is useful. - A widget's `ActiveData::region` is its box in its parent node. A region-node widget draws in `FULL`; its box lives in its node. Moving an ordinary retained subtree instead remaps its primitive, mask, and active regions. Remapping stops at a descendant region node after rewriting that one entry. - Changing `region_node` redraws the subtree once to rebuild the coordinate boundary. The property belongs to widget identity, which is safe because a widget has one parent. `.scrollable()` sets it once; raw `Scroll::new` does not, and `Scroll` never reasserts it while drawing. - The first box a parent asks about is the offer. A later box chosen from the child's answer is the final box, not another independent answer. Dirty widgets are re-asked at the offer and only then drawn in the final box. - A retained drawing can be reused only when its `Holds` interval contains the new pixel box on both axes, its parent node is unchanged, its region-node choice matches the retained structure, and the widget is clean. A valid ordinary subtree may move without redrawing because its regions are recursively remapped. - `Painter` records size-dependency edges only when a parent reads a child's size or hint. An undrawn measured child remains recorded so a later change reaches the parent that decided whether to draw it. - Dirty widgets settle deepest-first. `dirty_size_under` prevents a reader from taking a retained answer while something below that answer is still dirty; the walk is an optimization against laying out twice, not a second validity mechanism. - Declared non-`leftover` lengths are resolved by the widget's parent where the widget is drawn. A declared-length change therefore redraws the parent. - Pixel comparison uses a 0.05 physical-pixel tolerance, against the last actual layout. Repeated subpixel changes accumulate and eventually redraw. - 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. - `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. ## Rigs and reproduction Ordinary framework verification: ```sh cd /home/bob/repos/iris-pr18 cargo fmt --all --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ``` Run the long generated oracle only after ordinary tests pass: ```sh cargo test --release --test generated -- --ignored a_long_run_of_seeds_agrees ``` `tests/generated.rs` compares a warm incremental tree with a cold tree of the same state. `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS`, and `IRIS_GENERATED_DEPTH` select failures. `tests/shrink.rs` reduces a failing tree; use it to turn a seed into a readable regression rather than leaving the seed as the only record. The headless reference set must be run one process at a time because the rig reuses one compositor. Comparison worktrees need separate target directories. Useful commands: ```sh ./scripts/run-headless.sh tabs --mode 1920x1200@60Hz --shot /tmp/tabs.png ./scripts/run-headless.sh tabs --mode 900x1200@60Hz --shot /tmp/cold.png ./scripts/run-headless.sh tabs --mode 1920x1200@60Hz \ --resize 900x1200@60Hz --shot /tmp/resized.png ./scripts/run-headless.sh tabs --mode 1920x1200@60Hz \ --replay /tmp/tabs.touch --shot /tmp/replay.png ``` The replay used for the final check was: ```text 0 down 1728 24 80 up 1728 24 400 down 1836 1116 480 up 1836 1116 800 down 1836 1116 880 up 1836 1116 ``` `tests/layout_diagnostics.rs` is the retained CPU rig. Select `cold`, `many`, `repaint`, `size`, `scroll`, or `resize` with `IRIS_PHASE`; use the feature for explanatory counters and an uninstrumented release binary under `perf` for 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. 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. Other queued work, in dependency order: - `UiRenderState` behind `Rc>`. - Split `Len`/layout length and add density-independent pixels. - Input restructuring: pointer capture, drag slop and axis, cancellation, mask-aware hit testing, and timestamps. - Retained paints, selection, overlays, and shared runtime state. - Generic desktop/Android hosts and reusable example/APK tooling. - Application-owned fonts and replaceable glyph-atlas buckets. - Positioned text overflow and cluster-safe ellipsis. The archive is a reference, not a patch: it predates returned `Size`, the current box chain, and the current length types. Recreate changes on current types and keep app/session concepts out of Iris.