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.
This commit is contained in:
iris-ai committed 2026-09-15 19:27:30 -04:00
1 parent 8db0969d9d
commit cf7b33da39
1 file changed
+57 -11
+57 -11
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 **`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: