diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index e5098ce..9a6319d 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -22,6 +22,8 @@ pub struct Painter<'a> { /// The children whose size this widget read while drawing. pub(super) size_deps: Vec, pub(super) reads_output: bool, + /// Whether this widget is drawing in the same pixel-sized box as before. + pub(super) same_box: bool, /// The slot this widget's primitives are positioned through: its own if /// its parent placed it, otherwise the nearest ancestor that has one. pub(super) move_idx: MoveIdx, @@ -144,6 +146,21 @@ impl<'a> Painter<'a> { Some(hint) } + /// A clean child's retained size, when this widget's own constraints are + /// unchanged. This is the answer from its last real draw, not a guess. + pub fn retained_size(&mut self, id: &StrongWidget) -> Option { + if !self.same_box || self.rsc.widgets().needs_redraw.contains(&id.id()) { + return None; + } + let active = self.state.active.get(&id.id())?; + if active.parent != Some(self.id) { + return None; + } + let size = active.size; + self.depend_on_size(id); + Some(size) + } + fn depend_on_size(&mut self, child: &StrongWidget) { if !self.size_deps.contains(&child.id()) { self.size_deps.push(child.id()); diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 531463f..658ad99 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -109,18 +109,19 @@ impl UiRenderState { parent_move: MoveIdx, slotted: bool, mask: MaskIdx, - old_children: Option>, + old_active: Option, rsc: &mut dyn UiRsc, ) -> Size { - let mut old_children = old_children.unwrap_or_default(); + let mut old_active = old_active; if self.active.contains_key(&id) { if let Some(size) = self.try_reuse(id, region, parent_move, rsc) { return size; } // if not, then maintain resize and track old children to remove unneeded - let active = self.remove(id, false, rsc).unwrap(); - old_children = active.children; + old_active = self.remove(id, false, rsc); } + let previous_px = old_active.as_ref().map(|active| active.px); + let old_children = old_active.map(|active| active.children).unwrap_or_default(); // draw widget let (move_idx, local) = match slotted { @@ -133,6 +134,7 @@ impl UiRenderState { } }; let px = self.px_of(move_idx, local); + let same_box = previous_px == Some(px); rsc.widgets_mut().needs_redraw.remove(&id); self.draw_started.insert(id); @@ -147,6 +149,7 @@ impl UiRenderState { children: Vec::new(), size_deps: Vec::new(), reads_output: false, + same_box, move_idx, rsc, }; @@ -165,6 +168,7 @@ impl UiRenderState { children, size_deps, reads_output, + same_box: _, move_idx, layer, id, @@ -449,14 +453,17 @@ impl UiRenderState { /// redraws a widget that's currently active (drawn) pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { self.draw_started.remove(&id); - // Whoever read this widget's size may be a different size now, so the - // highest reader is what draws. Everything between the two is marked - // as well: their own boxes have not changed, so the mark is the only - // thing stopping the draw reusing its way past this widget. - if let Some(top) = self.mark_readers(id, rsc) { + // A widget can only answer whether its size changed by drawing in the + // box its parent chose. If that box changed in pixels, its retained + // placement is stale and the highest size reader must choose the new + // box first. Otherwise the widget can draw locally, and its readers + // only matter if the returned size actually changed. + let box_changed = self + .active + .get(&id) + .is_some_and(|active| self.px_of(active.parent_move, active.region) != active.px); + if box_changed && let Some(top) = self.mark_readers(id, rsc) { self.redraw(top, rsc); - // Cleared by that draw if it reached here; if it did not, this is - // no longer drawn and asking again would not end. rsc.widgets_mut().needs_redraw.remove(&id); return; } @@ -470,7 +477,8 @@ impl UiRenderState { return; }; - self.draw_inner( + let was = active.size; + let size = self.draw_inner( active.layer, id, active.region, @@ -478,9 +486,16 @@ impl UiRenderState { active.parent_move, active.move_idx != active.parent_move, active.mask, - Some(active.children), + Some(active), rsc, ); + + if size != was + && let Some(top) = self.mark_readers(id, rsc) + { + self.redraw(top, rsc); + rsc.widgets_mut().needs_redraw.remove(&id); + } } /// The furthest ancestor that read this widget's size, directly or through diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 5b7cd6f..3150128 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -13,9 +13,13 @@ impl Widget for Scroll { fn draw(&mut self, painter: &mut Painter) -> Size { let output_len = painter.output_size().axis(self.axis); let container_len = UiScalar::abs(painter.px_size().axis(self.axis)); - // Drawn in the whole container to learn its length, then placed at - // the scrolled offset. - let child = painter.place(&self.inner, UiRegion::FULL).size(); + // Its last measured size stays valid while neither the child nor this + // container's constraints changed. Otherwise draw it in the whole + // container to learn its length, then place it at the scrolled offset. + let child = match painter.retained_size(&self.inner) { + Some(size) => size, + None => painter.place(&self.inner, UiRegion::FULL).size(), + }; let content_len = child .axis(self.axis) .apply_rest() diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 94346e5..886fec2 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -12,14 +12,24 @@ impl Widget for Span { let axis = self.dir.axis; // A length for every child before any is placed: from its own hint // where it has one, and from drawing it where it does not. - let lens: Vec = self - .children - .iter() - .map(|child| match painter.size_hint(child, axis) { + let mut cursor = UiScalar::rel_min(); + let mut lens = Vec::with_capacity(self.children.len()); + for child in &self.children { + let len = match painter.size_hint(child, axis) { Some(len) => len, - None => painter.place(child, UiRegion::FULL).len(axis), - }) - .collect(); + None => { + let mut span = UiSpan::new(cursor, UiScalar::rel_max()); + if self.dir.sign == Sign::Neg { + span.flip(); + } + let region = UiRegion::from_axis(axis, span, UiSpan::FULL); + painter.place(child, region).len(axis) + } + }; + cursor.abs += len.abs + self.gap; + cursor.rel += len.rel; + lens.push(len); + } let gap = self.gap * self.children.len().saturating_sub(1) as f32; let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len); diff --git a/tests/retained.rs b/tests/retained.rs index 73fdc8c..019a33c 100644 --- a/tests/retained.rs +++ b/tests/retained.rs @@ -79,10 +79,9 @@ fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() { h.rsc[first].size = Size::from((150, 200)); h.frame(); - // Twice: once for the span to measure it, once for its real box. A child - // that can hint its length is spared the first, and a smaller number here - // means someone has made that cheaper rather than broken it. - assert_eq!(draws.get(), settled + 2); + // The preceding fixed child makes the remaining box this child's real + // box, so measuring it also draws it in its final place. + assert_eq!(draws.get(), settled + 1); assert_corners!(h, second, (150, 0), (400, 200)); } @@ -116,6 +115,50 @@ fn a_span_relays_out_when_a_child_it_measured_changes() { assert_corners!(h, second, (250, 0), (400, 200)); } +#[test] +fn a_repaint_that_keeps_its_size_does_not_relay_out() { + let mut h = Harness::new((400, 200)); + let (first, draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate); + let (second, _) = counted(&mut h, Size::REST, OnResize::Translate); + h.set_root((first, second).span(Dir::RIGHT)); + let settled = draws.get(); + + // Taking mutable access is the ordinary content-change signal. This + // widget returns the same size, so the parent has nothing to lay out. + let _ = h.rsc.widgets_mut().get_dyn_mut(first.id()); + h.frame(); + + assert_eq!(draws.get(), settled + 1); +} + +#[test] +fn scrolling_reuses_the_clean_contents_size() { + let mut h = Harness::new((400, 200)); + let (inner, draws) = counted(&mut h, Size::from((400, 600)), OnResize::Translate); + let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc); + h.set_root(scroll); + let settled = draws.get(); + + h.rsc[scroll].scroll(40.0); + h.frame(); + + assert_eq!(draws.get(), settled); + assert_corners!(h, inner, (0, -360), (400, 240)); +} + +#[test] +fn scrolling_remeasures_changed_contents() { + let mut h = Harness::new((400, 200)); + let (inner, _) = counted(&mut h, Size::from((400, 600)), OnResize::Translate); + let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc); + h.set_root(scroll); + + h.rsc[inner].size = Size::from((400, 800)); + h.frame(); + + assert_corners!(h, inner, (0, -600), (400, 200)); +} + #[test] fn a_placed_child_survives_the_next_frame() { let mut h = Harness::new((400, 200));