1#![forbid(unsafe_code)]
2
3use std::time::Duration;
41
42use super::Animation;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum LoopCount {
51 Once,
53 Times(u32),
55 Infinite,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum PlaybackState {
62 Idle,
64 Playing,
66 Paused,
68 Finished,
70}
71
72struct TimelineEvent {
74 offset: Duration,
76 animation: Box<dyn Animation>,
78 label: Option<String>,
80}
81
82impl std::fmt::Debug for TimelineEvent {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("TimelineEvent")
85 .field("offset", &self.offset)
86 .field("label", &self.label)
87 .finish_non_exhaustive()
88 }
89}
90
91pub struct Timeline {
95 events: Vec<TimelineEvent>,
96 total_duration: Duration,
99 duration_explicit: bool,
101 loop_count: LoopCount,
102 loops_remaining: u32,
104 state: PlaybackState,
105 current_time: Duration,
106}
107
108impl std::fmt::Debug for Timeline {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct("Timeline")
111 .field("event_count", &self.events.len())
112 .field("total_duration", &self.total_duration)
113 .field("loop_count", &self.loop_count)
114 .field("state", &self.state)
115 .field("current_time", &self.current_time)
116 .finish()
117 }
118}
119
120impl Timeline {
125 #[must_use]
127 pub fn new() -> Self {
128 Self {
129 events: Vec::new(),
130 total_duration: Duration::from_nanos(1),
131 duration_explicit: false,
132 loop_count: LoopCount::Once,
133 loops_remaining: 0,
134 state: PlaybackState::Idle,
135 current_time: Duration::ZERO,
136 }
137 }
138
139 #[must_use]
141 pub fn add(mut self, offset: Duration, animation: impl Animation + 'static) -> Self {
142 self.push_event(offset, Box::new(animation), None);
143 self
144 }
145
146 #[must_use]
148 pub fn add_labeled(
149 mut self,
150 label: &str,
151 offset: Duration,
152 animation: impl Animation + 'static,
153 ) -> Self {
154 self.push_event(offset, Box::new(animation), Some(label.to_string()));
155 self
156 }
157
158 #[must_use]
162 pub fn then(self, animation: impl Animation + 'static) -> Self {
163 let offset = self.events.last().map_or(Duration::ZERO, |e| e.offset);
164 self.add(offset, animation)
165 }
166
167 #[must_use]
172 pub fn set_duration(mut self, d: Duration) -> Self {
173 self.total_duration = if d.is_zero() {
174 Duration::from_nanos(1)
175 } else {
176 d
177 };
178 self.duration_explicit = true;
179 self
180 }
181
182 #[must_use]
184 pub fn set_loop_count(mut self, count: LoopCount) -> Self {
185 self.loop_count = count;
186 self.loops_remaining = match count {
187 LoopCount::Once => 0,
188 LoopCount::Times(n) => n,
189 LoopCount::Infinite => u32::MAX,
190 };
191 self
192 }
193
194 fn push_event(
196 &mut self,
197 offset: Duration,
198 animation: Box<dyn Animation>,
199 label: Option<String>,
200 ) {
201 let event = TimelineEvent {
202 offset,
203 animation,
204 label,
205 };
206 let pos = self.events.partition_point(|e| e.offset <= offset);
208 self.events.insert(pos, event);
209
210 if !self.duration_explicit {
212 self.total_duration = self.events.last().map_or(Duration::from_nanos(1), |e| {
213 if e.offset.is_zero() {
214 Duration::from_nanos(1)
215 } else {
216 e.offset
217 }
218 });
219 }
220 }
221}
222
223impl Default for Timeline {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229impl Timeline {
234 pub fn play(&mut self) {
236 self.current_time = Duration::ZERO;
237 self.loops_remaining = match self.loop_count {
238 LoopCount::Once => 0,
239 LoopCount::Times(n) => n,
240 LoopCount::Infinite => u32::MAX,
241 };
242 for event in &mut self.events {
243 event.animation.reset();
244 }
245 self.state = PlaybackState::Playing;
246 }
247
248 pub fn pause(&mut self) {
250 if self.state == PlaybackState::Playing {
251 self.state = PlaybackState::Paused;
252 }
253 }
254
255 pub fn resume(&mut self) {
257 if self.state == PlaybackState::Paused {
258 self.state = PlaybackState::Playing;
259 }
260 }
261
262 pub fn stop(&mut self) {
264 self.state = PlaybackState::Idle;
265 self.current_time = Duration::ZERO;
266 for event in &mut self.events {
267 event.animation.reset();
268 }
269 }
270
271 pub fn seek(&mut self, time: Duration) {
276 let clamped = if time > self.total_duration {
277 self.total_duration
278 } else {
279 time
280 };
281
282 for event in &mut self.events {
284 event.animation.reset();
285 if clamped > event.offset {
286 let dt = clamped.saturating_sub(event.offset);
287 event.animation.tick(dt);
288 }
289 }
290 self.current_time = clamped;
291
292 if self.state == PlaybackState::Idle || self.state == PlaybackState::Finished {
294 self.state = PlaybackState::Paused;
295 }
296 }
297
298 pub fn seek_label(&mut self, label: &str) -> bool {
302 let offset = self
303 .events
304 .iter()
305 .find(|e| e.label.as_deref() == Some(label))
306 .map(|e| e.offset);
307 if let Some(offset) = offset {
308 self.seek(offset);
309 true
310 } else {
311 false
312 }
313 }
314
315 #[inline]
317 #[must_use]
318 pub fn progress(&self) -> f32 {
319 if self.events.is_empty() {
320 return 1.0;
321 }
322 if self.total_duration.is_zero() {
323 return 1.0;
324 }
325 let t = self.current_time.as_secs_f64() / self.total_duration.as_secs_f64();
326 (t as f32).clamp(0.0, 1.0)
327 }
328
329 #[inline]
331 #[must_use]
332 pub fn state(&self) -> PlaybackState {
333 self.state
334 }
335
336 #[inline]
338 #[must_use]
339 pub fn current_time(&self) -> Duration {
340 self.current_time
341 }
342
343 #[inline]
345 #[must_use]
346 pub fn duration(&self) -> Duration {
347 self.total_duration
348 }
349
350 #[inline]
352 #[must_use]
353 pub fn event_count(&self) -> usize {
354 self.events.len()
355 }
356
357 #[must_use]
361 pub fn event_value(&self, label: &str) -> Option<f32> {
362 self.events
363 .iter()
364 .find(|e| e.label.as_deref() == Some(label))
365 .map(|e| e.animation.value())
366 }
367
368 #[must_use]
372 pub fn event_value_at(&self, index: usize) -> Option<f32> {
373 self.events.get(index).map(|e| e.animation.value())
374 }
375}
376
377impl Animation for Timeline {
382 fn tick(&mut self, dt: Duration) {
383 if self.state != PlaybackState::Playing {
384 return;
385 }
386
387 let new_time = self.current_time.saturating_add(dt);
388
389 for event in &mut self.events {
391 if new_time > event.offset && !event.animation.is_complete() {
392 let event_start = event.offset;
394 if self.current_time >= event_start {
395 event.animation.tick(dt);
397 } else {
398 let partial = new_time.saturating_sub(event_start);
400 event.animation.tick(partial);
401 }
402 }
403 }
404
405 self.current_time = new_time;
406
407 if self.current_time >= self.total_duration {
409 match self.loop_count {
410 LoopCount::Once => {
411 self.current_time = self.total_duration;
412 self.state = PlaybackState::Finished;
413 }
414 LoopCount::Times(_) | LoopCount::Infinite => {
415 if self.loops_remaining > 0 {
416 if self.loop_count != LoopCount::Infinite {
417 self.loops_remaining -= 1;
418 }
419 let overshoot = self.current_time.saturating_sub(self.total_duration);
421 self.current_time = Duration::ZERO;
422 for event in &mut self.events {
423 event.animation.reset();
424 }
425 if !overshoot.is_zero() {
427 self.tick(overshoot);
428 }
429 } else {
430 self.current_time = self.total_duration;
431 self.state = PlaybackState::Finished;
432 }
433 }
434 }
435 }
436 }
437
438 fn is_complete(&self) -> bool {
439 self.state == PlaybackState::Finished
440 }
441
442 fn value(&self) -> f32 {
443 self.progress()
444 }
445
446 fn reset(&mut self) {
447 self.current_time = Duration::ZERO;
448 self.loops_remaining = match self.loop_count {
449 LoopCount::Once => 0,
450 LoopCount::Times(n) => n,
451 LoopCount::Infinite => u32::MAX,
452 };
453 self.state = PlaybackState::Idle;
454 for event in &mut self.events {
455 event.animation.reset();
456 }
457 }
458
459 fn overshoot(&self) -> Duration {
460 if self.state == PlaybackState::Finished {
461 self.current_time.saturating_sub(self.total_duration)
462 } else {
463 Duration::ZERO
464 }
465 }
466}
467
468#[cfg(test)]
473mod tests {
474 use super::*;
475 use crate::animation::Fade;
476
477 const MS_100: Duration = Duration::from_millis(100);
478 const MS_200: Duration = Duration::from_millis(200);
479 const MS_250: Duration = Duration::from_millis(250);
480 const MS_300: Duration = Duration::from_millis(300);
481 const MS_500: Duration = Duration::from_millis(500);
482 const SEC_1: Duration = Duration::from_secs(1);
483
484 #[test]
485 fn empty_timeline_is_immediately_complete() {
486 let tl = Timeline::new();
487 assert_eq!(tl.progress(), 1.0);
488 assert_eq!(tl.event_count(), 0);
489 }
490
491 #[test]
492 fn sequential_events() {
493 let mut tl = Timeline::new()
494 .add(Duration::ZERO, Fade::new(MS_200))
495 .add(MS_200, Fade::new(MS_200))
496 .add(Duration::from_millis(400), Fade::new(MS_200))
497 .set_duration(Duration::from_millis(600));
498
499 tl.play();
500
501 tl.tick(MS_100);
503 assert!((tl.event_value_at(0).unwrap() - 0.5).abs() < 0.01);
504 assert!((tl.event_value_at(1).unwrap() - 0.0).abs() < 0.01);
505
506 tl.tick(MS_200);
508 assert!(tl.event_value_at(0).unwrap() > 0.99);
509 assert!((tl.event_value_at(1).unwrap() - 0.5).abs() < 0.01);
510
511 tl.tick(MS_300);
513 assert!(tl.is_complete());
514 assert!((tl.progress() - 1.0).abs() < f32::EPSILON);
515 }
516
517 #[test]
518 fn overlapping_events() {
519 let mut tl = Timeline::new()
520 .add(Duration::ZERO, Fade::new(MS_500))
521 .add(MS_200, Fade::new(MS_500))
522 .set_duration(Duration::from_millis(700));
523
524 tl.play();
525
526 tl.tick(MS_300);
528 assert!((tl.event_value_at(0).unwrap() - 0.6).abs() < 0.02);
529 assert!((tl.event_value_at(1).unwrap() - 0.2).abs() < 0.02);
530 }
531
532 #[test]
533 fn labeled_events_and_seek() {
534 let mut tl = Timeline::new()
535 .add_labeled("intro", Duration::ZERO, Fade::new(MS_500))
536 .add_labeled("main", MS_500, Fade::new(MS_500))
537 .set_duration(SEC_1);
538
539 tl.play();
540
541 assert!(tl.seek_label("main"));
543 assert!(tl.event_value("intro").unwrap() > 0.99);
545 assert!((tl.event_value("main").unwrap() - 0.0).abs() < f32::EPSILON);
547
548 assert!(!tl.seek_label("nonexistent"));
550 }
551
552 #[test]
553 fn loop_finite() {
554 let mut tl = Timeline::new()
555 .add(Duration::ZERO, Fade::new(MS_100))
556 .set_duration(MS_100)
557 .set_loop_count(LoopCount::Times(2));
558
559 tl.play();
560
561 tl.tick(MS_100);
563 assert!(!tl.is_complete());
564 assert_eq!(tl.state(), PlaybackState::Playing);
565
566 tl.tick(MS_100);
568 assert!(!tl.is_complete());
569
570 tl.tick(MS_100);
572 assert!(tl.is_complete());
573 }
574
575 #[test]
576 fn loop_infinite_never_finishes() {
577 let mut tl = Timeline::new()
578 .add(Duration::ZERO, Fade::new(MS_100))
579 .set_duration(MS_100)
580 .set_loop_count(LoopCount::Infinite);
581
582 tl.play();
583
584 for _ in 0..100 {
586 tl.tick(MS_100);
587 }
588 assert!(!tl.is_complete());
589 assert_eq!(tl.state(), PlaybackState::Playing);
590 }
591
592 #[test]
593 fn pause_resume() {
594 let mut tl = Timeline::new()
595 .add(Duration::ZERO, Fade::new(SEC_1))
596 .set_duration(SEC_1);
597
598 tl.play();
599 tl.tick(MS_250);
600
601 tl.pause();
602 assert_eq!(tl.state(), PlaybackState::Paused);
603 let time_at_pause = tl.current_time();
604
605 tl.tick(MS_500);
607 assert_eq!(tl.current_time(), time_at_pause);
608
609 tl.resume();
610 assert_eq!(tl.state(), PlaybackState::Playing);
611
612 tl.tick(MS_250);
614 assert!(tl.current_time() > time_at_pause);
615 }
616
617 #[test]
618 fn seek_clamps_to_duration() {
619 let mut tl = Timeline::new()
620 .add(Duration::ZERO, Fade::new(MS_500))
621 .set_duration(MS_500);
622
623 tl.play();
624 tl.seek(SEC_1); assert_eq!(tl.current_time(), MS_500);
626 }
627
628 #[test]
629 fn seek_resets_and_reticks_animations() {
630 let mut tl = Timeline::new()
631 .add(Duration::ZERO, Fade::new(SEC_1))
632 .set_duration(SEC_1);
633
634 tl.play();
635 tl.tick(MS_500);
636 assert!((tl.event_value_at(0).unwrap() - 0.5).abs() < 0.02);
638
639 tl.seek(MS_250);
641 assert!((tl.event_value_at(0).unwrap() - 0.25).abs() < 0.02);
642 }
643
644 #[test]
645 fn stop_resets_everything() {
646 let mut tl = Timeline::new()
647 .add(Duration::ZERO, Fade::new(SEC_1))
648 .set_duration(SEC_1);
649
650 tl.play();
651 tl.tick(MS_500);
652 tl.stop();
653
654 assert_eq!(tl.state(), PlaybackState::Idle);
655 assert_eq!(tl.current_time(), Duration::ZERO);
656 assert!((tl.event_value_at(0).unwrap() - 0.0).abs() < f32::EPSILON);
657 }
658
659 #[test]
660 fn play_restarts_from_beginning() {
661 let mut tl = Timeline::new()
662 .add(Duration::ZERO, Fade::new(SEC_1))
663 .set_duration(SEC_1);
664
665 tl.play();
666 tl.tick(SEC_1);
667 assert!(tl.is_complete());
668
669 tl.play();
670 assert_eq!(tl.state(), PlaybackState::Playing);
671 assert_eq!(tl.current_time(), Duration::ZERO);
672 assert!((tl.event_value_at(0).unwrap() - 0.0).abs() < f32::EPSILON);
673 }
674
675 #[test]
676 fn then_chains_at_same_offset() {
677 let tl = Timeline::new()
678 .add(MS_100, Fade::new(MS_100))
679 .then(Fade::new(MS_100)); assert_eq!(tl.event_count(), 2);
682 }
683
684 #[test]
685 fn progress_tracks_time() {
686 let mut tl = Timeline::new()
687 .add(Duration::ZERO, Fade::new(SEC_1))
688 .set_duration(SEC_1);
689
690 tl.play();
691 assert!((tl.progress() - 0.0).abs() < f32::EPSILON);
692
693 tl.tick(MS_250);
694 assert!((tl.progress() - 0.25).abs() < 0.02);
695
696 tl.tick(MS_250);
697 assert!((tl.progress() - 0.5).abs() < 0.02);
698 }
699
700 #[test]
701 fn animation_trait_value_matches_progress() {
702 let mut tl = Timeline::new()
703 .add(Duration::ZERO, Fade::new(SEC_1))
704 .set_duration(SEC_1);
705
706 tl.play();
707 tl.tick(MS_500);
708
709 assert!((tl.value() - tl.progress()).abs() < f32::EPSILON);
710 }
711
712 #[test]
713 fn animation_trait_reset() {
714 let mut tl = Timeline::new()
715 .add(Duration::ZERO, Fade::new(SEC_1))
716 .set_duration(SEC_1);
717
718 tl.play();
719 tl.tick(SEC_1);
720 assert!(tl.is_complete());
721
722 tl.reset();
723 assert_eq!(tl.state(), PlaybackState::Idle);
724 assert!(!tl.is_complete());
725 }
726
727 #[test]
728 fn debug_format() {
729 let tl = Timeline::new()
730 .add(Duration::ZERO, Fade::new(MS_100))
731 .set_duration(MS_100);
732
733 let dbg = format!("{:?}", tl);
734 assert!(dbg.contains("Timeline"));
735 assert!(dbg.contains("event_count"));
736 }
737
738 #[test]
739 fn loop_once_plays_exactly_once() {
740 let mut tl = Timeline::new()
741 .add(Duration::ZERO, Fade::new(MS_100))
742 .set_duration(MS_100)
743 .set_loop_count(LoopCount::Once);
744
745 tl.play();
746 tl.tick(MS_100);
747 assert!(tl.is_complete());
748 }
749
750 #[test]
751 fn event_value_by_label_missing_returns_none() {
752 let tl = Timeline::new()
753 .add(Duration::ZERO, Fade::new(MS_100))
754 .set_duration(MS_100);
755
756 assert!(tl.event_value("nope").is_none());
757 }
758
759 #[test]
760 fn event_value_at_out_of_bounds() {
761 let tl = Timeline::new()
762 .add(Duration::ZERO, Fade::new(MS_100))
763 .set_duration(MS_100);
764
765 assert!(tl.event_value_at(5).is_none());
766 }
767
768 #[test]
769 fn idle_timeline_value_is_zero() {
770 let tl = Timeline::new()
771 .add(Duration::ZERO, Fade::new(MS_500))
772 .set_duration(MS_500);
773
774 assert!((tl.value() - 0.0).abs() < f32::EPSILON);
776 assert_eq!(tl.state(), PlaybackState::Idle);
777 }
778
779 #[test]
780 fn overshoot_is_zero_while_playing() {
781 let mut tl = Timeline::new()
782 .add(Duration::ZERO, Fade::new(MS_500))
783 .set_duration(MS_500);
784
785 tl.play();
786 tl.tick(MS_250);
787
788 assert_eq!(tl.overshoot(), Duration::ZERO);
789 }
790
791 #[test]
792 fn seek_to_zero_resets_animations() {
793 let mut tl = Timeline::new()
794 .add(Duration::ZERO, Fade::new(MS_500))
795 .set_duration(MS_500);
796
797 tl.play();
798 tl.tick(MS_250);
799 assert!(tl.event_value_at(0).unwrap() > 0.0);
800
801 tl.seek(Duration::ZERO);
802 assert_eq!(tl.current_time(), Duration::ZERO);
803 }
804
805 #[test]
808 fn default_trait() {
809 let tl = Timeline::default();
810 assert_eq!(tl.event_count(), 0);
811 assert_eq!(tl.state(), PlaybackState::Idle);
812 assert_eq!(tl.progress(), 1.0); }
814
815 #[test]
816 fn zero_duration_clamped_to_1ns() {
817 let tl = Timeline::new()
818 .add(Duration::ZERO, Fade::new(MS_100))
819 .set_duration(Duration::ZERO);
820 assert_eq!(tl.duration(), Duration::from_nanos(1));
821 }
822
823 #[test]
824 fn then_on_empty_timeline_uses_zero_offset() {
825 let tl = Timeline::new().then(Fade::new(MS_100));
826 assert_eq!(tl.event_count(), 1);
827 assert_eq!(tl.duration(), Duration::from_nanos(1));
829 }
830
831 #[test]
832 fn pause_when_not_playing_is_noop() {
833 let mut tl = Timeline::new()
834 .add(Duration::ZERO, Fade::new(MS_100))
835 .set_duration(MS_100);
836
837 tl.pause();
839 assert_eq!(tl.state(), PlaybackState::Idle);
840
841 tl.play();
843 tl.tick(MS_100);
844 assert_eq!(tl.state(), PlaybackState::Finished);
845 tl.pause();
846 assert_eq!(tl.state(), PlaybackState::Finished);
847 }
848
849 #[test]
850 fn resume_when_not_paused_is_noop() {
851 let mut tl = Timeline::new()
852 .add(Duration::ZERO, Fade::new(MS_100))
853 .set_duration(MS_100);
854
855 tl.resume();
857 assert_eq!(tl.state(), PlaybackState::Idle);
858
859 tl.play();
861 tl.resume();
862 assert_eq!(tl.state(), PlaybackState::Playing);
863 }
864
865 #[test]
866 fn seek_from_idle_transitions_to_paused() {
867 let mut tl = Timeline::new()
868 .add(Duration::ZERO, Fade::new(MS_500))
869 .set_duration(MS_500);
870
871 assert_eq!(tl.state(), PlaybackState::Idle);
872 tl.seek(MS_250);
873 assert_eq!(tl.state(), PlaybackState::Paused);
874 assert_eq!(tl.current_time(), MS_250);
875 }
876
877 #[test]
878 fn seek_from_finished_transitions_to_paused() {
879 let mut tl = Timeline::new()
880 .add(Duration::ZERO, Fade::new(MS_500))
881 .set_duration(MS_500);
882
883 tl.play();
884 tl.tick(MS_500);
885 assert_eq!(tl.state(), PlaybackState::Finished);
886
887 tl.seek(MS_250);
888 assert_eq!(tl.state(), PlaybackState::Paused);
889 assert_eq!(tl.current_time(), MS_250);
890 }
891
892 #[test]
893 fn seek_from_playing_stays_playing() {
894 let mut tl = Timeline::new()
895 .add(Duration::ZERO, Fade::new(MS_500))
896 .set_duration(MS_500);
897
898 tl.play();
899 tl.tick(MS_100);
900 assert_eq!(tl.state(), PlaybackState::Playing);
901
902 tl.seek(MS_300);
903 assert_eq!(tl.state(), PlaybackState::Playing);
905 }
906
907 #[test]
908 fn overshoot_when_finished() {
909 let mut tl = Timeline::new()
910 .add(Duration::ZERO, Fade::new(MS_100))
911 .set_duration(MS_100);
912
913 tl.play();
914 tl.tick(MS_100);
915 assert!(tl.is_complete());
916 assert_eq!(tl.overshoot(), Duration::ZERO);
918 }
919
920 #[test]
921 fn tick_when_idle_does_not_advance() {
922 let mut tl = Timeline::new()
923 .add(Duration::ZERO, Fade::new(MS_500))
924 .set_duration(MS_500);
925
926 tl.tick(MS_250);
927 assert_eq!(tl.current_time(), Duration::ZERO);
928 assert_eq!(tl.state(), PlaybackState::Idle);
929 }
930
931 #[test]
932 fn tick_when_paused_does_not_advance() {
933 let mut tl = Timeline::new()
934 .add(Duration::ZERO, Fade::new(SEC_1))
935 .set_duration(SEC_1);
936
937 tl.play();
938 tl.tick(MS_250);
939 tl.pause();
940 let paused_time = tl.current_time();
941
942 tl.tick(MS_500);
943 assert_eq!(tl.current_time(), paused_time);
944 }
945
946 #[test]
947 fn tick_when_finished_does_not_advance() {
948 let mut tl = Timeline::new()
949 .add(Duration::ZERO, Fade::new(MS_100))
950 .set_duration(MS_100);
951
952 tl.play();
953 tl.tick(MS_100);
954 assert!(tl.is_complete());
955
956 let time_at_finish = tl.current_time();
957 tl.tick(MS_500);
958 assert_eq!(tl.current_time(), time_at_finish);
959 }
960
961 #[test]
962 fn multiple_events_at_same_offset() {
963 let mut tl = Timeline::new()
964 .add(Duration::ZERO, Fade::new(MS_200))
965 .add(Duration::ZERO, Fade::new(MS_200))
966 .add(Duration::ZERO, Fade::new(MS_200))
967 .set_duration(MS_200);
968
969 assert_eq!(tl.event_count(), 3);
970 tl.play();
971 tl.tick(MS_100);
972
973 for i in 0..3 {
975 assert!(
976 (tl.event_value_at(i).unwrap() - 0.5).abs() < 0.02,
977 "event {i} should be at ~50%"
978 );
979 }
980 }
981
982 #[test]
983 fn auto_computed_duration_uses_max_offset() {
984 let tl = Timeline::new()
985 .add(MS_100, Fade::new(MS_100))
986 .add(MS_500, Fade::new(MS_100))
987 .add(MS_300, Fade::new(MS_100));
988
989 assert_eq!(tl.duration(), MS_500);
992 }
993
994 #[test]
995 fn explicit_duration_overrides_auto() {
996 let tl = Timeline::new()
997 .add(MS_100, Fade::new(MS_100))
998 .add(MS_500, Fade::new(MS_100))
999 .set_duration(SEC_1);
1000
1001 assert_eq!(tl.duration(), SEC_1);
1002 }
1003
1004 #[test]
1005 fn seek_label_on_empty_timeline() {
1006 let mut tl = Timeline::new();
1007 assert!(!tl.seek_label("foo"));
1008 }
1009
1010 #[test]
1011 fn event_value_at_on_empty_timeline() {
1012 let tl = Timeline::new();
1013 assert!(tl.event_value_at(0).is_none());
1014 }
1015
1016 #[test]
1017 fn loop_times_zero_plays_once() {
1018 let mut tl = Timeline::new()
1019 .add(Duration::ZERO, Fade::new(MS_100))
1020 .set_duration(MS_100)
1021 .set_loop_count(LoopCount::Times(0));
1022
1023 tl.play();
1024 tl.tick(MS_100);
1025 assert!(tl.is_complete());
1026 }
1027
1028 #[test]
1029 fn loop_count_eq() {
1030 assert_eq!(LoopCount::Once, LoopCount::Once);
1031 assert_eq!(LoopCount::Times(5), LoopCount::Times(5));
1032 assert_ne!(LoopCount::Times(5), LoopCount::Times(3));
1033 assert_eq!(LoopCount::Infinite, LoopCount::Infinite);
1034 assert_ne!(LoopCount::Once, LoopCount::Infinite);
1035 }
1036
1037 #[test]
1038 fn playback_state_eq() {
1039 assert_eq!(PlaybackState::Idle, PlaybackState::Idle);
1040 assert_eq!(PlaybackState::Playing, PlaybackState::Playing);
1041 assert_eq!(PlaybackState::Paused, PlaybackState::Paused);
1042 assert_eq!(PlaybackState::Finished, PlaybackState::Finished);
1043 assert_ne!(PlaybackState::Idle, PlaybackState::Playing);
1044 }
1045
1046 #[test]
1047 fn loop_count_clone() {
1048 let lc = LoopCount::Times(3);
1049 let lc2 = lc;
1050 assert_eq!(lc, lc2);
1051 }
1052
1053 #[test]
1054 fn playback_state_clone() {
1055 let ps = PlaybackState::Paused;
1056 let ps2 = ps;
1057 assert_eq!(ps, ps2);
1058 }
1059
1060 #[test]
1061 fn play_after_stop_resets() {
1062 let mut tl = Timeline::new()
1063 .add(Duration::ZERO, Fade::new(MS_500))
1064 .set_duration(MS_500);
1065
1066 tl.play();
1067 tl.tick(MS_250);
1068 tl.stop();
1069 assert_eq!(tl.state(), PlaybackState::Idle);
1070
1071 tl.play();
1072 assert_eq!(tl.state(), PlaybackState::Playing);
1073 assert_eq!(tl.current_time(), Duration::ZERO);
1074 }
1075
1076 #[test]
1077 fn seek_past_end_then_resume_and_tick_finishes() {
1078 let mut tl = Timeline::new()
1079 .add(Duration::ZERO, Fade::new(MS_100))
1080 .set_duration(MS_100);
1081
1082 tl.play();
1083 tl.seek(MS_100); tl.resume(); tl.tick(Duration::from_nanos(1));
1088 assert!(tl.is_complete());
1089 }
1090
1091 #[test]
1092 fn progress_clamps_to_zero_one() {
1093 let mut tl = Timeline::new()
1094 .add(Duration::ZERO, Fade::new(MS_100))
1095 .set_duration(MS_100);
1096
1097 assert!(tl.progress() >= 0.0);
1099 assert!(tl.progress() <= 1.0);
1100
1101 tl.play();
1102 tl.tick(MS_100);
1103 assert!(tl.progress() >= 0.0);
1104 assert!(tl.progress() <= 1.0);
1105 }
1106
1107 #[test]
1108 fn animation_trait_is_complete_false_while_playing() {
1109 let mut tl = Timeline::new()
1110 .add(Duration::ZERO, Fade::new(MS_500))
1111 .set_duration(MS_500);
1112
1113 tl.play();
1114 tl.tick(MS_250);
1115 assert!(!tl.is_complete());
1116 }
1117
1118 #[test]
1119 fn reset_from_finished() {
1120 let mut tl = Timeline::new()
1121 .add(Duration::ZERO, Fade::new(MS_100))
1122 .set_duration(MS_100);
1123
1124 tl.play();
1125 tl.tick(MS_100);
1126 assert!(tl.is_complete());
1127
1128 tl.reset();
1129 assert_eq!(tl.state(), PlaybackState::Idle);
1130 assert_eq!(tl.current_time(), Duration::ZERO);
1131 assert!(!tl.is_complete());
1132 }
1133
1134 #[test]
1135 fn reset_from_paused() {
1136 let mut tl = Timeline::new()
1137 .add(Duration::ZERO, Fade::new(MS_500))
1138 .set_duration(MS_500);
1139
1140 tl.play();
1141 tl.tick(MS_250);
1142 tl.pause();
1143 assert_eq!(tl.state(), PlaybackState::Paused);
1144
1145 tl.reset();
1146 assert_eq!(tl.state(), PlaybackState::Idle);
1147 assert_eq!(tl.current_time(), Duration::ZERO);
1148 }
1149
1150 #[test]
1151 fn labeled_event_value() {
1152 let mut tl = Timeline::new()
1153 .add_labeled("fade", Duration::ZERO, Fade::new(MS_200))
1154 .set_duration(MS_200);
1155
1156 tl.play();
1157 tl.tick(MS_100);
1158
1159 let v = tl.event_value("fade").unwrap();
1160 assert!((v - 0.5).abs() < 0.02);
1161 }
1162
1163 #[test]
1164 fn events_sorted_by_offset_on_insert() {
1165 let tl = Timeline::new()
1166 .add(MS_500, Fade::new(MS_100))
1167 .add(MS_100, Fade::new(MS_100))
1168 .add(MS_300, Fade::new(MS_100));
1169
1170 let mut tl = tl.set_duration(MS_500);
1173 tl.play();
1174 tl.tick(Duration::from_millis(150));
1175
1176 let v = tl.event_value_at(0).unwrap();
1178 assert!((v - 0.5).abs() < 0.02);
1179
1180 let v = tl.event_value_at(1).unwrap();
1182 assert!(v < 0.01);
1183 }
1184
1185 #[test]
1186 fn debug_format_includes_fields() {
1187 let tl = Timeline::new()
1188 .add_labeled("intro", Duration::ZERO, Fade::new(MS_100))
1189 .set_duration(MS_100);
1190
1191 let dbg = format!("{:?}", tl);
1192 assert!(dbg.contains("event_count"));
1193 assert!(dbg.contains("total_duration"));
1194 assert!(dbg.contains("state"));
1195 }
1196
1197 #[test]
1198 fn loop_finite_with_overshoot_tick() {
1199 let mut tl = Timeline::new()
1200 .add(Duration::ZERO, Fade::new(MS_100))
1201 .set_duration(MS_100)
1202 .set_loop_count(LoopCount::Times(1));
1203
1204 tl.play();
1205 tl.tick(Duration::from_millis(150));
1207 assert!(!tl.is_complete());
1209 assert!(tl.current_time() <= MS_100);
1211 }
1212}