`.width()` built a `SetSize` whose whole job was to answer `size_hint`, so
every declared length cost a widget, an `ActiveData` and a link of chain to
say one number. It is now a `SizeRule` per axis on `WidgetData`, beside
`region_node`, resolved by `Painter` where the widget is drawn. `SetSize`
and `MaxSize` are gone; `MaxSize` had no caller but its own builders.
That settles which of two answers is the size. A rule wins on the axis it
names and the `Size` returned by `draw` answers the rest, applied once in
`draw_inner` rather than by each widget that could carry one -- so the
widget under a rule never learns of it. `Painter::size_hint` reads the rule
first for the same reason: a rule that beats what a widget would draw has
to beat what it says about itself.
`declared_lens` still falls back to a non-leftover `size_hint`, which is
how an image or a gap gets its own pixel size rather than the whole offer.
That is the offer's business rather than a declaration's, and it falls away
when a widget occupies its reported size inside the box it was offered.
`known` and `declared` are separate because a share is a length to whoever
divides one and not to whoever composes a box: `.width(leftover(3))` is
known without drawing but cannot narrow anything.
Checked: fmt, clippy, 85 tests, and 100 generated seeds agreeing warm
against cold in 67.6 s. `minimal`, `text` and `view` render byte-identical
at 1920x1200; `tabs` differs only in the widget count it prints about
itself, which is two wrapper types smaller.
The length kind that asks for a part of what is left once the fixed
lengths are taken is called leftover: Len::leftover(2), Len::LEFTOVER,
Size::LEFTOVER, Len::leftover the field, and apply_leftover. It says
what it is where "rest" reads as "the remainder of the list" as often as
"the remaining space", and every agent who has touched this has reached
for a third word for it.
Locals called rest that meant a region or a widget are renamed with it,
since the word now names something else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SetSize took its declared length out of UiRegion::FULL, which is the box
it was already given, so a span that had sized that box from the same
hint had the fraction taken twice: .width(rel(0.5)) in a 400-wide span
drew its child 100 wide. It held under a Pad or the root, which do not
honour a hint, and hid under px, where 200 of a 200-wide box is all of
it. The text example was 111,923 pixels from upstream/main because of
it.
Whoever draws a widget now takes its declared length, in its own box,
which is what a fraction of one means, and is the identity for a caller
that already reserved the space. rest is not taken: a share of what is
left over is only a length to the widget dividing one, so it passes up
in the size as it does out of a span. SetSize keeps only what it
declares.
A declared length is then part of the box its parent decided, so
changing one has to redraw the parent; the lengths resolved into a box
are kept beside it and compared. Assuming instead that any dirty widget
which declares a length needs its parent costs 17% of a frame that
dirties 130 of 260 widgets, and buys nothing.
All five reference renders, the resize render and the image replay are
byte-identical to upstream/main, the 100-seed sweep passes, and the
resize fixture is 1.286 ms against 1.289 before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A local redraw drew a dirty widget in the box it was placed in. When a reader
decided that box from the widget's own answer -- an aligned span sized to its
children, a text at the tail of a row, a scroll's content -- the old answer is a
fixed point of measuring there whatever the content now says, so the layout had
two stable answers and which one it reached depended on the tree's history.
`tests/unsettled.rs` has the two shrunk cases: the four-widget aligned span,
and a scroll placing a pass-through `SetSize` in a box the content decided,
where the span under it was placed once and nothing at its own edge said so.
`ActiveData::offered_px` keeps the pixel size of the box the parent first asked
about the child in, whether through `known_len` or a first `place`, beside `px`,
the box it drew against. A dirty widget whose size reads an axis on which some
reader up its chain gave what it read a box other than the one it asked in is
not drawn locally: the chain is marked and the parent of the highest such
placement draws, since above it every box is a constraint rather than an
answer. The walk goes up the whole reader chain because a pass-through hands a
derived box down unchanged.
`Scroll` read its box's length for the clamp through `px_len`, which records
the reported size as depending on it, and it does not: its size is its
content's. That made every scroll tick a size question asked in a derived box,
at 34x the instructions. `Painter::px_len_for_draw` is the read that records
nothing. Instructions per frame on the depth-8 rig against the previous head:
`many` at 32 dirty 0.66M to 0.74M, at 130 dirty 27.7M to 26.5M, `resize` 15.8M
to 15.0M, `scroll`, `repaint` and `size` unchanged. The shrinking fuzzer passes
200 trees at depth 7 in all four cases, the hundred-seed sweep passes, and the
five reference renders and the resize render are byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`SetSize` drew its child in whatever box it had been given and then
reported its declared length, so the child answered about a box it was
never going to have -- and the answer on the *other* axis was taken under
that. A wrapping text under `SetSize(x: 76px)` was measured in the whole
640 available, reported one line, and the parent sized itself to one line.
The text was then drawn again at 76 and reported two, but by then its box
was settled and nothing revisited it. A repaint put it right, which is why
the first frame and the second disagreed.
So the layout was not a function of the state, and "cold" was not a fixed
point -- which means the warm-against-cold oracle has been measuring
against a tree that had not settled, and some of what it reported as a
retained-layout defect was the cold side being wrong. Nothing about
retained state is involved in this: it reproduces in six widgets on a
first frame.
The declared length is what the child gets, so that is where it is
measured. `apply_rest` carries `rel` and `rest` through unchanged, and a
`px` length composes as an offset, so the child's box does not move again
when this widget's own box shrinks to what it declared.
`tests/unsettled.rs` passes, and the generated sweep now passes at depth 5
where it failed. Depth 6 and 7 still fail; there is more than one of these.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A span reporting `Len::default()` whenever a child had a share threw away
how many shares it was holding, so each level of nesting re-divided a
share rather than dividing the same space. One span of a rect beside a
span of three gave 1/2 and 1/6 each, where the same four rects directly
in one span get a quarter.
A span that sizes from its children does not resolve `rest`, it passes
the weight up; resolution belongs at the nearest ancestor with a length,
and since the output became a box there is always one. The placement loop
already divides by `len.rest / total.rest`, so it consumes carried
weights unchanged -- only what the span reported was wrong.
The uneven nesting is the case that fails without this; the even one
passes either way and is here as the statement of intent.
Decided by the owner, 2026-09-14.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`dp` is coming, and then `abs` says which of the two it is not. The
component has always been a pixel count, so the name that admits it is
the one that leaves room for a second unit beside it.
Mechanical: the field on `Len` and `UiScalar`, their constructors,
`to_abs`/`get_abs`, the matching WGSL struct member and the locals
composing it. Field order and types are unchanged, so the `Pod` layout
the shader reads is the same bytes. `f32::abs` is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A widget's region is now held in the coordinates of the slot it draws in
rather than the window's, and `Painter::place` is how a container asks for a
slot: it draws a child it decides the box of and may decide again. Everything
under that slot is a fraction of its box, so placing the child a second time
is one entry to write whether it moved or changed length. A child drawn any
other way has no slot and shares its nearest ancestor's.
That is what keeps the chain short. `chain_cost` measured depth as the cost
-- free to 8, +42.6% at 16 -- and a slot per widget put a transcript's glyphs
past that for nothing, since almost every slot was zero. `Span`, `Aligned`
and `Scroll` are the containers that re-place a child after drawing it, and
`tests/layout.rs` pins that four widgets between a span and a leaf leave the
leaf's chain one deep.
`UiRegion::stretch`, `UiRegion::stretchable` and `UiScalar::stretch` are
gone. Nothing is inverted any more: a box that changed length is written to
its slot, and the descendants recompose against it in the shader. That also
retires the case the guard existed for, where a fixed length has no fraction
to recover -- `tests/layout.rs` now stretches a 40-tall row on its other
axis, which `stretchable` refused outright.
What still walks the CPU is deciding who must draw again, which no chain can
answer: `mark_resized` descends from the widget whose box changed and marks
anything whose own box changed length and whose drawing reads it. A part of
a box with no relative extent on an axis is a fixed length, and composing
into it leaves none either, so the walk stops where a length did not change
-- an 80-wide child in a widened row is not redrawn though it says `Redraw`.
`Span`, `Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say
`Scale`: each places in fractions and offsets of its own box and none reads
the box's pixel length. `Scroll` and `MaxSize` do read pixels and stay
`Redraw`.
45 tests pass, five of them new. Render verification comes after the CPU
side, per the owner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Sized` shadowed the marker trait, so a `?Sized` bound in any crate that imports the prelude failed to resolve -- a compile error in someone else's code that nothing here would have caught. It was already biting inside iris: `default/mod.rs`, `widget/ptr.rs` and `widget/text/build.rs` all imported `std::marker::Sized` explicitly to get out from under it, which they no longer need.
`SetSize` rather than `FixedSize` because the size it sets need not be fixed -- `width(rest(2))` (a flex weight) and `width(rel(0.5))` (half the parent) build the same widget, and both are more common than `sized((100, 100))`. It also pairs with the `MaxSize` beside it in that module: one sets a length, the other caps it. The builders are unchanged.
`tests/prelude_bounds.rs` is a compile-level guard -- it fails to build if the prelude shadows `Sized` again, which I checked by reverting `src/` under it:
```
error[E0404]: expected trait, found struct `Sized`
--> tests/prelude_bounds.rs:8:22
|
8 | fn takes_unsized<T: ?Sized>(_: &T) {}
| ^^^^^ not a trait
```
The pad tab of the tabs example -- the one built out of `sized` and the flexible widths -- renders pixel-identical to before the rename.
---------
Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#14
Co-authored-by: AIris <4+iris-ai@noreply.localhost>