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<'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 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 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}