From 502a93a00a8f879f7b87cf0d69c3f8875a2b1d23 Mon Sep 17 00:00:00 2001 From: atagen Date: Wed, 27 May 2026 22:08:09 +1000 Subject: [PATCH] Use live content for normal animations --- src/animation.rs | 49 +++++++++++++++++--- src/animation/multiphase.rs | 92 +++++++++++++++++++++++++++++++------ src/state.rs | 25 +++------- src/tree/float.rs | 6 +-- src/tree/toplevel.rs | 14 ++---- 5 files changed, 134 insertions(+), 52 deletions(-) diff --git a/src/animation.rs b/src/animation.rs index f0721562..d31ec6a5 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -290,7 +290,7 @@ impl AnimationState { node_id: NodeId, old: Rect, new: Rect, - retained: Option>, + _retained: Option>, now_nsec: u64, duration_ms: u32, curve: AnimationCurve, @@ -302,7 +302,6 @@ impl AnimationState { } let duration_nsec = duration_ms as u64 * 1_000_000; let mut from = old; - let mut retained = retained; { let phased = self.phased.borrow(); if let Some(anim) = phased.get(&node_id) { @@ -310,7 +309,6 @@ impl AnimationState { return false; } from = anim.rect_at(now_nsec); - retained = anim.retained.clone().or(retained); } } { @@ -320,7 +318,6 @@ impl AnimationState { return false; } from = anim.rect_at(now_nsec); - retained = anim.retained.clone().or(retained); } } if from == new { @@ -338,7 +335,7 @@ impl AnimationState { duration_nsec, curve, last_damage: from, - retained, + retained: None, }, ); true @@ -348,7 +345,7 @@ impl AnimationState { &self, node_id: NodeId, phases: Vec<(Rect, Rect)>, - retained: Option>, + _retained: Option>, now_nsec: u64, duration_ms: u32, curve: AnimationCurve, @@ -388,7 +385,7 @@ impl AnimationState { last_damage: from, final_rect, route_edges, - retained, + retained: None, }, ); true @@ -994,6 +991,44 @@ mod tests { assert!(state.exit_frames(160_000_000).is_empty()); } + #[test] + fn normal_window_animations_do_not_retain_content() { + let state = AnimationState::default(); + let id = NodeId(1); + let from = Rect::new_sized_saturating(0, 0, 100, 100); + let to = Rect::new_sized_saturating(100, 0, 100, 100); + assert!(state.set_target( + id, + from, + to, + Some(retained_for_tests()), + 0, + 160, + AnimationCurve::Linear + )); + + assert!(state.retained_snapshot(id, 80_000_000).is_none()); + } + + #[test] + fn phased_window_animations_do_not_retain_content() { + let state = AnimationState::default(); + let id = NodeId(1); + let a = Rect::new_sized_saturating(0, 0, 100, 100); + let b = Rect::new_sized_saturating(0, 0, 100, 50); + let c = Rect::new_sized_saturating(100, 0, 100, 50); + assert!(state.set_phased_target( + id, + vec![(a, b), (b, c)], + Some(retained_for_tests()), + 0, + 100, + AnimationCurve::Linear + )); + + assert!(state.retained_snapshot(id, 50_000_000).is_none()); + } + #[test] fn phased_animation_uses_full_duration_per_phase() { let state = AnimationState::default(); diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index 31c406b5..1b4e83b8 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -809,7 +809,7 @@ fn plan_axis_crossing_lanes( .copied() .filter(|window| window.from != window.to) .collect(); - if moving_windows.len() != 2 { + if moving_windows.len() < 2 { return Err(MultiphasePlanFailure::NoPattern); } let orth_min = request @@ -855,12 +855,7 @@ fn plan_axis_crossing_lanes( } 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) - }) { - return Err(MultiphasePlanFailure::NoPattern); - } + windows.sort_by_key(|window| lane_sort_key(*window, axis)); let mut phase1 = vec![]; let mut phase2 = vec![]; let mut phase3 = vec![]; @@ -935,13 +930,19 @@ fn crossing_lane_move_rect(from: Rect, target: Rect, axis: PhaseAxis) -> Rect { } } -fn lane_index_for_direction(window: MultiphaseWindow, axis: PhaseAxis) -> Option { +fn lane_sort_key(window: MultiphaseWindow, axis: PhaseAxis) -> (usize, i32, i32, u32) { 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, - } + let direction = match delta.cmp(&0) { + std::cmp::Ordering::Greater => 0, + std::cmp::Ordering::Less => 1, + std::cmp::Ordering::Equal => 2, + }; + ( + direction, + main_start(window.from, axis), + main_start(window.to, axis), + window.node_id.0, + ) } fn plan_space_then_orthogonal_growth( @@ -2151,6 +2152,41 @@ mod tests { assert!(validate_plan_continuous(&req, &planned.plan)); } + #[test] + fn horizontal_rotation_uses_crossing_lanes() { + let req = request(vec![ + window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), + window(2, rect(100, 0, 200, 100), rect(200, 0, 300, 100)), + window(3, rect(200, 0, 300, 100), rect(0, 0, 100, 100)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(&planned.plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + #[test] fn vertical_swap_lanes_follow_motion_direction_not_node_id() { let req = request(vec![ @@ -2875,6 +2911,36 @@ mod tests { assert!(validate_plan_continuous(&req, &plan)); } + #[test] + fn vertical_stack_extraction_with_clearance_still_plans() { + let old = split( + 20, + PhaseAxis::Vertical, + &[1, 1], + vec![ + leaf(1), + split(21, PhaseAxis::Horizontal, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ); + let new = split( + 20, + PhaseAxis::Vertical, + &[1, 2, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + let mut req = generated_request(&old, &new, rect(0, 0, 100, 400)); + req.clearance = 10; + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Vertical, + } + ); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + #[test] fn vertical_stack_extraction_reverse_replays_phases_in_reverse() { let req = request(vec![ diff --git a/src/state.rs b/src/state.rs index b32cfd30..bf806640 100644 --- a/src/state.rs +++ b/src/state.rs @@ -167,7 +167,6 @@ pub(crate) struct LayoutAnimationCandidate { node_id: NodeId, old: Rect, new: Rect, - retained: Option>, curve: AnimationCurve, hierarchy: MultiphaseWindowHierarchy, } @@ -1503,7 +1502,6 @@ impl State { node_id: NodeId, old: Rect, new: Rect, - retained: Option>, ) { let curve = self .layout_animation_curve_override @@ -1513,7 +1511,6 @@ impl State { node_id, old, new, - retained, curve, MultiphaseWindowHierarchy::default(), ); @@ -1524,14 +1521,13 @@ impl State { 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); + self.queue_layout_animation(node_id, old, new, curve, hierarchy); } pub fn queue_linear_layout_animation( @@ -1539,13 +1535,11 @@ impl State { node_id: NodeId, old: Rect, new: Rect, - retained: Option>, ) { self.queue_layout_animation( node_id, old, new, - retained, AnimationCurve::Linear, MultiphaseWindowHierarchy::default(), ); @@ -1556,7 +1550,6 @@ impl State { node_id: NodeId, old: Rect, new: Rect, - retained: Option>, curve: AnimationCurve, hierarchy: MultiphaseWindowHierarchy, ) { @@ -1583,7 +1576,6 @@ impl State { node_id, old, new, - retained, curve, hierarchy, }; @@ -1603,7 +1595,7 @@ impl State { candidate.node_id, candidate.old, candidate.new, - candidate.retained, + None, now_nsec, self.animations.duration_ms.get(), candidate.curve, @@ -1763,18 +1755,14 @@ impl State { if current != window.to { return false; } - let retained = self - .animations - .retained_snapshot(candidate.node_id, now_nsec) - .or_else(|| candidate.retained.clone()); - entries.push((candidate.clone(), phases, damage, retained)); + entries.push((candidate.clone(), phases, damage)); } let mut started_any = false; - for (candidate, phases, damage, retained) in entries { + for (candidate, phases, damage) in entries { if self.animations.set_phased_target( candidate.node_id, phases, - retained, + None, now_nsec, self.animations.duration_ms.get(), candidate.curve, @@ -1796,7 +1784,6 @@ impl State { self: &Rc, node_id: NodeId, target: Rect, - retained: Option>, ) { if !self.animations.enabled.get() || target.is_empty() { return; @@ -1806,7 +1793,7 @@ impl State { let started = self.animations.set_spawn_in( node_id, target, - retained, + None, now, self.animations.duration_ms.get(), self.animations.curve.get(), diff --git a/src/tree/float.rs b/src/tree/float.rs index f2f96681..a57c2b91 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -161,8 +161,7 @@ impl FloatNode { data.spawn_in_pending.get() && data.kind.is_app_window() && !data.is_fullscreen.get() }; if spawn_in_pending && self.visible.get() { - self.state - .queue_spawn_in_animation(self.id.into(), pos, None); + self.state.queue_spawn_in_animation(self.id.into(), pos); } let theme = &self.state.theme; let bw = theme.sizes.border_width.get(); @@ -401,7 +400,7 @@ impl FloatNode { fn queue_position_animation(&self, old_pos: Rect, new_pos: Rect) { self.state .clone() - .queue_tiled_animation(self.id.into(), old_pos, new_pos, None); + .queue_tiled_animation(self.id.into(), old_pos, new_pos); let Some(child) = self.child.get() else { return; }; @@ -409,7 +408,6 @@ impl FloatNode { child.node_id(), self.body_for_outer(old_pos), self.body_for_outer(new_pos), - child.tl_animation_snapshot(), ); } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 5c8c1351..312b4ac6 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -232,16 +232,13 @@ impl ToplevelNode for T { data.node_id, prev, *rect, - self.tl_animation_snapshot(), hierarchy, ); } if spawn_in_eligible { - data.state.clone().queue_spawn_in_animation( - data.node_id, - *rect, - self.tl_animation_snapshot(), - ); + data.state + .clone() + .queue_spawn_in_animation(data.node_id, *rect); } if spawn_in_eligible { data.spawn_in_pending.set(false); @@ -1273,14 +1270,13 @@ pub fn toplevel_set_floating(state: &Rc, tl: Rc, floati .animations .visual_rect(node_id, tl.node_absolute_position(), state.now_nsec()); let old_outer = float_outer_for_body(state, old_body); - let retained = tl.tl_animation_snapshot(); parent.cnode_remove_child2(&*tl, true); let (width, height) = data.float_size(&ws); let floater = state.map_floating(tl, width, height, &ws, None); let new_outer = floater.position.get(); let new_body = float_body_for_outer(state, new_outer); - state.queue_linear_layout_animation(floater.node_id(), old_outer, new_outer, None); - state.queue_linear_layout_animation(node_id, old_body, new_body, retained); + state.queue_linear_layout_animation(floater.node_id(), old_outer, new_outer); + state.queue_linear_layout_animation(node_id, old_body, new_body); } }