Carry a length as a rule beside a widget, not a widget around it

`.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.
This commit is contained in:
iris-ai committed 2026-09-15 19:44:21 -04:00
1 parent 0283c9d6c7
commit 8220a78d4a
21 files changed
+252 -214

No files matched your search

+13 -26
View File
@@ -81,7 +81,7 @@ fn a_child_drawn_twice_moves_once() {
h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (100, 0), (300, 200));
h.rsc[left].x = Some(Len::px(150));
h.set_len(left, Axis::X, 150);
h.frame();
assert_corners!(h, inner, (150, 0), (350, 200));
@@ -131,7 +131,7 @@ fn a_fixed_box_is_drawn_again_rather_than_stretched() {
h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100));
h.rsc[leaf].y = Some(Len::px(250));
h.set_len(leaf, Axis::Y, 250);
h.frame();
assert_corners!(h, panel, (0, 0), (400, 250));
@@ -146,7 +146,7 @@ fn a_moved_subtree_takes_its_children_with_it() {
h.set_root((first, row).span(Dir::DOWN));
assert_corners!(h, inner, (10, 50), (390, 70));
h.rsc[first].y = Some(Len::px(80));
h.set_len(first, Axis::Y, 80);
h.frame();
// The row opted into one movable region, so its descendants follow one
@@ -167,7 +167,7 @@ fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
assert_corners!(h, fixed, (100, 0), (150, 200));
assert_corners!(h, leftover, (150, 0), (400, 200));
h.rsc[bar].x = Some(Len::px(200));
h.set_len(bar, Axis::X, 200);
h.frame();
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
@@ -195,7 +195,7 @@ fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
h.set_root((bar, column).span(Dir::RIGHT));
assert_corners!(h, inner, (110, 10), (390, 30));
h.rsc[bar].x = Some(Len::px(200));
h.set_len(bar, Axis::X, 200);
h.frame();
assert_corners!(h, inner, (210, 10), (390, 30));
@@ -282,24 +282,14 @@ fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
}
fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
let inner = rect(Color::RED).add_strong(&mut h.rsc);
let mark = SetSize {
inner,
x: Some(Len::px(1.0)),
y: None,
}
.add_strong(&mut h.rsc);
let mark = rect(Color::RED).width(1).add_strong(&mut h.rsc);
marks.push(mark.id());
mark
}
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
SetSize {
inner,
x: Some(Len::leftover(ratio)),
y: None,
}
.add_strong(&mut h.rsc)
h.set_len(&inner, Axis::X, Len::leftover(ratio));
inner
}
/// Shares in weights no binary fraction lands on, a padding on one branch
@@ -402,18 +392,15 @@ fn only_a_pure_leftover_child_disappears_when_nothing_is_left() {
// An undrawn child remains a dependency of the span, so making room for
// it draws it without rebuilding the tree.
h.rsc[fixed].x = Some(Len::px(60));
h.set_len(fixed, Axis::X, 60);
h.frame();
assert_corners!(h, leftover, (60, 0), (100, 20));
let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let mixed = SetSize {
inner: rect(Color::BLUE).add_strong(&mut h.rsc),
x: Some(Len::px(20) + Len::LEFTOVER),
y: None,
}
.add(&mut h.rsc);
let mixed = rect(Color::BLUE)
.width(Len::px(20) + Len::LEFTOVER)
.add(&mut h.rsc);
h.set_root((fixed, mixed).span(Dir::RIGHT));
// Pixels and fractions still overflow; only a child whose entire length
@@ -432,7 +419,7 @@ fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
assert!(h.region(&a).is_some());
assert!(h.region(&b).is_some());
h.rsc[first].y = Some(Len::px(96.0));
h.set_len(first, Axis::Y, 96.0);
h.frame();
assert!(h.region(&a).is_none());