use crate::styles::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToastVariant {
Success,
Info,
Warning,
Error,
}
impl ToastVariant {
#[must_use]
pub fn icon(&self) -> &'static str {
match self {
ToastVariant::Success => "\u{2714}", ToastVariant::Info => "\u{2139}", ToastVariant::Warning => "\u{26A0}", ToastVariant::Error => "\u{2718}", }
}
#[must_use]
pub fn color(&self) -> ratatui::style::Color {
let t = theme();
match self {
ToastVariant::Success => t.success,
ToastVariant::Info => t.primary,
ToastVariant::Warning => t.warning,
ToastVariant::Error => t.error,
}
}
}
#[derive(Debug, Clone)]
pub struct Toast {
pub message: String,
pub variant: ToastVariant,
pub created_at: Instant,
pub duration: Duration,
}
impl Toast {
pub fn new(message: impl Into<String>, variant: ToastVariant) -> Self {
Self {
message: message.into(),
variant,
created_at: Instant::now(),
duration: Duration::from_secs(3),
}
}
pub fn success(message: impl Into<String>) -> Self {
Self::new(message, ToastVariant::Success)
}
pub fn info(message: impl Into<String>) -> Self {
Self::new(message, ToastVariant::Info)
}
pub fn warning(message: impl Into<String>) -> Self {
Self::new(message, ToastVariant::Warning)
}
pub fn error(message: impl Into<String>) -> Self {
Self::new(message, ToastVariant::Error)
}
#[must_use]
pub fn with_duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
#[must_use]
pub fn is_expired(&self) -> bool {
self.created_at.elapsed() >= self.duration
}
}
pub struct ToastWidget<'a> {
toast: &'a Toast,
}
impl<'a> ToastWidget<'a> {
#[must_use]
pub fn new(toast: &'a Toast) -> Self {
Self { toast }
}
fn calculate_area(&self, area: Rect) -> Rect {
let toast_width = 40u16.min(area.width.saturating_sub(4));
let toast_height = 3u16;
let x = area.x + area.width.saturating_sub(toast_width + 2);
let y = area.y + area.height.saturating_sub(toast_height + 3);
Rect::new(x, y, toast_width, toast_height)
}
}
impl Widget for ToastWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let toast_area = self.calculate_area(area);
let t = theme();
Widget::render(Clear, toast_area, buf);
let icon = self.toast.variant.icon();
let message = format!(" {} {} ", icon, self.toast.message);
let color = self.toast.variant.color();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(color))
.style(Style::default().bg(t.background));
let paragraph = Paragraph::new(message)
.block(block)
.style(Style::default().fg(t.text).add_modifier(Modifier::BOLD))
.alignment(Alignment::Left)
.wrap(Wrap { trim: true });
Widget::render(paragraph, toast_area, buf);
}
}
#[derive(Debug, Default)]
pub struct ToastManager {
current: Option<Toast>,
}
impl ToastManager {
#[must_use]
pub fn new() -> Self {
Self { current: None }
}
pub fn push(&mut self, toast: Toast) {
self.current = Some(toast);
}
pub fn success(&mut self, message: impl Into<String>) {
self.push(Toast::success(message));
}
pub fn info(&mut self, message: impl Into<String>) {
self.push(Toast::info(message));
}
pub fn warning(&mut self, message: impl Into<String>) {
self.push(Toast::warning(message));
}
pub fn error(&mut self, message: impl Into<String>) {
self.push(Toast::error(message));
}
pub fn tick(&mut self) -> bool {
if let Some(ref toast) = self.current {
if toast.is_expired() {
self.current = None;
}
}
self.current.is_some()
}
#[must_use]
pub fn current(&self) -> Option<&Toast> {
self.current.as_ref()
}
#[must_use]
pub fn has_toast(&self) -> bool {
self.current.is_some()
}
pub fn render(&self, frame: &mut ratatui::Frame, area: Rect) {
if let Some(toast) = self.current() {
frame.render_widget(ToastWidget::new(toast), area);
}
}
pub fn clear(&mut self) {
self.current = None;
}
}