Compare commits

...
2 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 6c84b6f2cb Read a slot's ends where they are used, not carry one between children
`start` looked like a third accumulator beside `fixed` and `taken`, carried
across iterations and assigned at three points. It was never independent:
every assignment was `shared(fixed, taken)`, so it was those two read
together. Reading it at each end of a slot instead drops the variable, drops
two of the three calls per child, and leaves the gap added after the last
child deriving nothing -- which was the thing that read as a bug, and is not
one because no end is taken from it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:13:52 -04:00
iris-aiandClaude Opus 5 8d2b7a512b Sum a span's cursor by name, and guard a scroll's re-clamp
`cursor` added `px` and `rel` by hand where the placing loop below now says
`fixed += len.without_leftover()` -- the same sum, one of them named. And
`let along = total` shadowed the closure that makes a span along the row,
two meanings for one word in one function; the local said nothing `total`
did not.

A scroll's draw writes `amt` and `snap_end`, so a second draw at another
viewport reads what the first wrote. Warm still matches cold because
re-clamping is idempotent, but nothing said so and nothing checked it: the
seed scans build scrolls and never scroll them. The test scrolls four
distances, one past the end, and widens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:04:39 -04:00
2 changed files with 40 additions and 15 deletions

No files matched your search

+11 -15
View File
@@ -37,8 +37,8 @@ impl Widget for Span {
painter.widget_at(child, room).len(axis) painter.widget_at(child, room).len(axis)
} }
}; };
cursor.px += len.px + self.gap; cursor += len.without_leftover();
cursor.rel += len.rel; cursor.px += self.gap;
lens.push(len); lens.push(len);
} }
@@ -83,15 +83,14 @@ impl Widget for Span {
let shrinks = !painter.has_exact_size(!axis); let shrinks = !painter.has_exact_size(!axis);
// What the fixed parts and the gaps before here take, which is a sum // What the fixed parts and the gaps before here take, which is a sum
// of lengths and exact, and how much of the leftover weight is // of lengths and exact, and how much of the leftover weight is
// spoken for. A position is one from the other rather than a step // spoken for. Both ends of a slot are read from those two rather
// from the last child: the share of the room is rounded, and taking // than stepped from the last child: the share of the room is
// each from the one before it would carry every rounding along the // rounded, and taking each end from the one before it would carry
// row. // every rounding along the row.
let mut fixed = Len::rel_min(); let mut fixed = Len::rel_min();
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO; let mut ortho = LayoutLen::ZERO;
let shared = |fixed: Len, taken: Weight| match taken == Weight::ZERO { let reached = |fixed: Len, taken: Weight| match taken == Weight::ZERO {
true => fixed, true => fixed,
false => fixed + room.scale(Rel::ratio(taken, total.leftover)), false => fixed + room.scale(Rel::ratio(taken, total.leftover)),
}; };
@@ -102,15 +101,14 @@ impl Widget for Span {
if len.is_only_leftover() && !shares { if len.is_only_leftover() && !shares {
painter.undraw(child); painter.undraw(child);
fixed.px += self.gap; fixed.px += self.gap;
start = shared(fixed, taken);
continue; continue;
} }
let from = start; let from = reached(fixed, taken);
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
taken += len.leftover; taken += len.leftover;
} }
fixed += len.without_leftover(); fixed += len.without_leftover();
start = shared(fixed, taken); let to = reached(fixed, taken);
// Along the row the span says where the child goes, and that slot // Along the row the span says where the child goes, and that slot
// is the child's box outright rather than something to place an // is the child's box outright rather than something to place an
// answer inside again. A share is decided here and nowhere // answer inside again. A share is decided here and nowhere
@@ -118,7 +116,7 @@ impl Widget for Span {
// it, since a text wraps at the width it is actually given. A // it, since a text wraps at the width it is actually given. A
// fixed child's slot is its own answer, so a drawing made in the // fixed child's slot is its own answer, so a drawing made in the
// room is put there as it is, and one not made yet is made here. // room is put there as it is, and one not made yet is made here.
let slot = along(from, start); let slot = along(from, to);
let mut place = slot.shifted_desc().fills().on_axis(axis); let mut place = slot.shifted_desc().fills().on_axis(axis);
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
place = place.rel_base(axis, slot.len()); place = place.rel_base(axis, slot.len());
@@ -136,7 +134,6 @@ impl Widget for Span {
} }
} }
fixed.px += self.gap; fixed.px += self.gap;
start = shared(fixed, taken);
} }
// Carried whole rather than collapsed to one share: a span that sizes // Carried whole rather than collapsed to one share: a span that sizes
@@ -146,12 +143,11 @@ impl Widget for Span {
// get a quarter each, which collapsing to `leftover(1)` per level does // get a quarter each, which collapsing to `leftover(1)` per level does
// not give. Resolution happens at the nearest ancestor with a length, // not give. Resolution happens at the nearest ancestor with a length,
// and the root always has one. // and the root always has one.
let along = total;
let ortho = match shrinks { let ortho = match shrinks {
true => ortho, true => ortho,
false => LayoutLen::rel(1.0), false => LayoutLen::rel(1.0),
}; };
Size::from_axis(axis, along, ortho) Size::from_axis(axis, total, ortho)
} }
} }
+29
View File
@@ -1067,3 +1067,32 @@ fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() {
assert_same_regions(&warm, &ids, &cold, &cold_ids); assert_same_regions(&warm, &ids, &cold, &cold_ids);
} }
/// A scroll clamps its position against the box it is drawn in, so drawing it
/// once at one viewport and again at another writes state the second draw then
/// reads. That the answer is still the one a cold layout gives is a property
/// of the clamp, not something the layout enforces.
#[test]
fn a_scrolled_view_resized_lands_where_a_cold_layout_puts_it() {
for amt in [10.0, 40.0, 90.0, 140.0] {
let mut warm = Harness::new((100, 100));
let (_, warm_scroll) = plant_wider(&mut warm, 100.0);
warm.move_to((50.0, 50.0));
warm.scroll((-amt, 0.0));
warm.frame();
warm.resize((160, 100));
warm.frame();
let mut cold = Harness::new((160, 100));
let (_, cold_scroll) = plant_wider(&mut cold, 100.0);
cold.move_to((50.0, 50.0));
cold.scroll((-amt, 0.0));
cold.frame();
assert_eq!(
warm.region(&warm_scroll),
cold.region(&cold_scroll),
"scrolled by {amt} then widened"
);
}
}