Record which widget is drawing a subtree that changed hands

A subtree can be reused whole under a different parent -- same box, same
layer, same region node, clean -- and nothing in the drawing says it
moved. Two things read who its parent is, and both were wrong after one
of these.

The old parent still listed it as a child, and a parent's next draw
undraws whatever is missing from that list: two spans under one root,
with the root swapping which of them it holds, drew the subtree under
the new span and then erased it when the old one drew. The move is
recorded on both sides where `draw_inner` already writes what the ask
decided, rather than guarded at each reader.

Its depth was also the one it had under the old parent, which is what
the settling walk orders by, so a change made under it afterwards
settled at the wrong point in the frame. `try_reuse` re-walks the
subtree's depths, and only where the top of it moved, which is what
makes that free in the ordinary case.

Two tests: one shape where the subtree's box does not move and the span
it left erases it, one where it changes depth and the change made under
it has to reach the span it moved to. Each fails without one half.
This commit is contained in:
iris-ai committed 2026-09-17 13:05:15 -04:00
1 parent a0693acc56
commit e44dea34b4
3 files changed
+161 -19

No files matched your search

+44 -19
View File
@@ -87,12 +87,11 @@ impl UiRenderState {
/// retained entry at all.
///
/// The root is the only widget a resize marks, and only where the new
/// output is outside what its answer holds for. Its range is the
/// output falls outside what its answer holds for: that range is the
/// intersection of everything under it, so admitting the new output says
/// the whole tree still stands -- and nothing above the root moved, the
/// window being no entry to rewrite. Where it does not admit it, the
/// ordinary bottom-up walk draws the root and `Holds` decides, per widget
/// and as the walk reaches it, how far down the new length reaches.
/// the whole tree still stands. Where it does not, the ordinary walk
/// draws the root, and each widget's own range decides how far down the
/// new length reaches.
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
let size = PxVec2::from_f32(size.into());
if size == self.output_size {
@@ -101,12 +100,11 @@ impl UiRenderState {
self.output_size = size;
self.resized = true;
let Some(root) = self.old_root else { return };
let Some(active) = self.active.get(&root) else {
return;
};
let px = active.given_len.to_px(size);
let holds = active.answer.1;
if !(holds[0].contains(px.x) && holds[1].contains(px.y)) {
let stands = self
.active
.get(&root)
.is_some_and(|active| active.answers_at(active.given_len.to_px(size)));
if !stands {
widgets.needs_redraw.insert(root);
}
}
@@ -204,10 +202,9 @@ impl UiRenderState {
diag::draw_request(id, info.parent, region, info.px, info.region_node);
}
let align = rsc.widgets().alignment(id);
// Only the widget's own mark is asked about. Nothing it measured can
// be dirty while it draws: layout is one bottom-up walk, so anything
// deeper has already settled or deferred to its own parent, and a
// deferred one leaves that parent marked.
// Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked.
let stale = rsc.widgets().needs_redraw.contains(&id);
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
let retained = match replace_answer || stale {
@@ -256,7 +253,18 @@ impl UiRenderState {
active.answer = settled;
active.decided = info.decided;
active.own_align = align;
active.depth = info.depth;
// A subtree can be reused whole under a different parent -- same box,
// same layer, same region node -- and nothing in the drawing says it
// changed hands. Two things read who its parent is: a deferral, which
// marks whoever has it to draw, and the old parent's list of children,
// which its next draw undraws whatever is missing from.
let old_parent = std::mem::replace(&mut active.parent, info.parent);
if old_parent != info.parent
&& let Some(old_parent) = old_parent
&& let Some(old_parent) = self.active.get_mut(&old_parent)
{
old_parent.children.retain(|child| *child != id);
}
settled
}
@@ -514,8 +522,7 @@ impl UiRenderState {
{
return None;
}
let (size, holds) = active.answer;
(holds[0].contains(info.px.x) && holds[1].contains(info.px.y)).then_some((size, holds))
active.answers_at(info.px).then_some(active.answer)
}
/// The pixel lengths of the box a widget was given and of the box it was
@@ -629,12 +636,12 @@ impl UiRenderState {
self.remap_subtree(id, &remap, info.parent_move, rsc);
}
}
self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap();
active.region = region;
active.given = region;
active.given_len = info.given_len;
active.offer_len = info.offer_len;
active.depth = info.depth;
#[cfg(feature = "layout-diagnostics")]
{
match (moved, has_region_node) {
@@ -658,6 +665,24 @@ impl UiRenderState {
Some(answer)
}
/// A reused subtree keeps its shape, so every widget in it moves by the
/// same amount -- and where the top of it did not move, none of it did,
/// which is what makes this free in the ordinary case.
fn redepth(&mut self, id: WidgetId, depth: usize) {
let Some(active) = self.active.get_mut(&id) else {
return;
};
if active.depth == depth {
return;
}
active.depth = depth;
let children = active.children.len();
for index in 0..children {
let child = self.active[&id].children[index];
self.redepth(child, depth + 1);
}
}
/// Re-expresses an ordinary retained subtree in a new parent region.
/// An independently movable descendant needs only its own region changed;
/// its contents stay in that region's coordinate space.