Explain multiphase planning decisions
This commit is contained in:
parent
0b6da9d8e0
commit
511e188d16
2 changed files with 398 additions and 62 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue