Skip to main content

smudge_core/
timer.rs

1//! Timers that run between frames, on the clock their [`App`](crate::App)
2//! keeps.
3//!
4//! [`after`] runs a closure once, [`every`] runs it repeatedly until the
5//! returned [`TimerHandle`] is dropped or the owner the timer was created
6//! under is cleaned up. Both read the app's clock, not the system clock, so a
7//! test that runs the app on a [`VirtualClock`] advances time by hand and
8//! observes the exact timers it expects, without sleeping.
9//!
10//! A timer is registered under the owner that is current when it is created,
11//! which is what makes the app find it: every owner chain leads to a root,
12//! and the root holds the timer book. Code that runs with no current owner,
13//! such as a bare test, creates no timer at all.
14//!
15//! ```
16//! use std::time::Duration;
17//!
18//! use smudge_core::app::{App, AppConfig, RepaintMode};
19//! use smudge_core::geometry::{Color, Size};
20//! use smudge_core::reactive::{prelude::*, signal_local};
21//! use smudge_core::timer::{self, VirtualClock};
22//! use smudge_core::widget::panel;
23//! use web_time::Instant;
24//!
25//! let clock = VirtualClock::new(Instant::now());
26//! let (fired, set_fired) = signal_local(false);
27//! let mut app = App::with_clock(
28//!     AppConfig {
29//!         viewport: Size {
30//!             width: 100.0,
31//!             height: 60.0,
32//!         },
33//!         scale: 1.0,
34//!         clear_color: Color::WHITE,
35//!         repaint: RepaintMode::Damage,
36//!     },
37//!     clock.clone().into(),
38//!     || {
39//!         timer::after(Duration::from_millis(40), move || set_fired.set(true));
40//!         panel()
41//!     },
42//! );
43//!
44//! // The timer has not run yet: only the passage of time runs it.
45//! clock.advance(Duration::from_millis(39));
46//! app.frame();
47//! assert!(!fired.get(), "the timer is not due yet");
48//!
49//! clock.advance(Duration::from_millis(1));
50//! app.frame();
51//! assert!(fired.get(), "the timer ran when its time was up");
52//! ```
53
54use std::cell::{Cell, RefCell};
55use std::rc::{Rc, Weak};
56use std::time::Duration;
57
58use web_time::Instant;
59
60use crate::reactive::runtime::{self, CurrentOwnerGuard, OwnerData};
61
62/// The clock an [`App`](crate::App) and its timers read the time from.
63///
64/// [`App::new`](crate::App::new) runs on [`Clock::system`]. A test creates a
65/// [`VirtualClock`], hands it to [`App::with_clock`](crate::App::with_clock)
66/// and advances its own copy, so timers fire at exact moments without the
67/// test sleeping.
68#[derive(Clone, Debug)]
69pub struct Clock(ClockKind);
70
71/// Where a clock reads its time from.
72#[derive(Clone, Debug)]
73enum ClockKind {
74    /// The operating system's monotonic time.
75    System,
76    /// A time that moves only when the virtual clock is advanced.
77    Virtual(VirtualClock),
78}
79
80impl Clock {
81    /// Creates a clock that reads the operating system's time.
82    pub fn system() -> Clock {
83        Clock(ClockKind::System)
84    }
85
86    /// Returns the current time of this clock.
87    pub fn now(&self) -> Instant {
88        match &self.0 {
89            ClockKind::System => Instant::now(),
90            ClockKind::Virtual(clock) => clock.now(),
91        }
92    }
93}
94
95impl From<VirtualClock> for Clock {
96    fn from(clock: VirtualClock) -> Clock {
97        Clock(ClockKind::Virtual(clock))
98    }
99}
100
101/// A clock whose time moves only when it is advanced.
102///
103/// Clones share one time, so a test keeps a clone after handing the clock to
104/// an app and advances the app's time through it.
105///
106/// ```
107/// use std::time::Duration;
108///
109/// use smudge_core::timer::VirtualClock;
110/// use web_time::Instant;
111///
112/// let start = Instant::now();
113/// let clock = VirtualClock::new(start);
114/// let shared = clock.clone();
115/// clock.advance(Duration::from_millis(16));
116/// assert_eq!(shared.now(), start + Duration::from_millis(16));
117/// ```
118#[derive(Clone, Debug)]
119pub struct VirtualClock(Rc<Cell<Instant>>);
120
121impl VirtualClock {
122    /// Creates a virtual clock that starts at `start`.
123    pub fn new(start: Instant) -> VirtualClock {
124        VirtualClock(Rc::new(Cell::new(start)))
125    }
126
127    /// Returns the current time of this clock.
128    pub fn now(&self) -> Instant {
129        self.0.get()
130    }
131
132    /// Moves the time of this clock and of every clone forward by `by`.
133    pub fn advance(&self, by: Duration) {
134        self.0.set(self.0.get() + by);
135    }
136}
137
138/// One registered timer, shared by the book that fires it and the handles
139/// that cancel it.
140struct TimerEntry {
141    /// The period of a repeating timer, or `None` for a one-shot.
142    period: Option<Duration>,
143    /// The time the timer fires at next.
144    next: Cell<Instant>,
145    /// The closure to run. Taken out while it runs, so a callback that drops
146    /// its own handle cannot leave the entry borrowed.
147    run: RefCell<Option<Box<dyn FnMut()>>>,
148    /// Whether the timer still fires. A handle drop or the fire loop clears
149    /// this.
150    active: Cell<bool>,
151    /// The owner the timer was created under. A cleaned up owner stops its
152    /// timers, even when a handle is still held elsewhere.
153    ///
154    /// The owner is checked rather than told: a cleanup hook per timer would
155    /// keep every timer an owner ever made alive until the owner goes, and an
156    /// input restarts its blink timer on every keystroke.
157    owner: Weak<OwnerData>,
158}
159
160impl TimerEntry {
161    /// Returns whether the timer can still fire: it was not canceled and its
162    /// owner was not cleaned up.
163    fn is_live(&self) -> bool {
164        self.active.get()
165            && self
166                .owner
167                .upgrade()
168                .is_some_and(|owner| !owner.cleaned.get())
169    }
170}
171
172/// A handle that cancels its timer when it is dropped.
173///
174/// Dropping the handle cancels the timer, and so does cleaning up the owner
175/// the timer was created under, whichever happens first. A repeating timer
176/// that should outlive the scope it was created in keeps its handle alive for
177/// as long as it should run.
178#[must_use = "a timer runs until its handle is dropped, so an unused handle \
179              cancels it at once"]
180pub struct TimerHandle {
181    entry: Rc<TimerEntry>,
182}
183
184impl Drop for TimerHandle {
185    fn drop(&mut self) {
186        self.entry.active.set(false);
187    }
188}
189
190/// The timers of one app.
191///
192/// The app owns the book and fires due timers at the start of every frame.
193/// No borrow of the book is held while a callback runs, so a callback can
194/// register and cancel timers freely.
195pub(crate) struct TimerBook {
196    /// The clock every timer reads.
197    pub(crate) clock: Clock,
198    /// The timers, in no particular order. Canceled timers are pruned after
199    /// each round of firing.
200    timers: Vec<Rc<TimerEntry>>,
201}
202
203impl TimerBook {
204    /// Creates a book that reads `clock`.
205    pub(crate) fn new(clock: Clock) -> TimerBook {
206        TimerBook {
207            clock,
208            timers: Vec::new(),
209        }
210    }
211
212    /// Returns the earliest time a live timer fires at, or `None` when the
213    /// book holds none.
214    pub(crate) fn next_deadline(&self) -> Option<Instant> {
215        self.timers
216            .iter()
217            .filter(|entry| entry.is_live())
218            .map(|entry| entry.next.get())
219            .min()
220    }
221}
222
223/// Registers a timer that fires after `delay` and, when `period` is given,
224/// again every period after that.
225///
226/// Returns the entry, or `None` when there is no current owner, the current
227/// owner has already been cleaned up, or no app holds a timer book above the
228/// owner.
229fn register(
230    period: Option<Duration>,
231    delay: Duration,
232    run: Box<dyn FnMut()>,
233) -> Option<Rc<TimerEntry>> {
234    let owner = runtime::with_runtime(|runtime| runtime.current_owner())?;
235    if owner.cleaned.get() {
236        return None;
237    }
238    let book = book_of(&owner)?;
239    let now = book.borrow().clock.now();
240    let entry = Rc::new(TimerEntry {
241        period,
242        next: Cell::new(now + delay),
243        run: RefCell::new(Some(run)),
244        active: Cell::new(true),
245        owner: Rc::downgrade(&owner),
246    });
247    book.borrow_mut().timers.push(Rc::clone(&entry));
248    Some(entry)
249}
250
251/// Runs every timer that was registered when the round began and whose time
252/// has come, once per timer.
253///
254/// Callbacks run inside the owner they were created under, and one that
255/// registers a timer due right now waits for the next round, so a callback
256/// that schedules work cannot loop inside one round. A timer that fell more
257/// than a period behind fires once and reschedules from the current time, so
258/// a slow frame never fires a timer many times to catch up.
259pub(crate) fn fire_due(book: &Rc<RefCell<TimerBook>>) {
260    let round: Vec<_> = book.borrow().timers.clone();
261    let mut fired: Vec<Rc<TimerEntry>> = Vec::new();
262    loop {
263        let now = book.borrow().clock.now();
264        let Some(entry) = due_entry(&round, now, &fired) else {
265            break;
266        };
267        fired.push(Rc::clone(&entry));
268        if !run_entry(&entry) {
269            entry.active.set(false);
270        } else if let Some(period) = entry.period {
271            entry.next.set(entry.next.get().max(now) + period);
272        }
273    }
274    book.borrow_mut().timers.retain(|entry| entry.is_live());
275}
276
277/// Returns the timer of `timers` that fires at or before `now`, is active and
278/// is not in `skip`, picking the one that is due first, or `None` when none
279/// is due.
280fn due_entry(
281    timers: &[Rc<TimerEntry>],
282    now: Instant,
283    skip: &[Rc<TimerEntry>],
284) -> Option<Rc<TimerEntry>> {
285    let mut best: Option<Rc<TimerEntry>> = None;
286    for entry in timers {
287        if !entry.active.get() || skip.iter().any(|seen| Rc::ptr_eq(seen, entry)) {
288            continue;
289        }
290        let next = entry.next.get();
291        if next <= now && best.as_ref().is_none_or(|best| next < best.next.get()) {
292            best = Some(Rc::clone(entry));
293        }
294    }
295    best
296}
297
298/// Runs the callback of `entry` inside its owner, and returns whether the
299/// timer may fire again.
300///
301/// A timer whose owner is gone or cleaned up stops, and so does one whose
302/// callback is somehow already running, or that another callback of the round
303/// canceled.
304fn run_entry(entry: &Rc<TimerEntry>) -> bool {
305    let Some(owner) = entry.owner.upgrade().filter(|owner| !owner.cleaned.get()) else {
306        return false;
307    };
308    let Ok(mut run) = entry.run.try_borrow_mut() else {
309        return false;
310    };
311    let Some(mut callback) = run.take() else {
312        return false;
313    };
314    {
315        let _guard = CurrentOwnerGuard::enter(&owner);
316        callback();
317    }
318    // The entry stays alive while the callback runs, because the book, the
319    // round and this call all hold a handle to it, so putting the closure
320    // back cannot race a drop.
321    *run = Some(callback);
322    entry.period.is_some() && entry.active.get()
323}
324
325/// Returns the timer book of the app `owner` belongs to, walking up the owner
326/// chain to the root that holds it.
327fn book_of(owner: &Rc<OwnerData>) -> Option<Rc<RefCell<TimerBook>>> {
328    let mut current = Rc::clone(owner);
329    loop {
330        let book = current.timer_book.borrow().as_ref().and_then(Weak::upgrade);
331        if book.is_some() {
332            return book;
333        }
334        let parent = current.parent.as_ref()?.upgrade()?;
335        current = parent;
336    }
337}
338
339/// Returns the current time of the clock of the app that owns the code
340/// running right now, or the system time when no app does.
341///
342/// Handlers run inside the owner of their node, so input handlers read the
343/// app's time this way, which makes double click detection and other timing
344/// testable with a virtual clock.
345pub fn now() -> Instant {
346    let owner = runtime::with_runtime(|runtime| runtime.current_owner());
347    match owner.as_ref().and_then(book_of) {
348        Some(book) => book.borrow().clock.now(),
349        None => Instant::now(),
350    }
351}
352
353/// Runs `fun` once, after `duration` has passed on the app's clock.
354///
355/// The timer is created under the current owner and stops when that owner is
356/// cleaned up or when `fun` has run, whichever comes first. The closure runs
357/// at the start of a frame, inside the owner it was created under, so it can
358/// write the signals that owner's bindings read. Code with no current owner,
359/// or with no app above it, creates no timer and drops `fun`.
360///
361/// A duration of zero fires at the start of the next frame.
362pub fn after(duration: Duration, fun: impl FnMut() + 'static) {
363    register(None, duration, Box::new(fun));
364}
365
366/// Runs `fun` every `period` on the app's clock, until the returned handle is
367/// dropped or the creating owner is cleaned up.
368///
369/// The first run comes one period after the timer was created. A timer that
370/// falls behind fires once per frame and reschedules from the current time,
371/// so a zero period behaves as a request to run once per frame, and every
372/// frame with a due timer keeps the app awake. Keep the handle alive for as
373/// long as the timer should run; see [`TimerHandle`]. When no timer can be
374/// created, the handle that comes back is inert and cancels nothing.
375pub fn every(period: Duration, fun: impl FnMut() + 'static) -> TimerHandle {
376    let entry = register(Some(period), period, Box::new(fun));
377    TimerHandle {
378        entry: entry.unwrap_or_else(|| inactive_entry(period)),
379    }
380}
381
382/// Builds the entry a handle holds when its timer was never registered.
383///
384/// The entry is inactive and has a dead owner, so the handle costs nothing
385/// and dropping it changes nothing.
386fn inactive_entry(period: Duration) -> Rc<TimerEntry> {
387    Rc::new(TimerEntry {
388        period: Some(period),
389        next: Cell::new(Instant::now()),
390        run: RefCell::new(None),
391        active: Cell::new(false),
392        owner: Weak::default(),
393    })
394}
395
396#[cfg(test)]
397#[path = "timer_tests.rs"]
398mod tests;