rsnaker/graphics/menus/
parameters_menu.rs

1use crate::controls::speed::Speed;
2use crate::graphics::menus::selectable_item::SelectableList;
3use crossterm::event::{Event, KeyCode, KeyEvent};
4use ratatui::{
5    buffer::Buffer,
6    layout::{Constraint, Direction as LayoutDirection, Layout, Rect},
7    style::{Color, Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, Borders, Paragraph, Widget},
10};
11use std::fmt::Display;
12
13///TODO finish it to have a graphical option setup alongside CLI !
14///
15/// Represents a single option in the multiple choice menu
16#[derive(Clone)]
17pub struct MenuOption<T: Clone> {
18    pub value: T,
19    pub label: String,
20    pub selected: bool,
21}
22
23impl<T: Clone> MenuOption<T> {
24    pub fn new(value: T, label: String) -> Self {
25        Self {
26            value,
27            label,
28            selected: false,
29        }
30    }
31
32    pub fn toggle(&mut self) {
33        self.selected = !self.selected;
34    }
35}
36
37impl<T: Clone> Display for MenuOption<T> {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        let prefix = if self.selected { "[X] " } else { "[ ] " };
40        write!(f, "{}{}", prefix, self.label)
41    }
42}
43
44/// A menu with multiple choice options and a save button
45pub struct MultipleChoiceMenu<T: Clone> {
46    pub title: String,
47    pub options: SelectableList<MenuOption<T>>,
48    pub has_save_button: bool,
49    pub is_on_save_button: bool,
50}
51
52impl<T: Clone> MultipleChoiceMenu<T> {
53    #[must_use]
54    pub fn new(title: String, options: Vec<MenuOption<T>>) -> Self {
55        let formatter = |opt: &MenuOption<T>| opt.to_string();
56
57        Self {
58            title: title.clone(),
59            options: SelectableList::new(title, options, formatter, Color::Green, Color::Black),
60            has_save_button: true,
61            is_on_save_button: false,
62        }
63    }
64
65    /// Move to the next option or save button
66    pub fn next(&mut self) {
67        if self.is_on_save_button {
68            self.is_on_save_button = false;
69            self.options.selected = 0;
70        } else if self.options.selected == self.options.items.len() - 1 && self.has_save_button {
71            self.is_on_save_button = true;
72        } else {
73            self.options.next();
74        }
75    }
76
77    /// Move to the previous option or save button
78    pub fn prev(&mut self) {
79        if self.is_on_save_button {
80            self.is_on_save_button = false;
81            self.options.selected = self.options.items.len() - 1;
82        } else if self.options.selected == 0 && self.has_save_button {
83            self.is_on_save_button = true;
84        } else {
85            self.options.prev();
86        }
87    }
88
89    /// Toggle the currently selected option
90    pub fn toggle_selected(&mut self) {
91        if !self.is_on_save_button {
92            let selected = self.options.selected;
93            self.options.items[selected].toggle();
94        }
95    }
96
97    /// Checks if the save button is selected
98    #[must_use]
99    pub fn is_save_selected(&self) -> bool {
100        self.is_on_save_button
101    }
102
103    /// Returns a list of all selected values
104    #[must_use]
105    pub fn get_selected_values(&self) -> Vec<T> {
106        self.options
107            .items
108            .iter()
109            .filter(|opt| opt.selected)
110            .map(|opt| opt.value.clone())
111            .collect()
112    }
113
114    /// Handle user input from the menu
115    pub fn handle_input(&mut self, event: &Event) -> bool {
116        if let Event::Key(KeyEvent { code, .. }) = event {
117            match code {
118                KeyCode::Down => self.next(),
119                KeyCode::Up => self.prev(),
120                KeyCode::Enter | KeyCode::Char(' ') => {
121                    if self.is_on_save_button {
122                        return true; // The Save button was pressed
123                    }
124                    self.toggle_selected();
125                }
126                _ => {}
127            }
128        }
129        false // Save not triggered
130    }
131}
132
133impl<T: Clone> Widget for &MultipleChoiceMenu<T> {
134    fn render(self, area: Rect, buf: &mut Buffer) {
135        // Create a layout with space for options and save button
136        let chunks = Layout::default()
137            .direction(LayoutDirection::Vertical)
138            .constraints([Constraint::Min(3), Constraint::Length(3)])
139            .split(area);
140
141        // Render the option list
142        (&self.options).render(chunks[0], buf);
143
144        // Render the save button
145        if self.has_save_button {
146            let save_style = if self.is_on_save_button {
147                Style::default()
148                    .fg(Color::Black)
149                    .bg(Color::Green)
150                    .add_modifier(Modifier::BOLD)
151            } else {
152                Style::default().fg(Color::Green)
153            };
154
155            let save_button = Paragraph::new(Line::from(Span::styled(" Save ", save_style)))
156                .block(Block::default().borders(Borders::ALL));
157
158            save_button.render(chunks[1], buf);
159        }
160    }
161}
162
163/// Runs a multiple-choice menu in the terminal
164///
165/// This function sets up a TUI menu where the user can select multiple options
166/// and confirm with a save button.
167///
168/// # Returns
169///
170/// A vector of selected option strings if saved, or an empty vector if quit without saving
171///
172/// # Errors
173///
174/// Returns an error if terminal operations fail, such as entering/leaving alternate screen,
175/// enabling/disabling raw mode, or other I/O operations.
176pub fn run_multiple_choice_menu() -> Result<Vec<Speed>, std::io::Error> {
177    // Setup terminal
178    crossterm::terminal::enable_raw_mode()?;
179    let mut stdout = std::io::stdout();
180    crossterm::execute!(
181        stdout,
182        crossterm::terminal::EnterAlternateScreen,
183        crossterm::event::EnableMouseCapture
184    )?;
185    let backend = ratatui::backend::CrosstermBackend::new(stdout);
186    let mut terminal = ratatui::Terminal::new(backend)?;
187
188    // Create menu options
189    let options: Vec<MenuOption<Speed>> = vec![
190        MenuOption::new(Speed::Slow, format!("{} Slow", Speed::Slow.symbol())),
191        MenuOption::new(Speed::Normal, format!("{} Normal", Speed::Normal.symbol())),
192        MenuOption::new(Speed::Fast, format!("{} Fast", Speed::Fast.symbol())),
193        MenuOption::new(
194            Speed::Tremendous,
195            format!("{} Tremendous", Speed::Tremendous.symbol()),
196        ),
197    ];
198
199    // Create a menu
200    let mut menu = MultipleChoiceMenu::new("Multiple Choice Menu".to_string(), options);
201
202    // Result to return
203    let mut selected_options = Vec::new();
204
205    // Main event loop
206    loop {
207        terminal.draw(|f| {
208            let size = f.area();
209            f.render_widget(&menu, size);
210        })?;
211
212        if let Ok(event) = crossterm::event::read() {
213            // Check for quit
214            if let Event::Key(KeyEvent {
215                code: KeyCode::Char('q'),
216                ..
217            }) = event
218            {
219                break;
220            }
221
222            // Handle menu input
223            let save_triggered = menu.handle_input(&event);
224
225            // If save was triggered, collect selected values and exit
226            if save_triggered {
227                selected_options = menu.get_selected_values();
228                break;
229            }
230        }
231    }
232
233    // Restore terminal
234    crossterm::terminal::disable_raw_mode()?;
235    crossterm::execute!(
236        terminal.backend_mut(),
237        crossterm::terminal::LeaveAlternateScreen,
238        crossterm::event::DisableMouseCapture
239    )?;
240    terminal.show_cursor()?;
241
242    Ok(selected_options)
243}