Skip to main content

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<'a> Notification<'a> {
22    #[must_use]
23    pub const fn new(line: Text<'a>, action_tx: UnboundedSender<Action>) -> Self {
24        Self { line, action_tx }
25    }
26}
27
28impl ComponentRender<Rect> for Notification<'_> {
29    fn render_border(&mut self, frame: &mut ratatui::Frame<'_>, area: Rect) -> Rect {
30        self.render_popup_border(frame, area)
31    }
32
33    fn render_content(&mut 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<'static> {
40        Line::raw("Notification")
41    }
42
43    fn instructions(&self) -> Line<'static> {
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 mut notification =
101            Notification::new(Text::from("Hello, World!"), unbounded_channel().0);
102        let buffer = terminal
103            .draw(|frame| notification.render_popup(frame))?
104            .buffer
105            .clone();
106        let style = Style::reset().fg(Color::Rgb(3, 169, 244));
107        let expected = Buffer::with_lines([
108            Line::styled("┌Notification──────┐", style),
109            Line::from(vec![
110                Span::styled("│", style),
111                Span::raw("Hello, World!     "),
112                Span::styled("│", style),
113            ]),
114            Line::styled("└Press ESC to close┘", style),
115        ]);
116        assert_eq!(buffer, expected);
117        Ok(())
118    }
119
120    #[test]
121    fn test_notification_render_small_terminal() -> Result<()> {
122        let (mut terminal, _) = setup_test_terminal(18, 2);
123        let mut notification =
124            Notification::new(Text::from("Hello, World!"), unbounded_channel().0);
125        let buffer = terminal
126            .draw(|frame| notification.render_popup(frame))?
127            .buffer
128            .clone();
129        let style = Style::reset().fg(Color::Rgb(3, 169, 244));
130        let expected = Buffer::with_lines([
131            Line::styled("┌Notification────┐", style),
132            Line::styled("└Press ESC to clo┘", style),
133        ]);
134        assert_eq!(buffer, expected);
135        Ok(())
136    }
137
138    #[test]
139    fn test_notification_render_multiline() -> Result<()> {
140        let (mut terminal, _) = setup_test_terminal(20, 5);
141        let mut notification =
142            Notification::new(Text::from("Hello,\nWorld!"), unbounded_channel().0);
143        let buffer = terminal
144            .draw(|frame| notification.render_popup(frame))?
145            .buffer
146            .clone();
147        let style = Style::reset().fg(Color::Rgb(3, 169, 244));
148        let expected = Buffer::with_lines([
149            Line::styled("┌Notification──────┐", style),
150            Line::from(vec![
151                Span::styled("│", style),
152                Span::raw("Hello,            "),
153                Span::styled("│", style),
154            ]),
155            Line::from(vec![
156                Span::styled("│", style),
157                Span::raw("World!            "),
158                Span::styled("│", style),
159            ]),
160            Line::styled("└Press ESC to close┘", style),
161            Line::raw("                    "),
162        ]);
163        assert_eq!(buffer, expected);
164        Ok(())
165    }
166
167    #[test]
168    fn test_click_to_close() {
169        let (mut terminal, area) = setup_test_terminal(20, 3);
170        let (action_tx, mut action_rx) = unbounded_channel();
171        let mut notification = Notification::new(Text::from("Hello, World!"), action_tx.clone());
172        terminal
173            .draw(|frame| notification.render_popup(frame))
174            .unwrap();
175        notification.handle_mouse_event(
176            MouseEvent {
177                kind: MouseEventKind::Down(MouseButton::Left),
178                column: 0,
179                row: 0,
180                modifiers: KeyModifiers::empty(),
181            },
182            area,
183            action_tx,
184        );
185        assert_eq!(
186            action_rx.try_recv().unwrap(),
187            Action::Popup(PopupAction::Close)
188        );
189    }
190}