rsnaker/graphics/menus/retro_parameter_table/
generic_logic.rs

1use 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    //logic options
38    Quit,
39    //Genericity using a trait to allow using any reference to a type implementing ActionParameter
40    ApplyAndSave(&'a mut dyn ActionParameter),
41    //Goal for loading: use the CLI fn to load from File and chain it
42    //le u16 permet de keep track of the current preset loaded
43    //Genericity using a closure to allow using any fn to load from File
44    LoadPreset(
45        u16,
46        fn(u16) -> (Option<Vec<RowData>>, Option<Vec<FooterData>>),
47    ),
48}
49
50// Define a generic cell value type
51#[derive(Clone)]
52pub enum CellValue {
53    //either text only
54    Text(String),
55    //or a list of value
56    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                //Add 6 for the size of bracket added around value for option when displaying
98                // (hardcoded for performance rather than using format and then count chars)
99                max + DISPLAY_CELL_OUT_SPACE
100            }
101            //count max chars on the same line
102            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            //number of lines
113            CellValue::Text(v) => v.split('\n').count(),
114        }
115    }
116}
117
118// A row data type with only one option of changing the parameter
119// (no use case for a lateral switch, to only switch a cell).
120// Changes all the rows at once
121// Easy to adapt by having a selected cell if you need
122#[derive(Clone)]
123pub struct RowData {
124    // The column cells inside the row
125    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}
150//The lain struct for the parameter
151pub 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        // Calculate constraints
170        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                                            //Refresh all the GUI, so recreate the table with the new data and recalculate constraint
274                                            //Clean Rust way with self-overwriting
275                                            *self = Self::new(
276                                                new_rows,
277                                                &self.table_custom.headers,
278                                                new_footer,
279                                                Some(*index),
280                                            );
281                                        }
282                                    } //match
283                                } // unitary action
284                            } //key code
285                        } //keyCodes
286                    } //ActionInputs
287                } //Press event
288            } //event readable
289        } //loop
290    }
291
292    fn draw(&mut self, frame: &mut Frame) {
293        let rects = self.vertical_layout.split(frame.area());
294        //render the custom table (could have implemented the statefulWidget trait or Widget, but state is badly handled)
295        // Bad API design in ratatui!
296        self.table_custom
297            .update_table_color_background(self.selected_row);
298        self.table_custom.render(frame, rects[0]);
299        //Unfortunately, scrollbar does not yet implement render_stateful_widget_ref,
300        // so we have to use the old way with clone
301        //https://docs.rs/ratatui/latest/ratatui/widgets/trait.StatefulWidgetRef.html#implementors
302        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}