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 let t = self.current_time.as_secs_f64() / self.total_duration.as_secs_f64();
323 (t as f32).clamp(0.0, 1.0)
324 }
325
326 #[inline]
328 #[must_use]
329 pub fn state(&self) -> PlaybackState {
330 self.state
331 }
332
333 #[inline]
335 #[must_use]
336 pub fn current_time(&self) -> Duration {
337 self.current_time
338 }
339
340 #[inline]
342 #[must_use]
343 pub fn duration(&self) -> Duration {
344 self.total_duration
345 }
346
347 #[inline]
349 #[must_use]
350 pub fn event_count(&self) -> usize {
351 self.events.len()
352 }
353
354 #[must_use]
358 pub fn event_value(&self, label: &str) -> Option<f32> {
359 self.events
360 .iter()
361 .find(|e| e.label.as_deref() == Some(label))
362 .map(|e| e.animation.value())
363 }
364
365 #[must_use]
369 pub fn event_value_at(&self, index: usize) -> Option<f32> {
370 self.events.get(index).map(|e| e.animation.value())
371 }
372}
373
374impl Animation for Timeline {
379 fn tick(&mut self, dt: Duration) {
380 if self.state != PlaybackState::Playing {
381 return;
382 }
383
384 let new_time = self.current_time.saturating_add(dt);
385
386 for event in &mut self.events {
388 if new_time > event.offset && !event.animation.is_complete() {
389 let event_start = event.offset;
391 if self.current_time >= event_start {
392 event.animation.tick(dt);
394 } else {
395 let partial = new_time.saturating_sub(event_start);
397 event.animation.tick(partial);
398 }
399 }
400 }
401
402 self.current_time = new_time;
403
404 if self.current_time >= self.total_duration {
406 match self.loop_count {
407 LoopCount::Once => {
408 self.current_time = self.total_duration;
409 self.state = PlaybackState::Finished;
410 }
411 LoopCount::Times(_) | LoopCount::Infinite => {
412 if self.loops_remaining > 0 {
413 if self.loop_count != LoopCount::Infinite {
414 self.loops_remaining -= 1;
415 }
416 let overshoot = self.current_time.saturating_sub(self.total_duration);
418 self.current_time = Duration::ZERO;
419 for event in &mut self.events {
420 event.animation.reset();
421 }
422 if !overshoot.is_zero() {
424 self.tick(overshoot);
425 }
426 } else {
427 self.current_time = self.total_duration;
428 self.state = PlaybackState::Finished;
429 }
430 }
431 }
432 }
433 }
434
435 fn is_complete(&self) -> bool {
436 self.state == PlaybackState::Finished
437 }
438
439 fn value(&self) -> f32 {
440 self.progress()
441 }
442
443 fn reset(&mut self) {
444 self.current_time = Duration::ZERO;
445 self.loops_remaining = match self.loop_count {
446 LoopCount::Once => 0,
447 LoopCount::Times(n) => n,
448 LoopCount::Infinite => u32::MAX,
449 };
450 self.state = PlaybackState::Idle;
451 for event in &mut self.events {
452 event.animation.reset();
453 }
454 }
455
456 fn overshoot(&self) -> Duration {
457 if self.state == PlaybackState::Finished {
458 self.current_time.saturating_sub(self.total_duration)
459 } else {
460 Duration::ZERO
461 }
462 }
463}
464
465#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::animation::Fade;
473
474 const MS_100: Duration = Duration::from_millis(100);
475 const MS_200: Duration = Duration::from_millis(200);
476 const MS_250: Duration = Duration::from_millis(250);
477 const MS_300: Duration = Duration::from_millis(300);
478 const MS_500: Duration = Duration::from_millis(500);
479 const SEC_1: Duration = Duration::from_secs(1);
480
481 #[test]
482 fn empty_timeline_is_immediately_complete() {
483 let tl = Timeline::new();
484 assert_eq!(tl.progress(), 1.0);
485 assert_eq!(tl.event_count(), 0);
486 }
487
488 #[test]
489 fn sequential_events() {
490 let mut tl = Timeline::new()
491 .add(Duration::ZERO, Fade::new(MS_200))
492 .add(MS_200, Fade::new(MS_200))
493 .add(Duration::from_millis(400), Fade::new(MS_200))
494 .set_duration(Duration::from_millis(600));
495
496 tl.play();
497
498 tl.tick(MS_100);
500 assert!((tl.event_value_at(0).unwrap() - 0.5).abs() < 0.01);
501 assert!((tl.event_value_at(1).unwrap() - 0.0).abs() < 0.01);
502
503 tl.tick(MS_200);
505 assert!(tl.event_value_at(0).unwrap() > 0.99);
506 assert!((tl.event_value_at(1).unwrap() - 0.5).abs() < 0.01);
507
508 tl.tick(MS_300);
510 assert!(tl.is_complete());
511 assert!((tl.progress() - 1.0).abs() < f32::EPSILON);
512 }
513
514 #[test]
515 fn overlapping_events() {
516 let mut tl = Timeline::new()
517 .add(Duration::ZERO, Fade::new(MS_500))
518 .add(MS_200, Fade::new(MS_500))
519 .set_duration(Duration::from_millis(700));
520
521 tl.play();
522
523 tl.tick(MS_300);
525 assert!((tl.event_value_at(0).unwrap() - 0.6).abs() < 0.02);
526 assert!((tl.event_value_at(1).unwrap() - 0.2).abs() < 0.02);
527 }
528
529 #[test]
530 fn labeled_events_and_seek() {
531 let mut tl = Timeline::new()
532 .add_labeled("intro", Duration::ZERO, Fade::new(MS_500))
533 .add_labeled("main", MS_500, Fade::new(MS_500))
534 .set_duration(SEC_1);
535
536 tl.play();
537
538 assert!(tl.seek_label("main"));
540 assert!(tl.event_value("intro").unwrap() > 0.99);
542 assert!((tl.event_value("main").unwrap() - 0.0).abs() < f32::EPSILON);
544
545 assert!(!tl.seek_label("nonexistent"));
547 }
548
549 #[test]
550 fn loop_finite() {
551 let mut tl = Timeline::new()
552 .add(Duration::ZERO, Fade::new(MS_100))
553 .set_duration(MS_100)
554 .set_loop_count(LoopCount::Times(2));
555
556 tl.play();
557
558 tl.tick(MS_100);
560 assert!(!tl.is_complete());
561 assert_eq!(tl.state(), PlaybackState::Playing);
562
563 tl.tick(MS_100);
565 assert!(!tl.is_complete());
566
567 tl.tick(MS_100);
569 assert!(tl.is_complete());
570 }
571
572 #[test]
573 fn loop_infinite_never_finishes() {
574 let mut tl = Timeline::new()
575 .add(Duration::ZERO, Fade::new(MS_100))
576 .set_duration(MS_100)
577 .set_loop_count(LoopCount::Infinite);
578
579 tl.play();
580
581 for _ in 0..100 {
583 tl.tick(MS_100);
584 }
585 assert!(!tl.is_complete());
586 assert_eq!(tl.state(), PlaybackState::Playing);
587 }
588
589 #[test]
590 fn pause_resume() {
591 let mut tl = Timeline::new()
592 .add(Duration::ZERO, Fade::new(SEC_1))
593 .set_duration(SEC_1);
594
595 tl.play();
596 tl.tick(MS_250);
597
598 tl.pause();
599 assert_eq!(tl.state(), PlaybackState::Paused);
600 let time_at_pause = tl.current_time();
601
602 tl.tick(MS_500);
604 assert_eq!(tl.current_time(), time_at_pause);
605
606 tl.resume();
607 assert_eq!(tl.state(), PlaybackState::Playing);
608
609 tl.tick(MS_250);
611 assert!(tl.current_time() > time_at_pause);
612 }
613
614 #[test]
615 fn seek_clamps_to_duration() {
616 let mut tl = Timeline::new()
617 .add(Duration::ZERO, Fade::new(MS_500))
618 .set_duration(MS_500);
619
620 tl.play();
621 tl.seek(SEC_1); assert_eq!(tl.current_time(), MS_500);
623 }
624
625 #[test]
626 fn seek_resets_and_reticks_animations() {
627 let mut tl = Timeline::new()
628 .add(Duration::ZERO, Fade::new(SEC_1))
629 .set_duration(SEC_1);
630
631 tl.play();
632 tl.tick(MS_500);
633 assert!((tl.event_value_at(0).unwrap() - 0.5).abs() < 0.02);
635
636 tl.seek(MS_250);
638 assert!((tl.event_value_at(0).unwrap() - 0.25).abs() < 0.02);
639 }
640
641 #[test]
642 fn stop_resets_everything() {
643 let mut tl = Timeline::new()
644 .add(Duration::ZERO, Fade::new(SEC_1))
645 .set_duration(SEC_1);
646
647 tl.play();
648 tl.tick(MS_500);
649 tl.stop();
650
651 assert_eq!(tl.state(), PlaybackState::Idle);
652 assert_eq!(tl.current_time(), Duration::ZERO);
653 assert!((tl.event_value_at(0).unwrap() - 0.0).abs() < f32::EPSILON);
654 }
655
656 #[test]
657 fn play_restarts_from_beginning() {
658 let mut tl = Timeline::new()
659 .add(Duration::ZERO, Fade::new(SEC_1))
660 .set_duration(SEC_1);
661
662 tl.play();
663 tl.tick(SEC_1);
664 assert!(tl.is_complete());
665
666 tl.play();
667 assert_eq!(tl.state(), PlaybackState::Playing);
668 assert_eq!(tl.current_time(), Duration::ZERO);
669 assert!((tl.event_value_at(0).unwrap() - 0.0).abs() < f32::EPSILON);
670 }
671
672 #[test]
673 fn then_chains_at_same_offset() {
674 let tl = Timeline::new()
675 .add(MS_100, Fade::new(MS_100))
676 .then(Fade::new(MS_100)); assert_eq!(tl.event_count(), 2);
679 }
680
681 #[test]
682 fn progress_tracks_time() {
683 let mut tl = Timeline::new()
684 .add(Duration::ZERO, Fade::new(SEC_1))
685 .set_duration(SEC_1);
686
687 tl.play();
688 assert!((tl.progress() - 0.0).abs() < f32::EPSILON);
689
690 tl.tick(MS_250);
691 assert!((tl.progress() - 0.25).abs() < 0.02);
692
693 tl.tick(MS_250);
694 assert!((tl.progress() - 0.5).abs() < 0.02);
695 }
696
697 #[test]
698 fn animation_trait_value_matches_progress() {
699 let mut tl = Timeline::new()
700 .add(Duration::ZERO, Fade::new(SEC_1))
701 .set_duration(SEC_1);
702
703 tl.play();
704 tl.tick(MS_500);
705
706 assert!((tl.value() - tl.progress()).abs() < f32::EPSILON);
707 }
708
709 #[test]
710 fn animation_trait_reset() {
711 let mut tl = Timeline::new()
712 .add(Duration::ZERO, Fade::new(SEC_1))
713 .set_duration(SEC_1);
714
715 tl.play();
716 tl.tick(SEC_1);
717 assert!(tl.is_complete());
718
719 tl.reset();
720 assert_eq!(tl.state(), PlaybackState::Idle);
721 assert!(!tl.is_complete());
722 }
723
724 #[test]
725 fn debug_format() {
726 let tl = Timeline::new()
727 .add(Duration::ZERO, Fade::new(MS_100))
728 .set_duration(MS_100);
729
730 let dbg = format!("{:?}", tl);
731 assert!(dbg.contains("Timeline"));
732 assert!(dbg.contains("event_count"));
733 }
734
735 #[test]
736 fn loop_once_plays_exactly_once() {
737 let mut tl = Timeline::new()
738 .add(Duration::ZERO, Fade::new(MS_100))
739 .set_duration(MS_100)
740 .set_loop_count(LoopCount::Once);
741
742 tl.play();
743 tl.tick(MS_100);
744 assert!(tl.is_complete());
745 }
746
747 #[test]
748 fn event_value_by_label_missing_returns_none() {
749 let tl = Timeline::new()
750 .add(Duration::ZERO, Fade::new(MS_100))
751 .set_duration(MS_100);
752
753 assert!(tl.event_value("nope").is_none());
754 }
755
756 #[test]
757 fn event_value_at_out_of_bounds() {
758 let tl = Timeline::new()
759 .add(Duration::ZERO, Fade::new(MS_100))
760 .set_duration(MS_100);
761
762 assert!(tl.event_value_at(5).is_none());
763 }
764
765 #[test]
766 fn idle_timeline_value_is_zero() {
767 let tl = Timeline::new()
768 .add(Duration::ZERO, Fade::new(MS_500))
769 .set_duration(MS_500);
770
771 assert!((tl.value() - 0.0).abs() < f32::EPSILON);
773 assert_eq!(tl.state(), PlaybackState::Idle);
774 }
775
776 #[test]
777 fn overshoot_is_zero_while_playing() {
778 let mut tl = Timeline::new()
779 .add(Duration::ZERO, Fade::new(MS_500))
780 .set_duration(MS_500);
781
782 tl.play();
783 tl.tick(MS_250);
784
785 assert_eq!(tl.overshoot(), Duration::ZERO);
786 }
787
788 #[test]
789 fn seek_to_zero_resets_animations() {
790 let mut tl = Timeline::new()
791 .add(Duration::ZERO, Fade::new(MS_500))
792 .set_duration(MS_500);
793
794 tl.play();
795 tl.tick(MS_250);
796 assert!(tl.event_value_at(0).unwrap() > 0.0);
797
798 tl.seek(Duration::ZERO);
799 assert_eq!(tl.current_time(), Duration::ZERO);
800 }
801
802 #[test]
805 fn default_trait() {
806 let tl = Timeline::default();
807 assert_eq!(tl.event_count(), 0);
808 assert_eq!(tl.state(), PlaybackState::Idle);
809 assert_eq!(tl.progress(), 1.0); }
811
812 #[test]
813 fn zero_duration_clamped_to_1ns() {
814 let tl = Timeline::new()
815 .add(Duration::ZERO, Fade::new(MS_100))
816 .set_duration(Duration::ZERO);
817 assert_eq!(tl.duration(), Duration::from_nanos(1));
818 }
819
820 #[test]
821 fn then_on_empty_timeline_uses_zero_offset() {
822 let tl = Timeline::new().then(Fade::new(MS_100));
823 assert_eq!(tl.event_count(), 1);
824 assert_eq!(tl.duration(), Duration::from_nanos(1));
826 }
827
828 #[test]
829 fn pause_when_not_playing_is_noop() {
830 let mut tl = Timeline::new()
831 .add(Duration::ZERO, Fade::new(MS_100))
832 .set_duration(MS_100);
833
834 tl.pause();
836 assert_eq!(tl.state(), PlaybackState::Idle);
837
838 tl.play();
840 tl.tick(MS_100);
841 assert_eq!(tl.state(), PlaybackState::Finished);
842 tl.pause();
843 assert_eq!(tl.state(), PlaybackState::Finished);
844 }
845
846 #[test]
847 fn resume_when_not_paused_is_noop() {
848 let mut tl = Timeline::new()
849 .add(Duration::ZERO, Fade::new(MS_100))
850 .set_duration(MS_100);
851
852 tl.resume();
854 assert_eq!(tl.state(), PlaybackState::Idle);
855
856 tl.play();
858 tl.resume();
859 assert_eq!(tl.state(), PlaybackState::Playing);
860 }
861
862 #[test]
863 fn seek_from_idle_transitions_to_paused() {
864 let mut tl = Timeline::new()
865 .add(Duration::ZERO, Fade::new(MS_500))
866 .set_duration(MS_500);
867
868 assert_eq!(tl.state(), PlaybackState::Idle);
869 tl.seek(MS_250);
870 assert_eq!(tl.state(), PlaybackState::Paused);
871 assert_eq!(tl.current_time(), MS_250);
872 }
873
874 #[test]
875 fn seek_from_finished_transitions_to_paused() {
876 let mut tl = Timeline::new()
877 .add(Duration::ZERO, Fade::new(MS_500))
878 .set_duration(MS_500);
879
880 tl.play();
881 tl.tick(MS_500);
882 assert_eq!(tl.state(), PlaybackState::Finished);
883
884 tl.seek(MS_250);
885 assert_eq!(tl.state(), PlaybackState::Paused);
886 assert_eq!(tl.current_time(), MS_250);
887 }
888
889 #[test]
890 fn seek_from_playing_stays_playing() {
891 let mut tl = Timeline::new()
892 .add(Duration::ZERO, Fade::new(MS_500))
893 .set_duration(MS_500);
894
895 tl.play();
896 tl.tick(MS_100);
897 assert_eq!(tl.state(), PlaybackState::Playing);
898
899 tl.seek(MS_300);
900 assert_eq!(tl.state(), PlaybackState::Playing);
902 }
903
904 #[test]
905 fn overshoot_when_finished() {
906 let mut tl = Timeline::new()
907 .add(Duration::ZERO, Fade::new(MS_100))
908 .set_duration(MS_100);
909
910 tl.play();
911 tl.tick(MS_100);
912 assert!(tl.is_complete());
913 assert_eq!(tl.overshoot(), Duration::ZERO);
915 }
916
917 #[test]
918 fn tick_when_idle_does_not_advance() {
919 let mut tl = Timeline::new()
920 .add(Duration::ZERO, Fade::new(MS_500))
921 .set_duration(MS_500);
922
923 tl.tick(MS_250);
924 assert_eq!(tl.current_time(), Duration::ZERO);
925 assert_eq!(tl.state(), PlaybackState::Idle);
926 }
927
928 #[test]
929 fn tick_when_paused_does_not_advance() {
930 let mut tl = Timeline::new()
931 .add(Duration::ZERO, Fade::new(SEC_1))
932 .set_duration(SEC_1);
933
934 tl.play();
935 tl.tick(MS_250);
936 tl.pause();
937 let paused_time = tl.current_time();
938
939 tl.tick(MS_500);
940 assert_eq!(tl.current_time(), paused_time);
941 }
942
943 #[test]
944 fn tick_when_finished_does_not_advance() {
945 let mut tl = Timeline::new()
946 .add(Duration::ZERO, Fade::new(MS_100))
947 .set_duration(MS_100);
948
949 tl.play();
950 tl.tick(MS_100);
951 assert!(tl.is_complete());
952
953 let time_at_finish = tl.current_time();
954 tl.tick(MS_500);
955 assert_eq!(tl.current_time(), time_at_finish);
956 }
957
958 #[test]
959 fn multiple_events_at_same_offset() {
960 let mut tl = Timeline::new()
961 .add(Duration::ZERO, Fade::new(MS_200))
962 .add(Duration::ZERO, Fade::new(MS_200))
963 .add(Duration::ZERO, Fade::new(MS_200))
964 .set_duration(MS_200);
965
966 assert_eq!(tl.event_count(), 3);
967 tl.play();
968 tl.tick(MS_100);
969
970 for i in 0..3 {
972 assert!(
973 (tl.event_value_at(i).unwrap() - 0.5).abs() < 0.02,
974 "event {i} should be at ~50%"
975 );
976 }
977 }
978
979 #[test]
980 fn auto_computed_duration_uses_max_offset() {
981 let tl = Timeline::new()
982 .add(MS_100, Fade::new(MS_100))
983 .add(MS_500, Fade::new(MS_100))
984 .add(MS_300, Fade::new(MS_100));
985
986 assert_eq!(tl.duration(), MS_500);
989 }
990
991 #[test]
992 fn explicit_duration_overrides_auto() {
993 let tl = Timeline::new()
994 .add(MS_100, Fade::new(MS_100))
995 .add(MS_500, Fade::new(MS_100))
996 .set_duration(SEC_1);
997
998 assert_eq!(tl.duration(), SEC_1);
999 }
1000
1001 #[test]
1002 fn seek_label_on_empty_timeline() {
1003 let mut tl = Timeline::new();
1004 assert!(!tl.seek_label("foo"));
1005 }
1006
1007 #[test]
1008 fn event_value_at_on_empty_timeline() {
1009 let tl = Timeline::new();
1010 assert!(tl.event_value_at(0).is_none());
1011 }
1012
1013 #[test]
1014 fn loop_times_zero_plays_once() {
1015 let mut tl = Timeline::new()
1016 .add(Duration::ZERO, Fade::new(MS_100))
1017 .set_duration(MS_100)
1018 .set_loop_count(LoopCount::Times(0));
1019
1020 tl.play();
1021 tl.tick(MS_100);
1022 assert!(tl.is_complete());
1023 }
1024
1025 #[test]
1026 fn loop_count_eq() {
1027 assert_eq!(LoopCount::Once, LoopCount::Once);
1028 assert_eq!(LoopCount::Times(5), LoopCount::Times(5));
1029 assert_ne!(LoopCount::Times(5), LoopCount::Times(3));
1030 assert_eq!(LoopCount::Infinite, LoopCount::Infinite);
1031 assert_ne!(LoopCount::Once, LoopCount::Infinite);
1032 }
1033
1034 #[test]
1035 fn playback_state_eq() {
1036 assert_eq!(PlaybackState::Idle, PlaybackState::Idle);
1037 assert_eq!(PlaybackState::Playing, PlaybackState::Playing);
1038 assert_eq!(PlaybackState::Paused, PlaybackState::Paused);
1039 assert_eq!(PlaybackState::Finished, PlaybackState::Finished);
1040 assert_ne!(PlaybackState::Idle, PlaybackState::Playing);
1041 }
1042
1043 #[test]
1044 fn loop_count_clone() {
1045 let lc = LoopCount::Times(3);
1046 let lc2 = lc;
1047 assert_eq!(lc, lc2);
1048 }
1049
1050 #[test]
1051 fn playback_state_clone() {
1052 let ps = PlaybackState::Paused;
1053 let ps2 = ps;
1054 assert_eq!(ps, ps2);
1055 }
1056
1057 #[test]
1058 fn play_after_stop_resets() {
1059 let mut tl = Timeline::new()
1060 .add(Duration::ZERO, Fade::new(MS_500))
1061 .set_duration(MS_500);
1062
1063 tl.play();
1064 tl.tick(MS_250);
1065 tl.stop();
1066 assert_eq!(tl.state(), PlaybackState::Idle);
1067
1068 tl.play();
1069 assert_eq!(tl.state(), PlaybackState::Playing);
1070 assert_eq!(tl.current_time(), Duration::ZERO);
1071 }
1072
1073 #[test]
1074 fn seek_past_end_then_resume_and_tick_finishes() {
1075 let mut tl = Timeline::new()
1076 .add(Duration::ZERO, Fade::new(MS_100))
1077 .set_duration(MS_100);
1078
1079 tl.play();
1080 tl.seek(MS_100); tl.resume(); tl.tick(Duration::from_nanos(1));
1085 assert!(tl.is_complete());
1086 }
1087
1088 #[test]
1089 fn progress_clamps_to_zero_one() {
1090 let mut tl = Timeline::new()
1091 .add(Duration::ZERO, Fade::new(MS_100))
1092 .set_duration(MS_100);
1093
1094 assert!(tl.progress() >= 0.0);
1096 assert!(tl.progress() <= 1.0);
1097
1098 tl.play();
1099 tl.tick(MS_100);
1100 assert!(tl.progress() >= 0.0);
1101 assert!(tl.progress() <= 1.0);
1102 }
1103
1104 #[test]
1105 fn animation_trait_is_complete_false_while_playing() {
1106 let mut tl = Timeline::new()
1107 .add(Duration::ZERO, Fade::new(MS_500))
1108 .set_duration(MS_500);
1109
1110 tl.play();
1111 tl.tick(MS_250);
1112 assert!(!tl.is_complete());
1113 }
1114
1115 #[test]
1116 fn reset_from_finished() {
1117 let mut tl = Timeline::new()
1118 .add(Duration::ZERO, Fade::new(MS_100))
1119 .set_duration(MS_100);
1120
1121 tl.play();
1122 tl.tick(MS_100);
1123 assert!(tl.is_complete());
1124
1125 tl.reset();
1126 assert_eq!(tl.state(), PlaybackState::Idle);
1127 assert_eq!(tl.current_time(), Duration::ZERO);
1128 assert!(!tl.is_complete());
1129 }
1130
1131 #[test]
1132 fn reset_from_paused() {
1133 let mut tl = Timeline::new()
1134 .add(Duration::ZERO, Fade::new(MS_500))
1135 .set_duration(MS_500);
1136
1137 tl.play();
1138 tl.tick(MS_250);
1139 tl.pause();
1140 assert_eq!(tl.state(), PlaybackState::Paused);
1141
1142 tl.reset();
1143 assert_eq!(tl.state(), PlaybackState::Idle);
1144 assert_eq!(tl.current_time(), Duration::ZERO);
1145 }
1146
1147 #[test]
1148 fn labeled_event_value() {
1149 let mut tl = Timeline::new()
1150 .add_labeled("fade", Duration::ZERO, Fade::new(MS_200))
1151 .set_duration(MS_200);
1152
1153 tl.play();
1154 tl.tick(MS_100);
1155
1156 let v = tl.event_value("fade").unwrap();
1157 assert!((v - 0.5).abs() < 0.02);
1158 }
1159
1160 #[test]
1161 fn events_sorted_by_offset_on_insert() {
1162 let tl = Timeline::new()
1163 .add(MS_500, Fade::new(MS_100))
1164 .add(MS_100, Fade::new(MS_100))
1165 .add(MS_300, Fade::new(MS_100));
1166
1167 let mut tl = tl.set_duration(MS_500);
1170 tl.play();
1171 tl.tick(Duration::from_millis(150));
1172
1173 let v = tl.event_value_at(0).unwrap();
1175 assert!((v - 0.5).abs() < 0.02);
1176
1177 let v = tl.event_value_at(1).unwrap();
1179 assert!(v < 0.01);
1180 }
1181
1182 #[test]
1183 fn debug_format_includes_fields() {
1184 let tl = Timeline::new()
1185 .add_labeled("intro", Duration::ZERO, Fade::new(MS_100))
1186 .set_duration(MS_100);
1187
1188 let dbg = format!("{:?}", tl);
1189 assert!(dbg.contains("event_count"));
1190 assert!(dbg.contains("total_duration"));
1191 assert!(dbg.contains("state"));
1192 }
1193
1194 #[test]
1195 fn loop_finite_with_overshoot_tick() {
1196 let mut tl = Timeline::new()
1197 .add(Duration::ZERO, Fade::new(MS_100))
1198 .set_duration(MS_100)
1199 .set_loop_count(LoopCount::Times(1));
1200
1201 tl.play();
1202 tl.tick(Duration::from_millis(150));
1204 assert!(!tl.is_complete());
1206 assert!(tl.current_time() <= MS_100);
1208 }
1209}