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, pub duration_ms: Cell, pub curve: Cell, pub style: Cell, windows: RefCell>, phased: RefCell>, exits: RefCell>, tick: CloneCell>>, } 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, pub above: Vec, } pub enum RetainedContent { Texture { texture: Rc, buffer: Rc, source: SampleRect, alpha: Option, color_description: Rc, render_intent: RenderIntent, alpha_mode: crate::gfx_api::AlphaMode, opaque: bool, }, Color { color: Color, alpha: Option, color_description: Rc, render_intent: RenderIntent, }, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum RetainedExitLayer { Tiled, Floating, } pub struct RetainedExitFrame { pub rect: Rect, pub retained: Rc, 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> { Some(Rc::new(Self { offset, surface: RetainedSurface::capture(surface, (0, 0))?, })) } } impl RetainedSurface { fn capture(surface: &WlSurface, offset: (i32, i32)) -> Option { 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>, 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>, 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>, 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, 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> { 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> { 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 { 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) { 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>, } 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, start_nsec: u64, duration_nsec: u64, curve: AnimationCurve, last_damage: Rect, final_rect: Rect, route_edges: Vec<(Rect, Rect)>, retained: Option>, } 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 { 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> { 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> { 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> { let mut nodes = vec![]; let mut adjacency: Vec> = 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, adjacency: &mut Vec>, 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, 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, slf: Weak, latch_listeners: RefCell>>, } impl AnimationTick { pub fn new(state: &Rc, slf: &Weak) -> Self { let slf: Weak = 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, _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 { 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) ); } }