diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 1009c74..0358ee0 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1207,6 +1207,16 @@ impl UiRenderState { /// redraws a widget that's currently active (drawn) pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { + self.redraw_and_settle(id, rsc); + } + + /// Measure a changed branch toward the root, then revisit each widget + /// whose reported size changed on the way back down. The upward pass gives + /// every parent the new child size; the downward pass is what lets those + /// children draw inside the final boxes their parents chose. Without it a + /// newly grown subtree can retain the provisional (even inverted) region + /// it was measured in until an unrelated later update redraws it. + fn redraw_and_settle(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { rsc.widgets_mut().needs_redraw.remove(&id); // An ancestor is drawing this widget right now, and that draw is // about to write fresh primitives for it. Drawing it a second time @@ -1248,11 +1258,15 @@ impl UiRenderState { // relay out too. Checked after the real draw, not before it -- // there is no query left that answers "what size would this be" // without actually drawing (LAYOUT.md section 5). - if let Some(pid) = parent { - let new_size = self.active.get(&id).map(|a| a.size); - if new_size != Some(old_size) { - self.redraw(pid, rsc); + let changed = self.active.get(&id).map(|a| a.size) != Some(old_size); + if changed { + if let Some(pid) = parent { + self.redraw_and_settle(pid, rsc); } + // The parent pass above has now placed this widget in its final + // region. Draw it once more there; unchanged descendants still + // take draw_inner's retained fast path. + self.redraw_and_settle(id, rsc); } } } diff --git a/src/layout_tests.rs b/src/layout_tests.rs index 29b4e6e..beaad8d 100644 --- a/src/layout_tests.rs +++ b/src/layout_tests.rs @@ -1172,3 +1172,67 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() { ); } } + +/// Growing an already-drawn row first measures its new child against the +/// row's old height. That provisional box can end before it starts when the +/// old trailing edge is above the new child's cursor. The size must bubble to +/// `LazySpan` and the corrected allocation must travel back down before this +/// update is presented; a later stream event is not a layout pass. +#[test] +fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() { + const FIRST: f32 = 30.0; + const SECOND: f32 = 70.0; + const GAP: f32 = 8.0; + + let mut rsc = TestRsc { + ui: UiData::default(), + }; + let first = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)); + let first_id = first.id(); + let first = rsc.ui.widgets.add_strong(Sized { + inner: first.any(), + x: None, + y: Some(Len::abs(FIRST)), + }); + let mut contents = Span::empty(Dir::DOWN).gap(Len::abs(GAP)); + contents.push(first.any()); + let contents = rsc.ui.widgets.add_strong(contents); + let contents_w = contents.weak(); + let row = rsc.ui.widgets.add_strong(Sized { + inner: contents.any(), + x: Some(Len::rest(1.0)), + y: None, + }); + let mut list = LazySpan::new(Dir::DOWN, Pin::End); + list.push_back(LazyItem::new(0, row.any())); + let root = rsc.ui.widgets.add_strong(list).any(); + + let mut render = UiRenderState::new(); + render.resize((200.0, 200.0)); + render.update(&root, &mut rsc); + + let second = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)); + let second_id = second.id(); + let second = rsc.ui.widgets.add_strong(Sized { + inner: second.any(), + x: None, + y: Some(Len::abs(SECOND)), + }); + rsc.ui + .widgets + .get_mut(&contents_w) + .unwrap() + .push(second.any()); + render.update(&root, &mut rsc); + + let first = render.primitive_corners(render.first_primitive(first_id).unwrap(), &rsc); + let second = render.primitive_corners(render.first_primitive(second_id).unwrap(), &rsc); + assert!( + (second.top_left.y - (first.bot_right.y + GAP)).abs() < 0.01, + "the new child should start after the old child and its gap: first={first:?} second={second:?}" + ); + assert!( + (second.bot_right.y - second.top_left.y - SECOND).abs() < 0.01, + "the new child retained its provisional box: {second:?}" + ); +} diff --git a/src/widget/position/sized.rs b/src/widget/position/sized.rs index d6d17ed..f34ff80 100644 --- a/src/widget/position/sized.rs +++ b/src/widget/position/sized.rs @@ -31,4 +31,12 @@ impl Widget for Sized { Axis::Y => self.y, } } + + fn requires_exact_region(&self) -> bool { + // `Sized` may be measured in a provisional box and then placed in + // the content-sized box it reported. Its own region can be corrected + // by a move, but its child consumed the original box and must see the + // final one too. + true + } }