Skip to main content

rat_ftable/
rowselection.rs

1use crate::event::TableOutcome;
2use crate::{TableSelection, TableState};
3use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Regular, ct_event};
4use rat_focus::HasFocus;
5use rat_scrolled::ScrollAreaState;
6use rat_scrolled::event::ScrollOutcome;
7use ratatui_crossterm::crossterm::event::Event;
8use std::cmp::{max, min};
9
10/// Allows selecting a single row of the table.
11///
12/// This is the right one if you want a list-style selection
13/// for your table.
14///
15/// This one only supports row-selection.
16#[derive(Debug, Default, Clone)]
17pub struct RowSelection {
18    /// Selected row.
19    pub lead_row: Option<usize>,
20    /// Scrolls the selection instead of the offset.
21    pub scroll_selected: bool,
22}
23
24impl TableSelection for RowSelection {
25    fn count(&self) -> usize {
26        if self.lead_row.is_some() { 1 } else { 0 }
27    }
28
29    fn clear(&mut self) {
30        self.lead_row = None;
31    }
32
33    fn is_selected_row(&self, row: usize) -> bool {
34        self.lead_row == Some(row)
35    }
36
37    fn is_selected_column(&self, _column: usize) -> bool {
38        false
39    }
40
41    fn is_selected_cell(&self, _column: usize, _row: usize) -> bool {
42        false
43    }
44
45    fn lead_selection(&self) -> Option<(usize, usize)> {
46        self.lead_row.map(|v| (0, v))
47    }
48
49    fn validate_rows(&mut self, rows: usize) {
50        if let Some(lead_row) = self.lead_row {
51            if rows == 0 {
52                self.lead_row = None;
53            } else if lead_row >= rows {
54                self.lead_row = Some(rows - 1);
55            }
56        }
57    }
58
59    fn validate_cols(&mut self, _cols: usize) {}
60
61    /// Update the state to match adding items.
62    #[allow(clippy::collapsible_if)]
63    fn items_added(&mut self, pos: usize, n: usize) {
64        if let Some(lead_row) = self.lead_row {
65            if lead_row > pos {
66                self.lead_row = Some(lead_row.saturating_add(n));
67            }
68        }
69    }
70
71    /// Update the state to match removing items.
72    fn items_removed(&mut self, pos: usize, n: usize, rows: usize) {
73        if let Some(lead_row) = self.lead_row {
74            if rows == 0 {
75                self.lead_row = None;
76            } else if lead_row == pos && lead_row + n >= rows {
77                self.lead_row = Some(rows.saturating_sub(1))
78            } else if lead_row > pos {
79                self.lead_row = Some(lead_row.saturating_sub(n).min(pos));
80            }
81        }
82    }
83}
84
85impl RowSelection {
86    /// New selection.
87    pub fn new() -> RowSelection {
88        Self::default()
89    }
90
91    /// Clear the selection.
92    pub fn clear(&mut self) {
93        self.lead_row = None;
94    }
95
96    /// Scroll selection instead of offset.
97    pub fn scroll_selected(&self) -> bool {
98        self.scroll_selected
99    }
100
101    /// Scroll selection instead of offset.
102    pub fn set_scroll_selected(&mut self, scroll: bool) {
103        self.scroll_selected = scroll;
104    }
105
106    /// The current selected row.
107    pub fn selected(&self) -> Option<usize> {
108        self.lead_row
109    }
110
111    /// Has some selection.
112    pub fn has_selection(&self) -> bool {
113        self.lead_row.is_some()
114    }
115
116    /// Select a row.
117    /// This function doesn't care if the given row actually exists in the table.
118    pub fn select(&mut self, select: Option<usize>) -> bool {
119        let old_row = self.lead_row;
120        self.lead_row = select;
121        old_row != self.lead_row
122    }
123
124    /// Select the given row, limit between 0 and maximum.
125    pub fn move_to(&mut self, select: usize, maximum: usize) -> bool {
126        let old_row = self.lead_row;
127        self.lead_row = Some(min(select, maximum));
128        old_row != self.lead_row
129    }
130
131    /// Select the next row, cap at maximum.
132    pub fn move_down(&mut self, n: usize, maximum: usize) -> bool {
133        let old_row = self.lead_row;
134        self.lead_row = Some(self.lead_row.map_or(0, |v| min(v + n, maximum)));
135        old_row != self.lead_row
136    }
137
138    /// Select the previous row.
139    pub fn move_up(&mut self, n: usize, maximum: usize) -> bool {
140        let old_row = self.lead_row;
141        self.lead_row = Some(self.lead_row.map_or(maximum, |v| v.saturating_sub(n)));
142        old_row != self.lead_row
143    }
144}
145
146impl HandleEvent<Event, Regular, TableOutcome> for TableState<RowSelection> {
147    fn handle(&mut self, event: &Event, _keymap: Regular) -> TableOutcome {
148        let res = if self.focus.is_focused() {
149            match event {
150                ct_event!(keycode press Up) => {
151                    if self.move_up(1) {
152                        TableOutcome::Selected
153                    } else {
154                        TableOutcome::Unchanged
155                    }
156                }
157                ct_event!(keycode press Down) => {
158                    if self.move_down(1) {
159                        TableOutcome::Selected
160                    } else {
161                        TableOutcome::Unchanged
162                    }
163                }
164                ct_event!(keycode press CONTROL-Up)
165                | ct_event!(keycode press CONTROL-Home)
166                | ct_event!(keycode press Home) => {
167                    if self.move_to(0) {
168                        TableOutcome::Selected
169                    } else {
170                        TableOutcome::Unchanged
171                    }
172                }
173                ct_event!(keycode press CONTROL-Down)
174                | ct_event!(keycode press CONTROL-End)
175                | ct_event!(keycode press End) => {
176                    if self.move_to(self.rows.saturating_sub(1)) {
177                        TableOutcome::Selected
178                    } else {
179                        TableOutcome::Unchanged
180                    }
181                }
182                ct_event!(keycode press PageUp) => {
183                    if self.move_up(max(1, self.page_len().saturating_sub(1))) {
184                        TableOutcome::Selected
185                    } else {
186                        TableOutcome::Unchanged
187                    }
188                }
189                ct_event!(keycode press PageDown) => {
190                    if self.move_down(max(1, self.page_len().saturating_sub(1))) {
191                        TableOutcome::Selected
192                    } else {
193                        TableOutcome::Unchanged
194                    }
195                }
196                ct_event!(keycode press Left) => {
197                    if self.scroll_left(1) {
198                        TableOutcome::Changed
199                    } else {
200                        TableOutcome::Unchanged
201                    }
202                }
203                ct_event!(keycode press Right) => {
204                    if self.scroll_right(1) {
205                        TableOutcome::Changed
206                    } else {
207                        TableOutcome::Unchanged
208                    }
209                }
210                ct_event!(keycode press CONTROL-Left) => {
211                    if self.scroll_to_x(0) {
212                        TableOutcome::Changed
213                    } else {
214                        TableOutcome::Unchanged
215                    }
216                }
217                ct_event!(keycode press CONTROL-Right) => {
218                    if self.scroll_to_x(self.x_max_offset()) {
219                        TableOutcome::Changed
220                    } else {
221                        TableOutcome::Unchanged
222                    }
223                }
224                _ => TableOutcome::Continue,
225            }
226        } else {
227            TableOutcome::Continue
228        };
229
230        if res == TableOutcome::Continue {
231            self.handle(event, MouseOnly)
232        } else {
233            res
234        }
235    }
236}
237
238impl HandleEvent<Event, MouseOnly, TableOutcome> for TableState<RowSelection> {
239    fn handle(&mut self, event: &Event, _keymap: MouseOnly) -> TableOutcome {
240        if self.focus.is_obscured() {
241            return TableOutcome::Continue;
242        }
243
244        let mut r = match event {
245            ct_event!(mouse any for m) if self.mouse.drag(self.table_area, m) => {
246                if self.move_to(self.row_at_drag((m.column, m.row))) {
247                    TableOutcome::Selected
248                } else {
249                    TableOutcome::Unchanged
250                }
251            }
252            ct_event!(mouse down Left for column, row) => {
253                if self.table_area.contains((*column, *row).into()) {
254                    if let Some(new_row) = self.row_at_clicked((*column, *row)) {
255                        if self.move_to(new_row) {
256                            TableOutcome::Selected
257                        } else {
258                            TableOutcome::Unchanged
259                        }
260                    } else {
261                        TableOutcome::Continue
262                    }
263                } else if self.header_area.contains((*column, *row).into()) {
264                    if let Some(col) = self.column_at_clicked((*column, *row)) {
265                        TableOutcome::Header(col)
266                    } else {
267                        TableOutcome::Continue
268                    }
269                } else if self.footer_area.contains((*column, *row).into()) {
270                    if let Some(col) = self.column_at_clicked((*column, *row)) {
271                        TableOutcome::Footer(col)
272                    } else {
273                        TableOutcome::Continue
274                    }
275                } else {
276                    TableOutcome::Continue
277                }
278            }
279
280            _ => TableOutcome::Continue,
281        };
282
283        r = r.or_else(|| {
284            let mut sas = ScrollAreaState::new()
285                .area(self.inner)
286                .h_scroll(&mut self.hscroll)
287                .v_scroll(&mut self.vscroll);
288            match sas.handle(event, MouseOnly) {
289                ScrollOutcome::Up(v) => {
290                    if self.selection.scroll_selected() {
291                        if self.move_up(1) {
292                            TableOutcome::Selected
293                        } else {
294                            TableOutcome::Unchanged
295                        }
296                    } else {
297                        if self.scroll_up(v) {
298                            TableOutcome::Changed
299                        } else {
300                            TableOutcome::Unchanged
301                        }
302                    }
303                }
304                ScrollOutcome::Down(v) => {
305                    if self.selection.scroll_selected() {
306                        if self.move_down(1) {
307                            TableOutcome::Selected
308                        } else {
309                            TableOutcome::Unchanged
310                        }
311                    } else {
312                        if self.scroll_down(v) {
313                            TableOutcome::Changed
314                        } else {
315                            TableOutcome::Unchanged
316                        }
317                    }
318                }
319                ScrollOutcome::VPos(v) => {
320                    if self.selection.scroll_selected {
321                        if self.move_to(self.remap_offset_selection(v)) {
322                            TableOutcome::Selected
323                        } else {
324                            TableOutcome::Unchanged
325                        }
326                    } else {
327                        if self.set_row_offset(self.vscroll.limited_offset(v)) {
328                            TableOutcome::Changed
329                        } else {
330                            TableOutcome::Unchanged
331                        }
332                    }
333                }
334                ScrollOutcome::Left(v) => {
335                    if self.scroll_left(v) {
336                        TableOutcome::Changed
337                    } else {
338                        TableOutcome::Unchanged
339                    }
340                }
341                ScrollOutcome::Right(v) => {
342                    if self.scroll_right(v) {
343                        TableOutcome::Changed
344                    } else {
345                        TableOutcome::Unchanged
346                    }
347                }
348                ScrollOutcome::HPos(v) => {
349                    if self.set_x_offset(self.hscroll.limited_offset(v)) {
350                        TableOutcome::Changed
351                    } else {
352                        TableOutcome::Unchanged
353                    }
354                }
355
356                ScrollOutcome::Continue => TableOutcome::Continue,
357                ScrollOutcome::Unchanged => TableOutcome::Unchanged,
358                ScrollOutcome::Changed => TableOutcome::Changed,
359            }
360        });
361
362        r
363    }
364}
365
366/// Handle all events.
367/// Table events are only processed if focus is true.
368/// Mouse events are processed if they are in range.
369pub fn handle_events(
370    state: &mut TableState<RowSelection>,
371    focus: bool,
372    event: &Event,
373) -> TableOutcome {
374    state.focus.set(focus);
375    state.handle(event, Regular)
376}
377
378/// Handle only mouse-events.
379pub fn handle_mouse_events(state: &mut TableState<RowSelection>, event: &Event) -> TableOutcome {
380    state.handle(event, MouseOnly)
381}