1use 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#[derive(Clone, Copy, Debug, PartialEq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum At<'a> {
16 Absolute(f32),
18 Start,
20 End,
22 Label(&'a str),
24 Offset(f32),
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub enum TimelineState {
32 Idle,
34 Playing,
36 Paused,
38 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
128pub struct Timeline {
134 entries: Vec<TimelineEntry>,
135 elapsed: f32,
136 state: TimelineState,
137 pub looping: Loop,
139 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 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 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 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 pub fn looping(mut self, mode: Loop) -> Self {
235 self.looping = mode;
236 self
237 }
238
239 pub fn time_scale(mut self, scale: f32) -> Self {
243 self.set_time_scale(scale);
244 self
245 }
246
247 pub fn set_time_scale(&mut self, scale: f32) {
251 self.time_scale = scale.max(0.0);
252 }
253
254 #[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 #[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 #[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 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 pub fn pause(&mut self) {
317 if self.state == TimelineState::Playing {
318 self.state = TimelineState::Paused;
319 }
320 }
321
322 pub fn resume(&mut self) {
324 if self.state == TimelineState::Paused {
325 self.state = TimelineState::Playing;
326 }
327 }
328
329 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 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 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 pub fn duration(&self) -> f32 {
373 self.entries
374 .iter()
375 .map(TimelineEntry::end_at)
376 .fold(0.0, f32::max)
377 }
378
379 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 pub fn is_complete(&self) -> bool {
399 self.state == TimelineState::Completed
400 }
401
402 pub fn state(&self) -> TimelineState {
404 self.state
405 }
406
407 pub fn elapsed(&self) -> f32 {
409 self.elapsed
410 }
411
412 pub fn entry_count(&self) -> usize {
414 self.entries.len()
415 }
416
417 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 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}