Skip to main content

rsnaker/graphics/menus/retro_parameter_table/
generic_logic.rs

1use crate::controls::playing_input::has_no_modifiers;
2use crate::graphics::menus::retro_parameter_table::generic_style::{
3    DISPLAY_CELL_OUT_SPACE, ScrollBarCustomRetroStyle, TableCustomRetroStyle, get_formated_footer,
4};
5use crate::graphics::menus::utils_layout::{
6    calculate_max_column_widths, calculate_sum_inner_row_heights, constraint_length_from_widths,
7};
8use crossterm::event;
9use crossterm::event::{Event, KeyCode, KeyEventKind};
10use ratatui::widgets::FrameExt;
11use ratatui::{
12    DefaultTerminal, Frame,
13    layout::{Constraint, Layout},
14    widgets::Paragraph,
15};
16use unicode_segmentation::UnicodeSegmentation;
17
18pub trait ActionParameter {
19    fn apply_and_save(&mut self, rows: &[RowData], current_preset: Option<u16>);
20}
21#[derive(Clone)]
22pub struct FooterData {
23    pub symbol: String,
24    pub text: String,
25    pub value: Option<u16>,
26}
27
28pub struct ActionInputs<'a> {
29    pub key: Vec<KeyCode>,
30    pub action: Vec<TableParameterAction<'a>>,
31}
32#[allow(clippy::type_complexity)]
33pub enum TableParameterAction<'a> {
34    NextValue,
35    PreviousValue,
36    NextRow,
37    PreviousRow,
38    //logic options
39    Quit,
40    //Genericity using a trait to allow using any reference to a type implementing ActionParameter
41    ApplyAndSave(&'a mut dyn ActionParameter),
42    //Goal for loading: use the CLI fn to load from File and chain it
43    //le u16 permet de keep track of the current preset loaded
44    //Genericity using a closure to allow using any fn to load from File
45    LoadPreset(
46        u16,
47        fn(u16) -> (Option<Vec<RowData>>, Option<Vec<FooterData>>),
48    ),
49    Randomize(fn(Option<u16>, &[RowData]) -> Vec<RowData>),
50}
51
52// Define a generic cell value type
53#[derive(Clone)]
54pub enum CellValue {
55    //either text only
56    Text(String),
57    //or a list of value
58    Options {
59        option_name: String,
60        values: Vec<String>,
61        index: usize,
62        index_ini: usize,
63    },
64}
65impl CellValue {
66    #[must_use]
67    pub fn new(text: String) -> Self {
68        Self::Text(text)
69    }
70    #[must_use]
71    pub fn new_with_options(option_name: String, values: Vec<String>, index: usize) -> Self {
72        Self::Options {
73            option_name,
74            values,
75            index,
76            index_ini: index,
77        }
78    }
79    fn next_value(&mut self) {
80        if let CellValue::Options { values, index, .. } = self {
81            *index = (*index + 1) % values.len();
82        }
83    }
84
85    fn previous_value(&mut self) {
86        if let CellValue::Options { values, index, .. } = self {
87            let max = values.len();
88            *index = (*index + max.saturating_sub(1)) % max;
89        }
90    }
91    fn width(&self) -> usize {
92        match self {
93            CellValue::Options { values, .. } => {
94                let max = values
95                    .iter()
96                    .map(|v| v.as_str().graphemes(true).count())
97                    .max()
98                    .unwrap_or(0);
99                //Add 6 for the size of bracket added around value for option when displaying
100                // (hardcoded for performance rather than using format and then count chars)
101                max + DISPLAY_CELL_OUT_SPACE
102            }
103            //count max chars on the same line
104            CellValue::Text(v) => v.split('\n').map(|s| s.chars().count()).max().unwrap_or(0),
105        }
106    }
107    fn height(&self) -> usize {
108        match self {
109            CellValue::Options { values, .. } => values
110                .iter()
111                .map(|v| v.split('\n').count())
112                .max()
113                .unwrap_or(0),
114            //number of lines
115            CellValue::Text(v) => v.split('\n').count(),
116        }
117    }
118}
119
120// A row data type with only one option of changing the parameter
121// (no use case for a lateral switch, to only switch a cell).
122// Changes all the rows at once
123// Easy to adapt by having a selected cell if you need
124#[derive(Clone)]
125pub struct RowData {
126    // The column cells inside the row
127    pub cells: Vec<CellValue>,
128}
129
130impl RowData {
131    #[must_use]
132    pub fn new(cells: Vec<CellValue>) -> Self {
133        Self { cells }
134    }
135    pub(crate) fn get_cell_widths(&self) -> Vec<usize> {
136        self.cells.iter().map(CellValue::width).collect()
137    }
138    pub(crate) fn get_cell_heights(&self) -> Vec<usize> {
139        self.cells.iter().map(CellValue::height).collect()
140    }
141    fn next_cell_value(&mut self) {
142        for c in &mut self.cells {
143            c.next_value();
144        }
145    }
146    fn previous_cell_value(&mut self) {
147        for c in &mut self.cells {
148            c.previous_value();
149        }
150    }
151}
152//The lain struct for the parameter
153pub struct GenericMenu<'a> {
154    table_custom: TableCustomRetroStyle<'a>,
155    scrollbar: ScrollBarCustomRetroStyle<'a>,
156    selected_row: usize,
157    info_footer: Paragraph<'a>,
158    info_footer_data: Vec<FooterData>,
159    vertical_layout: Layout,
160    current_preset: Option<u16>,
161}
162
163impl<'a> GenericMenu<'a> {
164    #[must_use]
165    pub fn new(
166        rows: Vec<RowData>,
167        headers: &[String],
168        info_footer: Vec<FooterData>,
169        current_preset: Option<u16>,
170    ) -> Self {
171        // Calculate constraints
172        let column_widths = calculate_max_column_widths(&rows, headers);
173        let constraints = constraint_length_from_widths(&column_widths);
174        let row_sum_height = calculate_sum_inner_row_heights(&rows);
175        let vertical_layout = Layout::vertical([
176            Constraint::Min(1),
177            Constraint::Length(
178                u16::try_from(headers.len()).expect("too much headers to store :p "),
179            ),
180        ]);
181        Self {
182            table_custom: TableCustomRetroStyle::new(headers, rows, 0, constraints),
183            scrollbar: ScrollBarCustomRetroStyle::new(row_sum_height),
184            selected_row: 0,
185            info_footer: get_formated_footer(&info_footer),
186            info_footer_data: info_footer,
187            vertical_layout,
188            current_preset,
189        }
190    }
191
192    pub fn next_row(&mut self) {
193        let i = match self.table_custom.state.selected() {
194            Some(i) => (i + 1) % self.table_custom.rows.len(),
195            None => 0,
196        };
197        self.table_custom.state.select(Some(i));
198        self.selected_row = i;
199        self.scrollbar.scroll_state =
200            self.scrollbar
201                .scroll_state
202                .position(calculate_sum_inner_row_heights(
203                    &self.table_custom.rows[..i],
204                ));
205    }
206
207    pub fn previous_row(&mut self) {
208        let i = match self.table_custom.state.selected() {
209            Some(i) => (i + self.table_custom.rows.len() - 1) % self.table_custom.rows.len(),
210            None => 0,
211        };
212        self.table_custom.state.select(Some(i));
213        self.selected_row = i;
214        self.scrollbar.scroll_state =
215            self.scrollbar
216                .scroll_state
217                .position(calculate_sum_inner_row_heights(
218                    &self.table_custom.rows[..i],
219                ));
220    }
221
222    pub fn next_parameter_value(&mut self) {
223        if let Some(row) = self.table_custom.rows.get_mut(self.selected_row) {
224            row.next_cell_value();
225        }
226    }
227
228    pub fn previous_parameter_value(&mut self) {
229        if let Some(row) = self.table_custom.rows.get_mut(self.selected_row) {
230            row.previous_cell_value();
231        }
232    }
233
234    pub fn run(
235        &mut self,
236        mut actions_inputs: Vec<ActionInputs<'a>>,
237        terminal: &mut DefaultTerminal,
238    ) {
239        loop {
240            terminal.draw(|frame| self.draw(frame)).unwrap();
241            if let Event::Key(key) = event::read().unwrap()
242                && key.kind == KeyEventKind::Press
243                && has_no_modifiers(&key)
244            {
245                for action_input in &mut actions_inputs {
246                    for key_code in action_input.key.clone() {
247                        if key_code == key.code {
248                            for unitary_tp_action in &mut action_input.action {
249                                match unitary_tp_action {
250                                    TableParameterAction::NextValue => {
251                                        self.next_parameter_value();
252                                    }
253                                    TableParameterAction::PreviousValue => {
254                                        self.previous_parameter_value();
255                                    }
256                                    TableParameterAction::NextRow => {
257                                        self.next_row();
258                                    }
259                                    TableParameterAction::PreviousRow => {
260                                        self.previous_row();
261                                    }
262                                    TableParameterAction::ApplyAndSave(action) => {
263                                        action.apply_and_save(
264                                            &self.table_custom.rows,
265                                            self.current_preset,
266                                        );
267                                    }
268                                    TableParameterAction::Quit => {
269                                        return;
270                                    }
271                                    TableParameterAction::LoadPreset(index, loader) => {
272                                        let (new_rows, new_footer) = loader(*index);
273                                        let new_rows =
274                                            new_rows.unwrap_or(self.table_custom.rows.clone());
275                                        let new_footer =
276                                            new_footer.unwrap_or(self.info_footer_data.clone());
277                                        //Refresh all the GUI, so recreate the table with the new data and recalculate constraint
278                                        //Clean Rust way with self-overwriting
279                                        *self = Self::new(
280                                            new_rows,
281                                            &self.table_custom.headers,
282                                            new_footer,
283                                            Some(*index),
284                                        );
285                                    }
286                                    TableParameterAction::Randomize(randomizer) => {
287                                        let new_rows = randomizer(
288                                            self.current_preset,
289                                            &self.table_custom.rows,
290                                        );
291                                        //Refresh all the GUI, so recreate the table with the new data and recalculate constraint
292                                        //Clean Rust way with self-overwriting
293                                        *self = Self::new(
294                                            new_rows,
295                                            &self.table_custom.headers,
296                                            self.info_footer_data.clone(),
297                                            self.current_preset,
298                                        );
299                                    }
300                                } //match
301                            } // unitary action
302                        } //key code
303                    } //keyCodes
304                } //ActionInputs
305            } //event readable
306        } //loop
307    }
308
309    fn draw(&mut self, frame: &mut Frame) {
310        let rects = self.vertical_layout.split(frame.area());
311        //render the custom table (could have implemented the statefulWidget trait or Widget, but state is badly handled)
312        // Bad API design in ratatui!
313        self.table_custom
314            .update_table_color_background(self.selected_row);
315        self.table_custom.render(frame, rects[0]);
316        //Unfortunately, scrollbar does not yet implement render_stateful_widget_ref,
317        // so we have to use the old way with clone
318        //https://docs.rs/ratatui/latest/ratatui/widgets/trait.StatefulWidgetRef.html#implementors
319        frame.render_stateful_widget(
320            self.scrollbar.widget.clone(),
321            rects[0].inner(self.scrollbar.margin),
322            &mut self.scrollbar.scroll_state,
323        );
324        frame.render_widget_ref(&self.info_footer, rects[1]);
325    }
326}
327
328#[must_use]
329pub fn get_default_action_input<'a>() -> Vec<ActionInputs<'a>> {
330    vec![
331        ActionInputs {
332            key: vec![KeyCode::Down],
333            action: vec![TableParameterAction::NextRow],
334        },
335        ActionInputs {
336            key: vec![KeyCode::Up],
337            action: vec![TableParameterAction::PreviousRow],
338        },
339        ActionInputs {
340            key: vec![KeyCode::Right],
341            action: vec![TableParameterAction::NextValue],
342        },
343        ActionInputs {
344            key: vec![KeyCode::Left],
345            action: vec![TableParameterAction::PreviousValue],
346        },
347        ActionInputs {
348            key: vec![KeyCode::Esc, KeyCode::Tab],
349            action: vec![TableParameterAction::Quit],
350        },
351    ]
352}