Skip to main content

animato_timeline/
timeline.rs

1//! Timeline composition primitives.
2
3use alloc::boxed::Box;
4use alloc::string::String;
5use alloc::vec::Vec;
6use animato_core::{Playable, Update};
7use animato_tween::Loop;
8use core::fmt;
9
10/// Positioning rule for a timeline entry.
11#[derive(Clone, Copy, Debug, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum At<'a> {
14    /// Start at an explicit absolute time in seconds.
15    Absolute(f32),
16    /// Start at timeline time `0.0`.
17    Start,
18    /// Start when the current last entry ends.
19    End,
20    /// Start at the same time as an existing labeled entry.
21    Label(&'a str),
22    /// Start relative to the current timeline end.
23    Offset(f32),
24}
25
26/// Current playback state of a [`Timeline`].
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum TimelineState {
30    /// Ready but not advancing.
31    Idle,
32    /// Actively advancing.
33    Playing,
34    /// Paused mid-playback.
35    Paused,
36    /// Finished all finite playback.
37    Completed,
38}
39
40struct TimelineEntry {
41    label: String,
42    animation: Box<dyn Playable + Send>,
43    start_at: f32,
44    duration: f32,
45    completed: bool,
46}
47
48impl fmt::Debug for TimelineEntry {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.debug_struct("TimelineEntry")
51            .field("label", &self.label)
52            .field("start_at", &self.start_at)
53            .field("duration", &self.duration)
54            .field("completed", &self.completed)
55            .finish()
56    }
57}
58
59impl TimelineEntry {
60    fn end_at(&self) -> f32 {
61        self.start_at + self.duration
62    }
63}
64
65#[cfg(feature = "std")]
66struct EntryCallback {
67    label: String,
68    callback: Box<dyn FnMut() + Send + 'static>,
69}
70
71#[cfg(feature = "std")]
72impl fmt::Debug for EntryCallback {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.debug_struct("EntryCallback")
75            .field("label", &self.label)
76            .finish()
77    }
78}
79
80#[cfg(feature = "std")]
81#[derive(Default)]
82struct TimelineCallbacks {
83    entry_complete: Vec<EntryCallback>,
84    complete: Option<Box<dyn FnMut() + Send + 'static>>,
85    complete_fired: bool,
86}
87
88#[cfg(feature = "std")]
89impl fmt::Debug for TimelineCallbacks {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        f.debug_struct("TimelineCallbacks")
92            .field("entry_complete", &self.entry_complete)
93            .field("has_complete", &self.complete.is_some())
94            .field("complete_fired", &self.complete_fired)
95            .finish()
96    }
97}
98
99#[cfg(feature = "std")]
100impl TimelineCallbacks {
101    fn fire_entry_complete(&mut self, completed_labels: &[String]) {
102        for completed_label in completed_labels {
103            for callback in self.entry_complete.iter_mut() {
104                if callback.label == *completed_label {
105                    (callback.callback)();
106                }
107            }
108        }
109    }
110
111    fn fire_complete(&mut self) {
112        if self.complete_fired {
113            return;
114        }
115        self.complete_fired = true;
116        if let Some(callback) = self.complete.as_mut() {
117            callback();
118        }
119    }
120
121    fn reset_completion(&mut self) {
122        self.complete_fired = false;
123    }
124}
125
126/// Composes multiple animations on one shared clock.
127///
128/// Entries are stored by label, absolute start time, and cached duration.
129/// Normal one-shot playback advances children incrementally. Seeking and
130/// timeline-level loops resynchronize children through [`Playable::seek_to`].
131pub struct Timeline {
132    entries: Vec<TimelineEntry>,
133    elapsed: f32,
134    state: TimelineState,
135    /// Timeline-level looping behavior.
136    pub looping: Loop,
137    /// Timeline time scale. `1.0` = normal speed, `2.0` = double speed.
138    pub time_scale: f32,
139    #[cfg(feature = "std")]
140    callbacks: TimelineCallbacks,
141    #[cfg(feature = "tokio")]
142    completion_tx: tokio::sync::watch::Sender<bool>,
143}
144
145impl fmt::Debug for Timeline {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        f.debug_struct("Timeline")
148            .field("entries", &self.entries)
149            .field("elapsed", &self.elapsed)
150            .field("state", &self.state)
151            .field("looping", &self.looping)
152            .field("time_scale", &self.time_scale)
153            .field("callbacks", &{
154                #[cfg(feature = "std")]
155                {
156                    &self.callbacks
157                }
158                #[cfg(not(feature = "std"))]
159                {
160                    &"disabled"
161                }
162            })
163            .finish()
164    }
165}
166
167impl Default for Timeline {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl Timeline {
174    /// Create an empty timeline.
175    pub fn new() -> Self {
176        Self {
177            entries: Vec::new(),
178            elapsed: 0.0,
179            state: TimelineState::Idle,
180            looping: Loop::Once,
181            time_scale: 1.0,
182            #[cfg(feature = "std")]
183            callbacks: TimelineCallbacks::default(),
184            #[cfg(feature = "tokio")]
185            completion_tx: tokio::sync::watch::channel(false).0,
186        }
187    }
188
189    /// Add an animation at the requested position.
190    ///
191    /// Missing [`At::Label`] references fall back to [`At::End`] behavior.
192    pub fn add<A>(mut self, label: impl Into<String>, animation: A, at: At<'_>) -> Self
193    where
194        A: Playable + Send + 'static,
195    {
196        let start_at = self.resolve_start(at);
197        let duration = animation.duration().max(0.0);
198        self.entries.push(TimelineEntry {
199            label: label.into(),
200            animation: Box::new(animation),
201            start_at,
202            duration,
203            completed: false,
204        });
205        self
206    }
207
208    pub(crate) fn add_boxed_with_duration(
209        mut self,
210        label: impl Into<String>,
211        animation: Box<dyn Playable + Send>,
212        at: At<'_>,
213        duration: f32,
214    ) -> Self {
215        let start_at = self.resolve_start(at);
216        self.entries.push(TimelineEntry {
217            label: label.into(),
218            animation,
219            start_at,
220            duration: duration.max(0.0),
221            completed: false,
222        });
223        self
224    }
225
226    /// Set timeline-level looping behavior.
227    pub fn looping(mut self, mode: Loop) -> Self {
228        self.looping = mode;
229        self
230    }
231
232    /// Set the timeline time scale.
233    ///
234    /// Negative values are clamped to `0.0`.
235    pub fn time_scale(mut self, scale: f32) -> Self {
236        self.set_time_scale(scale);
237        self
238    }
239
240    /// Change the timeline time scale at runtime.
241    ///
242    /// `1.0` is normal speed, `2.0` is double speed, and `0.0` freezes time.
243    pub fn set_time_scale(&mut self, scale: f32) {
244        self.time_scale = scale.max(0.0);
245    }
246
247    /// Register a callback fired when the labeled entry completes during `update`.
248    ///
249    /// This is available with the `std` feature. Seeking and resetting do not
250    /// fire callbacks.
251    #[cfg(feature = "std")]
252    pub fn on_entry_complete(
253        mut self,
254        label: impl Into<String>,
255        f: impl FnMut() + Send + 'static,
256    ) -> Self {
257        self.callbacks.entry_complete.push(EntryCallback {
258            label: label.into(),
259            callback: Box::new(f),
260        });
261        self
262    }
263
264    /// Register a callback fired once when finite timeline playback completes during `update`.
265    ///
266    /// This is available with the `std` feature. Seeking to the end does not
267    /// fire this callback.
268    #[cfg(feature = "std")]
269    pub fn on_complete(mut self, f: impl FnMut() + Send + 'static) -> Self {
270        self.callbacks.complete = Some(Box::new(f));
271        self
272    }
273
274    /// Return a future that resolves when the timeline reaches `Completed`.
275    ///
276    /// The future only observes completion; it does not drive the timeline.
277    /// Continue calling [`update`](Update::update) from your runtime loop.
278    #[cfg(feature = "tokio")]
279    pub fn wait(&self) -> impl core::future::Future<Output = ()> + Send + 'static {
280        let mut rx = self.completion_tx.subscribe();
281        async move {
282            loop {
283                if *rx.borrow() {
284                    return;
285                }
286                if rx.changed().await.is_err() {
287                    return;
288                }
289            }
290        }
291    }
292
293    /// Begin playback.
294    pub fn play(&mut self) {
295        if self.state == TimelineState::Completed {
296            self.reset();
297        }
298        if self.duration() == 0.0 {
299            self.state = TimelineState::Completed;
300            self.notify_completion_state(true);
301        } else {
302            self.state = TimelineState::Playing;
303            self.notify_completion_state(false);
304            self.sync_to_elapsed();
305        }
306    }
307
308    /// Pause playback.
309    pub fn pause(&mut self) {
310        if self.state == TimelineState::Playing {
311            self.state = TimelineState::Paused;
312        }
313    }
314
315    /// Resume playback after a pause.
316    pub fn resume(&mut self) {
317        if self.state == TimelineState::Paused {
318            self.state = TimelineState::Playing;
319        }
320    }
321
322    /// Reset the timeline and all children to the beginning.
323    pub fn reset(&mut self) {
324        self.elapsed = 0.0;
325        self.state = TimelineState::Idle;
326        self.reset_completion_callbacks();
327        self.notify_completion_state(false);
328        for entry in self.entries.iter_mut() {
329            entry.animation.reset();
330            entry.completed = false;
331        }
332    }
333
334    /// Seek by normalized progress through the timeline.
335    pub fn seek(&mut self, progress: f32) {
336        let total = self.playback_duration();
337        let seek_duration = if total.is_finite() {
338            total
339        } else {
340            self.duration()
341        };
342        self.seek_abs(seek_duration * progress.clamp(0.0, 1.0));
343    }
344
345    /// Seek to an absolute time in seconds.
346    pub fn seek_abs(&mut self, secs: f32) {
347        let total = self.playback_duration();
348        let secs = secs.max(0.0);
349        self.elapsed = if total.is_finite() {
350            secs.min(total)
351        } else {
352            secs
353        };
354        self.sync_to_elapsed();
355        if total.is_finite() && self.elapsed >= total {
356            self.state = TimelineState::Completed;
357            self.notify_completion_state(true);
358        } else if self.state == TimelineState::Completed {
359            self.state = TimelineState::Playing;
360            self.notify_completion_state(false);
361        }
362    }
363
364    /// Base duration in seconds, equal to the last finishing entry.
365    pub fn duration(&self) -> f32 {
366        self.entries
367            .iter()
368            .map(TimelineEntry::end_at)
369            .fold(0.0, f32::max)
370    }
371
372    /// Current normalized progress through finite playback.
373    pub fn progress(&self) -> f32 {
374        let total = self.playback_duration();
375        if total == 0.0 {
376            return 1.0;
377        }
378        if total.is_finite() {
379            (self.elapsed / total).clamp(0.0, 1.0)
380        } else {
381            let base = self.duration();
382            if base == 0.0 {
383                1.0
384            } else {
385                (self.local_time_for_elapsed(self.elapsed) / base).clamp(0.0, 1.0)
386            }
387        }
388    }
389
390    /// `true` when the timeline has finished all finite playback.
391    pub fn is_complete(&self) -> bool {
392        self.state == TimelineState::Completed
393    }
394
395    /// Current timeline state.
396    pub fn state(&self) -> TimelineState {
397        self.state
398    }
399
400    /// Current total elapsed timeline time in seconds.
401    pub fn elapsed(&self) -> f32 {
402        self.elapsed
403    }
404
405    /// Number of entries in the timeline.
406    pub fn entry_count(&self) -> usize {
407        self.entries.len()
408    }
409
410    /// Find a child animation by label and concrete type.
411    pub fn get<T>(&self, label: &str) -> Option<&T>
412    where
413        T: Playable + 'static,
414    {
415        self.entries
416            .iter()
417            .find(|entry| entry.label == label)
418            .and_then(|entry| entry.animation.as_any().downcast_ref::<T>())
419    }
420
421    /// Find a mutable child animation by label and concrete type.
422    pub fn get_mut<T>(&mut self, label: &str) -> Option<&mut T>
423    where
424        T: Playable + 'static,
425    {
426        self.entries
427            .iter_mut()
428            .find(|entry| entry.label == label)
429            .and_then(|entry| entry.animation.as_any_mut().downcast_mut::<T>())
430    }
431
432    fn fire_entry_callbacks(&mut self, completed_labels: &[String]) {
433        #[cfg(feature = "std")]
434        self.callbacks.fire_entry_complete(completed_labels);
435
436        #[cfg(not(feature = "std"))]
437        let _ = completed_labels;
438    }
439
440    fn fire_complete_callback(&mut self) {
441        #[cfg(feature = "std")]
442        self.callbacks.fire_complete();
443    }
444
445    fn reset_completion_callbacks(&mut self) {
446        #[cfg(feature = "std")]
447        self.callbacks.reset_completion();
448    }
449
450    fn notify_completion_state(&self, complete: bool) {
451        #[cfg(feature = "tokio")]
452        let _ = self.completion_tx.send_replace(complete);
453
454        #[cfg(not(feature = "tokio"))]
455        let _ = complete;
456    }
457
458    fn complete_from_update(&mut self) -> bool {
459        self.state = TimelineState::Completed;
460        self.fire_complete_callback();
461        self.notify_completion_state(true);
462        false
463    }
464
465    fn resolve_start(&self, at: At<'_>) -> f32 {
466        match at {
467            At::Absolute(secs) => secs.max(0.0),
468            At::Start => 0.0,
469            At::End => self.duration(),
470            At::Label(label) => self
471                .entries
472                .iter()
473                .find(|entry| entry.label == label)
474                .map_or_else(|| self.duration(), |entry| entry.start_at),
475            At::Offset(offset) => (self.duration() + offset).max(0.0),
476        }
477    }
478
479    fn playback_duration(&self) -> f32 {
480        let base = self.duration();
481        if base == 0.0 {
482            return 0.0;
483        }
484        match self.looping {
485            Loop::Once => base,
486            Loop::Times(n) => base * n.max(1) as f32,
487            Loop::Forever | Loop::PingPong => f32::INFINITY,
488        }
489    }
490
491    fn local_time_for_elapsed(&self, elapsed: f32) -> f32 {
492        let base = self.duration();
493        if base == 0.0 {
494            return 0.0;
495        }
496
497        match self.looping {
498            Loop::Once => elapsed.min(base),
499            Loop::Times(n) => {
500                let total = base * n.max(1) as f32;
501                if elapsed >= total {
502                    base
503                } else {
504                    elapsed % base
505                }
506            }
507            Loop::Forever => elapsed % base,
508            Loop::PingPong => {
509                let cycle = elapsed % (base * 2.0);
510                if cycle <= base {
511                    cycle
512                } else {
513                    base * 2.0 - cycle
514                }
515            }
516        }
517    }
518
519    fn entry_completion_labels_between(&self, prev: f32, next: f32, base: f32) -> Vec<String> {
520        let mut labels = Vec::new();
521        if next <= prev || base <= 0.0 {
522            return labels;
523        }
524
525        let (max_cycles, period) = match self.looping {
526            Loop::Once => (Some(1), base),
527            Loop::Times(n) => (Some(n.max(1)), base),
528            Loop::Forever => (None, base),
529            Loop::PingPong => (None, base * 2.0),
530        };
531
532        if period <= 0.0 {
533            return labels;
534        }
535
536        let mut cycle = (prev / period).max(0.0) as u32;
537        loop {
538            if let Some(max_cycles) = max_cycles {
539                if cycle >= max_cycles {
540                    break;
541                }
542            }
543
544            let cycle_start = cycle as f32 * period;
545            if cycle_start > next {
546                break;
547            }
548
549            for entry in self.entries.iter() {
550                let completion = cycle_start + entry.end_at();
551                if prev < completion && completion <= next {
552                    labels.push(entry.label.clone());
553                }
554            }
555
556            cycle = cycle.saturating_add(1);
557            if cycle == u32::MAX {
558                break;
559            }
560        }
561
562        labels
563    }
564
565    fn tick_forward(&mut self, prev: f32, next: f32) -> Vec<String> {
566        let mut completed_labels = Vec::new();
567        for entry in self.entries.iter_mut() {
568            let start = entry.start_at;
569            let end = entry.end_at();
570            let was_completed = entry.completed;
571
572            if next < start {
573                entry.animation.reset();
574                entry.completed = false;
575                continue;
576            }
577
578            if prev <= start && next >= start {
579                entry.animation.reset();
580                entry.completed = false;
581            }
582
583            if entry.duration == 0.0 {
584                if next >= start {
585                    entry.animation.seek_to(1.0);
586                    entry.completed = true;
587                }
588                if !was_completed && entry.completed {
589                    completed_labels.push(entry.label.clone());
590                }
591                continue;
592            }
593
594            let overlap_start = prev.max(start);
595            let overlap_end = next.min(end);
596            if overlap_end > overlap_start {
597                let still_running = entry.animation.update(overlap_end - overlap_start);
598                if !still_running {
599                    entry.completed = true;
600                }
601            }
602
603            if next >= end {
604                entry.animation.seek_to(1.0);
605                entry.completed = true;
606            }
607
608            if !was_completed && entry.completed {
609                completed_labels.push(entry.label.clone());
610            }
611        }
612        completed_labels
613    }
614
615    fn sync_to_elapsed(&mut self) {
616        let local_time = self.local_time_for_elapsed(self.elapsed);
617        for entry in self.entries.iter_mut() {
618            let start = entry.start_at;
619            let end = entry.end_at();
620
621            if local_time <= start {
622                entry.animation.reset();
623                entry.completed = false;
624            } else if local_time >= end || entry.duration == 0.0 {
625                entry.animation.seek_to(1.0);
626                entry.completed = true;
627            } else {
628                let progress = (local_time - start) / entry.duration;
629                entry.animation.seek_to(progress);
630                entry.completed = false;
631            }
632        }
633    }
634}
635
636impl Update for Timeline {
637    fn update(&mut self, dt: f32) -> bool {
638        match self.state {
639            TimelineState::Completed => return false,
640            TimelineState::Paused | TimelineState::Idle => return true,
641            TimelineState::Playing => {}
642        }
643
644        let base = self.duration();
645        if base == 0.0 {
646            return self.complete_from_update();
647        }
648
649        let dt = dt.max(0.0) * self.time_scale;
650        let previous_elapsed = self.elapsed;
651        let next_elapsed = previous_elapsed + dt;
652
653        match self.looping {
654            Loop::Once => {
655                let prev_local = previous_elapsed.min(base);
656                let next_local = next_elapsed.min(base);
657                let completed_labels = self.tick_forward(prev_local, next_local);
658                self.fire_entry_callbacks(&completed_labels);
659                self.elapsed = next_elapsed.min(base);
660                if next_elapsed >= base {
661                    return self.complete_from_update();
662                }
663            }
664            Loop::Times(n) => {
665                let total = base * n.max(1) as f32;
666                let completed_labels =
667                    self.entry_completion_labels_between(previous_elapsed, next_elapsed, base);
668                self.elapsed = next_elapsed.min(total);
669                self.sync_to_elapsed();
670                self.fire_entry_callbacks(&completed_labels);
671                if next_elapsed >= total {
672                    return self.complete_from_update();
673                }
674            }
675            Loop::Forever | Loop::PingPong => {
676                let completed_labels =
677                    self.entry_completion_labels_between(previous_elapsed, next_elapsed, base);
678                self.elapsed = next_elapsed;
679                self.sync_to_elapsed();
680                self.fire_entry_callbacks(&completed_labels);
681            }
682        }
683
684        true
685    }
686}
687
688impl Playable for Timeline {
689    fn duration(&self) -> f32 {
690        self.playback_duration()
691    }
692
693    fn reset(&mut self) {
694        Timeline::reset(self);
695    }
696
697    fn seek_to(&mut self, progress: f32) {
698        Timeline::seek(self, progress);
699    }
700
701    fn is_complete(&self) -> bool {
702        Timeline::is_complete(self)
703    }
704
705    fn as_any(&self) -> &dyn core::any::Any {
706        self
707    }
708
709    fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
710        self
711    }
712}
713
714#[cfg(test)]
715mod tests {
716    use super::*;
717    use animato_core::Easing;
718    use animato_tween::Tween;
719
720    fn tween(end: f32, duration: f32) -> Tween<f32> {
721        Tween::new(0.0_f32, end)
722            .duration(duration)
723            .easing(Easing::Linear)
724            .build()
725    }
726
727    #[test]
728    fn concurrent_entries_advance_together() {
729        let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start).add(
730            "b",
731            tween(100.0, 1.0),
732            At::Label("a"),
733        );
734
735        timeline.play();
736        timeline.update(0.5);
737
738        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.5);
739        assert_eq!(timeline.get::<Tween<f32>>("b").unwrap().value(), 50.0);
740    }
741
742    #[test]
743    fn end_and_offset_position_entries() {
744        let timeline = Timeline::new()
745            .add("first", tween(1.0, 1.0), At::Start)
746            .add("second", tween(1.0, 0.5), At::End)
747            .add("third", tween(1.0, 0.25), At::Offset(0.25));
748
749        assert_eq!(timeline.duration(), 2.0);
750    }
751
752    #[test]
753    fn seek_abs_synchronizes_children() {
754        let mut timeline = Timeline::new().add("a", tween(100.0, 2.0), At::Start);
755
756        timeline.seek_abs(0.5);
757
758        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
759    }
760
761    #[test]
762    fn pause_stops_timeline_progress() {
763        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
764        timeline.play();
765        timeline.update(0.25);
766        timeline.pause();
767        timeline.update(0.5);
768
769        assert_eq!(timeline.elapsed(), 0.25);
770        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
771    }
772
773    #[test]
774    fn resume_continues_after_pause() {
775        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
776        timeline.play();
777        timeline.update(0.25);
778        timeline.pause();
779        timeline.resume();
780        timeline.update(0.25);
781
782        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 50.0);
783    }
784
785    #[test]
786    fn once_timeline_completes() {
787        let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start);
788        timeline.play();
789
790        assert!(!timeline.update(1.0));
791        assert!(timeline.is_complete());
792    }
793
794    #[test]
795    fn times_loop_repeats_then_completes() {
796        let mut timeline = Timeline::new()
797            .add("a", tween(100.0, 1.0), At::Start)
798            .looping(Loop::Times(2));
799        timeline.play();
800
801        timeline.update(1.25);
802        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
803
804        assert!(!timeline.update(1.0));
805        assert!(timeline.is_complete());
806        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 100.0);
807    }
808
809    #[test]
810    fn ping_pong_reflects_timeline_time() {
811        let mut timeline = Timeline::new()
812            .add("a", tween(100.0, 1.0), At::Start)
813            .looping(Loop::PingPong);
814        timeline.play();
815        timeline.update(1.25);
816
817        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 75.0);
818        assert!(!timeline.is_complete());
819    }
820
821    #[test]
822    fn time_scale_speeds_up_timeline() {
823        let mut timeline = Timeline::new()
824            .add("a", tween(100.0, 1.0), At::Start)
825            .time_scale(2.0);
826        timeline.play();
827        timeline.update(0.25);
828
829        assert_eq!(timeline.elapsed(), 0.5);
830        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 50.0);
831    }
832
833    #[test]
834    fn set_time_scale_clamps_negative_to_zero() {
835        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
836        timeline.set_time_scale(-1.0);
837        timeline.play();
838        timeline.update(0.5);
839
840        assert_eq!(timeline.elapsed(), 0.0);
841        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.0);
842    }
843
844    #[cfg(feature = "std")]
845    #[test]
846    fn callbacks_fire_once_during_update() {
847        use std::sync::Arc;
848        use std::sync::atomic::{AtomicUsize, Ordering};
849
850        let entry_count = Arc::new(AtomicUsize::new(0));
851        let complete_count = Arc::new(AtomicUsize::new(0));
852        let entry_seen = Arc::clone(&entry_count);
853        let complete_seen = Arc::clone(&complete_count);
854
855        let mut timeline = Timeline::new()
856            .add("a", tween(100.0, 1.0), At::Start)
857            .on_entry_complete("a", move || {
858                entry_seen.fetch_add(1, Ordering::SeqCst);
859            })
860            .on_complete(move || {
861                complete_seen.fetch_add(1, Ordering::SeqCst);
862            });
863
864        timeline.play();
865        assert!(!timeline.update(1.0));
866        assert!(!timeline.update(1.0));
867
868        assert_eq!(entry_count.load(Ordering::SeqCst), 1);
869        assert_eq!(complete_count.load(Ordering::SeqCst), 1);
870    }
871
872    #[cfg(feature = "std")]
873    #[test]
874    fn callbacks_do_not_fire_on_seek_or_reset() {
875        use std::sync::Arc;
876        use std::sync::atomic::{AtomicUsize, Ordering};
877
878        let entry_count = Arc::new(AtomicUsize::new(0));
879        let complete_count = Arc::new(AtomicUsize::new(0));
880        let entry_seen = Arc::clone(&entry_count);
881        let complete_seen = Arc::clone(&complete_count);
882
883        let mut timeline = Timeline::new()
884            .add("a", tween(100.0, 1.0), At::Start)
885            .on_entry_complete("a", move || {
886                entry_seen.fetch_add(1, Ordering::SeqCst);
887            })
888            .on_complete(move || {
889                complete_seen.fetch_add(1, Ordering::SeqCst);
890            });
891
892        timeline.seek(1.0);
893        timeline.reset();
894
895        assert_eq!(entry_count.load(Ordering::SeqCst), 0);
896        assert_eq!(complete_count.load(Ordering::SeqCst), 0);
897    }
898
899    #[cfg(feature = "tokio")]
900    #[test]
901    fn wait_is_ready_after_completion() {
902        use core::future::Future;
903        use std::sync::Arc;
904        use std::task::{Context, Poll, Wake, Waker};
905
906        struct NoopWaker;
907        impl Wake for NoopWaker {
908            fn wake(self: Arc<Self>) {}
909        }
910
911        let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start);
912        timeline.play();
913        timeline.update(1.0);
914
915        let mut wait = Box::pin(timeline.wait());
916        let waker = Waker::from(Arc::new(NoopWaker));
917        let mut cx = Context::from_waker(&waker);
918
919        assert!(matches!(wait.as_mut().poll(&mut cx), Poll::Ready(())));
920    }
921}