Allow proven mixed multiphase actions
This commit is contained in:
parent
01d1545c40
commit
632873ec5a
2 changed files with 179 additions and 29 deletions
|
|
@ -228,6 +228,9 @@ Current pure planner status:
|
|||
- Nested size redistribution can use hierarchy metadata to decompose two-axis
|
||||
resizing into parent-axis then child-axis scale phases, but only when the
|
||||
source/target ancestor split depths give a deterministic order.
|
||||
- A phase can carry mixed per-window actions when each window still performs one
|
||||
classified move/scale on one axis and the exact validator proves the combined
|
||||
phase is non-overlapping.
|
||||
- Every produced plan is checked analytically for overlap over the full duration
|
||||
of each phase before it is accepted. This solves the linear edge inequalities
|
||||
for each pair of moving rectangles instead of relying on sampled frames.
|
||||
|
|
@ -266,6 +269,7 @@ Tests:
|
|||
- accepted and rejected plans expose deterministic strategy explanations
|
||||
- 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
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ pub struct MultiphasePlanExplanation {
|
|||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PhaseExplanation {
|
||||
pub action: PhaseAction,
|
||||
pub action: MultiphasePhaseAction,
|
||||
pub reason: PhaseReason,
|
||||
pub nodes: Vec<NodeId>,
|
||||
}
|
||||
|
|
@ -150,6 +150,7 @@ pub struct ValidationExplanation {
|
|||
pub enum PlanStrategy {
|
||||
NoOp,
|
||||
SingleAction,
|
||||
MixedSinglePhase,
|
||||
HierarchyOrderedScales,
|
||||
SwapLanes { axis: PhaseAxis },
|
||||
SpaceThenOrthogonalGrowth { axis: PhaseAxis },
|
||||
|
|
@ -173,6 +174,7 @@ pub struct RejectedStrategy {
|
|||
pub enum PhaseReason {
|
||||
SingleAction,
|
||||
SameAxisRedistribution,
|
||||
MixedAxisActions,
|
||||
ShrinkIntoLanes {
|
||||
lane_axis: PhaseAxis,
|
||||
},
|
||||
|
|
@ -191,10 +193,16 @@ pub enum PhaseReason {
|
|||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MultiphasePhase {
|
||||
pub action: PhaseAction,
|
||||
pub action: MultiphasePhaseAction,
|
||||
pub steps: Vec<MultiphaseStep>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum MultiphasePhaseAction {
|
||||
Uniform(PhaseAction),
|
||||
Mixed(Vec<PhaseAction>),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MultiphaseStep {
|
||||
pub node_id: NodeId,
|
||||
|
|
@ -220,6 +228,32 @@ pub enum PhaseAxis {
|
|||
Vertical,
|
||||
}
|
||||
|
||||
impl MultiphasePhaseAction {
|
||||
fn from_step_actions(actions: Vec<PhaseAction>) -> 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<PhaseAction> {
|
||||
match self {
|
||||
Self::Uniform(action) => Some(*action),
|
||||
Self::Mixed(actions) => actions.get(idx).copied(),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_uniform(&self) -> Option<PhaseAction> {
|
||||
match self {
|
||||
Self::Uniform(action) => Some(*action),
|
||||
Self::Mixed(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum MultiphaseError {
|
||||
EmptyBounds,
|
||||
|
|
@ -273,11 +307,31 @@ pub enum MultiphasePlanFailure {
|
|||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum MultiphaseValidationError {
|
||||
DuplicatePhaseStep { phase: usize, node_id: NodeId },
|
||||
UnknownPhaseStep { phase: usize, node_id: NodeId },
|
||||
StaleStepStart { phase: usize, node_id: NodeId },
|
||||
PhaseOverlap { phase: usize, a: NodeId, b: NodeId },
|
||||
FinalMismatch { node_id: NodeId },
|
||||
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)]
|
||||
|
|
@ -501,8 +555,10 @@ fn record_rejection(
|
|||
fn plan_single_action_phase(
|
||||
request: &MultiphaseRequest,
|
||||
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
||||
let mut action = None;
|
||||
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;
|
||||
|
|
@ -528,21 +584,33 @@ fn plan_single_action_phase(
|
|||
});
|
||||
}
|
||||
}
|
||||
if action.is_some_and(|action| action != step_action) {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
if uniform_action.is_some_and(|action| action != step_action) {
|
||||
is_uniform = false;
|
||||
}
|
||||
action = Some(step_action);
|
||||
uniform_action.get_or_insert(step_action);
|
||||
steps.push(step);
|
||||
step_actions.push(step_action);
|
||||
}
|
||||
let Some(action) = action else {
|
||||
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(
|
||||
action.kind,
|
||||
action.axis,
|
||||
[phase_draft_uniform(
|
||||
action,
|
||||
steps,
|
||||
single_action_reason(action),
|
||||
)],
|
||||
|
|
@ -853,9 +921,26 @@ fn plan_space_then_orthogonal_growth(
|
|||
}
|
||||
|
||||
struct MultiphasePhaseDraft {
|
||||
action: MultiphasePhaseActionDraft,
|
||||
steps: Vec<MultiphaseStep>,
|
||||
reason: PhaseReason,
|
||||
}
|
||||
|
||||
enum MultiphasePhaseActionDraft {
|
||||
Uniform(PhaseAction),
|
||||
Mixed(Vec<PhaseAction>),
|
||||
}
|
||||
|
||||
fn phase_draft_uniform(
|
||||
action: PhaseAction,
|
||||
steps: Vec<MultiphaseStep>,
|
||||
reason: PhaseReason,
|
||||
) -> MultiphasePhaseDraft {
|
||||
MultiphasePhaseDraft {
|
||||
action: MultiphasePhaseActionDraft::Uniform(action),
|
||||
steps,
|
||||
reason,
|
||||
}
|
||||
}
|
||||
|
||||
fn phase_draft(
|
||||
|
|
@ -863,9 +948,17 @@ fn phase_draft(
|
|||
axis: PhaseAxis,
|
||||
steps: Vec<MultiphaseStep>,
|
||||
reason: PhaseReason,
|
||||
) -> MultiphasePhaseDraft {
|
||||
phase_draft_uniform(PhaseAction { kind, axis }, steps, reason)
|
||||
}
|
||||
|
||||
fn phase_draft_mixed(
|
||||
steps: Vec<MultiphaseStep>,
|
||||
actions: Vec<PhaseAction>,
|
||||
reason: PhaseReason,
|
||||
) -> MultiphasePhaseDraft {
|
||||
MultiphasePhaseDraft {
|
||||
action: PhaseAction { kind, axis },
|
||||
action: MultiphasePhaseActionDraft::Mixed(actions),
|
||||
steps,
|
||||
reason,
|
||||
}
|
||||
|
|
@ -885,22 +978,32 @@ fn build_validated_plan<const N: usize>(
|
|||
}
|
||||
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: draft.action,
|
||||
action: action.clone(),
|
||||
reason: draft.reason,
|
||||
nodes,
|
||||
});
|
||||
Some(MultiphasePhase {
|
||||
action: draft.action,
|
||||
action,
|
||||
steps: draft.steps,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for phase in &phases {
|
||||
for step in &phase.steps {
|
||||
if classify_step(*step) != Some(phase.action) {
|
||||
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: phase.action,
|
||||
action,
|
||||
node_id: step.node_id,
|
||||
});
|
||||
}
|
||||
|
|
@ -933,6 +1036,15 @@ fn validate_plan_continuous_diagnostic(
|
|||
.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()
|
||||
|
|
@ -1526,7 +1638,10 @@ mod tests {
|
|||
}
|
||||
|
||||
fn actions(plan: &MultiphasePlan) -> Vec<PhaseAction> {
|
||||
plan.phases.iter().map(|phase| phase.action).collect()
|
||||
plan.phases
|
||||
.iter()
|
||||
.map(|phase| phase.action.as_uniform().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn step_to(plan: &MultiphasePlan, phase: usize, node_id: NodeId) -> Rect {
|
||||
|
|
@ -1764,6 +1879,37 @@ mod tests {
|
|||
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 generated_nested_size_redistribution_scales_parent_axis_first() {
|
||||
let old = split(
|
||||
|
|
@ -2317,10 +2463,10 @@ mod tests {
|
|||
]);
|
||||
let plan = MultiphasePlan {
|
||||
phases: vec![MultiphasePhase {
|
||||
action: PhaseAction {
|
||||
action: MultiphasePhaseAction::Uniform(PhaseAction {
|
||||
kind: PhaseKind::Move,
|
||||
axis: PhaseAxis::Horizontal,
|
||||
},
|
||||
}),
|
||||
steps: vec![MultiphaseStep {
|
||||
node_id: id(1),
|
||||
from: rect(0, 0, 10, 10),
|
||||
|
|
@ -2357,10 +2503,10 @@ mod tests {
|
|||
]);
|
||||
let plan = MultiphasePlan {
|
||||
phases: vec![MultiphasePhase {
|
||||
action: PhaseAction {
|
||||
action: MultiphasePhaseAction::Uniform(PhaseAction {
|
||||
kind: PhaseKind::Move,
|
||||
axis: PhaseAxis::Horizontal,
|
||||
},
|
||||
}),
|
||||
steps: vec![MultiphaseStep {
|
||||
node_id: id(1),
|
||||
from: rect(0, 0, 10, 10),
|
||||
|
|
@ -2382,10 +2528,10 @@ mod tests {
|
|||
}]);
|
||||
let plan = MultiphasePlan {
|
||||
phases: vec![MultiphasePhase {
|
||||
action: PhaseAction {
|
||||
action: MultiphasePhaseAction::Uniform(PhaseAction {
|
||||
kind: PhaseKind::Move,
|
||||
axis: PhaseAxis::Horizontal,
|
||||
},
|
||||
}),
|
||||
steps: vec![MultiphaseStep {
|
||||
node_id: id(1),
|
||||
from: rect(5, 0, 15, 10),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue