Another project
1use core::num::NonZeroU32;
2use std::time::Duration;
3
4use bone_ui::input::FrameInstant;
5use serde::{Deserialize, Serialize};
6
7#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(try_from = "u32", into = "u32")]
9pub struct FrameCount(NonZeroU32);
10
11#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
12#[error("frame count must be a positive integer, got {0}")]
13pub struct ZeroFrameCount(u32);
14
15impl FrameCount {
16 pub const ONE: Self = Self(NonZeroU32::MIN);
17
18 #[must_use]
19 pub const fn new(frames: NonZeroU32) -> Self {
20 Self(frames)
21 }
22
23 #[must_use]
24 pub const fn get(self) -> u32 {
25 self.0.get()
26 }
27}
28
29impl TryFrom<u32> for FrameCount {
30 type Error = ZeroFrameCount;
31
32 fn try_from(value: u32) -> Result<Self, Self::Error> {
33 NonZeroU32::new(value)
34 .map(Self)
35 .ok_or(ZeroFrameCount(value))
36 }
37}
38
39impl From<FrameCount> for u32 {
40 fn from(value: FrameCount) -> Self {
41 value.0.get()
42 }
43}
44
45#[derive(Copy, Clone, Debug, PartialEq, Eq)]
46pub struct FrameClock(FrameInstant);
47
48impl FrameClock {
49 pub const FIXED_STEP: Duration = Duration::from_micros(16_667);
50
51 #[must_use]
52 pub const fn start() -> Self {
53 Self(FrameInstant::ZERO)
54 }
55
56 #[must_use]
57 pub const fn now(self) -> FrameInstant {
58 self.0
59 }
60
61 pub fn feed(&mut self, now: FrameInstant) {
62 self.0 = self.0.max(now);
63 }
64
65 pub fn advance(&mut self, frames: FrameCount) {
66 self.0 = self.0.after(Self::FIXED_STEP.saturating_mul(frames.get()));
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 fn frames(n: u32) -> FrameCount {
75 match NonZeroU32::new(n) {
76 Some(count) => FrameCount::new(count),
77 None => panic!("frame count must be nonzero"),
78 }
79 }
80
81 #[test]
82 fn advance_steps_exactly_one_fixed_step_per_frame() {
83 let mut clock = FrameClock::start();
84 clock.advance(frames(60));
85 assert_eq!(
86 clock.now().duration(),
87 FrameClock::FIXED_STEP * 60,
88 "sixty fixed steps land on a bit-exact instant"
89 );
90 }
91
92 #[test]
93 fn feed_never_rewinds() {
94 let mut clock = FrameClock::start();
95 clock.feed(FrameInstant::from_duration(Duration::from_millis(100)));
96 clock.feed(FrameInstant::from_duration(Duration::from_millis(40)));
97 assert_eq!(
98 clock.now(),
99 FrameInstant::from_duration(Duration::from_millis(100)),
100 "a stale feed must not move time backward"
101 );
102 }
103
104 #[test]
105 fn a_180ms_animation_finishes_on_frame_11() {
106 let finished_at = (1_u32..=20).find(|frame| {
107 let mut clock = FrameClock::start();
108 clock.advance(frames(*frame));
109 clock.now().duration() >= Duration::from_millis(180)
110 });
111 assert_eq!(
112 finished_at,
113 Some(11),
114 "a 180 ms tween completes on a known frame under fixed step"
115 );
116 }
117}