Skip to main content

dioxus_tw_components/components/
toast.rs

1use crate::{components::icon::*, use_unique_id};
2use dioxus::prelude::*;
3
4#[cfg(target_arch = "wasm32")]
5use gloo_timers::future::TimeoutFuture;
6
7#[derive(Default, Debug, Clone, Copy, PartialEq)]
8pub enum ToastColor {
9    #[default]
10    Default,
11    Primary,
12    Secondary,
13    Destructive,
14    Success,
15}
16
17impl std::fmt::Display for ToastColor {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        write!(
20            f,
21            "{}",
22            match self {
23                ToastColor::Default => "default",
24                ToastColor::Primary => "primary",
25                ToastColor::Secondary => "secondary",
26                ToastColor::Destructive => "destructive",
27                ToastColor::Success => "success",
28            }
29        )
30    }
31}
32
33#[derive(Default, Debug, Clone, Copy, PartialEq)]
34pub enum ToastAnimation {
35    None,
36    Light,
37    #[default]
38    Full,
39}
40
41impl std::fmt::Display for ToastAnimation {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(
44            f,
45            "{}",
46            match self {
47                ToastAnimation::None => "none",
48                ToastAnimation::Light => "light",
49                ToastAnimation::Full => "full",
50            }
51        )
52    }
53}
54
55#[derive(Clone, PartialEq, Props)]
56pub struct ToasterProps {
57    #[props(extends = ol, extends = GlobalAttributes)]
58    attributes: Vec<Attribute>,
59
60    children: Element,
61}
62
63/// The toaster must wrap around your App as high as possible to be used
64#[component]
65pub fn Toaster(mut props: ToasterProps) -> Element {
66    let default_classes = "toaster";
67    crate::setup_class_attribute(&mut props.attributes, default_classes);
68
69    let state =
70        use_context_provider::<Signal<ToasterState>>(|| Signal::new(ToasterState::default()));
71
72    rsx! {
73        {props.children}
74        ol { role: "alert", id: "dx-toast", ..props.attributes,
75            if let Some(toast) = &state.read().toast {
76                ToastView { state, toast: toast.clone() }
77            }
78        }
79    }
80}
81
82pub trait ToastRenderer {
83    fn description(&mut self, description: Element) -> &mut Self;
84    fn color(&mut self, color: ToastColor) -> &mut Self;
85    fn title(&mut self, title: impl ToString) -> &mut Self;
86    fn duration_in_ms(&mut self, duration: u32) -> &mut Self;
87    fn animation(&mut self, animation: ToastAnimation) -> &mut Self;
88    fn is_closable(&mut self, is_closable: bool) -> &mut Self;
89    fn success(&mut self, description: impl ToString);
90    fn error(&mut self, description: impl ToString);
91    fn loading(&mut self, description: impl ToString);
92    fn render(&mut self);
93}
94
95impl ToastRenderer for Signal<ToasterState> {
96    fn description(&mut self, description: Element) -> &mut Self {
97        let shape = self.peek().shape.clone();
98        self.write().shape = shape.description(description);
99        self
100    }
101
102    fn color(&mut self, color: ToastColor) -> &mut Self {
103        let shape = self.peek().shape.clone();
104        self.write().shape = shape.color(color);
105        self
106    }
107
108    fn title(&mut self, title: impl ToString) -> &mut Self {
109        let shape = self.peek().shape.clone();
110        self.write().shape = shape.title(title);
111        self
112    }
113
114    fn duration_in_ms(&mut self, duration: u32) -> &mut Self {
115        let shape = self.peek().shape.clone();
116        self.write().shape = shape.duration_in_ms(duration);
117        self
118    }
119
120    fn animation(&mut self, animation: ToastAnimation) -> &mut Self {
121        let shape = self.peek().shape.clone();
122        self.write().shape = shape.animation(animation);
123        self
124    }
125
126    fn is_closable(&mut self, is_closable: bool) -> &mut Self {
127        let shape = self.peek().shape.clone();
128        self.write().shape = shape.is_closable(is_closable);
129        self
130    }
131
132    /// Build a toast with success background color and title "Success"
133    /// The string passed as argument will be the description of the Toast
134    fn success(&mut self, description: impl ToString) {
135        let toast = Toast::default()
136            .title(String::from("Success"))
137            .color(ToastColor::Success)
138            .description(rsx! {
139                p { "{description.to_string()}" }
140            });
141        self.set(ToasterState {
142            toast: Some(toast),
143            shape: Toast::default(),
144        });
145    }
146
147    /// Build a toast with destructive background color and title "Error"
148    /// The string passed as argument will be the description of the Toast
149    fn error(&mut self, description: impl ToString) {
150        let toast = Toast::default()
151            .title(String::from("Error"))
152            .color(ToastColor::Destructive)
153            .description(rsx! {
154                p { "{description.to_string()}" }
155            });
156        self.set(ToasterState {
157            toast: Some(toast),
158            shape: Toast::default(),
159        });
160    }
161
162    /// Build a toast with primary background color and title "Loading"
163    /// The string passed as argument will be the description of the Toast
164    fn loading(&mut self, description: impl ToString) {
165        let toast = Toast::default()
166            .title(String::from("Loading"))
167            .color(ToastColor::Primary)
168            .description(rsx! {
169                p { "{description.to_string()}" }
170            });
171        self.set(ToasterState {
172            toast: Some(toast),
173            shape: Toast::default(),
174        });
175    }
176
177    fn render(&mut self) {
178        let shape = self.peek().shape.clone();
179        self.set(ToasterState {
180            toast: Some(shape),
181            shape: Toast::default(),
182        });
183    }
184}
185
186/// Used to keep track of all the current toasts, for now it only keeps 1 Toast
187#[derive(Default)]
188pub struct ToasterState {
189    pub toast: Option<Toast>,
190    pub shape: Toast,
191}
192
193/// A Toast with a default duration of 10s
194#[derive(Clone, Debug, PartialEq)]
195pub struct Toast {
196    id: String,
197    title: String,
198    description: Element,
199    duration_in_ms: u32,
200    is_closable: bool,
201    pub color: ToastColor,
202    pub animation: ToastAnimation,
203    state: ToastState,
204}
205
206impl std::default::Default for Toast {
207    fn default() -> Self {
208        Self {
209            id: use_unique_id(),
210            title: String::default(),
211            description: Ok(VNode::default()), // Default this way to be able to check the children
212            duration_in_ms: 6_000,
213            is_closable: true,
214            color: ToastColor::default(),
215            animation: ToastAnimation::default(),
216            state: ToastState::Opening,
217        }
218    }
219}
220
221impl Toast {
222    pub fn title(mut self, title: impl ToString) -> Self {
223        self.title = title.to_string();
224        self
225    }
226
227    pub fn description(mut self, description: Element) -> Self {
228        self.description = description;
229        self
230    }
231
232    pub fn color(mut self, color: ToastColor) -> Self {
233        self.color = color;
234        self
235    }
236
237    pub fn animation(mut self, animation: ToastAnimation) -> Self {
238        self.animation = animation;
239        self
240    }
241
242    pub fn duration_in_ms(mut self, duration: u32) -> Self {
243        self.duration_in_ms = duration;
244        self
245    }
246
247    pub fn is_closable(mut self, is_closable: bool) -> Self {
248        self.is_closable = is_closable;
249        self
250    }
251}
252
253/// Define the state of an individual toast, used to animate the Toast
254#[derive(Clone, Debug, PartialEq, Default)]
255enum ToastState {
256    #[default]
257    Opening,
258    Open,
259    Closing,
260    // Close is not needed since it means the Toast does not exist anymore
261}
262
263impl std::fmt::Display for ToastState {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        write!(
266            f,
267            "{}",
268            match self {
269                ToastState::Opening => "opening",
270                ToastState::Open => "open",
271                ToastState::Closing => "closing",
272            }
273        )
274    }
275}
276
277/// Used to render the Toast, also update the ToasterState
278#[component]
279fn ToastView(mut state: Signal<ToasterState>, toast: ReadSignal<Toast>) -> Element {
280    let mut toast_state = use_signal(|| ToastState::Opening);
281
282    let duration_in_ms = toast.read().duration_in_ms;
283    let toast_animation = toast.read().animation;
284
285    // This is to animate the Toast in and out
286    //   use_effect(move || {
287    //       let mut timer = document::eval(&format!(
288    //           "setInterval(() => {{
289    //               dioxus.send(true);
290    //           }}, {});",
291    //       ), );
292    //   });
293    use_future(move || async move {
294        if toast_animation != ToastAnimation::None {
295            #[cfg(target_arch = "wasm32")]
296            {
297                TimeoutFuture::new(10).await;
298            }
299            #[cfg(not(target_arch = "wasm32"))]
300            {
301                let _ = tokio::time::sleep(std::time::Duration::from_millis(10)).await;
302            }
303            toast_state.set(ToastState::Open);
304
305            let animation_play_time = 150;
306            let animation_duration = duration_in_ms.saturating_sub(animation_play_time);
307            #[cfg(target_arch = "wasm32")]
308            {
309                TimeoutFuture::new(animation_duration).await;
310            }
311            #[cfg(not(target_arch = "wasm32"))]
312            {
313                let _ =
314                    tokio::time::sleep(std::time::Duration::from_millis(animation_duration as u64))
315                        .await;
316            }
317
318            toast_state.set(ToastState::Closing);
319            #[cfg(target_arch = "wasm32")]
320            {
321                TimeoutFuture::new(animation_play_time).await;
322            }
323            #[cfg(not(target_arch = "wasm32"))]
324            {
325                let _ = tokio::time::sleep(std::time::Duration::from_millis(
326                    animation_play_time as u64,
327                ))
328                .await;
329            }
330        } else {
331            #[cfg(target_arch = "wasm32")]
332            {
333                TimeoutFuture::new(duration_in_ms).await;
334            }
335            #[cfg(not(target_arch = "wasm32"))]
336            {
337                let _ = tokio::time::sleep(std::time::Duration::from_millis(duration_in_ms as u64))
338                    .await;
339            }
340        }
341
342        state.set(ToasterState::default());
343    });
344
345    rsx! {
346        li {
347            class: "toast",
348            id: "{toast.read().id}",
349            "data-state": toast_state.read().to_string(),
350            "data-style": toast.read().color.to_string(),
351            "data-animation": toast_animation.to_string(),
352            h6 { class: "h6", "{toast.read().title}" }
353            if toast.read().is_closable {
354                ToastClose { state, toast_state }
355            }
356            {toast.read().description.clone()}
357        }
358    }
359}
360
361/// Used to add a cross mark to manually close the Toast
362/// The Timeout is there to let the animation some time to play
363#[component]
364fn ToastClose(mut state: Signal<ToasterState>, mut toast_state: Signal<ToastState>) -> Element {
365    rsx! {
366        button {
367            class: "toast-close",
368            r#type: "button",
369            onclick: move |_| {
370                spawn(async move {
371                    toast_state.set(ToastState::Closing);
372                    #[cfg(target_arch = "wasm32")]
373                    {
374                        TimeoutFuture::new(150).await;
375                    }
376                    #[cfg(not(target_arch = "wasm32"))]
377                    {
378                        let _ = tokio::time::sleep(std::time::Duration::from_millis(150)).await;
379                    }
380                    state.set(ToasterState::default());
381                });
382            },
383            Icon { style: "font-size: 0.75rem;", icon: Icons::Close }
384        }
385    }
386}
387
388/// Hook that returns the ToasterState to spawn a Toast
389pub fn use_toast() -> Signal<ToasterState> {
390    // Will panic if no Toaster {} upper in the DOM
391    use_context::<Signal<ToasterState>>()
392}