From 158682757af48227ccff412f55d81b542b330a7a Mon Sep 17 00:00:00 2001 From: atagen Date: Thu, 28 May 2026 17:13:24 +1000 Subject: [PATCH] Skip tiny swap redistribution phases --- src/animation/multiphase.rs | 95 +++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index 1b4e83b8..cb067241 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -1,6 +1,9 @@ use {crate::rect::Rect, crate::tree::NodeId}; const MIN_SHRINK_DENOMINATOR: i32 = 8; +// Integer split remainders can make swapped siblings differ by one pixel. Do +// not spend a full animation phase on that imperceptible bookkeeping step. +const SWAP_AXIS_SNAP_PX: i32 = 1; #[derive(Clone, Debug)] pub struct MultiphaseRequest { @@ -876,7 +879,10 @@ fn plan_axis_crossing_lanes( main_start(window.to, axis), main_end(window.to, axis), ); - let lane_move = crossing_lane_move_rect(lane_from, window.to, axis); + let mut lane_move = crossing_lane_move_rect(lane_from, window.to, axis); + if same_axis_delta_within(lane_move, lane_to, axis, SWAP_AXIS_SNAP_PX) { + lane_move = lane_to; + } push_step(&mut phase1, window.node_id, window.from, lane_from); push_step(&mut phase2, window.node_id, lane_from, lane_move); push_step(&mut phase3, window.node_id, lane_move, lane_to); @@ -897,12 +903,10 @@ fn plan_axis_crossing_lanes( lane_axis: axis.other(), }, ), - phase_draft( - PhaseKind::Move, - axis, + phase_draft_classified( phase2, PhaseReason::MoveThroughFreedSpace, - ), + )?, phase_draft( PhaseKind::Scale, axis, @@ -919,6 +923,17 @@ fn plan_axis_crossing_lanes( ) } +fn phase_draft_classified( + steps: Vec, + reason: PhaseReason, +) -> Result { + let actions = steps + .iter() + .map(|step| classify_step(*step).ok_or(MultiphasePlanFailure::NoPattern)) + .collect::, _>>()?; + Ok(phase_draft_mixed(steps, actions, reason)) +} + fn crossing_lane_move_rect(from: Rect, target: Rect, axis: PhaseAxis) -> Rect { let size = main_size(from, axis); if main_start(target, axis) > main_start(from, axis) { @@ -930,6 +945,12 @@ fn crossing_lane_move_rect(from: Rect, target: Rect, axis: PhaseAxis) -> Rect { } } +fn same_axis_delta_within(from: Rect, to: Rect, axis: PhaseAxis, max_delta: i32) -> bool { + let start_delta = (main_start(from, axis) - main_start(to, axis)).abs(); + let end_delta = (main_end(from, axis) - main_end(to, axis)).abs(); + start_delta.max(end_delta) <= max_delta +} + fn lane_sort_key(window: MultiphaseWindow, axis: PhaseAxis) -> (usize, i32, i32, u32) { let delta = main_start(window.to, axis) - main_start(window.from, axis); let direction = match delta.cmp(&0) { @@ -2092,7 +2113,7 @@ mod tests { } #[test] - fn uneven_swap_lanes_split_move_and_same_axis_scale() { + fn one_pixel_uneven_swap_lanes_fold_remainder_into_crossing_phase() { let req = request(vec![ window(1, rect(0, 0, 101, 100), rect(101, 0, 201, 100)), window(2, rect(101, 0, 201, 100), rect(0, 0, 101, 100)), @@ -2100,6 +2121,43 @@ mod tests { let planned = plan_no_overlap_explained(&req).unwrap(); let plan = &planned.plan; + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert_eq!(step_to(plan, 1, id(1)), rect(101, 0, 201, 50)); + assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 101, 100)); + assert!(validate_plan_continuous(&req, plan)); + } + + #[test] + fn large_uneven_swap_lanes_split_move_and_same_axis_scale() { + let req = request(vec![ + window(1, rect(0, 0, 120, 100), rect(120, 0, 200, 100)), + window(2, rect(120, 0, 200, 100), rect(0, 0, 120, 100)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + assert_eq!( planned.explanation.strategy, PlanStrategy::SwapLanes { @@ -2127,10 +2185,10 @@ mod tests { }, ] ); - assert_eq!(step_to(plan, 1, id(1)), rect(100, 0, 201, 50)); - assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 100, 100)); - assert_eq!(step_to(plan, 2, id(1)), rect(101, 0, 201, 50)); - assert_eq!(step_to(plan, 2, id(2)), rect(0, 50, 101, 100)); + assert_eq!(step_to(plan, 1, id(1)), rect(80, 0, 200, 50)); + assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 80, 100)); + assert_eq!(step_to(plan, 2, id(1)), rect(120, 0, 200, 50)); + assert_eq!(step_to(plan, 2, id(2)), rect(0, 50, 120, 100)); assert!(validate_plan_continuous(&req, plan)); } @@ -2149,6 +2207,23 @@ mod tests { axis: PhaseAxis::Horizontal, } ); + assert_eq!( + actions(&planned.plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); assert!(validate_plan_continuous(&req, &planned.plan)); }