Skip to main content

telex/
toast.rs

1//! Toast notification system for ephemeral messages.
2//!
3//! Toasts are non-interactive, auto-dismissing notifications that appear
4//! in a corner of the screen.
5
6use std::cell::RefCell;
7use std::rc::Rc;
8use std::time::{Duration, Instant};
9
10/// The severity level of a toast notification.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum ToastLevel {
13    /// Informational message (default).
14    #[default]
15    Info,
16    /// Success message.
17    Success,
18    /// Warning message.
19    Warning,
20    /// Error message.
21    Error,
22}
23
24/// A single toast notification.
25#[derive(Clone)]
26pub struct Toast {
27    /// Unique identifier for this toast.
28    pub id: u64,
29    /// The message to display.
30    pub message: String,
31    /// Severity level.
32    pub level: ToastLevel,
33    /// Duration before auto-dismiss.
34    pub duration: Duration,
35    /// When this toast was created.
36    pub created_at: Instant,
37}
38
39impl Toast {
40    /// Check if this toast has expired.
41    pub fn is_expired(&self) -> bool {
42        self.created_at.elapsed() >= self.duration
43    }
44
45    /// Get the remaining time as a fraction (0.0 to 1.0).
46    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/// A queue of toast notifications.
54#[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    /// Create a new toast queue.
73    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    /// Create a new toast queue with a custom default duration.
84    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    /// Show an info toast.
95    pub fn info(&self, message: impl Into<String>) -> u64 {
96        self.push(message, ToastLevel::Info)
97    }
98
99    /// Show a success toast.
100    pub fn success(&self, message: impl Into<String>) -> u64 {
101        self.push(message, ToastLevel::Success)
102    }
103
104    /// Show a warning toast.
105    pub fn warning(&self, message: impl Into<String>) -> u64 {
106        self.push(message, ToastLevel::Warning)
107    }
108
109    /// Show an error toast.
110    pub fn error(&self, message: impl Into<String>) -> u64 {
111        self.push(message, ToastLevel::Error)
112    }
113
114    /// Show an error toast with a longer duration (5 seconds).
115    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    /// Push a toast with a specific level.
120    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    /// Push a toast with a specific level and duration.
126    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    /// Remove a specific toast by ID.
148    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    /// Clear all toasts.
154    pub fn clear(&self) {
155        let mut inner = self.inner.borrow_mut();
156        inner.toasts.clear();
157    }
158
159    /// Remove expired toasts and return the current list.
160    pub fn collect(&self) -> Vec<Toast> {
161        let mut inner = self.inner.borrow_mut();
162        // Remove expired toasts
163        inner.toasts.retain(|t| !t.is_expired());
164        // Return a copy
165        inner.toasts.clone()
166    }
167
168    /// Check if there are any toasts to display.
169    pub fn is_empty(&self) -> bool {
170        let inner = self.inner.borrow();
171        inner.toasts.is_empty()
172    }
173
174    /// Get the number of active toasts.
175    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}