use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ToastIntent {
Debug,
Info,
Success,
Warning,
Error,
}
impl std::fmt::Display for ToastIntent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
ToastIntent::Debug => "Debug",
ToastIntent::Info => "Info",
ToastIntent::Success => "Success",
ToastIntent::Warning => "Warning",
ToastIntent::Error => "Error",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ToastPosition {
#[default]
TopEnd,
TopStart,
TopCenter,
BottomEnd,
BottomStart,
BottomCenter,
}
impl ToastPosition {
#[must_use]
pub fn is_top(self) -> bool {
matches!(
self,
ToastPosition::TopEnd | ToastPosition::TopStart | ToastPosition::TopCenter
)
}
#[must_use]
pub fn is_bottom(self) -> bool {
!self.is_top()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToastLifetime {
Transient(Duration),
Persistent,
}
impl ToastLifetime {
pub const DEFAULT: ToastLifetime = ToastLifetime::Transient(Duration::from_secs(4));
#[must_use]
pub const fn seconds(secs: u64) -> Self {
ToastLifetime::Transient(Duration::from_secs(secs))
}
#[must_use]
pub const fn millis(ms: u64) -> Self {
ToastLifetime::Transient(Duration::from_millis(ms))
}
}
#[derive(Debug, Clone)]
pub struct Toast<Message: Clone> {
pub id: u64,
pub title: String,
pub message: String,
pub intent: ToastIntent,
pub lifetime: ToastLifetime,
pub created_at: Instant,
pub on_dismiss: Message,
}
impl<Message: Clone> Toast<Message> {
pub fn new(
id: u64,
intent: ToastIntent,
title: impl Into<String>,
message: impl Into<String>,
on_dismiss: Message,
) -> Self {
Self {
id,
title: title.into(),
message: message.into(),
intent,
lifetime: ToastLifetime::DEFAULT,
created_at: Instant::now(),
on_dismiss,
}
}
#[must_use]
pub fn with_lifetime(mut self, lifetime: ToastLifetime) -> Self {
self.lifetime = lifetime;
self
}
#[must_use]
pub fn persistent(mut self) -> Self {
self.lifetime = ToastLifetime::Persistent;
self
}
#[must_use]
pub fn with_created_at(mut self, created_at: Instant) -> Self {
self.created_at = created_at;
self
}
#[must_use]
pub fn is_expired(&self, now: Instant) -> bool {
match self.lifetime {
ToastLifetime::Persistent => false,
ToastLifetime::Transient(d) => now.saturating_duration_since(self.created_at) >= d,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn persistent_never_expires() {
let t = Toast::new(1, ToastIntent::Info, "t", "m", ()).persistent();
assert!(!t.is_expired(Instant::now() + Duration::from_secs(3600)));
}
#[test]
fn transient_expires_past_deadline() {
let base = Instant::now();
let t = Toast::new(1, ToastIntent::Info, "t", "m", ())
.with_lifetime(ToastLifetime::millis(100))
.with_created_at(base);
assert!(!t.is_expired(base));
assert!(!t.is_expired(base + Duration::from_millis(50)));
assert!(t.is_expired(base + Duration::from_millis(100)));
assert!(t.is_expired(base + Duration::from_millis(200)));
}
#[test]
fn default_toast_position_is_top_end() {
assert_eq!(ToastPosition::default(), ToastPosition::TopEnd);
}
#[test]
fn top_positions_classify_as_top() {
assert!(ToastPosition::TopEnd.is_top());
assert!(ToastPosition::TopStart.is_top());
assert!(ToastPosition::TopCenter.is_top());
assert!(!ToastPosition::BottomEnd.is_top());
assert!(!ToastPosition::BottomStart.is_top());
assert!(!ToastPosition::BottomCenter.is_top());
}
#[test]
fn is_top_and_is_bottom_partition() {
for pos in [
ToastPosition::TopEnd,
ToastPosition::TopStart,
ToastPosition::TopCenter,
ToastPosition::BottomEnd,
ToastPosition::BottomStart,
ToastPosition::BottomCenter,
] {
assert_ne!(pos.is_top(), pos.is_bottom());
}
}
}