dioxus_tw_components/components/
toast.rs1use 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#[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 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 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 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#[derive(Default)]
188pub struct ToasterState {
189 pub toast: Option<Toast>,
190 pub shape: Toast,
191}
192
193#[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()), 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#[derive(Clone, Debug, PartialEq, Default)]
255enum ToastState {
256 #[default]
257 Opening,
258 Open,
259 Closing,
260 }
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#[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 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#[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
388pub fn use_toast() -> Signal<ToasterState> {
390 use_context::<Signal<ToasterState>>()
392}