1
0
Fork 0
forked from wry/wry

Allow proven mixed multiphase actions

This commit is contained in:
atagen 2026-05-21 21:23:56 +10:00
parent 01d1545c40
commit 632873ec5a
2 changed files with 179 additions and 29 deletions

View file

@ -228,6 +228,9 @@ Current pure planner status:
- Nested size redistribution can use hierarchy metadata to decompose two-axis - Nested size redistribution can use hierarchy metadata to decompose two-axis
resizing into parent-axis then child-axis scale phases, but only when the resizing into parent-axis then child-axis scale phases, but only when the
source/target ancestor split depths give a deterministic order. 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 - 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 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. 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 - accepted and rejected plans expose deterministic strategy explanations
- bounded generated split-tree corpus produces identical plans on repeated runs - bounded generated split-tree corpus produces identical plans on repeated runs
- unsupported and invalid candidate plans produce exact expected diagnostics - 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 - 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

@ -135,7 +135,7 @@ pub struct MultiphasePlanExplanation {
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct PhaseExplanation { pub struct PhaseExplanation {
pub action: PhaseAction, pub action: MultiphasePhaseAction,
pub reason: PhaseReason, pub reason: PhaseReason,
pub nodes: Vec<NodeId>, pub nodes: Vec<NodeId>,
} }
@ -150,6 +150,7 @@ pub struct ValidationExplanation {
pub enum PlanStrategy { pub enum PlanStrategy {
NoOp, NoOp,
SingleAction, SingleAction,
MixedSinglePhase,
HierarchyOrderedScales, HierarchyOrderedScales,
SwapLanes { axis: PhaseAxis }, SwapLanes { axis: PhaseAxis },
SpaceThenOrthogonalGrowth { axis: PhaseAxis }, SpaceThenOrthogonalGrowth { axis: PhaseAxis },
@ -173,6 +174,7 @@ pub struct RejectedStrategy {
pub enum PhaseReason { pub enum PhaseReason {
SingleAction, SingleAction,
SameAxisRedistribution, SameAxisRedistribution,
MixedAxisActions,
ShrinkIntoLanes { ShrinkIntoLanes {
lane_axis: PhaseAxis, lane_axis: PhaseAxis,
}, },
@ -191,10 +193,16 @@ pub enum PhaseReason {
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultiphasePhase { pub struct MultiphasePhase {
pub action: PhaseAction, pub action: MultiphasePhaseAction,
pub steps: Vec<MultiphaseStep>, pub steps: Vec<MultiphaseStep>,
} }
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MultiphasePhaseAction {
Uniform(PhaseAction),
Mixed(Vec<PhaseAction>),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct MultiphaseStep { pub struct MultiphaseStep {
pub node_id: NodeId, pub node_id: NodeId,
@ -220,6 +228,32 @@ pub enum PhaseAxis {
Vertical, 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)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum MultiphaseError { pub enum MultiphaseError {
EmptyBounds, EmptyBounds,
@ -273,11 +307,31 @@ pub enum MultiphasePlanFailure {
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum MultiphaseValidationError { pub enum MultiphaseValidationError {
DuplicatePhaseStep { phase: usize, node_id: NodeId }, DuplicatePhaseStep {
UnknownPhaseStep { phase: usize, node_id: NodeId }, phase: usize,
StaleStepStart { phase: usize, node_id: NodeId }, node_id: NodeId,
PhaseOverlap { phase: usize, a: NodeId, b: NodeId }, },
FinalMismatch { 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)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -501,8 +555,10 @@ fn record_rejection(
fn plan_single_action_phase( fn plan_single_action_phase(
request: &MultiphaseRequest, request: &MultiphaseRequest,
) -> Result<MultiphasePlanned, MultiphasePlanFailure> { ) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
let mut action = None; let mut uniform_action = None;
let mut is_uniform = true;
let mut steps = vec![]; let mut steps = vec![];
let mut step_actions = vec![];
for window in &request.windows { for window in &request.windows {
if window.from == window.to { if window.from == window.to {
continue; continue;
@ -528,21 +584,33 @@ fn plan_single_action_phase(
}); });
} }
} }
if action.is_some_and(|action| action != step_action) { if uniform_action.is_some_and(|action| action != step_action) {
return Err(MultiphasePlanFailure::NoPattern); is_uniform = false;
} }
action = Some(step_action); uniform_action.get_or_insert(step_action);
steps.push(step); steps.push(step);
step_actions.push(step_action);
} }
let Some(action) = action else { if steps.is_empty() {
return Err(MultiphasePlanFailure::NoPattern); 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( build_validated_plan(
request, request,
PlanStrategy::SingleAction, PlanStrategy::SingleAction,
[phase_draft( [phase_draft_uniform(
action.kind, action,
action.axis,
steps, steps,
single_action_reason(action), single_action_reason(action),
)], )],
@ -853,9 +921,26 @@ fn plan_space_then_orthogonal_growth(
} }
struct MultiphasePhaseDraft { struct MultiphasePhaseDraft {
action: MultiphasePhaseActionDraft,
steps: Vec<MultiphaseStep>,
reason: PhaseReason,
}
enum MultiphasePhaseActionDraft {
Uniform(PhaseAction),
Mixed(Vec<PhaseAction>),
}
fn phase_draft_uniform(
action: PhaseAction, action: PhaseAction,
steps: Vec<MultiphaseStep>, steps: Vec<MultiphaseStep>,
reason: PhaseReason, reason: PhaseReason,
) -> MultiphasePhaseDraft {
MultiphasePhaseDraft {
action: MultiphasePhaseActionDraft::Uniform(action),
steps,
reason,
}
} }
fn phase_draft( fn phase_draft(
@ -863,9 +948,17 @@ fn phase_draft(
axis: PhaseAxis, axis: PhaseAxis,
steps: Vec<MultiphaseStep>, steps: Vec<MultiphaseStep>,
reason: PhaseReason, 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 {
MultiphasePhaseDraft { MultiphasePhaseDraft {
action: PhaseAction { kind, axis }, action: MultiphasePhaseActionDraft::Mixed(actions),
steps, steps,
reason, 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(); let mut nodes: Vec<_> = draft.steps.iter().map(|step| step.node_id).collect();
nodes.sort_by_key(|node_id| node_id.0); 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 { explanations.push(PhaseExplanation {
action: draft.action, action: action.clone(),
reason: draft.reason, reason: draft.reason,
nodes, nodes,
}); });
Some(MultiphasePhase { Some(MultiphasePhase {
action: draft.action, action,
steps: draft.steps, steps: draft.steps,
}) })
}) })
.collect(); .collect();
for phase in &phases { for phase in &phases {
for step in &phase.steps { for (idx, step) in phase.steps.iter().enumerate() {
if classify_step(*step) != Some(phase.action) { let action = phase.action.action_for_step(idx).unwrap();
if classify_step(*step) != Some(action) {
return Err(MultiphasePlanFailure::InvalidPhaseStep { return Err(MultiphasePlanFailure::InvalidPhaseStep {
action: phase.action, action,
node_id: step.node_id, node_id: step.node_id,
}); });
} }
@ -933,6 +1036,15 @@ fn validate_plan_continuous_diagnostic(
.map(|window| (window.node_id, window.from)) .map(|window| (window.node_id, window.from))
.collect(); .collect();
for (phase_idx, phase) in plan.phases.iter().enumerate() { 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() { for (idx, step) in phase.steps.iter().enumerate() {
if phase.steps[..idx] if phase.steps[..idx]
.iter() .iter()
@ -1526,7 +1638,10 @@ mod tests {
} }
fn actions(plan: &MultiphasePlan) -> Vec<PhaseAction> { 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 { 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)); 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] #[test]
fn generated_nested_size_redistribution_scales_parent_axis_first() { fn generated_nested_size_redistribution_scales_parent_axis_first() {
let old = split( let old = split(
@ -2317,10 +2463,10 @@ mod tests {
]); ]);
let plan = MultiphasePlan { let plan = MultiphasePlan {
phases: vec![MultiphasePhase { phases: vec![MultiphasePhase {
action: PhaseAction { action: MultiphasePhaseAction::Uniform(PhaseAction {
kind: PhaseKind::Move, kind: PhaseKind::Move,
axis: PhaseAxis::Horizontal, axis: PhaseAxis::Horizontal,
}, }),
steps: vec![MultiphaseStep { steps: vec![MultiphaseStep {
node_id: id(1), node_id: id(1),
from: rect(0, 0, 10, 10), from: rect(0, 0, 10, 10),
@ -2357,10 +2503,10 @@ mod tests {
]); ]);
let plan = MultiphasePlan { let plan = MultiphasePlan {
phases: vec![MultiphasePhase { phases: vec![MultiphasePhase {
action: PhaseAction { action: MultiphasePhaseAction::Uniform(PhaseAction {
kind: PhaseKind::Move, kind: PhaseKind::Move,
axis: PhaseAxis::Horizontal, axis: PhaseAxis::Horizontal,
}, }),
steps: vec![MultiphaseStep { steps: vec![MultiphaseStep {
node_id: id(1), node_id: id(1),
from: rect(0, 0, 10, 10), from: rect(0, 0, 10, 10),
@ -2382,10 +2528,10 @@ mod tests {
}]); }]);
let plan = MultiphasePlan { let plan = MultiphasePlan {
phases: vec![MultiphasePhase { phases: vec![MultiphasePhase {
action: PhaseAction { action: MultiphasePhaseAction::Uniform(PhaseAction {
kind: PhaseKind::Move, kind: PhaseKind::Move,
axis: PhaseAxis::Horizontal, axis: PhaseAxis::Horizontal,
}, }),
steps: vec![MultiphaseStep { steps: vec![MultiphaseStep {
node_id: id(1), node_id: id(1),
from: rect(5, 0, 15, 10), from: rect(5, 0, 15, 10),