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.
|
||||
- Mono mode should mostly avoid animations. Exceptions are windows entering or
|
||||
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
|
||||
|
||||
|
|
@ -188,12 +197,22 @@ Preferred approach:
|
|||
animated visual actors.
|
||||
- Derive every leaf's per-phase rect from one phase schedule so parent and child
|
||||
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.
|
||||
- Include hierarchy-transition metadata in the planner input: source parent,
|
||||
target parent, source depth, target depth, and whether the window is ascending,
|
||||
descending, or staying at the same hierarchy level.
|
||||
- For mono containers, suppress ordinary in-mono focus/tab changes. Animate only
|
||||
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:
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ use {
|
|||
},
|
||||
};
|
||||
|
||||
pub mod multiphase;
|
||||
|
||||
const DEFAULT_DURATION_MS: u32 = 160;
|
||||
const CURVE_MAX_POINTS: usize = 33;
|
||||
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