Update region-node handoff

This commit is contained in:
iris-ai committed 2026-09-15 18:03:37 -04:00
1 parent 7b8b43a7d2
commit 8db0969d9d
1 file changed
+52 -25
+52 -25
View File
@@ -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 **`f437495`**, forty-six commits, pushed to the fork. No PR reviews
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.
@@ -30,8 +30,18 @@ add defensive work to recover optimizations from a false declaration.
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 placement, so a resize repositions them
without redrawing when their own contracts permit it.
`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
@@ -52,19 +62,23 @@ rewrite:
- 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 `f437495`
## Verification at `71c9c39`
- `cargo fmt --all --check`
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
- `cargo test --workspace`: 77 passed, 8 ignored
- `cargo test --workspace --all-features`: 84 passed, 10 ignored
- `cargo test --release --test generated -- --ignored
a_long_run_of_seeds_agrees`: 100 seeds passed in 66.81 s
- The `tabs` reference at 900x1200 remained byte-identical to `ca2b4b2` after
retaining `Children` as the default.
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
@@ -72,7 +86,10 @@ pin those rules. Seeds 10 and 86 are now in the ordinary generated set.
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 earlier retained-layout
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
@@ -98,19 +115,27 @@ tighter figures.
## Retained-layout invariants
- A slot holds a whole `UiRegion` in the coordinates of the slot it names.
`UiRegion::FULL` is the identity. Slots are opt-in: `Painter::place` gives a
child a slot; `widget` and `widget_within` share the nearest ancestor's.
- A widget's `ActiveData::region` is where it was placed in its parent's slot.
A placed widget draws in `FULL`; its box lives in its slot. This keeps chains
at positioning-container depth rather than full widget-tree depth.
- 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 a placement, not another independent answer. Dirty widgets
are re-asked at the offer and only then placed again.
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 slot is unchanged, and the widget is
clean. A moved unslotted widget must draw again because it has no slot to
rewrite.
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.
@@ -126,8 +151,10 @@ tighter figures.
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 at the length where fixed parts fill the box; pure
`leftover` children are undrawn on the no-space side.
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.
@@ -186,9 +213,9 @@ instruction totals.
## Next
Let PR #18 review. The next small LAYOUT.md §2 item is `LazySpan`;
`set_child_offset` is no longer separate, because placing a child is
`Painter::place`.
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