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

@ -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),