use crate::{
AnimationError, AnimationHandle, AnimationTarget, AnimationValue, Easing, RepeatConfig, Result,
Timeline,
};
use std::collections::HashMap;
use std::sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicUsize, Ordering},
};
use std::time::Instant;
#[derive(Debug)]
pub struct AnimationEngine {
animations: Arc<Mutex<HashMap<u64, ActiveAnimation>>>,
timelines: Arc<Mutex<HashMap<u64, Timeline>>>,
next_id: Arc<AtomicUsize>,
active_count: Arc<AtomicUsize>,
is_stable: Arc<AtomicBool>,
created_at: Instant,
}
#[derive(Debug, Clone)]
pub struct AnimationConfig {
pub id: Option<String>,
pub target: AnimationTarget,
pub duration: Option<f64>,
pub ease: Easing,
pub delay: Option<f64>,
pub repeat: RepeatConfig,
}
impl Default for AnimationConfig {
fn default() -> Self {
Self {
id: None,
target: AnimationTarget::new(),
duration: Some(1.0),
ease: Easing::Linear,
delay: None,
repeat: RepeatConfig::Never,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TDDAnimationHandle(pub u64);
impl TDDAnimationHandle {
pub fn new(id: u64) -> Self {
Self(id)
}
pub fn to_animation_handle(&self) -> AnimationHandle {
AnimationHandle(self.0)
}
pub fn is_active(&self) -> bool {
true
}
pub fn stop(&self) {
}
}
#[derive(Debug, Clone)]
struct ActiveAnimation {
config: AnimationConfig,
handle: TDDAnimationHandle,
start_time: Instant,
state: AnimationState,
}
#[derive(Debug, Clone, PartialEq)]
enum AnimationState {
Running,
Paused,
Completed,
Cancelled,
}
impl AnimationEngine {
pub fn new() -> Self {
Self {
animations: Arc::new(Mutex::new(HashMap::new())),
timelines: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(AtomicUsize::new(1)),
active_count: Arc::new(AtomicUsize::new(0)),
is_stable: Arc::new(AtomicBool::new(true)),
created_at: Instant::now(),
}
}
pub fn start_animation(&self, config: AnimationConfig) -> Result<TDDAnimationHandle> {
self.validate_animation_config(&config)?;
let id = self.next_id.fetch_add(1, Ordering::SeqCst) as u64;
let handle = TDDAnimationHandle::new(id);
let animation = ActiveAnimation {
config,
handle,
start_time: Instant::now(),
state: AnimationState::Running,
};
{
let mut animations = self.animations.lock().map_err(|_| {
AnimationError::MemoryError("Failed to acquire animation lock".to_string())
})?;
animations.insert(id, animation);
}
self.active_count.fetch_add(1, Ordering::SeqCst);
Ok(handle)
}
pub fn active_animations_count(&self) -> usize {
self.active_count.load(Ordering::SeqCst)
}
pub fn is_stable(&self) -> bool {
self.is_stable.load(Ordering::SeqCst)
}
pub fn stop_animation(&self, handle: TDDAnimationHandle) -> Result<()> {
let mut animations = self.animations.lock().map_err(|_| {
AnimationError::MemoryError("Failed to acquire animation lock".to_string())
})?;
if let Some(animation) = animations.get_mut(&handle.0) {
animation.state = AnimationState::Cancelled;
self.active_count.fetch_sub(1, Ordering::SeqCst);
Ok(())
} else {
Err(AnimationError::NotFound {
handle: handle.to_animation_handle(),
})
}
}
pub fn cleanup_completed_animations(&self) -> Result<usize> {
let mut animations = self.animations.lock().map_err(|_| {
AnimationError::MemoryError("Failed to acquire animation lock".to_string())
})?;
let initial_count = animations.len();
let now = Instant::now();
animations.retain(|_, animation| {
let is_completed = match animation.state {
AnimationState::Completed | AnimationState::Cancelled => true,
AnimationState::Running => {
if let Some(duration) = animation.config.duration {
let elapsed = now.duration_since(animation.start_time).as_secs_f64();
elapsed >= duration
} else {
false
}
}
AnimationState::Paused => false,
};
if is_completed {
self.active_count.fetch_sub(1, Ordering::SeqCst);
}
!is_completed
});
let final_count = animations.len();
Ok(initial_count - final_count)
}
pub fn force_cleanup(&self) -> Result<()> {
let mut animations = self.animations.lock().map_err(|_| {
AnimationError::MemoryError("Failed to acquire animation lock".to_string())
})?;
let _count = animations.len();
animations.clear();
self.active_count.store(0, Ordering::SeqCst);
Ok(())
}
pub fn start_timeline(&self, timeline: &Timeline) -> Result<TDDAnimationHandle> {
let id = self.next_id.fetch_add(1, Ordering::SeqCst) as u64;
let handle = TDDAnimationHandle::new(id);
let timeline_clone = timeline.clone();
{
let mut timelines = self.timelines.lock().map_err(|_| {
AnimationError::MemoryError("Failed to acquire timeline lock".to_string())
})?;
timelines.insert(id, timeline_clone);
}
self.active_count.fetch_add(1, Ordering::SeqCst);
Ok(handle)
}
pub fn stop_timeline(&self, handle: TDDAnimationHandle) -> Result<()> {
let mut timelines = self.timelines.lock().map_err(|_| {
AnimationError::MemoryError("Failed to acquire timeline lock".to_string())
})?;
if let Some(mut timeline) = timelines.remove(&handle.0) {
timeline.stop();
self.active_count.fetch_sub(1, Ordering::SeqCst);
Ok(())
} else {
Err(AnimationError::NotFound {
handle: handle.to_animation_handle(),
})
}
}
pub fn get_timeline(&self, handle: TDDAnimationHandle) -> Result<Timeline> {
let timelines = self.timelines.lock().map_err(|_| {
AnimationError::MemoryError("Failed to acquire timeline lock".to_string())
})?;
timelines
.get(&handle.0)
.cloned()
.ok_or(AnimationError::NotFound {
handle: handle.to_animation_handle(),
})
}
pub fn update_timeline(&self, handle: TDDAnimationHandle, timeline: Timeline) -> Result<()> {
let mut timelines = self.timelines.lock().map_err(|_| {
AnimationError::MemoryError("Failed to acquire timeline lock".to_string())
})?;
if timelines.contains_key(&handle.0) {
timelines.insert(handle.0, timeline);
Ok(())
} else {
Err(AnimationError::NotFound {
handle: handle.to_animation_handle(),
})
}
}
pub fn get_memory_stats(&self) -> MemoryStats {
let animations = match self.animations.lock() {
Ok(animations) => animations,
Err(_) => {
return MemoryStats::default();
}
};
let animation_count = animations.len();
let estimated_memory = animation_count * std::mem::size_of::<ActiveAnimation>();
MemoryStats {
active_animations: animation_count,
estimated_memory_bytes: estimated_memory,
engine_age: self.created_at.elapsed(),
}
}
fn validate_animation_config(&self, config: &AnimationConfig) -> Result<()> {
if let Some(duration) = config.duration {
if !duration.is_finite() {
return Err(AnimationError::InvalidValue(
"Duration must be finite".to_string(),
));
}
if duration < 0.0 {
return Err(AnimationError::InvalidValue(
"Duration cannot be negative".to_string(),
));
}
if duration == 0.0 {
return Err(AnimationError::InvalidValue(
"Duration cannot be zero".to_string(),
));
}
}
if let Some(delay) = config.delay {
if !delay.is_finite() {
return Err(AnimationError::InvalidValue(
"Delay must be finite".to_string(),
));
}
if delay < 0.0 {
return Err(AnimationError::InvalidValue(
"Delay cannot be negative".to_string(),
));
}
}
for (property_name, animation_value) in config.target.iter() {
self.validate_animation_value(property_name, animation_value)?;
}
Ok(())
}
fn validate_animation_value(&self, _property: &str, value: &AnimationValue) -> Result<()> {
match value {
AnimationValue::Number(n) => {
if !n.is_finite() {
return Err(AnimationError::InvalidValue(format!(
"Animation value must be finite, got: {}",
n
)));
}
}
AnimationValue::Pixels(p) => {
if !p.is_finite() {
return Err(AnimationError::InvalidValue(format!(
"Pixel value must be finite, got: {}",
p
)));
}
}
AnimationValue::Percentage(p) => {
if !p.is_finite() {
return Err(AnimationError::InvalidValue(format!(
"Percentage value must be finite, got: {}",
p
)));
}
}
AnimationValue::Degrees(d) => {
if !d.is_finite() {
return Err(AnimationError::InvalidValue(format!(
"Degree value must be finite, got: {}",
d
)));
}
}
AnimationValue::Color(_) | AnimationValue::String(_) => {}
AnimationValue::Transform(_) => {
}
_ => {
}
}
Ok(())
}
}
impl Default for AnimationEngine {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct MemoryStats {
pub active_animations: usize,
pub estimated_memory_bytes: usize,
pub engine_age: std::time::Duration,
}
pub fn motion_target_macro_impl(values: Vec<(&str, AnimationValue)>) -> AnimationTarget {
let mut target = AnimationTarget::new();
for (key, value) in values {
target.insert(key.to_string(), value);
}
target
}
#[macro_export]
macro_rules! motion_target {
($($key:expr => $value:expr),* $(,)?) => {
$crate::tdd_engine::motion_target_macro_impl(vec![$(($key, $value)),*])
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_animation_engine_creation() {
let engine = AnimationEngine::new();
assert_eq!(engine.active_animations_count(), 0);
assert!(engine.is_stable());
}
#[test]
fn test_animation_config_default() {
let config = AnimationConfig::default();
assert!(config.id.is_none());
assert_eq!(config.duration, Some(1.0));
assert_eq!(config.ease, Easing::Linear);
assert!(config.delay.is_none());
assert_eq!(config.repeat, RepeatConfig::Never);
}
#[test]
fn test_animation_handle_creation() {
let handle = TDDAnimationHandle::new(123);
assert_eq!(handle.0, 123);
let handle2 = TDDAnimationHandle::new(456);
assert_eq!(handle2.0, 456);
assert_ne!(handle, handle2);
}
#[test]
fn test_memory_stats_default() {
let stats = MemoryStats::default();
assert_eq!(stats.active_animations, 0);
assert_eq!(stats.estimated_memory_bytes, 0);
}
#[test]
fn test_motion_target_macro() {
let target = motion_target!(
"opacity" => AnimationValue::Number(1.0),
"scale" => AnimationValue::Number(1.5)
);
assert_eq!(target.len(), 2);
assert_eq!(target.get("opacity"), Some(&AnimationValue::Number(1.0)));
assert_eq!(target.get("scale"), Some(&AnimationValue::Number(1.5)));
}
#[test]
fn test_animation_validation_valid_config() {
let engine = AnimationEngine::new();
let config = AnimationConfig {
id: Some("test".to_string()),
target: motion_target!("opacity" => AnimationValue::Number(1.0)),
duration: Some(0.5),
ease: Easing::Linear,
delay: Some(0.1),
repeat: RepeatConfig::Never,
};
assert!(engine.validate_animation_config(&config).is_ok());
}
#[test]
fn test_animation_validation_invalid_duration() {
let engine = AnimationEngine::new();
let mut config = AnimationConfig::default();
config.duration = Some(-1.0);
assert!(engine.validate_animation_config(&config).is_err());
config.duration = Some(0.0);
assert!(engine.validate_animation_config(&config).is_err());
config.duration = Some(f64::INFINITY);
assert!(engine.validate_animation_config(&config).is_err());
config.duration = Some(f64::NAN);
assert!(engine.validate_animation_config(&config).is_err());
}
#[test]
fn test_animation_validation_invalid_values() {
let engine = AnimationEngine::new();
let config = AnimationConfig {
target: motion_target!("opacity" => AnimationValue::Number(f64::NAN)),
..AnimationConfig::default()
};
assert!(engine.validate_animation_config(&config).is_err());
let config = AnimationConfig {
target: motion_target!("x" => AnimationValue::Pixels(f64::INFINITY)),
..AnimationConfig::default()
};
assert!(engine.validate_animation_config(&config).is_err());
}
}