use std::collections::HashMap;
use crate::types::WidgetId;
use super::{Decay, Easing, InterruptionStrategy, Spring};
use super::types::{ActiveAnimation, AnimationKey};
pub struct AnimationCoordinator {
active: HashMap<AnimationKey, ActiveAnimation>,
default_interruption: InterruptionStrategy,
}
impl AnimationCoordinator {
pub fn new() -> Self {
Self {
active: HashMap::new(),
default_interruption: InterruptionStrategy::default(),
}
}
pub fn update(&mut self, time_secs: f64) -> bool {
for anim in self.active.values_mut() {
anim.update(time_secs);
}
self.cleanup_completed();
!self.active.is_empty()
}
pub fn get(&self, widget_id: &WidgetId, property: &str) -> Option<f64> {
let key = AnimationKey::new(widget_id.clone(), property);
self.active.get(&key).map(|anim| anim.current_value)
}
pub fn get_or(&self, widget_id: &WidgetId, property: &str, default: f64) -> f64 {
self.get(widget_id, property).unwrap_or(default)
}
#[allow(clippy::too_many_arguments)]
pub fn tween(
&mut self,
widget_id: WidgetId,
property: impl Into<String>,
from: f64,
to: f64,
duration_secs: f64,
easing: Easing,
time_secs: f64,
) {
let key = AnimationKey::new(widget_id, property);
let anim = ActiveAnimation::tween(from, to, time_secs, duration_secs, easing);
self.insert_animation(key, anim);
}
pub fn spring(
&mut self,
widget_id: WidgetId,
property: impl Into<String>,
spring: Spring,
target: f64,
time_secs: f64,
) {
let key = AnimationKey::new(widget_id, property);
let anim = ActiveAnimation::spring(spring, time_secs, target);
self.insert_animation(key, anim);
}
pub fn decay(
&mut self,
widget_id: WidgetId,
property: impl Into<String>,
decay: Decay,
initial_value: f64,
time_secs: f64,
) {
let key = AnimationKey::new(widget_id, property);
let anim = ActiveAnimation::decay(decay, time_secs, initial_value);
self.insert_animation(key, anim);
}
pub fn cancel_widget(&mut self, widget_id: &WidgetId) {
self.active
.retain(|key, _| &key.widget_id != widget_id);
}
pub fn cancel(&mut self, widget_id: &WidgetId, property: &str) {
let key = AnimationKey::new(widget_id.clone(), property);
self.active.remove(&key);
}
pub fn has_active(&self) -> bool {
!self.active.is_empty()
}
pub fn is_animating(&self, widget_id: &WidgetId) -> bool {
self.active.keys().any(|key| &key.widget_id == widget_id)
}
pub fn set_interruption_strategy(&mut self, strategy: InterruptionStrategy) {
self.default_interruption = strategy;
}
pub fn active_count(&self) -> usize {
self.active.len()
}
fn insert_animation(&mut self, key: AnimationKey, anim: ActiveAnimation) {
self.active.insert(key, anim);
}
fn cleanup_completed(&mut self) {
self.active.retain(|_, anim| !anim.completed);
}
}
impl Default for AnimationCoordinator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tween_lifecycle() {
let mut coord = AnimationCoordinator::new();
let widget_id = WidgetId::new("widget1");
coord.tween(
widget_id.clone(),
"opacity",
0.0,
1.0,
1.0,
Easing::Linear,
0.0,
);
assert!(coord.has_active());
assert!(coord.is_animating(&widget_id));
let val = coord.get(&widget_id, "opacity").unwrap();
assert!((val - 0.0).abs() < 0.001);
coord.update(0.5);
let val = coord.get(&widget_id, "opacity").unwrap();
assert!((val - 0.5).abs() < 0.001);
coord.update(0.99);
let val = coord.get(&widget_id, "opacity").unwrap();
assert!((val - 0.99).abs() < 0.01);
assert!(coord.has_active());
coord.update(1.0);
assert!(!coord.has_active());
assert_eq!(coord.get(&widget_id, "opacity"), None);
}
#[test]
fn test_spring_animation() {
let mut coord = AnimationCoordinator::new();
let widget_id = WidgetId::new("widget1");
let spring = Spring::stiff();
coord.spring(widget_id.clone(), "x", spring, 100.0, 0.0);
assert!(coord.has_active());
coord.update(0.0);
let val = coord.get(&widget_id, "x").unwrap();
assert!((val - 99.0).abs() < 0.1);
coord.update(0.5);
let val = coord.get(&widget_id, "x").unwrap();
assert!(val > 99.0 && val < 101.0);
coord.update(5.0);
assert!(!coord.has_active()); }
#[test]
fn test_decay_animation() {
let mut coord = AnimationCoordinator::new();
let widget_id = WidgetId::new("widget1");
let decay = Decay::new(500.0).friction(0.95);
coord.decay(widget_id.clone(), "scroll_y", decay, 0.0, 0.0);
assert!(coord.has_active());
coord.update(0.0);
let val = coord.get(&widget_id, "scroll_y").unwrap();
assert!((val - 0.0).abs() < 0.1);
coord.update(0.1);
let val = coord.get(&widget_id, "scroll_y").unwrap();
assert!(val > 0.0);
coord.update(10.0);
assert!(!coord.has_active());
}
#[test]
fn test_multiple_widgets_simultaneously() {
let mut coord = AnimationCoordinator::new();
let widget1 = WidgetId::new("widget1");
let widget2 = WidgetId::new("widget2");
coord.tween(widget1.clone(), "x", 0.0, 100.0, 1.0, Easing::Linear, 0.0);
coord.tween(widget2.clone(), "y", 0.0, 200.0, 1.0, Easing::Linear, 0.0);
assert_eq!(coord.active_count(), 2);
coord.update(0.5);
let val1 = coord.get(&widget1, "x").unwrap();
let val2 = coord.get(&widget2, "y").unwrap();
assert!((val1 - 50.0).abs() < 0.1);
assert!((val2 - 100.0).abs() < 0.1);
}
#[test]
fn test_cancel_widget() {
let mut coord = AnimationCoordinator::new();
let widget_id = WidgetId::new("widget1");
coord.tween(
widget_id.clone(),
"x",
0.0,
100.0,
1.0,
Easing::Linear,
0.0,
);
coord.tween(
widget_id.clone(),
"y",
0.0,
100.0,
1.0,
Easing::Linear,
0.0,
);
assert_eq!(coord.active_count(), 2);
coord.cancel_widget(&widget_id);
assert_eq!(coord.active_count(), 0);
}
#[test]
fn test_cancel_property() {
let mut coord = AnimationCoordinator::new();
let widget_id = WidgetId::new("widget1");
coord.tween(
widget_id.clone(),
"x",
0.0,
100.0,
1.0,
Easing::Linear,
0.0,
);
coord.tween(
widget_id.clone(),
"y",
0.0,
100.0,
1.0,
Easing::Linear,
0.0,
);
assert_eq!(coord.active_count(), 2);
coord.cancel(&widget_id, "x");
assert_eq!(coord.active_count(), 1);
assert_eq!(coord.get(&widget_id, "x"), None);
assert!(coord.get(&widget_id, "y").is_some());
}
#[test]
fn test_interruption_replaces_old() {
let mut coord = AnimationCoordinator::new();
let widget_id = WidgetId::new("widget1");
coord.tween(
widget_id.clone(),
"x",
0.0,
100.0,
1.0,
Easing::Linear,
0.0,
);
coord.update(0.5);
let val = coord.get(&widget_id, "x").unwrap();
assert!((val - 50.0).abs() < 0.1);
coord.tween(
widget_id.clone(),
"x",
50.0,
200.0,
1.0,
Easing::Linear,
0.5,
);
coord.update(0.5);
let val = coord.get(&widget_id, "x").unwrap();
assert!((val - 50.0).abs() < 0.1); }
#[test]
fn test_get_or_with_default() {
let coord = AnimationCoordinator::new();
let widget_id = WidgetId::new("widget1");
let val = coord.get_or(&widget_id, "opacity", 1.0);
assert_eq!(val, 1.0);
}
#[test]
fn test_has_active_is_animating() {
let mut coord = AnimationCoordinator::new();
let widget1 = WidgetId::new("widget1");
let widget2 = WidgetId::new("widget2");
assert!(!coord.has_active());
assert!(!coord.is_animating(&widget1));
coord.tween(widget1.clone(), "x", 0.0, 100.0, 1.0, Easing::Linear, 0.0);
assert!(coord.has_active());
assert!(coord.is_animating(&widget1));
assert!(!coord.is_animating(&widget2));
}
#[test]
fn test_easing_functions() {
let mut coord = AnimationCoordinator::new();
let widget_id = WidgetId::new("widget1");
coord.tween(
widget_id.clone(),
"x",
0.0,
100.0,
1.0,
Easing::EaseInQuad,
0.0,
);
coord.update(0.5);
let val = coord.get(&widget_id, "x").unwrap();
assert!((val - 25.0).abs() < 0.1);
}
}