Skip to main content

oxiui_core/
scheduler.rs

1//! Frame-aligned callback scheduling plus debounce/throttle helpers.
2//!
3//! [`Scheduler`] is a deterministic, virtual-clock scheduler: the caller drives
4//! it by calling [`Scheduler::tick`] once per frame with the elapsed `dt`
5//! (seconds). It maintains a monotonically-increasing virtual `now`, runs any
6//! callbacks whose fire time has arrived, and supports one-shot timers
7//! (`after`), repeating timers (`every`), and next-frame callbacks
8//! (`request_frame`, the `requestAnimationFrame` analogue).
9//!
10//! Keeping time virtual (rather than reading a wall clock) makes the scheduler
11//! testable and frame-rate independent, and lets the same logic run under wasm,
12//! native, and headless test harnesses.
13//!
14//! [`Debounce`] and [`Throttle`] are standalone rate-limiters usable with the
15//! same virtual clock or any `f32` timestamp source.
16
17/// An opaque handle to a scheduled callback, usable for cancellation.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct TimerId(u64);
20
21/// What a fired timer should do next.
22enum Repeat {
23    /// Fire once and drop.
24    Once,
25    /// Re-arm to fire again every `interval` seconds.
26    Every(f32),
27}
28
29struct Timer {
30    id: TimerId,
31    /// Virtual time at which this timer next fires.
32    fire_at: f32,
33    repeat: Repeat,
34    callback: Box<dyn FnMut()>,
35}
36
37/// A virtual-clock scheduler for frame-aligned callbacks.
38#[derive(Default)]
39pub struct Scheduler {
40    now: f32,
41    next_id: u64,
42    timers: Vec<Timer>,
43    /// Callbacks to run on the next `tick`, regardless of time (rAF analogue).
44    frame_callbacks: Vec<(TimerId, Box<dyn FnMut()>)>,
45}
46
47impl Scheduler {
48    /// Create a scheduler with its virtual clock at zero.
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// The current virtual time in seconds.
54    pub fn now(&self) -> f32 {
55        self.now
56    }
57
58    /// Number of pending timers (excluding next-frame callbacks).
59    pub fn pending(&self) -> usize {
60        self.timers.len()
61    }
62
63    /// Number of pending next-frame callbacks.
64    pub fn pending_frames(&self) -> usize {
65        self.frame_callbacks.len()
66    }
67
68    fn alloc_id(&mut self) -> TimerId {
69        let id = TimerId(self.next_id);
70        self.next_id += 1;
71        id
72    }
73
74    /// Schedule `callback` to fire once, `delay` seconds from now. A `delay`
75    /// of `0` (or negative) fires on the next [`tick`](Scheduler::tick).
76    pub fn after(&mut self, delay: f32, callback: impl FnMut() + 'static) -> TimerId {
77        let id = self.alloc_id();
78        self.timers.push(Timer {
79            id,
80            fire_at: self.now + delay.max(0.0),
81            repeat: Repeat::Once,
82            callback: Box::new(callback),
83        });
84        id
85    }
86
87    /// Schedule `callback` to fire every `interval` seconds, beginning
88    /// `interval` seconds from now. `interval` is clamped to a tiny positive
89    /// value to avoid a zero-period busy loop.
90    pub fn every(&mut self, interval: f32, callback: impl FnMut() + 'static) -> TimerId {
91        let interval = interval.max(1e-4);
92        let id = self.alloc_id();
93        self.timers.push(Timer {
94            id,
95            fire_at: self.now + interval,
96            repeat: Repeat::Every(interval),
97            callback: Box::new(callback),
98        });
99        id
100    }
101
102    /// Schedule `callback` to run on the next [`tick`](Scheduler::tick), once.
103    /// This is the `requestAnimationFrame` analogue.
104    pub fn request_frame(&mut self, callback: impl FnMut() + 'static) -> TimerId {
105        let id = self.alloc_id();
106        self.frame_callbacks.push((id, Box::new(callback)));
107        id
108    }
109
110    /// Cancel a pending timer or next-frame callback. Returns `true` if found.
111    pub fn cancel(&mut self, id: TimerId) -> bool {
112        let before = self.timers.len() + self.frame_callbacks.len();
113        self.timers.retain(|t| t.id != id);
114        self.frame_callbacks.retain(|(fid, _)| *fid != id);
115        self.timers.len() + self.frame_callbacks.len() != before
116    }
117
118    /// Advance the virtual clock by `dt` seconds and run every callback that is
119    /// now due. Returns the number of callbacks fired.
120    ///
121    /// Repeating timers may fire multiple times within one large `dt`, and are
122    /// re-armed relative to their *scheduled* fire time (so they don't drift).
123    /// Next-frame callbacks always fire exactly once, before timers.
124    pub fn tick(&mut self, dt: f32) -> usize {
125        self.now += dt.max(0.0);
126        let mut fired = 0;
127
128        // Next-frame callbacks fire first, exactly once each.
129        let frames = std::mem::take(&mut self.frame_callbacks);
130        for (_, mut cb) in frames {
131            cb();
132            fired += 1;
133        }
134
135        // Timers: collect due ones (in fire-time order) and run them. Re-arm
136        // repeats; this loop handles a single `dt` that spans several periods.
137        loop {
138            // Find the earliest due timer not yet past `now`.
139            let due_idx = self
140                .timers
141                .iter()
142                .enumerate()
143                .filter(|(_, t)| t.fire_at <= self.now)
144                .min_by(|(_, a), (_, b)| {
145                    a.fire_at
146                        .partial_cmp(&b.fire_at)
147                        .unwrap_or(std::cmp::Ordering::Equal)
148                })
149                .map(|(i, _)| i);
150
151            let Some(idx) = due_idx else { break };
152
153            match self.timers[idx].repeat {
154                Repeat::Once => {
155                    let mut timer = self.timers.remove(idx);
156                    (timer.callback)();
157                    fired += 1;
158                }
159                Repeat::Every(interval) => {
160                    (self.timers[idx].callback)();
161                    fired += 1;
162                    // Re-arm relative to the scheduled time to avoid drift.
163                    self.timers[idx].fire_at += interval;
164                }
165            }
166        }
167        fired
168    }
169}
170
171/// Trailing-edge debouncer: an action only fires once input has been quiet for
172/// `delay` seconds. Each [`Debounce::signal`] resets the quiet timer; only when
173/// [`Debounce::poll`] is called after `delay` of silence does it report ready.
174#[derive(Clone, Copy, Debug)]
175pub struct Debounce {
176    delay: f32,
177    /// Virtual time of the most recent signal, or `None` if idle/already fired.
178    last_signal: Option<f32>,
179}
180
181impl Debounce {
182    /// Create a debouncer with the given quiet-period `delay` in seconds.
183    pub fn new(delay: f32) -> Self {
184        Self {
185            delay: delay.max(0.0),
186            last_signal: None,
187        }
188    }
189
190    /// Register activity at virtual time `now`, resetting the quiet timer.
191    pub fn signal(&mut self, now: f32) {
192        self.last_signal = Some(now);
193    }
194
195    /// If a signal is pending and `now` is at least `delay` past the last
196    /// signal, consume it and return `true` (the action should fire). Otherwise
197    /// `false`.
198    pub fn poll(&mut self, now: f32) -> bool {
199        match self.last_signal {
200            Some(t) if now - t >= self.delay => {
201                self.last_signal = None;
202                true
203            }
204            _ => false,
205        }
206    }
207
208    /// Whether a signal is currently waiting to fire.
209    pub fn is_pending(&self) -> bool {
210        self.last_signal.is_some()
211    }
212}
213
214/// Leading-edge throttler: an action may fire at most once per `interval`
215/// seconds. The first attempt fires immediately; subsequent attempts within the
216/// interval are suppressed.
217#[derive(Clone, Copy, Debug)]
218pub struct Throttle {
219    interval: f32,
220    /// Virtual time the action last fired, or `None` if it never has.
221    last_fire: Option<f32>,
222}
223
224impl Throttle {
225    /// Create a throttle allowing one fire per `interval` seconds.
226    pub fn new(interval: f32) -> Self {
227        Self {
228            interval: interval.max(0.0),
229            last_fire: None,
230        }
231    }
232
233    /// Attempt to fire at virtual time `now`. Returns `true` if allowed (and
234    /// records the time); `false` if still within the cool-down window.
235    pub fn try_fire(&mut self, now: f32) -> bool {
236        match self.last_fire {
237            Some(t) if now - t < self.interval => false,
238            _ => {
239                self.last_fire = Some(now);
240                true
241            }
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use std::cell::Cell;
250    use std::rc::Rc;
251
252    #[test]
253    fn after_fires_once_when_due() {
254        let mut s = Scheduler::new();
255        let n = Rc::new(Cell::new(0u32));
256        let n_c = Rc::clone(&n);
257        s.after(1.0, move || n_c.set(n_c.get() + 1));
258        // Not due yet.
259        assert_eq!(s.tick(0.5), 0);
260        assert_eq!(n.get(), 0);
261        // Now due.
262        assert_eq!(s.tick(0.6), 1);
263        assert_eq!(n.get(), 1);
264        // Does not fire again.
265        assert_eq!(s.tick(5.0), 0);
266        assert_eq!(n.get(), 1);
267        assert_eq!(s.pending(), 0);
268    }
269
270    #[test]
271    fn every_repeats_and_handles_large_dt() {
272        let mut s = Scheduler::new();
273        let n = Rc::new(Cell::new(0u32));
274        let n_c = Rc::clone(&n);
275        s.every(1.0, move || n_c.set(n_c.get() + 1));
276        // A single 3.5s tick spans three full intervals (1,2,3).
277        let fired = s.tick(3.5);
278        assert_eq!(fired, 3);
279        assert_eq!(n.get(), 3);
280        // Next interval at t=4.
281        s.tick(0.6); // now = 4.1
282        assert_eq!(n.get(), 4);
283    }
284
285    #[test]
286    fn request_frame_fires_next_tick_only() {
287        let mut s = Scheduler::new();
288        let n = Rc::new(Cell::new(0u32));
289        let n_c = Rc::clone(&n);
290        s.request_frame(move || n_c.set(n_c.get() + 1));
291        assert_eq!(s.pending_frames(), 1);
292        assert_eq!(s.tick(0.0), 1);
293        assert_eq!(n.get(), 1);
294        // Gone after one tick.
295        assert_eq!(s.tick(0.0), 0);
296        assert_eq!(n.get(), 1);
297    }
298
299    #[test]
300    fn cancel_prevents_fire() {
301        let mut s = Scheduler::new();
302        let n = Rc::new(Cell::new(0u32));
303        let n_c = Rc::clone(&n);
304        let id = s.after(1.0, move || n_c.set(n_c.get() + 1));
305        assert!(s.cancel(id));
306        assert!(!s.cancel(id));
307        s.tick(2.0);
308        assert_eq!(n.get(), 0);
309    }
310
311    #[test]
312    fn debounce_fires_only_after_quiet_period() {
313        let mut d = Debounce::new(0.3);
314        d.signal(0.0);
315        assert!(d.is_pending());
316        // Re-signalled before the quiet period elapses -> resets.
317        assert!(!d.poll(0.2));
318        d.signal(0.2);
319        assert!(!d.poll(0.4)); // only 0.2s since last signal
320                               // 0.3s of quiet -> fires.
321        assert!(d.poll(0.5));
322        assert!(!d.is_pending());
323        // Nothing pending -> no fire.
324        assert!(!d.poll(1.0));
325    }
326
327    #[test]
328    fn throttle_limits_rate_leading_edge() {
329        let mut t = Throttle::new(1.0);
330        assert!(t.try_fire(0.0)); // first fires
331        assert!(!t.try_fire(0.5)); // within cooldown
332        assert!(!t.try_fire(0.99));
333        assert!(t.try_fire(1.0)); // cooldown elapsed
334        assert!(!t.try_fire(1.5));
335    }
336}