1
0
Fork 0
forked from wry/wry

Assert mixed multiphase action boundaries

This commit is contained in:
atagen 2026-05-21 21:34:53 +10:00
parent 632873ec5a
commit a5845fb293
2 changed files with 91 additions and 1 deletions

View file

@ -170,7 +170,9 @@ Core rules:
- Each phase is a discrete animation using the full curve.
- A phase performs only one action kind per window: move or scale.
- Movement and scaling are split by axis.
- No diagonal motion.
- No diagonal motion. Mixed-action phases may combine different per-window
actions, but no single window may move or resize on more than one axis in one
step.
- A window or synchronized group owns its own timeline.
- New layout changes interrupt only windows/groups with changed destinations.
- Current hierarchy and target hierarchy both matter. The planner must know
@ -270,6 +272,7 @@ Tests:
- bounded generated split-tree corpus produces identical plans on repeated runs
- unsupported and invalid candidate plans produce exact expected diagnostics
- mixed-action phases are accepted only under exact continuous validation
- diagonal per-window motion remains a hard rejection even inside mixed phases
- child waits for parent/container-space phases when moving upward into a
toplevel peer position
- mono-mode tab switches do not animate, while entering/exiting mono can animate

View file

@ -1910,6 +1910,56 @@ mod tests {
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 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(
@ -2518,6 +2568,43 @@ mod tests {
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 {