diff --git a/docs/window-animations-plan.md b/docs/window-animations-plan.md index 3a79fafd..fd99ac39 100644 --- a/docs/window-animations-plan.md +++ b/docs/window-animations-plan.md @@ -229,6 +229,10 @@ Current pure planner status: - Live layout batches are partitioned by overlapping motion bounds, so unrelated groups can still use multiphase animation when another group falls back to linear motion. +- Planner requests now carry per-window hierarchy metadata for source/target + parent, depth, sibling index, split axis, mono state, and transition kind. + The current planner records this information but does not yet use it to order + nested-container phases. Tests: diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index 3a259359..75c63493 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -13,6 +13,104 @@ pub struct MultiphaseWindow { pub node_id: NodeId, pub from: Rect, pub to: Rect, + pub hierarchy: MultiphaseWindowHierarchy, +} + +impl MultiphaseWindow { + pub fn new(node_id: NodeId, from: Rect, to: Rect) -> Self { + Self { + node_id, + from, + to, + hierarchy: Default::default(), + } + } + + pub fn with_hierarchy( + node_id: NodeId, + from: Rect, + to: Rect, + hierarchy: MultiphaseWindowHierarchy, + ) -> Self { + Self { + node_id, + from, + to, + hierarchy, + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct MultiphaseWindowHierarchy { + pub source: MultiphaseHierarchyPosition, + pub target: MultiphaseHierarchyPosition, + pub transition: MultiphaseHierarchyTransition, +} + +impl MultiphaseWindowHierarchy { + pub fn new(source: MultiphaseHierarchyPosition, target: MultiphaseHierarchyPosition) -> Self { + let transition = if !source.parent_is_mono && target.parent_is_mono { + MultiphaseHierarchyTransition::EnteringMono + } else if source.parent_is_mono && !target.parent_is_mono { + MultiphaseHierarchyTransition::ExitingMono + } else if source.parent.is_none() || target.parent.is_none() { + MultiphaseHierarchyTransition::Unknown + } else if target.depth < source.depth { + MultiphaseHierarchyTransition::Ascending + } else if target.depth > source.depth { + MultiphaseHierarchyTransition::Descending + } else { + MultiphaseHierarchyTransition::SameLevel + }; + Self { + source, + target, + transition, + } + } + + fn reversed(self) -> Self { + Self { + source: self.target, + target: self.source, + transition: self.transition.reversed(), + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct MultiphaseHierarchyPosition { + pub parent: Option, + pub depth: u16, + pub sibling_index: Option, + pub split_axis: Option, + pub parent_is_mono: bool, + pub mono_active: bool, +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum MultiphaseHierarchyTransition { + #[default] + Unknown, + SameLevel, + Ascending, + Descending, + EnteringMono, + ExitingMono, +} + +impl MultiphaseHierarchyTransition { + fn reversed(self) -> Self { + match self { + Self::Unknown => Self::Unknown, + Self::SameLevel => Self::SameLevel, + Self::Ascending => Self::Descending, + Self::Descending => Self::Ascending, + Self::EnteringMono => Self::ExitingMono, + Self::ExitingMono => Self::EnteringMono, + } + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -519,6 +617,7 @@ fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest { node_id: window.node_id, from: window.to, to: window.from, + hierarchy: window.hierarchy.reversed(), }) .collect(), } @@ -628,6 +727,10 @@ mod tests { Rect::new_saturating(x1, y1, x2, y2) } + fn window(raw: u32, from: Rect, to: Rect) -> MultiphaseWindow { + MultiphaseWindow::new(id(raw), from, to) + } + fn request(windows: Vec) -> MultiphaseRequest { let bounds = windows .iter() @@ -653,15 +756,12 @@ mod tests { #[test] fn horizontal_swap_shrinks_moves_then_grows_without_overlap() { let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 100, 100), - to: rect(100, 0, 200, 100), - }, + window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), MultiphaseWindow { node_id: id(2), from: rect(100, 0, 200, 100), to: rect(0, 0, 100, 100), + hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); @@ -694,11 +794,13 @@ mod tests { node_id: id(1), from: rect(100, 0, 200, 100), to: rect(0, 0, 100, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 0, 100, 100), to: rect(100, 0, 200, 100), + hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); @@ -714,11 +816,13 @@ mod tests { node_id: id(1), from: rect(100, 0, 200, 100), to: rect(0, 0, 100, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 0, 100, 100), to: rect(100, 0, 200, 100), + hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); @@ -734,11 +838,13 @@ mod tests { node_id: id(1), from: rect(0, 100, 100, 200), to: rect(0, 0, 100, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 0, 100, 100), to: rect(0, 100, 100, 200), + hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); @@ -754,16 +860,19 @@ mod tests { node_id: id(1), from: rect(0, 0, 200, 100), to: rect(0, 0, 100, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(200, 0, 400, 50), to: rect(100, 0, 300, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(200, 50, 400, 100), to: rect(300, 0, 400, 100), + hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); @@ -799,16 +908,19 @@ mod tests { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(0, 0, 200, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(100, 0, 300, 100), to: rect(200, 0, 400, 50), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(300, 0, 400, 100), to: rect(200, 50, 400, 100), + hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); @@ -839,16 +951,19 @@ mod tests { node_id: id(1), from: rect(0, 0, 100, 200), to: rect(0, 0, 100, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 200, 50, 400), to: rect(0, 100, 100, 300), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(50, 200, 100, 400), to: rect(0, 300, 100, 400), + hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); @@ -884,16 +999,19 @@ mod tests { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(0, 0, 100, 200), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 100, 100, 300), to: rect(0, 200, 50, 400), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(0, 300, 100, 400), to: rect(50, 200, 100, 400), + hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); @@ -923,10 +1041,50 @@ mod tests { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(100, 100, 200, 200), + hierarchy: Default::default(), }]); assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); } + #[test] + fn hierarchy_metadata_classifies_depth_and_mono_transitions() { + let source = MultiphaseHierarchyPosition { + parent: Some(id(10)), + depth: 2, + sibling_index: Some(0), + split_axis: Some(PhaseAxis::Vertical), + ..Default::default() + }; + let target = MultiphaseHierarchyPosition { + parent: Some(id(11)), + depth: 1, + sibling_index: Some(2), + split_axis: Some(PhaseAxis::Horizontal), + ..Default::default() + }; + assert_eq!( + MultiphaseWindowHierarchy::new(source, target).transition, + MultiphaseHierarchyTransition::Ascending + ); + + let entering_mono = MultiphaseWindowHierarchy::new( + source, + MultiphaseHierarchyPosition { + parent_is_mono: true, + mono_active: true, + ..target + }, + ); + assert_eq!( + entering_mono.transition, + MultiphaseHierarchyTransition::EnteringMono + ); + assert_eq!( + entering_mono.reversed().transition, + MultiphaseHierarchyTransition::ExitingMono + ); + } + #[test] fn continuous_validation_rejects_narrow_mid_phase_overlap() { let req = request(vec![ @@ -934,11 +1092,13 @@ mod tests { node_id: id(1), from: rect(0, 0, 10, 10), to: rect(100, 0, 110, 10), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(13, 0, 14, 10), to: rect(13, 0, 14, 10), + hierarchy: Default::default(), }, ]); let plan = MultiphasePlan { @@ -965,11 +1125,13 @@ mod tests { node_id: id(1), from: rect(0, 0, 10, 10), to: rect(10, 0, 20, 10), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(20, 0, 30, 10), to: rect(20, 0, 30, 10), + hierarchy: Default::default(), }, ]); let plan = MultiphasePlan { @@ -995,6 +1157,7 @@ mod tests { node_id: id(1), from: rect(0, 0, 10, 10), to: rect(20, 0, 30, 10), + hierarchy: Default::default(), }]); let plan = MultiphasePlan { phases: vec![MultiphasePhase { @@ -1020,16 +1183,19 @@ mod tests { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(100, 0, 200, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(100, 0, 200, 100), to: rect(0, 0, 100, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(300, 0, 400, 100), to: rect(400, 0, 500, 100), + hierarchy: Default::default(), }, ]; assert_eq!(partition_motion_groups(&windows), vec![vec![0, 1], vec![2]]); @@ -1042,16 +1208,19 @@ mod tests { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(80, 0, 180, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(170, 0, 270, 100), to: rect(250, 0, 350, 100), + hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(90, 0, 180, 100), to: rect(180, 0, 260, 100), + hierarchy: Default::default(), }, ]; assert_eq!(partition_motion_groups(&windows), vec![vec![0, 1, 2]]); diff --git a/src/state.rs b/src/state.rs index 835678f8..0d120721 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,7 +6,8 @@ use { AnimationCurve, AnimationState, AnimationTick, RetainedExitLayer, RetainedToplevel, expand_damage_rect, multiphase::{ - MultiphaseRequest, MultiphaseWindow, partition_motion_groups, plan_no_overlap, + MultiphaseRequest, MultiphaseWindow, MultiphaseWindowHierarchy, + partition_motion_groups, plan_no_overlap, }, spawn_in_start_rect, }, @@ -168,6 +169,7 @@ pub(crate) struct LayoutAnimationCandidate { new: Rect, retained: Option>, curve: AnimationCurve, + hierarchy: MultiphaseWindowHierarchy, } pub struct State { @@ -1500,7 +1502,29 @@ impl State { .layout_animation_curve_override .get() .unwrap_or_else(|| self.animations.curve.get()); - self.queue_layout_animation(node_id, old, new, retained, curve); + self.queue_layout_animation( + node_id, + old, + new, + retained, + curve, + MultiphaseWindowHierarchy::default(), + ); + } + + pub fn queue_tiled_animation_with_hierarchy( + self: &Rc, + node_id: NodeId, + old: Rect, + new: Rect, + retained: Option>, + 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, retained, curve, hierarchy); } pub fn queue_linear_layout_animation( @@ -1510,7 +1534,14 @@ impl State { new: Rect, retained: Option>, ) { - self.queue_layout_animation(node_id, old, new, retained, AnimationCurve::Linear); + self.queue_layout_animation( + node_id, + old, + new, + retained, + AnimationCurve::Linear, + MultiphaseWindowHierarchy::default(), + ); } fn queue_layout_animation( @@ -1520,6 +1551,7 @@ impl State { new: Rect, retained: Option>, curve: AnimationCurve, + hierarchy: MultiphaseWindowHierarchy, ) { if !self.animations.enabled.get() || !self.layout_animations_active.get() @@ -1546,6 +1578,7 @@ impl State { new, retained, curve, + hierarchy, }; if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() { batch.push(candidate); @@ -1590,12 +1623,14 @@ impl State { let now = self.now_nsec(); let windows: Vec<_> = candidates .iter() - .map(|candidate| MultiphaseWindow { - node_id: candidate.node_id, - from: self - .animations - .visual_rect(candidate.node_id, candidate.old, now), - to: candidate.new, + .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) { diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 41db95d1..d5ace6bb 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -1,6 +1,9 @@ use { crate::{ - animation::{RetainedExitLayer, RetainedToplevel}, + animation::{ + RetainedExitLayer, RetainedToplevel, + multiphase::{MultiphaseHierarchyPosition, MultiphaseWindowHierarchy, PhaseAxis}, + }, client::{Client, ClientId}, criteria::{ CritDestroyListener, CritMatcherId, @@ -186,6 +189,11 @@ impl ToplevelNode for T { fn tl_change_extents(self: Rc, rect: &Rect) { let data = self.tl_data(); let prev = data.desired_extents.replace(*rect); + let target_hierarchy = self.tl_multiphase_hierarchy_position(); + let hierarchy = MultiphaseWindowHierarchy::new( + data.layout_animation_position.replace(target_hierarchy), + target_hierarchy, + ); let spawn_in_pending = data.spawn_in_pending.get(); let parent_is_mono = data .parent @@ -200,11 +208,12 @@ impl ToplevelNode for T { && !self.node_is_container() && !parent_is_mono { - data.state.clone().queue_tiled_animation( + data.state.clone().queue_tiled_animation_with_hierarchy( data.node_id, prev, *rect, self.tl_animation_snapshot(), + hierarchy, ); } if spawn_in_pending @@ -314,6 +323,35 @@ pub trait ToplevelNodeBase: Node { true } + fn tl_multiphase_hierarchy_position(&self) -> MultiphaseHierarchyPosition { + let data = self.tl_data(); + let Some(parent) = data.parent.get() else { + return Default::default(); + }; + let mut position = MultiphaseHierarchyPosition { + parent: Some(parent.node_id()), + depth: multiphase_parent_depth(Some(parent.clone())), + ..Default::default() + }; + if let Some(container) = parent.node_into_container() { + position.split_axis = Some(match container.split.get() { + ContainerSplit::Horizontal => PhaseAxis::Horizontal, + ContainerSplit::Vertical => PhaseAxis::Vertical, + }); + if let Some(mono) = container.mono_child.get() { + position.parent_is_mono = true; + position.mono_active = mono.node.node_id() == data.node_id; + } + for (idx, child) in container.children.iter().enumerate() { + if child.node.node_id() == data.node_id { + position.sibling_index = Some(idx.min(u16::MAX as usize) as u16); + break; + } + } + } + position + } + fn tl_set_active(&self, active: bool) { let _ = active; } @@ -383,6 +421,18 @@ pub trait ToplevelNodeBase: Node { } } +fn multiphase_parent_depth(mut parent: Option>) -> u16 { + let mut depth = 0u16; + while let Some(node) = parent { + let Some(toplevel) = node.node_into_toplevel() else { + break; + }; + depth = depth.saturating_add(1); + parent = toplevel.tl_data().parent.get(); + } + depth +} + pub struct FullscreenedData { pub placeholder: Rc, pub workspace: Rc, @@ -453,6 +503,7 @@ pub struct ToplevelData { pub spawn_in_pending: Cell, pub pos: Cell, pub desired_extents: Cell, + pub layout_animation_position: Cell, pub seat_state: NodeSeatState, pub wants_attention: Cell, pub requested_attention: Cell, @@ -517,6 +568,7 @@ impl ToplevelData { spawn_in_pending: Cell::new(false), pos: Default::default(), desired_extents: Default::default(), + layout_animation_position: Default::default(), seat_state: Default::default(), wants_attention: Cell::new(false), requested_attention: Cell::new(false),