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