From 23523eea29d08560b3eea3ee77fa53b92babcf3a Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Sat, 19 Sep 2026 00:21:11 -0400 Subject: [PATCH] Take a window read where a length is resolved against it A widget that resolves a window length in pixels depends on that window wherever the length is a fraction of it, and nothing was recording that: Painter::to_px replaces window_px_len and pins the window it read, while a length that is only pixels is that many pixels in any window and pins nothing. Span still states the range it actually branched on, which replaces the pin with something wider. Scroll is where it showed: its content's answer is a window length now, so a viewport whose own box does not change with the window -- 40 px of a branch's box -- kept an end-snapped offset from the window before. Seed 942 at depth 6 under resize, pinned as unsettled::resizing_under_a_short_scroll_snaps_its_window_tall_content_again. Co-Authored-By: Claude Opus 5 --- core/src/ui/painter.rs | 17 ++++++++++---- src/random.rs | 4 +--- src/widget/position/scroll.rs | 3 +-- src/widget/position/span.rs | 2 +- tests/cases/determinism.rs | 4 +--- tests/cases/unsettled.rs | 44 +++++++++++++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 13 deletions(-) diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 0859325..86fbf0f 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -422,10 +422,19 @@ impl<'a> Painter<'a> { self.extent_own[axis as usize] = holds; } - /// One window axis in pixels. Every length in layout is a length of the - /// window, so this is what one becomes pixels against. - pub fn window_px_len(&self, axis: Axis) -> Px { - self.window.axis(axis) + /// A window length in pixels, which is what every length in layout is + /// measured in. Reading one pins the drawing to this window wherever the + /// length is a fraction of it; one that is only pixels is that many + /// pixels in any window and pins nothing. + pub fn to_px(&mut self, len: Len, axis: Axis) -> Px { + let window = self.window.axis(axis); + if len.rel != Rel::ZERO { + let own = &mut self.frame_own[axis as usize]; + if *own == Holds::ANY { + *own = Holds::at(window); + } + } + len.to_px(window) } /// A validity range already stated about the window. Containers use diff --git a/src/random.rs b/src/random.rs index 870b992..b792266 100644 --- a/src/random.rs +++ b/src/random.rs @@ -122,9 +122,7 @@ impl Widget for Branch { let measured = painter .widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top]) .len(Axis::X); - let px = measured - .apply_leftover() - .to_px(painter.window_px_len(Axis::X)); + let px = painter.to_px(measured.apply_leftover(), Axis::X); let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let place = [Place::Within(Part::All), below]; diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index bca8770..8a29257 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -16,8 +16,7 @@ impl Widget for Scroll { let answer_len = painter .widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2]) .len(self.axis); - let fixed = - Len::from_parts(answer_len.rel, answer_len.px).to_px(painter.window_px_len(self.axis)); + let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis); self.container_len = container_len; self.content_len = fixed.max(container_len); diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 5c8bb08..327bf9a 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -68,7 +68,7 @@ impl Widget for Span { // exist at all turns on this. let mut shares = false; if total.leftover > Weight::ZERO { - shares = room.to_px(painter.window_px_len(axis)) > Px::ZERO; + shares = painter.to_px(room, axis) > Px::ZERO; let holds = match shares { true => Holds::from(Px::STEP..=Px::MAX), false => Holds::from(Px::MIN..=Px::ZERO), diff --git a/tests/cases/determinism.rs b/tests/cases/determinism.rs index 4f16d7e..4601f59 100644 --- a/tests/cases/determinism.rs +++ b/tests/cases/determinism.rs @@ -26,9 +26,7 @@ impl Widget for BranchesOnMeasurement { let measured = painter .widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top]) .len(Axis::X); - let px = measured - .apply_leftover() - .to_px(painter.window_px_len(Axis::X)); + let px = painter.to_px(measured.apply_leftover(), Axis::X); let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let place = [Place::Within(Part::All), below]; diff --git a/tests/cases/unsettled.rs b/tests/cases/unsettled.rs index 4337214..f2e4303 100644 --- a/tests/cases/unsettled.rs +++ b/tests/cases/unsettled.rs @@ -1023,3 +1023,47 @@ fn changing_a_rule_over_two_pads_divides_the_column_again() { assert_same_regions(&warm, &ids, &cold, &cold_ids); } + +/// Six widgets, shrunk from seed 942 at depth 6 (`resize`). A `Branch` asks +/// its probe in the top 40 px of its box and forwards the frame, so the +/// scroll's own box is 40 px tall whatever the window is -- but its content +/// is as tall as the frame, which is the window, and a scroll kept to its +/// end has to be told when that changes. Resolving a length against the +/// window is what reads it, so that is where the dependency is taken. +fn plant_a_window_tall_column_in_a_short_scroll(h: &mut Harness) -> Vec { + let leaf = rect(Color::RED).add(&mut h.rsc); + let column = Span { + children: vec![leaf.add_strong(&mut h.rsc)], + dir: Dir::RIGHT, + gap: Px::ZERO, + } + .height(rel(1.0)) + .add(&mut h.rsc); + let scroll = Scroll::new(column.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc); + let wide = rect(Color::BLUE).add(&mut h.rsc); + let narrow = rect(Color::GREEN).add(&mut h.rsc); + let root = Branch { + probe: scroll.add_strong(&mut h.rsc), + wide: wide.add_strong(&mut h.rsc), + narrow: narrow.add_strong(&mut h.rsc), + threshold: 55.0, + } + .add(&mut h.rsc); + h.set_root(root); + vec![leaf.id(), column.id(), scroll.id(), root.id()] +} + +#[test] +fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() { + let mut warm = Harness::new((1920, 1200)); + let ids = plant_a_window_tall_column_in_a_short_scroll(&mut warm); + warm.frame(); + warm.resize((640, 900)); + warm.frame(); + + let mut cold = Harness::new((640, 900)); + let cold_ids = plant_a_window_tall_column_in_a_short_scroll(&mut cold); + cold.frame(); + + assert_same_regions(&warm, &ids, &cold, &cold_ids); +}