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
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue