1use std::cell::RefCell;
7use std::rc::Rc;
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum ToastLevel {
13 #[default]
15 Info,
16 Success,
18 Warning,
20 Error,
22}
23
24#[derive(Clone)]
26pub struct Toast {
27 pub id: u64,
29 pub message: String,
31 pub level: ToastLevel,
33 pub duration: Duration,
35 pub created_at: Instant,
37}
38
39impl Toast {
40 pub fn is_expired(&self) -> bool {
42 self.created_at.elapsed() >= self.duration
43 }
44
45 pub fn remaining_fraction(&self) -> f32 {
47 let elapsed = self.created_at.elapsed().as_secs_f32();
48 let total = self.duration.as_secs_f32();
49 (1.0 - elapsed / total).max(0.0)
50 }
51}
52
53#[derive(Clone)]
55pub struct ToastQueue {
56 inner: Rc<RefCell<ToastQueueInner>>,
57}
58
59struct ToastQueueInner {
60 toasts: Vec<Toast>,
61 next_id: u64,
62 default_duration: Duration,
63}
64
65impl Default for ToastQueue {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl ToastQueue {
72 pub fn new() -> Self {
74 Self {
75 inner: Rc::new(RefCell::new(ToastQueueInner {
76 toasts: Vec::new(),
77 next_id: 1,
78 default_duration: Duration::from_secs(3),
79 })),
80 }
81 }
82
83 pub fn with_duration(duration: Duration) -> Self {
85 Self {
86 inner: Rc::new(RefCell::new(ToastQueueInner {
87 toasts: Vec::new(),
88 next_id: 1,
89 default_duration: duration,
90 })),
91 }
92 }
93
94 pub fn info(&self, message: impl Into<String>) -> u64 {
96 self.push(message, ToastLevel::Info)
97 }
98
99 pub fn success(&self, message: impl Into<String>) -> u64 {
101 self.push(message, ToastLevel::Success)
102 }
103
104 pub fn warning(&self, message: impl Into<String>) -> u64 {
106 self.push(message, ToastLevel::Warning)
107 }
108
109 pub fn error(&self, message: impl Into<String>) -> u64 {
111 self.push(message, ToastLevel::Error)
112 }
113
114 pub fn error_long(&self, message: impl Into<String>) -> u64 {
116 self.push_with_duration(message, ToastLevel::Error, Duration::from_secs(5))
117 }
118
119 pub fn push(&self, message: impl Into<String>, level: ToastLevel) -> u64 {
121 let duration = self.inner.borrow().default_duration;
122 self.push_with_duration(message, level, duration)
123 }
124
125 pub fn push_with_duration(
127 &self,
128 message: impl Into<String>,
129 level: ToastLevel,
130 duration: Duration,
131 ) -> u64 {
132 let mut inner = self.inner.borrow_mut();
133 let id = inner.next_id;
134 inner.next_id += 1;
135
136 inner.toasts.push(Toast {
137 id,
138 message: message.into(),
139 level,
140 duration,
141 created_at: Instant::now(),
142 });
143
144 id
145 }
146
147 pub fn dismiss(&self, id: u64) {
149 let mut inner = self.inner.borrow_mut();
150 inner.toasts.retain(|t| t.id != id);
151 }
152
153 pub fn clear(&self) {
155 let mut inner = self.inner.borrow_mut();
156 inner.toasts.clear();
157 }
158
159 pub fn collect(&self) -> Vec<Toast> {
161 let mut inner = self.inner.borrow_mut();
162 inner.toasts.retain(|t| !t.is_expired());
164 inner.toasts.clone()
166 }
167
168 pub fn is_empty(&self) -> bool {
170 let inner = self.inner.borrow();
171 inner.toasts.is_empty()
172 }
173
174 pub fn len(&self) -> usize {
176 let inner = self.inner.borrow();
177 inner.toasts.len()
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_toast_queue() {
187 let queue = ToastQueue::with_duration(Duration::from_millis(100));
188
189 let id1 = queue.info("Test message");
190 assert_eq!(queue.len(), 1);
191
192 let _id2 = queue.success("Success!");
193 assert_eq!(queue.len(), 2);
194
195 queue.dismiss(id1);
196 assert_eq!(queue.len(), 1);
197
198 queue.clear();
199 assert!(queue.is_empty());
200 }
201
202 #[test]
203 fn test_toast_expiry() {
204 let toast = Toast {
205 id: 1,
206 message: "Test".to_string(),
207 level: ToastLevel::Info,
208 duration: Duration::from_millis(1),
209 created_at: Instant::now() - Duration::from_millis(10),
210 };
211
212 assert!(toast.is_expired());
213 }
214
215 #[test]
216 fn test_toast_levels() {
217 let queue = ToastQueue::new();
218
219 queue.info("Info");
220 queue.success("Success");
221 queue.warning("Warning");
222 queue.error("Error");
223
224 let toasts = queue.collect();
225 assert_eq!(toasts.len(), 4);
226 assert_eq!(toasts[0].level, ToastLevel::Info);
227 assert_eq!(toasts[1].level, ToastLevel::Success);
228 assert_eq!(toasts[2].level, ToastLevel::Warning);
229 assert_eq!(toasts[3].level, ToastLevel::Error);
230 }
231}