use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::overlays::{Overlay, OverlayPosition, Transition};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub enum ToastKind {
Info,
Success,
Warning,
Error,
Copy,
}
impl ToastKind {
pub fn icon(&self) -> &str {
match self {
ToastKind::Info => "ℹ",
ToastKind::Success => "✓",
ToastKind::Warning => "⚠",
ToastKind::Error => "✗",
ToastKind::Copy => "📋",
}
}
pub fn color(&self, theme_color: Color) -> Color {
match self {
ToastKind::Info => theme_color,
ToastKind::Success => Color::rgb(63, 185, 80),
ToastKind::Warning => Color::rgb(210, 153, 34),
ToastKind::Error => Color::rgb(248, 81, 73),
ToastKind::Copy => Color::rgb(88, 166, 255),
}
}
}
#[derive(Debug, Clone)]
pub struct Toast {
pub message: String,
pub kind: ToastKind,
pub position: OverlayPosition,
pub visible: bool,
pub opacity: f32,
pub lifetime: Duration,
pub elapsed: Duration,
pub transition: Transition,
pub transition_progress: f32,
pub max_width: u16,
}
impl Toast {
pub fn new(message: &str, kind: ToastKind) -> Self {
Self {
message: message.to_string(),
kind,
position: OverlayPosition::BottomCenter,
visible: true,
opacity: 1.0,
lifetime: Duration::from_secs(3),
elapsed: Duration::ZERO,
transition: Transition::Fade,
transition_progress: 1.0,
max_width: 50,
}
}
pub fn with_position(mut self, pos: OverlayPosition) -> Self {
self.position = pos;
self
}
pub fn with_lifetime(mut self, dur: Duration) -> Self {
self.lifetime = dur;
self
}
pub fn with_transition(mut self, t: Transition) -> Self {
self.transition = t;
self
}
pub fn with_max_width(mut self, w: u16) -> Self {
self.max_width = w;
self
}
pub fn is_expired(&self) -> bool {
self.elapsed >= self.lifetime
}
fn render_toast(&self, buffer: &mut Buffer, area: Rect) {
let icon = self.kind.icon();
let color = self.kind.color(Color::rgb(88, 166, 255));
let text = format!(" {} {} ", icon, self.message);
let display_text: String = text.chars().take(self.max_width as usize).collect();
let w = display_text.len() as u16 + 4;
let h = 3;
let pos = self.position_in_area(area, w, h);
let toast_rect = Rect::new(pos.0, pos.1, w, h);
let bg = Color::rgb(22, 27, 34);
let border = color;
buffer.fill(toast_rect, ' ', Color::WHITE, Some(bg));
for x in toast_rect.x..toast_rect.right() {
buffer.set(
x as usize,
toast_rect.y as usize,
crate::core::buffer::Cell {
ch: '─',
fg: border.dim(0.3),
bg: Some(bg),
bold: false,
italic: false,
underlined: false,
},
);
buffer.set(
x as usize,
toast_rect.bottom() as usize - 1,
crate::core::buffer::Cell {
ch: '─',
fg: border.dim(0.3),
bg: Some(bg),
bold: false,
italic: false,
underlined: false,
},
);
}
buffer.set(
toast_rect.x as usize,
toast_rect.y as usize,
crate::core::buffer::Cell {
ch: '╭',
fg: border,
bg: Some(bg),
bold: true,
italic: false,
underlined: false,
},
);
buffer.set(
toast_rect.right() as usize - 1,
toast_rect.y as usize,
crate::core::buffer::Cell {
ch: '╮',
fg: border,
bg: Some(bg),
bold: true,
italic: false,
underlined: false,
},
);
buffer.set(
toast_rect.x as usize,
toast_rect.bottom() as usize - 1,
crate::core::buffer::Cell {
ch: '╰',
fg: border,
bg: Some(bg),
bold: true,
italic: false,
underlined: false,
},
);
buffer.set(
toast_rect.right() as usize - 1,
toast_rect.bottom() as usize - 1,
crate::core::buffer::Cell {
ch: '╯',
fg: border,
bg: Some(bg),
bold: true,
italic: false,
underlined: false,
},
);
let text_x = toast_rect.x as usize + 2;
let text_y = toast_rect.y as usize + 1;
buffer.set_str(text_x, text_y, &display_text, color, Some(bg));
}
fn position_in_area(&self, area: Rect, w: u16, h: u16) -> (u16, u16) {
match self.position {
OverlayPosition::TopLeft => (area.x + 1, area.y + 1),
OverlayPosition::TopCenter => (area.x + (area.width.saturating_sub(w)) / 2, area.y + 1),
OverlayPosition::TopRight => (area.right().saturating_sub(w + 1), area.y + 1),
OverlayPosition::CenterLeft => {
(area.x + 1, area.y + (area.height.saturating_sub(h)) / 2)
}
OverlayPosition::Center => (
area.x + (area.width.saturating_sub(w)) / 2,
area.y + (area.height.saturating_sub(h)) / 2,
),
OverlayPosition::CenterRight => (
area.right().saturating_sub(w + 1),
area.y + (area.height.saturating_sub(h)) / 2,
),
OverlayPosition::BottomLeft => (area.x + 1, area.bottom().saturating_sub(h + 1)),
OverlayPosition::BottomCenter => (
area.x + (area.width.saturating_sub(w)) / 2,
area.bottom().saturating_sub(h + 1),
),
OverlayPosition::BottomRight => (
area.right().saturating_sub(w + 1),
area.bottom().saturating_sub(h + 1),
),
}
}
}
impl Overlay for Toast {
fn is_visible(&self) -> bool {
self.visible && !self.is_expired()
}
fn show(&mut self) {
self.visible = true;
self.elapsed = Duration::ZERO;
self.transition_progress = 0.0;
}
fn hide(&mut self) {
self.visible = false;
}
fn toggle(&mut self) {
if self.visible {
self.hide();
} else {
self.show();
}
}
fn update(&mut self, delta: Duration) {
self.elapsed += delta;
let fade_start = self.lifetime.saturating_sub(Duration::from_secs(1));
if self.elapsed > fade_start {
let fade_elapsed = self.elapsed - fade_start;
let fade_duration = self.lifetime - fade_start;
if !fade_duration.is_zero() {
self.opacity =
1.0 - (fade_elapsed.as_secs_f32() / fade_duration.as_secs_f32()).min(1.0);
}
}
if self.transition == Transition::Fade && self.transition_progress < 1.0 {
self.transition_progress =
(self.transition_progress + delta.as_secs_f32() * 3.0).min(1.0);
}
}
fn render(&self, buffer: &mut Buffer, area: Rect) {
if self.is_visible() {
self.render_toast(buffer, area);
}
}
fn position(&self, screen: Rect) -> Rect {
screen
}
}
pub fn copy_toast(message: &str) -> Toast {
Toast::new(message, ToastKind::Copy)
.with_position(OverlayPosition::BottomCenter)
.with_lifetime(Duration::from_secs(2))
.with_transition(Transition::Fade)
}