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}