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    /// Add another timeline as a nested child entry.
209    pub fn add_timeline(self, label: impl Into<String>, timeline: Timeline, at: At<'_>) -> Self {
210        self.add(label, timeline, at)
211    }
212
213    pub(crate) fn add_boxed_with_duration(
214        mut self,
215        label: impl Into<String>,
216        animation: Box<dyn Playable + Send>,
217        at: At<'_>,
218        duration: f32,
219    ) -> Self {
220        let start_at = self.resolve_start(at);
221        self.entries.push(TimelineEntry {
222            label: label.into(),
223            animation,
224            start_at,
225            duration: duration.max(0.0),
226            completed: false,
227        });
228        self
229    }
230
231    /// Set timeline-level looping behavior.
232    pub fn looping(mut self, mode: Loop) -> Self {
233        self.looping = mode;
234        self
235    }
236
237    /// Set the timeline time scale.
238    ///
239    /// Negative values are clamped to `0.0`.
240    pub fn time_scale(mut self, scale: f32) -> Self {
241        self.set_time_scale(scale);
242        self
243    }
244
245    /// Change the timeline time scale at runtime.
246    ///
247    /// `1.0` is normal speed, `2.0` is double speed, and `0.0` freezes time.
248    pub fn set_time_scale(&mut self, scale: f32) {
249        self.time_scale = scale.max(0.0);
250    }
251
252    /// Register a callback fired when the labeled entry completes during `update`.
253    ///
254    /// This is available with the `std` feature. Seeking and resetting do not
255    /// fire callbacks.
256    #[cfg(feature = "std")]
257    pub fn on_entry_complete(
258        mut self,
259        label: impl Into<String>,
260        f: impl FnMut() + Send + 'static,
261    ) -> Self {
262        self.callbacks.entry_complete.push(EntryCallback {
263            label: label.into(),
264            callback: Box::new(f),
265        });
266        self
267    }
268
269    /// Register a callback fired once when finite timeline playback completes during `update`.
270    ///
271    /// This is available with the `std` feature. Seeking to the end does not
272    /// fire this callback.
273    #[cfg(feature = "std")]
274    pub fn on_complete(mut self, f: impl FnMut() + Send + 'static) -> Self {
275        self.callbacks.complete = Some(Box::new(f));
276        self
277    }
278
279    /// Return a future that resolves when the timeline reaches `Completed`.
280    ///
281    /// The future only observes completion; it does not drive the timeline.
282    /// Continue calling [`update`](Update::update) from your runtime loop.
283    #[cfg(feature = "tokio")]
284    pub fn wait(&self) -> impl core::future::Future<Output = ()> + Send + 'static {
285        let mut rx = self.completion_tx.subscribe();
286        async move {
287            loop {
288                if *rx.borrow() {
289                    return;
290                }
291                if rx.changed().await.is_err() {
292                    return;
293                }
294            }
295        }
296    }
297
298    /// Begin playback.
299    pub fn play(&mut self) {
300        if self.state == TimelineState::Completed {
301            self.reset();
302        }
303        if self.duration() == 0.0 {
304            self.state = TimelineState::Completed;
305            self.notify_completion_state(true);
306        } else {
307            self.state = TimelineState::Playing;
308            self.notify_completion_state(false);
309            self.sync_to_elapsed();
310        }
311    }
312
313    /// Pause playback.
314    pub fn pause(&mut self) {
315        if self.state == TimelineState::Playing {
316            self.state = TimelineState::Paused;
317        }
318    }
319
320    /// Resume playback after a pause.
321    pub fn resume(&mut self) {
322        if self.state == TimelineState::Paused {
323            self.state = TimelineState::Playing;
324        }
325    }
326
327    /// Reset the timeline and all children to the beginning.
328    pub fn reset(&mut self) {
329        self.elapsed = 0.0;
330        self.state = TimelineState::Idle;
331        self.reset_completion_callbacks();
332        self.notify_completion_state(false);
333        for entry in self.entries.iter_mut() {
334            entry.animation.reset();
335            entry.completed = false;
336        }
337    }
338
339    /// Seek by normalized progress through the timeline.
340    pub fn seek(&mut self, progress: f32) {
341        let total = self.playback_duration();
342        let seek_duration = if total.is_finite() {
343            total
344        } else {
345            self.duration()
346        };
347        self.seek_abs(seek_duration * progress.clamp(0.0, 1.0));
348    }
349
350    /// Seek to an absolute time in seconds.
351    pub fn seek_abs(&mut self, secs: f32) {
352        let total = self.playback_duration();
353        let secs = secs.max(0.0);
354        self.elapsed = if total.is_finite() {
355            secs.min(total)
356        } else {
357            secs
358        };
359        self.sync_to_elapsed();
360        if total.is_finite() && self.elapsed >= total {
361            self.state = TimelineState::Completed;
362            self.notify_completion_state(true);
363        } else if self.state == TimelineState::Completed {
364            self.state = TimelineState::Playing;
365            self.notify_completion_state(false);
366        }
367    }
368
369    /// Base duration in seconds, equal to the last finishing entry.
370    pub fn duration(&self) -> f32 {
371        self.entries
372            .iter()
373            .map(TimelineEntry::end_at)
374            .fold(0.0, f32::max)
375    }
376
377    /// Current normalized progress through finite playback.
378    pub fn progress(&self) -> f32 {
379        let total = self.playback_duration();
380        if total == 0.0 {
381            return 1.0;
382        }
383        if total.is_finite() {
384            (self.elapsed / total).clamp(0.0, 1.0)
385        } else {
386            let base = self.duration();
387            if base == 0.0 {
388                1.0
389            } else {
390                (self.local_time_for_elapsed(self.elapsed) / base).clamp(0.0, 1.0)
391            }
392        }
393    }
394
395    /// `true` when the timeline has finished all finite playback.
396    pub fn is_complete(&self) -> bool {
397        self.state == TimelineState::Completed
398    }
399
400    /// Current timeline state.
401    pub fn state(&self) -> TimelineState {
402        self.state
403    }
404
405    /// Current total elapsed timeline time in seconds.
406    pub fn elapsed(&self) -> f32 {
407        self.elapsed
408    }
409
410    /// Number of entries in the timeline.
411    pub fn entry_count(&self) -> usize {
412        self.entries.len()
413    }
414
415    /// Find a child animation by label and concrete type.
416    pub fn get<T>(&self, label: &str) -> Option<&T>
417    where
418        T: Playable + 'static,
419    {
420        self.entries
421            .iter()
422            .find(|entry| entry.label == label)
423            .and_then(|entry| entry.animation.as_any().downcast_ref::<T>())
424    }
425
426    /// Find a mutable child animation by label and concrete type.
427    pub fn get_mut<T>(&mut self, label: &str) -> Option<&mut T>
428    where
429        T: Playable + 'static,
430    {
431        self.entries
432            .iter_mut()
433            .find(|entry| entry.label == label)
434            .and_then(|entry| entry.animation.as_any_mut().downcast_mut::<T>())
435    }
436
437    fn fire_entry_callbacks(&mut self, completed_labels: &[String]) {
438        #[cfg(feature = "std")]
439        self.callbacks.fire_entry_complete(completed_labels);
440
441        #[cfg(not(feature = "std"))]
442        let _ = completed_labels;
443    }
444
445    fn fire_complete_callback(&mut self) {
446        #[cfg(feature = "std")]
447        self.callbacks.fire_complete();
448    }
449
450    fn reset_completion_callbacks(&mut self) {
451        #[cfg(feature = "std")]
452        self.callbacks.reset_completion();
453    }
454
455    fn notify_completion_state(&self, complete: bool) {
456        #[cfg(feature = "tokio")]
457        let _ = self.completion_tx.send_replace(complete);
458
459        #[cfg(not(feature = "tokio"))]
460        let _ = complete;
461    }
462
463    fn complete_from_update(&mut self) -> bool {
464        self.state = TimelineState::Completed;
465        self.fire_complete_callback();
466        self.notify_completion_state(true);
467        false
468    }
469
470    fn resolve_start(&self, at: At<'_>) -> f32 {
471        match at {
472            At::Absolute(secs) => secs.max(0.0),
473            At::Start => 0.0,
474            At::End => self.duration(),
475            At::Label(label) => self
476                .entries
477                .iter()
478                .find(|entry| entry.label == label)
479                .map_or_else(|| self.duration(), |entry| entry.start_at),
480            At::Offset(offset) => (self.duration() + offset).max(0.0),
481        }
482    }
483
484    fn playback_duration(&self) -> f32 {
485        let base = self.duration();
486        if base == 0.0 {
487            return 0.0;
488        }
489        match self.looping {
490            Loop::Once => base,
491            Loop::Times(n) => base * n.max(1) as f32,
492            Loop::PingPongTimes(n) => base * n.max(1) as f32,
493            Loop::Forever | Loop::PingPong => f32::INFINITY,
494        }
495    }
496
497    fn local_time_for_elapsed(&self, elapsed: f32) -> f32 {
498        let base = self.duration();
499        if base == 0.0 {
500            return 0.0;
501        }
502
503        match self.looping {
504            Loop::Once => elapsed.min(base),
505            Loop::Times(n) => {
506                let total = base * n.max(1) as f32;
507                if elapsed >= total {
508                    base
509                } else {
510                    elapsed % base
511                }
512            }
513            Loop::Forever => elapsed % base,
514            Loop::PingPong => {
515                let cycle = elapsed % (base * 2.0);
516                if cycle <= base {
517                    cycle
518                } else {
519                    base * 2.0 - cycle
520                }
521            }
522            Loop::PingPongTimes(_) => {
523                let cycle = elapsed % (base * 2.0);
524                if cycle <= base {
525                    cycle
526                } else {
527                    base * 2.0 - cycle
528                }
529            }
530        }
531    }
532
533    fn entry_completion_labels_between(&self, prev: f32, next: f32, base: f32) -> Vec<String> {
534        let mut labels = Vec::new();
535        if next <= prev || base <= 0.0 {
536            return labels;
537        }
538
539        let (max_cycles, period) = match self.looping {
540            Loop::Once => (Some(1), base),
541            Loop::Times(n) => (Some(n.max(1)), base),
542            Loop::Forever => (None, base),
543            Loop::PingPong => (None, base * 2.0),
544            Loop::PingPongTimes(n) => {
545                let passes = n.max(1);
546                (Some(passes / 2 + passes % 2), base * 2.0)
547            }
548        };
549
550        if period <= 0.0 {
551            return labels;
552        }
553
554        let mut cycle = (prev / period).max(0.0) as u32;
555        loop {
556            if let Some(max_cycles) = max_cycles
557                && cycle >= max_cycles
558            {
559                break;
560            }
561
562            let cycle_start = cycle as f32 * period;
563            if cycle_start > next {
564                break;
565            }
566
567            for entry in self.entries.iter() {
568                let completion = cycle_start + entry.end_at();
569                if prev < completion && completion <= next {
570                    labels.push(entry.label.clone());
571                }
572            }
573
574            cycle = cycle.saturating_add(1);
575            if cycle == u32::MAX {
576                break;
577            }
578        }
579
580        labels
581    }
582
583    fn tick_forward(&mut self, prev: f32, next: f32) -> Vec<String> {
584        let mut completed_labels = Vec::new();
585        for entry in self.entries.iter_mut() {
586            let start = entry.start_at;
587            let end = entry.end_at();
588            let was_completed = entry.completed;
589
590            if next < start {
591                entry.animation.reset();
592                entry.completed = false;
593                continue;
594            }
595
596            if prev <= start && next >= start {
597                entry.animation.reset();
598                entry.completed = false;
599            }
600
601            if entry.duration == 0.0 {
602                if next >= start {
603                    entry.animation.seek_to(1.0);
604                    entry.completed = true;
605                }
606                if !was_completed && entry.completed {
607                    completed_labels.push(entry.label.clone());
608                }
609                continue;
610            }
611
612            let overlap_start = prev.max(start);
613            let overlap_end = next.min(end);
614            if overlap_end > overlap_start {
615                let still_running = entry.animation.update(overlap_end - overlap_start);
616                if !still_running {
617                    entry.completed = true;
618                }
619            }
620
621            if next >= end {
622                entry.animation.seek_to(1.0);
623                entry.completed = true;
624            }
625
626            if !was_completed && entry.completed {
627                completed_labels.push(entry.label.clone());
628            }
629        }
630        completed_labels
631    }
632
633    fn sync_to_elapsed(&mut self) {
634        let local_time = self.local_time_for_elapsed(self.elapsed);
635        for entry in self.entries.iter_mut() {
636            let start = entry.start_at;
637            let end = entry.end_at();
638
639            if local_time <= start {
640                entry.animation.reset();
641                entry.completed = false;
642            } else if local_time >= end || entry.duration == 0.0 {
643                entry.animation.seek_to(1.0);
644                entry.completed = true;
645            } else {
646                let progress = (local_time - start) / entry.duration;
647                entry.animation.seek_to(progress);
648                entry.completed = false;
649            }
650        }
651    }
652}
653
654impl Update for Timeline {
655    fn update(&mut self, dt: f32) -> bool {
656        match self.state {
657            TimelineState::Completed => return false,
658            TimelineState::Paused | TimelineState::Idle => return true,
659            TimelineState::Playing => {}
660        }
661
662        let base = self.duration();
663        if base == 0.0 {
664            return self.complete_from_update();
665        }
666
667        let dt = dt.max(0.0) * self.time_scale;
668        let previous_elapsed = self.elapsed;
669        let next_elapsed = previous_elapsed + dt;
670
671        match self.looping {
672            Loop::Once => {
673                let prev_local = previous_elapsed.min(base);
674                let next_local = next_elapsed.min(base);
675                let completed_labels = self.tick_forward(prev_local, next_local);
676                self.fire_entry_callbacks(&completed_labels);
677                self.elapsed = next_elapsed.min(base);
678                if next_elapsed >= base {
679                    return self.complete_from_update();
680                }
681            }
682            Loop::Times(n) => {
683                let total = base * n.max(1) as f32;
684                let completed_labels =
685                    self.entry_completion_labels_between(previous_elapsed, next_elapsed, base);
686                self.elapsed = next_elapsed.min(total);
687                self.sync_to_elapsed();
688                self.fire_entry_callbacks(&completed_labels);
689                if next_elapsed >= total {
690                    return self.complete_from_update();
691                }
692            }
693            Loop::PingPongTimes(n) => {
694                let total = base * n.max(1) as f32;
695                let completed_labels =
696                    self.entry_completion_labels_between(previous_elapsed, next_elapsed, base);
697                self.elapsed = next_elapsed.min(total);
698                self.sync_to_elapsed();
699                self.fire_entry_callbacks(&completed_labels);
700                if next_elapsed >= total {
701                    return self.complete_from_update();
702                }
703            }
704            Loop::Forever | Loop::PingPong => {
705                let completed_labels =
706                    self.entry_completion_labels_between(previous_elapsed, next_elapsed, base);
707                self.elapsed = next_elapsed;
708                self.sync_to_elapsed();
709                self.fire_entry_callbacks(&completed_labels);
710            }
711        }
712
713        true
714    }
715}
716
717impl Playable for Timeline {
718    fn duration(&self) -> f32 {
719        self.playback_duration()
720    }
721
722    fn reset(&mut self) {
723        Timeline::reset(self);
724    }
725
726    fn seek_to(&mut self, progress: f32) {
727        Timeline::seek(self, progress);
728    }
729
730    fn is_complete(&self) -> bool {
731        Timeline::is_complete(self)
732    }
733
734    fn as_any(&self) -> &dyn core::any::Any {
735        self
736    }
737
738    fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
739        self
740    }
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746    use animato_core::Easing;
747    use animato_tween::Tween;
748
749    fn tween(end: f32, duration: f32) -> Tween<f32> {
750        Tween::new(0.0_f32, end)
751            .duration(duration)
752            .easing(Easing::Linear)
753            .build()
754    }
755
756    #[test]
757    fn concurrent_entries_advance_together() {
758        let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start).add(
759            "b",
760            tween(100.0, 1.0),
761            At::Label("a"),
762        );
763
764        timeline.play();
765        timeline.update(0.5);
766
767        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.5);
768        assert_eq!(timeline.get::<Tween<f32>>("b").unwrap().value(), 50.0);
769    }
770
771    #[test]
772    fn end_and_offset_position_entries() {
773        let timeline = Timeline::new()
774            .add("first", tween(1.0, 1.0), At::Start)
775            .add("second", tween(1.0, 0.5), At::End)
776            .add("third", tween(1.0, 0.25), At::Offset(0.25));
777
778        assert_eq!(timeline.duration(), 2.0);
779    }
780
781    #[test]
782    fn seek_abs_synchronizes_children() {
783        let mut timeline = Timeline::new().add("a", tween(100.0, 2.0), At::Start);
784
785        timeline.seek_abs(0.5);
786
787        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
788    }
789
790    #[test]
791    fn pause_stops_timeline_progress() {
792        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
793        timeline.play();
794        timeline.update(0.25);
795        timeline.pause();
796        timeline.update(0.5);
797
798        assert_eq!(timeline.elapsed(), 0.25);
799        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
800    }
801
802    #[test]
803    fn resume_continues_after_pause() {
804        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
805        timeline.play();
806        timeline.update(0.25);
807        timeline.pause();
808        timeline.resume();
809        timeline.update(0.25);
810
811        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 50.0);
812    }
813
814    #[test]
815    fn once_timeline_completes() {
816        let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start);
817        timeline.play();
818
819        assert!(!timeline.update(1.0));
820        assert!(timeline.is_complete());
821    }
822
823    #[test]
824    fn times_loop_repeats_then_completes() {
825        let mut timeline = Timeline::new()
826            .add("a", tween(100.0, 1.0), At::Start)
827            .looping(Loop::Times(2));
828        timeline.play();
829
830        timeline.update(1.25);
831        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
832
833        assert!(!timeline.update(1.0));
834        assert!(timeline.is_complete());
835        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 100.0);
836    }
837
838    #[test]
839    fn ping_pong_reflects_timeline_time() {
840        let mut timeline = Timeline::new()
841            .add("a", tween(100.0, 1.0), At::Start)
842            .looping(Loop::PingPong);
843        timeline.play();
844        timeline.update(1.25);
845
846        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 75.0);
847        assert!(!timeline.is_complete());
848    }
849
850    #[test]
851    fn ping_pong_times_reflects_then_completes() {
852        let mut timeline = Timeline::new()
853            .add("a", tween(100.0, 1.0), At::Start)
854            .looping(Loop::PingPongTimes(2));
855        timeline.play();
856
857        timeline.update(1.25);
858        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 75.0);
859        assert!(!timeline.is_complete());
860
861        assert!(!timeline.update(0.75));
862        assert!(timeline.is_complete());
863        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.0);
864    }
865
866    #[test]
867    fn ping_pong_times_odd_passes_end_forward() {
868        let mut timeline = Timeline::new()
869            .add("a", tween(100.0, 1.0), At::Start)
870            .looping(Loop::PingPongTimes(3));
871        timeline.play();
872
873        assert!(!timeline.update(3.0));
874        assert!(timeline.is_complete());
875        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 100.0);
876    }
877
878    #[test]
879    fn time_scale_speeds_up_timeline() {
880        let mut timeline = Timeline::new()
881            .add("a", tween(100.0, 1.0), At::Start)
882            .time_scale(2.0);
883        timeline.play();
884        timeline.update(0.25);
885
886        assert_eq!(timeline.elapsed(), 0.5);
887        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 50.0);
888    }
889
890    #[test]
891    fn set_time_scale_clamps_negative_to_zero() {
892        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
893        timeline.set_time_scale(-1.0);
894        timeline.play();
895        timeline.update(0.5);
896
897        assert_eq!(timeline.elapsed(), 0.0);
898        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.0);
899    }
900
901    #[cfg(feature = "std")]
902    #[test]
903    fn callbacks_fire_once_during_update() {
904        use std::sync::Arc;
905        use std::sync::atomic::{AtomicUsize, Ordering};
906
907        let entry_count = Arc::new(AtomicUsize::new(0));
908        let complete_count = Arc::new(AtomicUsize::new(0));
909        let entry_seen = Arc::clone(&entry_count);
910        let complete_seen = Arc::clone(&complete_count);
911
912        let mut timeline = Timeline::new()
913            .add("a", tween(100.0, 1.0), At::Start)
914            .on_entry_complete("a", move || {
915                entry_seen.fetch_add(1, Ordering::SeqCst);
916            })
917            .on_complete(move || {
918                complete_seen.fetch_add(1, Ordering::SeqCst);
919            });
920
921        timeline.play();
922        assert!(!timeline.update(1.0));
923        assert!(!timeline.update(1.0));
924
925        assert_eq!(entry_count.load(Ordering::SeqCst), 1);
926        assert_eq!(complete_count.load(Ordering::SeqCst), 1);
927    }
928
929    #[cfg(feature = "std")]
930    #[test]
931    fn callbacks_do_not_fire_on_seek_or_reset() {
932        use std::sync::Arc;
933        use std::sync::atomic::{AtomicUsize, Ordering};
934
935        let entry_count = Arc::new(AtomicUsize::new(0));
936        let complete_count = Arc::new(AtomicUsize::new(0));
937        let entry_seen = Arc::clone(&entry_count);
938        let complete_seen = Arc::clone(&complete_count);
939
940        let mut timeline = Timeline::new()
941            .add("a", tween(100.0, 1.0), At::Start)
942            .on_entry_complete("a", move || {
943                entry_seen.fetch_add(1, Ordering::SeqCst);
944            })
945            .on_complete(move || {
946                complete_seen.fetch_add(1, Ordering::SeqCst);
947            });
948
949        timeline.seek(1.0);
950        timeline.reset();
951
952        assert_eq!(entry_count.load(Ordering::SeqCst), 0);
953        assert_eq!(complete_count.load(Ordering::SeqCst), 0);
954    }
955
956    #[cfg(feature = "tokio")]
957    #[test]
958    fn wait_is_ready_after_completion() {
959        use core::future::Future;
960        use std::sync::Arc;
961        use std::task::{Context, Poll, Wake, Waker};
962
963        struct NoopWaker;
964        impl Wake for NoopWaker {
965            fn wake(self: Arc<Self>) {}
966        }
967
968        let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start);
969        timeline.play();
970        timeline.update(1.0);
971
972        let mut wait = Box::pin(timeline.wait());
973        let waker = Waker::from(Arc::new(NoopWaker));
974        let mut cx = Context::from_waker(&waker);
975
976        assert!(matches!(wait.as_mut().poll(&mut cx), Poll::Ready(())));
977    }
978
979    #[test]
980    fn empty_timeline_completes_on_play_and_reports_progress() {
981        let mut timeline = Timeline::default();
982
983        assert_eq!(timeline.state(), TimelineState::Idle);
984        assert_eq!(timeline.progress(), 1.0);
985        timeline.play();
986
987        assert_eq!(timeline.state(), TimelineState::Completed);
988        assert!(timeline.is_complete());
989        assert!(!timeline.update(1.0));
990    }
991
992    #[test]
993    fn completed_timeline_restarts_when_played_again() {
994        let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start);
995
996        timeline.play();
997        assert!(!timeline.update(1.0));
998        timeline.play();
999
1000        assert_eq!(timeline.state(), TimelineState::Playing);
1001        assert_eq!(timeline.elapsed(), 0.0);
1002        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.0);
1003    }
1004
1005    #[test]
1006    fn absolute_and_missing_label_positions_are_resolved() {
1007        let timeline = Timeline::new()
1008            .add("first", tween(1.0, 0.5), At::Absolute(-1.0))
1009            .add("second", tween(1.0, 0.5), At::Label("missing"));
1010
1011        assert_eq!(timeline.entry_count(), 2);
1012        assert_eq!(timeline.duration(), 1.0);
1013    }
1014
1015    #[test]
1016    fn get_mut_can_edit_child_animation() {
1017        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
1018
1019        timeline.get_mut::<Tween<f32>>("a").unwrap().seek(0.75);
1020
1021        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 75.0);
1022        assert!(timeline.get::<Tween<f32>>("missing").is_none());
1023    }
1024
1025    #[test]
1026    fn seek_clamps_and_uncompletes_when_returning_from_end() {
1027        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
1028
1029        timeline.seek(2.0);
1030        assert!(timeline.is_complete());
1031        timeline.seek_abs(0.25);
1032
1033        assert_eq!(timeline.state(), TimelineState::Playing);
1034        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
1035        timeline.seek_abs(-5.0);
1036        assert_eq!(timeline.elapsed(), 0.0);
1037    }
1038
1039    #[test]
1040    fn forever_loop_progress_uses_local_time() {
1041        let mut timeline = Timeline::new()
1042            .add("a", tween(100.0, 1.0), At::Start)
1043            .looping(Loop::Forever);
1044
1045        timeline.play();
1046        timeline.update(2.25);
1047
1048        assert!((timeline.progress() - 0.25).abs() < 0.001);
1049        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
1050        assert!(!timeline.is_complete());
1051    }
1052
1053    #[test]
1054    fn zero_duration_entry_completes_and_fires_once() {
1055        let mut timeline = Timeline::new().add("instant", tween(1.0, 0.0), At::Start);
1056
1057        timeline.play();
1058
1059        assert!(!timeline.update(0.0));
1060        assert_eq!(timeline.get::<Tween<f32>>("instant").unwrap().value(), 1.0);
1061    }
1062
1063    #[test]
1064    fn negative_update_delta_does_not_advance() {
1065        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
1066
1067        timeline.play();
1068        assert!(timeline.update(-1.0));
1069
1070        assert_eq!(timeline.elapsed(), 0.0);
1071        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.0);
1072    }
1073
1074    #[test]
1075    fn playable_trait_for_timeline_exposes_downcast_hooks() {
1076        let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
1077
1078        assert_eq!(Playable::duration(&timeline), 1.0);
1079        Playable::seek_to(&mut timeline, 0.5);
1080        assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 50.0);
1081        assert!(Playable::as_any(&timeline).is::<Timeline>());
1082        assert!(Playable::as_any_mut(&mut timeline).is::<Timeline>());
1083        Playable::reset(&mut timeline);
1084        assert_eq!(timeline.state(), TimelineState::Idle);
1085    }
1086
1087    #[cfg(feature = "std")]
1088    #[test]
1089    fn debug_formats_entries_and_callbacks() {
1090        let timeline = Timeline::new()
1091            .add("a", tween(1.0, 1.0), At::Start)
1092            .on_entry_complete("a", || {})
1093            .on_complete(|| {});
1094
1095        let debug = format!("{timeline:?}");
1096
1097        assert!(debug.contains("Timeline"));
1098        assert!(debug.contains("entry_complete"));
1099        assert!(debug.contains("has_complete"));
1100    }
1101}