rsnaker/graphics/menus/retro_parameter_table/
generic_logic.rs1use crate::graphics::menus::retro_parameter_table::generic_style::{
2 get_formated_footer, ScrollBarCustomRetroStyle, TableCustomRetroStyle, DISPLAY_CELL_OUT_SPACE,
3};
4use crate::graphics::menus::utils_layout::{
5 calculate_max_column_widths, calculate_sum_inner_row_heights, constraint_length_from_widths,
6};
7use crossterm::event;
8use crossterm::event::{Event, KeyCode, KeyEventKind};
9use ratatui::widgets::FrameExt;
10use ratatui::{
11 layout::{Constraint, Layout}, widgets::Paragraph,
12 DefaultTerminal,
13 Frame,
14};
15use unicode_segmentation::UnicodeSegmentation;
16
17pub trait ActionParameter {
18 fn apply_and_save(&mut self, rows: &[RowData], current_preset: Option<u16>);
19}
20#[derive(Clone)]
21pub struct FooterData {
22 pub symbol: String,
23 pub text: String,
24 pub value: Option<u16>,
25}
26
27pub struct ActionInputs<'a> {
28 pub key: Vec<KeyCode>,
29 pub action: Vec<TableParameterAction<'a>>,
30}
31#[allow(clippy::type_complexity)]
32pub enum TableParameterAction<'a> {
33 NextValue,
34 PreviousValue,
35 NextRow,
36 PreviousRow,
37 Quit,
39 ApplyAndSave(&'a mut dyn ActionParameter),
41 LoadPreset(
45 u16,
46 fn(u16) -> (Option<Vec<RowData>>, Option<Vec<FooterData>>),
47 ),
48}
49
50#[derive(Clone)]
52pub enum CellValue {
53 Text(String),
55 Options {
57 option_name: String,
58 values: Vec<String>,
59 index: usize,
60 index_ini: usize,
61 },
62}
63impl CellValue {
64 #[must_use]
65 pub fn new(text: String) -> Self {
66 Self::Text(text)
67 }
68 #[must_use]
69 pub fn new_with_options(option_name: String, values: Vec<String>, index: usize) -> Self {
70 Self::Options {
71 option_name,
72 values,
73 index,
74 index_ini: index,
75 }
76 }
77 fn next_value(&mut self) {
78 if let CellValue::Options { values, index, .. } = self {
79 *index = (*index + 1) % values.len();
80 }
81 }
82
83 fn previous_value(&mut self) {
84 if let CellValue::Options { values, index, .. } = self {
85 let max = values.len();
86 *index = (*index + max.saturating_sub(1)) % max;
87 }
88 }
89 fn width(&self) -> usize {
90 match self {
91 CellValue::Options { values, .. } => {
92 let max = values
93 .iter()
94 .map(|v| v.as_str().graphemes(true).count())
95 .max()
96 .unwrap_or(0);
97 max + DISPLAY_CELL_OUT_SPACE
100 }
101 CellValue::Text(v) => v.split('\n').map(|s| s.chars().count()).max().unwrap_or(0),
103 }
104 }
105 fn height(&self) -> usize {
106 match self {
107 CellValue::Options { values, .. } => values
108 .iter()
109 .map(|v| v.split('\n').count())
110 .max()
111 .unwrap_or(0),
112 CellValue::Text(v) => v.split('\n').count(),
114 }
115 }
116}
117
118#[derive(Clone)]
123pub struct RowData {
124 pub cells: Vec<CellValue>,
126}
127
128impl RowData {
129 #[must_use]
130 pub fn new(cells: Vec<CellValue>) -> Self {
131 Self { cells }
132 }
133 pub(crate) fn get_cell_widths(&self) -> Vec<usize> {
134 self.cells.iter().map(CellValue::width).collect()
135 }
136 pub(crate) fn get_cell_heights(&self) -> Vec<usize> {
137 self.cells.iter().map(CellValue::height).collect()
138 }
139 fn next_cell_value(&mut self) {
140 for c in &mut self.cells {
141 c.next_value();
142 }
143 }
144 fn previous_cell_value(&mut self) {
145 for c in &mut self.cells {
146 c.previous_value();
147 }
148 }
149}
150pub struct GenericMenu<'a> {
152 table_custom: TableCustomRetroStyle<'a>,
153 scrollbar: ScrollBarCustomRetroStyle<'a>,
154 selected_row: usize,
155 info_footer: Paragraph<'a>,
156 info_footer_data: Vec<FooterData>,
157 vertical_layout: Layout,
158 current_preset: Option<u16>,
159}
160
161impl<'a> GenericMenu<'a> {
162 #[must_use]
163 pub fn new(
164 rows: Vec<RowData>,
165 headers: &[String],
166 info_footer: Vec<FooterData>,
167 current_preset: Option<u16>,
168 ) -> Self {
169 let column_widths = calculate_max_column_widths(&rows, headers);
171 let constraints = constraint_length_from_widths(&column_widths);
172 let row_sum_height = calculate_sum_inner_row_heights(&rows);
173 let vertical_layout = Layout::vertical([
174 Constraint::Min(1),
175 Constraint::Length(
176 u16::try_from(headers.len()).expect("too much headers to store :p "),
177 ),
178 ]);
179 Self {
180 table_custom: TableCustomRetroStyle::new(headers, rows, 0, constraints),
181 scrollbar: ScrollBarCustomRetroStyle::new(row_sum_height),
182 selected_row: 0,
183 info_footer: get_formated_footer(&info_footer),
184 info_footer_data: info_footer,
185 vertical_layout,
186 current_preset,
187 }
188 }
189
190 pub fn next_row(&mut self) {
191 let i = match self.table_custom.state.selected() {
192 Some(i) => (i + 1) % self.table_custom.rows.len(),
193 None => 0,
194 };
195 self.table_custom.state.select(Some(i));
196 self.selected_row = i;
197 self.scrollbar.scroll_state =
198 self.scrollbar
199 .scroll_state
200 .position(calculate_sum_inner_row_heights(
201 &self.table_custom.rows[..i],
202 ));
203 }
204
205 pub fn previous_row(&mut self) {
206 let i = match self.table_custom.state.selected() {
207 Some(i) => (i + self.table_custom.rows.len() - 1) % self.table_custom.rows.len(),
208 None => 0,
209 };
210 self.table_custom.state.select(Some(i));
211 self.selected_row = i;
212 self.scrollbar.scroll_state =
213 self.scrollbar
214 .scroll_state
215 .position(calculate_sum_inner_row_heights(
216 &self.table_custom.rows[..i],
217 ));
218 }
219
220 pub fn next_parameter_value(&mut self) {
221 if let Some(row) = self.table_custom.rows.get_mut(self.selected_row) {
222 row.next_cell_value();
223 }
224 }
225
226 pub fn previous_parameter_value(&mut self) {
227 if let Some(row) = self.table_custom.rows.get_mut(self.selected_row) {
228 row.previous_cell_value();
229 }
230 }
231
232 pub fn run(
233 &mut self,
234 mut actions_inputs: Vec<ActionInputs<'a>>,
235 terminal: &mut DefaultTerminal,
236 ) {
237 loop {
238 terminal.draw(|frame| self.draw(frame)).unwrap();
239 if let Event::Key(key) = event::read().unwrap() {
240 if key.kind == KeyEventKind::Press {
241 for action_input in &mut actions_inputs {
242 for key_code in action_input.key.clone() {
243 if key_code == key.code {
244 for unitary_tp_action in &mut action_input.action {
245 match unitary_tp_action {
246 TableParameterAction::NextValue => {
247 self.next_parameter_value();
248 }
249 TableParameterAction::PreviousValue => {
250 self.previous_parameter_value();
251 }
252 TableParameterAction::NextRow => {
253 self.next_row();
254 }
255 TableParameterAction::PreviousRow => {
256 self.previous_row();
257 }
258 TableParameterAction::ApplyAndSave(action) => {
259 action.apply_and_save(
260 &self.table_custom.rows,
261 self.current_preset,
262 );
263 }
264 TableParameterAction::Quit => {
265 return;
266 }
267 TableParameterAction::LoadPreset(index, loader) => {
268 let (new_rows, new_footer) = loader(*index);
269 let new_rows =
270 new_rows.unwrap_or(self.table_custom.rows.clone());
271 let new_footer =
272 new_footer.unwrap_or(self.info_footer_data.clone());
273 *self = Self::new(
276 new_rows,
277 &self.table_custom.headers,
278 new_footer,
279 Some(*index),
280 );
281 }
282 } } } } } } } } }
291
292 fn draw(&mut self, frame: &mut Frame) {
293 let rects = self.vertical_layout.split(frame.area());
294 self.table_custom
297 .update_table_color_background(self.selected_row);
298 self.table_custom.render(frame, rects[0]);
299 frame.render_stateful_widget(
303 self.scrollbar.widget.clone(),
304 rects[0].inner(self.scrollbar.margin),
305 &mut self.scrollbar.scroll_state,
306 );
307 frame.render_widget_ref(&self.info_footer, rects[1]);
308 }
309}
310
311#[must_use]
312pub fn get_default_action_input<'a>() -> Vec<ActionInputs<'a>> {
313 vec![
314 ActionInputs {
315 key: vec![KeyCode::Down],
316 action: vec![TableParameterAction::NextRow],
317 },
318 ActionInputs {
319 key: vec![KeyCode::Up],
320 action: vec![TableParameterAction::PreviousRow],
321 },
322 ActionInputs {
323 key: vec![KeyCode::Right],
324 action: vec![TableParameterAction::NextValue],
325 },
326 ActionInputs {
327 key: vec![KeyCode::Left],
328 action: vec![TableParameterAction::PreviousValue],
329 },
330 ActionInputs {
331 key: vec![KeyCode::Esc],
332 action: vec![TableParameterAction::Quit],
333 },
334 ]
335}