1
0
Fork 0
forked from wry/wry
wry/src/animation.rs

1076 lines
33 KiB
Rust

use {
crate::{
cmm::{cmm_description::ColorDescription, cmm_render_intent::RenderIntent},
gfx_api::{GfxTexture, SampleRect},
ifs::wl_surface::{SurfaceBuffer, WlSurface},
rect::Rect,
state::State,
theme::Color,
tree::{LatchListener, NodeId, OutputNode},
utils::{clonecell::CloneCell, event_listener::EventListener},
},
ahash::AHashMap,
std::{
cell::{Cell, RefCell},
collections::VecDeque,
rc::{Rc, Weak},
},
};
pub mod multiphase;
pub use jay_layout_animation::{AnimationCurve, AnimationStyle};
const DEFAULT_DURATION_MS: u32 = 160;
pub struct AnimationState {
pub enabled: Cell<bool>,
pub duration_ms: Cell<u32>,
pub curve: Cell<AnimationCurve>,
pub style: Cell<AnimationStyle>,
windows: RefCell<AHashMap<NodeId, WindowAnimation>>,
phased: RefCell<AHashMap<NodeId, PhasedWindowAnimation>>,
exits: RefCell<Vec<ExitAnimation>>,
tick: CloneCell<Option<Rc<AnimationTick>>>,
}
pub struct RetainedToplevel {
pub offset: (i32, i32),
pub surface: RetainedSurface,
}
pub struct RetainedSurface {
pub offset: (i32, i32),
pub size: (i32, i32),
pub content: RetainedContent,
pub below: Vec<RetainedSurface>,
pub above: Vec<RetainedSurface>,
}
pub enum RetainedContent {
Texture {
texture: Rc<dyn GfxTexture>,
buffer: Rc<SurfaceBuffer>,
source: SampleRect,
alpha: Option<f32>,
color_description: Rc<ColorDescription>,
render_intent: RenderIntent,
alpha_mode: crate::gfx_api::AlphaMode,
opaque: bool,
},
Color {
color: Color,
alpha: Option<f32>,
color_description: Rc<ColorDescription>,
render_intent: RenderIntent,
},
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RetainedExitLayer {
Tiled,
Floating,
}
pub struct RetainedExitFrame {
pub rect: Rect,
pub retained: Rc<RetainedToplevel>,
pub frame_inset: i32,
pub source_body_size: (i32, i32),
pub active: bool,
pub layer: RetainedExitLayer,
}
impl RetainedToplevel {
pub fn capture_surface(surface: &WlSurface, offset: (i32, i32)) -> Option<Rc<Self>> {
Some(Rc::new(Self {
offset,
surface: RetainedSurface::capture(surface, (0, 0))?,
}))
}
}
impl RetainedSurface {
fn capture(surface: &WlSurface, offset: (i32, i32)) -> Option<Self> {
let buffer = surface.buffer.get()?;
buffer.buffer.buf.update_texture_or_log(surface, true);
let size = surface.buffer_abs_pos.get().size();
let source = *surface.buffer_points_norm.borrow();
let color_description = surface.color_description();
let render_intent = surface.render_intent();
let alpha_mode = surface.alpha_mode();
let alpha = surface.alpha();
let content = match buffer.buffer.buf.get_texture(surface) {
Some(texture) => RetainedContent::Texture {
opaque: surface.opaque(),
texture,
buffer,
source,
alpha,
color_description,
render_intent,
alpha_mode,
},
None => {
let color = buffer.buffer.buf.color?;
RetainedContent::Color {
color: Color::from_u32(
color_description.eotf,
alpha_mode,
color[0],
color[1],
color[2],
color[3],
),
alpha,
color_description,
render_intent,
}
}
};
let mut below = vec![];
let mut above = vec![];
if let Some(children) = surface.children.borrow().as_deref() {
for child in children.below.iter() {
if child.pending.get() {
continue;
}
let pos = child.sub_surface.position.get();
if let Some(surface) = Self::capture(&child.sub_surface.surface, pos) {
below.push(surface);
}
}
for child in children.above.iter() {
if child.pending.get() {
continue;
}
let pos = child.sub_surface.position.get();
if let Some(surface) = Self::capture(&child.sub_surface.surface, pos) {
above.push(surface);
}
}
}
Some(Self {
offset,
size,
content,
below,
above,
})
}
}
impl Default for AnimationState {
fn default() -> Self {
Self {
enabled: Cell::new(false),
duration_ms: Cell::new(DEFAULT_DURATION_MS),
curve: Cell::new(AnimationCurve::from_config(3)),
style: Cell::new(AnimationStyle::Multiphase),
windows: Default::default(),
phased: Default::default(),
exits: Default::default(),
tick: Default::default(),
}
}
}
impl AnimationState {
pub fn clear(&self) {
self.windows.borrow_mut().clear();
self.phased.borrow_mut().clear();
self.exits.borrow_mut().clear();
if let Some(tick) = self.tick.take() {
tick.detach();
}
}
pub fn set_target(
&self,
node_id: NodeId,
old: Rect,
new: Rect,
_retained: Option<Rc<RetainedToplevel>>,
now_nsec: u64,
duration_ms: u32,
curve: AnimationCurve,
) -> bool {
if old == new || new.is_empty() || duration_ms == 0 {
self.windows.borrow_mut().remove(&node_id);
self.phased.borrow_mut().remove(&node_id);
return false;
}
let duration_nsec = duration_ms as u64 * 1_000_000;
let mut from = old;
{
let phased = self.phased.borrow();
if let Some(anim) = phased.get(&node_id) {
if anim.final_rect == new {
return false;
}
from = anim.rect_at(now_nsec);
}
}
{
let windows = self.windows.borrow();
if let Some(anim) = windows.get(&node_id) {
if anim.to == new {
return false;
}
from = anim.rect_at(now_nsec);
}
}
if from == new {
self.windows.borrow_mut().remove(&node_id);
self.phased.borrow_mut().remove(&node_id);
return false;
}
self.phased.borrow_mut().remove(&node_id);
self.windows.borrow_mut().insert(
node_id,
WindowAnimation {
from,
to: new,
start_nsec: now_nsec,
duration_nsec,
curve,
last_damage: from,
retained: None,
},
);
true
}
pub fn set_phased_target(
&self,
node_id: NodeId,
phases: Vec<(Rect, Rect)>,
_retained: Option<Rc<RetainedToplevel>>,
now_nsec: u64,
duration_ms: u32,
curve: AnimationCurve,
) -> bool {
if phases.is_empty() || duration_ms == 0 {
return false;
}
let Some((from, _)) = phases.first().copied() else {
return false;
};
let Some((_, final_rect)) = phases.last().copied() else {
return false;
};
if from.is_empty() || final_rect.is_empty() || from == final_rect {
return false;
}
let segments: Vec<_> = phases
.into_iter()
.map(|(from, to)| PhasedSegment { from, to })
.collect();
let mut route_edges = route_edges_from_segments(&segments);
if let Some(anim) = self.phased.borrow().get(&node_id)
&& !anim.done(now_nsec)
{
for &(from, to) in &anim.route_edges {
push_unique_route_edge(&mut route_edges, from, to);
}
}
self.windows.borrow_mut().remove(&node_id);
self.phased.borrow_mut().insert(
node_id,
PhasedWindowAnimation {
segments,
start_nsec: now_nsec,
duration_nsec: duration_ms as u64 * 1_000_000,
curve,
last_damage: from,
final_rect,
route_edges,
retained: None,
},
);
true
}
pub fn set_spawn_in(
&self,
node_id: NodeId,
target: Rect,
retained: Option<Rc<RetainedToplevel>>,
now_nsec: u64,
duration_ms: u32,
curve: AnimationCurve,
) -> bool {
let start = spawn_in_start_rect(target);
self.set_target(
node_id,
start,
target,
retained,
now_nsec,
duration_ms,
curve,
)
}
pub fn set_spawn_out(
&self,
from: Rect,
frame_inset: i32,
retained: Rc<RetainedToplevel>,
active: bool,
layer: RetainedExitLayer,
now_nsec: u64,
duration_ms: u32,
curve: AnimationCurve,
) -> bool {
if from.is_empty() || duration_ms == 0 {
return false;
}
let to = spawn_in_start_rect(from);
if to == from {
return false;
}
let source_body_size = body_size_for_frame(from, frame_inset);
if source_body_size.0 <= 0 || source_body_size.1 <= 0 {
return false;
}
self.exits.borrow_mut().push(ExitAnimation {
from,
to,
start_nsec: now_nsec,
duration_nsec: duration_ms as u64 * 1_000_000,
curve,
last_damage: from,
retained,
frame_inset,
source_body_size,
active,
layer,
});
true
}
pub fn visual_rect(&self, node_id: NodeId, layout: Rect, now_nsec: u64) -> Rect {
let phased = self.phased.borrow();
if let Some(anim) = phased.get(&node_id)
&& !anim.done(now_nsec)
{
return anim.rect_at(now_nsec);
}
drop(phased);
let windows = self.windows.borrow();
match windows.get(&node_id) {
Some(anim) if !anim.done(now_nsec) => anim.rect_at(now_nsec),
_ => layout,
}
}
pub fn retained_snapshot(
&self,
node_id: NodeId,
now_nsec: u64,
) -> Option<Rc<RetainedToplevel>> {
let phased = self.phased.borrow();
if let Some(anim) = phased.get(&node_id)
&& !anim.done(now_nsec)
{
return anim.retained.clone();
}
drop(phased);
let windows = self.windows.borrow();
match windows.get(&node_id) {
Some(anim) if !anim.done(now_nsec) => anim.retained.clone(),
_ => None,
}
}
pub fn phased_route_to(
&self,
node_id: NodeId,
target: Rect,
now_nsec: u64,
) -> Option<Vec<(Rect, Rect)>> {
let phased = self.phased.borrow();
let anim = phased.get(&node_id)?;
if anim.done(now_nsec) {
return None;
}
anim.route_to(target, now_nsec)
}
pub fn exit_frames(&self, now_nsec: u64) -> Vec<RetainedExitFrame> {
self.exits
.borrow()
.iter()
.filter(|exit| !exit.done(now_nsec))
.map(|exit| RetainedExitFrame {
rect: exit.rect_at(now_nsec),
retained: exit.retained.clone(),
frame_inset: exit.frame_inset,
source_body_size: exit.source_body_size,
active: exit.active,
layer: exit.layer,
})
.collect()
}
fn damage_active(&self, state: &State, now_nsec: u64) -> bool {
let mut damages = vec![];
let mut any_active = false;
{
let mut windows = self.windows.borrow_mut();
windows.retain(|_, anim| {
let current = anim.rect_at(now_nsec);
let damage = anim.last_damage.union(current).union(anim.to);
damages.push(expand_damage_rect(
damage,
state.theme.sizes.border_width.get().max(0),
));
anim.last_damage = current;
let active = !anim.done(now_nsec);
any_active |= active;
active
});
self.phased.borrow_mut().retain(|_, anim| {
let current = anim.rect_at(now_nsec);
let damage = anim.last_damage.union(current).union(anim.final_rect);
damages.push(expand_damage_rect(
damage,
state.theme.sizes.border_width.get().max(0),
));
anim.last_damage = current;
let active = !anim.done(now_nsec);
any_active |= active;
active
});
self.exits.borrow_mut().retain_mut(|exit| {
let current = exit.rect_at(now_nsec);
let damage = exit.last_damage.union(current).union(exit.to);
damages.push(expand_damage_rect(
damage,
state.theme.sizes.border_width.get().max(0),
));
exit.last_damage = current;
let active = !exit.done(now_nsec);
any_active |= active;
active
});
}
for damage in damages {
state.damage(damage);
}
any_active
}
pub(crate) fn tick_is_active(&self) -> bool {
self.tick.is_some()
}
pub(crate) fn set_tick(&self, tick: Rc<AnimationTick>) {
self.tick.set(Some(tick));
}
pub(crate) fn clear_tick(&self) {
self.tick.take();
}
}
struct WindowAnimation {
from: Rect,
to: Rect,
start_nsec: u64,
duration_nsec: u64,
curve: AnimationCurve,
last_damage: Rect,
retained: Option<Rc<RetainedToplevel>>,
}
impl WindowAnimation {
fn done(&self, now_nsec: u64) -> bool {
now_nsec.saturating_sub(self.start_nsec) >= self.duration_nsec
}
fn rect_at(&self, now_nsec: u64) -> Rect {
if self.duration_nsec == 0 {
return self.to;
}
let elapsed = now_nsec.saturating_sub(self.start_nsec);
let t = (elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
let t = self.curve.sample(t);
lerp_rect(self.from, self.to, t)
}
}
struct PhasedWindowAnimation {
segments: Vec<PhasedSegment>,
start_nsec: u64,
duration_nsec: u64,
curve: AnimationCurve,
last_damage: Rect,
final_rect: Rect,
route_edges: Vec<(Rect, Rect)>,
retained: Option<Rc<RetainedToplevel>>,
}
struct PhasedSegment {
from: Rect,
to: Rect,
}
impl PhasedWindowAnimation {
fn done(&self, now_nsec: u64) -> bool {
let total_duration = self
.duration_nsec
.saturating_mul(self.segments.len() as u64);
now_nsec.saturating_sub(self.start_nsec) >= total_duration
}
fn rect_at(&self, now_nsec: u64) -> Rect {
if self.duration_nsec == 0 {
return self.final_rect;
}
let elapsed = now_nsec.saturating_sub(self.start_nsec);
let phase = (elapsed / self.duration_nsec) as usize;
let Some(segment) = self.segments.get(phase) else {
return self.final_rect;
};
let phase_elapsed = elapsed % self.duration_nsec;
let t = (phase_elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
let t = self.curve.sample(t);
lerp_rect(segment.from, segment.to, t)
}
fn phase_at(&self, now_nsec: u64) -> Option<usize> {
if self.duration_nsec == 0 || self.segments.is_empty() {
return None;
}
let elapsed = now_nsec.saturating_sub(self.start_nsec);
let phase = (elapsed / self.duration_nsec) as usize;
(phase < self.segments.len()).then_some(phase)
}
fn route_to(&self, target: Rect, now_nsec: u64) -> Option<Vec<(Rect, Rect)>> {
let phase = self.phase_at(now_nsec)?;
let current = self.rect_at(now_nsec);
if current == target {
return Some(vec![]);
}
let segment = self.segments.get(phase)?;
route_through_edges(current, target, segment.from, segment.to, &self.route_edges)
}
}
fn route_edges_from_segments(segments: &[PhasedSegment]) -> Vec<(Rect, Rect)> {
let mut edges = vec![];
for segment in segments {
push_unique_route_edge(&mut edges, segment.from, segment.to);
}
edges
}
fn push_unique_route_edge(edges: &mut Vec<(Rect, Rect)>, from: Rect, to: Rect) {
if from == to {
return;
}
if edges
.iter()
.any(|&(a, b)| (a == from && b == to) || (a == to && b == from))
{
return;
}
edges.push((from, to));
}
fn route_through_edges(
current: Rect,
target: Rect,
current_from: Rect,
current_to: Rect,
known_edges: &[(Rect, Rect)],
) -> Option<Vec<(Rect, Rect)>> {
let mut edges = known_edges.to_vec();
push_unique_route_edge(&mut edges, current, current_from);
push_unique_route_edge(&mut edges, current, current_to);
rect_graph_route(current, target, &edges)
}
fn rect_graph_route(
start: Rect,
target: Rect,
edges: &[(Rect, Rect)],
) -> Option<Vec<(Rect, Rect)>> {
let mut nodes = vec![];
let mut adjacency: Vec<Vec<usize>> = vec![];
let start_idx = rect_graph_node(&mut nodes, &mut adjacency, start);
let target_idx = rect_graph_node(&mut nodes, &mut adjacency, target);
for &(from, to) in edges {
let from_idx = rect_graph_node(&mut nodes, &mut adjacency, from);
let to_idx = rect_graph_node(&mut nodes, &mut adjacency, to);
if !adjacency[from_idx].contains(&to_idx) {
adjacency[from_idx].push(to_idx);
}
if !adjacency[to_idx].contains(&from_idx) {
adjacency[to_idx].push(from_idx);
}
}
let mut previous = vec![None; nodes.len()];
let mut queue = VecDeque::from([start_idx]);
previous[start_idx] = Some(start_idx);
while let Some(idx) = queue.pop_front() {
if idx == target_idx {
break;
}
for &next in &adjacency[idx] {
if previous[next].is_none() {
previous[next] = Some(idx);
queue.push_back(next);
}
}
}
previous[target_idx]?;
let mut reversed_nodes = vec![target_idx];
let mut idx = target_idx;
while idx != start_idx {
idx = previous[idx]?;
reversed_nodes.push(idx);
}
reversed_nodes.reverse();
let mut route = vec![];
for pair in reversed_nodes.windows(2) {
push_non_empty_segment(&mut route, nodes[pair[0]], nodes[pair[1]]);
}
Some(route)
}
fn rect_graph_node(nodes: &mut Vec<Rect>, adjacency: &mut Vec<Vec<usize>>, rect: Rect) -> usize {
if let Some(idx) = nodes.iter().position(|&node| node == rect) {
return idx;
}
let idx = nodes.len();
nodes.push(rect);
adjacency.push(vec![]);
idx
}
fn push_non_empty_segment(route: &mut Vec<(Rect, Rect)>, from: Rect, to: Rect) {
if from != to {
route.push((from, to));
}
}
struct ExitAnimation {
from: Rect,
to: Rect,
start_nsec: u64,
duration_nsec: u64,
curve: AnimationCurve,
last_damage: Rect,
retained: Rc<RetainedToplevel>,
frame_inset: i32,
source_body_size: (i32, i32),
active: bool,
layer: RetainedExitLayer,
}
impl ExitAnimation {
fn done(&self, now_nsec: u64) -> bool {
now_nsec.saturating_sub(self.start_nsec) >= self.duration_nsec
}
fn rect_at(&self, now_nsec: u64) -> Rect {
if self.duration_nsec == 0 {
return self.to;
}
let elapsed = now_nsec.saturating_sub(self.start_nsec);
let t = (elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
let t = self.curve.sample(t);
lerp_rect(self.from, self.to, t)
}
}
pub struct AnimationTick {
state: Weak<State>,
slf: Weak<dyn LatchListener>,
latch_listeners: RefCell<Vec<EventListener<dyn LatchListener>>>,
}
impl AnimationTick {
pub fn new(state: &Rc<State>, slf: &Weak<Self>) -> Self {
let slf: Weak<dyn LatchListener> = slf.clone();
Self {
state: Rc::downgrade(state),
slf,
latch_listeners: Default::default(),
}
}
pub fn attach(&self, output: &OutputNode) {
let listener = EventListener::new(self.slf.clone());
listener.attach(&output.latch_event);
self.latch_listeners.borrow_mut().push(listener);
}
pub fn detach(&self) {
for listener in self.latch_listeners.borrow_mut().drain(..) {
listener.detach();
}
}
}
impl LatchListener for AnimationTick {
fn after_latch(self: Rc<Self>, _on: &OutputNode, _tearing: bool) {
let Some(state) = self.state.upgrade() else {
self.detach();
return;
};
let active = state.animations.damage_active(&state, state.now_nsec());
if !active {
self.detach();
state.animations.clear_tick();
}
}
}
pub(crate) fn spawn_in_start_rect(target: Rect) -> Rect {
let (cx, cy) = target.center();
Rect::new_empty(cx, cy)
}
fn body_size_for_frame(rect: Rect, frame_inset: i32) -> (i32, i32) {
(
rect.width().saturating_sub(2 * frame_inset),
rect.height().saturating_sub(2 * frame_inset),
)
}
fn lerp_rect(from: Rect, to: Rect, t: f64) -> Rect {
fn lerp(from: i32, to: i32, t: f64) -> i32 {
(from as f64 + (to as f64 - from as f64) * t).round() as i32
}
Rect::new_saturating(
lerp(from.x1(), to.x1(), t),
lerp(from.y1(), to.y1(), t),
lerp(from.x2(), to.x2(), t),
lerp(from.y2(), to.y2(), t),
)
}
pub(crate) fn expand_damage_rect(rect: Rect, width: i32) -> Rect {
Rect::new_saturating(
rect.x1().saturating_sub(width),
rect.y1().saturating_sub(width),
rect.x2().saturating_add(width),
rect.y2().saturating_add(width),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cmm::cmm_manager::ColorManager;
fn retained_for_tests() -> Rc<RetainedToplevel> {
let color_manager = ColorManager::new();
Rc::new(RetainedToplevel {
offset: (0, 0),
surface: RetainedSurface {
offset: (0, 0),
size: (100, 100),
content: RetainedContent::Color {
color: Color::SOLID_BLACK,
alpha: None,
color_description: color_manager.srgb_gamma22().clone(),
render_intent: RenderIntent::Perceptual,
},
below: vec![],
above: vec![],
},
})
}
#[test]
fn linear_rect_interpolation_is_symmetric() {
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(100, 40, 200, 80);
assert_eq!(lerp_rect(a, b, 0.25), lerp_rect(b, a, 0.75));
}
#[test]
fn custom_cubic_bezier_curve_is_prepared() {
let curve = AnimationCurve::from_cubic_bezier(0.0, 0.0, 1.0, 1.0).unwrap();
assert_eq!(curve.sample(0.0), 0.0);
assert_eq!(curve.sample(1.0), 1.0);
assert!((curve.sample(0.5) - 0.5).abs() < 0.001);
let ease_out = AnimationCurve::from_cubic_bezier(0.0, 0.0, 0.58, 1.0).unwrap();
let mid = ease_out.sample(0.5);
assert!(mid > 0.5);
assert!(mid < 1.0);
}
#[test]
fn invalid_custom_cubic_bezier_curve_is_rejected() {
assert!(AnimationCurve::from_cubic_bezier(-0.1, 0.0, 0.58, 1.0).is_none());
assert!(AnimationCurve::from_cubic_bezier(0.0, 0.0, 1.1, 1.0).is_none());
assert!(AnimationCurve::from_cubic_bezier(0.0, f32::NAN, 0.58, 1.0).is_none());
}
#[test]
fn spawn_out_frames_use_configured_curve_and_expire() {
let state = AnimationState::default();
let retained = retained_for_tests();
let from = Rect::new_sized_saturating(10, 20, 100, 80);
let to = spawn_in_start_rect(from);
let curve = AnimationCurve::from_config(3);
assert!(state.set_spawn_out(
from,
2,
retained.clone(),
true,
RetainedExitLayer::Floating,
0,
160,
curve
));
let start = state.exit_frames(0);
assert_eq!(start.len(), 1);
assert_eq!(start[0].rect, from);
assert_eq!(start[0].source_body_size, (96, 76));
assert!(start[0].active);
assert_eq!(start[0].layer, RetainedExitLayer::Floating);
assert!(Rc::ptr_eq(&start[0].retained, &retained));
let middle = state.exit_frames(80_000_000);
assert_eq!(middle.len(), 1);
assert_eq!(middle[0].rect, lerp_rect(from, to, curve.sample(0.5)));
assert_ne!(middle[0].rect, lerp_rect(from, to, 0.5));
assert!(state.exit_frames(160_000_000).is_empty());
}
#[test]
fn normal_window_animations_do_not_retain_content() {
let state = AnimationState::default();
let id = NodeId(1);
let from = Rect::new_sized_saturating(0, 0, 100, 100);
let to = Rect::new_sized_saturating(100, 0, 100, 100);
assert!(state.set_target(
id,
from,
to,
Some(retained_for_tests()),
0,
160,
AnimationCurve::Linear
));
assert!(state.retained_snapshot(id, 80_000_000).is_none());
}
#[test]
fn phased_window_animations_do_not_retain_content() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(0, 0, 100, 50);
let c = Rect::new_sized_saturating(100, 0, 100, 50);
assert!(state.set_phased_target(
id,
vec![(a, b), (b, c)],
Some(retained_for_tests()),
0,
100,
AnimationCurve::Linear
));
assert!(state.retained_snapshot(id, 50_000_000).is_none());
}
#[test]
fn phased_animation_uses_full_duration_per_phase() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(0, 0, 100, 50);
let c = Rect::new_sized_saturating(100, 0, 100, 50);
assert!(state.set_phased_target(
id,
vec![(a, b), (b, c)],
None,
0,
100,
AnimationCurve::Linear
));
assert_eq!(state.visual_rect(id, c, 0), a);
assert_eq!(state.visual_rect(id, c, 50_000_000), lerp_rect(a, b, 0.5));
assert_eq!(state.visual_rect(id, c, 100_000_000), b);
assert_eq!(state.visual_rect(id, c, 150_000_000), lerp_rect(b, c, 0.5));
assert_eq!(state.visual_rect(id, c, 200_000_000), c);
}
#[test]
fn phased_route_reverses_to_existing_endpoint() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(0, 0, 100, 50);
let c = Rect::new_sized_saturating(100, 0, 100, 50);
assert!(state.set_phased_target(
id,
vec![(a, b), (b, c)],
None,
0,
100,
AnimationCurve::Linear
));
let current = lerp_rect(b, c, 0.5);
assert_eq!(
state.phased_route_to(id, a, 150_000_000).unwrap(),
vec![(current, b), (b, a)]
);
}
#[test]
fn phased_route_continues_to_existing_endpoint() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(0, 0, 100, 50);
let c = Rect::new_sized_saturating(100, 0, 100, 50);
assert!(state.set_phased_target(
id,
vec![(a, b), (b, c)],
None,
0,
100,
AnimationCurve::Linear
));
let current = lerp_rect(a, b, 0.5);
assert_eq!(
state.phased_route_to(id, c, 50_000_000).unwrap(),
vec![(current, b), (b, c)]
);
}
#[test]
fn phased_route_remembers_original_path_after_retarget() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(0, 0, 100, 50);
let c = Rect::new_sized_saturating(100, 0, 100, 50);
assert!(state.set_phased_target(
id,
vec![(a, b), (b, c)],
None,
0,
100,
AnimationCurve::Linear
));
let current = lerp_rect(b, c, 0.5);
let reverse = state.phased_route_to(id, a, 150_000_000).unwrap();
assert_eq!(reverse, vec![(current, b), (b, a)]);
assert!(state.set_phased_target(
id,
reverse,
None,
150_000_000,
100,
AnimationCurve::Linear
));
let current = lerp_rect(current, b, 0.5);
assert_eq!(
state.phased_route_to(id, c, 200_000_000).unwrap(),
vec![(current, b), (b, c)]
);
}
#[test]
fn linear_retarget_interrupts_phased_animation_from_current_rect() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(0, 0, 100, 50);
let c = Rect::new_sized_saturating(100, 0, 100, 50);
let d = Rect::new_sized_saturating(100, 100, 100, 50);
assert!(state.set_phased_target(
id,
vec![(a, b), (b, c)],
None,
0,
100,
AnimationCurve::Linear
));
let current = lerp_rect(a, b, 0.5);
assert!(state.set_target(id, a, d, None, 50_000_000, 100, AnimationCurve::Linear));
assert_eq!(state.visual_rect(id, d, 50_000_000), current);
assert_eq!(
state.visual_rect(id, d, 100_000_000),
lerp_rect(current, d, 0.5)
);
}
#[test]
fn unchanged_target_does_not_restart() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(100, 0, 100, 100);
assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear));
assert!(!state.set_target(id, a, b, None, 80_000_000, 160, AnimationCurve::Linear));
assert_eq!(
state.visual_rect(id, b, 80_000_000),
Rect::new_sized_saturating(50, 0, 100, 100)
);
}
#[test]
fn changed_target_restarts_from_current_visual_rect() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(100, 0, 100, 100);
let c = Rect::new_sized_saturating(200, 0, 100, 100);
assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear));
assert!(state.set_target(id, a, c, None, 80_000_000, 160, AnimationCurve::Linear));
assert_eq!(
state.visual_rect(id, c, 80_000_000),
Rect::new_sized_saturating(50, 0, 100, 100)
);
assert_eq!(
state.visual_rect(id, c, 160_000_000),
Rect::new_sized_saturating(125, 0, 100, 100)
);
}
#[test]
fn spawn_in_start_rect_is_centered_and_empty() {
let target = Rect::new_sized_saturating(10, 20, 100, 50);
assert_eq!(spawn_in_start_rect(target), Rect::new_empty(60, 45));
}
#[test]
fn spawn_in_uses_configured_curve() {
let state = AnimationState::default();
let id = NodeId(1);
let target = Rect::new_sized_saturating(10, 20, 100, 50);
let curve = AnimationCurve::from_config(3);
assert!(state.set_spawn_in(id, target, None, 0, 160, curve));
assert_eq!(
state.visual_rect(id, target, 80_000_000),
lerp_rect(spawn_in_start_rect(target), target, curve.sample(0.5))
);
assert_ne!(
state.visual_rect(id, target, 80_000_000),
Rect::new_sized_saturating(35, 33, 50, 25)
);
}
}