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