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(crate) fn add_boxed_with_duration(
209 mut self,
210 label: impl Into<String>,
211 animation: Box<dyn Playable + Send>,
212 at: At<'_>,
213 duration: f32,
214 ) -> Self {
215 let start_at = self.resolve_start(at);
216 self.entries.push(TimelineEntry {
217 label: label.into(),
218 animation,
219 start_at,
220 duration: duration.max(0.0),
221 completed: false,
222 });
223 self
224 }
225
226 pub fn looping(mut self, mode: Loop) -> Self {
228 self.looping = mode;
229 self
230 }
231
232 pub fn time_scale(mut self, scale: f32) -> Self {
236 self.set_time_scale(scale);
237 self
238 }
239
240 pub fn set_time_scale(&mut self, scale: f32) {
244 self.time_scale = scale.max(0.0);
245 }
246
247 #[cfg(feature = "std")]
252 pub fn on_entry_complete(
253 mut self,
254 label: impl Into<String>,
255 f: impl FnMut() + Send + 'static,
256 ) -> Self {
257 self.callbacks.entry_complete.push(EntryCallback {
258 label: label.into(),
259 callback: Box::new(f),
260 });
261 self
262 }
263
264 #[cfg(feature = "std")]
269 pub fn on_complete(mut self, f: impl FnMut() + Send + 'static) -> Self {
270 self.callbacks.complete = Some(Box::new(f));
271 self
272 }
273
274 #[cfg(feature = "tokio")]
279 pub fn wait(&self) -> impl core::future::Future<Output = ()> + Send + 'static {
280 let mut rx = self.completion_tx.subscribe();
281 async move {
282 loop {
283 if *rx.borrow() {
284 return;
285 }
286 if rx.changed().await.is_err() {
287 return;
288 }
289 }
290 }
291 }
292
293 pub fn play(&mut self) {
295 if self.state == TimelineState::Completed {
296 self.reset();
297 }
298 if self.duration() == 0.0 {
299 self.state = TimelineState::Completed;
300 self.notify_completion_state(true);
301 } else {
302 self.state = TimelineState::Playing;
303 self.notify_completion_state(false);
304 self.sync_to_elapsed();
305 }
306 }
307
308 pub fn pause(&mut self) {
310 if self.state == TimelineState::Playing {
311 self.state = TimelineState::Paused;
312 }
313 }
314
315 pub fn resume(&mut self) {
317 if self.state == TimelineState::Paused {
318 self.state = TimelineState::Playing;
319 }
320 }
321
322 pub fn reset(&mut self) {
324 self.elapsed = 0.0;
325 self.state = TimelineState::Idle;
326 self.reset_completion_callbacks();
327 self.notify_completion_state(false);
328 for entry in self.entries.iter_mut() {
329 entry.animation.reset();
330 entry.completed = false;
331 }
332 }
333
334 pub fn seek(&mut self, progress: f32) {
336 let total = self.playback_duration();
337 let seek_duration = if total.is_finite() {
338 total
339 } else {
340 self.duration()
341 };
342 self.seek_abs(seek_duration * progress.clamp(0.0, 1.0));
343 }
344
345 pub fn seek_abs(&mut self, secs: f32) {
347 let total = self.playback_duration();
348 let secs = secs.max(0.0);
349 self.elapsed = if total.is_finite() {
350 secs.min(total)
351 } else {
352 secs
353 };
354 self.sync_to_elapsed();
355 if total.is_finite() && self.elapsed >= total {
356 self.state = TimelineState::Completed;
357 self.notify_completion_state(true);
358 } else if self.state == TimelineState::Completed {
359 self.state = TimelineState::Playing;
360 self.notify_completion_state(false);
361 }
362 }
363
364 pub fn duration(&self) -> f32 {
366 self.entries
367 .iter()
368 .map(TimelineEntry::end_at)
369 .fold(0.0, f32::max)
370 }
371
372 pub fn progress(&self) -> f32 {
374 let total = self.playback_duration();
375 if total == 0.0 {
376 return 1.0;
377 }
378 if total.is_finite() {
379 (self.elapsed / total).clamp(0.0, 1.0)
380 } else {
381 let base = self.duration();
382 if base == 0.0 {
383 1.0
384 } else {
385 (self.local_time_for_elapsed(self.elapsed) / base).clamp(0.0, 1.0)
386 }
387 }
388 }
389
390 pub fn is_complete(&self) -> bool {
392 self.state == TimelineState::Completed
393 }
394
395 pub fn state(&self) -> TimelineState {
397 self.state
398 }
399
400 pub fn elapsed(&self) -> f32 {
402 self.elapsed
403 }
404
405 pub fn entry_count(&self) -> usize {
407 self.entries.len()
408 }
409
410 pub fn get<T>(&self, label: &str) -> Option<&T>
412 where
413 T: Playable + 'static,
414 {
415 self.entries
416 .iter()
417 .find(|entry| entry.label == label)
418 .and_then(|entry| entry.animation.as_any().downcast_ref::<T>())
419 }
420
421 pub fn get_mut<T>(&mut self, label: &str) -> Option<&mut T>
423 where
424 T: Playable + 'static,
425 {
426 self.entries
427 .iter_mut()
428 .find(|entry| entry.label == label)
429 .and_then(|entry| entry.animation.as_any_mut().downcast_mut::<T>())
430 }
431
432 fn fire_entry_callbacks(&mut self, completed_labels: &[String]) {
433 #[cfg(feature = "std")]
434 self.callbacks.fire_entry_complete(completed_labels);
435
436 #[cfg(not(feature = "std"))]
437 let _ = completed_labels;
438 }
439
440 fn fire_complete_callback(&mut self) {
441 #[cfg(feature = "std")]
442 self.callbacks.fire_complete();
443 }
444
445 fn reset_completion_callbacks(&mut self) {
446 #[cfg(feature = "std")]
447 self.callbacks.reset_completion();
448 }
449
450 fn notify_completion_state(&self, complete: bool) {
451 #[cfg(feature = "tokio")]
452 let _ = self.completion_tx.send_replace(complete);
453
454 #[cfg(not(feature = "tokio"))]
455 let _ = complete;
456 }
457
458 fn complete_from_update(&mut self) -> bool {
459 self.state = TimelineState::Completed;
460 self.fire_complete_callback();
461 self.notify_completion_state(true);
462 false
463 }
464
465 fn resolve_start(&self, at: At<'_>) -> f32 {
466 match at {
467 At::Absolute(secs) => secs.max(0.0),
468 At::Start => 0.0,
469 At::End => self.duration(),
470 At::Label(label) => self
471 .entries
472 .iter()
473 .find(|entry| entry.label == label)
474 .map_or_else(|| self.duration(), |entry| entry.start_at),
475 At::Offset(offset) => (self.duration() + offset).max(0.0),
476 }
477 }
478
479 fn playback_duration(&self) -> f32 {
480 let base = self.duration();
481 if base == 0.0 {
482 return 0.0;
483 }
484 match self.looping {
485 Loop::Once => base,
486 Loop::Times(n) => base * n.max(1) as f32,
487 Loop::Forever | Loop::PingPong => f32::INFINITY,
488 }
489 }
490
491 fn local_time_for_elapsed(&self, elapsed: f32) -> f32 {
492 let base = self.duration();
493 if base == 0.0 {
494 return 0.0;
495 }
496
497 match self.looping {
498 Loop::Once => elapsed.min(base),
499 Loop::Times(n) => {
500 let total = base * n.max(1) as f32;
501 if elapsed >= total {
502 base
503 } else {
504 elapsed % base
505 }
506 }
507 Loop::Forever => elapsed % base,
508 Loop::PingPong => {
509 let cycle = elapsed % (base * 2.0);
510 if cycle <= base {
511 cycle
512 } else {
513 base * 2.0 - cycle
514 }
515 }
516 }
517 }
518
519 fn entry_completion_labels_between(&self, prev: f32, next: f32, base: f32) -> Vec<String> {
520 let mut labels = Vec::new();
521 if next <= prev || base <= 0.0 {
522 return labels;
523 }
524
525 let (max_cycles, period) = match self.looping {
526 Loop::Once => (Some(1), base),
527 Loop::Times(n) => (Some(n.max(1)), base),
528 Loop::Forever => (None, base),
529 Loop::PingPong => (None, base * 2.0),
530 };
531
532 if period <= 0.0 {
533 return labels;
534 }
535
536 let mut cycle = (prev / period).max(0.0) as u32;
537 loop {
538 if let Some(max_cycles) = max_cycles {
539 if cycle >= max_cycles {
540 break;
541 }
542 }
543
544 let cycle_start = cycle as f32 * period;
545 if cycle_start > next {
546 break;
547 }
548
549 for entry in self.entries.iter() {
550 let completion = cycle_start + entry.end_at();
551 if prev < completion && completion <= next {
552 labels.push(entry.label.clone());
553 }
554 }
555
556 cycle = cycle.saturating_add(1);
557 if cycle == u32::MAX {
558 break;
559 }
560 }
561
562 labels
563 }
564
565 fn tick_forward(&mut self, prev: f32, next: f32) -> Vec<String> {
566 let mut completed_labels = Vec::new();
567 for entry in self.entries.iter_mut() {
568 let start = entry.start_at;
569 let end = entry.end_at();
570 let was_completed = entry.completed;
571
572 if next < start {
573 entry.animation.reset();
574 entry.completed = false;
575 continue;
576 }
577
578 if prev <= start && next >= start {
579 entry.animation.reset();
580 entry.completed = false;
581 }
582
583 if entry.duration == 0.0 {
584 if next >= start {
585 entry.animation.seek_to(1.0);
586 entry.completed = true;
587 }
588 if !was_completed && entry.completed {
589 completed_labels.push(entry.label.clone());
590 }
591 continue;
592 }
593
594 let overlap_start = prev.max(start);
595 let overlap_end = next.min(end);
596 if overlap_end > overlap_start {
597 let still_running = entry.animation.update(overlap_end - overlap_start);
598 if !still_running {
599 entry.completed = true;
600 }
601 }
602
603 if next >= end {
604 entry.animation.seek_to(1.0);
605 entry.completed = true;
606 }
607
608 if !was_completed && entry.completed {
609 completed_labels.push(entry.label.clone());
610 }
611 }
612 completed_labels
613 }
614
615 fn sync_to_elapsed(&mut self) {
616 let local_time = self.local_time_for_elapsed(self.elapsed);
617 for entry in self.entries.iter_mut() {
618 let start = entry.start_at;
619 let end = entry.end_at();
620
621 if local_time <= start {
622 entry.animation.reset();
623 entry.completed = false;
624 } else if local_time >= end || entry.duration == 0.0 {
625 entry.animation.seek_to(1.0);
626 entry.completed = true;
627 } else {
628 let progress = (local_time - start) / entry.duration;
629 entry.animation.seek_to(progress);
630 entry.completed = false;
631 }
632 }
633 }
634}
635
636impl Update for Timeline {
637 fn update(&mut self, dt: f32) -> bool {
638 match self.state {
639 TimelineState::Completed => return false,
640 TimelineState::Paused | TimelineState::Idle => return true,
641 TimelineState::Playing => {}
642 }
643
644 let base = self.duration();
645 if base == 0.0 {
646 return self.complete_from_update();
647 }
648
649 let dt = dt.max(0.0) * self.time_scale;
650 let previous_elapsed = self.elapsed;
651 let next_elapsed = previous_elapsed + dt;
652
653 match self.looping {
654 Loop::Once => {
655 let prev_local = previous_elapsed.min(base);
656 let next_local = next_elapsed.min(base);
657 let completed_labels = self.tick_forward(prev_local, next_local);
658 self.fire_entry_callbacks(&completed_labels);
659 self.elapsed = next_elapsed.min(base);
660 if next_elapsed >= base {
661 return self.complete_from_update();
662 }
663 }
664 Loop::Times(n) => {
665 let total = base * n.max(1) as f32;
666 let completed_labels =
667 self.entry_completion_labels_between(previous_elapsed, next_elapsed, base);
668 self.elapsed = next_elapsed.min(total);
669 self.sync_to_elapsed();
670 self.fire_entry_callbacks(&completed_labels);
671 if next_elapsed >= total {
672 return self.complete_from_update();
673 }
674 }
675 Loop::Forever | Loop::PingPong => {
676 let completed_labels =
677 self.entry_completion_labels_between(previous_elapsed, next_elapsed, base);
678 self.elapsed = next_elapsed;
679 self.sync_to_elapsed();
680 self.fire_entry_callbacks(&completed_labels);
681 }
682 }
683
684 true
685 }
686}
687
688impl Playable for Timeline {
689 fn duration(&self) -> f32 {
690 self.playback_duration()
691 }
692
693 fn reset(&mut self) {
694 Timeline::reset(self);
695 }
696
697 fn seek_to(&mut self, progress: f32) {
698 Timeline::seek(self, progress);
699 }
700
701 fn is_complete(&self) -> bool {
702 Timeline::is_complete(self)
703 }
704
705 fn as_any(&self) -> &dyn core::any::Any {
706 self
707 }
708
709 fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
710 self
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717 use animato_core::Easing;
718 use animato_tween::Tween;
719
720 fn tween(end: f32, duration: f32) -> Tween<f32> {
721 Tween::new(0.0_f32, end)
722 .duration(duration)
723 .easing(Easing::Linear)
724 .build()
725 }
726
727 #[test]
728 fn concurrent_entries_advance_together() {
729 let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start).add(
730 "b",
731 tween(100.0, 1.0),
732 At::Label("a"),
733 );
734
735 timeline.play();
736 timeline.update(0.5);
737
738 assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.5);
739 assert_eq!(timeline.get::<Tween<f32>>("b").unwrap().value(), 50.0);
740 }
741
742 #[test]
743 fn end_and_offset_position_entries() {
744 let timeline = Timeline::new()
745 .add("first", tween(1.0, 1.0), At::Start)
746 .add("second", tween(1.0, 0.5), At::End)
747 .add("third", tween(1.0, 0.25), At::Offset(0.25));
748
749 assert_eq!(timeline.duration(), 2.0);
750 }
751
752 #[test]
753 fn seek_abs_synchronizes_children() {
754 let mut timeline = Timeline::new().add("a", tween(100.0, 2.0), At::Start);
755
756 timeline.seek_abs(0.5);
757
758 assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
759 }
760
761 #[test]
762 fn pause_stops_timeline_progress() {
763 let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
764 timeline.play();
765 timeline.update(0.25);
766 timeline.pause();
767 timeline.update(0.5);
768
769 assert_eq!(timeline.elapsed(), 0.25);
770 assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
771 }
772
773 #[test]
774 fn resume_continues_after_pause() {
775 let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
776 timeline.play();
777 timeline.update(0.25);
778 timeline.pause();
779 timeline.resume();
780 timeline.update(0.25);
781
782 assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 50.0);
783 }
784
785 #[test]
786 fn once_timeline_completes() {
787 let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start);
788 timeline.play();
789
790 assert!(!timeline.update(1.0));
791 assert!(timeline.is_complete());
792 }
793
794 #[test]
795 fn times_loop_repeats_then_completes() {
796 let mut timeline = Timeline::new()
797 .add("a", tween(100.0, 1.0), At::Start)
798 .looping(Loop::Times(2));
799 timeline.play();
800
801 timeline.update(1.25);
802 assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 25.0);
803
804 assert!(!timeline.update(1.0));
805 assert!(timeline.is_complete());
806 assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 100.0);
807 }
808
809 #[test]
810 fn ping_pong_reflects_timeline_time() {
811 let mut timeline = Timeline::new()
812 .add("a", tween(100.0, 1.0), At::Start)
813 .looping(Loop::PingPong);
814 timeline.play();
815 timeline.update(1.25);
816
817 assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 75.0);
818 assert!(!timeline.is_complete());
819 }
820
821 #[test]
822 fn time_scale_speeds_up_timeline() {
823 let mut timeline = Timeline::new()
824 .add("a", tween(100.0, 1.0), At::Start)
825 .time_scale(2.0);
826 timeline.play();
827 timeline.update(0.25);
828
829 assert_eq!(timeline.elapsed(), 0.5);
830 assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 50.0);
831 }
832
833 #[test]
834 fn set_time_scale_clamps_negative_to_zero() {
835 let mut timeline = Timeline::new().add("a", tween(100.0, 1.0), At::Start);
836 timeline.set_time_scale(-1.0);
837 timeline.play();
838 timeline.update(0.5);
839
840 assert_eq!(timeline.elapsed(), 0.0);
841 assert_eq!(timeline.get::<Tween<f32>>("a").unwrap().value(), 0.0);
842 }
843
844 #[cfg(feature = "std")]
845 #[test]
846 fn callbacks_fire_once_during_update() {
847 use std::sync::Arc;
848 use std::sync::atomic::{AtomicUsize, Ordering};
849
850 let entry_count = Arc::new(AtomicUsize::new(0));
851 let complete_count = Arc::new(AtomicUsize::new(0));
852 let entry_seen = Arc::clone(&entry_count);
853 let complete_seen = Arc::clone(&complete_count);
854
855 let mut timeline = Timeline::new()
856 .add("a", tween(100.0, 1.0), At::Start)
857 .on_entry_complete("a", move || {
858 entry_seen.fetch_add(1, Ordering::SeqCst);
859 })
860 .on_complete(move || {
861 complete_seen.fetch_add(1, Ordering::SeqCst);
862 });
863
864 timeline.play();
865 assert!(!timeline.update(1.0));
866 assert!(!timeline.update(1.0));
867
868 assert_eq!(entry_count.load(Ordering::SeqCst), 1);
869 assert_eq!(complete_count.load(Ordering::SeqCst), 1);
870 }
871
872 #[cfg(feature = "std")]
873 #[test]
874 fn callbacks_do_not_fire_on_seek_or_reset() {
875 use std::sync::Arc;
876 use std::sync::atomic::{AtomicUsize, Ordering};
877
878 let entry_count = Arc::new(AtomicUsize::new(0));
879 let complete_count = Arc::new(AtomicUsize::new(0));
880 let entry_seen = Arc::clone(&entry_count);
881 let complete_seen = Arc::clone(&complete_count);
882
883 let mut timeline = Timeline::new()
884 .add("a", tween(100.0, 1.0), At::Start)
885 .on_entry_complete("a", move || {
886 entry_seen.fetch_add(1, Ordering::SeqCst);
887 })
888 .on_complete(move || {
889 complete_seen.fetch_add(1, Ordering::SeqCst);
890 });
891
892 timeline.seek(1.0);
893 timeline.reset();
894
895 assert_eq!(entry_count.load(Ordering::SeqCst), 0);
896 assert_eq!(complete_count.load(Ordering::SeqCst), 0);
897 }
898
899 #[cfg(feature = "tokio")]
900 #[test]
901 fn wait_is_ready_after_completion() {
902 use core::future::Future;
903 use std::sync::Arc;
904 use std::task::{Context, Poll, Wake, Waker};
905
906 struct NoopWaker;
907 impl Wake for NoopWaker {
908 fn wake(self: Arc<Self>) {}
909 }
910
911 let mut timeline = Timeline::new().add("a", tween(1.0, 1.0), At::Start);
912 timeline.play();
913 timeline.update(1.0);
914
915 let mut wait = Box::pin(timeline.wait());
916 let waker = Waker::from(Arc::new(NoopWaker));
917 let mut cx = Context::from_waker(&waker);
918
919 assert!(matches!(wait.as_mut().poll(&mut cx), Poll::Ready(())));
920 }
921}