1
0
Fork 0
forked from wry/wry

Explain multiphase planning decisions

This commit is contained in:
atagen 2026-05-21 21:10:27 +10:00
parent 0b6da9d8e0
commit 511e188d16
2 changed files with 398 additions and 62 deletions

View file

@ -242,6 +242,9 @@ Current pure planner status:
It distinguishes request validation errors, missing patterns, shrink-bound It distinguishes request validation errors, missing patterns, shrink-bound
rejections, invalid phase steps, and exact validation failures such as stale rejections, invalid phase steps, and exact validation failures such as stale
starts or phase overlap. starts or phase overlap.
- Multiphase planning also has an explained-plan entry point. Accepted plans
report the deterministic strategy, phase reasons, participating nodes, and
validation result; rejected plans report every attempted strategy and failure.
- Planner tests now include a deterministic split-tree generator. It builds - Planner tests now include a deterministic split-tree generator. It builds
valid weighted tiling layouts, derives leaf hierarchy metadata, mutates them valid weighted tiling layouts, derives leaf hierarchy metadata, mutates them
through supported transitions, and runs the real planner plus exact validator. through supported transitions, and runs the real planner plus exact validator.
@ -254,6 +257,7 @@ Tests:
- nested containers do not produce simultaneous cross-axis motion - nested containers do not produce simultaneous cross-axis motion
- interruption restarts only affected phase groups - interruption restarts only affected phase groups
- reversing direction produces equivalent motion in reverse - reversing direction produces equivalent motion in reverse
- accepted and rejected plans expose deterministic strategy explanations
- child waits for parent/container-space phases when moving upward into a - child waits for parent/container-space phases when moving upward into a
toplevel peer position toplevel peer position
- mono-mode tab switches do not animate, while entering/exiting mono can animate - mono-mode tab switches do not animate, while entering/exiting mono can animate

View file

@ -120,6 +120,75 @@ pub struct MultiphasePlan {
pub phases: Vec<MultiphasePhase>, pub phases: Vec<MultiphasePhase>,
} }
#[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<PhaseExplanation>,
pub validation: ValidationExplanation,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PhaseExplanation {
pub action: PhaseAction,
pub reason: PhaseReason,
pub nodes: Vec<NodeId>,
}
#[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,
HierarchyOrderedScales,
SwapLanes { axis: PhaseAxis },
SpaceThenOrthogonalGrowth { axis: PhaseAxis },
ReversedForwardPlan { original: Box<PlanStrategy> },
}
#[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,
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)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultiphasePhase { pub struct MultiphasePhase {
pub action: PhaseAction, pub action: PhaseAction,
@ -161,10 +230,11 @@ pub enum MultiphaseError {
NoPlan, NoPlan,
} }
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultiphasePlanDiagnostic { pub struct MultiphasePlanDiagnostic {
pub forward: MultiphasePlanFailure, pub forward: MultiphasePlanFailure,
pub reverse: Option<MultiphasePlanFailure>, pub reverse: Option<MultiphasePlanFailure>,
pub attempted: Vec<RejectedStrategy>,
} }
impl MultiphasePlanDiagnostic { impl MultiphasePlanDiagnostic {
@ -176,6 +246,15 @@ impl MultiphasePlanDiagnostic {
} }
} }
impl ValidationExplanation {
fn passed() -> Self {
Self {
continuous_overlap_passed: true,
final_rects_matched: true,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum MultiphasePlanFailure { pub enum MultiphasePlanFailure {
Request(MultiphaseError), Request(MultiphaseError),
@ -201,6 +280,12 @@ pub enum MultiphaseValidationError {
FinalMismatch { node_id: NodeId }, FinalMismatch { node_id: NodeId },
} }
#[derive(Clone, Debug, Eq, PartialEq)]
struct PlanForwardFailure {
reason: MultiphasePlanFailure,
attempted: Vec<RejectedStrategy>,
}
pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result<MultiphasePlan, MultiphaseError> { pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result<MultiphasePlan, MultiphaseError> {
plan_no_overlap_with_diagnostics(request).map_err(|diagnostic| diagnostic.legacy_error()) plan_no_overlap_with_diagnostics(request).map_err(|diagnostic| diagnostic.legacy_error())
} }
@ -208,10 +293,17 @@ pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result<MultiphasePlan, Mu
pub fn plan_no_overlap_with_diagnostics( pub fn plan_no_overlap_with_diagnostics(
request: &MultiphaseRequest, request: &MultiphaseRequest,
) -> Result<MultiphasePlan, MultiphasePlanDiagnostic> { ) -> Result<MultiphasePlan, MultiphasePlanDiagnostic> {
plan_no_overlap_explained(request).map(|planned| planned.plan)
}
pub fn plan_no_overlap_explained(
request: &MultiphaseRequest,
) -> Result<MultiphasePlanned, MultiphasePlanDiagnostic> {
if let Err(error) = validate_request(request) { if let Err(error) = validate_request(request) {
return Err(MultiphasePlanDiagnostic { return Err(MultiphasePlanDiagnostic {
forward: MultiphasePlanFailure::Request(error), forward: MultiphasePlanFailure::Request(error),
reverse: None, reverse: None,
attempted: vec![],
}); });
} }
if request if request
@ -219,25 +311,38 @@ pub fn plan_no_overlap_with_diagnostics(
.iter() .iter()
.all(|window| window.from == window.to) .all(|window| window.from == window.to)
{ {
return Ok(MultiphasePlan { phases: vec![] }); 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) { if let Some(failure) = target_shrink_bound_failure(request) {
return Err(MultiphasePlanDiagnostic { return Err(MultiphasePlanDiagnostic {
forward: failure, forward: failure,
reverse: None, reverse: None,
attempted: vec![],
}); });
} }
let forward = match plan_forward(request) { let forward = match plan_forward(request, PlanDirection::Forward) {
Ok(plan) => return Ok(plan), Ok(plan) => return Ok(plan),
Err(error) => error, Err(error) => error,
}; };
let reversed = reverse_request(request); let reversed = reverse_request(request);
match plan_forward(&reversed) { match plan_forward(&reversed, PlanDirection::Reverse) {
Ok(plan) => Ok(reverse_plan(plan)), Ok(plan) => Ok(reverse_planned(plan)),
Err(reverse) => Err(MultiphasePlanDiagnostic { Err(reverse) => {
forward, let mut attempted = forward.attempted;
reverse: Some(reverse), attempted.extend(reverse.attempted);
}), Err(MultiphasePlanDiagnostic {
forward: forward.reason,
reverse: Some(reverse.reason),
attempted,
})
}
} }
} }
@ -313,46 +418,89 @@ fn target_shrink_bound_failure(request: &MultiphaseRequest) -> Option<Multiphase
None None
} }
fn plan_forward(request: &MultiphaseRequest) -> Result<MultiphasePlan, MultiphasePlanFailure> { fn plan_forward(
request: &MultiphaseRequest,
direction: PlanDirection,
) -> Result<MultiphasePlanned, PlanForwardFailure> {
let mut rejection = None; let mut rejection = None;
let mut attempted = vec![];
match plan_single_action_phase(request) { match plan_single_action_phase(request) {
Ok(plan) => return Ok(plan), Ok(plan) => return Ok(plan),
Err(MultiphasePlanFailure::NoPattern) => {}
Err(error) => { Err(error) => {
rejection.get_or_insert(error); record_rejection(&mut attempted, direction, PlanStrategy::SingleAction, error);
if error != MultiphasePlanFailure::NoPattern {
rejection.get_or_insert(error);
}
} }
} }
match plan_hierarchy_ordered_axis_scales(request) { match plan_hierarchy_ordered_axis_scales(request) {
Ok(plan) => return Ok(plan), Ok(plan) => return Ok(plan),
Err(MultiphasePlanFailure::NoPattern) => {}
Err(error) => { Err(error) => {
rejection.get_or_insert(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] { for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
match plan_axis_crossing_lanes(request, axis) { match plan_axis_crossing_lanes(request, axis) {
Ok(plan) => return Ok(plan), Ok(plan) => return Ok(plan),
Err(MultiphasePlanFailure::NoPattern) => {}
Err(error) => { Err(error) => {
rejection.get_or_insert(error); record_rejection(
&mut attempted,
direction,
PlanStrategy::SwapLanes { axis },
error,
);
if error != MultiphasePlanFailure::NoPattern {
rejection.get_or_insert(error);
}
} }
} }
} }
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
match plan_space_then_orthogonal_growth(request, axis) { match plan_space_then_orthogonal_growth(request, axis) {
Ok(plan) => return Ok(plan), Ok(plan) => return Ok(plan),
Err(MultiphasePlanFailure::NoPattern) => {}
Err(error) => { Err(error) => {
rejection.get_or_insert(error); record_rejection(
&mut attempted,
direction,
PlanStrategy::SpaceThenOrthogonalGrowth { axis },
error,
);
if error != MultiphasePlanFailure::NoPattern {
rejection.get_or_insert(error);
}
} }
} }
} }
Err(rejection.unwrap_or(MultiphasePlanFailure::NoPattern)) Err(PlanForwardFailure {
reason: rejection.unwrap_or(MultiphasePlanFailure::NoPattern),
attempted,
})
}
fn record_rejection(
attempted: &mut Vec<RejectedStrategy>,
direction: PlanDirection,
strategy: PlanStrategy,
reason: MultiphasePlanFailure,
) {
attempted.push(RejectedStrategy {
direction,
strategy,
reason,
});
} }
fn plan_single_action_phase( fn plan_single_action_phase(
request: &MultiphaseRequest, request: &MultiphaseRequest,
) -> Result<MultiphasePlan, MultiphasePlanFailure> { ) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
let mut action = None; let mut action = None;
let mut steps = vec![]; let mut steps = vec![];
for window in &request.windows { for window in &request.windows {
@ -389,12 +537,21 @@ fn plan_single_action_phase(
let Some(action) = action else { let Some(action) = action else {
return Err(MultiphasePlanFailure::NoPattern); return Err(MultiphasePlanFailure::NoPattern);
}; };
build_validated_plan(request, [(action.kind, action.axis, steps)]) build_validated_plan(
request,
PlanStrategy::SingleAction,
[phase_draft(
action.kind,
action.axis,
steps,
single_action_reason(action),
)],
)
} }
fn plan_hierarchy_ordered_axis_scales( fn plan_hierarchy_ordered_axis_scales(
request: &MultiphaseRequest, request: &MultiphaseRequest,
) -> Result<MultiphasePlan, MultiphasePlanFailure> { ) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
let mut changed_axes = vec![]; let mut changed_axes = vec![];
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
if request if request
@ -408,7 +565,7 @@ fn plan_hierarchy_ordered_axis_scales(
let [first_axis, second_axis] = changed_axes let [first_axis, second_axis] = changed_axes
.try_into() .try_into()
.map_err(|_| MultiphasePlanFailure::NoPattern)?; .map_err(|_| MultiphasePlanFailure::NoPattern)?;
let axes = hierarchy_scale_axis_order(request, first_axis, second_axis) let order = hierarchy_scale_axis_order(request, first_axis, second_axis)
.ok_or(MultiphasePlanFailure::NoPattern)?; .ok_or(MultiphasePlanFailure::NoPattern)?;
let mut current: Vec<_> = request let mut current: Vec<_> = request
.windows .windows
@ -416,7 +573,13 @@ fn plan_hierarchy_ordered_axis_scales(
.map(|window| (window.node_id, window.from)) .map(|window| (window.node_id, window.from))
.collect(); .collect();
let mut phases = vec![]; let mut phases = vec![];
for axis in axes { 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![]; let mut steps = vec![];
for window in &request.windows { for window in &request.windows {
let (_, rect) = current let (_, rect) = current
@ -445,28 +608,44 @@ fn plan_hierarchy_ordered_axis_scales(
if steps.is_empty() { if steps.is_empty() {
return Err(MultiphasePlanFailure::NoPattern); return Err(MultiphasePlanFailure::NoPattern);
} }
phases.push((PhaseKind::Scale, axis, steps)); phases.push(phase_draft(PhaseKind::Scale, axis, steps, reason));
} }
let [first, second] = phases let [first, second] = phases
.try_into() .try_into()
.map_err(|_| MultiphasePlanFailure::NoPattern)?; .map_err(|_| MultiphasePlanFailure::NoPattern)?;
build_validated_plan(request, [first, second]) build_validated_plan(
request,
PlanStrategy::HierarchyOrderedScales,
[first, second],
)
} }
fn hierarchy_scale_axis_order( fn hierarchy_scale_axis_order(
request: &MultiphaseRequest, request: &MultiphaseRequest,
first_axis: PhaseAxis, first_axis: PhaseAxis,
second_axis: PhaseAxis, second_axis: PhaseAxis,
) -> Option<[PhaseAxis; 2]> { ) -> Option<HierarchyScaleAxisOrder> {
let first_priority = hierarchy_axis_priority(request, first_axis)?; let first_priority = hierarchy_axis_priority(request, first_axis)?;
let second_priority = hierarchy_axis_priority(request, second_axis)?; let second_priority = hierarchy_axis_priority(request, second_axis)?;
match first_priority.cmp(&second_priority) { match first_priority.cmp(&second_priority) {
std::cmp::Ordering::Less => Some([first_axis, second_axis]), std::cmp::Ordering::Less => Some(HierarchyScaleAxisOrder {
std::cmp::Ordering::Greater => Some([second_axis, first_axis]), 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, 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<u16> { fn hierarchy_axis_priority(request: &MultiphaseRequest, axis: PhaseAxis) -> Option<u16> {
request request
.windows .windows
@ -485,7 +664,7 @@ fn hierarchy_axis_priority(request: &MultiphaseRequest, axis: PhaseAxis) -> Opti
fn plan_axis_crossing_lanes( fn plan_axis_crossing_lanes(
request: &MultiphaseRequest, request: &MultiphaseRequest,
axis: PhaseAxis, axis: PhaseAxis,
) -> Result<MultiphasePlan, MultiphasePlanFailure> { ) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
if request.windows.len() != 2 { if request.windows.len() != 2 {
return Err(MultiphasePlanFailure::NoPattern); return Err(MultiphasePlanFailure::NoPattern);
} }
@ -551,10 +730,28 @@ fn plan_axis_crossing_lanes(
} }
build_validated_plan( build_validated_plan(
request, request,
PlanStrategy::SwapLanes { axis },
[ [
(PhaseKind::Scale, axis.other(), phase1), phase_draft(
(PhaseKind::Move, axis, phase2), PhaseKind::Scale,
(PhaseKind::Scale, axis.other(), phase3), axis.other(),
phase1,
PhaseReason::ShrinkIntoLanes {
lane_axis: axis.other(),
},
),
phase_draft(
PhaseKind::Move,
axis,
phase2,
PhaseReason::MoveThroughFreedSpace,
),
phase_draft(
PhaseKind::Scale,
axis.other(),
phase3,
PhaseReason::GrowOutOfLanes,
),
], ],
) )
} }
@ -571,7 +768,7 @@ fn lane_index_for_direction(window: MultiphaseWindow, axis: PhaseAxis) -> Option
fn plan_space_then_orthogonal_growth( fn plan_space_then_orthogonal_growth(
request: &MultiphaseRequest, request: &MultiphaseRequest,
axis: PhaseAxis, axis: PhaseAxis,
) -> Result<MultiphasePlan, MultiphasePlanFailure> { ) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
if request.windows.len() < 2 { if request.windows.len() < 2 {
return Err(MultiphasePlanFailure::NoPattern); return Err(MultiphasePlanFailure::NoPattern);
} }
@ -631,24 +828,71 @@ fn plan_space_then_orthogonal_growth(
} }
build_validated_plan( build_validated_plan(
request, request,
PlanStrategy::SpaceThenOrthogonalGrowth { axis },
[ [
(PhaseKind::Scale, axis, phase1), phase_draft(
(PhaseKind::Move, axis, phase2), PhaseKind::Scale,
(PhaseKind::Scale, orth_axis, phase3), axis,
phase1,
PhaseReason::CreateSpaceForAscendingChild,
),
phase_draft(
PhaseKind::Move,
axis,
phase2,
PhaseReason::MoveAscendingChildAfterSpaceExists,
),
phase_draft(
PhaseKind::Scale,
orth_axis,
phase3,
PhaseReason::OrthogonalGrowthAfterMove,
),
], ],
) )
} }
struct MultiphasePhaseDraft {
action: PhaseAction,
steps: Vec<MultiphaseStep>,
reason: PhaseReason,
}
fn phase_draft(
kind: PhaseKind,
axis: PhaseAxis,
steps: Vec<MultiphaseStep>,
reason: PhaseReason,
) -> MultiphasePhaseDraft {
MultiphasePhaseDraft {
action: PhaseAction { kind, axis },
steps,
reason,
}
}
fn build_validated_plan<const N: usize>( fn build_validated_plan<const N: usize>(
request: &MultiphaseRequest, request: &MultiphaseRequest,
phases: [(PhaseKind, PhaseAxis, Vec<MultiphaseStep>); N], strategy: PlanStrategy,
) -> Result<MultiphasePlan, MultiphasePlanFailure> { phases: [MultiphasePhaseDraft; N],
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
let mut explanations = vec![];
let phases: Vec<_> = phases let phases: Vec<_> = phases
.into_iter() .into_iter()
.filter_map(|(kind, axis, steps)| { .filter_map(|draft| {
(!steps.is_empty()).then_some(MultiphasePhase { if draft.steps.is_empty() {
action: PhaseAction { kind, axis }, return None;
steps, }
let mut nodes: Vec<_> = draft.steps.iter().map(|step| step.node_id).collect();
nodes.sort_by_key(|node_id| node_id.0);
explanations.push(PhaseExplanation {
action: draft.action,
reason: draft.reason,
nodes,
});
Some(MultiphasePhase {
action: draft.action,
steps: draft.steps,
}) })
}) })
.collect(); .collect();
@ -664,7 +908,14 @@ fn build_validated_plan<const N: usize>(
} }
let plan = MultiphasePlan { phases }; let plan = MultiphasePlan { phases };
validate_plan_continuous_diagnostic(request, &plan) validate_plan_continuous_diagnostic(request, &plan)
.map(|_| plan) .map(|_| MultiphasePlanned {
plan,
explanation: MultiphasePlanExplanation {
strategy,
phases: explanations,
validation: ValidationExplanation::passed(),
},
})
.map_err(MultiphasePlanFailure::Validation) .map_err(MultiphasePlanFailure::Validation)
} }
@ -894,6 +1145,13 @@ fn classify_step(step: MultiphaseStep) -> Option<PhaseAction> {
} }
} }
fn single_action_reason(action: PhaseAction) -> PhaseReason {
match action.kind {
PhaseKind::Move => PhaseReason::SingleAction,
PhaseKind::Scale => PhaseReason::SameAxisRedistribution,
}
}
fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest { fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest {
MultiphaseRequest { MultiphaseRequest {
bounds: request.bounds, bounds: request.bounds,
@ -932,6 +1190,21 @@ fn reverse_plan(plan: MultiphasePlan) -> MultiphasePlan {
} }
} }
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<Item = Rect>) -> bool { fn overlaps(rects: impl IntoIterator<Item = Rect>) -> bool {
let rects: Vec<_> = rects.into_iter().collect(); let rects: Vec<_> = rects.into_iter().collect();
for (idx, rect) in rects.iter().enumerate() { for (idx, rect) in rects.iter().enumerate() {
@ -1240,9 +1513,10 @@ mod tests {
hierarchy: Default::default(), hierarchy: Default::default(),
}, },
]); ]);
let plan = plan_no_overlap(&req).unwrap(); let planned = plan_no_overlap_explained(&req).unwrap();
let plan = &planned.plan;
assert_eq!( assert_eq!(
actions(&plan), actions(plan),
vec![ vec![
PhaseAction { PhaseAction {
kind: PhaseKind::Scale, kind: PhaseKind::Scale,
@ -1260,7 +1534,29 @@ mod tests {
); );
assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 50)); 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!(plan.phases[0].steps[1].to, rect(100, 50, 200, 100));
assert!(validate_plan_continuous(&req, &plan)); assert_eq!(
planned.explanation.strategy,
PlanStrategy::SwapLanes {
axis: PhaseAxis::Horizontal
}
);
assert_eq!(
planned
.explanation
.phases
.iter()
.map(|phase| phase.reason)
.collect::<Vec<_>>(),
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] #[test]
@ -1410,9 +1706,10 @@ mod tests {
], ],
); );
let req = generated_request(&old, &new, rect(0, 0, 400, 100)); let req = generated_request(&old, &new, rect(0, 0, 400, 100));
let plan = plan_no_overlap(&req).unwrap(); let planned = plan_no_overlap_explained(&req).unwrap();
let plan = &planned.plan;
assert_eq!( assert_eq!(
actions(&plan), actions(plan),
vec![ vec![
PhaseAction { PhaseAction {
kind: PhaseKind::Scale, kind: PhaseKind::Scale,
@ -1424,10 +1721,32 @@ mod tests {
}, },
] ]
); );
assert_eq!(step_to(&plan, 0, id(1)), rect(0, 0, 100, 100)); 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(2)), rect(100, 0, 400, 50));
assert_eq!(step_to(&plan, 0, id(3)), rect(100, 50, 400, 100)); assert_eq!(step_to(plan, 0, id(3)), rect(100, 50, 400, 100));
assert!(validate_plan_continuous(&req, &plan)); 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] #[test]
@ -1549,9 +1868,10 @@ mod tests {
hierarchy: Default::default(), hierarchy: Default::default(),
}, },
]); ]);
let plan = plan_no_overlap(&req).unwrap(); let planned = plan_no_overlap_explained(&req).unwrap();
let plan = &planned.plan;
assert_eq!( assert_eq!(
actions(&plan), actions(plan),
vec![ vec![
PhaseAction { PhaseAction {
kind: PhaseKind::Scale, kind: PhaseKind::Scale,
@ -1567,7 +1887,15 @@ mod tests {
}, },
] ]
); );
assert!(validate_plan_continuous(&req, &plan)); assert_eq!(
planned.explanation.strategy,
PlanStrategy::ReversedForwardPlan {
original: Box::new(PlanStrategy::SpaceThenOrthogonalGrowth {
axis: PhaseAxis::Horizontal
})
}
);
assert!(validate_plan_continuous(&req, plan));
} }
#[test] #[test]
@ -1670,12 +1998,16 @@ mod tests {
hierarchy: Default::default(), hierarchy: Default::default(),
}]); }]);
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
assert_eq!( let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err();
plan_no_overlap_with_diagnostics(&req).unwrap_err(), assert_eq!(diagnostic.forward, MultiphasePlanFailure::NoPattern);
MultiphasePlanDiagnostic { assert_eq!(diagnostic.reverse, Some(MultiphasePlanFailure::NoPattern));
forward: MultiphasePlanFailure::NoPattern, assert!(
reverse: Some(MultiphasePlanFailure::NoPattern), diagnostic
} .attempted
.iter()
.any(|attempt| attempt.direction == PlanDirection::Forward
&& attempt.strategy == PlanStrategy::SingleAction
&& attempt.reason == MultiphasePlanFailure::NoPattern)
); );
} }