Skip to main content

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/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
46///
47/// On the server this does basically nothing. The provided closure will never be called.
48pub fn use_raf_fn(
49    callback: impl Fn(UseRafFnCallbackArgs) + 'static,
50) -> Pausable<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
51    use_raf_fn_with_options(callback, UseRafFnOptions::default())
52}
53
54/// Version of [`use_raf_fn`] that takes a `UseRafFnOptions`. See [`use_raf_fn`] for how to use.
55pub fn use_raf_fn_with_options(
56    callback: impl Fn(UseRafFnCallbackArgs) + 'static,
57    options: UseRafFnOptions,
58) -> Pausable<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
59    let UseRafFnOptions { immediate } = options;
60
61    let raf_handle = Rc::new(Cell::new(None::<i32>));
62
63    let (is_active, set_active) = signal(false);
64
65    let loop_ref = Rc::new(RefCell::new(Box::new(|_: f64| {}) as Box<dyn Fn(f64)>));
66
67    let request_next_frame = {
68        cfg_if! { if #[cfg(feature = "ssr")] {
69            move || ()
70        } else {
71            use wasm_bindgen::JsCast;
72            use wasm_bindgen::closure::Closure;
73
74            let loop_ref = Rc::clone(&loop_ref);
75            let raf_handle = Rc::clone(&raf_handle);
76
77            move || {
78                let loop_ref = Rc::clone(&loop_ref);
79
80                raf_handle.set(
81                    window()
82                        .request_animation_frame(
83                            Closure::once_into_js(move |timestamp: f64| {
84                                loop_ref.borrow()(timestamp);
85                            })
86                            .as_ref()
87                            .unchecked_ref(),
88                        )
89                        .ok(),
90                );
91            }
92        }}
93    };
94
95    // Shared with `pause` so that resuming starts a fresh measurement instead
96    // of reporting the whole paused duration as a single frame delta.
97    let previous_frame_timestamp = Rc::new(Cell::new(0.0_f64));
98
99    let loop_fn = {
100        #[allow(clippy::clone_on_copy)]
101        let request_next_frame = request_next_frame.clone();
102        let previous_frame_timestamp = Rc::clone(&previous_frame_timestamp);
103
104        move |timestamp: f64| {
105            if !is_active.try_get_untracked().unwrap_or_default() {
106                return;
107            }
108
109            let prev_timestamp = previous_frame_timestamp.get();
110            let delta = if prev_timestamp > 0.0 {
111                timestamp - prev_timestamp
112            } else {
113                0.0
114            };
115
116            #[cfg(debug_assertions)]
117            let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
118
119            callback(UseRafFnCallbackArgs { delta, timestamp });
120
121            #[cfg(debug_assertions)]
122            drop(zone);
123
124            previous_frame_timestamp.set(timestamp);
125
126            request_next_frame();
127        }
128    };
129
130    let _ = loop_ref.replace(Box::new(loop_fn));
131
132    let resume = sendwrap_fn!(move || {
133        if !is_active.get_untracked() {
134            set_active.set(true);
135            request_next_frame();
136        }
137    });
138
139    let pause = sendwrap_fn!(move || {
140        set_active.set(false);
141        previous_frame_timestamp.set(0.0);
142
143        let handle = raf_handle.get();
144        if let Some(handle) = handle {
145            let _ = window().cancel_animation_frame(handle);
146        }
147        raf_handle.set(None);
148    });
149
150    if immediate {
151        resume();
152    }
153
154    on_cleanup({
155        let pause = pause.clone();
156        #[allow(clippy::redundant_closure)]
157        move || pause()
158    });
159
160    Pausable {
161        resume,
162        pause,
163        is_active: is_active.into(),
164    }
165}
166
167/// Options for [`use_raf_fn_with_options`].
168#[derive(DefaultBuilder)]
169pub struct UseRafFnOptions {
170    /// Start the requestAnimationFrame loop immediately on creation. Defaults to `true`.
171    /// If false, the loop will only start when you call `resume()`.
172    immediate: bool,
173}
174
175impl Default for UseRafFnOptions {
176    fn default() -> Self {
177        Self { immediate: true }
178    }
179}
180
181/// Type of the argument for the callback of [`use_raf_fn`].
182pub struct UseRafFnCallbackArgs {
183    /// Time elapsed between this and the last frame.
184    pub delta: f64,
185
186    /// 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.
187    pub timestamp: f64,
188}