From 218c1cb2309ee1bc65ab8a513502ccb35a41974a Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sat, 12 Sep 2026 22:05:06 -0400 Subject: [PATCH] iris: process dirty widgets one at a time --- core/src/ui/render_state.rs | 77 +++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 7495d43..78107b5 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -40,10 +40,10 @@ pub struct UiRenderState { old_root: Option, resized: bool, - /// It used to only ever be inserted into, and `redraw` removed the id - /// *before* testing for it, which made the test constant `false`: the - /// guard could never fire and the set grew by one entry per widget - /// ever drawn and was never emptied. + /// Widgets whose `draw` call is on the stack now. A reentrant draw would + /// create two retained primitive sets with one owner, so it is rejected; + /// completed draws are removed immediately and are instead tracked by + /// consuming their outstanding dirty mark. draw_started: HashSet, /// Widgets which asked from inside `draw` to be drawn on the following /// frame. Kept separate from `Widgets::needs_redraw` until `update_at` @@ -824,49 +824,42 @@ impl UiRenderState { } pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) { - while rsc.widgets().has_updates() { - let pending: Vec<_> = rsc.widgets().needs_redraw.iter().copied().collect(); - for mut child in pending { - for _ in 0..PARENT_CHAIN_LIMIT { - if self.size_matches_hints(child, rsc) { - break; - } - let Some(parent) = self.active.get(&child).and_then(|active| active.parent) - else { - break; - }; - let depends = self - .active - .get(&parent) - .is_some_and(|active| active.size_dependencies.contains(&child)); - if !depends { - break; - } - rsc.widgets_mut().needs_redraw.insert(parent); - child = parent; + while let Some(id) = rsc.widgets().needs_redraw.iter().next().copied() { + // A parent which read this child's size must lay out again before + // the changed child is drawn. Mark the whole dependent path so + // drawing its highest member reaches every dirty descendant. + let mut child = id; + for _ in 0..PARENT_CHAIN_LIMIT { + if self.size_matches_hints(child, rsc) { + break; } + let Some(parent) = self.active.get(&child).and_then(|active| active.parent) else { + break; + }; + let depends = self + .active + .get(&parent) + .is_some_and(|active| active.size_dependencies.contains(&child)); + if !depends { + break; + } + rsc.widgets_mut().needs_redraw.insert(parent); + child = parent; } - let dirty: Vec<_> = rsc.widgets().needs_redraw.iter().copied().collect(); - let mut roots = Vec::new(); - for id in dirty { - let mut ancestor = self.active.get(&id).and_then(|active| active.parent); - let mut covered = false; - for _ in 0..PARENT_CHAIN_LIMIT { - let Some(parent) = ancestor else { break }; - if rsc.widgets().needs_redraw.contains(&parent) { - covered = true; - break; - } - ancestor = self.active.get(&parent).and_then(|active| active.parent); - } - if !covered { - roots.push(id); + // Prefer an already-dirty ancestor. Its draw either consumes this + // mark on the way down or leaves it for the next loop iteration if + // a retained intermediate subtree means it never reaches here. + let mut root = id; + let mut ancestor = self.active.get(&id).and_then(|active| active.parent); + for _ in 0..PARENT_CHAIN_LIMIT { + let Some(parent) = ancestor else { break }; + if rsc.widgets().needs_redraw.contains(&parent) { + root = parent; } + ancestor = self.active.get(&parent).and_then(|active| active.parent); } - for id in roots { - self.redraw(id, rsc); - } + self.redraw(root, rsc); } rsc.free(); }