1use 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#[derive(Clone, Copy, Debug, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum At<'a> {
14 Absolute(f32),
16 Start,
18 End,
20 Label(&'a str),
22 Offset(f32),
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum TimelineState {
30 Idle,
32 Playing,
34 Paused,
36 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
126pub struct Timeline {
132 entries: Vec<TimelineEntry>,
133 elapsed: f32,
134 state: TimelineState,
135 pub looping: Loop,
137 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 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 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 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 pub fn looping(mut self, mode: Loop) -> Self {
233 self.looping = mode;
234 self
235 }
236
237 pub fn time_scale(mut self, scale: f32) -> Self {
241 self.set_time_scale(scale);
242 self
243 }
244
245 pub fn set_time_scale(&mut self, scale: f32) {
249 self.time_scale = scale.max(0.0);
250 }
251
252 #[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 #[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 #[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 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 pub fn pause(&mut self) {
315 if self.state == TimelineState::Playing {
316 self.state = TimelineState::Paused;
317 }
318 }
319
320 pub fn resume(&mut self) {
322 if self.state == TimelineState::Paused {
323 self.state = TimelineState::Playing;
324 }
325 }
326
327 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 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 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 pub fn duration(&self) -> f32 {
371 self.entries
372 .iter()
373 .map(TimelineEntry::end_at)
374 .fold(0.0, f32::max)
375 }
376
377 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 pub fn is_complete(&self) -> bool {
397 self.state == TimelineState::Completed
398 }
399
400 pub fn state(&self) -> TimelineState {
402 self.state
403 }
404
405 pub fn elapsed(&self) -> f32 {
407 self.elapsed
408 }
409
410 pub fn entry_count(&self) -> usize {
412 self.entries.len()
413 }
414
415 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 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}