wiki_tui/components/
message_popup.rs1use std::sync::Arc;
2
3use crossterm::event::{KeyCode, KeyEvent};
4use ratatui::{
5 layout::{Alignment, Rect},
6 style::{Style, Stylize},
7 text::Line,
8 widgets::{Block, Clear},
9};
10
11use crate::{
12 action::{Action, ActionPacket, ActionResult},
13 config::Theme,
14 ui::centered_rect,
15};
16
17use super::Component;
18
19pub struct MessagePopupComponent {
20 title: String,
21 content: String,
22 content_alignment: Alignment,
23
24 theme: Arc<Theme>,
25
26 confirmation: Option<ActionPacket>,
27}
28
29impl MessagePopupComponent {
30 pub fn new_raw(title: String, content: String, theme: Arc<Theme>) -> Self {
31 Self {
32 title,
33 content,
34 content_alignment: Alignment::Center,
35
36 theme,
37
38 confirmation: None,
39 }
40 }
41
42 pub fn new_error(error: String, theme: Arc<Theme>) -> Self {
43 const ERROR_MESSAGE: &str =
44 "An error occurred\nCheck the logs for further information\n\nError: {ERROR}";
45
46 Self {
47 title: "Error".to_string(),
48 content: ERROR_MESSAGE.replace("{ERROR}", &error),
49 content_alignment: Alignment::Left,
50
51 theme,
52
53 confirmation: None,
54 }
55 }
56
57 pub fn new_confirmation(
58 title: String,
59 content: String,
60 cb: ActionPacket,
61 theme: Arc<Theme>,
62 ) -> Self {
63 Self {
64 title,
65 content,
66 content_alignment: Alignment::Center,
67
68 theme,
69
70 confirmation: Some(cb),
71 }
72 }
73}
74
75impl Component for MessagePopupComponent {
76 fn handle_key_events(&mut self, key: KeyEvent) -> ActionResult {
77 match key.code {
78 KeyCode::Char('y') if self.confirmation.is_some() => self
79 .confirmation
80 .take()
81 .unwrap()
82 .action(Action::PopPopup)
83 .into(),
84 KeyCode::Char('n') if self.confirmation.is_some() => Action::PopPopup.into(),
85
86 KeyCode::Esc => Action::PopPopup.into(),
87 _ => ActionResult::Ignored,
88 }
89 }
90
91 fn render(&mut self, f: &mut crate::terminal::Frame<'_>, area: ratatui::prelude::Rect) {
92 let max_area = centered_rect(area, 50, 80);
93
94 let width = (max_area.width as usize).min(self.content.chars().count() + 2) as usize;
95 let wrapped_message = textwrap::wrap(&self.content, width);
96
97 let height = (max_area.height as usize).min(wrapped_message.len() + 2);
98
99 let area = Rect {
100 x: area.x + (area.width - width as u16) / 2,
101 y: area.y + (area.height - height as u16) / 2,
102 width: width as u16,
103 height: height as u16,
104 };
105
106 f.render_widget(Clear, area);
107 f.render_widget(
108 Block::default().style(Style::default().bg(self.theme.bg)),
109 area,
110 );
111
112 let title_line = if self.title == "Error" {
113 Line::from("Error".bold().red()).centered()
114 } else {
115 Line::from(self.title.clone()).centered()
116 };
117
118 let mut block = self.theme.default_block().title_top(title_line);
119
120 block = if self.confirmation.is_some() {
121 block
122 .title_bottom(Line::from(vec!["Y".bold(), "es".into()]).right_aligned())
123 .title_bottom(Line::from(vec!["N".bold(), "o".into()]).right_aligned())
124 } else {
125 block.title_bottom(Line::from("<ESC> Dismiss").right_aligned())
126 };
127
128 let message_widget = self
129 .theme
130 .default_paragraph(
131 wrapped_message
132 .iter()
133 .map(|x| Line::from(x.to_string()))
134 .collect::<Vec<Line>>(),
135 )
136 .alignment(self.content_alignment)
137 .block(block);
138 f.render_widget(message_widget, area);
139 }
140}