Stop composer layout recursion on spaces

This commit is contained in:
iris committed 2026-09-09 19:54:53 -04:00
1 parent 46e6edfd0b
commit 2bc0ff1866
1 file changed
+37 -13
+37 -13
View File
@@ -1217,18 +1217,50 @@ impl UiRenderState {
/// newly grown subtree can retain the provisional (even inverted) region /// newly grown subtree can retain the provisional (even inverted) region
/// it was measured in until an unrelated later update redraws it. /// it was measured in until an unrelated later update redraws it.
fn redraw_and_settle(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { fn redraw_and_settle(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
let Some((parent, changed)) = self.redraw_once(id, rsc) else {
return;
};
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. This is deliberately one
// redraw rather than another settling pass: feeding its size
// back into the same upward walk can alternate between the
// provisional and final regions forever (a text edit first did
// that when an Android IME committed a space), overflowing the
// native thread's stack before Rust can report a panic.
let settled_size = self.active.get(&id).map(|active| active.size);
let _ = self.redraw_once(id, rsc);
debug_assert_eq!(
self.active.get(&id).map(|active| active.size),
settled_size,
"a widget changed size after its parent settled its final region"
);
}
}
/// Redraw `id` exactly once, returning its parent and whether the size it
/// reports changed. [`Self::redraw_and_settle`] owns any propagation; in
/// particular, its final downward redraw must not start another upward
/// pass through the same branch.
fn redraw_once(
&mut self,
id: WidgetId,
rsc: &mut dyn UiRsc,
) -> Option<(Option<WidgetId>, bool)> {
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
// here would leave one of the two copies on screen with nothing // here would leave one of the two copies on screen with nothing
// owning it -- see `draw_started`'s own doc. // owning it -- see `draw_started`'s own doc.
if self.draw_started.contains(&id) { if self.draw_started.contains(&id) {
return; return None;
} }
let Some(active) = self.remove(id, false, true, rsc) else { let active = self.remove(id, false, true, rsc)?;
return;
};
let old_size = active.size; let old_size = active.size;
let parent = active.parent; let parent = active.parent;
// `old_move_slot` being `Some` below means the slot is reused in // `old_move_slot` being `Some` below means the slot is reused in
@@ -1259,15 +1291,7 @@ impl UiRenderState {
// 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); let changed = self.active.get(&id).map(|a| a.size) != Some(old_size);
if changed { Some((parent, 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);
}
} }
} }