leptos_use/
use_raf_fn.rs

1use crate::sendwrap_fn;
2use crate::utils::Pausable;
3use cfg_if::cfg_if;
4use default_struct_builder::DefaultBuilder;
5use leptos::prelude::*;
6use std::cell::{Cell, RefCell};
7use std::rc::Rc;
8
9/// Call function on every requestAnimationFrame.
10/// With controls of pausing and resuming.
11///
12/// ## Demo
13///
14/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_raf_fn)
15///
16/// ## Usage
17///
18/// ```
19/// # use leptos::prelude::*;
20/// # use leptos_use::use_raf_fn;
21/// use leptos_use::utils::Pausable;
22/// #
23/// # #[component]
24/// # fn Demo() -> impl IntoView {
25/// let (count, set_count) = signal(0);
26///
27/// let Pausable { pause, resume, is_active } = use_raf_fn(move |_| {
28///     set_count.update(|count| *count += 1);
29/// });
30///
31/// view! { <div>Count: { count }</div> }
32/// }
33/// ```
34///
35/// You can use `use_raf_fn_with_options` and set `immediate` to `false`. In that case
36/// you have to call `resume()` before the `callback` is executed.
37///
38/// ## SendWrapped Return
39///
40/// The returned closures `pause` and `resume` are sendwrapped functions. They can
41/// only be called from the same thread that called `use_interval_fn`.
42///
43/// ## Server-Side Rendering
44///
45/// On the server this does basically nothing. The provided closure will never be called.
46pub fn use_raf_fn(
47    callback: impl Fn(UseRafFnCallbackArgs) + 'static,
48) -> Pausable<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
49    use_raf_fn_with_options(callback, UseRafFnOptions::default())
50}
51
52/// Version of [`use_raf_fn`] that takes a `UseRafFnOptions`. See [`use_raf_fn`] for how to use.
53pub fn use_raf_fn_with_options(
54    callback: impl Fn(UseRafFnCallbackArgs) + 'static,
55    options: UseRafFnOptions,
56) -> Pausable<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
57    let UseRafFnOptions { immediate } = options;
58
59    let raf_handle = Rc::new(Cell::new(None::<i32>));
60
61    let (is_active, set_active) = signal(false);
62
63    let loop_ref = Rc::new(RefCell::new(Box::new(|_: f64| {}) as Box<dyn Fn(f64)>));
64
65    let request_next_frame = {
66        cfg_if! { if #[cfg(feature = "ssr")] {
67            move || ()
68        } else {
69            use wasm_bindgen::JsCast;
70            use wasm_bindgen::closure::Closure;
71
72            let loop_ref = Rc::clone(&loop_ref);
73            let raf_handle = Rc::clone(&raf_handle);
74
75            move || {
76                let loop_ref = Rc::clone(&loop_ref);
77
78                raf_handle.set(
79                    window()
80                        .request_animation_frame(
81                            Closure::once_into_js(move |timestamp: f64| {
82                                loop_ref.borrow()(timestamp);
83                            })
84                            .as_ref()
85                            .unchecked_ref(),
86                        )
87                        .ok(),
88                );
89            }
90        }}
91    };
92
93    let loop_fn = {
94        #[allow(clippy::clone_on_copy)]
95        let request_next_frame = request_next_frame.clone();
96        let previous_frame_timestamp = Cell::new(0.0_f64);
97
98        move |timestamp: f64| {
99            if !is_active.try_get_untracked().unwrap_or_default() {
100                return;
101            }
102
103            let prev_timestamp = previous_frame_timestamp.get();
104            let delta = if prev_timestamp > 0.0 {
105                timestamp - prev_timestamp
106            } else {
107                0.0
108            };
109
110            #[cfg(debug_assertions)]
111            let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
112
113            callback(UseRafFnCallbackArgs { delta, timestamp });
114
115            #[cfg(debug_assertions)]
116            drop(zone);
117
118            previous_frame_timestamp.set(timestamp);
119
120            request_next_frame();
121        }
122    };
123
124    let _ = loop_ref.replace(Box::new(loop_fn));
125
126    let resume = sendwrap_fn!(move || {
127        if !is_active.get_untracked() {
128            set_active.set(true);
129            request_next_frame();
130        }
131    });
132
133    let pause = sendwrap_fn!(move || {
134        set_active.set(false);
135
136        let handle = raf_handle.get();
137        if let Some(handle) = handle {
138            let _ = window().cancel_animation_frame(handle);
139        }
140        raf_handle.set(None);
141    });
142
143    if immediate {
144        resume();
145    }
146
147    on_cleanup({
148        let pause = pause.clone();
149        #[allow(clippy::redundant_closure)]
150        move || pause()
151    });
152
153    Pausable {
154        resume,
155        pause,
156        is_active: is_active.into(),
157    }
158}
159
160/// Options for [`use_raf_fn_with_options`].
161#[derive(DefaultBuilder)]
162pub struct UseRafFnOptions {
163    /// Start the requestAnimationFrame loop immediately on creation. Defaults to `true`.
164    /// If false, the loop will only start when you call `resume()`.
165    immediate: bool,
166}
167
168impl Default for UseRafFnOptions {
169    fn default() -> Self {
170        Self { immediate: true }
171    }
172}
173
174/// Type of the argument for the callback of [`use_raf_fn`].
175pub struct UseRafFnCallbackArgs {
176    /// Time elapsed between this and the last frame.
177    pub delta: f64,
178
179    /// Time elapsed since the creation of the web page. See [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin) Time origin.
180    pub timestamp: f64,
181}