From e8a5792dcb9b006a1b8e3b9b819dac37f429704e Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Sat, 19 Sep 2026 01:23:34 -0400 Subject: [PATCH] Place a scroll's fitting content in the viewport, not in the window A scroll that has not been scrolled and whose content fits asked for its content box as `Part::From(UiSpan::FULL)`. A `Part::From` span is in window lengths, so `rel(1.0)` in one is the whole window rather than the whole box, and the content landed in a window-tall box anchored at the viewport's start -- 50 px low for a 300 px viewport in a 400 px window. Saying the whole of the box as `Part::All` is the one expression that cannot mean anything else, and it is also the place the child was already asked in, so the placement becomes a no-op. --- src/widget/position/scroll.rs | 15 ++++++++------- tests/cases/scroll.rs | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 8a29257..9771df1 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -44,7 +44,6 @@ impl Widget for Scroll { // have placed the whole scroll in a box longer than it. let slack = (self.container_len - self.content_len).max(Px::ZERO); let anchor = slack.mul(align.rel()); - let mut content = UiSpan::FULL; // Content that fills the viewport and has not been scrolled is the // viewport, and is handed back as it came. Writing the same box as // its own length in pixels is the same box in another form, and the @@ -52,18 +51,20 @@ impl Widget for Scroll { // one centred in `px 900`, since halving a difference is not halving // each part of it. let moved = anchor != Px::ZERO || self.amt != Px::ZERO; - if moved || self.content_len != self.container_len { - let start = Len::from_parts(Rel::ZERO, anchor - self.amt); - content = UiSpan::new(start, start.offset(self.content_len)); - } + let content = match moved || self.content_len != self.container_len { + true => { + let start = Len::from_parts(Rel::ZERO, anchor - self.amt); + Part::From(UiSpan::new(start, start.offset(self.content_len))) + } + false => Part::All, + }; // The viewport is the inner's frame, so a fraction it declares or // reports is a fraction of what is on screen rather than of the // content box its own answer decided. Where it goes is the content // box, scrolled: its drawing moved there, not made again there. painter.place_at( &self.inner, - self.axis - .pair(Place::Fill(Part::From(content)), Place::Fill(Part::All)), + self.axis.pair(Place::Fill(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 diff --git a/tests/cases/scroll.rs b/tests/cases/scroll.rs index 2de8567..e7094c9 100644 --- a/tests/cases/scroll.rs +++ b/tests/cases/scroll.rs @@ -145,3 +145,19 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() { h.set_root(clipper); h.frame(); } + +/// Content that fits sits in the viewport, not in a box of the window's +/// length anchored at the viewport's start. `Part::From` takes window +/// lengths, so a `rel(1.0)` span in one is the window, and only a scroll +/// filling the window would land right. +#[test] +fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() { + let mut h = Harness::new((400, 400)); + let head = rect(Color::RED).height(100).add(&mut h.rsc); + let inner = rect(Color::BLUE).height(50).add(&mut h.rsc); + let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc); + h.set_root((head, scroll).span(Dir::DOWN)); + + assert_corners!(h, scroll, (0, 100), (400, 400)); + assert_corners!(h, inner, (0, 225), (400, 275)); +}