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)]
13pub struct TimerHandle(u32);
14
15impl TimerHandle {
16    pub fn raw(self) -> u32 {
17        self.0
18    }
19}
20
21pub fn set_timeout(delay_ms: i32, callback: impl Fn() + 'static) -> TimerHandle {
22    let timer_id = NEXT_TIMER_ID.with(|next| {
23        let timer_id = next.get();
24        next.set(timer_id.saturating_add(1));
25        timer_id
26    });
27    ACTIVE_TIMERS.with(|timers| {
28        timers.borrow_mut().insert(timer_id, Box::new(callback));
29    });
30    unsafe { ffi::fui_start_timer(timer_id, delay_ms) };
31    TimerHandle(timer_id)
32}
33
34pub(crate) fn schedule_internal_timer(timer_id: u32, delay_ms: i32, callback: impl Fn() + 'static) {
35    ACTIVE_TIMERS.with(|timers| {
36        timers.borrow_mut().insert(timer_id, Box::new(callback));
37    });
38    unsafe { ffi::fui_start_timer(timer_id, delay_ms) };
39}
40
41pub(crate) fn cancel_internal_timer(timer_id: u32) -> bool {
42    let removed = ACTIVE_TIMERS.with(|timers| timers.borrow_mut().remove(&timer_id).is_some());
43    if removed {
44        unsafe { ffi::fui_cancel_timer(timer_id) };
45    }
46    removed
47}
48
49pub fn cancel_timeout(handle: TimerHandle) -> bool {
50    let removed = ACTIVE_TIMERS.with(|timers| timers.borrow_mut().remove(&handle.0).is_some());
51    if removed {
52        unsafe { ffi::fui_cancel_timer(handle.0) };
53    }
54    removed
55}
56
57pub fn cancel_all_timers() {
58    ACTIVE_TIMERS.with(|timers| timers.borrow_mut().clear());
59}
60
61#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
62pub extern "C" fn __fui_on_timer(timer_id: u32) {
63    let callback = ACTIVE_TIMERS.with(|timers| timers.borrow_mut().remove(&timer_id));
64    if let Some(callback) = callback {
65        callback();
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use crate::ffi::{self, Call};
72    use std::cell::Cell;
73    use std::rc::Rc;
74
75    #[test]
76    fn timer_callback_fires_once() {
77        ffi::test::reset();
78        let fired = Rc::new(Cell::new(0));
79        let handle = super::set_timeout(25, || {});
80        let timer_id = handle.raw();
81        let calls = ffi::test::take_calls();
82        assert!(calls.iter().any(|call| matches!(
83            call,
84            Call::StartTimer { timer_id: captured, delay_ms } if *captured == timer_id && *delay_ms == 25
85        )));
86
87        let fired_clone = fired.clone();
88        let handle = super::set_timeout(10, move || fired_clone.set(fired_clone.get() + 1));
89        super::__fui_on_timer(handle.raw());
90        super::__fui_on_timer(handle.raw());
91        assert_eq!(fired.get(), 1);
92    }
93
94    #[test]
95    fn cancel_timeout_emits_host_cancel() {
96        ffi::test::reset();
97        let handle = super::set_timeout(100, || {});
98        ffi::test::take_calls();
99        assert!(super::cancel_timeout(handle));
100        let calls = ffi::test::take_calls();
101        assert!(calls.iter().any(|call| matches!(
102            call,
103            Call::CancelTimer { timer_id } if *timer_id == handle.raw()
104        )));
105    }
106}