diff --git a/src/animation.rs b/src/animation.rs index 2e93df1c..19647b80 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -22,8 +22,6 @@ const DEFAULT_DURATION_MS: u32 = 160; const CURVE_MAX_POINTS: usize = 33; const CURVE_FLATNESS_EPSILON: f32 = 0.001; const CURVE_MAX_DEPTH: u8 = 8; -const SPAWN_IN_INITIAL_SCALE_NUMERATOR: i32 = 4; -const SPAWN_IN_INITIAL_SCALE_DENOMINATOR: i32 = 5; #[derive(Copy, Clone, Debug, PartialEq)] pub enum AnimationCurve { @@ -296,7 +294,7 @@ impl AnimationState { duration_ms: u32, curve: AnimationCurve, ) -> bool { - if old == new || old.is_empty() || new.is_empty() || duration_ms == 0 { + if old == new || new.is_empty() || duration_ms == 0 { self.windows.borrow_mut().remove(&node_id); self.phased.borrow_mut().remove(&node_id); return false; @@ -420,7 +418,7 @@ impl AnimationState { return false; } let to = spawn_in_start_rect(from); - if to == from || to.is_empty() { + if to == from { return false; } let source_body_size = body_size_for_frame(from, frame_inset); @@ -690,20 +688,8 @@ impl LatchListener for AnimationTick { } pub(crate) fn spawn_in_start_rect(target: Rect) -> Rect { - fn scaled_dimension(value: i32) -> i32 { - let scaled = (value as i64 * SPAWN_IN_INITIAL_SCALE_NUMERATOR as i64 - / SPAWN_IN_INITIAL_SCALE_DENOMINATOR as i64) as i32; - scaled.clamp(1, value.max(1)) - } - - let width = scaled_dimension(target.width()); - let height = scaled_dimension(target.height()); - Rect::new_sized_saturating( - target.x1() + (target.width() - width) / 2, - target.y1() + (target.height() - height) / 2, - width, - height, - ) + let (cx, cy) = target.center(); + Rect::new_empty(cx, cy) } fn body_size_for_frame(rect: Rect, frame_inset: i32) -> (i32, i32) { @@ -936,12 +922,9 @@ mod tests { } #[test] - fn spawn_in_start_rect_is_centered_and_non_empty() { + fn spawn_in_start_rect_is_centered_and_empty() { let target = Rect::new_sized_saturating(10, 20, 100, 50); - assert_eq!( - spawn_in_start_rect(target), - Rect::new_sized_saturating(20, 25, 80, 40) - ); + assert_eq!(spawn_in_start_rect(target), Rect::new_empty(60, 45)); } #[test] @@ -952,7 +935,7 @@ mod tests { assert!(state.set_spawn_in(id, target, None, 0, 160)); assert_eq!( state.visual_rect(id, target, 80_000_000), - Rect::new_sized_saturating(15, 23, 90, 45) + Rect::new_sized_saturating(35, 33, 50, 25) ); } } diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index f397c70e..ff7c111c 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -6,6 +6,7 @@ const MIN_SHRINK_DENOMINATOR: i32 = 4; pub struct MultiphaseRequest { pub bounds: Rect, pub windows: Vec, + pub clearance: i32, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -488,6 +489,22 @@ fn plan_forward( } } } + for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { + match plan_space_then_orthogonal_growth(request, axis) { + Ok(plan) => return Ok(plan), + Err(error) => { + record_rejection( + &mut attempted, + direction, + PlanStrategy::SpaceThenOrthogonalGrowth { axis }, + error, + ); + if error != MultiphasePlanFailure::NoPattern { + rejection.get_or_insert(error); + } + } + } + } match plan_hierarchy_ordered_axis_scales(request) { Ok(plan) => return Ok(plan), Err(error) => { @@ -534,22 +551,6 @@ fn plan_forward( } } } - for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { - match plan_space_then_orthogonal_growth(request, axis) { - Ok(plan) => return Ok(plan), - Err(error) => { - record_rejection( - &mut attempted, - direction, - PlanStrategy::SpaceThenOrthogonalGrowth { axis }, - error, - ); - if error != MultiphasePlanFailure::NoPattern { - rejection.get_or_insert(error); - } - } - } - } Err(PlanForwardFailure { reason: rejection.unwrap_or(MultiphasePlanFailure::NoPattern), attempted, @@ -750,7 +751,13 @@ fn plan_axis_crossing_lanes( request: &MultiphaseRequest, axis: PhaseAxis, ) -> Result { - if request.windows.len() != 2 { + let moving_windows: Vec<_> = request + .windows + .iter() + .copied() + .filter(|window| window.from != window.to) + .collect(); + if moving_windows.len() != 2 { return Err(MultiphasePlanFailure::NoPattern); } let orth_min = request @@ -765,7 +772,7 @@ fn plan_axis_crossing_lanes( .map(|window| orth_end(window.from, axis)) .max() .ok_or(MultiphasePlanFailure::NoPattern)?; - if request.windows.iter().any(|window| { + if moving_windows.iter().any(|window| { main_size(window.from, axis) != main_size(window.to, axis) || orth_start(window.from, axis) != orth_min || orth_end(window.from, axis) != orth_max @@ -775,7 +782,18 @@ fn plan_axis_crossing_lanes( }) { return Err(MultiphasePlanFailure::NoPattern); } - let lane_size = (orth_max - orth_min) / request.windows.len() as i32; + let clearance = request.clearance.max(0); + let lane_count = moving_windows.len() as i32; + let available = (orth_max - orth_min) - clearance.saturating_mul(lane_count - 1); + if available <= 0 { + return Err(MultiphasePlanFailure::ShrinkBound { + axis: axis.other(), + available: 0, + required: sane_min_size(orth_max - orth_min), + }); + } + let lane_size = available / lane_count; + let mut lane_remainder = available % lane_count; let required = sane_min_size(orth_max - orth_min); if lane_size < required { return Err(MultiphasePlanFailure::ShrinkBound { @@ -785,7 +803,7 @@ fn plan_axis_crossing_lanes( }); } - let mut windows = request.windows.clone(); + let mut windows = moving_windows; 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) @@ -795,13 +813,15 @@ fn plan_axis_crossing_lanes( let mut phase1 = vec![]; let mut phase2 = vec![]; let mut phase3 = vec![]; + let mut lane_start = orth_min; for (idx, window) in windows.iter().enumerate() { - let lane_start = orth_min + lane_size * idx as i32; - let lane_end = if idx + 1 == windows.len() { - orth_max + let extra = if lane_remainder > 0 { + lane_remainder -= 1; + 1 } else { - lane_start + lane_size + 0 }; + let lane_end = lane_start + lane_size + extra; let lane_from = with_orth_interval(window.from, axis, lane_start, lane_end); let lane_to = with_main_interval( lane_from, @@ -812,6 +832,9 @@ fn plan_axis_crossing_lanes( push_step(&mut phase1, window.node_id, window.from, lane_from); push_step(&mut phase2, window.node_id, lane_from, lane_to); push_step(&mut phase3, window.node_id, lane_to, window.to); + if idx + 1 < windows.len() { + lane_start = lane_end + clearance; + } } build_validated_plan( request, @@ -882,6 +905,7 @@ fn plan_space_then_orthogonal_growth( || main_end(window.from, axis) != main_end(window.to, axis); let orth_changes = orth_start(window.from, axis) != orth_start(window.to, axis) || orth_end(window.from, axis) != orth_end(window.to, axis); + let mut orth_from = window.from; if main_changes && main_size(window.from, axis) == main_size(window.to, axis) { let after_move = with_main_interval( window.from, @@ -890,22 +914,50 @@ fn plan_space_then_orthogonal_growth( main_end(window.to, axis), ); push_step(&mut phase2, window.node_id, window.from, after_move); - if orth_changes { - push_step(&mut phase3, window.node_id, after_move, window.to); - } + orth_from = after_move; } else if main_changes { - let after_main_scale = with_main_interval( - window.from, - axis, - main_start(window.to, axis), - main_end(window.to, axis), - ); + let target_size = main_size(window.to, axis); + let after_main_scale = if main_start(window.from, axis) == main_start(window.to, axis) + || main_end(window.from, axis) == main_end(window.to, axis) + { + with_main_interval( + window.from, + axis, + main_start(window.to, axis), + main_end(window.to, axis), + ) + } else if main_start(window.to, axis) < main_start(window.from, axis) { + with_main_interval( + window.from, + axis, + main_end(window.from, axis) - target_size, + main_end(window.from, axis), + ) + } else { + with_main_interval( + window.from, + axis, + main_start(window.from, axis), + main_start(window.from, axis) + target_size, + ) + }; push_step(&mut phase1, window.node_id, window.from, after_main_scale); - if orth_changes { - push_step(&mut phase3, window.node_id, after_main_scale, window.to); + orth_from = after_main_scale; + if main_start(after_main_scale, axis) != main_start(window.to, axis) + || main_end(after_main_scale, axis) != main_end(window.to, axis) + { + let after_move = with_main_interval( + after_main_scale, + axis, + main_start(window.to, axis), + main_end(window.to, axis), + ); + push_step(&mut phase2, window.node_id, after_main_scale, after_move); + orth_from = after_move; } - } else if orth_changes { - push_step(&mut phase3, window.node_id, window.from, window.to); + } + if orth_changes { + push_step(&mut phase3, window.node_id, orth_from, window.to); } } if phase1.is_empty() || phase2.is_empty() || phase3.is_empty() { @@ -1372,6 +1424,7 @@ fn single_action_reason(action: PhaseAction) -> PhaseReason { fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest { MultiphaseRequest { bounds: request.bounds, + clearance: request.clearance, windows: request .windows .iter() @@ -1686,7 +1739,11 @@ mod tests { MultiphaseWindowHierarchy::new(old_leaf.hierarchy, new_leaf.hierarchy), )); } - MultiphaseRequest { bounds, windows } + MultiphaseRequest { + bounds, + windows, + clearance: 0, + } } fn assert_generated_case_plans(old: &TestTree, new: &TestTree, bounds: Rect) { @@ -1739,7 +1796,11 @@ mod tests { .map(|window| window.from.union(window.to)) .reduce(|bounds, rect| bounds.union(rect)) .unwrap_or_else(|| rect(0, 0, 1, 1)); - MultiphaseRequest { bounds, windows } + MultiphaseRequest { + bounds, + windows, + clearance: 0, + } } fn actions(plan: &MultiphasePlan) -> Vec { @@ -1765,6 +1826,20 @@ mod tests { strategy: PlanStrategy::SingleAction, reason: MultiphasePlanFailure::NoPattern, }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Horizontal, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Vertical, + }, + reason: MultiphasePlanFailure::NoPattern, + }, RejectedStrategy { direction, strategy: PlanStrategy::HierarchyOrderedScales, @@ -1798,20 +1873,6 @@ mod tests { }, reason: MultiphasePlanFailure::NoPattern, }, - RejectedStrategy { - direction, - strategy: PlanStrategy::SpaceThenOrthogonalGrowth { - axis: PhaseAxis::Horizontal, - }, - reason: MultiphasePlanFailure::NoPattern, - }, - RejectedStrategy { - direction, - strategy: PlanStrategy::SpaceThenOrthogonalGrowth { - axis: PhaseAxis::Vertical, - }, - reason: MultiphasePlanFailure::NoPattern, - }, ] } @@ -1916,6 +1977,38 @@ mod tests { assert!(validate_plan_continuous(&req, &plan)); } + #[test] + fn swap_lanes_respect_requested_clearance() { + let mut req = request(vec![ + window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), + window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)), + ]); + req.clearance = 10; + + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!(step_to(&plan, 0, id(1)), rect(0, 0, 100, 45)); + assert_eq!(step_to(&plan, 0, id(2)), rect(100, 55, 200, 100)); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn swap_lanes_tolerate_stationary_siblings_in_request() { + let req = request(vec![ + window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), + window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)), + window(3, rect(200, 0, 300, 100), rect(200, 0, 300, 100)), + ]); + + let planned = plan_no_overlap_explained(&req).unwrap(); + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + #[test] fn vertical_swap_lanes_follow_motion_direction_not_node_id() { let req = request(vec![ @@ -2232,6 +2325,59 @@ mod tests { assert_generated_case_plans(&vertical_new, &vertical_old, rect(0, 0, 100, 400)); } + #[test] + fn stack_extraction_with_resized_moving_child_still_moves_before_growth() { + let old = split( + 10, + PhaseAxis::Horizontal, + &[1, 1], + vec![ + leaf(1), + split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ); + let new = split( + 10, + PhaseAxis::Horizontal, + &[1, 1, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + let req = generated_request(&old, &new, rect(0, 0, 300, 120)); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 100, 120)); + assert_eq!(step_to(plan, 0, id(2)), rect(200, 0, 300, 60)); + assert_eq!(step_to(plan, 0, id(3)), rect(200, 60, 300, 120)); + assert_eq!(step_to(plan, 1, id(2)), rect(100, 0, 200, 60)); + assert_eq!(step_to(plan, 2, id(2)), rect(100, 0, 200, 120)); + assert_eq!(step_to(plan, 2, id(3)), rect(200, 0, 300, 120)); + assert!(validate_plan_continuous(&req, plan)); + } + #[test] fn bounded_generated_supported_split_tree_corpus_is_deterministic() { let mut cases = vec![]; @@ -2543,6 +2689,7 @@ mod tests { fn diagnostics_report_shrink_bound_rejections() { let req = MultiphaseRequest { bounds: rect(0, 0, 400, 100), + clearance: 0, windows: vec![ MultiphaseWindow { node_id: id(1), diff --git a/src/state.rs b/src/state.rs index 98ad8cfe..c21b575d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1650,6 +1650,12 @@ impl State { } } + 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], @@ -1671,6 +1677,7 @@ impl State { let request = MultiphaseRequest { bounds, windows: request_windows, + clearance: self.layout_animation_clearance(), }; let plan = match plan_no_overlap_with_diagnostics(&request) { Ok(plan) => plan,