dioxus_sdk_time/timeout.rs
1use dioxus::{
2 core::Task,
3 dioxus_core::SpawnIfAsync,
4 prelude::{Callback, spawn, use_hook},
5 signals::Signal,
6};
7use futures::{SinkExt, StreamExt, channel::mpsc};
8use std::time::Duration;
9
10/// The interface to a timeout.
11///
12/// This is used to trigger the timeout with [`UseTimeout::action`].
13///
14/// See [`use_timeout`] for more information.
15pub struct UseTimeout<Args: 'static> {
16 duration: Duration,
17 sender: Signal<mpsc::UnboundedSender<Args>>,
18}
19
20impl<Args> UseTimeout<Args> {
21 /// Trigger the timeout.
22 ///
23 /// If no arguments are desired, use the [`unit`] type.
24 /// See [`use_timeout`] for more information.
25 pub fn action(&self, args: Args) -> TimeoutHandle {
26 let mut sender = (self.sender)();
27 let duration = self.duration;
28
29 let handle = spawn(async move {
30 #[cfg(not(target_family = "wasm"))]
31 tokio::time::sleep(duration).await;
32
33 #[cfg(target_family = "wasm")]
34 gloo_timers::future::sleep(duration).await;
35
36 // If this errors then the timeout was likely dropped.
37 let _ = sender.send(args).await;
38 });
39
40 TimeoutHandle { handle }
41 }
42}
43
44impl<Args> Clone for UseTimeout<Args> {
45 fn clone(&self) -> Self {
46 *self
47 }
48}
49impl<Args> Copy for UseTimeout<Args> {}
50impl<Args> PartialEq for UseTimeout<Args> {
51 fn eq(&self, other: &Self) -> bool {
52 self.duration == other.duration && self.sender == other.sender
53 }
54}
55
56/// A handle to a pending timeout.
57///
58/// A handle to a running timeout triggered with [`UseTimeout::action`].
59/// This handle allows you to cancel the timeout from triggering with [`TimeoutHandle::cancel`]
60///
61/// See [`use_timeout`] for more information.
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct TimeoutHandle {
64 handle: Task,
65}
66
67impl TimeoutHandle {
68 /// Cancel the timeout associated with this handle.
69 pub fn cancel(self) {
70 self.handle.cancel();
71 }
72}
73
74/// A hook to run a callback after a period of time.
75///
76/// Timeouts allow you to trigger a callback that occurs after a period of time. Unlike a debounce, a timeout will not
77/// reset it's timer when triggered again. Instead, calling a timeout while it is already running will start another instance
78/// to run the callback after the provided period.
79///
80/// This hook is similar to the web [setTimeout()](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout) API.
81///
82/// # Examples
83///
84/// Example of using a timeout:
85/// ```rust
86/// use dioxus::prelude::*;
87/// use dioxus_sdk_time::use_timeout;
88/// use std::time::Duration;
89///
90/// #[component]
91/// fn App() -> Element {
92/// // Create a timeout for two seconds.
93/// // Once triggered, this timeout will print "timeout called" after two seconds.
94/// let timeout = use_timeout(Duration::from_secs(2), |()| println!("timeout called"));
95///
96/// rsx! {
97/// button {
98/// onclick: move |_| {
99/// // Trigger the timeout.
100/// timeout.action(());
101/// },
102/// "Click!"
103/// }
104/// }
105/// }
106/// ```
107///
108/// #### Cancelling Timeouts
109/// Example of cancelling a timeout. This is the equivalent of a debounce.
110/// ```rust
111/// use dioxus::prelude::*;
112/// use dioxus_sdk_time::{use_timeout, TimeoutHandle};
113/// use std::time::Duration;
114///
115/// #[component]
116/// fn App() -> Element {
117/// let mut current_timeout: Signal<Option<TimeoutHandle>> = use_signal(|| None);
118/// let timeout = use_timeout(Duration::from_secs(2), move |()| {
119/// current_timeout.set(None);
120/// println!("timeout called");
121/// });
122///
123/// rsx! {
124/// button {
125/// onclick: move |_| {
126/// // Cancel any currently running timeouts.
127/// if let Some(handle) = *current_timeout.read() {
128/// handle.cancel();
129/// }
130///
131/// // Trigger the timeout.
132/// let handle = timeout.action(());
133/// current_timeout.set(Some(handle));
134/// },
135/// "Click!"
136/// }
137/// }
138/// }
139/// ```
140///
141/// #### Async Timeouts
142/// Timeouts can accept an async callback:
143/// ```rust
144/// use dioxus::prelude::*;
145/// use dioxus_sdk_time::use_timeout;
146/// use std::time::Duration;
147///
148/// #[component]
149/// fn App() -> Element {
150/// // Create a timeout for two seconds.
151/// // We use an async sleep to wait an even longer duration after the timeout is called.
152/// let timeout = use_timeout(Duration::from_secs(2), |()| async {
153/// println!("Timeout after two total seconds.");
154/// tokio::time::sleep(Duration::from_secs(2)).await;
155/// println!("Timeout after four total seconds.");
156/// });
157///
158/// rsx! {
159/// button {
160/// onclick: move |_| {
161/// // Trigger the timeout.
162/// timeout.action(());
163/// },
164/// "Click!"
165/// }
166/// }
167/// }
168/// ```
169pub fn use_timeout<Args: 'static, MaybeAsync: SpawnIfAsync<Marker>, Marker>(
170 duration: Duration,
171 callback: impl FnMut(Args) -> MaybeAsync + 'static,
172) -> UseTimeout<Args> {
173 use_hook(|| {
174 let callback = Callback::new(callback);
175 let (sender, mut receiver) = mpsc::unbounded();
176
177 spawn(async move {
178 loop {
179 if let Some(args) = receiver.next().await {
180 callback.call(args);
181 }
182 }
183 });
184
185 UseTimeout {
186 duration,
187 sender: Signal::new(sender),
188 }
189 })
190}