From 02222d5189abf563206e8abbb7accb4e2b1264cf Mon Sep 17 00:00:00 2001 From: atagen Date: Wed, 27 May 2026 22:24:01 +1000 Subject: [PATCH] Coalesce layout animation candidates --- src/state.rs | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/src/state.rs b/src/state.rs index bf806640..071a054d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -171,6 +171,28 @@ pub(crate) struct LayoutAnimationCandidate { hierarchy: MultiphaseWindowHierarchy, } +fn coalesce_layout_animation_candidates( + candidates: Vec, +) -> Vec { + let mut merged: Vec = vec![]; + for candidate in candidates { + if let Some(existing) = merged + .iter_mut() + .find(|existing| existing.node_id == candidate.node_id) + { + existing.new = candidate.new; + existing.curve = candidate.curve; + existing.hierarchy = MultiphaseWindowHierarchy::new( + existing.hierarchy.source, + candidate.hierarchy.target, + ); + } else { + merged.push(candidate); + } + } + merged +} + pub struct State { pub pid: c::pid_t, pub kb_ctx: KbvmContext, @@ -1619,6 +1641,10 @@ impl State { let Some(candidates) = self.layout_animation_batch.borrow_mut().take() else { return; }; + let candidates = coalesce_layout_animation_candidates(candidates); + if candidates.is_empty() { + return; + } let now = self.now_nsec(); let windows: Vec<_> = candidates .iter() @@ -2431,6 +2457,132 @@ impl State { } } +#[cfg(test)] +mod tests { + use { + super::*, + crate::animation::multiphase::MultiphaseHierarchyPosition, + }; + + fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect { + Rect::new_saturating(x1, y1, x2, y2) + } + + fn hierarchy( + source: MultiphaseHierarchyPosition, + target: MultiphaseHierarchyPosition, + ) -> MultiphaseWindowHierarchy { + MultiphaseWindowHierarchy::new(source, target) + } + + #[test] + fn layout_animation_candidates_coalesce_duplicate_nodes() { + let source = MultiphaseHierarchyPosition { + parent: Some(NodeId(10)), + depth: 2, + sibling_index: Some(1), + ..Default::default() + }; + let intermediate = MultiphaseHierarchyPosition { + parent: Some(NodeId(11)), + depth: 1, + sibling_index: Some(0), + ..Default::default() + }; + let target = MultiphaseHierarchyPosition { + parent: Some(NodeId(12)), + depth: 0, + sibling_index: Some(2), + ..Default::default() + }; + let second_source = MultiphaseHierarchyPosition { + parent: Some(NodeId(20)), + depth: 1, + sibling_index: Some(0), + ..Default::default() + }; + let second_target = MultiphaseHierarchyPosition { + parent: Some(NodeId(20)), + depth: 1, + sibling_index: Some(1), + ..Default::default() + }; + + let candidates = vec![ + LayoutAnimationCandidate { + node_id: NodeId(1), + old: rect(0, 0, 100, 100), + new: rect(0, 0, 80, 100), + curve: AnimationCurve::Linear, + hierarchy: hierarchy(source, intermediate), + }, + LayoutAnimationCandidate { + node_id: NodeId(2), + old: rect(100, 0, 200, 100), + new: rect(120, 0, 220, 100), + curve: AnimationCurve::Linear, + hierarchy: hierarchy(second_source, second_target), + }, + LayoutAnimationCandidate { + node_id: NodeId(1), + old: rect(0, 0, 80, 100), + new: rect(0, 0, 60, 100), + curve: AnimationCurve::from_config(4), + hierarchy: hierarchy(intermediate, target), + }, + ]; + + let merged = coalesce_layout_animation_candidates(candidates); + + assert_eq!(merged.len(), 2); + assert_eq!(merged[0].node_id, NodeId(1)); + assert_eq!(merged[0].old, rect(0, 0, 100, 100)); + assert_eq!(merged[0].new, rect(0, 0, 60, 100)); + assert_eq!(merged[0].curve, AnimationCurve::from_config(4)); + assert_eq!(merged[0].hierarchy, hierarchy(source, target)); + assert_eq!(merged[1].node_id, NodeId(2)); + assert_eq!(merged[1].old, rect(100, 0, 200, 100)); + assert_eq!(merged[1].new, rect(120, 0, 220, 100)); + assert_eq!(merged[1].hierarchy, hierarchy(second_source, second_target)); + } + + #[test] + fn layout_animation_candidates_keep_coalesced_layout_noops() { + let hierarchy = MultiphaseWindowHierarchy::default(); + let candidates = vec![ + LayoutAnimationCandidate { + node_id: NodeId(1), + old: rect(0, 0, 100, 100), + new: rect(0, 0, 80, 100), + curve: AnimationCurve::Linear, + hierarchy, + }, + LayoutAnimationCandidate { + node_id: NodeId(1), + old: rect(0, 0, 80, 100), + new: rect(0, 0, 100, 100), + curve: AnimationCurve::Linear, + hierarchy, + }, + LayoutAnimationCandidate { + node_id: NodeId(2), + old: rect(100, 0, 200, 100), + new: rect(120, 0, 220, 100), + curve: AnimationCurve::Linear, + hierarchy, + }, + ]; + + let merged = coalesce_layout_animation_candidates(candidates); + + assert_eq!(merged.len(), 2); + assert_eq!(merged[0].node_id, NodeId(1)); + assert_eq!(merged[0].old, rect(0, 0, 100, 100)); + assert_eq!(merged[0].new, rect(0, 0, 100, 100)); + assert_eq!(merged[1].node_id, NodeId(2)); + } +} + #[derive(Debug, Error)] pub enum ShmScreencopyError { #[error("There is no render context")]