Say what a child gets of a container's box in that box's own lengths

Three things the measurements asked for, all about how much a box that came
from an answer costs.

**A part in the box's own coordinates.** Saying "less eleven pixels at the
end" in frame lengths from the box's start means reading how long the box is,
and a container whose box is its own answer then depends on its own answer:
`Pad` drew sixty-four times in one resize frame at seed 13, chasing its own
width. `Part::Of` says the same thing as a part of the box, which composes
without a length -- pixels are pixels wherever the box lands -- and what a
child under it holds for maps back through that part onto the container's
own box rather than onto the frame.

**One axis of the box at a time.** `extent_len` pinned both axes, so a span
dividing one of them held for one length of the other as well, and a resize
broke every span whose cross-axis answer moved.

**No lazy placement.** Leaving a child's answer to be placed at the end of
the parent's draw, rather than as the child answers, was meant to save a
recomposition. It costs one instead: the drawing is put in the part first
and in the answer's box after, and where it does not hold for both that is
two drawings rather than one. Seed 1 at depth 8 went from 391 widget draws
on a resize to 29 with it gone. The test that pinned three draws for a
numeric leaf in a span goes with it.

Seed 1 at depth 8, widget draws / distinct widgets / update, against #18's
head and against the commit this branch started from:

| phase   | e44dea3      | 34cafb6      | here          |
| ---     | ---          | ---          | ---           |
| cold    | 369/261/10.6 | 463/274/13.3 | 516/288/12.0  |
| repaint | 1            | 1            | 1             |
| many    | 157/95/0.33  | 263/108/0.59 | 187/119/0.52  |
| size    | 16/12/0.018  | 3/3          | 3/3/0.010     |
| scroll  | 2/0.002      | 1            | 1/0.004       |
| resize  | 13/13/0.019  | 22/15/0.032  | 24/76/0.090   |

Seed 13 at depth 8 is where the protocol still costs: `many` 1091 draws
against #18's 524, and `resize` 2215 against a frame #18 does not draw at
all. Both are the same shape -- an answer measured in one box and drawn in
another -- and the handoff says where that comes from.

Checked: fmt, clippy with -D warnings, 108 suite tests, 20 core tests, the
11 generated cases, and the shrinker at 400 trees of depth 5, which fails
seeds 2 (repaint) and 108 (reorder).
This commit is contained in:
iris-ai committed 2026-09-18 01:06:05 -04:00
1 parent 1956be3f3d
commit 0954770ceb
11 files changed
+134 -222

No files matched your search

+4 -5
View File
@@ -117,16 +117,15 @@ pub struct Branch {
impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.extent_len();
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Some(UiSpan::new(Len::ZERO, cut)));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter
.widget_at(&self.probe, UiRegion::FULL, [Place::Within(None), top])
.widget_at(&self.probe, UiRegion::FULL, [Place::Within(Part::All), top])
.len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let below = Place::Within(Some(UiSpan::new(cut, len.y)));
let place = [Place::Within(None), below];
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
let place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) {
true => painter.widget_at(&self.wide, UiRegion::FULL, place),
false => painter.widget_at(&self.narrow, UiRegion::FULL, place),
+5 -3
View File
@@ -10,9 +10,11 @@ impl Widget for Offset {
// The whole of this widget's box, moved: the frame passes through, so
// what the child declares or reports means the same as it would
// without the offset.
let len = painter.extent_len();
let moved = |len: Len, amt: Len| Place::Within(Some(UiSpan::new(amt, len + amt)));
let place = [moved(len.x, self.amt.x), moved(len.y, self.amt.y)];
let moved = |len: Len, amt: Len| Place::Within(Part::From(UiSpan::new(amt, len + amt)));
let place = [
moved(painter.extent_len(Axis::X), self.amt.x),
moved(painter.extent_len(Axis::Y), self.amt.y),
];
painter.widget_at(&self.inner, UiRegion::FULL, place).size()
}
}
+10 -6
View File
@@ -17,16 +17,20 @@ impl Widget for Pad {
// The padding goes around what it pads: the frame passes through, so
// the inner's fractions mean what they would without it, and only
// the box it draws in is moved in by the pixels.
let len = painter.extent_len();
let inset = |len: Len, lead: Px, trail: Px| {
Place::Within(Some(UiSpan::new(
// A part of this widget's own box, in that box's own lengths: the
// padding is pixels, which are the same pixels wherever the box
// lands, so nothing here reads how long the box is -- and a box
// chosen from this widget's own answer therefore does not feed back
// into that answer.
let inset = |lead: Px, trail: Px| {
Place::Within(Part::Of(UiSpan::new(
Len::from_parts(Rel::ZERO, lead),
len - Len::from_parts(Rel::ZERO, trail),
Len::from_parts(Rel::ONE, -trail),
)))
};
let place = [
inset(len.x, self.padding.left, self.padding.right),
inset(len.y, self.padding.top, self.padding.bottom),
inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom),
];
let inner = painter.widget_at(&self.inner, UiRegion::FULL, place).size();
Size {
+2 -2
View File
@@ -15,7 +15,7 @@ impl Widget for Scroll {
// Measured in the whole viewport, then drawn at the scrolled offset.
let whole = UiRegion::FULL;
let answer_len = painter
.widget_at(&self.inner, whole, [Place::Fill(None); 2])
.widget_at(&self.inner, whole, [Place::Fill(Part::All); 2])
.len(self.axis);
let content = answer_len.apply_leftover();
self.container_len = container_len;
@@ -65,7 +65,7 @@ impl Widget for Scroll {
&self.inner,
whole,
self.axis
.pair(Place::Fill(Some(content)), Place::Fill(None)),
.pair(Place::Fill(Part::From(content)), Place::Fill(Part::All)),
);
// What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for
+4 -4
View File
@@ -13,7 +13,7 @@ impl Widget for Span {
// The row: this span's own box, as a length of the frame its children
// are laid out against. Its start is nothing's business -- a slot is
// a length from it -- so what this reads is the length alone.
let far = painter.extent_len().axis(axis);
let far = painter.extent_len(axis);
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from),
@@ -21,7 +21,7 @@ impl Widget for Span {
// Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and
// nothing divides that axis.
let across = Place::Within(None);
let across = Place::Within(Part::All);
// A length for every child before their final slots are chosen. The
// frame passes through unchanged, so `rel(0.5)` is half the area this
// span was given whatever else is in it and wherever this child sits
@@ -30,7 +30,7 @@ impl Widget for Span {
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children {
let room = Place::Within(Some(along(cursor, far)));
let room = Place::Within(Part::From(along(cursor, far)));
let len = painter
.widget_at(child, UiRegion::FULL, axis.pair(room, across))
.len(axis);
@@ -108,7 +108,7 @@ impl Widget for Span {
// Along the row the span says where the child goes, and that slot
// is the drawing's box outright rather than something to place an
// answer inside again.
let slot = Place::Fill(Some(along(from, start)));
let slot = Place::Fill(Part::From(along(from, start)));
let placed = painter.widget_at(child, UiRegion::FULL, axis.pair(slot, across));
if shrinks {
let used = placed.len(!axis);
+1 -1
View File
@@ -23,7 +23,7 @@ impl Widget for Stack {
Some((i, child)) => {
painter.child_layer_at(i);
painter
.widget_at(child, UiRegion::FULL, [Place::Fill(None); 2])
.widget_at(child, UiRegion::FULL, [Place::Fill(Part::All); 2])
.size()
}
None => Size::LEFTOVER,