dioxus_motion/
sequence.rs

1//! `AnimationSequence<T>` - Optimized animation step sequences
2
3use crate::animations::core::Animatable;
4use crate::prelude::AnimationConfig;
5
6use std::sync::Arc;
7use std::sync::Mutex;
8use std::sync::atomic::{AtomicU8, Ordering};
9
10#[derive(Clone)]
11pub struct AnimationStep<T: Animatable> {
12    pub target: T,
13    pub config: Arc<AnimationConfig>,
14    pub predicted_next: Option<T>,
15}
16
17/// Optimized animation sequence that uses shared immutable data and atomic counters
18/// to avoid cloning sequences on step transitions
19pub struct AnimationSequence<T: Animatable> {
20    /// Immutable shared steps - no cloning needed
21    steps: Arc<[AnimationStep<T>]>,
22    /// Atomic counter for current step - thread-safe without locks
23    current_step: AtomicU8,
24    /// Completion callback - thread-safe with Mutex to allow execution without ownership
25    #[allow(clippy::type_complexity)]
26    on_complete: Arc<Mutex<Option<Box<dyn FnOnce() + Send>>>>,
27}
28
29impl<T: Animatable> AnimationSequence<T> {
30    /// Creates a new empty animation sequence
31    pub fn new() -> Self {
32        Self {
33            steps: Arc::new([]),
34            current_step: AtomicU8::new(0),
35            on_complete: Arc::new(Mutex::new(None)),
36        }
37    }
38
39    /// Creates a new animation sequence with specified capacity hint
40    /// Note: This is kept for API compatibility but doesn't pre-allocate since we use Arc<\[T\]>
41    pub fn with_capacity(_capacity: u8) -> Self {
42        Self::new()
43    }
44
45    /// Creates a new animation sequence from a vector of steps
46    pub fn from_steps(steps: Vec<AnimationStep<T>>) -> Self {
47        Self {
48            steps: steps.into(),
49            current_step: AtomicU8::new(0),
50            on_complete: Arc::new(Mutex::new(None)),
51        }
52    }
53
54    /// Creates a new animation sequence with a completion callback
55    pub fn with_on_complete<F>(steps: Vec<AnimationStep<T>>, on_complete: F) -> Self
56    where
57        F: FnOnce() + Send + 'static,
58    {
59        Self {
60            steps: steps.into(),
61            current_step: AtomicU8::new(0),
62            on_complete: Arc::new(Mutex::new(Some(Box::new(on_complete)))),
63        }
64    }
65
66    /// Reserve additional capacity (kept for API compatibility, but no-op since we use Arc<\[T\]>)
67    pub fn reserve(&mut self, _additional: u8) {
68        // No-op for Arc<[T]> - kept for backward compatibility
69    }
70
71    /// Adds a new step to the sequence and returns a new sequence
72    /// This creates a new Arc with all steps to maintain immutability
73    pub fn then(self, target: T, config: AnimationConfig) -> Self {
74        let predicted_next = if self.steps.is_empty() {
75            None
76        } else {
77            self.steps
78                .last()
79                .map(|last_step| last_step.target.interpolate(&target, 0.5))
80        };
81
82        let new_step = AnimationStep {
83            target,
84            config: Arc::new(config),
85            predicted_next,
86        };
87
88        // Create new vector with existing steps plus the new one
89        let mut new_steps: Vec<AnimationStep<T>> = self.steps.iter().cloned().collect();
90        new_steps.push(new_step);
91
92        Self {
93            steps: new_steps.into(),
94            current_step: AtomicU8::new(self.current_step.load(Ordering::Relaxed)),
95            on_complete: self.on_complete,
96        }
97    }
98
99    /// Sets a completion callback
100    pub fn on_complete<F: FnOnce() + Send + 'static>(self, f: F) -> Self {
101        if let Ok(mut guard) = self.on_complete.lock() {
102            *guard = Some(Box::new(f));
103        }
104        self
105    }
106
107    /// Advances to the next step in the sequence
108    /// Returns true if advanced, false if already at the end
109    pub fn advance_step(&self) -> bool {
110        let current = self.current_step.load(Ordering::Relaxed);
111        let total_steps = self.steps.len() as u8;
112
113        if current < total_steps.saturating_sub(1) {
114            self.current_step.store(current + 1, Ordering::Relaxed);
115            true
116        } else {
117            false
118        }
119    }
120
121    /// Gets the current step index
122    pub fn current_step_index(&self) -> u8 {
123        self.current_step.load(Ordering::Relaxed)
124    }
125
126    /// Gets the current step index (kept for backward compatibility)
127    pub fn current_step(&self) -> u8 {
128        self.current_step_index()
129    }
130
131    /// Gets the configuration for the current step
132    pub fn current_config(&self) -> Option<&AnimationConfig> {
133        let current = self.current_step.load(Ordering::Relaxed) as usize;
134        self.steps.get(current).map(|step| step.config.as_ref())
135    }
136
137    /// Gets the target value for the current step
138    pub fn current_target(&self) -> Option<T> {
139        let current = self.current_step.load(Ordering::Relaxed) as usize;
140        self.steps.get(current).map(|step| step.target)
141    }
142
143    /// Gets the current step data
144    pub fn current_step_data(&self) -> Option<&AnimationStep<T>> {
145        let current = self.current_step.load(Ordering::Relaxed) as usize;
146        self.steps.get(current)
147    }
148
149    /// Gets all steps (for backward compatibility)
150    pub fn steps(&self) -> &[AnimationStep<T>] {
151        &self.steps
152    }
153
154    /// Checks if the sequence is complete (at the last step)
155    pub fn is_complete(&self) -> bool {
156        let current = self.current_step.load(Ordering::Relaxed);
157        let total_steps = self.steps.len() as u8;
158        current >= total_steps.saturating_sub(1)
159    }
160
161    /// Gets the total number of steps
162    pub fn total_steps(&self) -> usize {
163        self.steps.len()
164    }
165
166    /// Resets the sequence to the first step
167    pub fn reset(&self) {
168        self.current_step.store(0, Ordering::Relaxed);
169    }
170
171    /// Executes the completion callback if present
172    /// This method is thread-safe and can be called without ownership
173    pub fn execute_completion(&self) {
174        if let Ok(mut guard) = self.on_complete.lock() {
175            if let Some(callback) = guard.take() {
176                callback();
177            }
178        }
179    }
180}
181
182impl<T: Animatable> Clone for AnimationSequence<T> {
183    fn clone(&self) -> Self {
184        Self {
185            steps: self.steps.clone(), // Arc clone is cheap
186            current_step: AtomicU8::new(self.current_step.load(Ordering::Relaxed)),
187            on_complete: Arc::new(Mutex::new(None)), // Callbacks can't be cloned
188        }
189    }
190}
191
192impl<T: Animatable> Default for AnimationSequence<T> {
193    fn default() -> Self {
194        Self::new()
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    #![allow(clippy::unwrap_used)]
201    use super::*;
202    use crate::animations::core::AnimationMode;
203    use crate::animations::spring::Spring;
204    use std::sync::{Arc, Mutex};
205
206    #[test]
207    fn test_animation_sequence_basic() {
208        let steps = vec![
209            AnimationStep {
210                target: 10.0f32,
211                config: Arc::new(AnimationConfig::new(AnimationMode::Spring(
212                    Spring::default(),
213                ))),
214                predicted_next: None,
215            },
216            AnimationStep {
217                target: 20.0f32,
218                config: Arc::new(AnimationConfig::new(AnimationMode::Spring(
219                    Spring::default(),
220                ))),
221                predicted_next: None,
222            },
223            AnimationStep {
224                target: 30.0f32,
225                config: Arc::new(AnimationConfig::new(AnimationMode::Spring(
226                    Spring::default(),
227                ))),
228                predicted_next: None,
229            },
230        ];
231
232        let sequence = AnimationSequence::from_steps(steps);
233
234        // Test initial state
235        assert_eq!(sequence.current_step_index(), 0);
236        assert_eq!(sequence.current_target().unwrap(), 10.0f32);
237        assert!(!sequence.is_complete());
238        assert_eq!(sequence.total_steps(), 3);
239
240        // Test advancing steps
241        assert!(sequence.advance_step());
242        assert_eq!(sequence.current_step_index(), 1);
243        assert_eq!(sequence.current_target().unwrap(), 20.0f32);
244        assert!(!sequence.is_complete());
245
246        assert!(sequence.advance_step());
247        assert_eq!(sequence.current_step_index(), 2);
248        assert_eq!(sequence.current_target().unwrap(), 30.0f32);
249        assert!(sequence.is_complete());
250
251        // Test can't advance past end
252        assert!(!sequence.advance_step());
253        assert_eq!(sequence.current_step_index(), 2);
254
255        // Test reset
256        sequence.reset();
257        assert_eq!(sequence.current_step_index(), 0);
258        assert!(!sequence.is_complete());
259    }
260
261    #[test]
262    fn test_animation_sequence_builder_pattern() {
263        let sequence = AnimationSequence::new()
264            .then(
265                10.0f32,
266                AnimationConfig::new(AnimationMode::Spring(Spring::default())),
267            )
268            .then(
269                20.0f32,
270                AnimationConfig::new(AnimationMode::Spring(Spring::default())),
271            )
272            .then(
273                30.0f32,
274                AnimationConfig::new(AnimationMode::Spring(Spring::default())),
275            );
276
277        assert_eq!(sequence.total_steps(), 3);
278        assert_eq!(sequence.current_target().unwrap(), 10.0f32);
279        assert!(!sequence.is_complete());
280
281        assert!(sequence.advance_step());
282        assert_eq!(sequence.current_target().unwrap(), 20.0f32);
283
284        assert!(sequence.advance_step());
285        assert_eq!(sequence.current_target().unwrap(), 30.0f32);
286        assert!(sequence.is_complete());
287    }
288
289    #[test]
290    fn test_animation_sequence_with_callback() {
291        let callback_executed = Arc::new(Mutex::new(false));
292        let callback_executed_clone = callback_executed.clone();
293
294        let steps = vec![AnimationStep {
295            target: 10.0f32,
296            config: Arc::new(AnimationConfig::new(AnimationMode::Spring(
297                Spring::default(),
298            ))),
299            predicted_next: None,
300        }];
301
302        let sequence = AnimationSequence::with_on_complete(steps, move || {
303            *callback_executed_clone.lock().unwrap() = true;
304        });
305
306        // Execute completion callback
307        sequence.execute_completion();
308
309        assert!(*callback_executed.lock().unwrap());
310    }
311
312    #[test]
313    fn test_animation_sequence_callback_with_shared_references() {
314        let callback_executed = Arc::new(Mutex::new(false));
315        let callback_executed_clone = callback_executed.clone();
316
317        let steps = vec![AnimationStep {
318            target: 10.0f32,
319            config: Arc::new(AnimationConfig::new(AnimationMode::Spring(
320                Spring::default(),
321            ))),
322            predicted_next: None,
323        }];
324
325        let sequence = AnimationSequence::with_on_complete(steps, move || {
326            *callback_executed_clone.lock().unwrap() = true;
327        });
328
329        // Create multiple Arc references to the sequence
330        let sequence_arc1 = Arc::new(sequence);
331        let sequence_arc2 = sequence_arc1.clone();
332        let sequence_arc3 = sequence_arc1.clone();
333
334        // Verify that Arc::try_unwrap would fail (multiple references exist)
335        assert!(Arc::try_unwrap(sequence_arc1.clone()).is_err());
336
337        // Execute completion callback through one of the references
338        // This should work even though we can't get ownership
339        sequence_arc1.execute_completion();
340
341        // Verify the callback was executed
342        assert!(*callback_executed.lock().unwrap());
343
344        // Verify that other references still exist and are valid
345        assert_eq!(sequence_arc2.current_step_index(), 0);
346        assert_eq!(sequence_arc3.current_step_index(), 0);
347    }
348
349    #[test]
350    fn test_animation_sequence_clone() {
351        let steps = vec![AnimationStep {
352            target: 10.0f32,
353            config: Arc::new(AnimationConfig::new(AnimationMode::Spring(
354                Spring::default(),
355            ))),
356            predicted_next: None,
357        }];
358
359        let sequence1 = AnimationSequence::from_steps(steps);
360        sequence1.advance_step(); // This won't work since there's only one step, but let's test the clone
361
362        let sequence2 = sequence1.clone();
363
364        // Both sequences should have the same step data but independent counters
365        assert_eq!(
366            sequence1.current_step_index(),
367            sequence2.current_step_index()
368        );
369        assert_eq!(sequence1.total_steps(), sequence2.total_steps());
370        assert_eq!(sequence1.current_target(), sequence2.current_target());
371    }
372
373    #[test]
374    fn test_animation_sequence_backward_compatibility() {
375        // Test that the old API still works
376        let sequence = AnimationSequence::new();
377        let sequence = sequence.then(
378            10.0f32,
379            AnimationConfig::new(AnimationMode::Spring(Spring::default())),
380        );
381        let sequence = sequence.then(
382            20.0f32,
383            AnimationConfig::new(AnimationMode::Spring(Spring::default())),
384        );
385
386        // Test old method names
387        assert_eq!(sequence.current_step(), 0);
388        assert_eq!(sequence.steps().len(), 2);
389
390        // Test with_capacity (should work but be a no-op)
391        let _sequence_with_capacity = AnimationSequence::<f32>::with_capacity(10);
392
393        // Test reserve (should work but be a no-op)
394        let mut sequence_mut = sequence.clone();
395        sequence_mut.reserve(5);
396    }
397}