rsnaker/graphics/menus/
parameters_menu.rs1use 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#[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
44pub 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 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 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 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 #[must_use]
99 pub fn is_save_selected(&self) -> bool {
100 self.is_on_save_button
101 }
102
103 #[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 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; }
124 self.toggle_selected();
125 }
126 _ => {}
127 }
128 }
129 false }
131}
132
133impl<T: Clone> Widget for &MultipleChoiceMenu<T> {
134 fn render(self, area: Rect, buf: &mut Buffer) {
135 let chunks = Layout::default()
137 .direction(LayoutDirection::Vertical)
138 .constraints([Constraint::Min(3), Constraint::Length(3)])
139 .split(area);
140
141 (&self.options).render(chunks[0], buf);
143
144 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
163pub fn run_multiple_choice_menu() -> Result<Vec<Speed>, std::io::Error> {
177 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 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 let mut menu = MultipleChoiceMenu::new("Multiple Choice Menu".to_string(), options);
201
202 let mut selected_options = Vec::new();
204
205 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 if let Event::Key(KeyEvent {
215 code: KeyCode::Char('q'),
216 ..
217 }) = event
218 {
219 break;
220 }
221
222 let save_triggered = menu.handle_input(&event);
224
225 if save_triggered {
227 selected_options = menu.get_selected_values();
228 break;
229 }
230 }
231 }
232
233 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}