rustic_rs/commands/tui/widgets/
select_table.rs

1use super::{
2    Color, Constraint, Draw, Event, Frame, KeyCode, KeyEventKind, Layout, Modifier, ProcessEvent,
3    Rect, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, SizedWidget, Style, Stylize, Table,
4    TableState, Text, layout, style,
5};
6use std::iter::once;
7use style::palette::tailwind;
8
9struct TableColors {
10    buffer_bg: Color,
11    header_bg: Color,
12    header_fg: Color,
13    row_fg: Color,
14    selected_style_fg: Color,
15    normal_row_color: Color,
16    alt_row_color: Color,
17}
18
19impl TableColors {
20    fn new(color: &tailwind::Palette) -> Self {
21        Self {
22            buffer_bg: tailwind::SLATE.c950,
23            header_bg: color.c900,
24            header_fg: tailwind::SLATE.c200,
25            row_fg: tailwind::SLATE.c200,
26            selected_style_fg: color.c400,
27            normal_row_color: tailwind::SLATE.c950,
28            alt_row_color: tailwind::SLATE.c900,
29        }
30    }
31}
32
33pub struct SelectTable {
34    header: Vec<Text<'static>>,
35    table: Table<'static>,
36    state: TableState,
37    scroll_state: ScrollbarState,
38    rows: usize,
39    rows_display: usize,
40    row_height: usize,
41}
42
43impl SelectTable {
44    pub fn new(header: Vec<Text<'static>>) -> Self {
45        let table = Table::default();
46
47        Self {
48            header,
49            table,
50            state: TableState::default(),
51            scroll_state: ScrollbarState::new(0),
52            rows: 0,
53            rows_display: 0,
54            row_height: 0,
55        }
56    }
57
58    pub fn set_content(&mut self, content: Vec<Vec<Text<'static>>>, row_height: usize) {
59        let colors = TableColors::new(&tailwind::BLUE);
60        let selected_style = Style::default()
61            .add_modifier(Modifier::REVERSED)
62            .fg(colors.selected_style_fg);
63
64        let header_style = Style::default().fg(colors.header_fg).bg(colors.header_bg);
65
66        self.row_height = row_height;
67        let widths = once(&self.header)
68            .chain(content.iter())
69            .map(|row| row.iter().map(Text::width).collect())
70            .reduce(|widths: Vec<usize>, row| {
71                row.iter()
72                    .zip(widths.iter())
73                    .map(|(r, w)| r.max(w))
74                    .copied()
75                    .collect()
76            })
77            .unwrap_or_default();
78
79        self.rows = content.len();
80        self.scroll_state = ScrollbarState::new(self.rows * self.row_height);
81
82        let content = content.into_iter().enumerate().map(|(i, row)| {
83            let color = match i % 2 {
84                0 => colors.normal_row_color,
85                _ => colors.alt_row_color,
86            };
87            Row::new(row)
88                .style(Style::new().fg(colors.row_fg).bg(color))
89                .height(self.row_height.try_into().unwrap())
90        });
91
92        self.table = Table::default()
93            .header(Row::new(self.header.clone()).style(header_style))
94            .row_highlight_style(selected_style)
95            .bg(colors.buffer_bg)
96            .widths(widths.iter().map(|w| {
97                (*w).try_into()
98                    .ok()
99                    .map_or(Constraint::Min(0), Constraint::Length)
100            }))
101            .flex(layout::Flex::SpaceBetween)
102            .rows(content);
103    }
104
105    pub fn selected(&self) -> Option<usize> {
106        self.state.selected()
107    }
108
109    pub fn select(&mut self, index: Option<usize>) {
110        self.state.select(index);
111    }
112
113    pub fn set_to(&mut self, i: usize) {
114        self.state.select(Some(i));
115        self.scroll_state = self.scroll_state.position(i * self.row_height);
116    }
117
118    pub fn go_forward(&mut self, step: usize) {
119        if let Some(selected_old) = self.state.selected() {
120            let selected = (selected_old + step).min(self.rows - 1);
121            self.set_to(selected);
122        }
123    }
124
125    pub fn go_back(&mut self, step: usize) {
126        if let Some(selected_old) = self.state.selected() {
127            let selected = selected_old.saturating_sub(step);
128            self.set_to(selected);
129        }
130    }
131
132    pub fn next(&mut self) {
133        self.go_forward(1);
134    }
135
136    pub fn page_down(&mut self) {
137        self.go_forward(self.rows_display);
138    }
139
140    pub fn previous(&mut self) {
141        self.go_back(1);
142    }
143
144    pub fn page_up(&mut self) {
145        self.go_back(self.rows_display);
146    }
147
148    pub fn home(&mut self) {
149        if self.state.selected().is_some() {
150            self.set_to(0);
151        }
152    }
153
154    pub fn end(&mut self) {
155        if self.state.selected().is_some() {
156            self.set_to(self.rows - 1);
157        }
158    }
159
160    pub fn set_rows(&mut self, rows: usize) {
161        self.rows_display = rows / self.row_height;
162    }
163}
164
165impl ProcessEvent for SelectTable {
166    type Result = ();
167    fn input(&mut self, event: Event) {
168        use KeyCode::{Down, End, Home, PageDown, PageUp, Up};
169        match event {
170            Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
171                Down => self.next(),
172                Up => self.previous(),
173                PageDown => self.page_down(),
174                PageUp => self.page_up(),
175                Home => self.home(),
176                End => self.end(),
177                _ => {}
178            },
179            _ => {}
180        }
181    }
182}
183
184impl SizedWidget for SelectTable {}
185
186impl Draw for SelectTable {
187    fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
188        self.set_rows(area.height.into());
189        let chunks = Layout::horizontal([Constraint::Min(0), Constraint::Length(1)]).split(area);
190        f.render_stateful_widget(&self.table, chunks[0], &mut self.state);
191        f.render_stateful_widget(
192            Scrollbar::default()
193                .orientation(ScrollbarOrientation::VerticalRight)
194                .begin_symbol(None)
195                .end_symbol(None),
196            chunks[1],
197            &mut self.scroll_state,
198        );
199    }
200}