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;