leptos_use/
use_raf_fn.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
use crate::utils::Pausable;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder;
use leptos::*;
use std::cell::{Cell, RefCell};
use std::rc::Rc;

/// Call function on every requestAnimationFrame.
/// With controls of pausing and resuming.
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_raf_fn)
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos_use::use_raf_fn;
/// use leptos_use::utils::Pausable;
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// let (count, set_count) = create_signal(0);
///
/// let Pausable { pause, resume, is_active } = use_raf_fn(move |_| {
///     set_count.update(|count| *count += 1);
/// });
///
/// view! { <div>Count: { count }</div> }
/// }
/// ```
///
/// You can use `use_raf_fn_with_options` and set `immediate` to `false`. In that case
/// you have to call `resume()` before the `callback` is executed.
///
/// ## Server-Side Rendering
///
/// On the server this does basically nothing. The provided closure will never be called.
pub fn use_raf_fn(
    callback: impl Fn(UseRafFnCallbackArgs) + 'static,
) -> Pausable<impl Fn() + Clone, impl Fn() + Clone> {
    use_raf_fn_with_options(callback, UseRafFnOptions::default())
}

/// Version of [`use_raf_fn`] that takes a `UseRafFnOptions`. See [`use_raf_fn`] for how to use.
pub fn use_raf_fn_with_options(
    callback: impl Fn(UseRafFnCallbackArgs) + 'static,
    options: UseRafFnOptions,
) -> Pausable<impl Fn() + Clone, impl Fn() + Clone> {
    let UseRafFnOptions { immediate } = options;

    let raf_handle = Rc::new(Cell::new(None::<i32>));

    let (is_active, set_active) = create_signal(false);

    let loop_ref = Rc::new(RefCell::new(Box::new(|_: f64| {}) as Box<dyn Fn(f64)>));

    let request_next_frame = {
        cfg_if! { if #[cfg(feature = "ssr")] {
            move || ()
        } else {
            use wasm_bindgen::JsCast;
            use wasm_bindgen::closure::Closure;

            let loop_ref = Rc::clone(&loop_ref);
            let raf_handle = Rc::clone(&raf_handle);

            move || {
                let loop_ref = Rc::clone(&loop_ref);

                raf_handle.set(
                    window()
                        .request_animation_frame(
                            Closure::once_into_js(move |timestamp: f64| {
                                loop_ref.borrow()(timestamp);
                            })
                            .as_ref()
                            .unchecked_ref(),
                        )
                        .ok(),
                );
            }
        }}
    };

    let loop_fn = {
        let request_next_frame = request_next_frame.clone();
        let previous_frame_timestamp = Cell::new(0.0_f64);

        move |timestamp: f64| {
            if !is_active.try_get_untracked().unwrap_or_default() {
                return;
            }

            let prev_timestamp = previous_frame_timestamp.get();
            let delta = if prev_timestamp > 0.0 {
                timestamp - prev_timestamp
            } else {
                0.0
            };

            #[cfg(debug_assertions)]
            let prev = SpecialNonReactiveZone::enter();

            callback(UseRafFnCallbackArgs { delta, timestamp });

            #[cfg(debug_assertions)]
            SpecialNonReactiveZone::exit(prev);

            previous_frame_timestamp.set(timestamp);

            request_next_frame();
        }
    };

    let _ = loop_ref.replace(Box::new(loop_fn));

    let resume = move || {
        if !is_active.get_untracked() {
            set_active.set(true);
            request_next_frame();
        }
    };

    let pause = move || {
        set_active.set(false);

        let handle = raf_handle.get();
        if let Some(handle) = handle {
            let _ = window().cancel_animation_frame(handle);
        }
        raf_handle.set(None);
    };

    if immediate {
        resume();
    }

    on_cleanup(pause.clone());

    Pausable {
        resume,
        pause,
        is_active: is_active.into(),
    }
}

/// Options for [`use_raf_fn_with_options`].
#[derive(DefaultBuilder)]
pub struct UseRafFnOptions {
    /// Start the requestAnimationFrame loop immediately on creation. Defaults to `true`.
    /// If false the loop will only start when you call `resume()`.
    immediate: bool,
}

impl Default for UseRafFnOptions {
    fn default() -> Self {
        Self { immediate: true }
    }
}

/// Type of the argument for the callback of [`use_raf_fn`].
pub struct UseRafFnCallbackArgs {
    /// Time elapsed between this and the last frame.
    pub delta: f64,

    /// 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.
    pub timestamp: f64,
}