862 lines
28 KiB
Rust
862 lines
28 KiB
Rust
use {
|
|
crate::{
|
|
animation::{
|
|
AnimationCurve, AnimationStyle, AnimationTick, RetainedExitLayer, RetainedToplevel,
|
|
expand_damage_rect,
|
|
multiphase::{
|
|
MultiphasePhase, MultiphasePlan, MultiphasePlanFailure, MultiphaseRequest,
|
|
MultiphaseWindow, MultiphaseWindowHierarchy, partition_motion_groups,
|
|
plan_no_overlap_with_diagnostics, validate_phase_paths,
|
|
},
|
|
spawn_in_start_rect,
|
|
},
|
|
rect::Rect,
|
|
tree::NodeId,
|
|
},
|
|
std::rc::Rc,
|
|
};
|
|
|
|
use super::State;
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct LayoutAnimationCandidate {
|
|
node_id: NodeId,
|
|
old: Rect,
|
|
new: Rect,
|
|
curve: AnimationCurve,
|
|
style: AnimationStyle,
|
|
hierarchy: MultiphaseWindowHierarchy,
|
|
}
|
|
|
|
fn coalesce_layout_animation_candidates(
|
|
candidates: Vec<LayoutAnimationCandidate>,
|
|
) -> Vec<LayoutAnimationCandidate> {
|
|
let mut merged: Vec<LayoutAnimationCandidate> = vec![];
|
|
for candidate in candidates {
|
|
if let Some(existing) = merged
|
|
.iter_mut()
|
|
.find(|existing| existing.node_id == candidate.node_id)
|
|
{
|
|
existing.new = candidate.new;
|
|
existing.curve = candidate.curve;
|
|
existing.style = candidate.style;
|
|
existing.hierarchy = MultiphaseWindowHierarchy::new(
|
|
existing.hierarchy.source,
|
|
candidate.hierarchy.target,
|
|
);
|
|
} else {
|
|
merged.push(candidate);
|
|
}
|
|
}
|
|
merged
|
|
}
|
|
|
|
fn layout_animation_group_uses_plain(
|
|
candidates: &[LayoutAnimationCandidate],
|
|
group: &[usize],
|
|
) -> bool {
|
|
group
|
|
.iter()
|
|
.any(|&idx| candidates[idx].style == AnimationStyle::Plain)
|
|
}
|
|
|
|
fn bridged_retarget_plan(
|
|
request: &MultiphaseRequest,
|
|
candidates: &[LayoutAnimationCandidate],
|
|
group: &[usize],
|
|
bridge_paths: &[Vec<(Rect, Rect)>],
|
|
bridge_phase_count: usize,
|
|
follow_phases: &[MultiphasePhase],
|
|
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
|
let mut paths = vec![];
|
|
for (group_pos, &idx) in group.iter().enumerate() {
|
|
let candidate = &candidates[idx];
|
|
let window = request.windows[group_pos];
|
|
let Some(bridge_path) = bridge_paths.get(group_pos) else {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
};
|
|
let mut path = bridge_path.clone();
|
|
let mut current = path
|
|
.last()
|
|
.map(|(_, to)| *to)
|
|
.unwrap_or(window.from);
|
|
while path.len() < bridge_phase_count {
|
|
path.push((current, current));
|
|
}
|
|
if current != candidate.old {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
for phase in follow_phases {
|
|
match phase
|
|
.steps
|
|
.iter()
|
|
.find(|step| step.node_id == candidate.node_id)
|
|
{
|
|
Some(step) => {
|
|
if step.from != current {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
path.push((step.from, step.to));
|
|
current = step.to;
|
|
}
|
|
None => path.push((current, current)),
|
|
}
|
|
}
|
|
if current != window.to {
|
|
return Err(MultiphasePlanFailure::NoPattern);
|
|
}
|
|
paths.push(path);
|
|
}
|
|
validate_phase_paths(request, &paths)
|
|
}
|
|
|
|
impl State {
|
|
pub fn queue_tiled_animation(
|
|
self: &Rc<Self>,
|
|
node_id: NodeId,
|
|
old: Rect,
|
|
new: Rect,
|
|
) {
|
|
let curve = self
|
|
.layout_animation_curve_override
|
|
.get()
|
|
.unwrap_or_else(|| self.animations.curve.get());
|
|
self.queue_layout_animation(
|
|
node_id,
|
|
old,
|
|
new,
|
|
curve,
|
|
MultiphaseWindowHierarchy::default(),
|
|
);
|
|
}
|
|
|
|
pub fn queue_tiled_animation_with_hierarchy(
|
|
self: &Rc<Self>,
|
|
node_id: NodeId,
|
|
old: Rect,
|
|
new: Rect,
|
|
hierarchy: MultiphaseWindowHierarchy,
|
|
) {
|
|
let curve = self
|
|
.layout_animation_curve_override
|
|
.get()
|
|
.unwrap_or_else(|| self.animations.curve.get());
|
|
self.queue_layout_animation(node_id, old, new, curve, hierarchy);
|
|
}
|
|
|
|
pub fn queue_linear_layout_animation(
|
|
self: &Rc<Self>,
|
|
node_id: NodeId,
|
|
old: Rect,
|
|
new: Rect,
|
|
) {
|
|
self.queue_layout_animation(
|
|
node_id,
|
|
old,
|
|
new,
|
|
AnimationCurve::Linear,
|
|
MultiphaseWindowHierarchy::default(),
|
|
);
|
|
}
|
|
|
|
fn queue_layout_animation(
|
|
self: &Rc<Self>,
|
|
node_id: NodeId,
|
|
old: Rect,
|
|
new: Rect,
|
|
curve: AnimationCurve,
|
|
hierarchy: MultiphaseWindowHierarchy,
|
|
) {
|
|
if !self.animations.enabled.get()
|
|
|| !self.layout_animations_active.get()
|
|
|| self.suppress_animations_for_next_layout.get()
|
|
{
|
|
return;
|
|
}
|
|
let (old_output, old_scale) = {
|
|
let (x, y) = old.center();
|
|
let (output, _, _) = self.find_closest_output(x, y);
|
|
(output.id, output.global.persistent.scale.get())
|
|
};
|
|
let (new_output, new_scale) = {
|
|
let (x, y) = new.center();
|
|
let (output, _, _) = self.find_closest_output(x, y);
|
|
(output.id, output.global.persistent.scale.get())
|
|
};
|
|
if old_output != new_output || old_scale != new_scale {
|
|
return;
|
|
}
|
|
let candidate = LayoutAnimationCandidate {
|
|
node_id,
|
|
old,
|
|
new,
|
|
curve,
|
|
style: self
|
|
.layout_animation_style_override
|
|
.get()
|
|
.unwrap_or_else(|| self.animations.style.get()),
|
|
hierarchy,
|
|
};
|
|
if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() {
|
|
batch.push(candidate);
|
|
return;
|
|
}
|
|
self.start_layout_animation_candidate(candidate, self.now_nsec());
|
|
}
|
|
|
|
fn start_layout_animation_candidate(
|
|
self: &Rc<Self>,
|
|
candidate: LayoutAnimationCandidate,
|
|
now_nsec: u64,
|
|
) {
|
|
let started = self.animations.set_target(
|
|
candidate.node_id,
|
|
candidate.old,
|
|
candidate.new,
|
|
None,
|
|
now_nsec,
|
|
self.animations.duration_ms.get(),
|
|
candidate.curve,
|
|
);
|
|
if started {
|
|
self.damage(expand_damage_rect(
|
|
candidate.old.union(candidate.new),
|
|
self.theme.sizes.border_width.get().max(0),
|
|
));
|
|
self.ensure_animation_tick();
|
|
}
|
|
}
|
|
|
|
pub fn begin_layout_animation_batch(&self) {
|
|
self.layout_animation_batch
|
|
.borrow_mut()
|
|
.get_or_insert_with(Vec::new);
|
|
}
|
|
|
|
pub fn finish_layout_animation_batch(self: &Rc<Self>) {
|
|
let Some(candidates) = self.layout_animation_batch.borrow_mut().take() else {
|
|
return;
|
|
};
|
|
let candidates = coalesce_layout_animation_candidates(candidates);
|
|
if candidates.is_empty() {
|
|
return;
|
|
}
|
|
let now = self.now_nsec();
|
|
let windows: Vec<_> = candidates
|
|
.iter()
|
|
.map(|candidate| {
|
|
MultiphaseWindow::with_hierarchy(
|
|
candidate.node_id,
|
|
self.animations
|
|
.visual_rect(candidate.node_id, candidate.old, now),
|
|
candidate.new,
|
|
candidate.hierarchy,
|
|
)
|
|
})
|
|
.collect();
|
|
for group in partition_motion_groups(&windows, self.layout_animation_clearance()) {
|
|
if layout_animation_group_uses_plain(&candidates, &group) {
|
|
for idx in group {
|
|
self.start_layout_animation_candidate(candidates[idx].clone(), now);
|
|
}
|
|
continue;
|
|
}
|
|
if self.start_multiphase_layout_animation(&candidates, &windows, &group, now) {
|
|
continue;
|
|
}
|
|
for idx in group {
|
|
self.start_layout_animation_candidate(candidates[idx].clone(), now);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn layout_animation_clearance(&self) -> i32 {
|
|
let border = self.theme.sizes.border_width.get().max(0);
|
|
let gap = self.theme.sizes.gap.get().max(0);
|
|
if gap == 0 { border } else { gap + 2 * border }
|
|
}
|
|
|
|
fn start_multiphase_layout_animation(
|
|
self: &Rc<Self>,
|
|
candidates: &[LayoutAnimationCandidate],
|
|
windows: &[MultiphaseWindow],
|
|
group: &[usize],
|
|
now_nsec: u64,
|
|
) -> bool {
|
|
let request_windows: Vec<_> = group.iter().map(|&idx| windows[idx]).collect();
|
|
let Some(first) = request_windows.first() else {
|
|
return false;
|
|
};
|
|
let mut bounds = first.from.union(first.to);
|
|
for window in &request_windows[1..] {
|
|
bounds = bounds.union(window.from).union(window.to);
|
|
}
|
|
let request = MultiphaseRequest {
|
|
bounds,
|
|
windows: request_windows,
|
|
clearance: self.layout_animation_clearance(),
|
|
};
|
|
if self.start_existing_phased_retarget(candidates, windows, group, &request, now_nsec) {
|
|
return true;
|
|
}
|
|
if self.start_bridged_phased_retarget(candidates, windows, group, &request, now_nsec) {
|
|
return true;
|
|
}
|
|
let plan = match plan_no_overlap_with_diagnostics(&request) {
|
|
Ok(plan) => plan,
|
|
Err(diagnostic) => {
|
|
log::debug!(
|
|
"falling back to plain layout animation for group {:?}: {:?}",
|
|
group,
|
|
diagnostic
|
|
);
|
|
return false;
|
|
}
|
|
};
|
|
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
|
|
}
|
|
|
|
fn start_existing_phased_retarget(
|
|
self: &Rc<Self>,
|
|
candidates: &[LayoutAnimationCandidate],
|
|
windows: &[MultiphaseWindow],
|
|
group: &[usize],
|
|
request: &MultiphaseRequest,
|
|
now_nsec: u64,
|
|
) -> bool {
|
|
let mut paths = vec![];
|
|
for &idx in group {
|
|
let candidate = &candidates[idx];
|
|
let window = windows[idx];
|
|
let Some(path) =
|
|
self.animations
|
|
.phased_route_to(candidate.node_id, window.to, now_nsec)
|
|
else {
|
|
return false;
|
|
};
|
|
paths.push(path);
|
|
}
|
|
let plan = match validate_phase_paths(request, &paths) {
|
|
Ok(plan) => plan,
|
|
Err(error) => {
|
|
log::debug!(
|
|
"existing phased retarget rejected for group {:?}: {:?}",
|
|
group,
|
|
error
|
|
);
|
|
return false;
|
|
}
|
|
};
|
|
log::debug!("retargeting active phased animation for group {:?}", group);
|
|
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
|
|
}
|
|
|
|
fn start_bridged_phased_retarget(
|
|
self: &Rc<Self>,
|
|
candidates: &[LayoutAnimationCandidate],
|
|
windows: &[MultiphaseWindow],
|
|
group: &[usize],
|
|
request: &MultiphaseRequest,
|
|
now_nsec: u64,
|
|
) -> bool {
|
|
let mut bridge_paths = vec![];
|
|
let mut bridge_phase_count = 0;
|
|
let mut has_bridge = false;
|
|
for &idx in group {
|
|
let candidate = &candidates[idx];
|
|
let window = windows[idx];
|
|
if window.from == candidate.old {
|
|
bridge_paths.push(vec![]);
|
|
continue;
|
|
}
|
|
let Some(path) =
|
|
self.animations
|
|
.phased_route_to(candidate.node_id, candidate.old, now_nsec)
|
|
else {
|
|
return false;
|
|
};
|
|
if !path.is_empty() {
|
|
has_bridge = true;
|
|
bridge_phase_count = bridge_phase_count.max(path.len());
|
|
}
|
|
bridge_paths.push(path);
|
|
}
|
|
if !has_bridge {
|
|
return false;
|
|
}
|
|
|
|
let settled_windows: Vec<_> = group
|
|
.iter()
|
|
.map(|&idx| {
|
|
let candidate = &candidates[idx];
|
|
MultiphaseWindow::with_hierarchy(
|
|
candidate.node_id,
|
|
candidate.old,
|
|
candidate.new,
|
|
candidate.hierarchy,
|
|
)
|
|
})
|
|
.collect();
|
|
let Some(first) = settled_windows.first() else {
|
|
return false;
|
|
};
|
|
let mut bounds = first.from.union(first.to);
|
|
for window in &settled_windows[1..] {
|
|
bounds = bounds.union(window.from).union(window.to);
|
|
}
|
|
let settled_request = MultiphaseRequest {
|
|
bounds,
|
|
windows: settled_windows,
|
|
clearance: self.layout_animation_clearance(),
|
|
};
|
|
let follow_plan = match plan_no_overlap_with_diagnostics(&settled_request) {
|
|
Ok(plan) => plan,
|
|
Err(diagnostic) => {
|
|
log::debug!(
|
|
"bridged phased retarget follow-up rejected for group {:?}: {:?}",
|
|
group,
|
|
diagnostic
|
|
);
|
|
return false;
|
|
}
|
|
};
|
|
let plan = match bridged_retarget_plan(
|
|
request,
|
|
candidates,
|
|
group,
|
|
&bridge_paths,
|
|
bridge_phase_count,
|
|
&follow_plan.phases,
|
|
) {
|
|
Ok(plan) => plan,
|
|
Err(error) => {
|
|
log::debug!(
|
|
"bridged phased retarget rejected for group {:?}: {:?}",
|
|
group,
|
|
error
|
|
);
|
|
return false;
|
|
}
|
|
};
|
|
log::debug!("bridging active phased animation for group {:?}", group);
|
|
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
|
|
}
|
|
|
|
fn start_multiphase_plan(
|
|
self: &Rc<Self>,
|
|
candidates: &[LayoutAnimationCandidate],
|
|
windows: &[MultiphaseWindow],
|
|
group: &[usize],
|
|
plan_phases: &[crate::animation::multiphase::MultiphasePhase],
|
|
now_nsec: u64,
|
|
) -> bool {
|
|
if plan_phases.is_empty() {
|
|
return false;
|
|
}
|
|
let mut entries = vec![];
|
|
for &idx in group {
|
|
let candidate = &candidates[idx];
|
|
let window = windows[idx];
|
|
let mut current = window.from;
|
|
let mut damage = current.union(window.to);
|
|
let mut phases = vec![];
|
|
for phase in plan_phases {
|
|
match phase
|
|
.steps
|
|
.iter()
|
|
.find(|step| step.node_id == candidate.node_id)
|
|
{
|
|
Some(step) => {
|
|
phases.push((step.from, step.to));
|
|
damage = damage.union(step.from).union(step.to);
|
|
current = step.to;
|
|
}
|
|
None => phases.push((current, current)),
|
|
}
|
|
}
|
|
if current != window.to {
|
|
return false;
|
|
}
|
|
entries.push((candidate.clone(), phases, damage));
|
|
}
|
|
let mut started_any = false;
|
|
for (candidate, phases, damage) in entries {
|
|
if self.animations.set_phased_target(
|
|
candidate.node_id,
|
|
phases,
|
|
None,
|
|
now_nsec,
|
|
self.animations.duration_ms.get(),
|
|
candidate.curve,
|
|
) {
|
|
started_any = true;
|
|
self.damage(expand_damage_rect(
|
|
damage,
|
|
self.theme.sizes.border_width.get().max(0),
|
|
));
|
|
}
|
|
}
|
|
if started_any {
|
|
self.ensure_animation_tick();
|
|
}
|
|
started_any
|
|
}
|
|
|
|
pub fn queue_spawn_in_animation(
|
|
self: &Rc<Self>,
|
|
node_id: NodeId,
|
|
target: Rect,
|
|
) {
|
|
if !self.animations.enabled.get() || target.is_empty() {
|
|
return;
|
|
}
|
|
let start = spawn_in_start_rect(target);
|
|
let now = self.now_nsec();
|
|
let started = self.animations.set_spawn_in(
|
|
node_id,
|
|
target,
|
|
None,
|
|
now,
|
|
self.animations.duration_ms.get(),
|
|
self.animations.curve.get(),
|
|
);
|
|
if started {
|
|
self.damage(expand_damage_rect(
|
|
start.union(target),
|
|
self.theme.sizes.border_width.get().max(0),
|
|
));
|
|
self.ensure_animation_tick();
|
|
}
|
|
}
|
|
|
|
pub fn queue_spawn_out_animation(
|
|
self: &Rc<Self>,
|
|
from: Rect,
|
|
frame_inset: i32,
|
|
retained: Rc<RetainedToplevel>,
|
|
active: bool,
|
|
layer: RetainedExitLayer,
|
|
) {
|
|
if !self.animations.enabled.get() || from.is_empty() {
|
|
return;
|
|
}
|
|
let now = self.now_nsec();
|
|
let started = self.animations.set_spawn_out(
|
|
from,
|
|
frame_inset,
|
|
retained,
|
|
active,
|
|
layer,
|
|
now,
|
|
self.animations.duration_ms.get(),
|
|
self.animations.curve.get(),
|
|
);
|
|
if started {
|
|
self.damage(expand_damage_rect(
|
|
from,
|
|
self.theme.sizes.border_width.get().max(0),
|
|
));
|
|
self.ensure_animation_tick();
|
|
}
|
|
}
|
|
|
|
pub fn set_animations_enabled(&self, enabled: bool) {
|
|
if self.animations.enabled.replace(enabled) && !enabled {
|
|
self.animations.clear();
|
|
self.damage(self.root.extents.get());
|
|
}
|
|
}
|
|
|
|
pub fn set_animation_duration_ms(&self, duration_ms: u32) {
|
|
self.animations.duration_ms.set(duration_ms);
|
|
}
|
|
|
|
pub fn set_animation_curve(&self, curve: u32) {
|
|
self.animations
|
|
.curve
|
|
.set(AnimationCurve::from_config(curve));
|
|
}
|
|
|
|
pub fn set_animation_style(&self, style: u32) -> bool {
|
|
let Some(style) = AnimationStyle::from_config(style) else {
|
|
return false;
|
|
};
|
|
self.animations.style.set(style);
|
|
true
|
|
}
|
|
|
|
pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
|
|
let Some(curve) = AnimationCurve::from_cubic_bezier(x1, y1, x2, y2) else {
|
|
return false;
|
|
};
|
|
self.animations.curve.set(curve);
|
|
true
|
|
}
|
|
|
|
pub fn with_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
|
|
let prev_requested = self.layout_animations_requested.replace(true);
|
|
let prev_active = self.layout_animations_active.replace(true);
|
|
let res = f();
|
|
self.layout_animations_requested.set(prev_requested);
|
|
self.layout_animations_active.set(prev_active);
|
|
res
|
|
}
|
|
|
|
pub fn with_linear_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
|
|
let prev_requested = self.layout_animations_requested.replace(true);
|
|
let prev_active = self.layout_animations_active.replace(true);
|
|
let prev_curve = self
|
|
.layout_animation_curve_override
|
|
.replace(Some(AnimationCurve::Linear));
|
|
let prev_style = self
|
|
.layout_animation_style_override
|
|
.replace(Some(AnimationStyle::Plain));
|
|
let res = f();
|
|
self.layout_animations_requested.set(prev_requested);
|
|
self.layout_animations_active.set(prev_active);
|
|
self.layout_animation_curve_override.set(prev_curve);
|
|
self.layout_animation_style_override.set(prev_style);
|
|
res
|
|
}
|
|
|
|
fn ensure_animation_tick(self: &Rc<Self>) {
|
|
if self.animations.tick_is_active() {
|
|
return;
|
|
}
|
|
let outputs: Vec<_> = self.root.outputs.lock().values().cloned().collect();
|
|
if outputs.is_empty() {
|
|
return;
|
|
}
|
|
let tick = Rc::new_cyclic(|weak| AnimationTick::new(self, weak));
|
|
for output in &outputs {
|
|
tick.attach(output);
|
|
}
|
|
self.animations.set_tick(tick);
|
|
for output in &outputs {
|
|
self.damage(output.global.pos.get());
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use {
|
|
super::*,
|
|
crate::animation::multiphase::MultiphaseHierarchyPosition,
|
|
};
|
|
|
|
fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect {
|
|
Rect::new_saturating(x1, y1, x2, y2)
|
|
}
|
|
|
|
fn hierarchy(
|
|
source: MultiphaseHierarchyPosition,
|
|
target: MultiphaseHierarchyPosition,
|
|
) -> MultiphaseWindowHierarchy {
|
|
MultiphaseWindowHierarchy::new(source, target)
|
|
}
|
|
|
|
fn candidate(node_id: u32, style: AnimationStyle) -> LayoutAnimationCandidate {
|
|
candidate_rects(
|
|
node_id,
|
|
rect(0, 0, 100, 100),
|
|
rect(100, 0, 200, 100),
|
|
style,
|
|
)
|
|
}
|
|
|
|
fn candidate_rects(
|
|
node_id: u32,
|
|
old: Rect,
|
|
new: Rect,
|
|
style: AnimationStyle,
|
|
) -> LayoutAnimationCandidate {
|
|
LayoutAnimationCandidate {
|
|
node_id: NodeId(node_id),
|
|
old,
|
|
new,
|
|
curve: AnimationCurve::Linear,
|
|
style,
|
|
hierarchy: MultiphaseWindowHierarchy::default(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn plain_style_candidate_forces_group_plain() {
|
|
let candidates = vec![
|
|
candidate(1, AnimationStyle::Multiphase),
|
|
candidate(2, AnimationStyle::Plain),
|
|
];
|
|
|
|
assert!(!layout_animation_group_uses_plain(&candidates, &[0]));
|
|
assert!(layout_animation_group_uses_plain(&candidates, &[0, 1]));
|
|
}
|
|
|
|
#[test]
|
|
fn bridged_retarget_handles_second_rotation_interrupt() {
|
|
let a_left = rect(0, 0, 100, 100);
|
|
let c_mid = rect(100, 0, 200, 100);
|
|
let c_left = a_left;
|
|
let a_mid = c_mid;
|
|
let c_current = rect(150, 50, 250, 100);
|
|
let c_mid_lane = rect(100, 50, 200, 100);
|
|
let candidates = vec![
|
|
candidate_rects(1, a_left, a_mid, AnimationStyle::Multiphase),
|
|
candidate_rects(3, c_mid, c_left, AnimationStyle::Multiphase),
|
|
];
|
|
let request = MultiphaseRequest {
|
|
bounds: rect(0, 0, 250, 100),
|
|
windows: vec![
|
|
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
|
|
MultiphaseWindow::new(NodeId(3), c_current, c_left),
|
|
],
|
|
clearance: 0,
|
|
};
|
|
let settled_request = MultiphaseRequest {
|
|
bounds: rect(0, 0, 200, 100),
|
|
windows: vec![
|
|
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
|
|
MultiphaseWindow::new(NodeId(3), c_mid, c_left),
|
|
],
|
|
clearance: 0,
|
|
};
|
|
let follow_plan = plan_no_overlap_with_diagnostics(&settled_request).unwrap();
|
|
let bridge_paths = vec![vec![], vec![(c_current, c_mid_lane), (c_mid_lane, c_mid)]];
|
|
|
|
let plan = bridged_retarget_plan(
|
|
&request,
|
|
&candidates,
|
|
&[0, 1],
|
|
&bridge_paths,
|
|
2,
|
|
&follow_plan.phases,
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(plan
|
|
.phases
|
|
.iter()
|
|
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(1))));
|
|
assert!(plan
|
|
.phases
|
|
.iter()
|
|
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(3))));
|
|
}
|
|
|
|
#[test]
|
|
fn layout_animation_candidates_coalesce_duplicate_nodes() {
|
|
let source = MultiphaseHierarchyPosition {
|
|
parent: Some(NodeId(10).into()),
|
|
depth: 2,
|
|
sibling_index: Some(1),
|
|
..Default::default()
|
|
};
|
|
let intermediate = MultiphaseHierarchyPosition {
|
|
parent: Some(NodeId(11).into()),
|
|
depth: 1,
|
|
sibling_index: Some(0),
|
|
..Default::default()
|
|
};
|
|
let target = MultiphaseHierarchyPosition {
|
|
parent: Some(NodeId(12).into()),
|
|
depth: 0,
|
|
sibling_index: Some(2),
|
|
..Default::default()
|
|
};
|
|
let second_source = MultiphaseHierarchyPosition {
|
|
parent: Some(NodeId(20).into()),
|
|
depth: 1,
|
|
sibling_index: Some(0),
|
|
..Default::default()
|
|
};
|
|
let second_target = MultiphaseHierarchyPosition {
|
|
parent: Some(NodeId(20).into()),
|
|
depth: 1,
|
|
sibling_index: Some(1),
|
|
..Default::default()
|
|
};
|
|
|
|
let candidates = vec![
|
|
LayoutAnimationCandidate {
|
|
node_id: NodeId(1),
|
|
old: rect(0, 0, 100, 100),
|
|
new: rect(0, 0, 80, 100),
|
|
curve: AnimationCurve::Linear,
|
|
style: AnimationStyle::Multiphase,
|
|
hierarchy: hierarchy(source, intermediate),
|
|
},
|
|
LayoutAnimationCandidate {
|
|
node_id: NodeId(2),
|
|
old: rect(100, 0, 200, 100),
|
|
new: rect(120, 0, 220, 100),
|
|
curve: AnimationCurve::Linear,
|
|
style: AnimationStyle::Multiphase,
|
|
hierarchy: hierarchy(second_source, second_target),
|
|
},
|
|
LayoutAnimationCandidate {
|
|
node_id: NodeId(1),
|
|
old: rect(0, 0, 80, 100),
|
|
new: rect(0, 0, 60, 100),
|
|
curve: AnimationCurve::from_config(4),
|
|
style: AnimationStyle::Plain,
|
|
hierarchy: hierarchy(intermediate, target),
|
|
},
|
|
];
|
|
|
|
let merged = coalesce_layout_animation_candidates(candidates);
|
|
|
|
assert_eq!(merged.len(), 2);
|
|
assert_eq!(merged[0].node_id, NodeId(1));
|
|
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
|
|
assert_eq!(merged[0].new, rect(0, 0, 60, 100));
|
|
assert_eq!(merged[0].curve, AnimationCurve::from_config(4));
|
|
assert_eq!(merged[0].style, AnimationStyle::Plain);
|
|
assert_eq!(merged[0].hierarchy, hierarchy(source, target));
|
|
assert_eq!(merged[1].node_id, NodeId(2));
|
|
assert_eq!(merged[1].old, rect(100, 0, 200, 100));
|
|
assert_eq!(merged[1].new, rect(120, 0, 220, 100));
|
|
assert_eq!(merged[1].hierarchy, hierarchy(second_source, second_target));
|
|
}
|
|
|
|
#[test]
|
|
fn layout_animation_candidates_keep_coalesced_layout_noops() {
|
|
let hierarchy = MultiphaseWindowHierarchy::default();
|
|
let candidates = vec![
|
|
LayoutAnimationCandidate {
|
|
node_id: NodeId(1),
|
|
old: rect(0, 0, 100, 100),
|
|
new: rect(0, 0, 80, 100),
|
|
curve: AnimationCurve::Linear,
|
|
style: AnimationStyle::Multiphase,
|
|
hierarchy,
|
|
},
|
|
LayoutAnimationCandidate {
|
|
node_id: NodeId(1),
|
|
old: rect(0, 0, 80, 100),
|
|
new: rect(0, 0, 100, 100),
|
|
curve: AnimationCurve::Linear,
|
|
style: AnimationStyle::Plain,
|
|
hierarchy,
|
|
},
|
|
LayoutAnimationCandidate {
|
|
node_id: NodeId(2),
|
|
old: rect(100, 0, 200, 100),
|
|
new: rect(120, 0, 220, 100),
|
|
curve: AnimationCurve::Linear,
|
|
style: AnimationStyle::Multiphase,
|
|
hierarchy,
|
|
},
|
|
];
|
|
|
|
let merged = coalesce_layout_animation_candidates(candidates);
|
|
|
|
assert_eq!(merged.len(), 2);
|
|
assert_eq!(merged[0].node_id, NodeId(1));
|
|
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
|
|
assert_eq!(merged[0].new, rect(0, 0, 100, 100));
|
|
assert_eq!(merged[0].style, AnimationStyle::Plain);
|
|
assert_eq!(merged[1].node_id, NodeId(2));
|
|
}
|
|
}
|
|
|