use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ToastLevel {
#[default]
Info,
Success,
Warning,
Error,
}
#[derive(Clone)]
pub struct Toast {
pub id: u64,
pub message: String,
pub level: ToastLevel,
pub duration: Duration,
pub created_at: Instant,
}
impl Toast {
pub fn is_expired(&self) -> bool {
self.created_at.elapsed() >= self.duration
}
pub fn remaining_fraction(&self) -> f32 {
let elapsed = self.created_at.elapsed().as_secs_f32();
let total = self.duration.as_secs_f32();
(1.0 - elapsed / total).max(0.0)
}
}
#[derive(Clone)]
pub struct ToastQueue {
inner: Rc<RefCell<ToastQueueInner>>,
}
struct ToastQueueInner {
toasts: Vec<Toast>,
next_id: u64,
default_duration: Duration,
}
impl Default for ToastQueue {
fn default() -> Self {
Self::new()
}
}
impl ToastQueue {
pub fn new() -> Self {
Self {
inner: Rc::new(RefCell::new(ToastQueueInner {
toasts: Vec::new(),
next_id: 1,
default_duration: Duration::from_secs(3),
})),
}
}
pub fn with_duration(duration: Duration) -> Self {
Self {
inner: Rc::new(RefCell::new(ToastQueueInner {
toasts: Vec::new(),
next_id: 1,
default_duration: duration,
})),
}
}
pub fn info(&self, message: impl Into<String>) -> u64 {
self.push(message, ToastLevel::Info)
}
pub fn success(&self, message: impl Into<String>) -> u64 {
self.push(message, ToastLevel::Success)
}
pub fn warning(&self, message: impl Into<String>) -> u64 {
self.push(message, ToastLevel::Warning)
}
pub fn error(&self, message: impl Into<String>) -> u64 {
self.push(message, ToastLevel::Error)
}
pub fn error_long(&self, message: impl Into<String>) -> u64 {
self.push_with_duration(message, ToastLevel::Error, Duration::from_secs(5))
}
pub fn push(&self, message: impl Into<String>, level: ToastLevel) -> u64 {
let duration = self.inner.borrow().default_duration;
self.push_with_duration(message, level, duration)
}
pub fn push_with_duration(
&self,
message: impl Into<String>,
level: ToastLevel,
duration: Duration,
) -> u64 {
let mut inner = self.inner.borrow_mut();
let id = inner.next_id;
inner.next_id += 1;
inner.toasts.push(Toast {
id,
message: message.into(),
level,
duration,
created_at: Instant::now(),
});
id
}
pub fn dismiss(&self, id: u64) {
let mut inner = self.inner.borrow_mut();
inner.toasts.retain(|t| t.id != id);
}
pub fn clear(&self) {
let mut inner = self.inner.borrow_mut();
inner.toasts.clear();
}
pub fn collect(&self) -> Vec<Toast> {
let mut inner = self.inner.borrow_mut();
inner.toasts.retain(|t| !t.is_expired());
inner.toasts.clone()
}
pub fn is_empty(&self) -> bool {
let inner = self.inner.borrow();
inner.toasts.is_empty()
}
pub fn len(&self) -> usize {
let inner = self.inner.borrow();
inner.toasts.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_toast_queue() {
let queue = ToastQueue::with_duration(Duration::from_millis(100));
let id1 = queue.info("Test message");
assert_eq!(queue.len(), 1);
let _id2 = queue.success("Success!");
assert_eq!(queue.len(), 2);
queue.dismiss(id1);
assert_eq!(queue.len(), 1);
queue.clear();
assert!(queue.is_empty());
}
#[test]
fn test_toast_expiry() {
let toast = Toast {
id: 1,
message: "Test".to_string(),
level: ToastLevel::Info,
duration: Duration::from_millis(1),
created_at: Instant::now() - Duration::from_millis(10),
};
assert!(toast.is_expired());
}
#[test]
fn test_toast_levels() {
let queue = ToastQueue::new();
queue.info("Info");
queue.success("Success");
queue.warning("Warning");
queue.error("Error");
let toasts = queue.collect();
assert_eq!(toasts.len(), 4);
assert_eq!(toasts[0].level, ToastLevel::Info);
assert_eq!(toasts[1].level, ToastLevel::Success);
assert_eq!(toasts[2].level, ToastLevel::Warning);
assert_eq!(toasts[3].level, ToastLevel::Error);
}
}