Settle growing layout branches in one frame

This commit is contained in:
iris committed 2026-09-09 16:54:39 -04:00
1 parent f95835593f
commit 46e6edfd0b
3 files changed
+89 -3

No files matched your search

+17 -3
View File
@@ -1207,6 +1207,16 @@ impl UiRenderState {
/// redraws a widget that's currently active (drawn) /// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { 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); rsc.widgets_mut().needs_redraw.remove(&id);
// An ancestor is drawing this widget right now, and that draw is // An ancestor is drawing this widget right now, and that draw is
// about to write fresh primitives for it. Drawing it a second time // 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 -- // relay out too. Checked after the real draw, not before it --
// there is no query left that answers "what size would this be" // there is no query left that answers "what size would this be"
// without actually drawing (LAYOUT.md section 5). // without actually drawing (LAYOUT.md section 5).
let changed = self.active.get(&id).map(|a| a.size) != Some(old_size);
if changed {
if let Some(pid) = parent { if let Some(pid) = parent {
let new_size = self.active.get(&id).map(|a| a.size); self.redraw_and_settle(pid, rsc);
if new_size != Some(old_size) {
self.redraw(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);
} }
} }
} }
+64
View File
@@ -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:?}"
);
}
+8
View File
@@ -31,4 +31,12 @@ impl Widget for Sized {
Axis::Y => self.y, 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
}
} }