From 2bc0ff18662613f54e9d28759f8248925b5c5665 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Wed, 9 Sep 2026 19:54:53 -0400 Subject: [PATCH] Stop composer layout recursion on spaces --- core/src/ui/render_state.rs | 50 +++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 0358ee0..d5f5d21 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1217,18 +1217,50 @@ impl UiRenderState { /// 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) { + 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, bool)> { 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 // here would leave one of the two copies on screen with nothing // owning it -- see `draw_started`'s own doc. if self.draw_started.contains(&id) { - return; + return None; } - let Some(active) = self.remove(id, false, true, rsc) else { - return; - }; + let active = self.remove(id, false, true, rsc)?; let old_size = active.size; let parent = active.parent; // `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" // 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 { - 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); - } + Some((parent, changed)) } }