Files
ai-app/docs/HANDOFF.md
T
iris-aiandClaude Opus 5 f83be016ba Update the layout measurements to the current head
Three more roundings and divisions taken out after measuring each, one
optimisation tried and reverted with its number kept so it is not tried
again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 04:14:18 -04:00

524 lines
28 KiB
Markdown

# 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 **`4f5e27c`**, sixty-three 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.
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. `5ed9e87` moved that boundary rather
than softening it, and `39e4ca2` removed the move: on the grid the box and
the sum are the same count. See "Fixed point" 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 `4f5e27c`
- `cargo fmt --all --check`
- `cargo build --workspace --all-features`
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
- `cargo test --workspace --all-features`: 105 passed, 10 ignored
- The release generated cold-layout oracle passed 100 seeds in 9.4 s, and a
shrinker case at 300 seeds in 3.5 s: both run a thread per core but one.
- The release shrinker passed **all five** cases -- `resize`, `repaint`,
`resize-repaint`, `reorder`, `size-change` -- at 300 seeds of depth 5:
26,001 widgets per case, largest tree 331. It passed 1000 seeds of depth 6
(159,024 widgets per case, largest tree 587) at `5ed9e87`, before fixed
point; re-run that before quoting it again.
- `tabs`, `view`, `minimal`, `text` and `random` render byte-identical at
1920x1200 across the whole fixed-point sequence.
- `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
`1d397c57b9914a2e596fa907029bc6aab4629e4d74deb0715e81325c703bcdb3`.
The run used the Venus adapter backed by the host RX 7900 XT.
- `minimal`, `text` and `view` render byte-identical at 1920x1200 across
`8220a78`. `tabs` differs only in the widget count it prints about itself,
which is two wrapper types smaller -- so it is no longer a byte-identical
reference and the generated oracle is the check that matters.
- 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 built-in-alignment pre-submit review was run in four passes. It caught
three distinctions the smaller tests had missed: an answer's validity must
include the drawing made in its final placed box; a region-node box is resolved
through its parent move rather than through its own move twice; and boxes with
equal dimensions but different positions still need the parent that placed
them. The 100-seed oracle and both 300-seed shrink cases pass after those
fixes. The earlier 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
Measured 2026-09-16 on the release `layout_diagnostics` fixture, seed 1,
depth 8, 500 frames, as median milliseconds a frame. `5ed9e87` is the commit
before fixed point; `39e4ca2` is fixed point complete.
| phase | `5ed9e87` | `39e4ca2` | `4f5e27c` |
| --- | ---: | ---: | ---: |
| many | 0.179 ms | 0.544 | 0.274 |
| resize | 0.020 | 0.035 | 0.033 |
| scroll | 0.011 | 0.030 | 0.019 |
| repaint | 0.012 | 0.031 | 0.021 |
**Fixed point cost 3x, and two thirds of that was not the grid.** The
counters said eight more "placed by redrawing" a frame in `scroll`, all of
them "reuse: another layer": a retained drawing belongs to the layer it was
made on, and `Stack` measured the child that sizes it on its own layer before
drawing it again on the child layer. `97cc8b3` measures on the layer the
child ends up on, which puts the scroll phase's counters back exactly where
they were -- 4 widget draws, 12 draw requests.
What is left is per-operation cost, not more work: the `many` phase does 125
widget draws against 121 before, and takes 1.5x as long. The arithmetic is
the difference -- an `i64` multiply and a rounding branch where there was an
`f32` multiply, and an `i64` division in the remap. Three were taken out
after measuring (`11c55bc`, `4f5e27c`, and halving the divisions in
`Holds::through`); `RegionRemap::apply_span` is still a fifth of the phase,
and the rest is spread thin. Measure with `perf stat -e instructions:u`
rather than the clock, which varies 2x here.
One thing tried and reverted, recorded so it is not tried again: short-
circuiting `apply_scalar` where the fraction is nought or one. Those are not
the common cases, and the comparisons cost 17% more than the divisions they
saved.
Earlier, against #18's own history: the retained rewrite took `many` from
25.17M instructions a frame at `691e3eb` to 6.14M at `29c7881`, and `resize`
from 16.08M to 8.39M. That fixture has since changed; do not compare across
it.
## Retained-layout invariants
- **A retained drawing belongs to the layer it was made on.** Asked for again
on another layer it is redrawn, since nothing about its geometry says it is
in a list that paints at a different moment. A container that measures a
child by drawing it therefore measures on the layer that child will draw on
-- `Painter::child_layer_at` addresses one -- or it pays two draws a frame
forever and keeps whichever the second ask left.
- A widget that clips its contents to its box reports its box: `Scroll` and
`Masked` both report `LEFTOVER`, and a `debug_assert` in `draw_at` holds
any widget that set a mask to it. Overflowing is otherwise ordinary and a
text too tall for its box says so.
- A span is as long across itself as its longest child, unless a rule beside
it says how long it is -- and then it does not read its children there at
all, since the answer is not wanted and reading one is what makes its size
depend on it. `OrthoSize` was that second case written twice and is gone
(`9d8415d`); `Painter::ruled` is how a container asks which it is in, and
the only thing a widget may learn about a rule over it.
- 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. An
offer composes through its ancestors' offers, not through their current
placed boxes.
- An answer is reusable only where both its measurement and the drawing made
in its final placed box remain valid. The final drawing's `Holds` interval
is translated back into lengths of the offered box and intersected with the
answer's interval.
- Equal box lengths do not imply equal placement. An ordinary widget whose
offered and current boxes differ in position must involve its parent again;
a region node can settle itself only when its own alignment, rather than a
container override, determines 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.
- A pixel comparison is equality: lengths are whole counts of `1/1024` px,
so a change too small to reach the next step is not a change and one that
reaches it is, however little of a pixel it is worth.
- 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. The room to
divide is `len * fixed - total.px`; pure `leftover` children are undrawn
where there is none. The box a parent hands back and the sum of what the
children asked for are counts of the same step, so the boundary needs no
margin and the validity interval is split exactly at it.
- `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 that keeps a box's length is a translation, and an offset is
exact on the grid.** A box that also changed length has to re-express each
part as a fraction of the new one, and that division and multiplication
round: `39e4ca2` translates where `from.len() == to.len()` and scales only
where it must, which is what made the shrinker's `resize` case agree
exactly. This inverts the float-era rule, and the measurements behind that
rule are why `tests/drift.rs` exists: in floats, offsetting both ends of a
span shortened that fixture's row by 0.071 px over 20,000 moves and 0.712
over 200,000, while re-expressing fractions stayed exact. 20,000 moves is
five minutes of scrolling at 60Hz. On the grid the drift is gone either
way, and `tests/drift.rs` pins that it stays gone.
## Built-in alignment
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
fuzzer covers size rules, alignment and region nodes and changes all three at
runtime. Region nodes had **no generated coverage at all** before that.
### Alignment is two fractions, not four directions
Decided 2026-09-15 (Bryan). A widget's alignment is one `f32` per axis, so a
quarter of the way along an axis is expressible. `AxisAlign`'s three familiar
positions are named constants over that number, which is what every expression
already uses: the layout math only ever reads `AxisAlign::rel()`, so nothing
downstream changes shape.
The default is **the middle on both axes**, because the two edges are the ones
that assume a direction -- which edge is the near one depends on the writing
system and on which way a container runs. "Near edge" throughout this document
means the *start of the box in the box's own orientation*, which for a
reversed span is its visually far end, not "top left".
### Placement cannot be applied after the fact
**Do not re-attempt "draw the widget, then move its drawing to where its
alignment says".** Three attempts failed, and the reason is structural: the
move is a change of coordinate frame, and no consistent split of the stored
state carries it.
- Move `ActiveData::region` with the drawing, and a later local redraw asks a
differently rounded question. Measured: `(0,0)..(0,305.936)` re-expressed as
`(0.5,-81.5)..(0.5,224.436)` reads its length back as `305.93604`, which
crosses `Span`'s leftover/no-leftover boundary -- the one that must be exact
-- and draws a child a cold layout leaves undrawn.
- Leave `region` alone, and `placed` accumulates without bound, because
`try_reuse` returns a clean subtree's size **without walking into it**: only
the top widget's `placed` is recomputed while an ancestor's shift carries the
whole subtree. Measured 7,048,813 where a cold layout says 456.
The replacement applies alignment in the two places that
already exist and are exact. Where the size is known before drawing, from a
rule, `declared_box` hands the child its aligned box directly -- one draw, no
move. Where the size is only known after drawing, the widget is **re-asked in
its placed box**, with its alignment forced to the near edge on the second ask
so it terminates; that ask goes through `try_reuse`, which moves by
recomposing, which `tests/drift.rs` pins as exact. That deletes
`ActiveData::placed` and `shift_subtree`. The cost is a second ask for a
measured widget that is not near-aligned, which is exactly what `Aligned` cost
before this work.
### Placement compounds, which is what the align override is for
A container that reports a child's size while handing that child a **bigger
box** gets its content placed twice: once by the child, once by the box around
it. Found three times before the class was fixed rather than the instances --
`Stack::size(Child(i))`, `Scroll`'s orthogonal axis, and `Pad`.
`Painter::widget_aligned(child, region, align)` is the override: an
`Option<RegionAlign>` carried in `DrawInfo` and resolved once in `draw_inner`,
so the root resolves like anything else. `Pad`, `Stack` and `Scroll` pass the
near edge for children whose size they report. `ActiveData` keeps both the
resolved alignment, so a local redraw asks the question its parent asked, and
the widget's own, which is what a change is compared against -- an override
means the answer is the parent's to give again.
### A widget occupies its box, and must not report more than it draws
`Scroll` reports `Size::LEFTOVER` on **both** axes: it clips its content to its
box, so it can neither take less of one nor honestly ask for more. The
content's length is what it scrolls through, not what it is. Reporting the
content length instead made the framework place a 400-long drawing in a
200-long box, and placement by recomposition **scales** in that case, because a
part stored at fraction 2 of its box stays at fraction 2 of a box twice as
long. Reporting the content's *cross* length had the box around it place
content already placed.
Where content shorter than the viewport sits is now `Scroll`'s own alignment.
Its `Holds` widening -- "content of a fixed length that fits sits at the start
of any box it fits in" -- is therefore gated on near alignment: anchored
anywhere else it is a part of the room left over, so it moves with every
length the box takes and the drawing holds for that length alone.
`Stack` gives every child the box its sizing child defines, through the new
`Painter::box_of`, for the same reason.
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.
## Fixed point
Layout decides on a grid rather than in floats, in four commits: `7548139`
the number, `4e28f10` positions, `bd6de71` lengths, `39e4ca2` the last of the
pixels and `Holds`. Decided with Bryan on 2026-09-15.
- **`Fixed<SHIFT>` is an `i32` counting `1 / 2^SHIFT`.** Adding and
subtracting are exact; a multiply or a conversion rounds once, back onto
the same steps. Two routes to one place that come within half a step land
on the same number, so everything downstream compares for equality.
- **`Px` is `1/1024` px, `Rel` is `1/2^24` of a box, `Weight` is `1/65536`
of a share.** `PX_SHIFT` and `REL_SHIFT` are the only statement of the
first two, and the shader's copy is prepended from them by
`render::module_source` rather than written again in WGSL.
- A weight is not a fraction: a list divides its room by the total of its
weights, so `Weight` trades precision for the range to hold a whole list,
and `Rel::ratio` turns two weights into a share on the finer grid.
- **Arithmetic saturates rather than wrapping**, because a clamped coordinate
keeps the ordering a wrapped one inverts.
- `Px` was `1/64` first. The residue of a length reached two ways is one
rounding, so it scales with the step: at `1/64` that was 0.016 px, enough
to move a box, and at `1/1024` it is a thousandth of a pixel. Range is
+/-2.1M px and conversion to `f32` is exact to 16,384 px.
- **What the fuzzers ask for is a step per level of nesting**, which is two
for these trees. Traced on 2026-09-16 to the same box reached two ways,
each rounding where the other does not -- not accumulation, and not one
place. Two of them are fixed in `bdab558`:
- `Scroll` wrote a box it had been given back out as its own length in
pixels. Centring a part in `rel 1` lands a step from centring it in
`px 900`, because `a(x - y)` and `ax - ay` do not round alike. Content
that fills the viewport unscrolled is handed back as it came, and the
`repaint` and `resize-repaint` cases became exact.
- `Span` placed each child a step from where the last ended, carrying every
share's rounding along the row. A position is now the fixed parts before
it, exact, plus one rounded share. Two hundred equal shares of a 1000 px
row ended at 999.999 and now end at 1000.
What is left is a box centred in a fraction of its parent against the same
box centred in its own pixels, one step per level between them. Closing it
means alignment resolved in pixels everywhere -- which costs the retained
resize path, since it is the fractional form that re-centres a subtree
without redrawing it. Not worth it at a thousandth of a pixel.
- `Holds::through` inverts `px + rel * box`, which rounds, so the answer is
an interval even for a single length. It maps the half step either side,
plus one more for the two ways above; inverting the length alone gives a
point that need not contain the box the part was drawn in.
- A pointer, a wheel notch, a shaped glyph advance and a window size arrive
as floats and are put on the grid where they arrive. `Vec2` stays what the
GPU and the platform speak; `PxVec2` is what layout decides in.
## 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:
```sh
cd /home/bob/repos/iris-pr18
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
```
`cb955f1` put the ordinary tests in `tests/cases/`, as modules of one
`tests/suite.rs` target -- eleven links became one, and with
`profile.test`'s `debug = "line-tables-only"` a rebuild of `iris`'s test
targets went from 14.3 s to 7.7 s and `target/` from 45 GB to 13 GB. Pick a
module out with `cargo test --test suite layout::`. The fuzzers and the
`*_cost` measurements are still their own targets.
The fuzzers take a thread per core but one (`9d8415d`), since a seed grows,
lays out and drops its tree alone: the oracle's hundred seeds went from 68 s
to 9.4 s and a shrinker case at 300 seeds from 18 s to 3.5 s. A failing seed
still shrinks and panics on its own thread.
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
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.
**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 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:
- `Painter::glyphs` converts four `f32`s to `Px` per glyph, every frame that
draws it. A `GlyphEntry` holding `Px` would convert once, when the glyph is
rasterised. It is the largest single thing left in the layout profile after
`InstanceList::push`.
- `Scroll` should take a direction rather than one axis: vertical, horizontal,
or both. Reporting `LEFTOVER` on both axes is already the right shape for it.
- Restore `max_width`/`max_height` as `SizeRule::{Min, Max, Clamp}`. `8220a78`
deleted `MaxSize` **and its builders**, so that is public API owed back. A
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: its `Holds` range must be exact and split at the crossover, the
way generated seed 16 taught for `Span`. On the grid the crossover no
longer needs moving off where boxes land -- see "Fixed point" -- but it
does need both sides of the comparison to be `Px`.
Other queued work, in dependency order:
- `UiRenderState` behind `Rc<RefCell<_>>`.
- 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.