diff --git a/src/state.rs b/src/state.rs index 8c2b1472..f6095892 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,22 +1,14 @@ +mod animations; mod connectors; +pub(crate) use animations::LayoutAnimationCandidate; pub use connectors::{ConnectorData, DrmDevData, OutputData}; use { crate::{ acceptor::Acceptor, allocator::BufferObject, - animation::{ - AnimationCurve, AnimationState, 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, - }, + animation::{AnimationCurve, AnimationState, AnimationStyle}, async_engine::{AsyncEngine, SpawnedFuture}, backend::{ Backend, BackendConnectorStateSerials, BackendEvent, ConnectorId, ConnectorIds, @@ -108,7 +100,7 @@ use { time::Time, tree::{ ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode, - FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode, + FoundNode, LatchListener, Node, NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier, ToplevelNode, ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder, WorkspaceNode, WorkspaceNodeId, WsMoveConfig, generic_node_visitor, move_ws_to_output, @@ -157,98 +149,6 @@ use { uapi::{OwnedFd, c}, }; -#[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) -} - pub struct State { pub pid: c::pid_t, pub kb_ctx: KbvmContext, @@ -1471,532 +1371,6 @@ impl State { self.eng.now().msec() } - 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()); - } - } - pub fn output_extents_changed(&self) { self.root.update_extents(); for seat in self.globals.seats.lock().values() { @@ -2508,227 +1882,6 @@ 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) - } - - 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)); - } -} - #[derive(Debug, Error)] pub enum ShmScreencopyError { #[error("There is no render context")] diff --git a/src/state/animations.rs b/src/state/animations.rs new file mode 100644 index 00000000..98bc1995 --- /dev/null +++ b/src/state/animations.rs @@ -0,0 +1,862 @@ +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)); + } +} +