Skip to main content

sequoia_view/
clock.rs

1use std::time::{Duration, Instant};
2
3/// Largest wall-clock step advanced in one frame.
4///
5/// A paused or occluded window can produce arbitrarily large deltas. Tweens
6/// catch up from their absolute start time, while springs integrate an
7/// accumulator so they keep a stable step size. Capping the accumulator avoids
8/// unbounded work after a long stall.
9pub const MAX_FRAME_STEP_SECS: f32 = 0.25;
10
11/// Shared frame clock owned by the runtime.
12///
13/// Every view animation samples this clock instead of calling
14/// `Instant::now()` on its own. That gives one coherent timestamp per frame,
15/// deterministic tests, global speed control and reduced-motion support.
16#[derive(Clone, Copy, Debug)]
17pub struct FrameClock {
18    origin: Instant,
19    last_wall: Instant,
20    elapsed: Duration,
21    dt: Duration,
22    frame: u64,
23    speed: f32,
24    reduced_motion: bool,
25    manual: bool,
26}
27
28impl Default for FrameClock {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl FrameClock {
35    pub fn new() -> Self {
36        let now = Instant::now();
37        Self {
38            origin: now,
39            last_wall: now,
40            elapsed: Duration::ZERO,
41            dt: Duration::ZERO,
42            frame: 0,
43            speed: 1.0,
44            reduced_motion: false,
45            manual: false,
46        }
47    }
48
49    /// Current animation time. This is not affected by wall-clock jumps.
50    pub fn now(&self) -> Instant {
51        self.origin + self.elapsed
52    }
53
54    pub fn elapsed(&self) -> Duration {
55        self.elapsed
56    }
57
58    pub fn elapsed_secs(&self) -> f32 {
59        self.elapsed.as_secs_f32()
60    }
61
62    pub fn dt(&self) -> Duration {
63        self.dt
64    }
65
66    pub fn dt_secs(&self) -> f32 {
67        self.dt.as_secs_f32()
68    }
69
70    pub fn frame(&self) -> u64 {
71        self.frame
72    }
73
74    pub fn speed(&self) -> f32 {
75        self.speed
76    }
77
78    pub fn set_speed(&mut self, speed: f32) {
79        self.speed = if speed.is_finite() {
80            speed.max(0.0)
81        } else {
82            1.0
83        };
84    }
85
86    pub fn reduced_motion(&self) -> bool {
87        self.reduced_motion
88    }
89
90    pub fn set_reduced_motion(&mut self, reduced: bool) {
91        self.reduced_motion = reduced;
92    }
93
94    pub fn is_manual(&self) -> bool {
95        self.manual
96    }
97
98    pub fn set_manual(&mut self, manual: bool) {
99        self.manual = manual;
100        self.last_wall = Instant::now();
101        self.dt = Duration::ZERO;
102    }
103
104    /// Advance from the wall clock. Manual clocks do not advance implicitly.
105    pub fn advance(&mut self) {
106        if self.manual {
107            self.dt = Duration::ZERO;
108            return;
109        }
110        let now = Instant::now();
111        let wall = now.saturating_duration_since(self.last_wall);
112        self.last_wall = now;
113        let scaled = if self.reduced_motion {
114            Duration::ZERO
115        } else {
116            wall.mul_f32(self.speed)
117        };
118        let capped = scaled.min(Duration::from_secs_f32(MAX_FRAME_STEP_SECS));
119        self.elapsed += capped;
120        self.dt = capped;
121        self.frame = self.frame.wrapping_add(1);
122    }
123
124    /// Advance by an explicit duration. Used by tests and deterministic hosts.
125    pub fn advance_by(&mut self, dt: Duration) {
126        let scaled = if self.reduced_motion {
127            Duration::ZERO
128        } else {
129            dt.mul_f32(self.speed)
130        };
131        self.elapsed += scaled;
132        self.dt = scaled;
133        self.frame = self.frame.wrapping_add(1);
134    }
135
136    pub fn reset(&mut self) {
137        let now = Instant::now();
138        self.origin = now;
139        self.last_wall = now;
140        self.elapsed = Duration::ZERO;
141        self.dt = Duration::ZERO;
142        self.frame = 0;
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn manual_clock_advances_exactly() {
152        let mut c = FrameClock::new();
153        c.set_manual(true);
154        c.advance_by(Duration::from_millis(16));
155        c.advance_by(Duration::from_millis(16));
156        assert_eq!(c.frame(), 2);
157        assert_eq!(c.elapsed(), Duration::from_millis(32));
158        assert_eq!(c.dt(), Duration::from_millis(16));
159    }
160
161    #[test]
162    fn reduced_motion_stops_time() {
163        let mut c = FrameClock::new();
164        c.set_manual(true);
165        c.set_reduced_motion(true);
166        c.advance_by(Duration::from_millis(100));
167        assert_eq!(c.elapsed(), Duration::ZERO);
168        assert_eq!(c.frame(), 1);
169    }
170
171    #[test]
172    fn speed_scales_manual_steps() {
173        let mut c = FrameClock::new();
174        c.set_manual(true);
175        c.set_speed(2.0);
176        c.advance_by(Duration::from_millis(50));
177        assert_eq!(c.elapsed(), Duration::from_millis(100));
178    }
179
180    #[test]
181    fn automatic_advance_is_disabled_in_manual_mode() {
182        let mut c = FrameClock::new();
183        c.set_manual(true);
184        c.advance();
185        assert_eq!(c.dt(), Duration::ZERO);
186        assert_eq!(c.frame(), 0);
187    }
188}