Skip to main content

animato_driver/
driver.rs

1//! [`AnimationDriver`] — owns and ticks multiple animations simultaneously.
2
3use animato_core::{AnimationIntrospection, Inspectable, Update};
4
5#[cfg(feature = "std")]
6use crate::recorder::AnimationRecorder;
7
8/// An opaque handle to an animation registered with [`AnimationDriver`].
9///
10/// Returned by [`AnimationDriver::add`]. Use it to cancel or query animations.
11#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
12pub struct AnimationId(u64);
13
14/// Snapshot of one inspectable animation registered with [`AnimationDriver`].
15#[derive(Clone, Debug, PartialEq)]
16pub struct DriverSnapshot {
17    /// Stable animation id returned by the driver.
18    pub id: AnimationId,
19    /// Optional user-facing label.
20    pub label: Option<String>,
21    /// Runtime state reported by the animation.
22    pub introspection: AnimationIntrospection,
23}
24
25/// Per-animation update cost produced by [`AnimationDriver::tick_profiled`].
26#[derive(Clone, Debug, PartialEq)]
27pub struct AnimationUpdateCost {
28    /// Stable animation id returned by the driver.
29    pub id: AnimationId,
30    /// Optional user-facing label.
31    pub label: Option<String>,
32    /// Wall-clock time spent updating this animation.
33    pub update_time_ms: f32,
34}
35
36/// Timing data produced by a profiled driver tick.
37#[derive(Clone, Debug, Default, PartialEq)]
38pub struct DriverFrameProfile {
39    /// Delta seconds passed to the tick.
40    pub dt: f32,
41    /// Total wall-clock update time for all active animations.
42    pub total_update_time_ms: f32,
43    /// Per-animation update costs.
44    pub animation_costs: Vec<AnimationUpdateCost>,
45}
46
47enum SlotAnimation {
48    Basic(Box<dyn Update + Send>),
49    Inspectable(Box<dyn Inspectable + Send>),
50}
51
52impl SlotAnimation {
53    fn update(&mut self, dt: f32) -> bool {
54        match self {
55            Self::Basic(animation) => animation.update(dt),
56            Self::Inspectable(animation) => animation.update(dt),
57        }
58    }
59
60    fn introspect(&self) -> Option<AnimationIntrospection> {
61        match self {
62            Self::Basic(_) => None,
63            Self::Inspectable(animation) => Some(animation.introspect()),
64        }
65    }
66}
67
68impl std::fmt::Debug for SlotAnimation {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            Self::Basic(_) => f.write_str("Basic(..)"),
72            Self::Inspectable(_) => f.write_str("Inspectable(..)"),
73        }
74    }
75}
76
77struct Slot {
78    id: AnimationId,
79    label: Option<String>,
80    animation: SlotAnimation,
81    remove: bool,
82    #[cfg(feature = "std")]
83    recorder: Option<RecordedSampler>,
84}
85
86#[cfg(feature = "std")]
87struct RecordedSampler {
88    label: String,
89    sample: Box<dyn Fn() -> f64 + Send + 'static>,
90}
91
92#[cfg(feature = "std")]
93impl std::fmt::Debug for RecordedSampler {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("RecordedSampler")
96            .field("label", &self.label)
97            .finish()
98    }
99}
100
101impl std::fmt::Debug for Slot {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.debug_struct("Slot")
104            .field("id", &self.id)
105            .field("label", &self.label)
106            .field("remove", &self.remove)
107            .finish()
108    }
109}
110
111/// Manages a collection of animations, ticking them each frame and
112/// automatically removing completed ones.
113///
114/// # Example
115///
116/// ```rust
117/// use animato_driver::{AnimationDriver, MockClock, Clock};
118/// use animato_tween::Tween;
119/// use animato_core::Easing;
120///
121/// let mut driver = AnimationDriver::new();
122/// let id = driver.add(
123///     Tween::new(0.0_f32, 1.0).duration(1.0).build()
124/// );
125///
126/// assert!(driver.is_active(id));
127/// assert_eq!(driver.active_count(), 1);
128///
129/// // Tick past completion:
130/// driver.tick(2.0);
131/// assert!(!driver.is_active(id));
132/// assert_eq!(driver.active_count(), 0);
133/// ```
134#[derive(Debug, Default)]
135pub struct AnimationDriver {
136    slots: Vec<Slot>,
137    next_id: u64,
138    completed_count: usize,
139}
140
141impl AnimationDriver {
142    /// Create a new, empty driver.
143    pub fn new() -> Self {
144        Self {
145            slots: Vec::new(),
146            next_id: 0,
147            completed_count: 0,
148        }
149    }
150
151    /// Register an animation and return its [`AnimationId`].
152    ///
153    /// The animation will be ticked on every call to [`tick`](Self::tick)
154    /// and automatically removed when it returns `false` from `update`.
155    pub fn add<A: Update + Send + 'static>(&mut self, anim: A) -> AnimationId {
156        let id = AnimationId(self.next_id);
157        self.next_id += 1;
158        self.slots.push(Slot {
159            id,
160            label: None,
161            animation: SlotAnimation::Basic(Box::new(anim)),
162            remove: false,
163            #[cfg(feature = "std")]
164            recorder: None,
165        });
166        id
167    }
168
169    /// Register an inspectable animation with a user-facing label.
170    ///
171    /// Inspectable animations can be captured by [`snapshots`](Self::snapshots)
172    /// and [`snapshots_into`](Self::snapshots_into). Ordinary animations added
173    /// through [`add`](Self::add) continue to work but do not appear in
174    /// snapshots.
175    pub fn add_inspectable<A>(&mut self, label: impl Into<String>, anim: A) -> AnimationId
176    where
177        A: Inspectable + Send + 'static,
178    {
179        let id = AnimationId(self.next_id);
180        self.next_id += 1;
181        self.slots.push(Slot {
182            id,
183            label: Some(label.into()),
184            animation: SlotAnimation::Inspectable(Box::new(anim)),
185            remove: false,
186            #[cfg(feature = "std")]
187            recorder: None,
188        });
189        id
190    }
191
192    /// Register an animation with a scalar sampler for [`tick_recorded`](Self::tick_recorded).
193    #[cfg(feature = "std")]
194    pub fn add_recorded<A, F>(
195        &mut self,
196        label: impl Into<String>,
197        anim: A,
198        sampler: F,
199    ) -> AnimationId
200    where
201        A: Update + Send + 'static,
202        F: Fn() -> f64 + Send + 'static,
203    {
204        let label = label.into();
205        let id = AnimationId(self.next_id);
206        self.next_id += 1;
207        self.slots.push(Slot {
208            id,
209            label: Some(label.clone()),
210            animation: SlotAnimation::Basic(Box::new(anim)),
211            remove: false,
212            recorder: Some(RecordedSampler {
213                label,
214                sample: Box::new(sampler),
215            }),
216        });
217        id
218    }
219
220    /// Advance all active animations by `dt` seconds.
221    ///
222    /// Animations that return `false` from `update` are marked and removed
223    /// at the end of the tick — no allocation during the tick itself.
224    pub fn tick(&mut self, dt: f32) {
225        for slot in self.slots.iter_mut() {
226            if slot.remove {
227                continue;
228            }
229            let still_running = slot.animation.update(dt);
230            if !still_running {
231                slot.remove = true;
232            }
233        }
234        self.drain_completed();
235    }
236
237    /// Advance all active animations and record sampled values after the tick.
238    #[cfg(feature = "std")]
239    pub fn tick_recorded(&mut self, dt: f32, time: f32, recorder: &mut AnimationRecorder) {
240        for slot in self.slots.iter_mut() {
241            if slot.remove {
242                continue;
243            }
244            let still_running = slot.animation.update(dt);
245            if let Some(recorded) = &slot.recorder {
246                recorder.record(&recorded.label, time, (recorded.sample)());
247            }
248            if !still_running {
249                slot.remove = true;
250            }
251        }
252        self.drain_completed();
253    }
254
255    /// Advance all active animations and return wall-clock update costs.
256    pub fn tick_profiled(&mut self, dt: f32) -> DriverFrameProfile {
257        let dt = dt.max(0.0);
258        let frame_start = std::time::Instant::now();
259        let mut animation_costs = Vec::with_capacity(self.slots.len());
260
261        for slot in self.slots.iter_mut() {
262            if slot.remove {
263                continue;
264            }
265            let animation_start = std::time::Instant::now();
266            let still_running = slot.animation.update(dt);
267            let update_time_ms = animation_start.elapsed().as_secs_f32() * 1000.0;
268            animation_costs.push(AnimationUpdateCost {
269                id: slot.id,
270                label: slot.label.clone(),
271                update_time_ms,
272            });
273            if !still_running {
274                slot.remove = true;
275            }
276        }
277
278        self.drain_completed();
279        DriverFrameProfile {
280            dt,
281            total_update_time_ms: frame_start.elapsed().as_secs_f32() * 1000.0,
282            animation_costs,
283        }
284    }
285
286    /// Cancel an animation by id.
287    ///
288    /// The animation is removed immediately, before the next tick.
289    /// No-op if the id is not found (e.g. already completed).
290    pub fn cancel(&mut self, id: AnimationId) {
291        self.slots.retain(|s| s.id != id);
292    }
293
294    /// Cancel all active animations.
295    pub fn cancel_all(&mut self) {
296        self.slots.clear();
297    }
298
299    /// Number of currently active animations.
300    pub fn active_count(&self) -> usize {
301        self.slots.len()
302    }
303
304    /// Number of animations that completed naturally during driver ticks.
305    pub fn completed_count(&self) -> usize {
306        self.completed_count
307    }
308
309    /// Return snapshots for all active inspectable animations.
310    pub fn snapshots(&self) -> Vec<DriverSnapshot> {
311        let mut snapshots = Vec::new();
312        self.snapshots_into(&mut snapshots);
313        snapshots
314    }
315
316    /// Write snapshots for all active inspectable animations into a reusable buffer.
317    pub fn snapshots_into(&self, out: &mut Vec<DriverSnapshot>) {
318        out.clear();
319        out.extend(self.slots.iter().filter_map(|slot| {
320            slot.animation
321                .introspect()
322                .map(|introspection| DriverSnapshot {
323                    id: slot.id,
324                    label: slot.label.clone(),
325                    introspection,
326                })
327        }));
328    }
329
330    /// `true` if the animation with the given id is still active.
331    pub fn is_active(&self, id: AnimationId) -> bool {
332        self.slots.iter().any(|s| s.id == id)
333    }
334
335    fn drain_completed(&mut self) {
336        let completed = self.slots.iter().filter(|slot| slot.remove).count();
337        self.completed_count = self.completed_count.saturating_add(completed);
338        self.slots.retain(|slot| !slot.remove);
339    }
340}
341
342// ──────────────────────────────────────────────────────────────────────────────
343// Tests
344// ──────────────────────────────────────────────────────────────────────────────
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use animato_core::Easing;
350    use animato_tween::Tween;
351
352    #[test]
353    fn auto_removes_completed() {
354        let mut driver = AnimationDriver::new();
355        let id = driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build());
356        assert!(driver.is_active(id));
357        driver.tick(2.0); // well past duration
358        assert!(!driver.is_active(id));
359        assert_eq!(driver.active_count(), 0);
360    }
361
362    #[test]
363    fn cancel_removes_mid_animation() {
364        let mut driver = AnimationDriver::new();
365        let id = driver.add(Tween::new(0.0_f32, 1.0).duration(10.0).build());
366        driver.tick(1.0);
367        assert!(driver.is_active(id));
368        driver.cancel(id);
369        assert!(!driver.is_active(id));
370    }
371
372    #[test]
373    fn cancel_noop_on_missing_id() {
374        let mut driver = AnimationDriver::new();
375        let id = driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build());
376        driver.tick(2.0); // completes, auto-removed
377        driver.cancel(id); // should not panic
378        assert_eq!(driver.active_count(), 0);
379    }
380
381    #[test]
382    fn active_count_tracks_correctly() {
383        let mut driver = AnimationDriver::new();
384        let _a = driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build());
385        let _b = driver.add(Tween::new(0.0_f32, 1.0).duration(2.0).build());
386        let _c = driver.add(Tween::new(0.0_f32, 1.0).duration(3.0).build());
387        assert_eq!(driver.active_count(), 3);
388
389        driver.tick(1.5); // first one completes
390        assert_eq!(driver.active_count(), 2);
391
392        driver.tick(1.0); // second completes
393        assert_eq!(driver.active_count(), 1);
394    }
395
396    #[test]
397    fn cancel_all_clears_everything() {
398        let mut driver = AnimationDriver::new();
399        for _ in 0..10 {
400            driver.add(Tween::new(0.0_f32, 1.0).duration(5.0).build());
401        }
402        assert_eq!(driver.active_count(), 10);
403        driver.cancel_all();
404        assert_eq!(driver.active_count(), 0);
405    }
406
407    #[test]
408    fn multiple_concurrent_animations_tick_independently() {
409        let mut driver = AnimationDriver::new();
410        let slow = driver.add(Tween::new(0.0_f32, 1.0).duration(2.0).build());
411        let fast = driver.add(
412            Tween::new(0.0_f32, 1.0)
413                .duration(0.5)
414                .easing(Easing::Linear)
415                .build(),
416        );
417
418        driver.tick(1.0); // fast completes, slow still running
419        assert!(!driver.is_active(fast));
420        assert!(driver.is_active(slow));
421
422        driver.tick(2.0); // slow completes
423        assert!(!driver.is_active(slow));
424    }
425
426    #[test]
427    fn animation_id_is_unique() {
428        let mut driver = AnimationDriver::new();
429        let ids: Vec<_> = (0..100)
430            .map(|_| driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build()))
431            .collect();
432        let unique: std::collections::HashSet<_> = ids.iter().collect();
433        assert_eq!(unique.len(), 100);
434    }
435}