use {crate::rect::Rect, crate::tree::NodeId}; const MIN_SHRINK_DENOMINATOR: i32 = 8; #[derive(Clone, Debug)] pub struct MultiphaseRequest { pub bounds: Rect, pub windows: Vec, pub clearance: i32, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct MultiphaseWindow { pub node_id: NodeId, pub from: Rect, pub to: Rect, pub hierarchy: MultiphaseWindowHierarchy, } impl MultiphaseWindow { pub fn new(node_id: NodeId, from: Rect, to: Rect) -> Self { Self { node_id, from, to, hierarchy: Default::default(), } } pub fn with_hierarchy( node_id: NodeId, from: Rect, to: Rect, hierarchy: MultiphaseWindowHierarchy, ) -> Self { Self { node_id, from, to, hierarchy, } } } #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct MultiphaseWindowHierarchy { pub source: MultiphaseHierarchyPosition, pub target: MultiphaseHierarchyPosition, pub transition: MultiphaseHierarchyTransition, } impl MultiphaseWindowHierarchy { pub fn new(source: MultiphaseHierarchyPosition, target: MultiphaseHierarchyPosition) -> Self { let transition = if !source.parent_is_mono && target.parent_is_mono { MultiphaseHierarchyTransition::EnteringMono } else if source.parent_is_mono && !target.parent_is_mono { MultiphaseHierarchyTransition::ExitingMono } else if source.parent.is_none() || target.parent.is_none() { MultiphaseHierarchyTransition::Unknown } else if target.depth < source.depth { MultiphaseHierarchyTransition::Ascending } else if target.depth > source.depth { MultiphaseHierarchyTransition::Descending } else { MultiphaseHierarchyTransition::SameLevel }; Self { source, target, transition, } } fn reversed(self) -> Self { Self { source: self.target, target: self.source, transition: self.transition.reversed(), } } } #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct MultiphaseHierarchyPosition { pub parent: Option, pub depth: u16, pub sibling_index: Option, pub split_axis: Option, pub nearest_horizontal_split_depth: Option, pub nearest_vertical_split_depth: Option, pub parent_is_mono: bool, pub mono_active: bool, } #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub enum MultiphaseHierarchyTransition { #[default] Unknown, SameLevel, Ascending, Descending, EnteringMono, ExitingMono, } impl MultiphaseHierarchyTransition { fn reversed(self) -> Self { match self { Self::Unknown => Self::Unknown, Self::SameLevel => Self::SameLevel, Self::Ascending => Self::Descending, Self::Descending => Self::Ascending, Self::EnteringMono => Self::ExitingMono, Self::ExitingMono => Self::EnteringMono, } } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MultiphasePlan { pub phases: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MultiphasePlanned { pub plan: MultiphasePlan, pub explanation: MultiphasePlanExplanation, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MultiphasePlanExplanation { pub strategy: PlanStrategy, pub phases: Vec, pub validation: ValidationExplanation, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct PhaseExplanation { pub action: MultiphasePhaseAction, pub reason: PhaseReason, pub nodes: Vec, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct ValidationExplanation { pub continuous_overlap_passed: bool, pub final_rects_matched: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum PlanStrategy { NoOp, SingleAction, MixedSinglePhase, HierarchyOrderedScales, OrientationChange { from_axis: PhaseAxis }, SwapLanes { axis: PhaseAxis }, SpaceThenOrthogonalGrowth { axis: PhaseAxis }, ReversedForwardPlan { original: Box }, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum PlanDirection { Forward, Reverse, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct RejectedStrategy { pub direction: PlanDirection, pub strategy: PlanStrategy, pub reason: MultiphasePlanFailure, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum PhaseReason { SingleAction, SameAxisRedistribution, MixedAxisActions, ShrinkIntoLanes { lane_axis: PhaseAxis, }, MoveThroughFreedSpace, GrowOutOfLanes, CreateSpaceForAscendingChild, MoveAscendingChildAfterSpaceExists, OrthogonalGrowthAfterMove, ParentAxisBeforeChildAxis { parent_axis: PhaseAxis, parent_depth: u16, child_axis: PhaseAxis, child_depth: u16, }, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MultiphasePhase { pub action: MultiphasePhaseAction, pub steps: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum MultiphasePhaseAction { Uniform(PhaseAction), Mixed(Vec), } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct MultiphaseStep { pub node_id: NodeId, pub from: Rect, pub to: Rect, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct PhaseAction { pub kind: PhaseKind, pub axis: PhaseAxis, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum PhaseKind { Move, Scale, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum PhaseAxis { Horizontal, Vertical, } impl MultiphasePhaseAction { fn from_step_actions(actions: Vec) -> Self { debug_assert!(!actions.is_empty()); let first = actions[0]; if actions.iter().all(|action| *action == first) { Self::Uniform(first) } else { Self::Mixed(actions) } } fn action_for_step(&self, idx: usize) -> Option { match self { Self::Uniform(action) => Some(*action), Self::Mixed(actions) => actions.get(idx).copied(), } } fn as_uniform(&self) -> Option { match self { Self::Uniform(action) => Some(*action), Self::Mixed(_) => None, } } } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum MultiphaseError { EmptyBounds, EmptyWindow, DuplicateWindow, InitialOverlap, FinalOverlap, NoPlan, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MultiphasePlanDiagnostic { pub forward: MultiphasePlanFailure, pub reverse: Option, pub attempted: Vec, } impl MultiphasePlanDiagnostic { fn legacy_error(self) -> MultiphaseError { match self.forward { MultiphasePlanFailure::Request(error) => error, _ => MultiphaseError::NoPlan, } } } impl ValidationExplanation { fn passed() -> Self { Self { continuous_overlap_passed: true, final_rects_matched: true, } } } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum MultiphasePlanFailure { Request(MultiphaseError), NoPattern, ShrinkBound { axis: PhaseAxis, available: i32, required: i32, }, InvalidPhaseStep { action: PhaseAction, node_id: NodeId, }, Validation(MultiphaseValidationError), } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum MultiphaseValidationError { DuplicatePhaseStep { phase: usize, node_id: NodeId, }, PhaseActionCount { phase: usize, actions: usize, steps: usize, }, UnknownPhaseStep { phase: usize, node_id: NodeId, }, StaleStepStart { phase: usize, node_id: NodeId, }, PhaseOverlap { phase: usize, a: NodeId, b: NodeId, }, FinalMismatch { node_id: NodeId, }, } #[derive(Clone, Debug, Eq, PartialEq)] struct PlanForwardFailure { reason: MultiphasePlanFailure, attempted: Vec, } pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result { plan_no_overlap_with_diagnostics(request).map_err(|diagnostic| diagnostic.legacy_error()) } pub fn plan_no_overlap_with_diagnostics( request: &MultiphaseRequest, ) -> Result { plan_no_overlap_explained(request).map(|planned| planned.plan) } pub fn plan_no_overlap_explained( request: &MultiphaseRequest, ) -> Result { if let Err(error) = validate_request(request) { return Err(MultiphasePlanDiagnostic { forward: MultiphasePlanFailure::Request(error), reverse: None, attempted: vec![], }); } if request .windows .iter() .all(|window| window.from == window.to) { return Ok(MultiphasePlanned { plan: MultiphasePlan { phases: vec![] }, explanation: MultiphasePlanExplanation { strategy: PlanStrategy::NoOp, phases: vec![], validation: ValidationExplanation::passed(), }, }); } if let Some(failure) = target_shrink_bound_failure(request) { return Err(MultiphasePlanDiagnostic { forward: failure, reverse: None, attempted: vec![], }); } let forward = match plan_forward(request, PlanDirection::Forward) { Ok(plan) => return Ok(plan), Err(error) => error, }; let reversed = reverse_request(request); match plan_forward(&reversed, PlanDirection::Reverse) { Ok(plan) => Ok(reverse_planned(plan)), Err(reverse) => { let mut attempted = forward.attempted; attempted.extend(reverse.attempted); Err(MultiphasePlanDiagnostic { forward: forward.reason, reverse: Some(reverse.reason), attempted, }) } } } pub(crate) fn validate_phase_paths( request: &MultiphaseRequest, paths: &[Vec<(Rect, Rect)>], ) -> Result { if paths.len() != request.windows.len() { return Err(MultiphasePlanFailure::NoPattern); } let phase_count = paths.iter().map(Vec::len).max().unwrap_or(0); if phase_count == 0 { return Err(MultiphasePlanFailure::NoPattern); } let mut phases = vec![]; for phase_idx in 0..phase_count { let mut steps = vec![]; let mut actions = vec![]; for (window_idx, path) in paths.iter().enumerate() { let Some((from, to)) = path.get(phase_idx).copied() else { continue; }; if from == to { continue; } let step = MultiphaseStep { node_id: request.windows[window_idx].node_id, from, to, }; let Some(action) = classify_step(step) else { return Err(MultiphasePlanFailure::NoPattern); }; steps.push(step); actions.push(action); } if !steps.is_empty() { phases.push(MultiphasePhase { action: MultiphasePhaseAction::from_step_actions(actions), steps, }); } } let plan = MultiphasePlan { phases }; validate_plan_continuous_diagnostic(request, &plan) .map(|_| plan) .map_err(MultiphasePlanFailure::Validation) } pub(crate) fn partition_motion_groups( windows: &[MultiphaseWindow], clearance: i32, ) -> Vec> { let clearance = clearance.max(0); let mut groups = vec![]; let mut seen = vec![false; windows.len()]; for start in 0..windows.len() { if seen[start] { continue; } seen[start] = true; let mut group = vec![]; let mut pending = vec![start]; while let Some(idx) = pending.pop() { group.push(idx); let bounds = motion_bounds_with_clearance(windows[idx], clearance); for other in 0..windows.len() { if seen[other] || !bounds.intersects(&motion_bounds_with_clearance(windows[other], clearance)) { continue; } seen[other] = true; pending.push(other); } } group.sort_unstable(); groups.push(group); } groups } fn validate_request(request: &MultiphaseRequest) -> Result<(), MultiphaseError> { if request.bounds.is_empty() { return Err(MultiphaseError::EmptyBounds); } for (idx, window) in request.windows.iter().enumerate() { if window.from.is_empty() || window.to.is_empty() { return Err(MultiphaseError::EmptyWindow); } for other in &request.windows[..idx] { if other.node_id == window.node_id { return Err(MultiphaseError::DuplicateWindow); } } } if overlaps(request.windows.iter().map(|window| window.from)) { return Err(MultiphaseError::InitialOverlap); } if overlaps(request.windows.iter().map(|window| window.to)) { return Err(MultiphaseError::FinalOverlap); } Ok(()) } fn target_shrink_bound_failure(request: &MultiphaseRequest) -> Option { let min_width = sane_min_size(request.bounds.width()); let min_height = sane_min_size(request.bounds.height()); for window in &request.windows { if window.to.width() < min_width { return Some(MultiphasePlanFailure::ShrinkBound { axis: PhaseAxis::Horizontal, available: window.to.width(), required: min_width, }); } if window.to.height() < min_height { return Some(MultiphasePlanFailure::ShrinkBound { axis: PhaseAxis::Vertical, available: window.to.height(), required: min_height, }); } } None } fn plan_forward( request: &MultiphaseRequest, direction: PlanDirection, ) -> Result { let mut rejection = None; let mut attempted = vec![]; match plan_single_action_phase(request) { Ok(plan) => return Ok(plan), Err(error) => { record_rejection(&mut attempted, direction, PlanStrategy::SingleAction, error); if error != MultiphasePlanFailure::NoPattern { rejection.get_or_insert(error); } } } 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) => { record_rejection( &mut attempted, direction, PlanStrategy::HierarchyOrderedScales, error, ); if error != MultiphasePlanFailure::NoPattern { rejection.get_or_insert(error); } } } for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { match plan_orientation_change(request, axis) { Ok(plan) => return Ok(plan), Err(error) => { record_rejection( &mut attempted, direction, PlanStrategy::OrientationChange { from_axis: axis }, error, ); if error != MultiphasePlanFailure::NoPattern { rejection.get_or_insert(error); } } } } for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { match plan_axis_crossing_lanes(request, axis) { Ok(plan) => return Ok(plan), Err(error) => { record_rejection( &mut attempted, direction, PlanStrategy::SwapLanes { axis }, error, ); if error != MultiphasePlanFailure::NoPattern { rejection.get_or_insert(error); } } } } Err(PlanForwardFailure { reason: rejection.unwrap_or(MultiphasePlanFailure::NoPattern), attempted, }) } fn record_rejection( attempted: &mut Vec, direction: PlanDirection, strategy: PlanStrategy, reason: MultiphasePlanFailure, ) { attempted.push(RejectedStrategy { direction, strategy, reason, }); } fn plan_single_action_phase( request: &MultiphaseRequest, ) -> Result { let mut uniform_action = None; let mut is_uniform = true; let mut steps = vec![]; let mut step_actions = vec![]; for window in &request.windows { if window.from == window.to { continue; } let step = MultiphaseStep { node_id: window.node_id, from: window.from, to: window.to, }; let Some(step_action) = classify_step(step) else { return Err(MultiphasePlanFailure::NoPattern); }; if step_action.kind == PhaseKind::Scale { let (available, required) = match step_action.axis { PhaseAxis::Horizontal => (window.to.width(), sane_min_size(request.bounds.width())), PhaseAxis::Vertical => (window.to.height(), sane_min_size(request.bounds.height())), }; if available < required { return Err(MultiphasePlanFailure::ShrinkBound { axis: step_action.axis, available, required, }); } } if uniform_action.is_some_and(|action| action != step_action) { is_uniform = false; } uniform_action.get_or_insert(step_action); steps.push(step); step_actions.push(step_action); } if steps.is_empty() { return Err(MultiphasePlanFailure::NoPattern); } if !is_uniform { return build_validated_plan( request, PlanStrategy::MixedSinglePhase, [phase_draft_mixed( steps, step_actions, PhaseReason::MixedAxisActions, )], ); } let action = uniform_action.unwrap(); build_validated_plan( request, PlanStrategy::SingleAction, [phase_draft_uniform( action, steps, single_action_reason(action), )], ) } fn plan_hierarchy_ordered_axis_scales( request: &MultiphaseRequest, ) -> Result { let mut changed_axes = vec![]; for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { if request .windows .iter() .any(|window| interval_changed(window.from, window.to, axis)) { changed_axes.push(axis); } } let [first_axis, second_axis] = changed_axes .try_into() .map_err(|_| MultiphasePlanFailure::NoPattern)?; let order = hierarchy_scale_axis_order(request, first_axis, second_axis) .ok_or(MultiphasePlanFailure::NoPattern)?; let mut current: Vec<_> = request .windows .iter() .map(|window| (window.node_id, window.from)) .collect(); let mut phases = vec![]; let reason = PhaseReason::ParentAxisBeforeChildAxis { parent_axis: order.axes[0], parent_depth: order.depths[0], child_axis: order.axes[1], child_depth: order.depths[1], }; for axis in order.axes { let mut steps = vec![]; for window in &request.windows { let (_, rect) = current .iter_mut() .find(|(node_id, _)| *node_id == window.node_id) .unwrap(); let next = with_main_interval( *rect, axis, main_start(window.to, axis), main_end(window.to, axis), ); if next == *rect { continue; } if main_size(*rect, axis) == main_size(next, axis) { return Err(MultiphasePlanFailure::NoPattern); } steps.push(MultiphaseStep { node_id: window.node_id, from: *rect, to: next, }); *rect = next; } if steps.is_empty() { return Err(MultiphasePlanFailure::NoPattern); } phases.push(phase_draft(PhaseKind::Scale, axis, steps, reason)); } let [first, second] = phases .try_into() .map_err(|_| MultiphasePlanFailure::NoPattern)?; build_validated_plan( request, PlanStrategy::HierarchyOrderedScales, [first, second], ) } fn hierarchy_scale_axis_order( request: &MultiphaseRequest, first_axis: PhaseAxis, second_axis: PhaseAxis, ) -> Option { let first_priority = hierarchy_axis_priority(request, first_axis)?; let second_priority = hierarchy_axis_priority(request, second_axis)?; match first_priority.cmp(&second_priority) { std::cmp::Ordering::Less => Some(HierarchyScaleAxisOrder { axes: [first_axis, second_axis], depths: [first_priority, second_priority], }), std::cmp::Ordering::Greater => Some(HierarchyScaleAxisOrder { axes: [second_axis, first_axis], depths: [second_priority, first_priority], }), std::cmp::Ordering::Equal => None, } } #[derive(Copy, Clone)] struct HierarchyScaleAxisOrder { axes: [PhaseAxis; 2], depths: [u16; 2], } fn hierarchy_axis_priority(request: &MultiphaseRequest, axis: PhaseAxis) -> Option { request .windows .iter() .filter(|window| interval_changed(window.from, window.to, axis)) .flat_map(|window| { [ split_depth_for_axis(window.hierarchy.source, axis), split_depth_for_axis(window.hierarchy.target, axis), ] }) .flatten() .min() } fn plan_axis_crossing_lanes( request: &MultiphaseRequest, axis: PhaseAxis, ) -> Result { 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 .windows .iter() .map(|window| orth_start(window.from, axis)) .min() .ok_or(MultiphasePlanFailure::NoPattern)?; let orth_max = request .windows .iter() .map(|window| orth_end(window.from, axis)) .max() .ok_or(MultiphasePlanFailure::NoPattern)?; if moving_windows.iter().any(|window| { orth_start(window.from, axis) != orth_min || orth_end(window.from, axis) != orth_max || orth_start(window.to, axis) != orth_min || orth_end(window.to, axis) != orth_max || main_start(window.from, axis) == main_start(window.to, axis) }) { return Err(MultiphasePlanFailure::NoPattern); } 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 { axis: axis.other(), available: lane_size, required, }); } let mut windows = moving_windows; windows.sort_by_key(|window| lane_sort_key(*window, axis)); let mut phase1 = vec![]; let mut phase2 = vec![]; let mut phase3 = vec![]; let mut phase4 = vec![]; let mut lane_start = orth_min; for (idx, window) in windows.iter().enumerate() { let extra = if lane_remainder > 0 { lane_remainder -= 1; 1 } else { 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, axis, main_start(window.to, axis), main_end(window.to, axis), ); let lane_move = crossing_lane_move_rect(lane_from, window.to, axis); 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); push_step(&mut phase4, window.node_id, lane_to, window.to); if idx + 1 < windows.len() { lane_start = lane_end + clearance; } } build_validated_plan( request, PlanStrategy::SwapLanes { axis }, [ phase_draft( PhaseKind::Scale, axis.other(), phase1, PhaseReason::ShrinkIntoLanes { lane_axis: axis.other(), }, ), phase_draft( PhaseKind::Move, axis, phase2, PhaseReason::MoveThroughFreedSpace, ), phase_draft( PhaseKind::Scale, axis, phase3, PhaseReason::SameAxisRedistribution, ), phase_draft( PhaseKind::Scale, axis.other(), phase4, PhaseReason::GrowOutOfLanes, ), ], ) } 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) { let end = main_end(target, axis); with_main_interval(from, axis, end - size, end) } else { let start = main_start(target, axis); with_main_interval(from, axis, start, start + size) } } 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) { 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( request: &MultiphaseRequest, axis: PhaseAxis, ) -> Result { if request.windows.len() < 2 { return Err(MultiphasePlanFailure::NoPattern); } let orth_axis = axis.other(); let min_width = sane_min_size(request.bounds.width()); let min_height = sane_min_size(request.bounds.height()); let mut phase1 = vec![]; let mut phase2 = vec![]; let mut phase3 = vec![]; for window in &request.windows { if window.to.width() < min_width { return Err(MultiphasePlanFailure::ShrinkBound { axis: PhaseAxis::Horizontal, available: window.to.width(), required: min_width, }); } if window.to.height() < min_height { return Err(MultiphasePlanFailure::ShrinkBound { axis: PhaseAxis::Vertical, available: window.to.height(), required: min_height, }); } let main_changes = main_start(window.from, axis) != main_start(window.to, axis) || 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, axis, main_start(window.to, axis), main_end(window.to, axis), ); push_step(&mut phase2, window.node_id, window.from, after_move); orth_from = after_move; } else if main_changes { 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); 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; } } if orth_changes { push_step(&mut phase3, window.node_id, orth_from, window.to); } } if phase1.is_empty() || phase2.is_empty() || phase3.is_empty() { return Err(MultiphasePlanFailure::NoPattern); } build_validated_plan( request, PlanStrategy::SpaceThenOrthogonalGrowth { axis }, [ phase_draft( PhaseKind::Scale, axis, phase1, PhaseReason::CreateSpaceForAscendingChild, ), phase_draft( PhaseKind::Move, axis, phase2, PhaseReason::MoveAscendingChildAfterSpaceExists, ), phase_draft( PhaseKind::Scale, orth_axis, phase3, PhaseReason::OrthogonalGrowthAfterMove, ), ], ) } fn plan_orientation_change( request: &MultiphaseRequest, from_axis: PhaseAxis, ) -> Result { if request.windows.len() < 2 { return Err(MultiphasePlanFailure::NoPattern); } let to_axis = from_axis.other(); let min_lane_size = sane_min_size(main_size(request.bounds, to_axis)); let target_start = request .windows .first() .map(|window| main_start(window.to, from_axis)) .ok_or(MultiphasePlanFailure::NoPattern)?; let target_end = request .windows .first() .map(|window| main_end(window.to, from_axis)) .ok_or(MultiphasePlanFailure::NoPattern)?; let source_start = request .windows .first() .map(|window| main_start(window.from, to_axis)) .ok_or(MultiphasePlanFailure::NoPattern)?; let source_end = request .windows .first() .map(|window| main_end(window.from, to_axis)) .ok_or(MultiphasePlanFailure::NoPattern)?; if request.windows.iter().any(|window| { main_start(window.from, to_axis) != source_start || main_end(window.from, to_axis) != source_end || main_start(window.to, from_axis) != target_start || main_end(window.to, from_axis) != target_end || main_size(window.to, to_axis) < min_lane_size }) { return Err(MultiphasePlanFailure::NoPattern); } let mut phase1 = vec![]; let mut phase2 = vec![]; let mut phase3 = vec![]; for window in &request.windows { let lane = with_main_interval( window.from, to_axis, main_start(window.to, to_axis), main_end(window.to, to_axis), ); let moved = with_main_interval( lane, from_axis, main_start(window.to, from_axis), main_start(window.to, from_axis) + main_size(lane, from_axis), ); push_step(&mut phase1, window.node_id, window.from, lane); push_step(&mut phase2, window.node_id, lane, moved); push_step(&mut phase3, window.node_id, moved, window.to); } if phase1.is_empty() || phase3.is_empty() { return Err(MultiphasePlanFailure::NoPattern); } build_validated_plan( request, PlanStrategy::OrientationChange { from_axis }, [ phase_draft( PhaseKind::Scale, to_axis, phase1, PhaseReason::ShrinkIntoLanes { lane_axis: to_axis }, ), phase_draft( PhaseKind::Move, from_axis, phase2, PhaseReason::MoveThroughFreedSpace, ), phase_draft( PhaseKind::Scale, from_axis, phase3, PhaseReason::GrowOutOfLanes, ), ], ) } struct MultiphasePhaseDraft { action: MultiphasePhaseActionDraft, steps: Vec, reason: PhaseReason, } enum MultiphasePhaseActionDraft { Uniform(PhaseAction), Mixed(Vec), } fn phase_draft_uniform( action: PhaseAction, steps: Vec, reason: PhaseReason, ) -> MultiphasePhaseDraft { MultiphasePhaseDraft { action: MultiphasePhaseActionDraft::Uniform(action), steps, reason, } } fn phase_draft( kind: PhaseKind, axis: PhaseAxis, steps: Vec, reason: PhaseReason, ) -> MultiphasePhaseDraft { phase_draft_uniform(PhaseAction { kind, axis }, steps, reason) } fn phase_draft_mixed( steps: Vec, actions: Vec, reason: PhaseReason, ) -> MultiphasePhaseDraft { MultiphasePhaseDraft { action: MultiphasePhaseActionDraft::Mixed(actions), steps, reason, } } fn build_validated_plan( request: &MultiphaseRequest, strategy: PlanStrategy, phases: [MultiphasePhaseDraft; N], ) -> Result { let mut explanations = vec![]; let phases: Vec<_> = phases .into_iter() .filter_map(|draft| { if draft.steps.is_empty() { return None; } let mut nodes: Vec<_> = draft.steps.iter().map(|step| step.node_id).collect(); nodes.sort_by_key(|node_id| node_id.0); let action = match draft.action { MultiphasePhaseActionDraft::Uniform(action) => { MultiphasePhaseAction::Uniform(action) } MultiphasePhaseActionDraft::Mixed(actions) => { debug_assert_eq!(actions.len(), draft.steps.len()); MultiphasePhaseAction::from_step_actions(actions) } }; explanations.push(PhaseExplanation { action: action.clone(), reason: draft.reason, nodes, }); Some(MultiphasePhase { action, steps: draft.steps, }) }) .collect(); for phase in &phases { for (idx, step) in phase.steps.iter().enumerate() { let action = phase.action.action_for_step(idx).unwrap(); if classify_step(*step) != Some(action) { return Err(MultiphasePlanFailure::InvalidPhaseStep { action, node_id: step.node_id, }); } } } let plan = MultiphasePlan { phases }; validate_plan_continuous_diagnostic(request, &plan) .map(|_| MultiphasePlanned { plan, explanation: MultiphasePlanExplanation { strategy, phases: explanations, validation: ValidationExplanation::passed(), }, }) .map_err(MultiphasePlanFailure::Validation) } fn validate_plan_continuous(request: &MultiphaseRequest, plan: &MultiphasePlan) -> bool { validate_plan_continuous_diagnostic(request, plan).is_ok() } fn validate_plan_continuous_diagnostic( request: &MultiphaseRequest, plan: &MultiphasePlan, ) -> Result<(), MultiphaseValidationError> { let mut current: Vec<_> = request .windows .iter() .map(|window| (window.node_id, window.from)) .collect(); for (phase_idx, phase) in plan.phases.iter().enumerate() { if let MultiphasePhaseAction::Mixed(actions) = &phase.action && actions.len() != phase.steps.len() { return Err(MultiphaseValidationError::PhaseActionCount { phase: phase_idx, actions: actions.len(), steps: phase.steps.len(), }); } for (idx, step) in phase.steps.iter().enumerate() { if phase.steps[..idx] .iter() .any(|prev| prev.node_id == step.node_id) { return Err(MultiphaseValidationError::DuplicatePhaseStep { phase: phase_idx, node_id: step.node_id, }); } let Some((_, rect)) = current.iter().find(|(node_id, _)| *node_id == step.node_id) else { return Err(MultiphaseValidationError::UnknownPhaseStep { phase: phase_idx, node_id: step.node_id, }); }; if *rect != step.from { return Err(MultiphaseValidationError::StaleStepStart { phase: phase_idx, node_id: step.node_id, }); } } let motions: Vec<_> = current .iter() .map(|(node_id, rect)| { let to = phase .steps .iter() .find(|step| step.node_id == *node_id) .map(|step| step.to) .unwrap_or(*rect); RectMotion { from: *rect, to } }) .collect(); for (idx, motion) in motions.iter().enumerate() { if let Some((other_idx, _)) = motions[idx + 1..] .iter() .enumerate() .find(|(_, other)| motions_overlap_during_phase(*motion, **other)) { return Err(MultiphaseValidationError::PhaseOverlap { phase: phase_idx, a: current[idx].0, b: current[idx + 1 + other_idx].0, }); } } for step in &phase.steps { let (_, rect) = current .iter_mut() .find(|(node_id, _)| *node_id == step.node_id) .unwrap(); *rect = step.to; } } for window in &request.windows { if !current .iter() .find(|(node_id, _)| *node_id == window.node_id) .is_some_and(|(_, rect)| *rect == window.to) { return Err(MultiphaseValidationError::FinalMismatch { node_id: window.node_id, }); } } Ok(()) } #[derive(Copy, Clone)] struct RectMotion { from: Rect, to: Rect, } fn motions_overlap_during_phase(a: RectMotion, b: RectMotion) -> bool { let mut interval = TimeInterval::unit(); interval.intersect_less_than(edge_delta(a.from.x1(), a.to.x1(), b.from.x2(), b.to.x2())) && interval.intersect_less_than(edge_delta(b.from.x1(), b.to.x1(), a.from.x2(), a.to.x2())) && interval.intersect_less_than(edge_delta(a.from.y1(), a.to.y1(), b.from.y2(), b.to.y2())) && interval.intersect_less_than(edge_delta(b.from.y1(), b.to.y1(), a.from.y2(), a.to.y2())) && interval.is_non_empty() } fn edge_delta(a0: i32, a1: i32, b0: i32, b1: i32) -> LinearDelta { let from = a0 as i64 - b0 as i64; let to = a1 as i64 - b1 as i64; LinearDelta { start: from, velocity: to - from, } } #[derive(Copy, Clone)] struct LinearDelta { start: i64, velocity: i64, } #[derive(Copy, Clone)] struct TimeInterval { lower: Rational, lower_open: bool, upper: Rational, upper_open: bool, } impl TimeInterval { fn unit() -> Self { Self { lower: Rational::new(0, 1), lower_open: false, upper: Rational::new(1, 1), upper_open: false, } } fn intersect_less_than(&mut self, delta: LinearDelta) -> bool { if delta.velocity == 0 { return delta.start < 0; } let boundary = Rational::new(-delta.start, delta.velocity); if delta.velocity > 0 { self.tighten_upper(boundary, true); } else { self.tighten_lower(boundary, true); } self.is_non_empty() } fn tighten_lower(&mut self, value: Rational, open: bool) { match value.cmp(&self.lower) { std::cmp::Ordering::Greater => { self.lower = value; self.lower_open = open; } std::cmp::Ordering::Equal => { self.lower_open |= open; } std::cmp::Ordering::Less => {} } } fn tighten_upper(&mut self, value: Rational, open: bool) { match value.cmp(&self.upper) { std::cmp::Ordering::Less => { self.upper = value; self.upper_open = open; } std::cmp::Ordering::Equal => { self.upper_open |= open; } std::cmp::Ordering::Greater => {} } } fn is_non_empty(&self) -> bool { match self.lower.cmp(&self.upper) { std::cmp::Ordering::Less => true, std::cmp::Ordering::Equal => !self.lower_open && !self.upper_open, std::cmp::Ordering::Greater => false, } } } #[derive(Copy, Clone, Eq, PartialEq)] struct Rational { num: i64, den: i64, } impl Rational { fn new(mut num: i64, mut den: i64) -> Self { if den < 0 { num = -num; den = -den; } Self { num, den } } fn cmp(&self, other: &Self) -> std::cmp::Ordering { (self.num as i128 * other.den as i128).cmp(&(other.num as i128 * self.den as i128)) } } fn classify_step(step: MultiphaseStep) -> Option { let same_x = step.from.x1() == step.to.x1() && step.from.x2() == step.to.x2(); let same_y = step.from.y1() == step.to.y1() && step.from.y2() == step.to.y2(); let same_size = step.from.size() == step.to.size(); match (same_x, same_y, same_size) { (false, true, true) => Some(PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }), (true, false, true) => Some(PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Vertical, }), (false, true, false) => Some(PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal, }), (true, false, false) => Some(PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical, }), _ => None, } } fn single_action_reason(action: PhaseAction) -> PhaseReason { match action.kind { PhaseKind::Move => PhaseReason::SingleAction, PhaseKind::Scale => PhaseReason::SameAxisRedistribution, } } fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest { MultiphaseRequest { bounds: request.bounds, clearance: request.clearance, windows: request .windows .iter() .map(|window| MultiphaseWindow { node_id: window.node_id, from: window.to, to: window.from, hierarchy: window.hierarchy.reversed(), }) .collect(), } } fn reverse_plan(plan: MultiphasePlan) -> MultiphasePlan { MultiphasePlan { phases: plan .phases .into_iter() .rev() .map(|phase| MultiphasePhase { action: phase.action, steps: phase .steps .into_iter() .map(|step| MultiphaseStep { node_id: step.node_id, from: step.to, to: step.from, }) .collect(), }) .collect(), } } fn reverse_planned(planned: MultiphasePlanned) -> MultiphasePlanned { let mut phases = planned.explanation.phases; phases.reverse(); MultiphasePlanned { plan: reverse_plan(planned.plan), explanation: MultiphasePlanExplanation { strategy: PlanStrategy::ReversedForwardPlan { original: Box::new(planned.explanation.strategy), }, phases, validation: planned.explanation.validation, }, } } fn overlaps(rects: impl IntoIterator) -> bool { let rects: Vec<_> = rects.into_iter().collect(); for (idx, rect) in rects.iter().enumerate() { if rects[idx + 1..].iter().any(|other| rect.intersects(other)) { return true; } } false } fn motion_bounds(window: MultiphaseWindow) -> Rect { window.from.union(window.to) } fn motion_bounds_with_clearance(window: MultiphaseWindow, clearance: i32) -> Rect { let bounds = motion_bounds(window); Rect::new_saturating( bounds.x1().saturating_sub(clearance), bounds.y1().saturating_sub(clearance), bounds.x2().saturating_add(clearance), bounds.y2().saturating_add(clearance), ) } fn interval_changed(from: Rect, to: Rect, axis: PhaseAxis) -> bool { main_start(from, axis) != main_start(to, axis) || main_end(from, axis) != main_end(to, axis) } fn split_depth_for_axis(position: MultiphaseHierarchyPosition, axis: PhaseAxis) -> Option { match axis { PhaseAxis::Horizontal => position.nearest_horizontal_split_depth, PhaseAxis::Vertical => position.nearest_vertical_split_depth, } } fn push_step(steps: &mut Vec, node_id: NodeId, from: Rect, to: Rect) { if from != to { steps.push(MultiphaseStep { node_id, from, to }); } } fn sane_min_size(size: i32) -> i32 { (size / MIN_SHRINK_DENOMINATOR).max(1) } fn main_start(rect: Rect, axis: PhaseAxis) -> i32 { match axis { PhaseAxis::Horizontal => rect.x1(), PhaseAxis::Vertical => rect.y1(), } } fn main_end(rect: Rect, axis: PhaseAxis) -> i32 { match axis { PhaseAxis::Horizontal => rect.x2(), PhaseAxis::Vertical => rect.y2(), } } fn main_size(rect: Rect, axis: PhaseAxis) -> i32 { main_end(rect, axis) - main_start(rect, axis) } fn orth_start(rect: Rect, axis: PhaseAxis) -> i32 { main_start(rect, axis.other()) } fn orth_end(rect: Rect, axis: PhaseAxis) -> i32 { main_end(rect, axis.other()) } fn with_main_interval(rect: Rect, axis: PhaseAxis, start: i32, end: i32) -> Rect { match axis { PhaseAxis::Horizontal => Rect::new_saturating(start, rect.y1(), end, rect.y2()), PhaseAxis::Vertical => Rect::new_saturating(rect.x1(), start, rect.x2(), end), } } fn with_orth_interval(rect: Rect, axis: PhaseAxis, start: i32, end: i32) -> Rect { with_main_interval(rect, axis.other(), start, end) } impl PhaseAxis { fn other(self) -> Self { match self { Self::Horizontal => Self::Vertical, Self::Vertical => Self::Horizontal, } } } #[cfg(test)] mod tests { use super::*; fn id(raw: u32) -> NodeId { NodeId(raw) } fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect { Rect::new_saturating(x1, y1, x2, y2) } fn window(raw: u32, from: Rect, to: Rect) -> MultiphaseWindow { MultiphaseWindow::new(id(raw), from, to) } #[derive(Clone)] enum TestTree { Leaf(u32), Split { id: u32, axis: PhaseAxis, weights: Vec, children: Vec, }, } struct TestLeaf { node_id: NodeId, rect: Rect, hierarchy: MultiphaseHierarchyPosition, } fn leaf(raw: u32) -> TestTree { TestTree::Leaf(raw) } fn split(id: u32, axis: PhaseAxis, weights: &[i32], children: Vec) -> TestTree { TestTree::Split { id, axis, weights: weights.to_vec(), children, } } fn layout_tree(tree: &TestTree, bounds: Rect) -> Vec { let mut leaves = vec![]; layout_tree_inner( tree, bounds, TestHierarchy { parent: None, depth: 0, sibling_index: None, split_axis: None, nearest_horizontal_split_depth: None, nearest_vertical_split_depth: None, }, &mut leaves, ); leaves.sort_by_key(|leaf| leaf.node_id.0); leaves } #[derive(Copy, Clone)] struct TestHierarchy { parent: Option, depth: u16, sibling_index: Option, split_axis: Option, nearest_horizontal_split_depth: Option, nearest_vertical_split_depth: Option, } fn layout_tree_inner( tree: &TestTree, bounds: Rect, hierarchy: TestHierarchy, leaves: &mut Vec, ) { match tree { TestTree::Leaf(raw) => leaves.push(TestLeaf { node_id: id(*raw), rect: bounds, hierarchy: MultiphaseHierarchyPosition { parent: hierarchy.parent, depth: hierarchy.depth, sibling_index: hierarchy.sibling_index, split_axis: hierarchy.split_axis, nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth, nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth, ..Default::default() }, }), TestTree::Split { id: split_id, axis, weights, children, } => { assert_eq!(weights.len(), children.len()); let rects = split_rect_by_weights(bounds, *axis, weights); for (idx, (child, rect)) in children.iter().zip(rects).enumerate() { let depth = hierarchy.depth.saturating_add(1); let mut child_hierarchy = TestHierarchy { parent: Some(id(*split_id)), depth, sibling_index: Some(idx.min(u16::MAX as usize) as u16), split_axis: Some(*axis), nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth, nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth, }; match axis { PhaseAxis::Horizontal => { child_hierarchy.nearest_horizontal_split_depth = Some(depth); } PhaseAxis::Vertical => { child_hierarchy.nearest_vertical_split_depth = Some(depth); } } layout_tree_inner(child, rect, child_hierarchy, leaves); } } } } fn split_rect_by_weights(bounds: Rect, axis: PhaseAxis, weights: &[i32]) -> Vec { let total_weight: i32 = weights.iter().sum(); assert!(total_weight > 0); let total_size = match axis { PhaseAxis::Horizontal => bounds.width(), PhaseAxis::Vertical => bounds.height(), }; let mut pos = match axis { PhaseAxis::Horizontal => bounds.x1(), PhaseAxis::Vertical => bounds.y1(), }; let mut remaining_size = total_size; let mut remaining_weight = total_weight; let mut rects = vec![]; for (idx, weight) in weights.iter().enumerate() { let size = if idx + 1 == weights.len() { remaining_size } else { total_size * *weight / total_weight }; let rect = match axis { PhaseAxis::Horizontal => { Rect::new_sized_saturating(pos, bounds.y1(), size, bounds.height()) } PhaseAxis::Vertical => { Rect::new_sized_saturating(bounds.x1(), pos, bounds.width(), size) } }; rects.push(rect); pos += size; remaining_size -= size; remaining_weight -= *weight; if remaining_weight == 0 { assert_eq!(remaining_size, 0); } } rects } fn generated_request(old: &TestTree, new: &TestTree, bounds: Rect) -> MultiphaseRequest { let old_leaves = layout_tree(old, bounds); let new_leaves = layout_tree(new, bounds); assert_eq!(old_leaves.len(), new_leaves.len()); let mut windows = vec![]; for old_leaf in &old_leaves { let new_leaf = new_leaves .iter() .find(|leaf| leaf.node_id == old_leaf.node_id) .unwrap(); windows.push(MultiphaseWindow::with_hierarchy( old_leaf.node_id, old_leaf.rect, new_leaf.rect, MultiphaseWindowHierarchy::new(old_leaf.hierarchy, new_leaf.hierarchy), )); } MultiphaseRequest { bounds, windows, clearance: 0, } } fn assert_generated_case_plans(old: &TestTree, new: &TestTree, bounds: Rect) { assert_generated_case_plans_deterministically(old, new, bounds); } fn assert_generated_case_plans_deterministically( old: &TestTree, new: &TestTree, bounds: Rect, ) -> MultiphasePlanned { let req = generated_request(old, new, bounds); assert!(!overlaps(req.windows.iter().map(|window| window.from))); assert!(!overlaps(req.windows.iter().map(|window| window.to))); let first = plan_no_overlap_explained(&req).unwrap(); let second = plan_no_overlap_explained(&req).unwrap(); assert_eq!(first, second); assert_eq!(first.plan.phases.len(), first.explanation.phases.len()); assert_eq!( first.explanation.validation, ValidationExplanation::passed() ); for phase in &first.explanation.phases { assert!(phase.nodes.windows(2).all(|pair| pair[0].0 < pair[1].0)); } assert!(validate_plan_continuous(&req, &first.plan)); first } fn bounds_for_axis(axis: PhaseAxis) -> Rect { match axis { PhaseAxis::Horizontal => rect(0, 0, 400, 100), PhaseAxis::Vertical => rect(0, 0, 100, 400), } } fn push_generated_case_bidirectional( cases: &mut Vec<(TestTree, TestTree, Rect)>, old: TestTree, new: TestTree, bounds: Rect, ) { cases.push((old.clone(), new.clone(), bounds)); cases.push((new, old, bounds)); } fn request(windows: Vec) -> MultiphaseRequest { let bounds = windows .iter() .map(|window| window.from.union(window.to)) .reduce(|bounds, rect| bounds.union(rect)) .unwrap_or_else(|| rect(0, 0, 1, 1)); MultiphaseRequest { bounds, windows, clearance: 0, } } fn actions(plan: &MultiphasePlan) -> Vec { plan.phases .iter() .map(|phase| phase.action.as_uniform().unwrap()) .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 } fn no_pattern_attempts(direction: PlanDirection) -> Vec { vec![ RejectedStrategy { direction, 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, reason: MultiphasePlanFailure::NoPattern, }, RejectedStrategy { direction, strategy: PlanStrategy::OrientationChange { from_axis: PhaseAxis::Horizontal, }, reason: MultiphasePlanFailure::NoPattern, }, RejectedStrategy { direction, strategy: PlanStrategy::OrientationChange { from_axis: PhaseAxis::Vertical, }, reason: MultiphasePlanFailure::NoPattern, }, RejectedStrategy { direction, strategy: PlanStrategy::SwapLanes { axis: PhaseAxis::Horizontal, }, reason: MultiphasePlanFailure::NoPattern, }, RejectedStrategy { direction, strategy: PlanStrategy::SwapLanes { axis: PhaseAxis::Vertical, }, reason: MultiphasePlanFailure::NoPattern, }, ] } #[test] fn horizontal_swap_shrinks_moves_then_grows_without_overlap() { let req = request(vec![ window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), MultiphaseWindow { node_id: id(2), from: rect(100, 0, 200, 100), to: rect(0, 0, 100, 100), hierarchy: Default::default(), }, ]); let planned = plan_no_overlap_explained(&req).unwrap(); let plan = &planned.plan; assert_eq!( actions(plan), vec![ PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical }, PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical }, ] ); assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 50)); assert_eq!(plan.phases[0].steps[1].to, rect(100, 50, 200, 100)); assert_eq!( planned.explanation.strategy, PlanStrategy::SwapLanes { axis: PhaseAxis::Horizontal } ); assert_eq!( planned .explanation .phases .iter() .map(|phase| phase.reason) .collect::>(), vec![ PhaseReason::ShrinkIntoLanes { lane_axis: PhaseAxis::Vertical }, PhaseReason::MoveThroughFreedSpace, PhaseReason::GrowOutOfLanes, ] ); assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]); assert!(validate_plan_continuous(&req, plan)); } #[test] fn horizontal_swap_reverse_uses_equivalent_lanes() { let req = request(vec![ MultiphaseWindow { node_id: id(1), from: rect(100, 0, 200, 100), to: rect(0, 0, 100, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 0, 100, 100), to: rect(100, 0, 200, 100), hierarchy: Default::default(), }, ]); 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 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), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 0, 100, 100), to: rect(100, 0, 200, 100), hierarchy: Default::default(), }, ]); 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 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 uneven_swap_lanes_split_move_and_same_axis_scale() { 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)), ]); 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::Move, axis: PhaseAxis::Horizontal, }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal, }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical, }, ] ); 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!(validate_plan_continuous(&req, plan)); } #[test] fn uneven_swap_lanes_tolerate_stationary_sibling_in_odd_layout() { 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)), window(3, rect(201, 0, 300, 100), rect(201, 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 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![ MultiphaseWindow { node_id: id(1), from: rect(0, 100, 100, 200), to: rect(0, 0, 100, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 0, 100, 100), to: rect(0, 100, 100, 200), hierarchy: Default::default(), }, ]); 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)); } #[test] fn generated_sibling_swaps_plan_for_both_axes() { let bounds = rect(0, 0, 240, 240); for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { let old = split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]); let new = split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]); assert_generated_case_plans(&old, &new, bounds); } } #[test] fn generated_size_redistributions_plan_as_single_axis_scale() { let horizontal_old = split( 10, PhaseAxis::Horizontal, &[1, 1, 1], vec![leaf(1), leaf(2), leaf(3)], ); let horizontal_new = split( 10, PhaseAxis::Horizontal, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)], ); let horizontal_req = generated_request(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100)); let horizontal_plan = plan_no_overlap(&horizontal_req).unwrap(); assert_eq!( actions(&horizontal_plan), vec![PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal, }] ); assert!(validate_plan_continuous(&horizontal_req, &horizontal_plan)); let vertical_old = split( 10, PhaseAxis::Vertical, &[1, 1, 1], vec![leaf(1), leaf(2), leaf(3)], ); let vertical_new = split( 10, PhaseAxis::Vertical, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)], ); let vertical_req = generated_request(&vertical_old, &vertical_new, rect(0, 0, 100, 400)); let vertical_plan = plan_no_overlap(&vertical_req).unwrap(); assert_eq!( actions(&vertical_plan), vec![PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical, }] ); assert!(validate_plan_continuous(&vertical_req, &vertical_plan)); } #[test] fn mixed_single_phase_accepts_distinct_axis_actions_when_proven() { let req = request(vec![ window(1, rect(0, 0, 100, 100), rect(0, 0, 150, 100)), window(2, rect(200, 0, 300, 100), rect(200, 0, 300, 150)), ]); let planned = plan_no_overlap_explained(&req).unwrap(); assert_eq!(planned.explanation.strategy, PlanStrategy::MixedSinglePhase); assert_eq!(planned.plan.phases.len(), 1); assert_eq!( planned.plan.phases[0].action, MultiphasePhaseAction::Mixed(vec![ PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal, }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical, }, ]) ); assert_eq!( planned.explanation.phases[0].reason, PhaseReason::MixedAxisActions ); assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]); assert!(validate_plan_continuous(&req, &planned.plan)); } #[test] fn mixed_single_phase_accepts_move_and_scale_when_proven() { let req = request(vec![ window(1, rect(0, 0, 80, 80), rect(40, 0, 120, 80)), window(2, rect(200, 0, 280, 80), rect(200, 0, 280, 120)), ]); let planned = plan_no_overlap_explained(&req).unwrap(); assert_eq!(planned.explanation.strategy, PlanStrategy::MixedSinglePhase); assert_eq!( planned.plan.phases[0].action, MultiphasePhaseAction::Mixed(vec![ PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical, }, ]) ); assert_eq!( planned.explanation.phases[0].reason, PhaseReason::MixedAxisActions ); assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]); assert!(validate_plan_continuous(&req, &planned.plan)); } #[test] fn single_window_one_axis_group_is_still_multiphase_plannable() { let req = request(vec![window(1, rect(0, 0, 100, 100), rect(40, 0, 140, 100))]); let planned = plan_no_overlap_explained(&req).unwrap(); assert_eq!(planned.explanation.strategy, PlanStrategy::SingleAction); assert_eq!( planned.plan.phases[0].action, MultiphasePhaseAction::Uniform(PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }) ); assert_eq!(planned.explanation.phases[0].nodes, vec![id(1)]); assert!(validate_plan_continuous(&req, &planned.plan)); } #[test] fn mixed_single_phase_still_rejects_diagonal_per_window_motion() { let req = request(vec![ window(1, rect(0, 0, 80, 80), rect(40, 40, 120, 120)), window(2, rect(200, 0, 280, 80), rect(200, 0, 280, 120)), ]); assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); let rejection = MultiphasePlanFailure::InvalidPhaseStep { action: PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal, }, node_id: id(1), }; assert_eq!(diagnostic.forward, rejection); assert_eq!(diagnostic.reverse, Some(rejection)); } #[test] fn generated_nested_size_redistribution_scales_parent_axis_first() { 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, 3], vec![ leaf(1), split(11, PhaseAxis::Vertical, &[3, 1], vec![leaf(2), leaf(3)]), ], ); let req = generated_request(&old, &new, rect(0, 0, 400, 100)); let planned = plan_no_overlap_explained(&req).unwrap(); let plan = &planned.plan; assert_eq!( actions(plan), vec![ PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal, }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical, }, ] ); assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 100, 100)); assert_eq!(step_to(plan, 0, id(2)), rect(100, 0, 400, 50)); assert_eq!(step_to(plan, 0, id(3)), rect(100, 50, 400, 100)); assert_eq!( planned.explanation.strategy, PlanStrategy::HierarchyOrderedScales ); assert_eq!( planned.explanation.phases[0].reason, PhaseReason::ParentAxisBeforeChildAxis { parent_axis: PhaseAxis::Horizontal, parent_depth: 1, child_axis: PhaseAxis::Vertical, child_depth: 2, } ); assert_eq!( planned.explanation.phases[0].nodes, vec![id(1), id(2), id(3)] ); assert_eq!(planned.explanation.phases[1].nodes, vec![id(2), id(3)]); assert_eq!( planned.explanation.validation, ValidationExplanation::passed() ); assert!(validate_plan_continuous(&req, plan)); } #[test] fn orientation_change_shrinks_moves_then_grows() { let req = request(vec![ window(1, rect(0, 0, 200, 400), rect(0, 0, 400, 200)), window(2, rect(200, 0, 400, 400), rect(0, 200, 400, 400)), ]); let planned = plan_no_overlap_explained(&req).unwrap(); let plan = &planned.plan; assert_eq!( planned.explanation.strategy, PlanStrategy::OrientationChange { from_axis: PhaseAxis::Horizontal, } ); assert_eq!( actions(plan), vec![ PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical, }, PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal, }, ] ); assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 200, 200)); assert_eq!(step_to(plan, 0, id(2)), rect(200, 200, 400, 400)); assert_eq!(step_to(plan, 1, id(2)), rect(0, 200, 200, 400)); assert_eq!(step_to(plan, 2, id(1)), rect(0, 0, 400, 200)); assert_eq!(step_to(plan, 2, id(2)), rect(0, 200, 400, 400)); assert!(validate_plan_continuous(&req, plan)); } #[test] fn two_axis_redistribution_without_hierarchy_still_falls_back() { let req = request(vec![ window(1, rect(0, 0, 200, 100), rect(0, 0, 100, 100)), window(2, rect(200, 0, 400, 50), rect(100, 0, 400, 75)), window(3, rect(200, 50, 400, 100), rect(100, 75, 400, 100)), ]); assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); } #[test] fn generated_stack_extractions_plan_for_both_axes_and_directions() { let horizontal_old = split( 10, PhaseAxis::Horizontal, &[1, 1], vec![ leaf(1), split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]), ], ); let horizontal_new = split( 10, PhaseAxis::Horizontal, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)], ); assert_generated_case_plans(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100)); assert_generated_case_plans(&horizontal_new, &horizontal_old, rect(0, 0, 400, 100)); let vertical_old = split( 20, PhaseAxis::Vertical, &[1, 1], vec![ leaf(1), split(21, PhaseAxis::Horizontal, &[1, 1], vec![leaf(2), leaf(3)]), ], ); let vertical_new = split( 20, PhaseAxis::Vertical, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)], ); assert_generated_case_plans(&vertical_old, &vertical_new, rect(0, 0, 100, 400)); 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 three_child_stack_extraction_plans_without_linear_fallback() { let old = split( 10, PhaseAxis::Horizontal, &[1, 1], vec![ leaf(1), split( 11, PhaseAxis::Vertical, &[1, 1, 1], vec![leaf(2), leaf(3), leaf(4)], ), ], ); let new = split( 10, PhaseAxis::Horizontal, &[1, 1, 1], vec![ leaf(1), leaf(3), split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(4)]), ], ); let req = generated_request(&old, &new, rect(0, 0, 600, 300)); let planned = plan_no_overlap_explained(&req).unwrap(); assert_eq!( planned.explanation.strategy, PlanStrategy::SpaceThenOrthogonalGrowth { axis: PhaseAxis::Horizontal, } ); assert_eq!( actions(&planned.plan), vec![ PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal, }, PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical, }, ] ); assert!(validate_plan_continuous(&req, &planned.plan)); } #[test] fn validated_phase_paths_accept_interrupted_reverse_route() { let a_current = rect(50, 0, 150, 50); let b_current = rect(50, 50, 150, 100); let req = request(vec![ window(1, a_current, rect(0, 0, 100, 100)), window(2, b_current, rect(100, 0, 200, 100)), ]); let paths = vec![ vec![ (a_current, rect(0, 0, 100, 50)), (rect(0, 0, 100, 50), rect(0, 0, 100, 100)), ], vec![ (b_current, rect(100, 50, 200, 100)), (rect(100, 50, 200, 100), rect(100, 0, 200, 100)), ], ]; let plan = validate_phase_paths(&req, &paths).unwrap(); assert_eq!( actions(&plan), vec![ PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical, }, ] ); assert!(validate_plan_continuous(&req, &plan)); } #[test] fn bounded_generated_supported_split_tree_corpus_is_deterministic() { let mut cases = vec![]; for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { let child_axis = axis.other(); let bounds = bounds_for_axis(axis); push_generated_case_bidirectional( &mut cases, split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]), split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]), bounds, ); push_generated_case_bidirectional( &mut cases, split(10, axis, &[1, 1, 1], vec![leaf(1), leaf(2), leaf(3)]), split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), bounds, ); push_generated_case_bidirectional( &mut cases, split( 10, axis, &[1, 1], vec![ leaf(1), split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]), ], ), split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), bounds, ); push_generated_case_bidirectional( &mut cases, split( 10, axis, &[1, 1], vec![ split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]), leaf(3), ], ), split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), bounds, ); push_generated_case_bidirectional( &mut cases, split( 10, axis, &[1, 1], vec![ leaf(1), split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]), ], ), split( 10, axis, &[1, 3], vec![ leaf(1), split(11, child_axis, &[3, 1], vec![leaf(2), leaf(3)]), ], ), bounds, ); push_generated_case_bidirectional( &mut cases, split( 10, axis, &[1, 1], vec![ split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]), leaf(3), ], ), split( 10, axis, &[3, 1], vec![ split(11, child_axis, &[1, 3], vec![leaf(1), leaf(2)]), leaf(3), ], ), bounds, ); } assert_eq!(cases.len(), 24); for (old, new, bounds) in cases { assert_generated_case_plans_deterministically(&old, &new, bounds); } } #[test] fn stack_extraction_creates_space_before_moving_child() { let req = request(vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 200, 100), to: rect(0, 0, 100, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(200, 0, 400, 50), to: rect(100, 0, 300, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(200, 50, 400, 100), to: rect(300, 0, 400, 100), hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); 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!(plan.phases[0].steps[0].to, rect(0, 0, 100, 100)); assert_eq!(plan.phases[0].steps[1].to, rect(300, 50, 400, 100)); assert_eq!(plan.phases[1].steps[0].to, rect(100, 0, 300, 50)); assert_eq!(plan.phases[2].steps[0].to, rect(100, 0, 300, 100)); assert_eq!(plan.phases[2].steps[1].to, rect(300, 0, 400, 100)); assert!(validate_plan_continuous(&req, &plan)); } #[test] fn stack_extraction_reverse_replays_phases_in_reverse() { let req = request(vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(0, 0, 200, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(100, 0, 300, 100), to: rect(200, 0, 400, 50), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(300, 0, 400, 100), to: rect(200, 50, 400, 100), hierarchy: Default::default(), }, ]); let planned = plan_no_overlap_explained(&req).unwrap(); let plan = &planned.plan; assert_eq!( actions(plan), vec![ PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical }, PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal }, ] ); assert_eq!( planned.explanation.strategy, PlanStrategy::ReversedForwardPlan { original: Box::new(PlanStrategy::SpaceThenOrthogonalGrowth { axis: PhaseAxis::Horizontal }) } ); assert!(validate_plan_continuous(&req, plan)); } #[test] fn vertical_stack_extraction_creates_space_before_moving_child() { let req = request(vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 100, 200), to: rect(0, 0, 100, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 200, 50, 400), to: rect(0, 100, 100, 300), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(50, 200, 100, 400), to: rect(0, 300, 100, 400), hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); assert_eq!( actions(&plan), vec![ PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical }, PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Vertical }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal }, ] ); assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 100)); assert_eq!(plan.phases[0].steps[1].to, rect(50, 300, 100, 400)); assert_eq!(plan.phases[1].steps[0].to, rect(0, 100, 50, 300)); assert_eq!(plan.phases[2].steps[0].to, rect(0, 100, 100, 300)); assert_eq!(plan.phases[2].steps[1].to, rect(0, 300, 100, 400)); 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![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(0, 0, 100, 200), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(0, 100, 100, 300), to: rect(0, 200, 50, 400), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(0, 300, 100, 400), to: rect(50, 200, 100, 400), hierarchy: Default::default(), }, ]); let plan = plan_no_overlap(&req).unwrap(); assert_eq!( actions(&plan), vec![ PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Horizontal }, PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Vertical }, PhaseAction { kind: PhaseKind::Scale, axis: PhaseAxis::Vertical }, ] ); assert!(validate_plan_continuous(&req, &plan)); } #[test] fn unsupported_diagonal_motion_falls_back_to_linear() { let req = request(vec![MultiphaseWindow { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(100, 100, 200, 200), hierarchy: Default::default(), }]); assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); assert_eq!(diagnostic.forward, MultiphasePlanFailure::NoPattern); assert_eq!(diagnostic.reverse, Some(MultiphasePlanFailure::NoPattern)); let mut expected = no_pattern_attempts(PlanDirection::Forward); expected.extend(no_pattern_attempts(PlanDirection::Reverse)); assert_eq!(diagnostic.attempted, expected); } #[test] fn diagnostics_report_shrink_bound_rejections() { let req = MultiphaseRequest { bounds: rect(0, 0, 400, 100), clearance: 0, windows: vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 200, 100), to: rect(0, 0, 10, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(200, 0, 400, 100), to: rect(10, 0, 400, 100), hierarchy: Default::default(), }, ], }; assert!(matches!( plan_no_overlap_with_diagnostics(&req).unwrap_err().forward, MultiphasePlanFailure::ShrinkBound { axis: PhaseAxis::Horizontal, available: 10, required: 50, } )); } #[test] fn diagnostics_report_candidate_validation_rejections() { let req = request(vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 60, 60), to: rect(180, 0, 240, 60), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(90, 0, 150, 60), to: rect(90, 0, 150, 60), hierarchy: Default::default(), }, ]); let rejection = MultiphasePlanFailure::Validation(MultiphaseValidationError::PhaseOverlap { phase: 0, a: id(1), b: id(2), }); let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); assert_eq!(diagnostic.forward, rejection); assert_eq!(diagnostic.reverse, Some(rejection)); assert_eq!( diagnostic.attempted[0], RejectedStrategy { direction: PlanDirection::Forward, strategy: PlanStrategy::SingleAction, reason: rejection, } ); assert!(diagnostic.attempted.iter().any(|attempt| *attempt == RejectedStrategy { direction: PlanDirection::Reverse, strategy: PlanStrategy::SingleAction, reason: rejection, })); } #[test] fn hierarchy_metadata_classifies_depth_and_mono_transitions() { let source = MultiphaseHierarchyPosition { parent: Some(id(10)), depth: 2, sibling_index: Some(0), split_axis: Some(PhaseAxis::Vertical), nearest_horizontal_split_depth: Some(1), nearest_vertical_split_depth: Some(2), ..Default::default() }; let target = MultiphaseHierarchyPosition { parent: Some(id(11)), depth: 1, sibling_index: Some(2), split_axis: Some(PhaseAxis::Horizontal), nearest_horizontal_split_depth: Some(1), ..Default::default() }; assert_eq!( MultiphaseWindowHierarchy::new(source, target).transition, MultiphaseHierarchyTransition::Ascending ); assert_eq!(source.nearest_vertical_split_depth, Some(2)); let entering_mono = MultiphaseWindowHierarchy::new( source, MultiphaseHierarchyPosition { parent_is_mono: true, mono_active: true, ..target }, ); assert_eq!( entering_mono.transition, MultiphaseHierarchyTransition::EnteringMono ); assert_eq!( entering_mono.reversed().transition, MultiphaseHierarchyTransition::ExitingMono ); } #[test] fn continuous_validation_rejects_narrow_mid_phase_overlap() { let req = request(vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 10, 10), to: rect(100, 0, 110, 10), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(13, 0, 14, 10), to: rect(13, 0, 14, 10), hierarchy: Default::default(), }, ]); let plan = MultiphasePlan { phases: vec![MultiphasePhase { action: MultiphasePhaseAction::Uniform(PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }), steps: vec![MultiphaseStep { node_id: id(1), from: rect(0, 0, 10, 10), to: rect(100, 0, 110, 10), }], }], }; assert_eq!( validate_plan_continuous_diagnostic(&req, &plan), Err(MultiphaseValidationError::PhaseOverlap { phase: 0, a: id(1), b: id(2), }) ); } #[test] fn continuous_validation_allows_edge_touching_motion() { let req = request(vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 10, 10), to: rect(10, 0, 20, 10), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(20, 0, 30, 10), to: rect(20, 0, 30, 10), hierarchy: Default::default(), }, ]); let plan = MultiphasePlan { phases: vec![MultiphasePhase { action: MultiphasePhaseAction::Uniform(PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }), steps: vec![MultiphaseStep { node_id: id(1), from: rect(0, 0, 10, 10), to: rect(10, 0, 20, 10), }], }], }; assert!(validate_plan_continuous(&req, &plan)); } #[test] fn continuous_validation_rejects_mixed_phase_action_count_mismatch() { let req = request(vec![ window(1, rect(0, 0, 40, 40), rect(40, 0, 80, 40)), window(2, rect(100, 0, 140, 40), rect(100, 0, 140, 80)), ]); let plan = MultiphasePlan { phases: vec![MultiphasePhase { action: MultiphasePhaseAction::Mixed(vec![PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }]), steps: vec![ MultiphaseStep { node_id: id(1), from: rect(0, 0, 40, 40), to: rect(40, 0, 80, 40), }, MultiphaseStep { node_id: id(2), from: rect(100, 0, 140, 40), to: rect(100, 0, 140, 80), }, ], }], }; assert_eq!( validate_plan_continuous_diagnostic(&req, &plan), Err(MultiphaseValidationError::PhaseActionCount { phase: 0, actions: 1, steps: 2, }) ); } #[test] fn continuous_validation_rejects_stale_step_start_rect() { let req = request(vec![MultiphaseWindow { node_id: id(1), from: rect(0, 0, 10, 10), to: rect(20, 0, 30, 10), hierarchy: Default::default(), }]); let plan = MultiphasePlan { phases: vec![MultiphasePhase { action: MultiphasePhaseAction::Uniform(PhaseAction { kind: PhaseKind::Move, axis: PhaseAxis::Horizontal, }), steps: vec![MultiphaseStep { node_id: id(1), from: rect(5, 0, 15, 10), to: rect(20, 0, 30, 10), }], }], }; assert_eq!( validate_plan_continuous_diagnostic(&req, &plan), Err(MultiphaseValidationError::StaleStepStart { phase: 0, node_id: id(1), }) ); } #[test] fn motion_groups_split_disjoint_layout_changes() { let windows = vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(100, 0, 200, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(100, 0, 200, 100), to: rect(0, 0, 100, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(300, 0, 400, 100), to: rect(400, 0, 500, 100), hierarchy: Default::default(), }, ]; assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1], vec![2]]); } #[test] fn motion_groups_are_transitive() { let windows = vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(80, 0, 180, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(170, 0, 270, 100), to: rect(250, 0, 350, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(3), from: rect(90, 0, 180, 100), to: rect(180, 0, 260, 100), hierarchy: Default::default(), }, ]; assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1, 2]]); } #[test] fn motion_groups_join_across_animation_clearance() { let windows = vec![ MultiphaseWindow { node_id: id(1), from: rect(0, 0, 100, 100), to: rect(0, 0, 80, 100), hierarchy: Default::default(), }, MultiphaseWindow { node_id: id(2), from: rect(120, 0, 220, 100), to: rect(110, 0, 210, 100), hierarchy: Default::default(), }, ]; assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0], vec![1]]); assert_eq!(partition_motion_groups(&windows, 10), vec![vec![0, 1]]); } }