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
use dioxus::prelude::*;
/// Toast notification type.
#[derive(Clone, Debug, PartialEq)]
pub enum ToastType {
Info,
Success,
Warning,
Error,
}
/// Toast notification data.
#[derive(Clone, Debug)]
pub struct Toast {
pub id: u64,
pub message: String,
pub toast_type: ToastType,
pub duration_ms: u64,
}
/// Global toast state shared via context.
#[derive(Clone)]
pub struct ToastState {
pub toasts: Vec<Toast>,
next_id: u64,
}
impl Default for ToastState {
fn default() -> Self {
Self {
toasts: Vec::new(),
next_id: 1,
}
}
}
impl ToastState {
/// Add a new toast notification.
pub fn push(&mut self, message: String, toast_type: ToastType) {
let id = self.next_id;
self.next_id += 1;
self.toasts.push(Toast {
id,
message,
toast_type,
duration_ms: 5000,
});
}
/// Add a toast with custom duration.
pub fn push_with_duration(&mut self, message: String, toast_type: ToastType, duration_ms: u64) {
let id = self.next_id;
self.next_id += 1;
self.toasts.push(Toast {
id,
message,
toast_type,
duration_ms,
});
}
/// Remove a toast by ID.
pub fn dismiss(&mut self, id: u64) {
self.toasts.retain(|t| t.id != id);
}
/// Convenience: show an info toast.
pub fn info(&mut self, message: impl Into<String>) {
self.push(message.into(), ToastType::Info);
}
/// Convenience: show a success toast.
pub fn success(&mut self, message: impl Into<String>) {
self.push(message.into(), ToastType::Success);
}
/// Convenience: show a warning toast.
pub fn warning(&mut self, message: impl Into<String>) {
self.push(message.into(), ToastType::Warning);
}
/// Convenience: show an error toast.
pub fn error(&mut self, message: impl Into<String>) {
self.push(message.into(), ToastType::Error);
}
}
/// Toast notification container — renders all active toasts.
///
/// Place this component once at the root of your app (e.g. in App).
/// Other components can show toasts via `use_context::<Signal<ToastState>>()`.
#[component]
pub fn ToastContainer() -> Element {
let mut toast_state = use_context::<Signal<ToastState>>();
// Auto-dismiss timer: check every second and remove expired toasts
use_effect(move || {
let toasts = toast_state.read().toasts.clone();
if !toasts.is_empty() {
spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// Remove all toasts that have been around for longer than their duration
let mut ts = toast_state.write();
let len = ts.toasts.len();
if len > 10 {
// Safety: don't accumulate too many
ts.toasts.drain(..len - 5);
}
// Simple approach: dismiss the oldest toast
if !ts.toasts.is_empty() {
ts.toasts.remove(0);
}
});
}
});
let toasts = toast_state.read().toasts.clone();
if toasts.is_empty() {
return rsx! {};
}
rsx! {
div {
class: "toast-container",
for toast in toasts.iter() {
{
let toast_id = toast.id;
let class = match toast.toast_type {
ToastType::Info => "toast toast--info",
ToastType::Success => "toast toast--success",
ToastType::Warning => "toast toast--warning",
ToastType::Error => "toast toast--error",
};
let msg = toast.message.clone();
rsx! {
div {
key: "{toast_id}",
class: "{class}",
span { class: "toast__icon",
match toast.toast_type {
ToastType::Info => "ℹ",
ToastType::Success => "✓",
ToastType::Warning => "⚠",
ToastType::Error => "✕",
}
}
span { class: "toast__message", "{msg}" }
button {
class: "toast__close",
onclick: move |_| {
toast_state.write().dismiss(toast_id);
},
"✕"
}
}
}
}
}
}
}
}