Skip to main content

fui/
timers.rs

1use crate::ffi;
2use std::cell::{Cell, RefCell};
3use std::collections::HashMap;
4
5type TimerCallback = Box<dyn Fn()>;
6
7thread_local! {
8    static NEXT_TIMER_ID: Cell<u32> = const { Cell::new(1) };
9    static ACTIVE_TIMERS: RefCell<HashMap<u32, TimerCallback>> = RefCell::new(HashMap::new());
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13/// Handle for one pending one-shot UI timer.
14pub struct TimerHandle(u32);
15
16impl TimerHandle {
17    /// Returns the opaque host timer ID.
18    pub fn raw(self) -> u32 {
19        self.0
20    }
21}
22
23/// Schedules a one-shot callback on the application UI queue.
24///
25/// Store the returned handle when cancellation is required. Recurring work
26/// must schedule its next timeout explicitly.
27///
28/// ```no_run
29/// use fui::prelude::*;
30///
31/// let timer = set_timeout(250, || {
32///     // Mutate retained application state, then invalidate affected UI.
33/// });
34/// assert!(cancel_timeout(timer));
35/// ```
36pub fn set_timeout(delay_ms: i32, callback: impl Fn() + 'static) -> TimerHandle {
37    let timer_id = NEXT_TIMER_ID.with(|next| {
38        let timer_id = next.get();
39        next.set(timer_id.saturating_add(1));
40        timer_id
41    });
42    ACTIVE_TIMERS.with(|timers| {
43        timers.borrow_mut().insert(timer_id, Box::new(callback));
44    });
45    unsafe { ffi::fui_start_timer(timer_id, delay_ms) };
46    TimerHandle(timer_id)
47}
48
49pub(crate) fn schedule_internal_timer(timer_id: u32, delay_ms: i32, callback: impl Fn() + 'static) {
50    ACTIVE_TIMERS.with(|timers| {
51        timers.borrow_mut().insert(timer_id, Box::new(callback));
52    });
53    unsafe { ffi::fui_start_timer(timer_id, delay_ms) };
54}
55
56pub(crate) fn cancel_internal_timer(timer_id: u32) -> bool {
57    let removed = ACTIVE_TIMERS.with(|timers| timers.borrow_mut().remove(&timer_id).is_some());
58    if removed {
59        unsafe { ffi::fui_cancel_timer(timer_id) };
60    }
61    removed
62}
63
64/// Cancels a pending timeout, returning whether it was still registered.
65pub fn cancel_timeout(handle: TimerHandle) -> bool {
66    let removed = ACTIVE_TIMERS.with(|timers| timers.borrow_mut().remove(&handle.0).is_some());
67    if removed {
68        unsafe { ffi::fui_cancel_timer(handle.0) };
69    }
70    removed
71}
72
73/// Clears all registered timer callbacks during application teardown.
74pub fn cancel_all_timers() {
75    ACTIVE_TIMERS.with(|timers| timers.borrow_mut().clear());
76}
77
78#[cfg_attr(
79    any(not(feature = "worker-runtime"), feature = "native-runtime"),
80    no_mangle
81)]
82pub extern "C" fn __fui_on_timer(timer_id: u32) {
83    let callback = ACTIVE_TIMERS.with(|timers| timers.borrow_mut().remove(&timer_id));
84    if let Some(callback) = callback {
85        callback();
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use crate::ffi::{self, Call};
92    use std::cell::Cell;
93    use std::rc::Rc;
94
95    #[test]
96    fn timer_callback_fires_once() {
97        ffi::test::reset();
98        let fired = Rc::new(Cell::new(0));
99        let handle = super::set_timeout(25, || {});
100        let timer_id = handle.raw();
101        let calls = ffi::test::take_calls();
102        assert!(calls.iter().any(|call| matches!(
103            call,
104            Call::StartTimer { timer_id: captured, delay_ms } if *captured == timer_id && *delay_ms == 25
105        )));
106
107        let fired_clone = fired.clone();
108        let handle = super::set_timeout(10, move || fired_clone.set(fired_clone.get() + 1));
109        super::__fui_on_timer(handle.raw());
110        super::__fui_on_timer(handle.raw());
111        assert_eq!(fired.get(), 1);
112    }
113
114    #[test]
115    fn cancel_timeout_emits_host_cancel() {
116        ffi::test::reset();
117        let handle = super::set_timeout(100, || {});
118        ffi::test::take_calls();
119        assert!(super::cancel_timeout(handle));
120        let calls = ffi::test::take_calls();
121        assert!(calls.iter().any(|call| matches!(
122            call,
123            Call::CancelTimer { timer_id } if *timer_id == handle.raw()
124        )));
125    }
126}