1use std::time::Instant;
2
3use ratatui::{
4 layout::Rect,
5 style::{Color, Style},
6 widgets::{Block, Borders, Clear, Paragraph, Wrap},
7 Frame,
8};
9
10use crate::Theme;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum ToastLevel {
14 Success,
15 Warning,
16 Error,
17}
18
19pub struct Toast {
20 pub message: String,
21 pub level: ToastLevel,
22 created_at: Instant,
23 pub duration_ms: u64,
24}
25
26impl Toast {
27 pub fn new(message: String, level: ToastLevel, duration_ms: u64) -> Self {
28 Self {
29 message,
30 level,
31 created_at: Instant::now(),
32 duration_ms,
33 }
34 }
35
36 pub fn is_expired(&self) -> bool {
37 self.created_at.elapsed().as_millis() as u64 >= self.duration_ms
38 }
39
40 fn is_fading(&self) -> bool {
43 let elapsed_ms = self.created_at.elapsed().as_millis() as u64;
44 let fade_start_ms = self.duration_ms * 2 / 3;
45 elapsed_ms >= fade_start_ms
46 }
47
48 fn content_lines(&self, content_width: usize) -> u16 {
50 if content_width == 0 {
51 return 1;
52 }
53 let chars = self.message.chars().count();
54 let lines = (chars + content_width - 1) / content_width;
55 (lines as u16).max(1).min(3)
56 }
57}
58
59pub fn render_toasts(f: &mut Frame, toasts: &[Toast], theme: &Theme) {
61 const TOAST_WIDTH: u16 = 56;
62 const MARGIN_RIGHT: u16 = 1;
63 const MARGIN_TOP: u16 = 1;
64 const CONTENT_WIDTH: usize = (TOAST_WIDTH - 4) as usize;
66
67 let area = f.area();
68 let x = area.width.saturating_sub(TOAST_WIDTH + MARGIN_RIGHT);
69
70 let mut y = MARGIN_TOP;
71 for toast in toasts.iter().rev() {
72 let content_lines = toast.content_lines(CONTENT_WIDTH);
73 let toast_height = 2 + content_lines; if y + toast_height > area.height {
76 break;
77 }
78
79 let normal_style = match toast.level {
80 ToastLevel::Success => theme.success,
81 ToastLevel::Warning => theme.border_warning,
82 ToastLevel::Error => theme.border_error,
83 };
84 let style = if toast.is_fading() {
85 Style::default().fg(Color::DarkGray)
86 } else {
87 normal_style
88 };
89
90 let toast_area = Rect { x, y, width: TOAST_WIDTH, height: toast_height };
91
92 let block = Block::default()
93 .borders(Borders::ALL)
94 .border_style(style);
95
96 let body = Paragraph::new(toast.message.as_str())
97 .style(style)
98 .block(block)
99 .wrap(Wrap { trim: true });
100
101 f.render_widget(Clear, toast_area);
102 f.render_widget(body, toast_area);
103
104 y += toast_height + 1; }
106}