node_launchpad/error.rs
1// Copyright 2025 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use crate::{
10 components::utils::centered_rect_fixed,
11 style::{clear_area, EUCALYPTUS, GHOST_WHITE, RED},
12 tui::Frame,
13};
14use crossterm::event::{KeyCode, KeyEvent};
15use ratatui::{
16 layout::{Alignment, Constraint, Direction, Layout, Rect},
17 style::{Style, Stylize},
18 text::{Line, Span},
19 widgets::{Block, Borders, Padding, Paragraph, Wrap},
20};
21
22/// Error popup is a popup that is used to display error messages to the user.
23///
24/// It accepts a title, a message and an error message.
25/// Handles key events to hide the popup (Enter and Esc keys).
26///
27/// How to use:
28/// 1. Create a new ErrorPopup member in your component.
29/// 2. Show the error popup by calling the `show` method.
30/// 3. Hide the error popup by calling the `hide` method.
31/// 4. Check if the error popup is visible by calling the `is_visible` method.
32/// 5. Draw the error popup by calling the `draw_error` method in your `draw` function.
33/// 6. Handle the input for the error popup by calling the `handle_input` method.
34///
35/// Example:
36/// ```ignore
37/// use crate::error::ErrorPopup;
38///
39/// pub struct MyComponent {
40/// error_popup: Option<ErrorPopup>,
41/// }
42///
43/// impl MyComponent {
44/// pub fn new() -> Self {
45/// Self {
46/// error_popup: None,
47/// }
48/// }
49/// }
50///
51/// impl Component for MyComponent {
52/// fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
53/// if let Some(error_popup) = &mut self.error_popup {
54/// if error_popup.is_visible() {
55/// error_popup.handle_input(key);
56/// return Ok(vec![Action::SwitchInputMode(InputMode::Navigation)]);
57/// }
58/// }
59/// // ... Your keys being handled here ...
60/// }
61/// fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
62/// // ... Your drawing code here ...
63/// // Be sure to include the background elements here
64/// if let Some(error_popup) = &self.error_popup {
65/// if error_popup.is_visible() {
66/// error_popup.draw_error(f, area);
67/// return Ok(());
68/// }
69/// }
70/// // Be sure to include your popups here
71/// // ... Your drawing code here ...
72/// }
73/// }
74/// ```
75///
76/// How to trigger the error
77///
78/// ```ignore
79/// self.error_popup = Some(ErrorPopup::new(
80/// "Error".to_string(),
81/// "This is a test error message".to_string(),
82/// "raw message".to_string(),
83/// ));
84/// if let Some(error_popup) = &mut self.error_popup {
85/// error_popup.show();
86/// }
87/// ```
88
89#[derive(Clone)]
90pub struct ErrorPopup {
91 visible: bool,
92 title: String,
93 message: String,
94 error_message: String,
95}
96
97impl ErrorPopup {
98 pub fn new(title: String, message: String, error_message: String) -> Self {
99 Self {
100 visible: false,
101 title,
102 message,
103 error_message,
104 }
105 }
106
107 pub fn draw_error(&self, f: &mut Frame, area: Rect) {
108 if !self.visible {
109 return;
110 }
111
112 let layer_zero = centered_rect_fixed(52, 15, area);
113
114 let layer_one = Layout::new(
115 Direction::Vertical,
116 [
117 // for the pop_up_border + padding
118 Constraint::Length(2),
119 // for the text
120 Constraint::Min(1),
121 // for the pop_up_border
122 Constraint::Length(1),
123 ],
124 )
125 .split(layer_zero);
126
127 let pop_up_border = Paragraph::new("").block(
128 Block::default()
129 .borders(Borders::ALL)
130 .title(format!(" {} ", self.title))
131 .bold()
132 .title_style(Style::new().fg(RED))
133 .padding(Padding::uniform(2))
134 .border_style(Style::new().fg(RED)),
135 );
136 clear_area(f, layer_zero);
137
138 let layer_two = Layout::new(
139 Direction::Vertical,
140 [
141 // for the message
142 Constraint::Length(4),
143 // for the error_message
144 Constraint::Length(7),
145 // gap
146 Constraint::Length(1),
147 // for the buttons
148 Constraint::Length(1),
149 ],
150 )
151 .split(layer_one[1]);
152
153 let prompt = Paragraph::new(self.message.clone())
154 .block(
155 Block::default()
156 .padding(Padding::horizontal(2))
157 .padding(Padding::vertical(1)),
158 )
159 .alignment(Alignment::Center)
160 .wrap(Wrap { trim: true });
161
162 f.render_widget(prompt.fg(GHOST_WHITE), layer_two[0]);
163
164 let text = Paragraph::new(self.error_message.clone())
165 .block(Block::default().padding(Padding::horizontal(2)))
166 .alignment(Alignment::Center)
167 .wrap(Wrap { trim: true });
168 f.render_widget(text.fg(GHOST_WHITE), layer_two[1]);
169
170 let dash = Block::new()
171 .borders(Borders::BOTTOM)
172 .border_style(Style::new().fg(GHOST_WHITE));
173 f.render_widget(dash, layer_two[2]);
174
175 let buttons_layer =
176 Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
177 .split(layer_two[3]);
178 let button_ok = Line::from(vec![
179 Span::styled("OK ", Style::default().fg(EUCALYPTUS)),
180 Span::styled("[Enter] ", Style::default().fg(GHOST_WHITE)),
181 ])
182 .alignment(Alignment::Right);
183
184 f.render_widget(button_ok, buttons_layer[1]);
185
186 // We render now so the borders are on top of the other widgets
187 f.render_widget(pop_up_border, layer_zero);
188 }
189
190 pub fn handle_input(&mut self, key: KeyEvent) -> bool {
191 if self.visible && (key.code == KeyCode::Esc || key.code == KeyCode::Enter) {
192 self.hide();
193 true
194 } else {
195 false
196 }
197 }
198
199 pub fn show(&mut self) {
200 debug!("Showing error popup");
201 self.visible = true;
202 }
203
204 pub fn hide(&mut self) {
205 debug!("Hiding error popup");
206 self.visible = false;
207 }
208
209 pub fn is_visible(&self) -> bool {
210 self.visible
211 }
212}