diff --git a/docs/LAYOUT.md b/docs/LAYOUT.md index f50ae2f..d3ebbe4 100644 --- a/docs/LAYOUT.md +++ b/docs/LAYOUT.md @@ -141,11 +141,12 @@ which is correct always, just not free. **Size invalidation travels upward before drawing; drawing itself travels only downward.** Every active widget retains the direct children whose size it read -through `DrawResult::size()` or `Painter::known_len`. Before a frame draws, -`redraw_updates` follows only those dependency edges from each dirty child and -marks the affected ancestors dirty. It then selects the highest dirty roots and -draws them top-down. Drawing never synchronously invalidates or invokes a -parent, so there is no layout recursion and no provisional child draw on a +through `DrawResult::size()` or `Painter::known_len`. `redraw_updates` takes +one id from the dirty set, follows only those dependency edges upward and marks +that path dirty, then redraws its highest already-dirty ancestor. Drawing that +ancestor consumes the marks of every dirty descendant it reaches; the loop +then takes whatever remains. Drawing never synchronously invalidates or invokes +a parent, so there is no layout recursion and no provisional child draw on a different layer. An exact `size_hint` stops propagation when both axes still equal the retained diff --git a/iris/core/src/ui/render_state.rs b/iris/core/src/ui/render_state.rs index 7495d43..78107b5 100644 --- a/iris/core/src/ui/render_state.rs +++ b/iris/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(); }