Skip to main content

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