3570 lines
113 KiB
Rust
3570 lines
113 KiB
Rust
use jay_geometry::Rect;
|
|
|
|
const CURVE_MAX_POINTS: usize = 33;
|
|
const CURVE_FLATNESS_EPSILON: f32 = 0.001;
|
|
const CURVE_MAX_DEPTH: u8 = 8;
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
pub enum AnimationCurve {
|
|
Linear,
|
|
Piecewise(PiecewiseCurve),
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub enum AnimationStyle {
|
|
Plain,
|
|
Multiphase,
|
|
}
|
|
|
|
impl AnimationStyle {
|
|
pub fn from_config(value: u32) -> Option<Self> {
|
|
match value {
|
|
0 => Some(Self::Plain),
|
|
1 => Some(Self::Multiphase),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AnimationCurve {
|
|
pub fn from_config(value: u32) -> Self {
|
|
match value {
|
|
0 => Self::Linear,
|
|
1 => Self::from_cubic_bezier(0.25, 0.1, 0.25, 1.0).unwrap(),
|
|
2 => Self::from_cubic_bezier(0.42, 0.0, 1.0, 1.0).unwrap(),
|
|
4 => Self::from_cubic_bezier(0.42, 0.0, 0.58, 1.0).unwrap(),
|
|
_ => Self::from_cubic_bezier(0.0, 0.0, 0.58, 1.0).unwrap(),
|
|
}
|
|
}
|
|
|
|
pub fn from_cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Option<Self> {
|
|
if !x1.is_finite()
|
|
|| !y1.is_finite()
|
|
|| !x2.is_finite()
|
|
|| !y2.is_finite()
|
|
|| !(0.0..=1.0).contains(&x1)
|
|
|| !(0.0..=1.0).contains(&x2)
|
|
{
|
|
return None;
|
|
}
|
|
Some(Self::Piecewise(PiecewiseCurve::from_cubic_bezier(
|
|
x1, y1, x2, y2,
|
|
)))
|
|
}
|
|
|
|
pub fn sample(self, t: f64) -> f64 {
|
|
let t = t.clamp(0.0, 1.0);
|
|
match self {
|
|
Self::Linear => t,
|
|
Self::Piecewise(curve) => curve.sample(t as f32) as f64,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
pub struct PiecewiseCurve {
|
|
len: u8,
|
|
points: [CurvePoint; CURVE_MAX_POINTS],
|
|
}
|
|
|
|
impl PiecewiseCurve {
|
|
fn from_cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
|
|
let mut points = Vec::with_capacity(CURVE_MAX_POINTS);
|
|
let p0 = cubic_bezier_point(x1, y1, x2, y2, 0.0);
|
|
let p1 = cubic_bezier_point(x1, y1, x2, y2, 1.0);
|
|
points.push(p0);
|
|
flatten_cubic_bezier(&mut points, (x1, y1, x2, y2), 0.0, p0, 1.0, p1, 0);
|
|
let mut array = [CurvePoint::default(); CURVE_MAX_POINTS];
|
|
let len = points.len().min(CURVE_MAX_POINTS);
|
|
array[..len].copy_from_slice(&points[..len]);
|
|
Self {
|
|
len: len as u8,
|
|
points: array,
|
|
}
|
|
}
|
|
|
|
fn sample(self, x: f32) -> f32 {
|
|
let len = self.len as usize;
|
|
if len <= 1 {
|
|
return x;
|
|
}
|
|
let points = &self.points[..len];
|
|
if x <= points[0].x {
|
|
return points[0].y;
|
|
}
|
|
if x >= points[len - 1].x {
|
|
return points[len - 1].y;
|
|
}
|
|
let mut lo = 0;
|
|
let mut hi = len - 1;
|
|
while lo + 1 < hi {
|
|
let mid = (lo + hi) / 2;
|
|
if points[mid].x <= x {
|
|
lo = mid;
|
|
} else {
|
|
hi = mid;
|
|
}
|
|
}
|
|
let from = points[lo];
|
|
let to = points[hi];
|
|
if to.x <= from.x {
|
|
return to.y;
|
|
}
|
|
let t = (x - from.x) / (to.x - from.x);
|
|
from.y + (to.y - from.y) * t
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
|
struct CurvePoint {
|
|
x: f32,
|
|
y: f32,
|
|
}
|
|
|
|
fn flatten_cubic_bezier(
|
|
points: &mut Vec<CurvePoint>,
|
|
controls: (f32, f32, f32, f32),
|
|
t0: f32,
|
|
p0: CurvePoint,
|
|
t1: f32,
|
|
p1: CurvePoint,
|
|
depth: u8,
|
|
) {
|
|
let tm = (t0 + t1) * 0.5;
|
|
let pm = cubic_bezier_point(controls.0, controls.1, controls.2, controls.3, tm);
|
|
let projected_y = if p1.x <= p0.x {
|
|
(p0.y + p1.y) * 0.5
|
|
} else {
|
|
let tx = (pm.x - p0.x) / (p1.x - p0.x);
|
|
p0.y + (p1.y - p0.y) * tx
|
|
};
|
|
if (pm.y - projected_y).abs() > CURVE_FLATNESS_EPSILON
|
|
&& depth < CURVE_MAX_DEPTH
|
|
&& points.len() + 2 < CURVE_MAX_POINTS
|
|
{
|
|
flatten_cubic_bezier(points, controls, t0, p0, tm, pm, depth + 1);
|
|
flatten_cubic_bezier(points, controls, tm, pm, t1, p1, depth + 1);
|
|
} else {
|
|
points.push(p1);
|
|
}
|
|
}
|
|
|
|
fn cubic_bezier_point(x1: f32, y1: f32, x2: f32, y2: f32, t: f32) -> CurvePoint {
|
|
fn bezier(a: f32, b: f32, t: f32) -> f32 {
|
|
let inv = 1.0 - t;
|
|
3.0 * inv * inv * t * a + 3.0 * inv * t * t * b + t * t * t
|
|
}
|
|
CurvePoint {
|
|
x: bezier(x1, x2, t),
|
|
y: bezier(y1, y2, t),
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
|
pub struct NodeId(pub u32);
|
|
|
|
const MIN_SHRINK_DENOMINATOR: i32 = 8;
|
|
// Integer split remainders can make swapped siblings differ by one pixel. Do
|
|
// not spend a full animation phase on that imperceptible bookkeeping step.
|
|
const SWAP_AXIS_SNAP_PX: i32 = 1;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct MultiphaseRequest {
|
|
pub bounds: Rect,
|
|
pub windows: Vec<MultiphaseWindow>,
|
|
pub clearance: i32,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub struct MultiphaseWindow {
|
|
pub node_id: NodeId,
|
|
pub from: Rect,
|
|
pub to: Rect,
|
|
pub hierarchy: MultiphaseWindowHierarchy,
|
|
}
|
|
|
|
impl MultiphaseWindow {
|
|
pub fn new(node_id: impl Into<NodeId>, from: Rect, to: Rect) -> Self {
|
|
Self {
|
|
node_id: node_id.into(),
|
|
from,
|
|
to,
|
|
hierarchy: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn with_hierarchy(
|
|
node_id: impl Into<NodeId>,
|
|
from: Rect,
|
|
to: Rect,
|
|
hierarchy: MultiphaseWindowHierarchy,
|
|
) -> Self {
|
|
Self {
|
|
node_id: node_id.into(),
|
|
from,
|
|
to,
|
|
hierarchy,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
|
pub struct MultiphaseWindowHierarchy {
|
|
pub source: MultiphaseHierarchyPosition,
|
|
pub target: MultiphaseHierarchyPosition,
|
|
pub transition: MultiphaseHierarchyTransition,
|
|
}
|
|
|
|
impl MultiphaseWindowHierarchy {
|
|
pub fn new(source: MultiphaseHierarchyPosition, target: MultiphaseHierarchyPosition) -> Self {
|
|
let transition = if !source.parent_is_mono && target.parent_is_mono {
|
|
MultiphaseHierarchyTransition::EnteringMono
|
|
} else if source.parent_is_mono && !target.parent_is_mono {
|
|
MultiphaseHierarchyTransition::ExitingMono
|
|
} else if source.parent.is_none() || target.parent.is_none() {
|
|
MultiphaseHierarchyTransition::Unknown
|
|
} else if target.depth < source.depth {
|
|
MultiphaseHierarchyTransition::Ascending
|
|
} else if target.depth > source.depth {
|
|
MultiphaseHierarchyTransition::Descending
|
|
} else {
|
|
MultiphaseHierarchyTransition::SameLevel
|
|
};
|
|
Self {
|
|
source,
|
|
target,
|
|
transition,
|
|
}
|
|
}
|
|
|
|
fn reversed(self) -> Self {
|
|
Self {
|
|
source: self.target,
|
|
target: self.source,
|
|
transition: self.transition.reversed(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
|
pub struct MultiphaseHierarchyPosition {
|
|
pub parent: Option<NodeId>,
|
|
pub depth: u16,
|
|
pub sibling_index: Option<u16>,
|
|
pub split_axis: Option<PhaseAxis>,
|
|
pub nearest_horizontal_split_depth: Option<u16>,
|
|
pub nearest_vertical_split_depth: Option<u16>,
|
|
pub parent_is_mono: bool,
|
|
pub mono_active: bool,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
|
pub enum MultiphaseHierarchyTransition {
|
|
#[default]
|
|
Unknown,
|
|
SameLevel,
|
|
Ascending,
|
|
Descending,
|
|
EnteringMono,
|
|
ExitingMono,
|
|
}
|
|
|
|
impl MultiphaseHierarchyTransition {
|
|
fn reversed(self) -> Self {
|
|
match self {
|
|
Self::Unknown => Self::Unknown,
|
|
Self::SameLevel => Self::SameLevel,
|
|
Self::Ascending => Self::Descending,
|
|
Self::Descending => Self::Ascending,
|
|
Self::EnteringMono => Self::ExitingMono,
|
|
Self::ExitingMono => Self::EnteringMono,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct MultiphasePlan {
|
|
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: MultiphasePhaseAction,
|
|
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,
|
|
MixedSinglePhase,
|
|
HierarchyOrderedScales,
|
|
OrientationChange { from_axis: PhaseAxis },
|
|
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,
|
|
MixedAxisActions,
|
|
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)]
|
|
pub struct MultiphasePhase {
|
|
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,
|
|
pub from: Rect,
|
|
pub to: Rect,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub struct PhaseAction {
|
|
pub kind: PhaseKind,
|
|
pub axis: PhaseAxis,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub enum PhaseKind {
|
|
Move,
|
|
Scale,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub enum PhaseAxis {
|
|
Horizontal,
|
|
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(),
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(not(test), expect(dead_code))]
|
|
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,
|
|
EmptyWindow,
|
|
DuplicateWindow,
|
|
InitialOverlap,
|
|
FinalOverlap,
|
|
NoPlan,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct MultiphasePlanDiagnostic {
|
|
pub forward: MultiphasePlanFailure,
|
|
pub reverse: Option<MultiphasePlanFailure>,
|
|
pub attempted: Vec<RejectedStrategy>,
|
|
}
|
|
|
|
impl MultiphasePlanDiagnostic {
|
|
fn legacy_error(self) -> MultiphaseError {
|
|
match self.forward {
|
|
MultiphasePlanFailure::Request(error) => error,
|
|
_ => MultiphaseError::NoPlan,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ValidationExplanation {
|
|
fn passed() -> Self {
|
|
Self {
|
|
continuous_overlap_passed: true,
|
|
final_rects_matched: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub enum MultiphasePlanFailure {
|
|
Request(MultiphaseError),
|
|
NoPattern,
|
|
ShrinkBound {
|
|
axis: PhaseAxis,
|
|
available: i32,
|
|
required: i32,
|
|
},
|
|
InvalidPhaseStep {
|
|
action: PhaseAction,
|
|
node_id: NodeId,
|
|
},
|
|
Validation(MultiphaseValidationError),
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub enum MultiphaseValidationError {
|
|
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)]
|
|
struct PlanForwardFailure {
|
|
reason: MultiphasePlanFailure,
|
|
attempted: Vec<RejectedStrategy>,
|
|
}
|
|
|
|
pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result<MultiphasePlan, MultiphaseError> {
|
|
plan_no_overlap_with_diagnostics(request).map_err(|diagnostic| diagnostic.legacy_error())
|
|
}
|
|
|
|
pub fn plan_no_overlap_with_diagnostics(
|
|
request: &MultiphaseRequest,
|
|
) -> 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) {
|
|
return Err(MultiphasePlanDiagnostic {
|
|
forward: MultiphasePlanFailure::Request(error),
|
|
reverse: None,
|
|
attempted: vec![],
|
|
});
|
|
}
|
|
if request
|
|
.windows
|
|
.iter()
|
|
.all(|window| window.from == window.to)
|
|
{
|
|
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) {
|
|
return Err(MultiphasePlanDiagnostic {
|
|
forward: failure,
|
|
reverse: None,
|
|
attempted: vec![],
|
|
});
|
|
}
|
|
let forward = match plan_forward(request, PlanDirection::Forward) {
|
|
Ok(plan) => return Ok(plan),
|
|
Err(error) => error,
|
|
};
|
|
let reversed = reverse_request(request);
|
|
match plan_forward(&reversed, PlanDirection::Reverse) {
|
|
Ok(plan) => Ok(reverse_planned(plan)),
|
|
Err(reverse) => {
|
|
let mut attempted = forward.attempted;
|
|
attempted.extend(reverse.attempted);
|
|
Err(MultiphasePlanDiagnostic {
|
|
forward: forward.reason,
|
|
reverse: Some(reverse.reason),
|
|
attempted,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn validate_phase_paths(
|
|
request: &MultiphaseRequest,
|
|
paths: &[Vec<(Rect, Rect)>],
|
|
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
|
if paths.len() != request.windows.len() {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
let phase_count = paths.iter().map(Vec::len).max().unwrap_or(0);
|
|
if phase_count == 0 {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
let mut phases = vec![];
|
|
for phase_idx in 0..phase_count {
|
|
let mut steps = vec![];
|
|
let mut actions = vec![];
|
|
for (window_idx, path) in paths.iter().enumerate() {
|
|
let Some((from, to)) = path.get(phase_idx).copied() else {
|
|
continue;
|
|
};
|
|
if from == to {
|
|
continue;
|
|
}
|
|
let step = MultiphaseStep {
|
|
node_id: request.windows[window_idx].node_id,
|
|
from,
|
|
to,
|
|
};
|
|
let Some(action) = classify_step(step) else {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
};
|
|
steps.push(step);
|
|
actions.push(action);
|
|
}
|
|
if !steps.is_empty() {
|
|
phases.push(MultiphasePhase {
|
|
action: MultiphasePhaseAction::from_step_actions(actions),
|
|
steps,
|
|
});
|
|
}
|
|
}
|
|
let plan = MultiphasePlan { phases };
|
|
validate_plan_continuous_diagnostic(request, &plan)
|
|
.map(|_| plan)
|
|
.map_err(MultiphasePlanFailure::Validation)
|
|
}
|
|
|
|
pub fn partition_motion_groups(
|
|
windows: &[MultiphaseWindow],
|
|
clearance: i32,
|
|
) -> Vec<Vec<usize>> {
|
|
let clearance = clearance.max(0);
|
|
let mut groups = vec![];
|
|
let mut seen = vec![false; windows.len()];
|
|
for start in 0..windows.len() {
|
|
if seen[start] {
|
|
continue;
|
|
}
|
|
seen[start] = true;
|
|
let mut group = vec![];
|
|
let mut pending = vec![start];
|
|
while let Some(idx) = pending.pop() {
|
|
group.push(idx);
|
|
let bounds = motion_bounds_with_clearance(windows[idx], clearance);
|
|
for other in 0..windows.len() {
|
|
if seen[other]
|
|
|| !bounds.intersects(&motion_bounds_with_clearance(windows[other], clearance))
|
|
{
|
|
continue;
|
|
}
|
|
seen[other] = true;
|
|
pending.push(other);
|
|
}
|
|
}
|
|
group.sort_unstable();
|
|
groups.push(group);
|
|
}
|
|
groups
|
|
}
|
|
|
|
fn validate_request(request: &MultiphaseRequest) -> Result<(), MultiphaseError> {
|
|
if request.bounds.is_empty() {
|
|
return Err(MultiphaseError::EmptyBounds);
|
|
}
|
|
for (idx, window) in request.windows.iter().enumerate() {
|
|
if window.from.is_empty() || window.to.is_empty() {
|
|
return Err(MultiphaseError::EmptyWindow);
|
|
}
|
|
for other in &request.windows[..idx] {
|
|
if other.node_id == window.node_id {
|
|
return Err(MultiphaseError::DuplicateWindow);
|
|
}
|
|
}
|
|
}
|
|
if overlaps(request.windows.iter().map(|window| window.from)) {
|
|
return Err(MultiphaseError::InitialOverlap);
|
|
}
|
|
if overlaps(request.windows.iter().map(|window| window.to)) {
|
|
return Err(MultiphaseError::FinalOverlap);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn target_shrink_bound_failure(request: &MultiphaseRequest) -> Option<MultiphasePlanFailure> {
|
|
let min_width = sane_min_size(request.bounds.width());
|
|
let min_height = sane_min_size(request.bounds.height());
|
|
for window in &request.windows {
|
|
if window.to.width() < min_width {
|
|
return Some(MultiphasePlanFailure::ShrinkBound {
|
|
axis: PhaseAxis::Horizontal,
|
|
available: window.to.width(),
|
|
required: min_width,
|
|
});
|
|
}
|
|
if window.to.height() < min_height {
|
|
return Some(MultiphasePlanFailure::ShrinkBound {
|
|
axis: PhaseAxis::Vertical,
|
|
available: window.to.height(),
|
|
required: min_height,
|
|
});
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn plan_forward(
|
|
request: &MultiphaseRequest,
|
|
direction: PlanDirection,
|
|
) -> Result<MultiphasePlanned, PlanForwardFailure> {
|
|
let mut rejection = None;
|
|
let mut attempted = vec![];
|
|
match plan_single_action_phase(request) {
|
|
Ok(plan) => return Ok(plan),
|
|
Err(error) => {
|
|
record_rejection(&mut attempted, direction, PlanStrategy::SingleAction, error);
|
|
if error != MultiphasePlanFailure::NoPattern {
|
|
rejection.get_or_insert(error);
|
|
}
|
|
}
|
|
}
|
|
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
|
match plan_space_then_orthogonal_growth(request, axis) {
|
|
Ok(plan) => return Ok(plan),
|
|
Err(error) => {
|
|
record_rejection(
|
|
&mut attempted,
|
|
direction,
|
|
PlanStrategy::SpaceThenOrthogonalGrowth { axis },
|
|
error,
|
|
);
|
|
if error != MultiphasePlanFailure::NoPattern {
|
|
rejection.get_or_insert(error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
match plan_hierarchy_ordered_axis_scales(request) {
|
|
Ok(plan) => return Ok(plan),
|
|
Err(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] {
|
|
match plan_orientation_change(request, axis) {
|
|
Ok(plan) => return Ok(plan),
|
|
Err(error) => {
|
|
record_rejection(
|
|
&mut attempted,
|
|
direction,
|
|
PlanStrategy::OrientationChange { from_axis: axis },
|
|
error,
|
|
);
|
|
if error != MultiphasePlanFailure::NoPattern {
|
|
rejection.get_or_insert(error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
|
match plan_axis_crossing_lanes(request, axis) {
|
|
Ok(plan) => return Ok(plan),
|
|
Err(error) => {
|
|
record_rejection(
|
|
&mut attempted,
|
|
direction,
|
|
PlanStrategy::SwapLanes { axis },
|
|
error,
|
|
);
|
|
if error != MultiphasePlanFailure::NoPattern {
|
|
rejection.get_or_insert(error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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(
|
|
request: &MultiphaseRequest,
|
|
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
|
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;
|
|
}
|
|
let step = MultiphaseStep {
|
|
node_id: window.node_id,
|
|
from: window.from,
|
|
to: window.to,
|
|
};
|
|
let Some(step_action) = classify_step(step) else {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
};
|
|
if step_action.kind == PhaseKind::Scale {
|
|
let (available, required) = match step_action.axis {
|
|
PhaseAxis::Horizontal => (window.to.width(), sane_min_size(request.bounds.width())),
|
|
PhaseAxis::Vertical => (window.to.height(), sane_min_size(request.bounds.height())),
|
|
};
|
|
if available < required {
|
|
return Err(MultiphasePlanFailure::ShrinkBound {
|
|
axis: step_action.axis,
|
|
available,
|
|
required,
|
|
});
|
|
}
|
|
}
|
|
if uniform_action.is_some_and(|action| action != step_action) {
|
|
is_uniform = false;
|
|
}
|
|
uniform_action.get_or_insert(step_action);
|
|
steps.push(step);
|
|
step_actions.push(step_action);
|
|
}
|
|
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_uniform(
|
|
action,
|
|
steps,
|
|
single_action_reason(action),
|
|
)],
|
|
)
|
|
}
|
|
|
|
fn plan_hierarchy_ordered_axis_scales(
|
|
request: &MultiphaseRequest,
|
|
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
|
let mut changed_axes = vec![];
|
|
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
|
if request
|
|
.windows
|
|
.iter()
|
|
.any(|window| interval_changed(window.from, window.to, axis))
|
|
{
|
|
changed_axes.push(axis);
|
|
}
|
|
}
|
|
let [first_axis, second_axis] = changed_axes
|
|
.try_into()
|
|
.map_err(|_| MultiphasePlanFailure::NoPattern)?;
|
|
let order = hierarchy_scale_axis_order(request, first_axis, second_axis)
|
|
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
|
let mut current: Vec<_> = request
|
|
.windows
|
|
.iter()
|
|
.map(|window| (window.node_id, window.from))
|
|
.collect();
|
|
let mut phases = vec![];
|
|
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![];
|
|
for window in &request.windows {
|
|
let (_, rect) = current
|
|
.iter_mut()
|
|
.find(|(node_id, _)| *node_id == window.node_id)
|
|
.unwrap();
|
|
let next = with_main_interval(
|
|
*rect,
|
|
axis,
|
|
main_start(window.to, axis),
|
|
main_end(window.to, axis),
|
|
);
|
|
if next == *rect {
|
|
continue;
|
|
}
|
|
if main_size(*rect, axis) == main_size(next, axis) {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
steps.push(MultiphaseStep {
|
|
node_id: window.node_id,
|
|
from: *rect,
|
|
to: next,
|
|
});
|
|
*rect = next;
|
|
}
|
|
if steps.is_empty() {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
phases.push(phase_draft(PhaseKind::Scale, axis, steps, reason));
|
|
}
|
|
let [first, second] = phases
|
|
.try_into()
|
|
.map_err(|_| MultiphasePlanFailure::NoPattern)?;
|
|
build_validated_plan(
|
|
request,
|
|
PlanStrategy::HierarchyOrderedScales,
|
|
[first, second],
|
|
)
|
|
}
|
|
|
|
fn hierarchy_scale_axis_order(
|
|
request: &MultiphaseRequest,
|
|
first_axis: PhaseAxis,
|
|
second_axis: PhaseAxis,
|
|
) -> Option<HierarchyScaleAxisOrder> {
|
|
let first_priority = hierarchy_axis_priority(request, first_axis)?;
|
|
let second_priority = hierarchy_axis_priority(request, second_axis)?;
|
|
match first_priority.cmp(&second_priority) {
|
|
std::cmp::Ordering::Less => Some(HierarchyScaleAxisOrder {
|
|
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,
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct HierarchyScaleAxisOrder {
|
|
axes: [PhaseAxis; 2],
|
|
depths: [u16; 2],
|
|
}
|
|
|
|
fn hierarchy_axis_priority(request: &MultiphaseRequest, axis: PhaseAxis) -> Option<u16> {
|
|
request
|
|
.windows
|
|
.iter()
|
|
.filter(|window| interval_changed(window.from, window.to, axis))
|
|
.flat_map(|window| {
|
|
[
|
|
split_depth_for_axis(window.hierarchy.source, axis),
|
|
split_depth_for_axis(window.hierarchy.target, axis),
|
|
]
|
|
})
|
|
.flatten()
|
|
.min()
|
|
}
|
|
|
|
fn plan_axis_crossing_lanes(
|
|
request: &MultiphaseRequest,
|
|
axis: PhaseAxis,
|
|
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
|
let moving_windows: Vec<_> = request
|
|
.windows
|
|
.iter()
|
|
.copied()
|
|
.filter(|window| window.from != window.to)
|
|
.collect();
|
|
if moving_windows.len() < 2 {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
let orth_min = request
|
|
.windows
|
|
.iter()
|
|
.map(|window| orth_start(window.from, axis))
|
|
.min()
|
|
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
|
let orth_max = request
|
|
.windows
|
|
.iter()
|
|
.map(|window| orth_end(window.from, axis))
|
|
.max()
|
|
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
|
if moving_windows.iter().any(|window| {
|
|
orth_start(window.from, axis) != orth_min
|
|
|| orth_end(window.from, axis) != orth_max
|
|
|| orth_start(window.to, axis) != orth_min
|
|
|| orth_end(window.to, axis) != orth_max
|
|
|| main_start(window.from, axis) == main_start(window.to, axis)
|
|
}) {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
let clearance = request.clearance.max(0);
|
|
let lane_count = moving_windows.len() as i32;
|
|
let available = (orth_max - orth_min) - clearance.saturating_mul(lane_count - 1);
|
|
if available <= 0 {
|
|
return Err(MultiphasePlanFailure::ShrinkBound {
|
|
axis: axis.other(),
|
|
available: 0,
|
|
required: sane_min_size(orth_max - orth_min),
|
|
});
|
|
}
|
|
let lane_size = available / lane_count;
|
|
let mut lane_remainder = available % lane_count;
|
|
let required = sane_min_size(orth_max - orth_min);
|
|
if lane_size < required {
|
|
return Err(MultiphasePlanFailure::ShrinkBound {
|
|
axis: axis.other(),
|
|
available: lane_size,
|
|
required,
|
|
});
|
|
}
|
|
|
|
let mut windows = moving_windows;
|
|
windows.sort_by_key(|window| lane_sort_key(*window, axis));
|
|
let mut phase1 = vec![];
|
|
let mut phase2 = vec![];
|
|
let mut phase3 = vec![];
|
|
let mut phase4 = vec![];
|
|
let mut lane_start = orth_min;
|
|
for (idx, window) in windows.iter().enumerate() {
|
|
let extra = if lane_remainder > 0 {
|
|
lane_remainder -= 1;
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
let lane_end = lane_start + lane_size + extra;
|
|
let lane_from = with_orth_interval(window.from, axis, lane_start, lane_end);
|
|
let lane_to = with_main_interval(
|
|
lane_from,
|
|
axis,
|
|
main_start(window.to, axis),
|
|
main_end(window.to, axis),
|
|
);
|
|
let mut lane_move = crossing_lane_move_rect(lane_from, window.to, axis);
|
|
if same_axis_delta_within(lane_move, lane_to, axis, SWAP_AXIS_SNAP_PX) {
|
|
lane_move = lane_to;
|
|
}
|
|
push_step(&mut phase1, window.node_id, window.from, lane_from);
|
|
push_step(&mut phase2, window.node_id, lane_from, lane_move);
|
|
push_step(&mut phase3, window.node_id, lane_move, lane_to);
|
|
push_step(&mut phase4, window.node_id, lane_to, window.to);
|
|
if idx + 1 < windows.len() {
|
|
lane_start = lane_end + clearance;
|
|
}
|
|
}
|
|
build_validated_plan(
|
|
request,
|
|
PlanStrategy::SwapLanes { axis },
|
|
[
|
|
phase_draft(
|
|
PhaseKind::Scale,
|
|
axis.other(),
|
|
phase1,
|
|
PhaseReason::ShrinkIntoLanes {
|
|
lane_axis: axis.other(),
|
|
},
|
|
),
|
|
phase_draft_classified(
|
|
phase2,
|
|
PhaseReason::MoveThroughFreedSpace,
|
|
)?,
|
|
phase_draft(
|
|
PhaseKind::Scale,
|
|
axis,
|
|
phase3,
|
|
PhaseReason::SameAxisRedistribution,
|
|
),
|
|
phase_draft(
|
|
PhaseKind::Scale,
|
|
axis.other(),
|
|
phase4,
|
|
PhaseReason::GrowOutOfLanes,
|
|
),
|
|
],
|
|
)
|
|
}
|
|
|
|
fn phase_draft_classified(
|
|
steps: Vec<MultiphaseStep>,
|
|
reason: PhaseReason,
|
|
) -> Result<MultiphasePhaseDraft, MultiphasePlanFailure> {
|
|
let actions = steps
|
|
.iter()
|
|
.map(|step| classify_step(*step).ok_or(MultiphasePlanFailure::NoPattern))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
Ok(phase_draft_mixed(steps, actions, reason))
|
|
}
|
|
|
|
fn crossing_lane_move_rect(from: Rect, target: Rect, axis: PhaseAxis) -> Rect {
|
|
let size = main_size(from, axis);
|
|
if main_start(target, axis) > main_start(from, axis) {
|
|
let end = main_end(target, axis);
|
|
with_main_interval(from, axis, end - size, end)
|
|
} else {
|
|
let start = main_start(target, axis);
|
|
with_main_interval(from, axis, start, start + size)
|
|
}
|
|
}
|
|
|
|
fn same_axis_delta_within(from: Rect, to: Rect, axis: PhaseAxis, max_delta: i32) -> bool {
|
|
let start_delta = (main_start(from, axis) - main_start(to, axis)).abs();
|
|
let end_delta = (main_end(from, axis) - main_end(to, axis)).abs();
|
|
start_delta.max(end_delta) <= max_delta
|
|
}
|
|
|
|
fn lane_sort_key(window: MultiphaseWindow, axis: PhaseAxis) -> (usize, i32, i32, u32) {
|
|
let delta = main_start(window.to, axis) - main_start(window.from, axis);
|
|
let direction = match delta.cmp(&0) {
|
|
std::cmp::Ordering::Greater => 0,
|
|
std::cmp::Ordering::Less => 1,
|
|
std::cmp::Ordering::Equal => 2,
|
|
};
|
|
(
|
|
direction,
|
|
main_start(window.from, axis),
|
|
main_start(window.to, axis),
|
|
window.node_id.0,
|
|
)
|
|
}
|
|
|
|
fn plan_space_then_orthogonal_growth(
|
|
request: &MultiphaseRequest,
|
|
axis: PhaseAxis,
|
|
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
|
if request.windows.len() < 2 {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
let orth_axis = axis.other();
|
|
let min_width = sane_min_size(request.bounds.width());
|
|
let min_height = sane_min_size(request.bounds.height());
|
|
let mut phase1 = vec![];
|
|
let mut phase2 = vec![];
|
|
let mut phase3 = vec![];
|
|
for window in &request.windows {
|
|
if window.to.width() < min_width {
|
|
return Err(MultiphasePlanFailure::ShrinkBound {
|
|
axis: PhaseAxis::Horizontal,
|
|
available: window.to.width(),
|
|
required: min_width,
|
|
});
|
|
}
|
|
if window.to.height() < min_height {
|
|
return Err(MultiphasePlanFailure::ShrinkBound {
|
|
axis: PhaseAxis::Vertical,
|
|
available: window.to.height(),
|
|
required: min_height,
|
|
});
|
|
}
|
|
let main_changes = main_start(window.from, axis) != main_start(window.to, axis)
|
|
|| main_end(window.from, axis) != main_end(window.to, axis);
|
|
let orth_changes = orth_start(window.from, axis) != orth_start(window.to, axis)
|
|
|| orth_end(window.from, axis) != orth_end(window.to, axis);
|
|
let mut orth_from = window.from;
|
|
if main_changes && main_size(window.from, axis) == main_size(window.to, axis) {
|
|
let after_move = with_main_interval(
|
|
window.from,
|
|
axis,
|
|
main_start(window.to, axis),
|
|
main_end(window.to, axis),
|
|
);
|
|
push_step(&mut phase2, window.node_id, window.from, after_move);
|
|
orth_from = after_move;
|
|
} else if main_changes {
|
|
let target_size = main_size(window.to, axis);
|
|
let after_main_scale = if main_start(window.from, axis) == main_start(window.to, axis)
|
|
|| main_end(window.from, axis) == main_end(window.to, axis)
|
|
{
|
|
with_main_interval(
|
|
window.from,
|
|
axis,
|
|
main_start(window.to, axis),
|
|
main_end(window.to, axis),
|
|
)
|
|
} else if main_start(window.to, axis) < main_start(window.from, axis) {
|
|
with_main_interval(
|
|
window.from,
|
|
axis,
|
|
main_end(window.from, axis) - target_size,
|
|
main_end(window.from, axis),
|
|
)
|
|
} else {
|
|
with_main_interval(
|
|
window.from,
|
|
axis,
|
|
main_start(window.from, axis),
|
|
main_start(window.from, axis) + target_size,
|
|
)
|
|
};
|
|
push_step(&mut phase1, window.node_id, window.from, after_main_scale);
|
|
orth_from = after_main_scale;
|
|
if main_start(after_main_scale, axis) != main_start(window.to, axis)
|
|
|| main_end(after_main_scale, axis) != main_end(window.to, axis)
|
|
{
|
|
let after_move = with_main_interval(
|
|
after_main_scale,
|
|
axis,
|
|
main_start(window.to, axis),
|
|
main_end(window.to, axis),
|
|
);
|
|
push_step(&mut phase2, window.node_id, after_main_scale, after_move);
|
|
orth_from = after_move;
|
|
}
|
|
}
|
|
if orth_changes {
|
|
push_step(&mut phase3, window.node_id, orth_from, window.to);
|
|
}
|
|
}
|
|
if phase1.is_empty() || phase2.is_empty() || phase3.is_empty() {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
build_validated_plan(
|
|
request,
|
|
PlanStrategy::SpaceThenOrthogonalGrowth { axis },
|
|
[
|
|
phase_draft(
|
|
PhaseKind::Scale,
|
|
axis,
|
|
phase1,
|
|
PhaseReason::CreateSpaceForAscendingChild,
|
|
),
|
|
phase_draft(
|
|
PhaseKind::Move,
|
|
axis,
|
|
phase2,
|
|
PhaseReason::MoveAscendingChildAfterSpaceExists,
|
|
),
|
|
phase_draft(
|
|
PhaseKind::Scale,
|
|
orth_axis,
|
|
phase3,
|
|
PhaseReason::OrthogonalGrowthAfterMove,
|
|
),
|
|
],
|
|
)
|
|
}
|
|
|
|
fn plan_orientation_change(
|
|
request: &MultiphaseRequest,
|
|
from_axis: PhaseAxis,
|
|
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
|
if request.windows.len() < 2 {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
let to_axis = from_axis.other();
|
|
let min_lane_size = sane_min_size(main_size(request.bounds, to_axis));
|
|
let target_start = request
|
|
.windows
|
|
.first()
|
|
.map(|window| main_start(window.to, from_axis))
|
|
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
|
let target_end = request
|
|
.windows
|
|
.first()
|
|
.map(|window| main_end(window.to, from_axis))
|
|
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
|
let source_start = request
|
|
.windows
|
|
.first()
|
|
.map(|window| main_start(window.from, to_axis))
|
|
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
|
let source_end = request
|
|
.windows
|
|
.first()
|
|
.map(|window| main_end(window.from, to_axis))
|
|
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
|
if request.windows.iter().any(|window| {
|
|
main_start(window.from, to_axis) != source_start
|
|
|| main_end(window.from, to_axis) != source_end
|
|
|| main_start(window.to, from_axis) != target_start
|
|
|| main_end(window.to, from_axis) != target_end
|
|
|| main_size(window.to, to_axis) < min_lane_size
|
|
}) {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
|
|
let mut phase1 = vec![];
|
|
let mut phase2 = vec![];
|
|
let mut phase3 = vec![];
|
|
for window in &request.windows {
|
|
let lane = with_main_interval(
|
|
window.from,
|
|
to_axis,
|
|
main_start(window.to, to_axis),
|
|
main_end(window.to, to_axis),
|
|
);
|
|
let moved = with_main_interval(
|
|
lane,
|
|
from_axis,
|
|
main_start(window.to, from_axis),
|
|
main_start(window.to, from_axis) + main_size(lane, from_axis),
|
|
);
|
|
push_step(&mut phase1, window.node_id, window.from, lane);
|
|
push_step(&mut phase2, window.node_id, lane, moved);
|
|
push_step(&mut phase3, window.node_id, moved, window.to);
|
|
}
|
|
if phase1.is_empty() || phase3.is_empty() {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
build_validated_plan(
|
|
request,
|
|
PlanStrategy::OrientationChange { from_axis },
|
|
[
|
|
phase_draft(
|
|
PhaseKind::Scale,
|
|
to_axis,
|
|
phase1,
|
|
PhaseReason::ShrinkIntoLanes { lane_axis: to_axis },
|
|
),
|
|
phase_draft(
|
|
PhaseKind::Move,
|
|
from_axis,
|
|
phase2,
|
|
PhaseReason::MoveThroughFreedSpace,
|
|
),
|
|
phase_draft(
|
|
PhaseKind::Scale,
|
|
from_axis,
|
|
phase3,
|
|
PhaseReason::GrowOutOfLanes,
|
|
),
|
|
],
|
|
)
|
|
}
|
|
|
|
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(
|
|
kind: PhaseKind,
|
|
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: MultiphasePhaseActionDraft::Mixed(actions),
|
|
steps,
|
|
reason,
|
|
}
|
|
}
|
|
|
|
fn build_validated_plan<const N: usize>(
|
|
request: &MultiphaseRequest,
|
|
strategy: PlanStrategy,
|
|
phases: [MultiphasePhaseDraft; N],
|
|
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
|
let mut explanations = vec![];
|
|
let phases: Vec<_> = phases
|
|
.into_iter()
|
|
.filter_map(|draft| {
|
|
if draft.steps.is_empty() {
|
|
return None;
|
|
}
|
|
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: action.clone(),
|
|
reason: draft.reason,
|
|
nodes,
|
|
});
|
|
Some(MultiphasePhase {
|
|
action,
|
|
steps: draft.steps,
|
|
})
|
|
})
|
|
.collect();
|
|
for phase in &phases {
|
|
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,
|
|
node_id: step.node_id,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
let plan = MultiphasePlan { phases };
|
|
validate_plan_continuous_diagnostic(request, &plan)
|
|
.map(|_| MultiphasePlanned {
|
|
plan,
|
|
explanation: MultiphasePlanExplanation {
|
|
strategy,
|
|
phases: explanations,
|
|
validation: ValidationExplanation::passed(),
|
|
},
|
|
})
|
|
.map_err(MultiphasePlanFailure::Validation)
|
|
}
|
|
|
|
#[cfg_attr(not(test), expect(dead_code))]
|
|
fn validate_plan_continuous(request: &MultiphaseRequest, plan: &MultiphasePlan) -> bool {
|
|
validate_plan_continuous_diagnostic(request, plan).is_ok()
|
|
}
|
|
|
|
fn validate_plan_continuous_diagnostic(
|
|
request: &MultiphaseRequest,
|
|
plan: &MultiphasePlan,
|
|
) -> Result<(), MultiphaseValidationError> {
|
|
let mut current: Vec<_> = request
|
|
.windows
|
|
.iter()
|
|
.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()
|
|
.any(|prev| prev.node_id == step.node_id)
|
|
{
|
|
return Err(MultiphaseValidationError::DuplicatePhaseStep {
|
|
phase: phase_idx,
|
|
node_id: step.node_id,
|
|
});
|
|
}
|
|
let Some((_, rect)) = current.iter().find(|(node_id, _)| *node_id == step.node_id)
|
|
else {
|
|
return Err(MultiphaseValidationError::UnknownPhaseStep {
|
|
phase: phase_idx,
|
|
node_id: step.node_id,
|
|
});
|
|
};
|
|
if *rect != step.from {
|
|
return Err(MultiphaseValidationError::StaleStepStart {
|
|
phase: phase_idx,
|
|
node_id: step.node_id,
|
|
});
|
|
}
|
|
}
|
|
let motions: Vec<_> = current
|
|
.iter()
|
|
.map(|(node_id, rect)| {
|
|
let to = phase
|
|
.steps
|
|
.iter()
|
|
.find(|step| step.node_id == *node_id)
|
|
.map(|step| step.to)
|
|
.unwrap_or(*rect);
|
|
RectMotion { from: *rect, to }
|
|
})
|
|
.collect();
|
|
for (idx, motion) in motions.iter().enumerate() {
|
|
if let Some((other_idx, _)) = motions[idx + 1..]
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(_, other)| motions_overlap_during_phase(*motion, **other))
|
|
{
|
|
return Err(MultiphaseValidationError::PhaseOverlap {
|
|
phase: phase_idx,
|
|
a: current[idx].0,
|
|
b: current[idx + 1 + other_idx].0,
|
|
});
|
|
}
|
|
}
|
|
for step in &phase.steps {
|
|
let (_, rect) = current
|
|
.iter_mut()
|
|
.find(|(node_id, _)| *node_id == step.node_id)
|
|
.unwrap();
|
|
*rect = step.to;
|
|
}
|
|
}
|
|
for window in &request.windows {
|
|
if !current
|
|
.iter()
|
|
.find(|(node_id, _)| *node_id == window.node_id)
|
|
.is_some_and(|(_, rect)| *rect == window.to)
|
|
{
|
|
return Err(MultiphaseValidationError::FinalMismatch {
|
|
node_id: window.node_id,
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct RectMotion {
|
|
from: Rect,
|
|
to: Rect,
|
|
}
|
|
|
|
fn motions_overlap_during_phase(a: RectMotion, b: RectMotion) -> bool {
|
|
let mut interval = TimeInterval::unit();
|
|
interval.intersect_less_than(edge_delta(a.from.x1(), a.to.x1(), b.from.x2(), b.to.x2()))
|
|
&& interval.intersect_less_than(edge_delta(b.from.x1(), b.to.x1(), a.from.x2(), a.to.x2()))
|
|
&& interval.intersect_less_than(edge_delta(a.from.y1(), a.to.y1(), b.from.y2(), b.to.y2()))
|
|
&& interval.intersect_less_than(edge_delta(b.from.y1(), b.to.y1(), a.from.y2(), a.to.y2()))
|
|
&& interval.is_non_empty()
|
|
}
|
|
|
|
fn edge_delta(a0: i32, a1: i32, b0: i32, b1: i32) -> LinearDelta {
|
|
let from = a0 as i64 - b0 as i64;
|
|
let to = a1 as i64 - b1 as i64;
|
|
LinearDelta {
|
|
start: from,
|
|
velocity: to - from,
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct LinearDelta {
|
|
start: i64,
|
|
velocity: i64,
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct TimeInterval {
|
|
lower: Rational,
|
|
lower_open: bool,
|
|
upper: Rational,
|
|
upper_open: bool,
|
|
}
|
|
|
|
impl TimeInterval {
|
|
fn unit() -> Self {
|
|
Self {
|
|
lower: Rational::new(0, 1),
|
|
lower_open: false,
|
|
upper: Rational::new(1, 1),
|
|
upper_open: false,
|
|
}
|
|
}
|
|
|
|
fn intersect_less_than(&mut self, delta: LinearDelta) -> bool {
|
|
if delta.velocity == 0 {
|
|
return delta.start < 0;
|
|
}
|
|
let boundary = Rational::new(-delta.start, delta.velocity);
|
|
if delta.velocity > 0 {
|
|
self.tighten_upper(boundary, true);
|
|
} else {
|
|
self.tighten_lower(boundary, true);
|
|
}
|
|
self.is_non_empty()
|
|
}
|
|
|
|
fn tighten_lower(&mut self, value: Rational, open: bool) {
|
|
match value.cmp(&self.lower) {
|
|
std::cmp::Ordering::Greater => {
|
|
self.lower = value;
|
|
self.lower_open = open;
|
|
}
|
|
std::cmp::Ordering::Equal => {
|
|
self.lower_open |= open;
|
|
}
|
|
std::cmp::Ordering::Less => {}
|
|
}
|
|
}
|
|
|
|
fn tighten_upper(&mut self, value: Rational, open: bool) {
|
|
match value.cmp(&self.upper) {
|
|
std::cmp::Ordering::Less => {
|
|
self.upper = value;
|
|
self.upper_open = open;
|
|
}
|
|
std::cmp::Ordering::Equal => {
|
|
self.upper_open |= open;
|
|
}
|
|
std::cmp::Ordering::Greater => {}
|
|
}
|
|
}
|
|
|
|
fn is_non_empty(&self) -> bool {
|
|
match self.lower.cmp(&self.upper) {
|
|
std::cmp::Ordering::Less => true,
|
|
std::cmp::Ordering::Equal => !self.lower_open && !self.upper_open,
|
|
std::cmp::Ordering::Greater => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
|
struct Rational {
|
|
num: i64,
|
|
den: i64,
|
|
}
|
|
|
|
impl Rational {
|
|
fn new(mut num: i64, mut den: i64) -> Self {
|
|
if den < 0 {
|
|
num = -num;
|
|
den = -den;
|
|
}
|
|
Self { num, den }
|
|
}
|
|
|
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
(self.num as i128 * other.den as i128).cmp(&(other.num as i128 * self.den as i128))
|
|
}
|
|
}
|
|
|
|
fn classify_step(step: MultiphaseStep) -> Option<PhaseAction> {
|
|
let same_x = step.from.x1() == step.to.x1() && step.from.x2() == step.to.x2();
|
|
let same_y = step.from.y1() == step.to.y1() && step.from.y2() == step.to.y2();
|
|
let same_size = step.from.size() == step.to.size();
|
|
match (same_x, same_y, same_size) {
|
|
(false, true, true) => Some(PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
}),
|
|
(true, false, true) => Some(PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Vertical,
|
|
}),
|
|
(false, true, false) => Some(PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
}),
|
|
(true, false, false) => Some(PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
}),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn single_action_reason(action: PhaseAction) -> PhaseReason {
|
|
match action.kind {
|
|
PhaseKind::Move => PhaseReason::SingleAction,
|
|
PhaseKind::Scale => PhaseReason::SameAxisRedistribution,
|
|
}
|
|
}
|
|
|
|
fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest {
|
|
MultiphaseRequest {
|
|
bounds: request.bounds,
|
|
clearance: request.clearance,
|
|
windows: request
|
|
.windows
|
|
.iter()
|
|
.map(|window| MultiphaseWindow {
|
|
node_id: window.node_id,
|
|
from: window.to,
|
|
to: window.from,
|
|
hierarchy: window.hierarchy.reversed(),
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
fn reverse_plan(plan: MultiphasePlan) -> MultiphasePlan {
|
|
MultiphasePlan {
|
|
phases: plan
|
|
.phases
|
|
.into_iter()
|
|
.rev()
|
|
.map(|phase| MultiphasePhase {
|
|
action: phase.action,
|
|
steps: phase
|
|
.steps
|
|
.into_iter()
|
|
.map(|step| MultiphaseStep {
|
|
node_id: step.node_id,
|
|
from: step.to,
|
|
to: step.from,
|
|
})
|
|
.collect(),
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
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 {
|
|
let rects: Vec<_> = rects.into_iter().collect();
|
|
for (idx, rect) in rects.iter().enumerate() {
|
|
if rects[idx + 1..].iter().any(|other| rect.intersects(other)) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn motion_bounds(window: MultiphaseWindow) -> Rect {
|
|
window.from.union(window.to)
|
|
}
|
|
|
|
fn motion_bounds_with_clearance(window: MultiphaseWindow, clearance: i32) -> Rect {
|
|
let bounds = motion_bounds(window);
|
|
Rect::new_saturating(
|
|
bounds.x1().saturating_sub(clearance),
|
|
bounds.y1().saturating_sub(clearance),
|
|
bounds.x2().saturating_add(clearance),
|
|
bounds.y2().saturating_add(clearance),
|
|
)
|
|
}
|
|
|
|
fn interval_changed(from: Rect, to: Rect, axis: PhaseAxis) -> bool {
|
|
main_start(from, axis) != main_start(to, axis) || main_end(from, axis) != main_end(to, axis)
|
|
}
|
|
|
|
fn split_depth_for_axis(position: MultiphaseHierarchyPosition, axis: PhaseAxis) -> Option<u16> {
|
|
match axis {
|
|
PhaseAxis::Horizontal => position.nearest_horizontal_split_depth,
|
|
PhaseAxis::Vertical => position.nearest_vertical_split_depth,
|
|
}
|
|
}
|
|
|
|
fn push_step(steps: &mut Vec<MultiphaseStep>, node_id: NodeId, from: Rect, to: Rect) {
|
|
if from != to {
|
|
steps.push(MultiphaseStep { node_id, from, to });
|
|
}
|
|
}
|
|
|
|
fn sane_min_size(size: i32) -> i32 {
|
|
(size / MIN_SHRINK_DENOMINATOR).max(1)
|
|
}
|
|
|
|
fn main_start(rect: Rect, axis: PhaseAxis) -> i32 {
|
|
match axis {
|
|
PhaseAxis::Horizontal => rect.x1(),
|
|
PhaseAxis::Vertical => rect.y1(),
|
|
}
|
|
}
|
|
|
|
fn main_end(rect: Rect, axis: PhaseAxis) -> i32 {
|
|
match axis {
|
|
PhaseAxis::Horizontal => rect.x2(),
|
|
PhaseAxis::Vertical => rect.y2(),
|
|
}
|
|
}
|
|
|
|
fn main_size(rect: Rect, axis: PhaseAxis) -> i32 {
|
|
main_end(rect, axis) - main_start(rect, axis)
|
|
}
|
|
|
|
fn orth_start(rect: Rect, axis: PhaseAxis) -> i32 {
|
|
main_start(rect, axis.other())
|
|
}
|
|
|
|
fn orth_end(rect: Rect, axis: PhaseAxis) -> i32 {
|
|
main_end(rect, axis.other())
|
|
}
|
|
|
|
fn with_main_interval(rect: Rect, axis: PhaseAxis, start: i32, end: i32) -> Rect {
|
|
match axis {
|
|
PhaseAxis::Horizontal => Rect::new_saturating(start, rect.y1(), end, rect.y2()),
|
|
PhaseAxis::Vertical => Rect::new_saturating(rect.x1(), start, rect.x2(), end),
|
|
}
|
|
}
|
|
|
|
fn with_orth_interval(rect: Rect, axis: PhaseAxis, start: i32, end: i32) -> Rect {
|
|
with_main_interval(rect, axis.other(), start, end)
|
|
}
|
|
|
|
impl PhaseAxis {
|
|
fn other(self) -> Self {
|
|
match self {
|
|
Self::Horizontal => Self::Vertical,
|
|
Self::Vertical => Self::Horizontal,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn id(raw: u32) -> NodeId {
|
|
NodeId(raw)
|
|
}
|
|
|
|
fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect {
|
|
Rect::new_saturating(x1, y1, x2, y2)
|
|
}
|
|
|
|
fn window(raw: u32, from: Rect, to: Rect) -> MultiphaseWindow {
|
|
MultiphaseWindow::new(id(raw), from, to)
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum TestTree {
|
|
Leaf(u32),
|
|
Split {
|
|
id: u32,
|
|
axis: PhaseAxis,
|
|
weights: Vec<i32>,
|
|
children: Vec<TestTree>,
|
|
},
|
|
}
|
|
|
|
struct TestLeaf {
|
|
node_id: NodeId,
|
|
rect: Rect,
|
|
hierarchy: MultiphaseHierarchyPosition,
|
|
}
|
|
|
|
fn leaf(raw: u32) -> TestTree {
|
|
TestTree::Leaf(raw)
|
|
}
|
|
|
|
fn split(id: u32, axis: PhaseAxis, weights: &[i32], children: Vec<TestTree>) -> TestTree {
|
|
TestTree::Split {
|
|
id,
|
|
axis,
|
|
weights: weights.to_vec(),
|
|
children,
|
|
}
|
|
}
|
|
|
|
fn layout_tree(tree: &TestTree, bounds: Rect) -> Vec<TestLeaf> {
|
|
let mut leaves = vec![];
|
|
layout_tree_inner(
|
|
tree,
|
|
bounds,
|
|
TestHierarchy {
|
|
parent: None,
|
|
depth: 0,
|
|
sibling_index: None,
|
|
split_axis: None,
|
|
nearest_horizontal_split_depth: None,
|
|
nearest_vertical_split_depth: None,
|
|
},
|
|
&mut leaves,
|
|
);
|
|
leaves.sort_by_key(|leaf| leaf.node_id.0);
|
|
leaves
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct TestHierarchy {
|
|
parent: Option<NodeId>,
|
|
depth: u16,
|
|
sibling_index: Option<u16>,
|
|
split_axis: Option<PhaseAxis>,
|
|
nearest_horizontal_split_depth: Option<u16>,
|
|
nearest_vertical_split_depth: Option<u16>,
|
|
}
|
|
|
|
fn layout_tree_inner(
|
|
tree: &TestTree,
|
|
bounds: Rect,
|
|
hierarchy: TestHierarchy,
|
|
leaves: &mut Vec<TestLeaf>,
|
|
) {
|
|
match tree {
|
|
TestTree::Leaf(raw) => leaves.push(TestLeaf {
|
|
node_id: id(*raw),
|
|
rect: bounds,
|
|
hierarchy: MultiphaseHierarchyPosition {
|
|
parent: hierarchy.parent,
|
|
depth: hierarchy.depth,
|
|
sibling_index: hierarchy.sibling_index,
|
|
split_axis: hierarchy.split_axis,
|
|
nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth,
|
|
nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth,
|
|
..Default::default()
|
|
},
|
|
}),
|
|
TestTree::Split {
|
|
id: split_id,
|
|
axis,
|
|
weights,
|
|
children,
|
|
} => {
|
|
assert_eq!(weights.len(), children.len());
|
|
let rects = split_rect_by_weights(bounds, *axis, weights);
|
|
for (idx, (child, rect)) in children.iter().zip(rects).enumerate() {
|
|
let depth = hierarchy.depth.saturating_add(1);
|
|
let mut child_hierarchy = TestHierarchy {
|
|
parent: Some(id(*split_id)),
|
|
depth,
|
|
sibling_index: Some(idx.min(u16::MAX as usize) as u16),
|
|
split_axis: Some(*axis),
|
|
nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth,
|
|
nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth,
|
|
};
|
|
match axis {
|
|
PhaseAxis::Horizontal => {
|
|
child_hierarchy.nearest_horizontal_split_depth = Some(depth);
|
|
}
|
|
PhaseAxis::Vertical => {
|
|
child_hierarchy.nearest_vertical_split_depth = Some(depth);
|
|
}
|
|
}
|
|
layout_tree_inner(child, rect, child_hierarchy, leaves);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn split_rect_by_weights(bounds: Rect, axis: PhaseAxis, weights: &[i32]) -> Vec<Rect> {
|
|
let total_weight: i32 = weights.iter().sum();
|
|
assert!(total_weight > 0);
|
|
let total_size = match axis {
|
|
PhaseAxis::Horizontal => bounds.width(),
|
|
PhaseAxis::Vertical => bounds.height(),
|
|
};
|
|
let mut pos = match axis {
|
|
PhaseAxis::Horizontal => bounds.x1(),
|
|
PhaseAxis::Vertical => bounds.y1(),
|
|
};
|
|
let mut remaining_size = total_size;
|
|
let mut remaining_weight = total_weight;
|
|
let mut rects = vec![];
|
|
for (idx, weight) in weights.iter().enumerate() {
|
|
let size = if idx + 1 == weights.len() {
|
|
remaining_size
|
|
} else {
|
|
total_size * *weight / total_weight
|
|
};
|
|
let rect = match axis {
|
|
PhaseAxis::Horizontal => {
|
|
Rect::new_sized_saturating(pos, bounds.y1(), size, bounds.height())
|
|
}
|
|
PhaseAxis::Vertical => {
|
|
Rect::new_sized_saturating(bounds.x1(), pos, bounds.width(), size)
|
|
}
|
|
};
|
|
rects.push(rect);
|
|
pos += size;
|
|
remaining_size -= size;
|
|
remaining_weight -= *weight;
|
|
if remaining_weight == 0 {
|
|
assert_eq!(remaining_size, 0);
|
|
}
|
|
}
|
|
rects
|
|
}
|
|
|
|
fn generated_request(old: &TestTree, new: &TestTree, bounds: Rect) -> MultiphaseRequest {
|
|
let old_leaves = layout_tree(old, bounds);
|
|
let new_leaves = layout_tree(new, bounds);
|
|
assert_eq!(old_leaves.len(), new_leaves.len());
|
|
let mut windows = vec![];
|
|
for old_leaf in &old_leaves {
|
|
let new_leaf = new_leaves
|
|
.iter()
|
|
.find(|leaf| leaf.node_id == old_leaf.node_id)
|
|
.unwrap();
|
|
windows.push(MultiphaseWindow::with_hierarchy(
|
|
old_leaf.node_id,
|
|
old_leaf.rect,
|
|
new_leaf.rect,
|
|
MultiphaseWindowHierarchy::new(old_leaf.hierarchy, new_leaf.hierarchy),
|
|
));
|
|
}
|
|
MultiphaseRequest {
|
|
bounds,
|
|
windows,
|
|
clearance: 0,
|
|
}
|
|
}
|
|
|
|
fn assert_generated_case_plans(old: &TestTree, new: &TestTree, bounds: Rect) {
|
|
assert_generated_case_plans_deterministically(old, new, bounds);
|
|
}
|
|
|
|
fn assert_generated_case_plans_deterministically(
|
|
old: &TestTree,
|
|
new: &TestTree,
|
|
bounds: Rect,
|
|
) -> MultiphasePlanned {
|
|
let req = generated_request(old, new, bounds);
|
|
assert!(!overlaps(req.windows.iter().map(|window| window.from)));
|
|
assert!(!overlaps(req.windows.iter().map(|window| window.to)));
|
|
let first = plan_no_overlap_explained(&req).unwrap();
|
|
let second = plan_no_overlap_explained(&req).unwrap();
|
|
assert_eq!(first, second);
|
|
assert_eq!(first.plan.phases.len(), first.explanation.phases.len());
|
|
assert_eq!(
|
|
first.explanation.validation,
|
|
ValidationExplanation::passed()
|
|
);
|
|
for phase in &first.explanation.phases {
|
|
assert!(phase.nodes.windows(2).all(|pair| pair[0].0 < pair[1].0));
|
|
}
|
|
assert!(validate_plan_continuous(&req, &first.plan));
|
|
first
|
|
}
|
|
|
|
fn bounds_for_axis(axis: PhaseAxis) -> Rect {
|
|
match axis {
|
|
PhaseAxis::Horizontal => rect(0, 0, 400, 100),
|
|
PhaseAxis::Vertical => rect(0, 0, 100, 400),
|
|
}
|
|
}
|
|
|
|
fn push_generated_case_bidirectional(
|
|
cases: &mut Vec<(TestTree, TestTree, Rect)>,
|
|
old: TestTree,
|
|
new: TestTree,
|
|
bounds: Rect,
|
|
) {
|
|
cases.push((old.clone(), new.clone(), bounds));
|
|
cases.push((new, old, bounds));
|
|
}
|
|
|
|
fn request(windows: Vec<MultiphaseWindow>) -> MultiphaseRequest {
|
|
let bounds = windows
|
|
.iter()
|
|
.map(|window| window.from.union(window.to))
|
|
.reduce(|bounds, rect| bounds.union(rect))
|
|
.unwrap_or_else(|| rect(0, 0, 1, 1));
|
|
MultiphaseRequest {
|
|
bounds,
|
|
windows,
|
|
clearance: 0,
|
|
}
|
|
}
|
|
|
|
fn actions(plan: &MultiphasePlan) -> Vec<PhaseAction> {
|
|
plan.phases
|
|
.iter()
|
|
.map(|phase| phase.action.as_uniform().unwrap())
|
|
.collect()
|
|
}
|
|
|
|
fn step_to(plan: &MultiphasePlan, phase: usize, node_id: NodeId) -> Rect {
|
|
plan.phases[phase]
|
|
.steps
|
|
.iter()
|
|
.find(|step| step.node_id == node_id)
|
|
.unwrap()
|
|
.to
|
|
}
|
|
|
|
fn no_pattern_attempts(direction: PlanDirection) -> Vec<RejectedStrategy> {
|
|
vec![
|
|
RejectedStrategy {
|
|
direction,
|
|
strategy: PlanStrategy::SingleAction,
|
|
reason: MultiphasePlanFailure::NoPattern,
|
|
},
|
|
RejectedStrategy {
|
|
direction,
|
|
strategy: PlanStrategy::SpaceThenOrthogonalGrowth {
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
reason: MultiphasePlanFailure::NoPattern,
|
|
},
|
|
RejectedStrategy {
|
|
direction,
|
|
strategy: PlanStrategy::SpaceThenOrthogonalGrowth {
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
reason: MultiphasePlanFailure::NoPattern,
|
|
},
|
|
RejectedStrategy {
|
|
direction,
|
|
strategy: PlanStrategy::HierarchyOrderedScales,
|
|
reason: MultiphasePlanFailure::NoPattern,
|
|
},
|
|
RejectedStrategy {
|
|
direction,
|
|
strategy: PlanStrategy::OrientationChange {
|
|
from_axis: PhaseAxis::Horizontal,
|
|
},
|
|
reason: MultiphasePlanFailure::NoPattern,
|
|
},
|
|
RejectedStrategy {
|
|
direction,
|
|
strategy: PlanStrategy::OrientationChange {
|
|
from_axis: PhaseAxis::Vertical,
|
|
},
|
|
reason: MultiphasePlanFailure::NoPattern,
|
|
},
|
|
RejectedStrategy {
|
|
direction,
|
|
strategy: PlanStrategy::SwapLanes {
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
reason: MultiphasePlanFailure::NoPattern,
|
|
},
|
|
RejectedStrategy {
|
|
direction,
|
|
strategy: PlanStrategy::SwapLanes {
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
reason: MultiphasePlanFailure::NoPattern,
|
|
},
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn horizontal_swap_shrinks_moves_then_grows_without_overlap() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)),
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(100, 0, 200, 100),
|
|
to: rect(0, 0, 100, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
let plan = &planned.plan;
|
|
assert_eq!(
|
|
actions(plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical
|
|
},
|
|
]
|
|
);
|
|
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!(
|
|
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]
|
|
fn horizontal_swap_reverse_uses_equivalent_lanes() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(100, 0, 200, 100),
|
|
to: rect(0, 0, 100, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(0, 0, 100, 100),
|
|
to: rect(100, 0, 200, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let plan = plan_no_overlap(&req).unwrap();
|
|
assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 100, 50));
|
|
assert_eq!(step_to(&plan, 0, id(1)), rect(100, 50, 200, 100));
|
|
assert!(validate_plan_continuous(&req, &plan));
|
|
}
|
|
|
|
#[test]
|
|
fn horizontal_swap_lanes_follow_motion_direction_not_node_id() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(100, 0, 200, 100),
|
|
to: rect(0, 0, 100, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(0, 0, 100, 100),
|
|
to: rect(100, 0, 200, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let plan = plan_no_overlap(&req).unwrap();
|
|
assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 100, 50));
|
|
assert_eq!(step_to(&plan, 0, id(1)), rect(100, 50, 200, 100));
|
|
assert!(validate_plan_continuous(&req, &plan));
|
|
}
|
|
|
|
#[test]
|
|
fn swap_lanes_respect_requested_clearance() {
|
|
let mut req = request(vec![
|
|
window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)),
|
|
window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)),
|
|
]);
|
|
req.clearance = 10;
|
|
|
|
let plan = plan_no_overlap(&req).unwrap();
|
|
assert_eq!(step_to(&plan, 0, id(1)), rect(0, 0, 100, 45));
|
|
assert_eq!(step_to(&plan, 0, id(2)), rect(100, 55, 200, 100));
|
|
assert!(validate_plan_continuous(&req, &plan));
|
|
}
|
|
|
|
#[test]
|
|
fn swap_lanes_tolerate_stationary_siblings_in_request() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)),
|
|
window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)),
|
|
window(3, rect(200, 0, 300, 100), rect(200, 0, 300, 100)),
|
|
]);
|
|
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::SwapLanes {
|
|
axis: PhaseAxis::Horizontal,
|
|
}
|
|
);
|
|
assert!(validate_plan_continuous(&req, &planned.plan));
|
|
}
|
|
|
|
#[test]
|
|
fn one_pixel_uneven_swap_lanes_fold_remainder_into_crossing_phase() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 101, 100), rect(101, 0, 201, 100)),
|
|
window(2, rect(101, 0, 201, 100), rect(0, 0, 101, 100)),
|
|
]);
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
let plan = &planned.plan;
|
|
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::SwapLanes {
|
|
axis: PhaseAxis::Horizontal,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
actions(plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(step_to(plan, 1, id(1)), rect(101, 0, 201, 50));
|
|
assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 101, 100));
|
|
assert!(validate_plan_continuous(&req, plan));
|
|
}
|
|
|
|
#[test]
|
|
fn large_uneven_swap_lanes_split_move_and_same_axis_scale() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 120, 100), rect(120, 0, 200, 100)),
|
|
window(2, rect(120, 0, 200, 100), rect(0, 0, 120, 100)),
|
|
]);
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
let plan = &planned.plan;
|
|
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::SwapLanes {
|
|
axis: PhaseAxis::Horizontal,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
actions(plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(step_to(plan, 1, id(1)), rect(80, 0, 200, 50));
|
|
assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 80, 100));
|
|
assert_eq!(step_to(plan, 2, id(1)), rect(120, 0, 200, 50));
|
|
assert_eq!(step_to(plan, 2, id(2)), rect(0, 50, 120, 100));
|
|
assert!(validate_plan_continuous(&req, plan));
|
|
}
|
|
|
|
#[test]
|
|
fn uneven_swap_lanes_tolerate_stationary_sibling_in_odd_layout() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 101, 100), rect(101, 0, 201, 100)),
|
|
window(2, rect(101, 0, 201, 100), rect(0, 0, 101, 100)),
|
|
window(3, rect(201, 0, 300, 100), rect(201, 0, 300, 100)),
|
|
]);
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::SwapLanes {
|
|
axis: PhaseAxis::Horizontal,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
actions(&planned.plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
]
|
|
);
|
|
assert!(validate_plan_continuous(&req, &planned.plan));
|
|
}
|
|
|
|
#[test]
|
|
fn horizontal_rotation_uses_crossing_lanes() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)),
|
|
window(2, rect(100, 0, 200, 100), rect(200, 0, 300, 100)),
|
|
window(3, rect(200, 0, 300, 100), rect(0, 0, 100, 100)),
|
|
]);
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::SwapLanes {
|
|
axis: PhaseAxis::Horizontal,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
actions(&planned.plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
]
|
|
);
|
|
assert!(validate_plan_continuous(&req, &planned.plan));
|
|
}
|
|
|
|
#[test]
|
|
fn vertical_swap_lanes_follow_motion_direction_not_node_id() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 100, 100, 200),
|
|
to: rect(0, 0, 100, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(0, 0, 100, 100),
|
|
to: rect(0, 100, 100, 200),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let plan = plan_no_overlap(&req).unwrap();
|
|
assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 50, 100));
|
|
assert_eq!(step_to(&plan, 0, id(1)), rect(50, 100, 100, 200));
|
|
assert!(validate_plan_continuous(&req, &plan));
|
|
}
|
|
|
|
#[test]
|
|
fn generated_sibling_swaps_plan_for_both_axes() {
|
|
let bounds = rect(0, 0, 240, 240);
|
|
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
|
let old = split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]);
|
|
let new = split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]);
|
|
assert_generated_case_plans(&old, &new, bounds);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn generated_size_redistributions_plan_as_single_axis_scale() {
|
|
let horizontal_old = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 1, 1],
|
|
vec![leaf(1), leaf(2), leaf(3)],
|
|
);
|
|
let horizontal_new = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 2, 1],
|
|
vec![leaf(1), leaf(2), leaf(3)],
|
|
);
|
|
let horizontal_req =
|
|
generated_request(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100));
|
|
let horizontal_plan = plan_no_overlap(&horizontal_req).unwrap();
|
|
assert_eq!(
|
|
actions(&horizontal_plan),
|
|
vec![PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
}]
|
|
);
|
|
assert!(validate_plan_continuous(&horizontal_req, &horizontal_plan));
|
|
|
|
let vertical_old = split(
|
|
10,
|
|
PhaseAxis::Vertical,
|
|
&[1, 1, 1],
|
|
vec![leaf(1), leaf(2), leaf(3)],
|
|
);
|
|
let vertical_new = split(
|
|
10,
|
|
PhaseAxis::Vertical,
|
|
&[1, 2, 1],
|
|
vec![leaf(1), leaf(2), leaf(3)],
|
|
);
|
|
let vertical_req = generated_request(&vertical_old, &vertical_new, rect(0, 0, 100, 400));
|
|
let vertical_plan = plan_no_overlap(&vertical_req).unwrap();
|
|
assert_eq!(
|
|
actions(&vertical_plan),
|
|
vec![PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
}]
|
|
);
|
|
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 mixed_single_phase_accepts_move_and_scale_when_proven() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 80, 80), rect(40, 0, 120, 80)),
|
|
window(2, rect(200, 0, 280, 80), rect(200, 0, 280, 120)),
|
|
]);
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
|
|
assert_eq!(planned.explanation.strategy, PlanStrategy::MixedSinglePhase);
|
|
assert_eq!(
|
|
planned.plan.phases[0].action,
|
|
MultiphasePhaseAction::Mixed(vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
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 single_window_one_axis_group_is_still_multiphase_plannable() {
|
|
let req = request(vec![window(1, rect(0, 0, 100, 100), rect(40, 0, 140, 100))]);
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
|
|
assert_eq!(planned.explanation.strategy, PlanStrategy::SingleAction);
|
|
assert_eq!(
|
|
planned.plan.phases[0].action,
|
|
MultiphasePhaseAction::Uniform(PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
})
|
|
);
|
|
assert_eq!(planned.explanation.phases[0].nodes, vec![id(1)]);
|
|
assert!(validate_plan_continuous(&req, &planned.plan));
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_single_phase_still_rejects_diagonal_per_window_motion() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 80, 80), rect(40, 40, 120, 120)),
|
|
window(2, rect(200, 0, 280, 80), rect(200, 0, 280, 120)),
|
|
]);
|
|
|
|
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
|
|
let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err();
|
|
let rejection = MultiphasePlanFailure::InvalidPhaseStep {
|
|
action: PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
node_id: id(1),
|
|
};
|
|
assert_eq!(diagnostic.forward, rejection);
|
|
assert_eq!(diagnostic.reverse, Some(rejection));
|
|
}
|
|
|
|
#[test]
|
|
fn generated_nested_size_redistribution_scales_parent_axis_first() {
|
|
let old = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 1],
|
|
vec![
|
|
leaf(1),
|
|
split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]),
|
|
],
|
|
);
|
|
let new = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 3],
|
|
vec![
|
|
leaf(1),
|
|
split(11, PhaseAxis::Vertical, &[3, 1], vec![leaf(2), leaf(3)]),
|
|
],
|
|
);
|
|
let req = generated_request(&old, &new, rect(0, 0, 400, 100));
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
let plan = &planned.plan;
|
|
assert_eq!(
|
|
actions(plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
]
|
|
);
|
|
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(3)), rect(100, 50, 400, 100));
|
|
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]
|
|
fn orientation_change_shrinks_moves_then_grows() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 200, 400), rect(0, 0, 400, 200)),
|
|
window(2, rect(200, 0, 400, 400), rect(0, 200, 400, 400)),
|
|
]);
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
let plan = &planned.plan;
|
|
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::OrientationChange {
|
|
from_axis: PhaseAxis::Horizontal,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
actions(plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 200, 200));
|
|
assert_eq!(step_to(plan, 0, id(2)), rect(200, 200, 400, 400));
|
|
assert_eq!(step_to(plan, 1, id(2)), rect(0, 200, 200, 400));
|
|
assert_eq!(step_to(plan, 2, id(1)), rect(0, 0, 400, 200));
|
|
assert_eq!(step_to(plan, 2, id(2)), rect(0, 200, 400, 400));
|
|
assert!(validate_plan_continuous(&req, plan));
|
|
}
|
|
|
|
#[test]
|
|
fn two_axis_redistribution_without_hierarchy_still_falls_back() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 200, 100), rect(0, 0, 100, 100)),
|
|
window(2, rect(200, 0, 400, 50), rect(100, 0, 400, 75)),
|
|
window(3, rect(200, 50, 400, 100), rect(100, 75, 400, 100)),
|
|
]);
|
|
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
|
|
}
|
|
|
|
#[test]
|
|
fn generated_stack_extractions_plan_for_both_axes_and_directions() {
|
|
let horizontal_old = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 1],
|
|
vec![
|
|
leaf(1),
|
|
split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]),
|
|
],
|
|
);
|
|
let horizontal_new = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 2, 1],
|
|
vec![leaf(1), leaf(2), leaf(3)],
|
|
);
|
|
assert_generated_case_plans(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100));
|
|
assert_generated_case_plans(&horizontal_new, &horizontal_old, rect(0, 0, 400, 100));
|
|
|
|
let vertical_old = split(
|
|
20,
|
|
PhaseAxis::Vertical,
|
|
&[1, 1],
|
|
vec![
|
|
leaf(1),
|
|
split(21, PhaseAxis::Horizontal, &[1, 1], vec![leaf(2), leaf(3)]),
|
|
],
|
|
);
|
|
let vertical_new = split(
|
|
20,
|
|
PhaseAxis::Vertical,
|
|
&[1, 2, 1],
|
|
vec![leaf(1), leaf(2), leaf(3)],
|
|
);
|
|
assert_generated_case_plans(&vertical_old, &vertical_new, rect(0, 0, 100, 400));
|
|
assert_generated_case_plans(&vertical_new, &vertical_old, rect(0, 0, 100, 400));
|
|
}
|
|
|
|
#[test]
|
|
fn stack_extraction_with_resized_moving_child_still_moves_before_growth() {
|
|
let old = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 1],
|
|
vec![
|
|
leaf(1),
|
|
split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]),
|
|
],
|
|
);
|
|
let new = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 1, 1],
|
|
vec![leaf(1), leaf(2), leaf(3)],
|
|
);
|
|
let req = generated_request(&old, &new, rect(0, 0, 300, 120));
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
let plan = &planned.plan;
|
|
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::SpaceThenOrthogonalGrowth {
|
|
axis: PhaseAxis::Horizontal,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
actions(plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 100, 120));
|
|
assert_eq!(step_to(plan, 0, id(2)), rect(200, 0, 300, 60));
|
|
assert_eq!(step_to(plan, 0, id(3)), rect(200, 60, 300, 120));
|
|
assert_eq!(step_to(plan, 1, id(2)), rect(100, 0, 200, 60));
|
|
assert_eq!(step_to(plan, 2, id(2)), rect(100, 0, 200, 120));
|
|
assert_eq!(step_to(plan, 2, id(3)), rect(200, 0, 300, 120));
|
|
assert!(validate_plan_continuous(&req, plan));
|
|
}
|
|
|
|
#[test]
|
|
fn three_child_stack_extraction_plans_without_linear_fallback() {
|
|
let old = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 1],
|
|
vec![
|
|
leaf(1),
|
|
split(
|
|
11,
|
|
PhaseAxis::Vertical,
|
|
&[1, 1, 1],
|
|
vec![leaf(2), leaf(3), leaf(4)],
|
|
),
|
|
],
|
|
);
|
|
let new = split(
|
|
10,
|
|
PhaseAxis::Horizontal,
|
|
&[1, 1, 1],
|
|
vec![
|
|
leaf(1),
|
|
leaf(3),
|
|
split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(4)]),
|
|
],
|
|
);
|
|
let req = generated_request(&old, &new, rect(0, 0, 600, 300));
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::SpaceThenOrthogonalGrowth {
|
|
axis: PhaseAxis::Horizontal,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
actions(&planned.plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
]
|
|
);
|
|
assert!(validate_plan_continuous(&req, &planned.plan));
|
|
}
|
|
|
|
#[test]
|
|
fn validated_phase_paths_accept_interrupted_reverse_route() {
|
|
let a_current = rect(50, 0, 150, 50);
|
|
let b_current = rect(50, 50, 150, 100);
|
|
let req = request(vec![
|
|
window(1, a_current, rect(0, 0, 100, 100)),
|
|
window(2, b_current, rect(100, 0, 200, 100)),
|
|
]);
|
|
let paths = vec![
|
|
vec![
|
|
(a_current, rect(0, 0, 100, 50)),
|
|
(rect(0, 0, 100, 50), rect(0, 0, 100, 100)),
|
|
],
|
|
vec![
|
|
(b_current, rect(100, 50, 200, 100)),
|
|
(rect(100, 50, 200, 100), rect(100, 0, 200, 100)),
|
|
],
|
|
];
|
|
|
|
let plan = validate_phase_paths(&req, &paths).unwrap();
|
|
assert_eq!(
|
|
actions(&plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical,
|
|
},
|
|
]
|
|
);
|
|
assert!(validate_plan_continuous(&req, &plan));
|
|
}
|
|
|
|
#[test]
|
|
fn bounded_generated_supported_split_tree_corpus_is_deterministic() {
|
|
let mut cases = vec![];
|
|
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
|
let child_axis = axis.other();
|
|
let bounds = bounds_for_axis(axis);
|
|
|
|
push_generated_case_bidirectional(
|
|
&mut cases,
|
|
split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]),
|
|
split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]),
|
|
bounds,
|
|
);
|
|
push_generated_case_bidirectional(
|
|
&mut cases,
|
|
split(10, axis, &[1, 1, 1], vec![leaf(1), leaf(2), leaf(3)]),
|
|
split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]),
|
|
bounds,
|
|
);
|
|
push_generated_case_bidirectional(
|
|
&mut cases,
|
|
split(
|
|
10,
|
|
axis,
|
|
&[1, 1],
|
|
vec![
|
|
leaf(1),
|
|
split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]),
|
|
],
|
|
),
|
|
split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]),
|
|
bounds,
|
|
);
|
|
push_generated_case_bidirectional(
|
|
&mut cases,
|
|
split(
|
|
10,
|
|
axis,
|
|
&[1, 1],
|
|
vec![
|
|
split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]),
|
|
leaf(3),
|
|
],
|
|
),
|
|
split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]),
|
|
bounds,
|
|
);
|
|
push_generated_case_bidirectional(
|
|
&mut cases,
|
|
split(
|
|
10,
|
|
axis,
|
|
&[1, 1],
|
|
vec![
|
|
leaf(1),
|
|
split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]),
|
|
],
|
|
),
|
|
split(
|
|
10,
|
|
axis,
|
|
&[1, 3],
|
|
vec![
|
|
leaf(1),
|
|
split(11, child_axis, &[3, 1], vec![leaf(2), leaf(3)]),
|
|
],
|
|
),
|
|
bounds,
|
|
);
|
|
push_generated_case_bidirectional(
|
|
&mut cases,
|
|
split(
|
|
10,
|
|
axis,
|
|
&[1, 1],
|
|
vec![
|
|
split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]),
|
|
leaf(3),
|
|
],
|
|
),
|
|
split(
|
|
10,
|
|
axis,
|
|
&[3, 1],
|
|
vec![
|
|
split(11, child_axis, &[1, 3], vec![leaf(1), leaf(2)]),
|
|
leaf(3),
|
|
],
|
|
),
|
|
bounds,
|
|
);
|
|
}
|
|
|
|
assert_eq!(cases.len(), 24);
|
|
for (old, new, bounds) in cases {
|
|
assert_generated_case_plans_deterministically(&old, &new, bounds);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stack_extraction_creates_space_before_moving_child() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 200, 100),
|
|
to: rect(0, 0, 100, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(200, 0, 400, 50),
|
|
to: rect(100, 0, 300, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(3),
|
|
from: rect(200, 50, 400, 100),
|
|
to: rect(300, 0, 400, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let plan = plan_no_overlap(&req).unwrap();
|
|
assert_eq!(
|
|
actions(&plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 100));
|
|
assert_eq!(plan.phases[0].steps[1].to, rect(300, 50, 400, 100));
|
|
assert_eq!(plan.phases[1].steps[0].to, rect(100, 0, 300, 50));
|
|
assert_eq!(plan.phases[2].steps[0].to, rect(100, 0, 300, 100));
|
|
assert_eq!(plan.phases[2].steps[1].to, rect(300, 0, 400, 100));
|
|
assert!(validate_plan_continuous(&req, &plan));
|
|
}
|
|
|
|
#[test]
|
|
fn stack_extraction_reverse_replays_phases_in_reverse() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 100, 100),
|
|
to: rect(0, 0, 200, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(100, 0, 300, 100),
|
|
to: rect(200, 0, 400, 50),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(3),
|
|
from: rect(300, 0, 400, 100),
|
|
to: rect(200, 50, 400, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
let plan = &planned.plan;
|
|
assert_eq!(
|
|
actions(plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::ReversedForwardPlan {
|
|
original: Box::new(PlanStrategy::SpaceThenOrthogonalGrowth {
|
|
axis: PhaseAxis::Horizontal
|
|
})
|
|
}
|
|
);
|
|
assert!(validate_plan_continuous(&req, plan));
|
|
}
|
|
|
|
#[test]
|
|
fn vertical_stack_extraction_creates_space_before_moving_child() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 100, 200),
|
|
to: rect(0, 0, 100, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(0, 200, 50, 400),
|
|
to: rect(0, 100, 100, 300),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(3),
|
|
from: rect(50, 200, 100, 400),
|
|
to: rect(0, 300, 100, 400),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let plan = plan_no_overlap(&req).unwrap();
|
|
assert_eq!(
|
|
actions(&plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Vertical
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 100));
|
|
assert_eq!(plan.phases[0].steps[1].to, rect(50, 300, 100, 400));
|
|
assert_eq!(plan.phases[1].steps[0].to, rect(0, 100, 50, 300));
|
|
assert_eq!(plan.phases[2].steps[0].to, rect(0, 100, 100, 300));
|
|
assert_eq!(plan.phases[2].steps[1].to, rect(0, 300, 100, 400));
|
|
assert!(validate_plan_continuous(&req, &plan));
|
|
}
|
|
|
|
#[test]
|
|
fn vertical_stack_extraction_with_clearance_still_plans() {
|
|
let old = split(
|
|
20,
|
|
PhaseAxis::Vertical,
|
|
&[1, 1],
|
|
vec![
|
|
leaf(1),
|
|
split(21, PhaseAxis::Horizontal, &[1, 1], vec![leaf(2), leaf(3)]),
|
|
],
|
|
);
|
|
let new = split(
|
|
20,
|
|
PhaseAxis::Vertical,
|
|
&[1, 2, 1],
|
|
vec![leaf(1), leaf(2), leaf(3)],
|
|
);
|
|
let mut req = generated_request(&old, &new, rect(0, 0, 100, 400));
|
|
req.clearance = 10;
|
|
let planned = plan_no_overlap_explained(&req).unwrap();
|
|
|
|
assert_eq!(
|
|
planned.explanation.strategy,
|
|
PlanStrategy::SpaceThenOrthogonalGrowth {
|
|
axis: PhaseAxis::Vertical,
|
|
}
|
|
);
|
|
assert!(validate_plan_continuous(&req, &planned.plan));
|
|
}
|
|
|
|
#[test]
|
|
fn vertical_stack_extraction_reverse_replays_phases_in_reverse() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 100, 100),
|
|
to: rect(0, 0, 100, 200),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(0, 100, 100, 300),
|
|
to: rect(0, 200, 50, 400),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(3),
|
|
from: rect(0, 300, 100, 400),
|
|
to: rect(50, 200, 100, 400),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let plan = plan_no_overlap(&req).unwrap();
|
|
assert_eq!(
|
|
actions(&plan),
|
|
vec![
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Horizontal
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Vertical
|
|
},
|
|
PhaseAction {
|
|
kind: PhaseKind::Scale,
|
|
axis: PhaseAxis::Vertical
|
|
},
|
|
]
|
|
);
|
|
assert!(validate_plan_continuous(&req, &plan));
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_diagonal_motion_falls_back_to_linear() {
|
|
let req = request(vec![MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 100, 100),
|
|
to: rect(100, 100, 200, 200),
|
|
hierarchy: Default::default(),
|
|
}]);
|
|
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
|
|
let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err();
|
|
assert_eq!(diagnostic.forward, MultiphasePlanFailure::NoPattern);
|
|
assert_eq!(diagnostic.reverse, Some(MultiphasePlanFailure::NoPattern));
|
|
let mut expected = no_pattern_attempts(PlanDirection::Forward);
|
|
expected.extend(no_pattern_attempts(PlanDirection::Reverse));
|
|
assert_eq!(diagnostic.attempted, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn diagnostics_report_shrink_bound_rejections() {
|
|
let req = MultiphaseRequest {
|
|
bounds: rect(0, 0, 400, 100),
|
|
clearance: 0,
|
|
windows: vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 200, 100),
|
|
to: rect(0, 0, 10, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(200, 0, 400, 100),
|
|
to: rect(10, 0, 400, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
],
|
|
};
|
|
|
|
assert!(matches!(
|
|
plan_no_overlap_with_diagnostics(&req).unwrap_err().forward,
|
|
MultiphasePlanFailure::ShrinkBound {
|
|
axis: PhaseAxis::Horizontal,
|
|
available: 10,
|
|
required: 50,
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn diagnostics_report_candidate_validation_rejections() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 60, 60),
|
|
to: rect(180, 0, 240, 60),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(90, 0, 150, 60),
|
|
to: rect(90, 0, 150, 60),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let rejection =
|
|
MultiphasePlanFailure::Validation(MultiphaseValidationError::PhaseOverlap {
|
|
phase: 0,
|
|
a: id(1),
|
|
b: id(2),
|
|
});
|
|
let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err();
|
|
|
|
assert_eq!(diagnostic.forward, rejection);
|
|
assert_eq!(diagnostic.reverse, Some(rejection));
|
|
assert_eq!(
|
|
diagnostic.attempted[0],
|
|
RejectedStrategy {
|
|
direction: PlanDirection::Forward,
|
|
strategy: PlanStrategy::SingleAction,
|
|
reason: rejection,
|
|
}
|
|
);
|
|
assert!(diagnostic.attempted.iter().any(|attempt| *attempt
|
|
== RejectedStrategy {
|
|
direction: PlanDirection::Reverse,
|
|
strategy: PlanStrategy::SingleAction,
|
|
reason: rejection,
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn hierarchy_metadata_classifies_depth_and_mono_transitions() {
|
|
let source = MultiphaseHierarchyPosition {
|
|
parent: Some(id(10)),
|
|
depth: 2,
|
|
sibling_index: Some(0),
|
|
split_axis: Some(PhaseAxis::Vertical),
|
|
nearest_horizontal_split_depth: Some(1),
|
|
nearest_vertical_split_depth: Some(2),
|
|
..Default::default()
|
|
};
|
|
let target = MultiphaseHierarchyPosition {
|
|
parent: Some(id(11)),
|
|
depth: 1,
|
|
sibling_index: Some(2),
|
|
split_axis: Some(PhaseAxis::Horizontal),
|
|
nearest_horizontal_split_depth: Some(1),
|
|
..Default::default()
|
|
};
|
|
assert_eq!(
|
|
MultiphaseWindowHierarchy::new(source, target).transition,
|
|
MultiphaseHierarchyTransition::Ascending
|
|
);
|
|
assert_eq!(source.nearest_vertical_split_depth, Some(2));
|
|
|
|
let entering_mono = MultiphaseWindowHierarchy::new(
|
|
source,
|
|
MultiphaseHierarchyPosition {
|
|
parent_is_mono: true,
|
|
mono_active: true,
|
|
..target
|
|
},
|
|
);
|
|
assert_eq!(
|
|
entering_mono.transition,
|
|
MultiphaseHierarchyTransition::EnteringMono
|
|
);
|
|
assert_eq!(
|
|
entering_mono.reversed().transition,
|
|
MultiphaseHierarchyTransition::ExitingMono
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn continuous_validation_rejects_narrow_mid_phase_overlap() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 10, 10),
|
|
to: rect(100, 0, 110, 10),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(13, 0, 14, 10),
|
|
to: rect(13, 0, 14, 10),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let plan = MultiphasePlan {
|
|
phases: vec![MultiphasePhase {
|
|
action: MultiphasePhaseAction::Uniform(PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
}),
|
|
steps: vec![MultiphaseStep {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 10, 10),
|
|
to: rect(100, 0, 110, 10),
|
|
}],
|
|
}],
|
|
};
|
|
|
|
assert_eq!(
|
|
validate_plan_continuous_diagnostic(&req, &plan),
|
|
Err(MultiphaseValidationError::PhaseOverlap {
|
|
phase: 0,
|
|
a: id(1),
|
|
b: id(2),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn continuous_validation_allows_edge_touching_motion() {
|
|
let req = request(vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 10, 10),
|
|
to: rect(10, 0, 20, 10),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(20, 0, 30, 10),
|
|
to: rect(20, 0, 30, 10),
|
|
hierarchy: Default::default(),
|
|
},
|
|
]);
|
|
let plan = MultiphasePlan {
|
|
phases: vec![MultiphasePhase {
|
|
action: MultiphasePhaseAction::Uniform(PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
}),
|
|
steps: vec![MultiphaseStep {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 10, 10),
|
|
to: rect(10, 0, 20, 10),
|
|
}],
|
|
}],
|
|
};
|
|
|
|
assert!(validate_plan_continuous(&req, &plan));
|
|
}
|
|
|
|
#[test]
|
|
fn continuous_validation_rejects_mixed_phase_action_count_mismatch() {
|
|
let req = request(vec![
|
|
window(1, rect(0, 0, 40, 40), rect(40, 0, 80, 40)),
|
|
window(2, rect(100, 0, 140, 40), rect(100, 0, 140, 80)),
|
|
]);
|
|
let plan = MultiphasePlan {
|
|
phases: vec![MultiphasePhase {
|
|
action: MultiphasePhaseAction::Mixed(vec![PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
}]),
|
|
steps: vec![
|
|
MultiphaseStep {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 40, 40),
|
|
to: rect(40, 0, 80, 40),
|
|
},
|
|
MultiphaseStep {
|
|
node_id: id(2),
|
|
from: rect(100, 0, 140, 40),
|
|
to: rect(100, 0, 140, 80),
|
|
},
|
|
],
|
|
}],
|
|
};
|
|
|
|
assert_eq!(
|
|
validate_plan_continuous_diagnostic(&req, &plan),
|
|
Err(MultiphaseValidationError::PhaseActionCount {
|
|
phase: 0,
|
|
actions: 1,
|
|
steps: 2,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn continuous_validation_rejects_stale_step_start_rect() {
|
|
let req = request(vec![MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 10, 10),
|
|
to: rect(20, 0, 30, 10),
|
|
hierarchy: Default::default(),
|
|
}]);
|
|
let plan = MultiphasePlan {
|
|
phases: vec![MultiphasePhase {
|
|
action: MultiphasePhaseAction::Uniform(PhaseAction {
|
|
kind: PhaseKind::Move,
|
|
axis: PhaseAxis::Horizontal,
|
|
}),
|
|
steps: vec![MultiphaseStep {
|
|
node_id: id(1),
|
|
from: rect(5, 0, 15, 10),
|
|
to: rect(20, 0, 30, 10),
|
|
}],
|
|
}],
|
|
};
|
|
|
|
assert_eq!(
|
|
validate_plan_continuous_diagnostic(&req, &plan),
|
|
Err(MultiphaseValidationError::StaleStepStart {
|
|
phase: 0,
|
|
node_id: id(1),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn motion_groups_split_disjoint_layout_changes() {
|
|
let windows = vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 100, 100),
|
|
to: rect(100, 0, 200, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(100, 0, 200, 100),
|
|
to: rect(0, 0, 100, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(3),
|
|
from: rect(300, 0, 400, 100),
|
|
to: rect(400, 0, 500, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
];
|
|
assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1], vec![2]]);
|
|
}
|
|
|
|
#[test]
|
|
fn motion_groups_are_transitive() {
|
|
let windows = vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 100, 100),
|
|
to: rect(80, 0, 180, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(170, 0, 270, 100),
|
|
to: rect(250, 0, 350, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(3),
|
|
from: rect(90, 0, 180, 100),
|
|
to: rect(180, 0, 260, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
];
|
|
assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1, 2]]);
|
|
}
|
|
|
|
#[test]
|
|
fn motion_groups_join_across_animation_clearance() {
|
|
let windows = vec![
|
|
MultiphaseWindow {
|
|
node_id: id(1),
|
|
from: rect(0, 0, 100, 100),
|
|
to: rect(0, 0, 80, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
MultiphaseWindow {
|
|
node_id: id(2),
|
|
from: rect(120, 0, 220, 100),
|
|
to: rect(110, 0, 210, 100),
|
|
hierarchy: Default::default(),
|
|
},
|
|
];
|
|
assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0], vec![1]]);
|
|
assert_eq!(partition_motion_groups(&windows, 10), vec![vec![0, 1]]);
|
|
}
|
|
}
|