rsnaker/graphics/menus/retro_parameter_table/
generic_style.rs

1use crate::graphics::menus::retro_parameter_table::generic_logic::{
2    CellValue, FooterData, RowData,
3};
4use ratatui::layout::{Constraint, Margin, Rect};
5use ratatui::prelude::{Color, Line, Modifier, Span, Style, Text};
6use ratatui::widgets::{
7    Block, BorderType, Borders, Cell, FrameExt, HighlightSpacing, Paragraph, Row, Scrollbar,
8    ScrollbarOrientation, ScrollbarState, Table, TableState,
9};
10use ratatui::Frame;
11use std::fmt;
12
13// Updated constants with emojis
14pub const DEFAULT_ITEM_HEIGHT: usize = 1;
15
16// Retro game colors - removed pink in favor of a more balanced palette
17const RETRO_PURPLE: Color = Color::Rgb(50, 50, 150); //150,50,50
18const RETRO_GREY: Color = Color::Rgb(128, 128, 128);
19const RETRO_YELLOW: Color = Color::Rgb(255, 255, 0);
20const RETRO_DARK_BLUE: Color = Color::Rgb(20, 20, 40);
21const RETRO_ORANGE: Color = Color::Rgb(255, 165, 0);
22const RETRO_BLUE: Color = Color::Rgb(0, 191, 255);
23const RETRO_GOLD: Color = Color::Rgb(212, 175, 55);
24// Create highlight symbols with more visual appeal
25const HIGHLIGHT_SYMBOL_LEFT: &str = "► ";
26const HIGHLIGHT_SYMBOL_RIGHT: &str = " ◄";
27pub const DISPLAY_CELL_OUT_SPACE: usize = 6;
28
29impl fmt::Display for CellValue {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            CellValue::Text(text) => write!(f, "{text}"),
33            CellValue::Options {
34                values,
35                index,
36                index_ini,
37                ..
38            } => {
39                let fallback = "Bad index".to_string();
40                let content = values.get(*index).unwrap_or(&fallback);
41                if index == index_ini {
42                    write!(f, "[ {content} ]")
43                } else {
44                    write!(f, "『 {content} 』")
45                }
46            }
47        }
48    }
49}
50pub(crate) struct ScrollBarCustomRetroStyle<'a> {
51    pub scroll_state: ScrollbarState,
52    pub margin: Margin,
53    pub widget: Scrollbar<'a>,
54}
55impl ScrollBarCustomRetroStyle<'_> {
56    pub fn new(row_sum_height: usize) -> Self {
57        Self {
58            scroll_state: ScrollbarState::new(row_sum_height),
59            margin: Margin {
60                vertical: 1,
61                horizontal: 1,
62            },
63            widget: Scrollbar::default()
64                .orientation(ScrollbarOrientation::VerticalRight)
65                .begin_symbol(Some("▲"))
66                .end_symbol(Some("▼"))
67                .track_symbol(Some("│"))
68                .thumb_symbol("█")
69                .thumb_style(Style::default().fg(Color::DarkGray))
70                .track_style(Style::default().fg(Color::Gray))
71                .begin_style(Style::default().fg(Color::Red))
72                .end_style(Style::default().fg(Color::Red)),
73        }
74    }
75}
76pub(crate) struct TableCustomRetroStyle<'a> {
77    pub(crate) state: TableState,
78    table: Table<'a>,
79    pub(crate) rows: Vec<RowData>,
80    pub(crate) headers: Vec<String>,
81}
82impl<'a> TableCustomRetroStyle<'a> {
83    pub fn new(
84        headers: &[String],
85        rows: Vec<RowData>,
86        selected_row: usize,
87        constraints: Vec<Constraint>,
88    ) -> Self {
89        let headers_vec = headers.to_vec();
90        // Create a header row using the custom headers with retro styling
91        let header = Row::new(headers.iter().map(|h| Cell::from(h.clone())))
92            .style(Style::default().fg(Color::Black).bg(RETRO_GREY))
93            .height(1);
94
95        //Create highlight symbols with more visual appeal
96        let mut highlight_symbols = vec![HIGHLIGHT_SYMBOL_LEFT.into()];
97        //To find the number of columns, we use header len,
98        // We need the number of columns to display the right header one row below
99        for _ in 1..headers.len() - 1 {
100            highlight_symbols.push("".into());
101        }
102        highlight_symbols.push(HIGHLIGHT_SYMBOL_RIGHT.into());
103
104        // Create a table with retro-style borders
105        Self {
106            state: TableState::default().with_selected(0),
107            table: Table::new(
108                TableCustomRetroStyle::get_rows_color(&rows, selected_row),
109                constraints,
110            )
111            .header(header)
112            .row_highlight_style(Style::default().bg(RETRO_PURPLE).fg(Color::White))
113            .highlight_symbol(Text::from(highlight_symbols))
114            .highlight_spacing(HighlightSpacing::Always)
115            .block(
116                Block::default()
117                    .borders(Borders::ALL)
118                    .border_type(BorderType::Double)
119                    .border_style(Style::default().fg(Color::DarkGray)),
120            ),
121            rows,
122            headers: headers_vec,
123        }
124    }
125    pub fn render(&mut self, frame: &mut Frame, area: Rect) {
126        frame.render_stateful_widget_ref(&self.table, area, &mut self.state);
127    }
128    fn get_rows_color(rows: &[RowData], selected_row: usize) -> impl IntoIterator<Item = Row<'a>> {
129        // Create rows with alternating background colors
130        rows.iter()
131            .enumerate()
132            .map(|(index_row, row_data)| {
133                let row_style = if index_row % 2 == 0 {
134                    Style::default().bg(Color::Black)
135                } else {
136                    Style::default().bg(RETRO_DARK_BLUE)
137                };
138                let mut max_lines_for_this_row: u16 = 1;
139                let cells = row_data.cells.iter().map(|cell| {
140                    let content = cell.to_string();
141                    let cell_style = if selected_row == index_row {
142                        Style::default()
143                            .fg(RETRO_YELLOW)
144                            .add_modifier(Modifier::BOLD)
145                    } else if let CellValue::Options {
146                        index, index_ini, ..
147                    } = cell
148                    {
149                        if index == index_ini {
150                            Style::default().fg(RETRO_ORANGE)
151                        } else {
152                            Style::default().fg(RETRO_YELLOW)
153                        }
154                    } else {
155                        Style::default().fg(RETRO_BLUE)
156                    };
157                    //useful for height calculation of the row
158                    #[allow(clippy::cast_possible_truncation)]
159                    let max = content.lines().count() as u16;
160                    if max > max_lines_for_this_row {
161                        max_lines_for_this_row = max;
162                    }
163                    Cell::from(Text::from(content)).style(cell_style)
164                });
165
166                Row::new(cells)
167                    .style(row_style)
168                    .height(max_lines_for_this_row)
169            })
170            .collect::<Vec<Row<'a>>>()
171    }
172    pub fn update_table_color_background(&mut self, selected_row: usize) {
173        let new_rows = TableCustomRetroStyle::get_rows_color(&self.rows, selected_row);
174        //Bad ratatui API design for widget reuse as the function consume self, without providing a setter with &mut
175        self.table = self.table.clone().rows(new_rows);
176    }
177}
178#[must_use]
179pub fn get_formated_footer<'a>(data: &[FooterData]) -> Paragraph<'a> {
180    // Create a multicolor, retro-styled footer
181    let mut info_spans = Vec::with_capacity(data.len() + 1);
182
183    //To act differently on the last element
184    let mut iter = data.iter().peekable();
185    while let Some(d) = iter.next() {
186        info_spans.push(Span::styled(
187            format!("({})", d.symbol),
188            Style::default().fg(RETRO_GOLD).add_modifier(Modifier::BOLD),
189        ));
190        info_spans.push(Span::styled(
191            format!(" {} ", d.text),
192            Style::default().fg(Color::White),
193        ));
194        if let Some(value) = d.value {
195            let red_style = Style::default().fg(Color::Red); //.add_modifier(Modifier::BOLD);
196            info_spans.push(Span::styled("[".to_string(), red_style));
197            info_spans.push(Span::styled(
198                value.to_string(),
199                Style::default().fg(Color::White), //.add_modifier(Modifier::BOLD),
200            ));
201            info_spans.push(Span::styled("]".to_string(), red_style));
202        }
203        //To have a closing pipe only if there are still others elements after
204        if iter.peek().is_some() {
205            info_spans.push(Span::styled(
206                " | ".to_string(),
207                Style::default().fg(Color::White),
208            ));
209        }
210    }
211    Paragraph::new(Line::from(info_spans)).centered()
212}