diff --git a/src/state.rs b/src/state.rs index 607ffa6b..42dd909d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,7 +7,8 @@ use { RetainedToplevel, expand_damage_rect, multiphase::{ - MultiphaseRequest, MultiphaseWindow, MultiphaseWindowHierarchy, + MultiphasePhase, MultiphasePlan, MultiphasePlanFailure, MultiphaseRequest, + MultiphaseWindow, MultiphaseWindowHierarchy, partition_motion_groups, plan_no_overlap_with_diagnostics, validate_phase_paths, }, spawn_in_start_rect, @@ -205,6 +206,56 @@ fn layout_animation_group_uses_plain( .any(|&idx| candidates[idx].style == AnimationStyle::Plain) } +fn bridged_retarget_plan( + request: &MultiphaseRequest, + candidates: &[LayoutAnimationCandidate], + group: &[usize], + bridge_paths: &[Vec<(Rect, Rect)>], + bridge_phase_count: usize, + follow_phases: &[MultiphasePhase], +) -> Result { + let mut paths = vec![]; + for (group_pos, &idx) in group.iter().enumerate() { + let candidate = &candidates[idx]; + let window = request.windows[group_pos]; + let Some(bridge_path) = bridge_paths.get(group_pos) else { + return Err(MultiphasePlanFailure::NoPattern); + }; + let mut path = bridge_path.clone(); + let mut current = path + .last() + .map(|(_, to)| *to) + .unwrap_or(window.from); + while path.len() < bridge_phase_count { + path.push((current, current)); + } + if current != candidate.old { + return Err(MultiphasePlanFailure::NoPattern); + } + for phase in follow_phases { + match phase + .steps + .iter() + .find(|step| step.node_id == candidate.node_id) + { + Some(step) => { + if step.from != current { + return Err(MultiphasePlanFailure::NoPattern); + } + path.push((step.from, step.to)); + current = step.to; + } + None => path.push((current, current)), + } + } + if current != window.to { + return Err(MultiphasePlanFailure::NoPattern); + } + paths.push(path); + } + validate_phase_paths(request, &paths) +} + pub struct State { pub pid: c::pid_t, pub kb_ctx: KbvmContext, @@ -1721,6 +1772,9 @@ impl State { if self.start_existing_phased_retarget(candidates, windows, group, &request, now_nsec) { return true; } + if self.start_bridged_phased_retarget(candidates, windows, group, &request, now_nsec) { + return true; + } let plan = match plan_no_overlap_with_diagnostics(&request) { Ok(plan) => plan, Err(diagnostic) => { @@ -1770,6 +1824,97 @@ impl State { self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec) } + fn start_bridged_phased_retarget( + self: &Rc, + candidates: &[LayoutAnimationCandidate], + windows: &[MultiphaseWindow], + group: &[usize], + request: &MultiphaseRequest, + now_nsec: u64, + ) -> bool { + let mut bridge_paths = vec![]; + let mut bridge_phase_count = 0; + let mut has_bridge = false; + for &idx in group { + let candidate = &candidates[idx]; + let window = windows[idx]; + if window.from == candidate.old { + bridge_paths.push(vec![]); + continue; + } + let Some(path) = + self.animations + .phased_route_to(candidate.node_id, candidate.old, now_nsec) + else { + return false; + }; + if !path.is_empty() { + has_bridge = true; + bridge_phase_count = bridge_phase_count.max(path.len()); + } + bridge_paths.push(path); + } + if !has_bridge { + return false; + } + + let settled_windows: Vec<_> = group + .iter() + .map(|&idx| { + let candidate = &candidates[idx]; + MultiphaseWindow::with_hierarchy( + candidate.node_id, + candidate.old, + candidate.new, + candidate.hierarchy, + ) + }) + .collect(); + let Some(first) = settled_windows.first() else { + return false; + }; + let mut bounds = first.from.union(first.to); + for window in &settled_windows[1..] { + bounds = bounds.union(window.from).union(window.to); + } + let settled_request = MultiphaseRequest { + bounds, + windows: settled_windows, + clearance: self.layout_animation_clearance(), + }; + let follow_plan = match plan_no_overlap_with_diagnostics(&settled_request) { + Ok(plan) => plan, + Err(diagnostic) => { + log::debug!( + "bridged phased retarget follow-up rejected for group {:?}: {:?}", + group, + diagnostic + ); + return false; + } + }; + let plan = match bridged_retarget_plan( + request, + candidates, + group, + &bridge_paths, + bridge_phase_count, + &follow_plan.phases, + ) { + Ok(plan) => plan, + Err(error) => { + log::debug!( + "bridged phased retarget rejected for group {:?}: {:?}", + group, + error + ); + return false; + } + }; + log::debug!("bridging active phased animation for group {:?}", group); + self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec) + } + fn start_multiphase_plan( self: &Rc, candidates: &[LayoutAnimationCandidate], @@ -2512,10 +2657,24 @@ mod tests { } fn candidate(node_id: u32, style: AnimationStyle) -> LayoutAnimationCandidate { + candidate_rects( + node_id, + rect(0, 0, 100, 100), + rect(100, 0, 200, 100), + style, + ) + } + + fn candidate_rects( + node_id: u32, + old: Rect, + new: Rect, + style: AnimationStyle, + ) -> LayoutAnimationCandidate { LayoutAnimationCandidate { node_id: NodeId(node_id), - old: rect(0, 0, 100, 100), - new: rect(100, 0, 200, 100), + old, + new, curve: AnimationCurve::Linear, style, hierarchy: MultiphaseWindowHierarchy::default(), @@ -2533,6 +2692,57 @@ mod tests { assert!(layout_animation_group_uses_plain(&candidates, &[0, 1])); } + #[test] + fn bridged_retarget_handles_second_rotation_interrupt() { + let a_left = rect(0, 0, 100, 100); + let c_mid = rect(100, 0, 200, 100); + let c_left = a_left; + let a_mid = c_mid; + let c_current = rect(150, 50, 250, 100); + let c_mid_lane = rect(100, 50, 200, 100); + let candidates = vec![ + candidate_rects(1, a_left, a_mid, AnimationStyle::Multiphase), + candidate_rects(3, c_mid, c_left, AnimationStyle::Multiphase), + ]; + let request = MultiphaseRequest { + bounds: rect(0, 0, 250, 100), + windows: vec![ + MultiphaseWindow::new(NodeId(1), a_left, a_mid), + MultiphaseWindow::new(NodeId(3), c_current, c_left), + ], + clearance: 0, + }; + let settled_request = MultiphaseRequest { + bounds: rect(0, 0, 200, 100), + windows: vec![ + MultiphaseWindow::new(NodeId(1), a_left, a_mid), + MultiphaseWindow::new(NodeId(3), c_mid, c_left), + ], + clearance: 0, + }; + let follow_plan = plan_no_overlap_with_diagnostics(&settled_request).unwrap(); + let bridge_paths = vec![vec![], vec![(c_current, c_mid_lane), (c_mid_lane, c_mid)]]; + + let plan = bridged_retarget_plan( + &request, + &candidates, + &[0, 1], + &bridge_paths, + 2, + &follow_plan.phases, + ) + .unwrap(); + + assert!(plan + .phases + .iter() + .any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(1)))); + assert!(plan + .phases + .iter() + .any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(3)))); + } + #[test] fn layout_animation_candidates_coalesce_duplicate_nodes() { let source = MultiphaseHierarchyPosition {