use { crate::{ animation::{ AnimationCurve, AnimationStyle, AnimationTick, RetainedExitLayer, RetainedToplevel, expand_damage_rect, multiphase::{ MultiphasePhase, MultiphasePlan, MultiphasePlanFailure, MultiphaseRequest, MultiphaseWindow, MultiphaseWindowHierarchy, partition_motion_groups, plan_no_overlap_with_diagnostics, validate_phase_paths, }, spawn_in_start_rect, }, rect::Rect, tree::NodeId, }, std::rc::Rc, }; use super::State; #[derive(Clone)] pub(crate) struct LayoutAnimationCandidate { node_id: NodeId, old: Rect, new: Rect, curve: AnimationCurve, style: AnimationStyle, 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.style = candidate.style; existing.hierarchy = MultiphaseWindowHierarchy::new( existing.hierarchy.source, candidate.hierarchy.target, ); } else { merged.push(candidate); } } merged } fn layout_animation_group_uses_plain( candidates: &[LayoutAnimationCandidate], group: &[usize], ) -> bool { group .iter() .any(|&idx| candidates[idx].style == AnimationStyle::Plain) } fn bridged_retarget_plan( request: &MultiphaseRequest, candidates: &[LayoutAnimationCandidate], group: &[usize], bridge_paths: &[Vec<(Rect, Rect)>], bridge_phase_count: usize, follow_phases: &[MultiphasePhase], ) -> Result { let mut paths = vec![]; for (group_pos, &idx) in group.iter().enumerate() { let candidate = &candidates[idx]; let window = request.windows[group_pos]; let Some(bridge_path) = bridge_paths.get(group_pos) else { return Err(MultiphasePlanFailure::NoPattern); }; let mut path = bridge_path.clone(); let mut current = path .last() .map(|(_, to)| *to) .unwrap_or(window.from); while path.len() < bridge_phase_count { path.push((current, current)); } if current != candidate.old { return Err(MultiphasePlanFailure::NoPattern); } for phase in follow_phases { match phase .steps .iter() .find(|step| step.node_id == candidate.node_id) { Some(step) => { if step.from != current { return Err(MultiphasePlanFailure::NoPattern); } path.push((step.from, step.to)); current = step.to; } None => path.push((current, current)), } } if current != window.to { return Err(MultiphasePlanFailure::NoPattern); } paths.push(path); } validate_phase_paths(request, &paths) } impl State { pub fn queue_tiled_animation( self: &Rc, node_id: NodeId, old: Rect, new: Rect, ) { let curve = self .layout_animation_curve_override .get() .unwrap_or_else(|| self.animations.curve.get()); self.queue_layout_animation( node_id, old, new, curve, MultiphaseWindowHierarchy::default(), ); } pub fn queue_tiled_animation_with_hierarchy( self: &Rc, node_id: NodeId, old: Rect, new: Rect, hierarchy: MultiphaseWindowHierarchy, ) { let curve = self .layout_animation_curve_override .get() .unwrap_or_else(|| self.animations.curve.get()); self.queue_layout_animation(node_id, old, new, curve, hierarchy); } pub fn queue_linear_layout_animation( self: &Rc, node_id: NodeId, old: Rect, new: Rect, ) { self.queue_layout_animation( node_id, old, new, AnimationCurve::Linear, MultiphaseWindowHierarchy::default(), ); } fn queue_layout_animation( self: &Rc, node_id: NodeId, old: Rect, new: Rect, curve: AnimationCurve, hierarchy: MultiphaseWindowHierarchy, ) { if !self.animations.enabled.get() || !self.layout_animations_active.get() || self.suppress_animations_for_next_layout.get() { return; } let (old_output, old_scale) = { let (x, y) = old.center(); let (output, _, _) = self.find_closest_output(x, y); (output.id, output.global.persistent.scale.get()) }; let (new_output, new_scale) = { let (x, y) = new.center(); let (output, _, _) = self.find_closest_output(x, y); (output.id, output.global.persistent.scale.get()) }; if old_output != new_output || old_scale != new_scale { return; } let candidate = LayoutAnimationCandidate { node_id, old, new, curve, style: self .layout_animation_style_override .get() .unwrap_or_else(|| self.animations.style.get()), hierarchy, }; if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() { batch.push(candidate); return; } self.start_layout_animation_candidate(candidate, self.now_nsec()); } fn start_layout_animation_candidate( self: &Rc, candidate: LayoutAnimationCandidate, now_nsec: u64, ) { let started = self.animations.set_target( candidate.node_id, candidate.old, candidate.new, None, now_nsec, self.animations.duration_ms.get(), candidate.curve, ); if started { self.damage(expand_damage_rect( candidate.old.union(candidate.new), self.theme.sizes.border_width.get().max(0), )); self.ensure_animation_tick(); } } pub fn begin_layout_animation_batch(&self) { self.layout_animation_batch .borrow_mut() .get_or_insert_with(Vec::new); } pub fn finish_layout_animation_batch(self: &Rc) { 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() .map(|candidate| { MultiphaseWindow::with_hierarchy( candidate.node_id, self.animations .visual_rect(candidate.node_id, candidate.old, now), candidate.new, candidate.hierarchy, ) }) .collect(); for group in partition_motion_groups(&windows, self.layout_animation_clearance()) { if layout_animation_group_uses_plain(&candidates, &group) { for idx in group { self.start_layout_animation_candidate(candidates[idx].clone(), now); } continue; } if self.start_multiphase_layout_animation(&candidates, &windows, &group, now) { continue; } for idx in group { self.start_layout_animation_candidate(candidates[idx].clone(), now); } } } fn layout_animation_clearance(&self) -> i32 { let border = self.theme.sizes.border_width.get().max(0); let gap = self.theme.sizes.gap.get().max(0); if gap == 0 { border } else { gap + 2 * border } } fn start_multiphase_layout_animation( self: &Rc, candidates: &[LayoutAnimationCandidate], windows: &[MultiphaseWindow], group: &[usize], now_nsec: u64, ) -> bool { let request_windows: Vec<_> = group.iter().map(|&idx| windows[idx]).collect(); let Some(first) = request_windows.first() else { return false; }; let mut bounds = first.from.union(first.to); for window in &request_windows[1..] { bounds = bounds.union(window.from).union(window.to); } let request = MultiphaseRequest { bounds, windows: request_windows, clearance: self.layout_animation_clearance(), }; if self.start_existing_phased_retarget(candidates, windows, group, &request, now_nsec) { return true; } if self.start_bridged_phased_retarget(candidates, windows, group, &request, now_nsec) { return true; } let plan = match plan_no_overlap_with_diagnostics(&request) { Ok(plan) => plan, Err(diagnostic) => { log::debug!( "falling back to plain layout animation for group {:?}: {:?}", group, diagnostic ); return false; } }; self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec) } fn start_existing_phased_retarget( self: &Rc, candidates: &[LayoutAnimationCandidate], windows: &[MultiphaseWindow], group: &[usize], request: &MultiphaseRequest, now_nsec: u64, ) -> bool { let mut paths = vec![]; for &idx in group { let candidate = &candidates[idx]; let window = windows[idx]; let Some(path) = self.animations .phased_route_to(candidate.node_id, window.to, now_nsec) else { return false; }; paths.push(path); } let plan = match validate_phase_paths(request, &paths) { Ok(plan) => plan, Err(error) => { log::debug!( "existing phased retarget rejected for group {:?}: {:?}", group, error ); return false; } }; log::debug!("retargeting active phased animation for group {:?}", group); self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec) } fn start_bridged_phased_retarget( self: &Rc, candidates: &[LayoutAnimationCandidate], windows: &[MultiphaseWindow], group: &[usize], request: &MultiphaseRequest, now_nsec: u64, ) -> bool { let mut bridge_paths = vec![]; let mut bridge_phase_count = 0; let mut has_bridge = false; for &idx in group { let candidate = &candidates[idx]; let window = windows[idx]; if window.from == candidate.old { bridge_paths.push(vec![]); continue; } let Some(path) = self.animations .phased_route_to(candidate.node_id, candidate.old, now_nsec) else { return false; }; if !path.is_empty() { has_bridge = true; bridge_phase_count = bridge_phase_count.max(path.len()); } bridge_paths.push(path); } if !has_bridge { return false; } let settled_windows: Vec<_> = group .iter() .map(|&idx| { let candidate = &candidates[idx]; MultiphaseWindow::with_hierarchy( candidate.node_id, candidate.old, candidate.new, candidate.hierarchy, ) }) .collect(); let Some(first) = settled_windows.first() else { return false; }; let mut bounds = first.from.union(first.to); for window in &settled_windows[1..] { bounds = bounds.union(window.from).union(window.to); } let settled_request = MultiphaseRequest { bounds, windows: settled_windows, clearance: self.layout_animation_clearance(), }; let follow_plan = match plan_no_overlap_with_diagnostics(&settled_request) { Ok(plan) => plan, Err(diagnostic) => { log::debug!( "bridged phased retarget follow-up rejected for group {:?}: {:?}", group, diagnostic ); return false; } }; let plan = match bridged_retarget_plan( request, candidates, group, &bridge_paths, bridge_phase_count, &follow_plan.phases, ) { Ok(plan) => plan, Err(error) => { log::debug!( "bridged phased retarget rejected for group {:?}: {:?}", group, error ); return false; } }; log::debug!("bridging active phased animation for group {:?}", group); self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec) } fn start_multiphase_plan( self: &Rc, candidates: &[LayoutAnimationCandidate], windows: &[MultiphaseWindow], group: &[usize], plan_phases: &[crate::animation::multiphase::MultiphasePhase], now_nsec: u64, ) -> bool { if plan_phases.is_empty() { return false; } let mut entries = vec![]; for &idx in group { let candidate = &candidates[idx]; let window = windows[idx]; let mut current = window.from; let mut damage = current.union(window.to); let mut phases = vec![]; for phase in plan_phases { match phase .steps .iter() .find(|step| step.node_id == candidate.node_id) { Some(step) => { phases.push((step.from, step.to)); damage = damage.union(step.from).union(step.to); current = step.to; } None => phases.push((current, current)), } } if current != window.to { return false; } entries.push((candidate.clone(), phases, damage)); } let mut started_any = false; for (candidate, phases, damage) in entries { if self.animations.set_phased_target( candidate.node_id, phases, None, now_nsec, self.animations.duration_ms.get(), candidate.curve, ) { started_any = true; self.damage(expand_damage_rect( damage, self.theme.sizes.border_width.get().max(0), )); } } if started_any { self.ensure_animation_tick(); } started_any } pub fn queue_spawn_in_animation( self: &Rc, node_id: NodeId, target: Rect, ) { if !self.animations.enabled.get() || target.is_empty() { return; } let start = spawn_in_start_rect(target); let now = self.now_nsec(); let started = self.animations.set_spawn_in( node_id, target, None, now, self.animations.duration_ms.get(), self.animations.curve.get(), ); if started { self.damage(expand_damage_rect( start.union(target), self.theme.sizes.border_width.get().max(0), )); self.ensure_animation_tick(); } } pub fn queue_spawn_out_animation( self: &Rc, from: Rect, frame_inset: i32, retained: Rc, active: bool, layer: RetainedExitLayer, ) { if !self.animations.enabled.get() || from.is_empty() { return; } let now = self.now_nsec(); let started = self.animations.set_spawn_out( from, frame_inset, retained, active, layer, now, self.animations.duration_ms.get(), self.animations.curve.get(), ); if started { self.damage(expand_damage_rect( from, self.theme.sizes.border_width.get().max(0), )); self.ensure_animation_tick(); } } pub fn set_animations_enabled(&self, enabled: bool) { if self.animations.enabled.replace(enabled) && !enabled { self.animations.clear(); self.damage(self.root.extents.get()); } } pub fn set_animation_duration_ms(&self, duration_ms: u32) { self.animations.duration_ms.set(duration_ms); } pub fn set_animation_curve(&self, curve: u32) { self.animations .curve .set(AnimationCurve::from_config(curve)); } pub fn set_animation_style(&self, style: u32) -> bool { let Some(style) = AnimationStyle::from_config(style) else { return false; }; self.animations.style.set(style); true } pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool { let Some(curve) = AnimationCurve::from_cubic_bezier(x1, y1, x2, y2) else { return false; }; self.animations.curve.set(curve); true } pub fn with_layout_animations(&self, f: impl FnOnce() -> T) -> T { let prev_requested = self.layout_animations_requested.replace(true); let prev_active = self.layout_animations_active.replace(true); let res = f(); self.layout_animations_requested.set(prev_requested); self.layout_animations_active.set(prev_active); res } pub fn with_linear_layout_animations(&self, f: impl FnOnce() -> T) -> T { let prev_requested = self.layout_animations_requested.replace(true); let prev_active = self.layout_animations_active.replace(true); let prev_curve = self .layout_animation_curve_override .replace(Some(AnimationCurve::Linear)); let prev_style = self .layout_animation_style_override .replace(Some(AnimationStyle::Plain)); let res = f(); self.layout_animations_requested.set(prev_requested); self.layout_animations_active.set(prev_active); self.layout_animation_curve_override.set(prev_curve); self.layout_animation_style_override.set(prev_style); res } fn ensure_animation_tick(self: &Rc) { if self.animations.tick_is_active() { return; } let outputs: Vec<_> = self.root.outputs.lock().values().cloned().collect(); if outputs.is_empty() { return; } let tick = Rc::new_cyclic(|weak| AnimationTick::new(self, weak)); for output in &outputs { tick.attach(output); } self.animations.set_tick(tick); for output in &outputs { self.damage(output.global.pos.get()); } } } #[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) } fn candidate(node_id: u32, style: AnimationStyle) -> LayoutAnimationCandidate { candidate_rects( node_id, rect(0, 0, 100, 100), rect(100, 0, 200, 100), style, ) } fn candidate_rects( node_id: u32, old: Rect, new: Rect, style: AnimationStyle, ) -> LayoutAnimationCandidate { LayoutAnimationCandidate { node_id: NodeId(node_id), old, new, curve: AnimationCurve::Linear, style, hierarchy: MultiphaseWindowHierarchy::default(), } } #[test] fn plain_style_candidate_forces_group_plain() { let candidates = vec![ candidate(1, AnimationStyle::Multiphase), candidate(2, AnimationStyle::Plain), ]; assert!(!layout_animation_group_uses_plain(&candidates, &[0])); assert!(layout_animation_group_uses_plain(&candidates, &[0, 1])); } #[test] fn bridged_retarget_handles_second_rotation_interrupt() { let a_left = rect(0, 0, 100, 100); let c_mid = rect(100, 0, 200, 100); let c_left = a_left; let a_mid = c_mid; let c_current = rect(150, 50, 250, 100); let c_mid_lane = rect(100, 50, 200, 100); let candidates = vec![ candidate_rects(1, a_left, a_mid, AnimationStyle::Multiphase), candidate_rects(3, c_mid, c_left, AnimationStyle::Multiphase), ]; let request = MultiphaseRequest { bounds: rect(0, 0, 250, 100), windows: vec![ MultiphaseWindow::new(NodeId(1), a_left, a_mid), MultiphaseWindow::new(NodeId(3), c_current, c_left), ], clearance: 0, }; let settled_request = MultiphaseRequest { bounds: rect(0, 0, 200, 100), windows: vec![ MultiphaseWindow::new(NodeId(1), a_left, a_mid), MultiphaseWindow::new(NodeId(3), c_mid, c_left), ], clearance: 0, }; let follow_plan = plan_no_overlap_with_diagnostics(&settled_request).unwrap(); let bridge_paths = vec![vec![], vec![(c_current, c_mid_lane), (c_mid_lane, c_mid)]]; let plan = bridged_retarget_plan( &request, &candidates, &[0, 1], &bridge_paths, 2, &follow_plan.phases, ) .unwrap(); assert!(plan .phases .iter() .any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(1)))); assert!(plan .phases .iter() .any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(3)))); } #[test] fn layout_animation_candidates_coalesce_duplicate_nodes() { let source = MultiphaseHierarchyPosition { parent: Some(NodeId(10).into()), depth: 2, sibling_index: Some(1), ..Default::default() }; let intermediate = MultiphaseHierarchyPosition { parent: Some(NodeId(11).into()), depth: 1, sibling_index: Some(0), ..Default::default() }; let target = MultiphaseHierarchyPosition { parent: Some(NodeId(12).into()), depth: 0, sibling_index: Some(2), ..Default::default() }; let second_source = MultiphaseHierarchyPosition { parent: Some(NodeId(20).into()), depth: 1, sibling_index: Some(0), ..Default::default() }; let second_target = MultiphaseHierarchyPosition { parent: Some(NodeId(20).into()), 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, style: AnimationStyle::Multiphase, hierarchy: hierarchy(source, intermediate), }, LayoutAnimationCandidate { node_id: NodeId(2), old: rect(100, 0, 200, 100), new: rect(120, 0, 220, 100), curve: AnimationCurve::Linear, style: AnimationStyle::Multiphase, 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), style: AnimationStyle::Plain, 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].style, AnimationStyle::Plain); 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, style: AnimationStyle::Multiphase, hierarchy, }, LayoutAnimationCandidate { node_id: NodeId(1), old: rect(0, 0, 80, 100), new: rect(0, 0, 100, 100), curve: AnimationCurve::Linear, style: AnimationStyle::Plain, hierarchy, }, LayoutAnimationCandidate { node_id: NodeId(2), old: rect(100, 0, 200, 100), new: rect(120, 0, 220, 100), curve: AnimationCurve::Linear, style: AnimationStyle::Multiphase, 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[0].style, AnimationStyle::Plain); assert_eq!(merged[1].node_id, NodeId(2)); } }