mecomp_tui/ui/widgets/popups/
notification.rs1use 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 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 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}