mecomp_tui/ui/widgets/popups/
notification.rs

1use crossterm::event::{KeyEvent, MouseButton, MouseEventKind};
2use ratatui::{
3    prelude::Rect,
4    text::{Line, Text},
5};
6use tokio::sync::mpsc::UnboundedSender;
7
8use crate::{
9    state::action::{Action, PopupAction},
10    ui::components::ComponentRender,
11};
12
13use super::Popup;
14
15#[derive(Debug)]
16pub struct Notification<'a> {
17    pub line: Text<'a>,
18    pub action_tx: UnboundedSender<Action>,
19}
20
21impl Notification<'_> {
22    #[must_use]
23    pub const fn new(line: Text, action_tx: UnboundedSender<Action>) -> Notification {
24        Notification { line, action_tx }
25    }
26}
27
28impl ComponentRender<Rect> for Notification<'_> {
29    fn render_border(&self, frame: &mut ratatui::Frame, area: Rect) -> Rect {
30        self.render_popup_border(frame, area)
31    }
32
33    fn render_content(&self, frame: &mut ratatui::Frame, area: Rect) {
34        frame.render_widget::<Text>(self.line.clone(), area);
35    }
36}
37
38impl Popup for Notification<'_> {
39    fn title(&self) -> Line {
40        Line::raw("Notification")
41    }
42
43    fn instructions(&self) -> Line {
44        Line::raw("Press ESC to close")
45    }
46
47    fn update_with_state(&mut self, _: &crate::ui::AppState) {}
48
49    fn area(&self, terminal_area: Rect) -> Rect {
50        // put in the top left corner, give enough width/height to display the text, and add 2 for the border
51        let width = u16::try_from(
52            self.line
53                .width()
54                .max(self.instructions().width())
55                .max(self.title().width())
56                + 2,
57        )
58        .unwrap_or(terminal_area.width)
59        .min(terminal_area.width);
60        let height = u16::try_from(self.line.height() + 2)
61            .unwrap_or(terminal_area.height)
62            .min(terminal_area.height);
63        Rect::new(0, 0, width, height)
64    }
65
66    fn inner_handle_key_event(&mut self, _key: KeyEvent) {}
67
68    fn inner_handle_mouse_event(&mut self, mouse: crossterm::event::MouseEvent, _: Rect) {
69        // Close the popup when the mouse is clicked
70        if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
71            self.action_tx.send(Action::Popup(PopupAction::Close)).ok();
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::test_utils::setup_test_terminal;
80    use anyhow::Result;
81    use crossterm::event::{KeyModifiers, MouseEvent};
82    use pretty_assertions::assert_eq;
83    use ratatui::{
84        buffer::Buffer,
85        style::{Color, Style},
86        text::Span,
87    };
88    use tokio::sync::mpsc::unbounded_channel;
89
90    #[test]
91    fn test_notification_area() {
92        let (_, area) = setup_test_terminal(100, 100);
93        let area = Notification::new(Text::from("Hello, World!"), unbounded_channel().0).area(area);
94        assert_eq!(area, Rect::new(0, 0, 20, 3));
95    }
96
97    #[test]
98    fn test_notification_render() -> Result<()> {
99        let (mut terminal, _) = setup_test_terminal(20, 3);
100        let notification = Notification::new(Text::from("Hello, World!"), unbounded_channel().0);
101        let buffer = terminal
102            .draw(|frame| notification.render_popup(frame))?
103            .buffer
104            .clone();
105        let style = Style::reset().fg(Color::Rgb(3, 169, 244));
106        let expected = Buffer::with_lines([
107            Line::styled("┌Notification──────┐", style),
108            Line::from(vec![
109                Span::styled("│", style),
110                Span::raw("Hello, World!     "),
111                Span::styled("│", style),
112            ]),
113            Line::styled("└Press ESC to close┘", style),
114        ]);
115        assert_eq!(buffer, expected);
116        Ok(())
117    }
118
119    #[test]
120    fn test_notification_render_small_terminal() -> Result<()> {
121        let (mut terminal, _) = setup_test_terminal(18, 2);
122        let notification = Notification::new(Text::from("Hello, World!"), unbounded_channel().0);
123        let buffer = terminal
124            .draw(|frame| notification.render_popup(frame))?
125            .buffer
126            .clone();
127        let style = Style::reset().fg(Color::Rgb(3, 169, 244));
128        let expected = Buffer::with_lines([
129            Line::styled("┌Notification────┐", style),
130            Line::styled("└Press ESC to clo┘", style),
131        ]);
132        assert_eq!(buffer, expected);
133        Ok(())
134    }
135
136    #[test]
137    fn test_nofitication_render_multiline() -> Result<()> {
138        let (mut terminal, _) = setup_test_terminal(20, 5);
139        let notification = Notification::new(Text::from("Hello,\nWorld!"), unbounded_channel().0);
140        let buffer = terminal
141            .draw(|frame| notification.render_popup(frame))?
142            .buffer
143            .clone();
144        let style = Style::reset().fg(Color::Rgb(3, 169, 244));
145        let expected = Buffer::with_lines([
146            Line::styled("┌Notification──────┐", style),
147            Line::from(vec![
148                Span::styled("│", style),
149                Span::raw("Hello,            "),
150                Span::styled("│", style),
151            ]),
152            Line::from(vec![
153                Span::styled("│", style),
154                Span::raw("World!            "),
155                Span::styled("│", style),
156            ]),
157            Line::styled("└Press ESC to close┘", style),
158            Line::raw("                    "),
159        ]);
160        assert_eq!(buffer, expected);
161        Ok(())
162    }
163
164    #[test]
165    fn test_click_to_close() {
166        let (mut terminal, area) = setup_test_terminal(20, 3);
167        let (action_tx, mut action_rx) = unbounded_channel();
168        let mut notification = Notification::new(Text::from("Hello, World!"), action_tx.clone());
169        terminal
170            .draw(|frame| notification.render_popup(frame))
171            .unwrap();
172        notification.handle_mouse_event(
173            MouseEvent {
174                kind: MouseEventKind::Down(MouseButton::Left),
175                column: 0,
176                row: 0,
177                modifiers: KeyModifiers::empty(),
178            },
179            area,
180            action_tx,
181        );
182        assert_eq!(
183            action_rx.try_recv().unwrap(),
184            Action::Popup(PopupAction::Close)
185        );
186    }
187}