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
use leptos::leptos_dom::helpers::TimeoutHandle;
use leptos::*;
use std::cell::Cell;
use std::marker::PhantomData;
use std::rc::Rc;
use std::time::Duration;

/// Wrapper for `setTimeout` with controls.
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_timeout_fn)
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos_use::{use_timeout_fn, UseTimeoutFnReturn};
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// let UseTimeoutFnReturn { start, stop, is_pending, .. } = use_timeout_fn(
///     |i: i32| {
///         // do sth
///     },
///     3000.0
/// );
///
/// start(3);
/// #
/// # view! { }
/// # }
/// ```
pub fn use_timeout_fn<CbFn, Arg, D>(
    callback: CbFn,
    delay: D,
) -> UseTimeoutFnReturn<impl Fn(Arg) + Clone, Arg, impl Fn() + Clone>
where
    CbFn: Fn(Arg) + Clone + 'static,
    Arg: 'static,
    D: Into<MaybeSignal<f64>>,
{
    let delay = delay.into();

    let (is_pending, set_pending) = create_signal(false);

    let timer = Rc::new(Cell::new(None::<TimeoutHandle>));

    let clear = {
        let timer = Rc::clone(&timer);

        move || {
            if let Some(timer) = timer.take() {
                timer.clear();
            }
        }
    };

    let stop = {
        let clear = clear.clone();

        move || {
            set_pending.set(false);
            clear();
        }
    };

    let start = {
        let timer = Rc::clone(&timer);
        let callback = callback.clone();

        move |arg: Arg| {
            set_pending.set(true);

            let handle = set_timeout_with_handle(
                {
                    let timer = Rc::clone(&timer);
                    let callback = callback.clone();

                    move || {
                        set_pending.set(false);
                        timer.set(None);

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

                        callback(arg);

                        #[cfg(debug_assertions)]
                        SpecialNonReactiveZone::exit(prev);
                    }
                },
                Duration::from_millis(delay.get_untracked() as u64),
            )
            .ok();

            timer.set(handle);
        }
    };

    on_cleanup(clear);

    UseTimeoutFnReturn {
        is_pending: is_pending.into(),
        start,
        stop,
        _marker: PhantomData,
    }
}

/// Return type of [`use_timeout_fn`].
pub struct UseTimeoutFnReturn<StartFn, StartArg, StopFn>
where
    StartFn: Fn(StartArg) + Clone,
    StopFn: Fn() + Clone,
{
    /// Whether the timeout is pending. When the `callback` is called this is set to `false`.
    pub is_pending: Signal<bool>,

    /// Start the timeout. The `callback` will be called after `delay` milliseconds.
    pub start: StartFn,

    /// Stop the timeout. If the timeout was still pending the `callback` is not called.
    pub stop: StopFn,

    _marker: PhantomData<StartArg>,
}