dioxus_motion/
motion.rs

1use crate::Duration;
2use crate::TimeProvider;
3use crate::animations::core::Animatable;
4use crate::animations::state_machine::AnimationState;
5use crate::keyframes::KeyframeAnimation;
6use crate::pool::{ConfigHandle, SpringIntegratorHandle, global};
7use crate::prelude::AnimationConfig;
8use crate::sequence::AnimationSequence;
9use std::sync::Arc;
10
11#[derive(Clone)]
12pub struct Motion<T: Animatable + Send + 'static> {
13    pub initial: T,
14    pub current: T,
15    pub target: T,
16    pub velocity: T,
17    pub running: bool,
18    pub elapsed: Duration,
19    pub delay_elapsed: Duration,
20    pub current_loop: u8,
21    pub reverse: bool,
22
23    // Optimized components (now the primary implementation)
24    /// State machine for efficient animation dispatch
25    pub animation_state: AnimationState<T>,
26    /// Handle to pooled configuration
27    config_handle: ConfigHandle,
28    /// Handle to pooled spring integrator for spring animations
29    spring_integrator_handle: Option<SpringIntegratorHandle>,
30    /// Current sequence being animated (if any)
31    pub sequence: Option<Arc<AnimationSequence<T>>>,
32    /// Current keyframe animation (if any)
33    pub keyframe_animation: Option<Arc<KeyframeAnimation<T>>>,
34
35    // Internal value cache: (value, frame_time)
36    value_cache: Option<(T, f32)>,
37}
38
39impl<T: Animatable + Send + 'static> Drop for Motion<T> {
40    fn drop(&mut self) {
41        // Return config handle to pool
42        global::return_config(self.config_handle.clone());
43
44        // Return spring integrator handle to pool
45        if let Some(handle) = self.spring_integrator_handle.take() {
46            self.try_return_spring_integrator(handle);
47        }
48    }
49}
50
51impl<T: Animatable + Send + 'static> Motion<T> {
52    pub fn new(initial: T) -> Self {
53        let config_handle = global::get_config();
54        global::modify_config(&config_handle, |config| {
55            *config = AnimationConfig::default();
56        });
57
58        Self {
59            initial,
60            current: initial,
61            target: initial,
62            velocity: T::default(),
63            running: false,
64            elapsed: Duration::default(),
65            delay_elapsed: Duration::default(),
66            current_loop: 0,
67            reverse: false,
68
69            // Optimized components
70            animation_state: AnimationState::new_idle(),
71            config_handle,
72            spring_integrator_handle: None,
73            sequence: None,
74            keyframe_animation: None,
75
76            value_cache: None,
77        }
78    }
79
80    pub fn animate_to(&mut self, target: T, config: AnimationConfig) {
81        self.value_cache = None;
82        self.sequence = None;
83        self.initial = self.current;
84        self.target = target;
85        self.running = true;
86        self.elapsed = Duration::default();
87        self.delay_elapsed = Duration::default();
88        self.velocity = T::default();
89        self.current_loop = 0;
90
91        // Update config handle
92        global::modify_config(&self.config_handle, |pooled_config| {
93            *pooled_config = config.clone();
94        });
95
96        // Set up spring integrator if needed
97        if matches!(
98            config.mode,
99            crate::animations::core::AnimationMode::Spring(_)
100        ) && self.spring_integrator_handle.is_none()
101        {
102            self.spring_integrator_handle = self.try_get_spring_integrator();
103        }
104
105        // Set up state machine for running animation
106        self.animation_state = AnimationState::new_running(config.mode, self.config_handle.clone());
107    }
108
109    pub fn animate_sequence(&mut self, sequence: AnimationSequence<T>) {
110        self.value_cache = None;
111        if let Some(first_step) = sequence.steps().first() {
112            let first_config = (*first_step.config).clone();
113            self.animate_to(first_step.target, first_config);
114            let new_sequence = sequence.clone();
115            new_sequence.reset(); // Reset to first step
116            self.sequence = Some(Arc::new(new_sequence.clone()));
117
118            // Set up state machine for sequence animation
119            self.animation_state =
120                AnimationState::new_sequence(Arc::new(new_sequence), self.config_handle.clone());
121        }
122    }
123
124    pub fn animate_keyframes(&mut self, animation: KeyframeAnimation<T>) {
125        self.value_cache = None;
126        self.keyframe_animation = Some(Arc::new(animation.clone()));
127        self.running = true;
128        self.elapsed = Duration::default();
129        self.velocity = T::default();
130
131        // Set up state machine for keyframe animation
132        self.animation_state =
133            AnimationState::new_keyframes(Arc::new(animation), self.config_handle.clone());
134    }
135
136    pub fn get_value(&self) -> T {
137        // If the cache is valid for this frame, return it
138        let now = crate::Time::now().elapsed().as_secs_f32();
139        if let Some((ref cached, cached_time)) = self.value_cache {
140            if (now - cached_time).abs() < 0.001 {
141                return *cached;
142            }
143        }
144        // Not cached or outdated, so cache and return current value
145        // (In practice, current is always up to date, but this is where you'd compute if needed)
146        // Note: This requires &mut self, so we need to use interior mutability (e.g., RefCell) for full effect.
147        // For now, just return current.
148        self.current
149    }
150
151    pub fn is_running(&self) -> bool {
152        self.animation_state.is_active()
153    }
154
155    pub fn reset(&mut self) {
156        self.value_cache = None;
157        self.stop();
158        self.current = self.initial;
159        self.elapsed = Duration::default();
160    }
161
162    pub fn stop(&mut self) {
163        self.value_cache = None;
164        self.running = false;
165        self.current_loop = 0;
166        self.velocity = T::default();
167        self.sequence = None;
168        self.keyframe_animation = None;
169        self.animation_state = AnimationState::new_idle();
170
171        // Return spring integrator to pool if we have one
172        if let Some(handle) = self.spring_integrator_handle.take() {
173            self.try_return_spring_integrator(handle);
174        }
175    }
176
177    pub fn delay(&mut self, duration: Duration) {
178        self.value_cache = None;
179
180        // Update config handle
181        global::modify_config(&self.config_handle, |pooled_config| {
182            pooled_config.delay = duration;
183        });
184    }
185
186    /// Gets the effective epsilon threshold for this animation
187    /// Uses the configured epsilon if present, otherwise falls back to the type's default
188    pub fn get_epsilon(&self) -> f32 {
189        if let Some(config) = global::get_config_ref(&self.config_handle) {
190            config.epsilon.unwrap_or_else(T::epsilon)
191        } else {
192            T::epsilon()
193        }
194    }
195
196    pub fn update(&mut self, dt: f32) -> bool {
197        // Invalidate value cache on update
198        self.value_cache = None;
199
200        // Use state machine dispatch instead of complex branching
201        // We need to temporarily take the state to avoid borrowing issues
202        let mut state = std::mem::replace(&mut self.animation_state, AnimationState::new_idle());
203        let result = state.update(dt, self);
204        self.animation_state = state;
205        result
206    }
207
208    /// Gets the current config handle for optimization purposes
209    pub fn config_handle(&self) -> &ConfigHandle {
210        &self.config_handle
211    }
212
213    /// Gets the current spring integrator handle for optimization purposes
214    pub fn spring_integrator_handle(&self) -> Option<&SpringIntegratorHandle> {
215        self.spring_integrator_handle.as_ref()
216    }
217
218    /// Gets optimization statistics for this Motion instance
219    pub fn optimization_stats(&self) -> MotionOptimizationStats {
220        MotionOptimizationStats {
221            has_config_handle: true, // Always true now
222            has_spring_integrator: self.spring_integrator_handle.is_some(),
223            state_machine_active: self.animation_state.is_active(),
224            value_cache_active: self.value_cache.is_some(),
225        }
226    }
227
228    /// Tries to get a spring integrator handle (now always available)
229    fn try_get_spring_integrator(&self) -> Option<SpringIntegratorHandle> {
230        Some(crate::pool::integrator::get_integrator::<T>())
231    }
232
233    /// Tries to return a spring integrator handle (now always available)
234    fn try_return_spring_integrator(&self, handle: SpringIntegratorHandle) {
235        crate::pool::integrator::return_integrator::<T>(handle);
236    }
237}
238
239/// Statistics about Motion optimization usage
240#[derive(Debug, Clone, PartialEq)]
241pub struct MotionOptimizationStats {
242    pub has_config_handle: bool,
243    pub has_spring_integrator: bool,
244    pub state_machine_active: bool,
245    pub value_cache_active: bool,
246}
247
248#[cfg(test)]
249mod tests {
250    #![allow(clippy::unwrap_used)]
251    use super::*;
252    use crate::animations::core::AnimationMode;
253    use crate::animations::spring::Spring;
254    use crate::keyframes::KeyframeAnimation;
255    use crate::prelude::Tween;
256    use crate::sequence::{AnimationSequence, AnimationStep};
257
258    #[test]
259    fn test_motion_new_with_optimizations() {
260        crate::pool::global::clear_pool();
261
262        let motion = Motion::new(0.0f32);
263
264        // Verify basic initialization
265        assert_eq!(motion.initial, 0.0);
266        assert_eq!(motion.current, 0.0);
267        assert_eq!(motion.target, 0.0);
268        assert!(!motion.running);
269
270        // Verify optimization components are initialized
271        let stats = motion.optimization_stats();
272        assert!(stats.has_config_handle);
273        assert!(!stats.has_spring_integrator); // Not needed for idle motion
274        assert!(!stats.state_machine_active); // Idle state
275        assert!(!stats.value_cache_active);
276    }
277
278    #[test]
279    fn test_motion_animate_to_with_optimizations() {
280        crate::pool::global::clear_pool();
281
282        let mut motion = Motion::new(0.0f32);
283        let config = AnimationConfig::new(AnimationMode::Tween(Tween::default()));
284
285        motion.animate_to(100.0, config);
286
287        // Verify animation setup
288        assert_eq!(motion.target, 100.0);
289        assert!(motion.running);
290
291        // Verify optimization components
292        let stats = motion.optimization_stats();
293        assert!(stats.has_config_handle);
294        assert!(!stats.has_spring_integrator); // Tween doesn't need integrator
295        assert!(stats.state_machine_active);
296
297        // Verify config handle contains correct config
298        let handle = motion.config_handle();
299        let config = crate::pool::global::get_config_ref(handle).unwrap();
300        assert!(matches!(config.mode, AnimationMode::Tween(_)));
301    }
302
303    #[test]
304    fn test_motion_spring_animation_with_integrator() {
305        crate::pool::global::clear_pool();
306        crate::pool::integrator::clear_pools();
307
308        let mut motion = Motion::new(0.0f32);
309        let config = AnimationConfig::new(AnimationMode::Spring(Spring::default()));
310
311        motion.animate_to(100.0, config);
312
313        // Verify spring integrator is allocated (now enabled by default)
314        let stats = motion.optimization_stats();
315        assert!(stats.has_config_handle);
316        assert!(stats.has_spring_integrator);
317        assert!(stats.state_machine_active);
318
319        // Verify integrator handle is allocated
320        assert!(motion.spring_integrator_handle().is_some());
321
322        // Verify pool statistics
323        let (in_use, _) = crate::pool::integrator::pool_stats::<f32>();
324        assert_eq!(in_use, 1);
325    }
326
327    #[test]
328    fn test_motion_sequence_with_optimizations() {
329        crate::pool::global::clear_pool();
330
331        let mut motion = Motion::new(0.0f32);
332
333        let steps = vec![
334            AnimationStep {
335                target: 50.0,
336                config: Arc::new(AnimationConfig::default()),
337                predicted_next: None,
338            },
339            AnimationStep {
340                target: 100.0,
341                config: Arc::new(AnimationConfig::default()),
342                predicted_next: None,
343            },
344        ];
345
346        let sequence = AnimationSequence::from_steps(steps);
347        motion.animate_sequence(sequence);
348
349        // Verify sequence setup
350        assert!(motion.sequence.is_some());
351        assert_eq!(motion.target, 50.0); // First step target
352
353        // Verify optimization components
354        let stats = motion.optimization_stats();
355        assert!(stats.has_config_handle);
356        assert!(stats.state_machine_active);
357
358        // Verify state machine is in sequence mode
359        assert!(matches!(
360            motion.animation_state,
361            AnimationState::Sequence { .. }
362        ));
363    }
364
365    #[test]
366    fn test_motion_keyframes_with_optimizations() {
367        crate::pool::global::clear_pool();
368
369        let mut motion = Motion::new(0.0f32);
370
371        let mut animation = KeyframeAnimation::new(Duration::from_secs(1));
372        animation = animation.add_keyframe(0.0, 0.0, None).unwrap();
373        animation = animation.add_keyframe(100.0, 1.0, None).unwrap();
374
375        motion.animate_keyframes(animation);
376
377        // Verify keyframe setup
378        assert!(motion.keyframe_animation.is_some());
379        assert!(motion.running);
380
381        // Verify optimization components
382        let stats = motion.optimization_stats();
383        assert!(stats.has_config_handle);
384        assert!(stats.state_machine_active);
385
386        // Verify state machine is in keyframes mode
387        assert!(matches!(
388            motion.animation_state,
389            AnimationState::Keyframes { .. }
390        ));
391    }
392
393    #[test]
394    fn test_motion_stop_cleanup() {
395        crate::pool::global::clear_pool();
396        crate::pool::integrator::clear_pools();
397
398        let mut motion = Motion::new(0.0f32);
399        let config = AnimationConfig::new(AnimationMode::Spring(Spring::default()));
400
401        motion.animate_to(100.0, config);
402
403        // Verify resources are allocated (now enabled by default)
404        assert!(motion.spring_integrator_handle().is_some());
405        let (in_use_before, _) = crate::pool::integrator::pool_stats::<f32>();
406        assert_eq!(in_use_before, 1);
407
408        motion.stop();
409
410        // Verify resources are cleaned up
411        assert!(motion.spring_integrator_handle().is_none());
412        assert!(!motion.running);
413        assert!(matches!(motion.animation_state, AnimationState::Idle));
414
415        let (in_use_after, available_after) = crate::pool::integrator::pool_stats::<f32>();
416        assert_eq!(in_use_after, 0);
417        assert_eq!(available_after, 1); // Returned to pool
418    }
419
420    #[test]
421    fn test_motion_drop_cleanup() {
422        crate::pool::global::clear_pool();
423        crate::pool::integrator::clear_pools();
424
425        {
426            let mut motion = Motion::new(0.0f32);
427            let config = AnimationConfig::new(AnimationMode::Spring(Spring::default()));
428            motion.animate_to(100.0, config);
429
430            // Verify resources are allocated (both config and integrator)
431            let (in_use, _) = crate::pool::integrator::pool_stats::<f32>();
432            assert_eq!(in_use, 1); // Integrator pooling now enabled
433
434            let (config_in_use, _) = crate::pool::global::pool_stats();
435            assert_eq!(config_in_use, 1);
436        } // Motion drops here
437
438        // Verify resources are returned to pools
439        let (in_use_after, available_after) = crate::pool::integrator::pool_stats::<f32>();
440        assert_eq!(in_use_after, 0);
441        assert_eq!(available_after, 1); // Returned to pool
442
443        let (config_in_use_after, config_available_after) = crate::pool::global::pool_stats();
444        assert_eq!(config_in_use_after, 0);
445        assert_eq!(config_available_after, 1);
446    }
447
448    #[test]
449    fn test_motion_get_epsilon_optimization() {
450        crate::pool::global::clear_pool();
451
452        let motion = Motion::new(0.0f32);
453
454        // Test default epsilon
455        assert_eq!(motion.get_epsilon(), f32::epsilon());
456
457        // Test custom epsilon through optimized config handle
458        let handle = motion.config_handle();
459        crate::pool::global::modify_config(handle, |config| {
460            config.epsilon = Some(0.01);
461        });
462
463        assert_eq!(motion.get_epsilon(), 0.01);
464    }
465
466    #[test]
467    fn test_motion_delay_optimization() {
468        crate::pool::global::clear_pool();
469
470        let mut motion = Motion::new(0.0f32);
471        let delay = Duration::from_millis(100);
472
473        motion.delay(delay);
474
475        // Verify config is updated through the optimized handle
476
477        let handle = motion.config_handle();
478        let config = crate::pool::global::get_config_ref(handle).unwrap();
479        assert_eq!(config.delay, delay);
480    }
481
482    #[test]
483    fn test_motion_update_with_state_machine() {
484        crate::pool::global::clear_pool();
485
486        let mut motion = Motion::new(0.0f32);
487        let config = AnimationConfig::new(AnimationMode::Tween(Tween::default()));
488
489        motion.animate_to(100.0, config);
490
491        // Verify state machine is active
492        assert!(motion.animation_state.is_active());
493
494        // Update animation
495        let should_continue = motion.update(1.0 / 60.0);
496
497        // Should continue animating
498        assert!(should_continue);
499
500        // Value should have changed
501        assert!(motion.current > 0.0);
502        assert!(motion.current < 100.0);
503    }
504
505    #[test]
506    fn test_motion_optimization_stats() {
507        crate::pool::global::clear_pool();
508        crate::pool::integrator::clear_pools();
509
510        let mut motion = Motion::new(0.0f32);
511
512        // Test idle state stats
513        let stats = motion.optimization_stats();
514        assert!(stats.has_config_handle);
515        assert!(!stats.has_spring_integrator);
516        assert!(!stats.state_machine_active);
517        assert!(!stats.value_cache_active);
518
519        // Test spring animation stats
520        let config = AnimationConfig::new(AnimationMode::Spring(Spring::default()));
521        motion.animate_to(100.0, config);
522
523        let stats = motion.optimization_stats();
524        assert!(stats.has_config_handle);
525        assert!(stats.has_spring_integrator); // Now enabled by default
526        assert!(stats.state_machine_active);
527        assert!(!stats.value_cache_active);
528    }
529
530    #[test]
531    fn test_motion_backward_compatibility() {
532        crate::pool::global::clear_pool();
533
534        let mut motion = Motion::new(0.0f32);
535        let config = AnimationConfig::default();
536
537        // Test that legacy API still works
538        motion.animate_to(100.0, config);
539
540        // Core fields should still be accessible
541        assert_eq!(motion.target, 100.0);
542        assert!(motion.running);
543
544        // But optimizations should also be active
545        let stats = motion.optimization_stats();
546        assert!(stats.has_config_handle);
547        assert!(stats.state_machine_active);
548    }
549}