Skip to main content

animato_driver/
driver.rs

1//! [`AnimationDriver`] — owns and ticks multiple animations simultaneously.
2
3use animato_core::Update;
4
5/// An opaque handle to an animation registered with [`AnimationDriver`].
6///
7/// Returned by [`AnimationDriver::add`]. Use it to cancel or query animations.
8#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
9pub struct AnimationId(u64);
10
11struct Slot {
12    id: AnimationId,
13    animation: Box<dyn Update + Send>,
14    remove: bool,
15}
16
17impl std::fmt::Debug for Slot {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        f.debug_struct("Slot")
20            .field("id", &self.id)
21            .field("remove", &self.remove)
22            .finish()
23    }
24}
25
26/// Manages a collection of animations, ticking them each frame and
27/// automatically removing completed ones.
28///
29/// # Example
30///
31/// ```rust
32/// use animato_driver::{AnimationDriver, MockClock, Clock};
33/// use animato_tween::Tween;
34/// use animato_core::Easing;
35///
36/// let mut driver = AnimationDriver::new();
37/// let id = driver.add(
38///     Tween::new(0.0_f32, 1.0).duration(1.0).build()
39/// );
40///
41/// assert!(driver.is_active(id));
42/// assert_eq!(driver.active_count(), 1);
43///
44/// // Tick past completion:
45/// driver.tick(2.0);
46/// assert!(!driver.is_active(id));
47/// assert_eq!(driver.active_count(), 0);
48/// ```
49#[derive(Debug, Default)]
50pub struct AnimationDriver {
51    slots: Vec<Slot>,
52    next_id: u64,
53}
54
55impl AnimationDriver {
56    /// Create a new, empty driver.
57    pub fn new() -> Self {
58        Self {
59            slots: Vec::new(),
60            next_id: 0,
61        }
62    }
63
64    /// Register an animation and return its [`AnimationId`].
65    ///
66    /// The animation will be ticked on every call to [`tick`](Self::tick)
67    /// and automatically removed when it returns `false` from `update`.
68    pub fn add<A: Update + Send + 'static>(&mut self, anim: A) -> AnimationId {
69        let id = AnimationId(self.next_id);
70        self.next_id += 1;
71        self.slots.push(Slot {
72            id,
73            animation: Box::new(anim),
74            remove: false,
75        });
76        id
77    }
78
79    /// Advance all active animations by `dt` seconds.
80    ///
81    /// Animations that return `false` from `update` are marked and removed
82    /// at the end of the tick — no allocation during the tick itself.
83    pub fn tick(&mut self, dt: f32) {
84        for slot in self.slots.iter_mut() {
85            if slot.remove {
86                continue;
87            }
88            let still_running = slot.animation.update(dt);
89            if !still_running {
90                slot.remove = true;
91            }
92        }
93        // Drain completed slots in one pass — O(n), no realloc.
94        self.slots.retain(|s| !s.remove);
95    }
96
97    /// Cancel an animation by id.
98    ///
99    /// The animation is removed immediately, before the next tick.
100    /// No-op if the id is not found (e.g. already completed).
101    pub fn cancel(&mut self, id: AnimationId) {
102        self.slots.retain(|s| s.id != id);
103    }
104
105    /// Cancel all active animations.
106    pub fn cancel_all(&mut self) {
107        self.slots.clear();
108    }
109
110    /// Number of currently active animations.
111    pub fn active_count(&self) -> usize {
112        self.slots.len()
113    }
114
115    /// `true` if the animation with the given id is still active.
116    pub fn is_active(&self, id: AnimationId) -> bool {
117        self.slots.iter().any(|s| s.id == id)
118    }
119}
120
121// ──────────────────────────────────────────────────────────────────────────────
122// Tests
123// ──────────────────────────────────────────────────────────────────────────────
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use animato_core::Easing;
129    use animato_tween::Tween;
130
131    #[test]
132    fn auto_removes_completed() {
133        let mut driver = AnimationDriver::new();
134        let id = driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build());
135        assert!(driver.is_active(id));
136        driver.tick(2.0); // well past duration
137        assert!(!driver.is_active(id));
138        assert_eq!(driver.active_count(), 0);
139    }
140
141    #[test]
142    fn cancel_removes_mid_animation() {
143        let mut driver = AnimationDriver::new();
144        let id = driver.add(Tween::new(0.0_f32, 1.0).duration(10.0).build());
145        driver.tick(1.0);
146        assert!(driver.is_active(id));
147        driver.cancel(id);
148        assert!(!driver.is_active(id));
149    }
150
151    #[test]
152    fn cancel_noop_on_missing_id() {
153        let mut driver = AnimationDriver::new();
154        let id = driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build());
155        driver.tick(2.0); // completes, auto-removed
156        driver.cancel(id); // should not panic
157        assert_eq!(driver.active_count(), 0);
158    }
159
160    #[test]
161    fn active_count_tracks_correctly() {
162        let mut driver = AnimationDriver::new();
163        let _a = driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build());
164        let _b = driver.add(Tween::new(0.0_f32, 1.0).duration(2.0).build());
165        let _c = driver.add(Tween::new(0.0_f32, 1.0).duration(3.0).build());
166        assert_eq!(driver.active_count(), 3);
167
168        driver.tick(1.5); // first one completes
169        assert_eq!(driver.active_count(), 2);
170
171        driver.tick(1.0); // second completes
172        assert_eq!(driver.active_count(), 1);
173    }
174
175    #[test]
176    fn cancel_all_clears_everything() {
177        let mut driver = AnimationDriver::new();
178        for _ in 0..10 {
179            driver.add(Tween::new(0.0_f32, 1.0).duration(5.0).build());
180        }
181        assert_eq!(driver.active_count(), 10);
182        driver.cancel_all();
183        assert_eq!(driver.active_count(), 0);
184    }
185
186    #[test]
187    fn multiple_concurrent_animations_tick_independently() {
188        let mut driver = AnimationDriver::new();
189        let slow = driver.add(Tween::new(0.0_f32, 1.0).duration(2.0).build());
190        let fast = driver.add(
191            Tween::new(0.0_f32, 1.0)
192                .duration(0.5)
193                .easing(Easing::Linear)
194                .build(),
195        );
196
197        driver.tick(1.0); // fast completes, slow still running
198        assert!(!driver.is_active(fast));
199        assert!(driver.is_active(slow));
200
201        driver.tick(2.0); // slow completes
202        assert!(!driver.is_active(slow));
203    }
204
205    #[test]
206    fn animation_id_is_unique() {
207        let mut driver = AnimationDriver::new();
208        let ids: Vec<_> = (0..100)
209            .map(|_| driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build()))
210            .collect();
211        let unique: std::collections::HashSet<_> = ids.iter().collect();
212        assert_eq!(unique.len(), 100);
213    }
214}