From a712786ecf5f39d89496ee529cbfda65dde37359 Mon Sep 17 00:00:00 2001 From: atagen Date: Thu, 21 May 2026 19:48:13 +1000 Subject: [PATCH] Choose swap lanes by motion direction --- docs/window-animations-plan.md | 3 ++ src/animation/multiphase.rs | 69 ++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/window-animations-plan.md b/docs/window-animations-plan.md index a659d76a..3a79fafd 100644 --- a/docs/window-animations-plan.md +++ b/docs/window-animations-plan.md @@ -217,6 +217,9 @@ Preferred approach: Current pure planner status: - Two-window same-axis swaps use shrink lanes, move, then grow. +- Swap lane choice follows motion direction, not node identity: right/down + moving windows take the first lane, and left/up moving windows take the second + lane. - Stack extraction/return patterns are covered in both horizontal and vertical orientations: peer/container space scales first, the extracted child moves only after space exists, and orthogonal growth happens in the final phase. diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index 37b31619..3a259359 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -173,7 +173,12 @@ fn plan_axis_crossing_lanes( } let mut windows = request.windows.clone(); - windows.sort_by_key(|window| window.node_id.0); + windows.sort_by_key(|window| lane_index_for_direction(*window, axis)); + if windows.windows(2).any(|pair| { + lane_index_for_direction(pair[0], axis) == lane_index_for_direction(pair[1], axis) + }) { + return None; + } let mut phase1 = vec![]; let mut phase2 = vec![]; let mut phase3 = vec![]; @@ -205,6 +210,15 @@ fn plan_axis_crossing_lanes( ) } +fn lane_index_for_direction(window: MultiphaseWindow, axis: PhaseAxis) -> Option { + let delta = main_start(window.to, axis) - main_start(window.from, axis); + match delta.cmp(&0) { + std::cmp::Ordering::Greater => Some(0), + std::cmp::Ordering::Less => Some(1), + std::cmp::Ordering::Equal => None, + } +} + fn plan_space_then_orthogonal_growth( request: &MultiphaseRequest, axis: PhaseAxis, @@ -627,6 +641,15 @@ mod tests { plan.phases.iter().map(|phase| phase.action).collect() } + fn step_to(plan: &MultiphasePlan, phase: usize, node_id: NodeId) -> Rect { + plan.phases[phase] + .steps + .iter() + .find(|step| step.node_id == node_id) + .unwrap() + .to + } + #[test] fn horizontal_swap_shrinks_moves_then_grows_without_overlap() { let req = request(vec![ @@ -679,8 +702,48 @@ mod tests { }, ]); let plan = plan_no_overlap(&req).unwrap(); - assert_eq!(plan.phases[0].steps[0].to, rect(100, 0, 200, 50)); - assert_eq!(plan.phases[0].steps[1].to, rect(0, 50, 100, 100)); + assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 100, 50)); + assert_eq!(step_to(&plan, 0, id(1)), rect(100, 50, 200, 100)); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn horizontal_swap_lanes_follow_motion_direction_not_node_id() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(100, 0, 200, 100), + to: rect(0, 0, 100, 100), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(0, 0, 100, 100), + to: rect(100, 0, 200, 100), + }, + ]); + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 100, 50)); + assert_eq!(step_to(&plan, 0, id(1)), rect(100, 50, 200, 100)); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn vertical_swap_lanes_follow_motion_direction_not_node_id() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 100, 100, 200), + to: rect(0, 0, 100, 100), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(0, 0, 100, 100), + to: rect(0, 100, 100, 200), + }, + ]); + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 50, 100)); + assert_eq!(step_to(&plan, 0, id(1)), rect(50, 100, 100, 200)); assert!(validate_plan_continuous(&req, &plan)); }