diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 4ac8e47..f6e9710 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -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) + } } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 46a27a7..cce4a39 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -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, 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. diff --git a/tests/cases/retained.rs b/tests/cases/retained.rs index 06dcd72..98368b9 100644 --- a/tests/cases/retained.rs +++ b/tests/cases/retained.rs @@ -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, + second: WeakWidget, + root: WeakWidget, + 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" + ); +}