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 #[must_use]
317 pub fn progress(&self) -> f32 {
318 if self.events.is_empty() {
319 return 1.0;
320 }
321 let t = self.current_time.as_secs_f64() / self.total_duration.as_secs_f64();
322 (t as f32).clamp(0.0, 1.0)
323 }
324
325 #[must_use]
327 pub fn state(&self) -> PlaybackState {
328 self.state
329 }
330
331 #[must_use]
333 pub fn current_time(&self) -> Duration {
334 self.current_time
335 }
336
337 #[must_use]
339 pub fn duration(&self) -> Duration {
340 self.total_duration
341 }
342
343 #[must_use]
345 pub fn event_count(&self) -> usize {
346 self.events.len()
347 }
348
349 #[must_use]
353 pub fn event_value(&self, label: &str) -> Option<f32> {
354 self.events
355 .iter()
356 .find(|e| e.label.as_deref() == Some(label))
357 .map(|e| e.animation.value())
358 }
359
360 #[must_use]
364 pub fn event_value_at(&self, index: usize) -> Option<f32> {
365 self.events.get(index).map(|e| e.animation.value())
366 }
367}
368
369impl Animation for Timeline {
374 fn tick(&mut self, dt: Duration) {
375 if self.state != PlaybackState::Playing {
376 return;
377 }
378
379 let new_time = self.current_time.saturating_add(dt);
380
381 for event in &mut self.events {
383 if new_time > event.offset && !event.animation.is_complete() {
384 let event_start = event.offset;
386 if self.current_time >= event_start {
387 event.animation.tick(dt);
389 } else {
390 let partial = new_time.saturating_sub(event_start);
392 event.animation.tick(partial);
393 }
394 }
395 }
396
397 self.current_time = new_time;
398
399 if self.current_time >= self.total_duration {
401 match self.loop_count {
402 LoopCount::Once => {
403 self.current_time = self.total_duration;
404 self.state = PlaybackState::Finished;
405 }
406 LoopCount::Times(_) | LoopCount::Infinite => {
407 if self.loops_remaining > 0 {
408 if self.loop_count != LoopCount::Infinite {
409 self.loops_remaining -= 1;
410 }
411 let overshoot = self.current_time.saturating_sub(self.total_duration);
413 self.current_time = Duration::ZERO;
414 for event in &mut self.events {
415 event.animation.reset();
416 }
417 if !overshoot.is_zero() {
419 self.tick(overshoot);
420 }
421 } else {
422 self.current_time = self.total_duration;
423 self.state = PlaybackState::Finished;
424 }
425 }
426 }
427 }
428 }
429
430 fn is_complete(&self) -> bool {
431 self.state == PlaybackState::Finished
432 }
433
434 fn value(&self) -> f32 {
435 self.progress()
436 }
437
438 fn reset(&mut self) {
439 self.current_time = Duration::ZERO;
440 self.loops_remaining = match self.loop_count {
441 LoopCount::Once => 0,
442 LoopCount::Times(n) => n,
443 LoopCount::Infinite => u32::MAX,
444 };
445 self.state = PlaybackState::Idle;
446 for event in &mut self.events {
447 event.animation.reset();
448 }
449 }
450
451 fn overshoot(&self) -> Duration {
452 if self.state == PlaybackState::Finished {
453 self.current_time.saturating_sub(self.total_duration)
454 } else {
455 Duration::ZERO
456 }
457 }
458}
459
460#[cfg(test)]
465mod tests {
466 use super::*;
467 use crate::animation::Fade;
468
469 const MS_100: Duration = Duration::from_millis(100);
470 const MS_200: Duration = Duration::from_millis(200);
471 const MS_250: Duration = Duration::from_millis(250);
472 const MS_300: Duration = Duration::from_millis(300);
473 const MS_500: Duration = Duration::from_millis(500);
474 const SEC_1: Duration = Duration::from_secs(1);
475
476 #[test]
477 fn empty_timeline_is_immediately_complete() {
478 let tl = Timeline::new();
479 assert_eq!(tl.progress(), 1.0);
480 assert_eq!(tl.event_count(), 0);
481 }
482
483 #[test]
484 fn sequential_events() {
485 let mut tl = Timeline::new()
486 .add(Duration::ZERO, Fade::new(MS_200))
487 .add(MS_200, Fade::new(MS_200))
488 .add(Duration::from_millis(400), Fade::new(MS_200))
489 .set_duration(Duration::from_millis(600));
490
491 tl.play();
492
493 tl.tick(MS_100);
495 assert!((tl.event_value_at(0).unwrap() - 0.5).abs() < 0.01);
496 assert!((tl.event_value_at(1).unwrap() - 0.0).abs() < 0.01);
497
498 tl.tick(MS_200);
500 assert!(tl.event_value_at(0).unwrap() > 0.99);
501 assert!((tl.event_value_at(1).unwrap() - 0.5).abs() < 0.01);
502
503 tl.tick(MS_300);
505 assert!(tl.is_complete());
506 assert!((tl.progress() - 1.0).abs() < f32::EPSILON);
507 }
508
509 #[test]
510 fn overlapping_events() {
511 let mut tl = Timeline::new()
512 .add(Duration::ZERO, Fade::new(MS_500))
513 .add(MS_200, Fade::new(MS_500))
514 .set_duration(Duration::from_millis(700));
515
516 tl.play();
517
518 tl.tick(MS_300);
520 assert!((tl.event_value_at(0).unwrap() - 0.6).abs() < 0.02);
521 assert!((tl.event_value_at(1).unwrap() - 0.2).abs() < 0.02);
522 }
523
524 #[test]
525 fn labeled_events_and_seek() {
526 let mut tl = Timeline::new()
527 .add_labeled("intro", Duration::ZERO, Fade::new(MS_500))
528 .add_labeled("main", MS_500, Fade::new(MS_500))
529 .set_duration(SEC_1);
530
531 tl.play();
532
533 assert!(tl.seek_label("main"));
535 assert!(tl.event_value("intro").unwrap() > 0.99);
537 assert!((tl.event_value("main").unwrap() - 0.0).abs() < f32::EPSILON);
539
540 assert!(!tl.seek_label("nonexistent"));
542 }
543
544 #[test]
545 fn loop_finite() {
546 let mut tl = Timeline::new()
547 .add(Duration::ZERO, Fade::new(MS_100))
548 .set_duration(MS_100)
549 .set_loop_count(LoopCount::Times(2));
550
551 tl.play();
552
553 tl.tick(MS_100);
555 assert!(!tl.is_complete());
556 assert_eq!(tl.state(), PlaybackState::Playing);
557
558 tl.tick(MS_100);
560 assert!(!tl.is_complete());
561
562 tl.tick(MS_100);
564 assert!(tl.is_complete());
565 }
566
567 #[test]
568 fn loop_infinite_never_finishes() {
569 let mut tl = Timeline::new()
570 .add(Duration::ZERO, Fade::new(MS_100))
571 .set_duration(MS_100)
572 .set_loop_count(LoopCount::Infinite);
573
574 tl.play();
575
576 for _ in 0..100 {
578 tl.tick(MS_100);
579 }
580 assert!(!tl.is_complete());
581 assert_eq!(tl.state(), PlaybackState::Playing);
582 }
583
584 #[test]
585 fn pause_resume() {
586 let mut tl = Timeline::new()
587 .add(Duration::ZERO, Fade::new(SEC_1))
588 .set_duration(SEC_1);
589
590 tl.play();
591 tl.tick(MS_250);
592
593 tl.pause();
594 assert_eq!(tl.state(), PlaybackState::Paused);
595 let time_at_pause = tl.current_time();
596
597 tl.tick(MS_500);
599 assert_eq!(tl.current_time(), time_at_pause);
600
601 tl.resume();
602 assert_eq!(tl.state(), PlaybackState::Playing);
603
604 tl.tick(MS_250);
606 assert!(tl.current_time() > time_at_pause);
607 }
608
609 #[test]
610 fn seek_clamps_to_duration() {
611 let mut tl = Timeline::new()
612 .add(Duration::ZERO, Fade::new(MS_500))
613 .set_duration(MS_500);
614
615 tl.play();
616 tl.seek(SEC_1); assert_eq!(tl.current_time(), MS_500);
618 }
619
620 #[test]
621 fn seek_resets_and_reticks_animations() {
622 let mut tl = Timeline::new()
623 .add(Duration::ZERO, Fade::new(SEC_1))
624 .set_duration(SEC_1);
625
626 tl.play();
627 tl.tick(MS_500);
628 assert!((tl.event_value_at(0).unwrap() - 0.5).abs() < 0.02);
630
631 tl.seek(MS_250);
633 assert!((tl.event_value_at(0).unwrap() - 0.25).abs() < 0.02);
634 }
635
636 #[test]
637 fn stop_resets_everything() {
638 let mut tl = Timeline::new()
639 .add(Duration::ZERO, Fade::new(SEC_1))
640 .set_duration(SEC_1);
641
642 tl.play();
643 tl.tick(MS_500);
644 tl.stop();
645
646 assert_eq!(tl.state(), PlaybackState::Idle);
647 assert_eq!(tl.current_time(), Duration::ZERO);
648 assert!((tl.event_value_at(0).unwrap() - 0.0).abs() < f32::EPSILON);
649 }
650
651 #[test]
652 fn play_restarts_from_beginning() {
653 let mut tl = Timeline::new()
654 .add(Duration::ZERO, Fade::new(SEC_1))
655 .set_duration(SEC_1);
656
657 tl.play();
658 tl.tick(SEC_1);
659 assert!(tl.is_complete());
660
661 tl.play();
662 assert_eq!(tl.state(), PlaybackState::Playing);
663 assert_eq!(tl.current_time(), Duration::ZERO);
664 assert!((tl.event_value_at(0).unwrap() - 0.0).abs() < f32::EPSILON);
665 }
666
667 #[test]
668 fn then_chains_at_same_offset() {
669 let tl = Timeline::new()
670 .add(MS_100, Fade::new(MS_100))
671 .then(Fade::new(MS_100)); assert_eq!(tl.event_count(), 2);
674 }
675
676 #[test]
677 fn progress_tracks_time() {
678 let mut tl = Timeline::new()
679 .add(Duration::ZERO, Fade::new(SEC_1))
680 .set_duration(SEC_1);
681
682 tl.play();
683 assert!((tl.progress() - 0.0).abs() < f32::EPSILON);
684
685 tl.tick(MS_250);
686 assert!((tl.progress() - 0.25).abs() < 0.02);
687
688 tl.tick(MS_250);
689 assert!((tl.progress() - 0.5).abs() < 0.02);
690 }
691
692 #[test]
693 fn animation_trait_value_matches_progress() {
694 let mut tl = Timeline::new()
695 .add(Duration::ZERO, Fade::new(SEC_1))
696 .set_duration(SEC_1);
697
698 tl.play();
699 tl.tick(MS_500);
700
701 assert!((tl.value() - tl.progress()).abs() < f32::EPSILON);
702 }
703
704 #[test]
705 fn animation_trait_reset() {
706 let mut tl = Timeline::new()
707 .add(Duration::ZERO, Fade::new(SEC_1))
708 .set_duration(SEC_1);
709
710 tl.play();
711 tl.tick(SEC_1);
712 assert!(tl.is_complete());
713
714 tl.reset();
715 assert_eq!(tl.state(), PlaybackState::Idle);
716 assert!(!tl.is_complete());
717 }
718
719 #[test]
720 fn debug_format() {
721 let tl = Timeline::new()
722 .add(Duration::ZERO, Fade::new(MS_100))
723 .set_duration(MS_100);
724
725 let dbg = format!("{:?}", tl);
726 assert!(dbg.contains("Timeline"));
727 assert!(dbg.contains("event_count"));
728 }
729
730 #[test]
731 fn loop_once_plays_exactly_once() {
732 let mut tl = Timeline::new()
733 .add(Duration::ZERO, Fade::new(MS_100))
734 .set_duration(MS_100)
735 .set_loop_count(LoopCount::Once);
736
737 tl.play();
738 tl.tick(MS_100);
739 assert!(tl.is_complete());
740 }
741
742 #[test]
743 fn event_value_by_label_missing_returns_none() {
744 let tl = Timeline::new()
745 .add(Duration::ZERO, Fade::new(MS_100))
746 .set_duration(MS_100);
747
748 assert!(tl.event_value("nope").is_none());
749 }
750
751 #[test]
752 fn event_value_at_out_of_bounds() {
753 let tl = Timeline::new()
754 .add(Duration::ZERO, Fade::new(MS_100))
755 .set_duration(MS_100);
756
757 assert!(tl.event_value_at(5).is_none());
758 }
759}