Add multiphase no-overlap planner groundwork
This commit is contained in:
parent
2115518edf
commit
41d2fef177
3 changed files with 629 additions and 0 deletions
|
|
@ -30,6 +30,15 @@ be handled deliberately.
|
||||||
partial inspiration for tiling style and titlebar/grouping behavior.
|
partial inspiration for tiling style and titlebar/grouping behavior.
|
||||||
- Mono mode should mostly avoid animations. Exceptions are windows entering or
|
- Mono mode should mostly avoid animations. Exceptions are windows entering or
|
||||||
exiting mono mode, where a visual transition can clarify the hierarchy change.
|
exiting mono mode, where a visual transition can clarify the hierarchy change.
|
||||||
|
- Multiphase shrink steps should not normally need to reduce a tiled window far
|
||||||
|
below roughly one quarter of the relevant full size. The implementation may
|
||||||
|
enforce a conservative sanity minimum, and pathological cases may fall back.
|
||||||
|
- If the no-overlap planner cannot produce a legal sequence, only the affected
|
||||||
|
group should fall back to linear animation. This is expected to be rare for
|
||||||
|
valid tiling layouts.
|
||||||
|
- When entering mono mode, the active child should animate to the mono geometry.
|
||||||
|
Inactive siblings may snap invisible. Floats may overlap normally and do not
|
||||||
|
need the no-overlap planner.
|
||||||
|
|
||||||
## Texture Freezing Decision
|
## Texture Freezing Decision
|
||||||
|
|
||||||
|
|
@ -188,12 +197,22 @@ Preferred approach:
|
||||||
animated visual actors.
|
animated visual actors.
|
||||||
- Derive every leaf's per-phase rect from one phase schedule so parent and child
|
- Derive every leaf's per-phase rect from one phase schedule so parent and child
|
||||||
effects cannot compose into forbidden motion.
|
effects cannot compose into forbidden motion.
|
||||||
|
- Build the planner as pure geometry first. Live integration should collect
|
||||||
|
eligible leaf `(old, new)` rects across a command-driven layout pass, then
|
||||||
|
submit planner-produced phases as a batch. Per-node `tl_change_extents` calls
|
||||||
|
are too incremental to plan safely by themselves.
|
||||||
- Add container-level grouping only after the leaf planner proves correct.
|
- Add container-level grouping only after the leaf planner proves correct.
|
||||||
- Include hierarchy-transition metadata in the planner input: source parent,
|
- Include hierarchy-transition metadata in the planner input: source parent,
|
||||||
target parent, source depth, target depth, and whether the window is ascending,
|
target parent, source depth, target depth, and whether the window is ascending,
|
||||||
descending, or staying at the same hierarchy level.
|
descending, or staying at the same hierarchy level.
|
||||||
- For mono containers, suppress ordinary in-mono focus/tab changes. Animate only
|
- For mono containers, suppress ordinary in-mono focus/tab changes. Animate only
|
||||||
transitions into mono, out of mono, or across the mono boundary.
|
transitions into mono, out of mono, or across the mono boundary.
|
||||||
|
- When entering mono, the active child animates to the full mono area and
|
||||||
|
inactive siblings snap invisible. When exiting mono, ordinary tiled geometry
|
||||||
|
may animate from the mono child where that produces a clear hierarchy
|
||||||
|
transition.
|
||||||
|
- If a legal no-overlap sequence cannot be found for a group, fall back to the
|
||||||
|
linear animator for that group only. Float windows are outside this invariant.
|
||||||
|
|
||||||
Tests:
|
Tests:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ use {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod multiphase;
|
||||||
|
|
||||||
const DEFAULT_DURATION_MS: u32 = 160;
|
const DEFAULT_DURATION_MS: u32 = 160;
|
||||||
const CURVE_MAX_POINTS: usize = 33;
|
const CURVE_MAX_POINTS: usize = 33;
|
||||||
const CURVE_FLATNESS_EPSILON: f32 = 0.001;
|
const CURVE_FLATNESS_EPSILON: f32 = 0.001;
|
||||||
|
|
|
||||||
608
src/animation/multiphase.rs
Normal file
608
src/animation/multiphase.rs
Normal file
|
|
@ -0,0 +1,608 @@
|
||||||
|
use {crate::rect::Rect, crate::tree::NodeId};
|
||||||
|
|
||||||
|
const MIN_SHRINK_DENOMINATOR: i32 = 4;
|
||||||
|
const PHASE_VALIDATION_SAMPLES: [f64; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MultiphaseRequest {
|
||||||
|
pub bounds: Rect,
|
||||||
|
pub windows: Vec<MultiphaseWindow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MultiphaseWindow {
|
||||||
|
pub node_id: NodeId,
|
||||||
|
pub from: Rect,
|
||||||
|
pub to: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MultiphasePlan {
|
||||||
|
pub phases: Vec<MultiphasePhase>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MultiphasePhase {
|
||||||
|
pub action: PhaseAction,
|
||||||
|
pub steps: Vec<MultiphaseStep>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MultiphaseError {
|
||||||
|
EmptyBounds,
|
||||||
|
EmptyWindow,
|
||||||
|
DuplicateWindow,
|
||||||
|
InitialOverlap,
|
||||||
|
FinalOverlap,
|
||||||
|
NoPlan,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result<MultiphasePlan, MultiphaseError> {
|
||||||
|
validate_request(request)?;
|
||||||
|
if request
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.all(|window| window.from == window.to)
|
||||||
|
{
|
||||||
|
return Ok(MultiphasePlan { phases: vec![] });
|
||||||
|
}
|
||||||
|
if let Some(plan) = plan_forward(request) {
|
||||||
|
return Ok(plan);
|
||||||
|
}
|
||||||
|
let reversed = reverse_request(request);
|
||||||
|
if let Some(plan) = plan_forward(&reversed) {
|
||||||
|
return Ok(reverse_plan(plan));
|
||||||
|
}
|
||||||
|
Err(MultiphaseError::NoPlan)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 plan_forward(request: &MultiphaseRequest) -> Option<MultiphasePlan> {
|
||||||
|
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
||||||
|
if let Some(plan) = plan_axis_crossing_lanes(request, axis) {
|
||||||
|
return Some(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plan_horizontal_space_then_vertical_growth(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plan_axis_crossing_lanes(
|
||||||
|
request: &MultiphaseRequest,
|
||||||
|
axis: PhaseAxis,
|
||||||
|
) -> Option<MultiphasePlan> {
|
||||||
|
if request.windows.len() != 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let orth_min = request
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.map(|window| orth_start(window.from, axis))
|
||||||
|
.min()?;
|
||||||
|
let orth_max = request
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.map(|window| orth_end(window.from, axis))
|
||||||
|
.max()?;
|
||||||
|
if request.windows.iter().any(|window| {
|
||||||
|
main_size(window.from, axis) != main_size(window.to, axis)
|
||||||
|
|| 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 None;
|
||||||
|
}
|
||||||
|
let lane_size = (orth_max - orth_min) / request.windows.len() as i32;
|
||||||
|
if lane_size < sane_min_size(orth_max - orth_min) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut windows = request.windows.clone();
|
||||||
|
windows.sort_by_key(|window| window.node_id.0);
|
||||||
|
let mut phase1 = vec![];
|
||||||
|
let mut phase2 = vec![];
|
||||||
|
let mut phase3 = vec![];
|
||||||
|
for (idx, window) in windows.iter().enumerate() {
|
||||||
|
let lane_start = orth_min + lane_size * idx as i32;
|
||||||
|
let lane_end = if idx + 1 == windows.len() {
|
||||||
|
orth_max
|
||||||
|
} else {
|
||||||
|
lane_start + lane_size
|
||||||
|
};
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
push_step(&mut phase1, window.node_id, window.from, lane_from);
|
||||||
|
push_step(&mut phase2, window.node_id, lane_from, lane_to);
|
||||||
|
push_step(&mut phase3, window.node_id, lane_to, window.to);
|
||||||
|
}
|
||||||
|
build_validated_plan(
|
||||||
|
request,
|
||||||
|
[
|
||||||
|
(PhaseKind::Scale, axis.other(), phase1),
|
||||||
|
(PhaseKind::Move, axis, phase2),
|
||||||
|
(PhaseKind::Scale, axis.other(), phase3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plan_horizontal_space_then_vertical_growth(
|
||||||
|
request: &MultiphaseRequest,
|
||||||
|
) -> Option<MultiphasePlan> {
|
||||||
|
if request.windows.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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 || window.to.height() < min_height {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let x_changes = window.from.x1() != window.to.x1() || window.from.x2() != window.to.x2();
|
||||||
|
let y_changes = window.from.y1() != window.to.y1() || window.from.y2() != window.to.y2();
|
||||||
|
if x_changes && window.from.width() == window.to.width() {
|
||||||
|
let after_move = Rect::new_sized_saturating(
|
||||||
|
window.to.x1(),
|
||||||
|
window.from.y1(),
|
||||||
|
window.to.width(),
|
||||||
|
window.from.height(),
|
||||||
|
);
|
||||||
|
push_step(&mut phase2, window.node_id, window.from, after_move);
|
||||||
|
if y_changes {
|
||||||
|
push_step(&mut phase3, window.node_id, after_move, window.to);
|
||||||
|
}
|
||||||
|
} else if x_changes {
|
||||||
|
let after_x_scale = Rect::new_sized_saturating(
|
||||||
|
window.to.x1(),
|
||||||
|
window.from.y1(),
|
||||||
|
window.to.width(),
|
||||||
|
window.from.height(),
|
||||||
|
);
|
||||||
|
push_step(&mut phase1, window.node_id, window.from, after_x_scale);
|
||||||
|
if y_changes {
|
||||||
|
push_step(&mut phase3, window.node_id, after_x_scale, window.to);
|
||||||
|
}
|
||||||
|
} else if y_changes {
|
||||||
|
push_step(&mut phase3, window.node_id, window.from, window.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if phase1.is_empty() || phase2.is_empty() || phase3.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
build_validated_plan(
|
||||||
|
request,
|
||||||
|
[
|
||||||
|
(PhaseKind::Scale, PhaseAxis::Horizontal, phase1),
|
||||||
|
(PhaseKind::Move, PhaseAxis::Horizontal, phase2),
|
||||||
|
(PhaseKind::Scale, PhaseAxis::Vertical, phase3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_validated_plan<const N: usize>(
|
||||||
|
request: &MultiphaseRequest,
|
||||||
|
phases: [(PhaseKind, PhaseAxis, Vec<MultiphaseStep>); N],
|
||||||
|
) -> Option<MultiphasePlan> {
|
||||||
|
let phases: Vec<_> = phases
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(kind, axis, steps)| {
|
||||||
|
(!steps.is_empty()).then_some(MultiphasePhase {
|
||||||
|
action: PhaseAction { kind, axis },
|
||||||
|
steps,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if phases.iter().any(|phase| {
|
||||||
|
phase
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.any(|step| classify_step(*step) != Some(phase.action))
|
||||||
|
}) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let plan = MultiphasePlan { phases };
|
||||||
|
validate_plan_samples(request, &plan).then_some(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_plan_samples(request: &MultiphaseRequest, plan: &MultiphasePlan) -> bool {
|
||||||
|
let mut current: Vec<_> = request
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.map(|window| (window.node_id, window.from))
|
||||||
|
.collect();
|
||||||
|
if overlaps(current.iter().map(|(_, rect)| *rect)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for phase in &plan.phases {
|
||||||
|
for t in PHASE_VALIDATION_SAMPLES {
|
||||||
|
let rects = current.iter().map(|(node_id, rect)| {
|
||||||
|
phase
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.find(|step| step.node_id == *node_id)
|
||||||
|
.map(|step| super::lerp_rect(step.from, step.to, t))
|
||||||
|
.unwrap_or(*rect)
|
||||||
|
});
|
||||||
|
if overlaps(rects) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for step in &phase.steps {
|
||||||
|
let Some((_, rect)) = current
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(node_id, _)| *node_id == step.node_id)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
*rect = step.to;
|
||||||
|
}
|
||||||
|
if overlaps(current.iter().map(|(_, rect)| *rect)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.windows.iter().all(|window| {
|
||||||
|
current
|
||||||
|
.iter()
|
||||||
|
.find(|(node_id, _)| *node_id == window.node_id)
|
||||||
|
.is_some_and(|(_, rect)| *rect == window.to)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest {
|
||||||
|
MultiphaseRequest {
|
||||||
|
bounds: request.bounds,
|
||||||
|
windows: request
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.map(|window| MultiphaseWindow {
|
||||||
|
node_id: window.node_id,
|
||||||
|
from: window.to,
|
||||||
|
to: window.from,
|
||||||
|
})
|
||||||
|
.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 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 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 request(windows: Vec<MultiphaseWindow>) -> MultiphaseRequest {
|
||||||
|
MultiphaseRequest {
|
||||||
|
bounds: rect(0, 0, 400, 100),
|
||||||
|
windows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn actions(plan: &MultiphasePlan) -> Vec<PhaseAction> {
|
||||||
|
plan.phases.iter().map(|phase| phase.action).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn horizontal_swap_shrinks_moves_then_grows_without_overlap() {
|
||||||
|
let req = request(vec![
|
||||||
|
MultiphaseWindow {
|
||||||
|
node_id: id(1),
|
||||||
|
from: rect(0, 0, 100, 100),
|
||||||
|
to: rect(100, 0, 200, 100),
|
||||||
|
},
|
||||||
|
MultiphaseWindow {
|
||||||
|
node_id: id(2),
|
||||||
|
from: rect(100, 0, 200, 100),
|
||||||
|
to: rect(0, 0, 100, 100),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
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::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!(validate_plan_samples(&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),
|
||||||
|
},
|
||||||
|
MultiphaseWindow {
|
||||||
|
node_id: id(2),
|
||||||
|
from: rect(0, 0, 100, 100),
|
||||||
|
to: rect(100, 0, 200, 100),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let plan = plan_no_overlap(&req).unwrap();
|
||||||
|
assert_eq!(plan.phases[0].steps[0].to, rect(100, 0, 200, 50));
|
||||||
|
assert_eq!(plan.phases[0].steps[1].to, rect(0, 50, 100, 100));
|
||||||
|
assert!(validate_plan_samples(&req, &plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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),
|
||||||
|
},
|
||||||
|
MultiphaseWindow {
|
||||||
|
node_id: id(2),
|
||||||
|
from: rect(200, 0, 400, 50),
|
||||||
|
to: rect(100, 0, 300, 100),
|
||||||
|
},
|
||||||
|
MultiphaseWindow {
|
||||||
|
node_id: id(3),
|
||||||
|
from: rect(200, 50, 400, 100),
|
||||||
|
to: rect(300, 0, 400, 100),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
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_samples(&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),
|
||||||
|
},
|
||||||
|
MultiphaseWindow {
|
||||||
|
node_id: id(2),
|
||||||
|
from: rect(100, 0, 300, 100),
|
||||||
|
to: rect(200, 0, 400, 50),
|
||||||
|
},
|
||||||
|
MultiphaseWindow {
|
||||||
|
node_id: id(3),
|
||||||
|
from: rect(300, 0, 400, 100),
|
||||||
|
to: rect(200, 50, 400, 100),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
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::Horizontal
|
||||||
|
},
|
||||||
|
PhaseAction {
|
||||||
|
kind: PhaseKind::Scale,
|
||||||
|
axis: PhaseAxis::Horizontal
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(validate_plan_samples(&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),
|
||||||
|
}]);
|
||||||
|
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue