node_launchpad/components/popup/
connection_mode.rs

1// Copyright 2024 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 std::{default::Default, rc::Rc};
10
11use super::super::utils::centered_rect_fixed;
12
13use color_eyre::Result;
14use crossterm::event::{KeyCode, KeyEvent};
15use ratatui::{
16    layout::{Alignment, Constraint, Direction, Layout, Rect},
17    style::{Style, Stylize},
18    text::{Line, Span},
19    widgets::{
20        Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, Wrap,
21    },
22};
23use strum::IntoEnumIterator;
24
25use crate::{
26    action::{Action, OptionsActions},
27    components::Component,
28    connection_mode::ConnectionMode,
29    mode::{InputMode, Scene},
30    style::{
31        clear_area, COOL_GREY, DARK_GUNMETAL, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE,
32        VIVID_SKY_BLUE,
33    },
34};
35
36#[derive(Default)]
37enum ChangeConnectionModeState {
38    #[default]
39    Selection,
40    ConfirmChange,
41}
42
43#[derive(Default)]
44pub struct ChangeConnectionModePopUp {
45    active: bool,
46    state: ChangeConnectionModeState,
47    items: StatefulList<ConnectionModeItem>,
48    connection_mode_selection: ConnectionModeItem,
49    connection_mode_initial_state: ConnectionModeItem,
50    can_select: bool, // If the user can select the connection mode
51}
52
53impl ChangeConnectionModePopUp {
54    pub fn new(connection_mode: ConnectionMode) -> Result<Self> {
55        let mut selected_connection_mode: ConnectionModeItem = ConnectionModeItem::default();
56        let connection_modes_items: Vec<ConnectionModeItem> = ConnectionMode::iter()
57            .filter(|cm| cm != &ConnectionMode::HomeNetwork)
58            .map(|connection_mode_item| ConnectionModeItem {
59                connection_mode: connection_mode_item,
60                status: if connection_mode == connection_mode_item {
61                    selected_connection_mode = ConnectionModeItem {
62                        connection_mode: connection_mode_item,
63                        status: ConnectionModeStatus::Selected,
64                    };
65                    ConnectionModeStatus::Selected
66                } else {
67                    ConnectionModeStatus::NotSelected
68                },
69            })
70            .collect::<Vec<ConnectionModeItem>>();
71        debug!("Connection Mode in Config: {:?}", connection_mode);
72        let items = StatefulList::with_items(connection_modes_items);
73        Ok(Self {
74            active: false,
75            state: ChangeConnectionModeState::Selection,
76            items,
77            connection_mode_selection: selected_connection_mode.clone(),
78            connection_mode_initial_state: selected_connection_mode.clone(),
79            can_select: false,
80        })
81    }
82
83    // --- Interactions with the List of modes ---
84
85    /// Deselects all modes in the list of items
86    ///
87    fn deselect_all(&mut self) {
88        for item in &mut self.items.items {
89            item.status = ConnectionModeStatus::NotSelected;
90        }
91    }
92    /// Assigns to self.connection_mode_selection the selected connection mode in the list
93    ///
94    fn assign_connection_mode_selection(&mut self) {
95        self.deselect_all();
96        if let Some(i) = self.items.state.selected() {
97            self.items.items[i].status = ConnectionModeStatus::Selected;
98            self.connection_mode_selection = self.items.items[i].clone();
99        }
100    }
101    /// Highlights the connection mode that is currently selected in the list of items.
102    ///
103    fn select_connection_mode(&mut self) {
104        self.deselect_all();
105        for (index, item) in self.items.items.iter_mut().enumerate() {
106            if item.connection_mode == self.connection_mode_selection.connection_mode {
107                item.status = ConnectionModeStatus::Selected;
108                self.items.state.select(Some(index));
109                break;
110            }
111        }
112    }
113    /// Returns the highlighted connection mode in the list of items.
114    ///
115    fn return_selection(&mut self) -> ConnectionModeItem {
116        if let Some(i) = self.items.state.selected() {
117            return self.items.items[i].clone();
118        }
119        ConnectionModeItem::default()
120    }
121
122    // Draw functions
123
124    fn draw_selection_state(
125        &mut self,
126        f: &mut crate::tui::Frame<'_>,
127        layer_zero: Rect,
128        layer_one: Rc<[Rect]>,
129    ) -> Paragraph {
130        let pop_up_border: Paragraph = Paragraph::new("").block(
131            Block::default()
132                .borders(Borders::ALL)
133                .title(" Connection Mode ")
134                .bold()
135                .title_style(Style::new().fg(VIVID_SKY_BLUE))
136                .padding(Padding::uniform(2))
137                .border_style(Style::new().fg(VIVID_SKY_BLUE)),
138        );
139        clear_area(f, layer_zero);
140
141        let layer_two = Layout::new(
142            Direction::Vertical,
143            [
144                // for the table
145                Constraint::Length(10),
146                // gap
147                Constraint::Length(3),
148                // for the buttons
149                Constraint::Length(1),
150            ],
151        )
152        .split(layer_one[1]);
153
154        // Connection Mode selector
155        let items: Vec<ListItem> = self
156            .items
157            .items
158            .iter()
159            .enumerate()
160            .map(|(i, connection_mode_item)| {
161                connection_mode_item.to_list_item(i, layer_two[0].width as usize)
162            })
163            .collect();
164
165        let items = List::new(items)
166            .block(Block::default().padding(Padding::uniform(1)))
167            .highlight_style(Style::default().bg(INDIGO))
168            .highlight_spacing(HighlightSpacing::Always);
169
170        f.render_stateful_widget(items, layer_two[0], &mut self.items.state);
171
172        // Dash
173        let dash = Block::new()
174            .borders(Borders::BOTTOM)
175            .border_style(Style::new().fg(GHOST_WHITE));
176        f.render_widget(dash, layer_two[1]);
177
178        // Buttons
179        let buttons_layer =
180            Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
181                .split(layer_two[2]);
182
183        let button_no = Line::from(vec![Span::styled(
184            "Cancel [Esc]",
185            Style::default().fg(LIGHT_PERIWINKLE),
186        )]);
187
188        f.render_widget(
189            Paragraph::new(button_no)
190                .block(Block::default().padding(Padding::horizontal(2)))
191                .alignment(Alignment::Left),
192            buttons_layer[0],
193        );
194
195        let button_yes = Line::from(vec![
196            Span::styled(
197                "Select ",
198                if self.can_select {
199                    Style::default().fg(EUCALYPTUS)
200                } else {
201                    Style::default().fg(COOL_GREY)
202                },
203            ),
204            Span::styled("[Enter]", Style::default().fg(LIGHT_PERIWINKLE).bold()),
205        ])
206        .alignment(Alignment::Right);
207
208        f.render_widget(
209            Paragraph::new(button_yes)
210                .block(Block::default().padding(Padding::horizontal(2)))
211                .alignment(Alignment::Right),
212            buttons_layer[1],
213        );
214
215        pop_up_border
216    }
217
218    fn draw_confirm_change(
219        &mut self,
220        f: &mut crate::tui::Frame<'_>,
221        layer_zero: Rect,
222        layer_one: Rc<[Rect]>,
223    ) -> Paragraph {
224        // layer zero
225        let pop_up_border = Paragraph::new("").block(
226            Block::default()
227                .borders(Borders::ALL)
228                .title(" Confirm & Reset ")
229                .bold()
230                .title_style(Style::new().fg(VIVID_SKY_BLUE))
231                .padding(Padding::uniform(2))
232                .border_style(Style::new().fg(VIVID_SKY_BLUE)),
233        );
234        clear_area(f, layer_zero);
235
236        // split into 3 parts, paragraph, dash, buttons
237        let layer_two = Layout::new(
238            Direction::Vertical,
239            [
240                // for the text
241                Constraint::Length(9),
242                // gap
243                Constraint::Length(3),
244                // for the buttons
245                Constraint::Length(1),
246            ],
247        )
248        .split(layer_one[1]);
249
250        let paragraph_text = Paragraph::new(vec![
251            Line::from(Span::styled("\n\n", Style::default())),
252            Line::from(Span::styled("\n\n", Style::default())),
253            Line::from(vec![
254                Span::styled(
255                    "Changing connection mode will ",
256                    Style::default().fg(LIGHT_PERIWINKLE),
257                ),
258                Span::styled("reset all nodes.", Style::default().fg(GHOST_WHITE)),
259            ]),
260            Line::from(Span::styled("\n\n", Style::default())),
261            Line::from(Span::styled("\n\n", Style::default())),
262            Line::from(Span::styled("\n\n", Style::default())),
263            Line::from(vec![
264                Span::styled("You’ll need to ", Style::default().fg(LIGHT_PERIWINKLE)),
265                Span::styled("Add", Style::default().fg(GHOST_WHITE)),
266                Span::styled(" and ", Style::default().fg(LIGHT_PERIWINKLE)),
267                Span::styled("Start", Style::default().fg(GHOST_WHITE)),
268                Span::styled(
269                    " them again afterwards. Are you sure you want to continue?",
270                    Style::default().fg(LIGHT_PERIWINKLE),
271                ),
272            ]),
273        ])
274        .alignment(Alignment::Left)
275        .wrap(Wrap { trim: true })
276        .block(Block::default().padding(Padding::horizontal(2)));
277
278        f.render_widget(paragraph_text, layer_two[0]);
279
280        let dash = Block::new()
281            .borders(Borders::BOTTOM)
282            .border_style(Style::new().fg(GHOST_WHITE));
283        f.render_widget(dash, layer_two[1]);
284
285        let buttons_layer =
286            Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
287                .split(layer_two[2]);
288
289        let button_no = Line::from(vec![Span::styled(
290            "  Cancel [Esc]",
291            Style::default().fg(LIGHT_PERIWINKLE),
292        )]);
293        let button_yes_style = if self.can_select {
294            Style::default().fg(EUCALYPTUS)
295        } else {
296            Style::default().fg(LIGHT_PERIWINKLE)
297        };
298        f.render_widget(button_no, buttons_layer[0]);
299
300        let button_yes = Line::from(vec![
301            Span::styled("Yes, Change Mode ", button_yes_style),
302            Span::styled("[Enter]", Style::default().fg(GHOST_WHITE)),
303        ]);
304        f.render_widget(button_yes, buttons_layer[1]);
305
306        pop_up_border
307    }
308}
309
310impl Component for ChangeConnectionModePopUp {
311    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
312        if !self.active {
313            return Ok(vec![]);
314        }
315        let send_back: Vec<Action> = match &self.state {
316            ChangeConnectionModeState::Selection => match key.code {
317                KeyCode::Enter => {
318                    let connection_mode = self.return_selection();
319                    self.connection_mode_initial_state = self.connection_mode_selection.clone();
320                    if connection_mode.connection_mode == ConnectionMode::CustomPorts {
321                        vec![
322                            Action::OptionsActions(OptionsActions::UpdateConnectionMode(
323                                ConnectionMode::CustomPorts,
324                            )),
325                            Action::SwitchScene(Scene::ChangePortsPopUp {
326                                connection_mode_old_value: Some(
327                                    self.connection_mode_initial_state.connection_mode,
328                                ),
329                            }),
330                        ]
331                    } else {
332                        self.state = ChangeConnectionModeState::ConfirmChange;
333                        vec![]
334                    }
335                }
336                KeyCode::Esc => {
337                    debug!("Got Esc, switching to Options");
338                    vec![Action::SwitchScene(Scene::Options)]
339                }
340                KeyCode::Up => {
341                    if self.items.items.len() > 1 {
342                        self.items.previous();
343                        let connection_mode = self.return_selection();
344                        self.can_select = connection_mode.connection_mode
345                            != self.connection_mode_selection.connection_mode;
346                    }
347                    vec![]
348                }
349                KeyCode::Down => {
350                    if self.items.items.len() > 1 {
351                        self.items.next();
352                        let connection_mode = self.return_selection();
353                        self.can_select = connection_mode.connection_mode
354                            != self.connection_mode_selection.connection_mode;
355                    }
356                    vec![]
357                }
358                _ => {
359                    vec![]
360                }
361            },
362            ChangeConnectionModeState::ConfirmChange => match key.code {
363                KeyCode::Enter => {
364                    self.state = ChangeConnectionModeState::Selection;
365                    // We allow action if we have more than one connection mode and the action is not
366                    // over the connection mode already selected
367                    let connection_mode = self.return_selection();
368                    if connection_mode.connection_mode
369                        != self.connection_mode_selection.connection_mode
370                    {
371                        debug!(
372                                        "Got Enter and there's a new selection, storing value and switching to Options"
373                                    );
374                        debug!("Connection Mode selected: {:?}", connection_mode);
375                        self.connection_mode_initial_state = self.connection_mode_selection.clone();
376                        self.assign_connection_mode_selection();
377                        vec![
378                            Action::StoreConnectionMode(
379                                self.connection_mode_selection.connection_mode,
380                            ),
381                            Action::OptionsActions(OptionsActions::UpdateConnectionMode(
382                                connection_mode.clone().connection_mode,
383                            )),
384                            Action::SwitchScene(Scene::Status),
385                        ]
386                    } else {
387                        debug!("Got Enter, but no new selection. We should not do anything");
388                        vec![Action::SwitchScene(Scene::ChangeConnectionModePopUp)]
389                    }
390                }
391                KeyCode::Esc => {
392                    self.state = ChangeConnectionModeState::Selection;
393                    vec![Action::SwitchScene(Scene::Options)]
394                }
395                _ => {
396                    vec![]
397                }
398            },
399        };
400        Ok(send_back)
401    }
402
403    fn update(&mut self, action: Action) -> Result<Option<Action>> {
404        let send_back = match action {
405            Action::SwitchScene(scene) => match scene {
406                Scene::ChangeConnectionModePopUp => {
407                    self.active = true;
408                    self.can_select = false;
409                    self.select_connection_mode();
410                    Some(Action::SwitchInputMode(InputMode::Entry))
411                }
412                _ => {
413                    self.active = false;
414                    None
415                }
416            },
417            // Useful when the user has selected a connection mode but didn't confirm it
418            Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => {
419                self.connection_mode_selection.connection_mode = connection_mode;
420                self.select_connection_mode();
421                None
422            }
423            _ => None,
424        };
425        Ok(send_back)
426    }
427
428    fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
429        if !self.active {
430            return Ok(());
431        }
432
433        let layer_zero = centered_rect_fixed(52, 15, area);
434
435        let layer_one = Layout::new(
436            Direction::Vertical,
437            [
438                // Padding from title to the table
439                Constraint::Length(1),
440                // Table
441                Constraint::Min(1),
442                // for the pop_up_border
443                Constraint::Length(1),
444            ],
445        )
446        .split(layer_zero);
447
448        let pop_up_border: Paragraph = match self.state {
449            ChangeConnectionModeState::Selection => {
450                self.draw_selection_state(f, layer_zero, layer_one)
451            }
452            ChangeConnectionModeState::ConfirmChange => {
453                self.draw_confirm_change(f, layer_zero, layer_one)
454            }
455        };
456
457        f.render_widget(pop_up_border, layer_zero);
458
459        Ok(())
460    }
461}
462
463#[derive(Default)]
464struct StatefulList<T> {
465    state: ListState,
466    items: Vec<T>,
467    last_selected: Option<usize>,
468}
469
470impl<T> StatefulList<T> {
471    fn with_items(items: Vec<T>) -> Self {
472        StatefulList {
473            state: ListState::default(),
474            items,
475            last_selected: None,
476        }
477    }
478
479    fn next(&mut self) {
480        let i = match self.state.selected() {
481            Some(i) => {
482                if i >= self.items.len() - 1 {
483                    0
484                } else {
485                    i + 1
486                }
487            }
488            None => self.last_selected.unwrap_or(0),
489        };
490        self.state.select(Some(i));
491    }
492
493    fn previous(&mut self) {
494        let i = match self.state.selected() {
495            Some(i) => {
496                if i == 0 {
497                    self.items.len() - 1
498                } else {
499                    i - 1
500                }
501            }
502            None => self.last_selected.unwrap_or(0),
503        };
504        self.state.select(Some(i));
505    }
506}
507
508#[derive(Default, Debug, Copy, Clone)]
509enum ConnectionModeStatus {
510    Selected,
511    #[default]
512    NotSelected,
513}
514
515#[derive(Default, Debug, Clone)]
516pub struct ConnectionModeItem {
517    connection_mode: ConnectionMode,
518    status: ConnectionModeStatus,
519}
520
521impl ConnectionModeItem {
522    fn to_list_item(&self, _index: usize, _width: usize) -> ListItem {
523        let line = match self.status {
524            ConnectionModeStatus::NotSelected => Line::from(vec![
525                Span::raw("   "),
526                Span::styled(
527                    self.connection_mode.to_string(),
528                    Style::default().fg(VIVID_SKY_BLUE),
529                ),
530            ]),
531            ConnectionModeStatus::Selected => Line::from(vec![
532                Span::styled(" ►", Style::default().fg(EUCALYPTUS)),
533                Span::raw(" "),
534                Span::styled(
535                    self.connection_mode.to_string(),
536                    Style::default().fg(VIVID_SKY_BLUE),
537                ),
538            ]),
539        };
540
541        ListItem::new(line).style(Style::default().bg(DARK_GUNMETAL))
542    }
543}