From 8d2b7a512b84c23bf61aa10df763576369d1cf95 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Sat, 19 Sep 2026 22:04:39 -0400 Subject: [PATCH] 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 --- src/widget/position/span.rs | 7 +++---- tests/cases/unsettled.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 0a3b822..7fa2f7c 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -37,8 +37,8 @@ impl Widget for Span { painter.widget_at(child, room).len(axis) } }; - cursor.px += len.px + self.gap; - cursor.rel += len.rel; + cursor += len.without_leftover(); + cursor.px += self.gap; lens.push(len); } @@ -146,12 +146,11 @@ impl Widget for Span { // get a quarter each, which collapsing to `leftover(1)` per level does // not give. Resolution happens at the nearest ancestor with a length, // and the root always has one. - let along = total; let ortho = match shrinks { true => ortho, false => LayoutLen::rel(1.0), }; - Size::from_axis(axis, along, ortho) + Size::from_axis(axis, total, ortho) } } diff --git a/tests/cases/unsettled.rs b/tests/cases/unsettled.rs index f2e4303..1d472f5 100644 --- a/tests/cases/unsettled.rs +++ b/tests/cases/unsettled.rs @@ -1067,3 +1067,32 @@ fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() { 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" + ); + } +}