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

+8
View File
@@ -74,4 +74,12 @@ impl ActiveData {
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
}
/// Whether what it answered still stands for a box of these pixel
/// lengths -- the box it was asked in, where `holds` is about the box its
/// answer then chose.
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
let (_, holds) = self.answer;
holds[0].contains(px.x) && holds[1].contains(px.y)
}
}
+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.
+109
View File
@@ -628,3 +628,112 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
h.frame();
assert_corners!(h, inner, (100, 0), (400, 200));
}
/// The two spans a subtree changes hands between, and the branch that is not
/// in the tree yet -- kept alive by the test until it is.
struct Handover {
leaf: WidgetId,
first: WeakWidget<Span>,
second: WeakWidget<Span>,
root: WeakWidget<Span>,
spare: StrongWidget,
}
/// A subtree that changes hands while its box does not move, so nothing about
/// reusing its drawing says it changed parents. `deeper` puts a span between
/// the root and `second`, so it changes depth by changing hands as well.
fn plant_handover(h: &mut Harness, moved: bool, deeper: bool, width: f32) -> Handover {
let leaf = rect(Color::RED).add(&mut h.rsc);
let sized = leaf.width(width).add(&mut h.rsc);
let holder = (sized,).span(Dir::RIGHT).add(&mut h.rsc);
let first = Span {
children: match moved {
true => Vec::new(),
false => vec![holder.add_strong(&mut h.rsc)],
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let second = Span {
children: match moved {
true => vec![holder.add_strong(&mut h.rsc)],
false => Vec::new(),
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let branch = match deeper {
true => (second,).span(Dir::RIGHT).add_strong(&mut h.rsc),
false => second.add_strong(&mut h.rsc),
};
let (in_tree, spare) = match moved {
true => (branch, first.add_strong(&mut h.rsc)),
false => (first.add_strong(&mut h.rsc), branch),
};
let root = Span {
children: vec![in_tree],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
Handover {
leaf: sized.id(),
first,
second,
root,
spare,
}
}
/// Moves the subtree and swaps the branch it sits in for the one it left.
fn hand_over(h: &mut Harness, tree: Handover) -> WidgetId {
let holder = h.rsc[tree.first].children.remove(0);
h.rsc[tree.second].children.push(holder);
h.rsc[tree.root].children.clear();
h.rsc[tree.root].children.push(tree.spare);
h.frame();
tree.leaf
}
#[test]
fn a_subtree_that_changed_parents_is_not_undrawn_by_the_one_it_left() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, false, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, false, 40.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it left still listed it and undrew it"
);
}
#[test]
fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, true, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
// After it has changed hands, so what has to reach the new parent is a
// change made under the subtree it now holds.
warm.set_len(leaf, Axis::X, LayoutLen::px(90.0));
warm.frame();
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, true, 90.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it moved to is the one the change has to reach"
);
}