Skip to main content

qframe/widgets/table/
mod.rs

1//! Tables: rows of cells under a header, virtualised, sortable and selectable.
2//!
3//! The columns, cells and rows an application builds are in `model`; `layout` decides column
4//! widths and which columns show; `paint` draws the header and the rows.
5
6mod layout;
7mod model;
8mod paint;
9#[cfg(test)]
10mod tests;
11
12use std::sync::Arc;
13
14use crate::event::{Event, MouseButton, MouseKind};
15use crate::geometry::{Rect, Size, clamp_u16};
16use crate::keymap::{Key, KeyChord, Modifiers};
17use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
18
19use super::click::Click;
20use super::row::LEAD;
21use super::row_menu::{self, RowAnchor, RowMenuItems};
22use super::row_pointer::{self, PickedRows, Picking, RowDrop, Spot};
23use super::rows::{self, RowScroll, Step};
24use super::select_box;
25use super::{ContextItem, IndexMessage};
26use layout::Placed;
27pub use model::{Column, ColumnWidth, SortDirection, TableCell, TableRow};
28use paint::RowPaint;
29
30/// Cells between two columns. Columns are told apart by space, never by a drawn line.
31const COLUMN_GAP: u16 = 2;
32
33/// Cells taken by the check mark of a multi-select table.
34const MARK: u16 = 2;
35
36/// Builds a message from a column and a direction.
37type SortMessage<Msg> = Box<dyn Fn(usize, SortDirection) -> Msg>;
38
39/// Rows of cells under a header row. Only the rows on screen are drawn, so a table stays fast
40/// with any number of rows; pass the rows as an `Arc<[TableRow]>` kept in your state to avoid
41/// copying them every frame.
42///
43/// The header sits on a raised surface with faint titles. Columns are separated by space; their
44/// widths follow [`ColumnWidth`] and [`Column::min`], and when the minimums do not fit, the
45/// columns scroll sideways: arrows at the ends of the header show which side hides columns. The
46/// application owns the selection, the checked rows and the sort order and is told about changes
47/// through messages. A hovered or selected row raises its surface and shows the pillar; only its
48/// first cell slides one cell right. The check mark of a multi-select table never moves.
49///
50/// Keys while focused: ↑/↓ or k/j, PgUp/PgDn, Home/End move (↑/↓ round the ends with
51/// [`wrap`](Self::wrap)); Enter activates; Space toggles in
52/// multi-select tables and activates otherwise; ←/→ scroll columns that overflow; with
53/// [`Table::on_sort`], `s` sorts by the next sortable column and `shift+s` reverses the order.
54///
55/// The mouse does the same: a click on a row selects and activates it, a click on its check mark
56/// (or the cell after it) only toggles, a click on a sortable title sorts by it and a second
57/// click reverses it, and a click on a header arrow scrolls the columns one step.
58///
59/// Four capabilities make the rows work the way a file explorer's do, each off until asked for:
60///
61/// - [`activate_on(Click::Double)`](Self::activate_on): a click only selects a row and a double
62///   click activates it, so a click can start a drag or a selection without opening anything.
63/// - [`multi_select`](Self::multi_select): several rows are selected at once with Ctrl+click,
64///   Shift+click, Shift+arrows, Ctrl+A and Space; they share the selection tone while only the cursor's row carries the
65///   pillar and slides.
66/// - [`box_select`](Self::box_select): a drag from the free space below the rows draws a box, and
67///   the rows it covers become the selection.
68/// - [`droppable`](Self::droppable): the selection is dragged onto a row that takes it, such as a
69///   folder, which takes the accent tone while the drag is over it.
70///
71/// Style keys: rows use `list-item` (`hover`, `selected`, `focus`, `pressed`) and
72/// `list-item.faint` like [`List`](super::List); `table-header` (`bg`, `fg`) with `hover` over a
73/// sortable title and `selected` on the sorted one; `table-sort` for the sort arrow;
74/// `table-scroll` (`fg`, `bg`) with `hover` for the header arrows; `list-header` for the empty
75/// text; `tree-drop` for the row a drag would drop on; `text-selection` (`bg`) for the selection
76/// box; `scrollbar`.
77pub struct Table<Msg> {
78    columns: Vec<Column>,
79    rows: Arc<[TableRow]>,
80    selected: Option<usize>,
81    checked: Option<Vec<bool>>,
82    sort: Option<(usize, SortDirection)>,
83    empty: String,
84    on_select: Option<IndexMessage<Msg>>,
85    on_activate: Option<IndexMessage<Msg>>,
86    on_toggle: Option<IndexMessage<Msg>>,
87    on_sort: Option<SortMessage<Msg>>,
88    menu: Option<RowMenuItems<Msg>>,
89    menu_on_activate: bool,
90    picking: Picking<Msg>,
91    wrap: bool,
92}
93
94#[derive(Debug, Default)]
95struct TableMemory {
96    /// Widest cell of every column, for the rows it was measured on.
97    fit: Option<(Arc<[TableRow]>, Vec<u16>)>,
98    /// First visible column when the columns overflow.
99    column_offset: usize,
100    /// The largest useful `column_offset` in the last frame; zero when nothing overflows.
101    max_column_offset: usize,
102    /// Whether columns were hidden on the right in the last frame.
103    more: bool,
104    placed: Vec<Placed>,
105}
106
107impl<Msg: 'static> Table<Msg> {
108    /// A table with `columns` showing `rows`.
109    #[must_use]
110    pub fn new(columns: impl IntoIterator<Item = Column>, rows: impl Into<Arc<[TableRow]>>) -> Self {
111        Self {
112            columns: columns.into_iter().collect(),
113            rows: rows.into(),
114            selected: None,
115            checked: None,
116            sort: None,
117            empty: String::new(),
118            on_select: None,
119            on_activate: None,
120            on_toggle: None,
121            on_sort: None,
122            menu: None,
123            menu_on_activate: false,
124            picking: Picking::default(),
125            wrap: false,
126        }
127    }
128
129    /// Lets ↓ (or j) on the last row go on to the first and ↑ (or k) on the first go to the
130    /// last, as a menu does. PgUp/PgDn and Home/End still stop at the ends, and Shift with an
131    /// arrow never wraps a range. Off by default: the keys stop at either end.
132    #[must_use]
133    pub fn wrap(mut self, wrap: bool) -> Self {
134        self.wrap = wrap;
135        self
136    }
137
138    /// The selected row index.
139    #[must_use]
140    pub fn selected(mut self, index: Option<usize>) -> Self {
141        self.selected = index;
142        self
143    }
144
145    /// Turns on multiple selection; `checked[i]` tells whether row `i` is checked.
146    #[must_use]
147    pub fn checked(mut self, checked: Vec<bool>) -> Self {
148        self.checked = Some(checked);
149        self
150    }
151
152    /// Shows the sort arrow on `column` pointing in `direction`. The rows must already be in
153    /// that order; the table does not reorder them.
154    #[must_use]
155    pub fn sort(mut self, column: usize, direction: SortDirection) -> Self {
156        self.sort = Some((column, direction));
157        self
158    }
159
160    /// Text shown under the header when there are no rows.
161    #[must_use]
162    pub fn empty_text(mut self, text: impl Into<String>) -> Self {
163        self.empty = text.into();
164        self
165    }
166
167    /// Message for moving the selection to a row.
168    #[must_use]
169    pub fn on_select(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
170        self.on_select = Some(Box::new(message));
171        self
172    }
173
174    /// Message for opening a row (Enter, click).
175    #[must_use]
176    pub fn on_activate(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
177        self.on_activate = Some(Box::new(message));
178        self
179    }
180
181    /// Message for checking or unchecking a row of a multi-select table (Space, click on the mark).
182    #[must_use]
183    pub fn on_toggle(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
184        self.on_toggle = Some(Box::new(message));
185        self
186    }
187
188    /// Message asking to sort by a column in a direction; turns on sorting by clicking titles of
189    /// [`Column::sortable`] columns and with `s` / `shift+s`.
190    #[must_use]
191    pub fn on_sort(mut self, message: impl Fn(usize, SortDirection) -> Msg + 'static) -> Self {
192        self.on_sort = Some(Box::new(message));
193        self
194    }
195
196    /// Gives every row a context menu: `items(index)` builds the entries for the row of that
197    /// index, and the menu acts on the row it was opened on rather than on the selected one.
198    ///
199    /// A right press on a row opens the menu at the pointer; the menu key or Shift+F10 opens the
200    /// menu of the selected row below it, scrolling it into view first. The row the menu belongs
201    /// to stays raised while it is open, so it is clear what the entries act on. A right press on
202    /// a row that is not checked makes it the selection first, so a menu never acts on rows the
203    /// person did not mean.
204    #[must_use]
205    pub fn context_menu(mut self, items: impl Fn(usize) -> Vec<ContextItem<Msg>> + 'static) -> Self {
206        self.menu = Some(Box::new(items));
207        self
208    }
209
210    /// Makes a row's [context menu](Self::context_menu) its action: Enter opens the menu of the
211    /// selected row below it and a click opens the menu of the clicked row where it was clicked,
212    /// instead of sending [`on_activate`](Self::on_activate).
213    ///
214    /// For a table whose rows are acted on only through a few choices: a right click is not what
215    /// most people try in a terminal and many keyboards have no menu key, so the menu is also
216    /// reached the way any row is opened. A row whose menu has no entries opens nothing. Off by
217    /// default; without a context menu it does nothing.
218    #[must_use]
219    pub fn menu_on_activate(mut self, on: bool) -> Self {
220        self.menu_on_activate = on;
221        self
222    }
223
224    /// How many clicks activate a row: [`Click::Single`], the default, selects and activates at
225    /// once; [`Click::Double`] only selects on a click and activates on a second press on the same
226    /// row within [`Click::INTERVAL`]. Enter activates either way.
227    #[must_use]
228    pub fn activate_on(mut self, click: Click) -> Self {
229        self.picking.activate_on = click;
230        self
231    }
232
233    /// Lets several rows be selected at once: `selected` holds their indexes and `message(rows)`
234    /// asks the application to make `rows` the whole new selection.
235    ///
236    /// The row given to [`selected`](Self::selected) stays the cursor: the row the keys move from
237    /// and the only one with the pillar, while every selected row takes the selection tone.
238    /// Ctrl+click adds a row or takes it out, Shift+click selects the rows from the last plain or
239    /// Ctrl click to this one; Shift with ↑/↓, PgUp/PgDn or Home/End extends that range, Ctrl+A
240    /// selects every row, Space adds or takes out the cursor's row, Esc reduces several selected
241    /// rows to the cursor's, and a plain click or arrow selects that one row. A right click on a selected row keeps the selection for its menu; on another
242    /// row it makes that row the selection first.
243    #[must_use]
244    pub fn multi_select(mut self, selected: &[usize], message: impl Fn(Vec<usize>) -> Msg + 'static) -> Self {
245        self.picking.chosen = selected.to_vec();
246        self.picking.on_choose = Some(Box::new(message));
247        self
248    }
249
250    /// Lets a drag from the free space below the rows draw a box: the rows it covers become the
251    /// selection while it is drawn, or join it when Ctrl was held at the press, and a click there
252    /// without a drag clears the selection. The box is a tone laid over the cells it covers, never
253    /// a frame. It needs [`multi_select`](Self::multi_select) and does nothing without it.
254    #[must_use]
255    pub fn box_select(mut self, on: bool) -> Self {
256        self.picking.box_select = on;
257        self
258    }
259
260    /// Lets rows be dragged onto other rows, such as files onto a folder: `accepts(index)` tells
261    /// whether a row takes drops and `message(RowDrop)` asks the application to move the rows.
262    ///
263    /// A drag carries the pressed row, or the whole [selection](Self::multi_select) when it is
264    /// pressed on one of its rows; a click on a selected row without a drag makes it the one
265    /// selected row on release. The row under the pointer takes the accent tone while it can take
266    /// the drag. A release anywhere else, or on one of the dragged rows, does nothing. With
267    /// [`Click::Single`] a row activates on release rather than on press, so pressing a row to
268    /// drag it does not activate it.
269    #[must_use]
270    pub fn droppable(
271        mut self,
272        message: impl Fn(RowDrop) -> Msg + 'static,
273        accepts: impl Fn(usize) -> bool + 'static,
274    ) -> Self {
275        self.picking.dropping = Some((Box::new(message), Box::new(accepts)));
276        self
277    }
278
279    /// A drop released with Ctrl held asks for a copy with `message` instead of the move of
280    /// [`droppable`](Self::droppable), the way a file explorer copies. A terminal that does not
281    /// report Ctrl with the pointer always moves. It does nothing without `droppable`.
282    #[must_use]
283    pub fn on_copy_drop(mut self, message: impl Fn(RowDrop) -> Msg + 'static) -> Self {
284        self.picking.copy_drop = Some(Box::new(message));
285        self
286    }
287
288    /// The cells the rows have to themselves: the scrollbar column is not part of a row.
289    fn rows_width(area: Rect, overflows: bool) -> u16 {
290        area.width.saturating_sub(u16::from(overflows))
291    }
292
293    /// Offers `event` to the row menu. A right press picks the row under the pointer and makes it
294    /// the selection unless it is checked, because a menu on a checked row acts on the checked
295    /// rows, which the application knows about.
296    fn menu_event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
297        let area = cx.area();
298        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
299        let total = self.rows.len();
300        let visible = usize::from(body.height);
301        let overflows = total > visible;
302        row_menu::event(
303            cx,
304            event,
305            self.menu.as_ref(),
306            total,
307            |cx, x, y| {
308                if y < body.y || x >= area.x + i32::from(Self::rows_width(area, overflows)) {
309                    return None;
310                }
311                let offset = cx.memory::<RowScroll>().offset;
312                let row = usize::try_from(y - body.y).ok().map(|row| offset + row).filter(|row| *row < total)?;
313                let checked = self.checked.as_ref().is_some_and(|checked| checked.get(row).copied().unwrap_or(false));
314                if self.picking.is_multi() && !self.picking.is_chosen(row) {
315                    self.picking.select_one(cx, self, row);
316                } else if !checked && !self.picking.is_multi() {
317                    self.select(cx, row);
318                }
319                Some(RowAnchor { row, at: Rect::new(x, y, 1, 1), keyboard: false })
320            },
321            |cx| self.selected_anchor(cx),
322        )
323    }
324
325    /// Where the menu of the selected row unfolds from for the keyboard: below the whole row,
326    /// scrolled into view first.
327    fn selected_anchor(&self, cx: &mut EventCx<'_, Msg>) -> Option<RowAnchor> {
328        let area = cx.area();
329        let body_y = area.y + 1;
330        let total = self.rows.len();
331        let visible = usize::from(area.height.saturating_sub(1));
332        let overflows = total > visible;
333        let row = self.selected.filter(|row| *row < total)?;
334        let memory = cx.memory::<RowScroll>();
335        if row < memory.offset {
336            memory.offset = row;
337        } else if visible > 0 && row >= memory.offset + visible {
338            memory.offset = row + 1 - visible;
339        }
340        let y = body_y + i32::try_from(row - memory.offset).unwrap_or(0);
341        let at = Rect::new(area.x, y, Self::rows_width(area, overflows), 1);
342        Some(RowAnchor { row, at, keyboard: true })
343    }
344
345    /// Whether Enter and a click open the row's menu rather than the row.
346    fn activation_is_menu(&self) -> bool {
347        self.menu_on_activate && self.menu.is_some()
348    }
349
350    fn lead(&self) -> u16 {
351        LEAD + if self.checked.is_some() { MARK } else { 0 }
352    }
353
354    fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
355        if Some(index) != self.selected
356            && let Some(message) = &self.on_select
357        {
358            cx.emit(message(index));
359        }
360    }
361
362    fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
363        let Some(message) = &self.on_activate else {
364            return false;
365        };
366        cx.memory::<RowScroll>().flashed = Some(index);
367        cx.flash();
368        cx.emit(message(index));
369        true
370    }
371
372    fn toggle(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
373        match (&self.checked, &self.on_toggle) {
374            (Some(_), Some(message)) => {
375                cx.emit(message(index));
376                true
377            }
378            _ => false,
379        }
380    }
381
382    fn request_sort(&self, cx: &mut EventCx<'_, Msg>, column: usize, direction: SortDirection) -> bool {
383        match &self.on_sort {
384            Some(message) if self.columns.get(column).is_some_and(|c| c.sortable) => {
385                cx.emit(message(column, direction));
386                true
387            }
388            _ => false,
389        }
390    }
391
392    /// Sorting after a click on `column`'s title: the other direction when it is already sorted.
393    fn click_sort(&self, column: usize) -> SortDirection {
394        match self.sort {
395            Some((sorted, direction)) if sorted == column => direction.reversed(),
396            _ => SortDirection::Ascending,
397        }
398    }
399
400    /// Scrolls overflowing columns one column forward or back. Returns whether the columns
401    /// overflow at all, so the key or press is used even at an end.
402    fn scroll_columns(cx: &mut EventCx<'_, Msg>, forward: bool) -> bool {
403        let memory = cx.memory::<TableMemory>();
404        if memory.max_column_offset == 0 {
405            return false;
406        }
407        memory.column_offset = if forward {
408            (memory.column_offset + 1).min(memory.max_column_offset)
409        } else {
410            memory.column_offset.saturating_sub(1)
411        };
412        true
413    }
414
415    /// Which way the header's scroll arrow at column `x` scrolls, when one is drawn there: the
416    /// back arrow in the first cell while columns are hidden on the left, the forward arrow in
417    /// the last cell while columns are hidden on the right.
418    fn scroll_arrow_at(cx: &mut EventCx<'_, Msg>, area: Rect, x: i32) -> Option<bool> {
419        let memory = cx.memory::<TableMemory>();
420        if x == area.x && memory.column_offset > 0 {
421            Some(false)
422        } else if x == area.right() - 1 && memory.more {
423            Some(true)
424        } else {
425            None
426        }
427    }
428}
429
430impl<Msg: 'static> Widget<Msg> for Table<Msg> {
431    fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
432        let rows = self.rows.len().max(1) + 1;
433        let widths =
434            self.columns.iter().fold(0u16, |sum, c| sum.saturating_add(c.title_width()).saturating_add(COLUMN_GAP));
435        Size::new(widths.saturating_add(self.lead() + 1), clamp_u16(i32::try_from(rows).unwrap_or(i32::MAX)))
436            .min(available)
437    }
438
439    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
440        if area.is_empty() {
441            return;
442        }
443        cx.register_hit(area);
444        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
445        let total = self.rows.len();
446        let visible = usize::from(body.height);
447        let overflows = total > visible;
448        let lead = self.lead();
449        let room = area.width.saturating_sub(lead + u16::from(overflows));
450
451        let (placed, column_offset, more) = {
452            let memory = cx.memory::<TableMemory>();
453            let widest = if self.columns.iter().any(|c| c.width == ColumnWidth::Fit) {
454                self.widest_cells(memory)
455            } else {
456                vec![0; self.columns.len()]
457            };
458            let (widths, overflow) = self.widths(&widest, room);
459            // Columns that scroll sideways keep the forward arrow's cell and one cell of air before
460            // it free, like the back arrow and the lead on the left, so the arrow never covers or
461            // touches a title or a value. A scrollbar column already gives the arrow its cell.
462            let arrow = if overflow { 2 - u16::from(overflows) } else { 0 };
463            let room = room.saturating_sub(arrow);
464            let max_offset = if overflow { Self::max_offset(&widths, room) } else { 0 };
465            memory.max_column_offset = max_offset;
466            memory.column_offset = memory.column_offset.min(max_offset);
467            let placed = Self::place(&widths, memory.column_offset, area.x + i32::from(lead), room);
468            let more =
469                placed.last().is_some_and(|last| last.column + 1 < widths.len() || last.width < widths[last.column]);
470            memory.placed.clone_from(&placed);
471            memory.more = more;
472            (placed, memory.column_offset, more)
473        };
474        self.paint_header(cx, area, &placed, column_offset, more);
475
476        if total == 0 {
477            let faint = cx.style("list-header", None, &[]).text();
478            let budget = area.width.saturating_sub(LEAD);
479            cx.text(area.x + i32::from(LEAD), body.y, &self.empty, faint, budget);
480            return;
481        }
482        let focused = cx.is_focused();
483        let pressed = cx.is_pressed();
484        // An open row menu takes the pointer: only the row it acts on stays raised, so the menu
485        // and the row it belongs to are read together.
486        let menu_row = row_menu::open_row(cx, self.menu.as_ref());
487        if menu_row.is_some() {
488            cx.request_overlay(area);
489        }
490        let offset = cx.memory::<RowScroll>().follow(self.selected, total, visible);
491        let row_width = Self::rows_width(area, overflows);
492        let rows_rect = Rect::new(area.x, body.y, row_width, body.height);
493        // The row a drag is over takes the accent tone when it can take what is dragged.
494        let target = row_pointer::dragged(cx).and_then(|((x, y), carried)| {
495            let index = offset + usize::try_from(y - body.y).ok()?;
496            (rows_rect.contains(x, y) && index < total && self.picking.takes_drop(&carried, index)).then_some(index)
497        });
498        for (row, index) in (offset..total).take(visible).enumerate() {
499            let rect = Rect::new(area.x, body.y + i32::try_from(row).unwrap_or(0), row_width, 1);
500            self.paint_row(cx, rect, index, &placed, RowPaint { focused, pressed, menu_row, target });
501        }
502        if let Some(drawn) = row_pointer::drawn_box(cx) {
503            select_box::paint(cx, drawn, rows_rect);
504        }
505        rows::paint_scrollbar(cx, body, total, offset, None);
506    }
507
508    fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
509        row_menu::paint(cx, self.menu.as_ref(), anchor);
510    }
511
512    fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
513        if self.menu_event(cx, event) {
514            return true;
515        }
516        let area = cx.area();
517        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
518        let total = self.rows.len();
519        match event {
520            Event::Key(key) => {
521                let page = usize::from(body.height);
522                let extend = |_: &mut EventCx<'_, Msg>, plain: &crate::event::KeyEvent| {
523                    rows::SHIFT_STEPS
524                        .contains(&plain.chord.key)
525                        .then(|| Step::from_key(plain).and_then(|step| step.apply(self.selected, total, page)))
526                };
527                if self.picking.selection_key(cx, key, self, total, extend) {
528                    return true;
529                }
530                if let Some(step) = Step::from_key(key) {
531                    let Some(target) = step.apply_wrapping(self.selected, total, page, self.wrap) else {
532                        return false;
533                    };
534                    if self.picking.is_multi() {
535                        self.picking.select_one(cx, self, target);
536                    } else {
537                        self.select(cx, target);
538                    }
539                    return true;
540                }
541                if key.is_plain(Key::Left) || key.is_plain(Key::Right) {
542                    return Self::scroll_columns(cx, key.is_plain(Key::Right));
543                }
544                if key.is_plain(Key::Enter) {
545                    if self.activation_is_menu() {
546                        return self
547                            .selected_anchor(cx)
548                            .is_some_and(|anchor| row_menu::open_as_action(cx, self.menu.as_ref(), &anchor));
549                    }
550                    return self.selected.is_some_and(|index| self.activate(cx, index));
551                }
552                if key.is_plain(Key::Space) {
553                    let Some(index) = self.selected else { return false };
554                    return self.picking.toggle(cx, self, index) || self.toggle(cx, index) || self.activate(cx, index);
555                }
556                let shift_s = KeyChord { key: Key::Char('s'), mods: Modifiers { shift: true, ..Modifiers::default() } };
557                if self.on_sort.is_some() && (key.is_plain(Key::Char('s')) || key.chord == shift_s) {
558                    return match (key.chord == shift_s, self.sort) {
559                        (true, Some((column, direction))) => self.request_sort(cx, column, direction.reversed()),
560                        (true, None) => false,
561                        (false, current) => {
562                            let start = current.map_or(0, |(column, _)| column + 1);
563                            let count = self.columns.len();
564                            let next = (0..count)
565                                .map(|step| (start + step) % count.max(1))
566                                .find(|i| self.columns[*i].sortable);
567                            next.is_some_and(|column| self.request_sort(cx, column, SortDirection::Ascending))
568                        }
569                    };
570                }
571                false
572            }
573            Event::Mouse(mouse) => {
574                if rows::scroll_mouse(cx, mouse, body, total) {
575                    return true;
576                }
577                if mouse.kind == MouseKind::Down(MouseButton::Left) {
578                    if mouse.y == area.y {
579                        if let Some(forward) = Self::scroll_arrow_at(cx, area, mouse.x) {
580                            return Self::scroll_columns(cx, forward);
581                        }
582                        let placed = cx.memory::<TableMemory>().placed.clone();
583                        let Some(place) = placed.iter().find(|place| Self::spans(place, mouse.x)) else {
584                            return false;
585                        };
586                        return self.request_sort(cx, place.column, self.click_sort(place.column));
587                    }
588                    if self.checked.is_some()
589                        && mouse.x < area.x + i32::from(LEAD + MARK)
590                        && let Spot::Row(index) = self.spot(cx, mouse.x, mouse.y)
591                        && self.toggle(cx, index)
592                    {
593                        return true;
594                    }
595                }
596                self.picking.mouse(cx, mouse, self).unwrap_or(false)
597            }
598            _ => false,
599        }
600    }
601
602    fn focusable(&self) -> bool {
603        !self.rows.is_empty()
604    }
605}
606
607impl<Msg: 'static> PickedRows<Msg> for Table<Msg> {
608    fn spot(&self, cx: &mut EventCx<'_, Msg>, x: i32, y: i32) -> Spot {
609        let area = cx.area();
610        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
611        let total = self.rows.len();
612        let overflows = total > usize::from(body.height);
613        let rows = Rect::new(area.x, body.y, Self::rows_width(area, overflows), body.height);
614        if !rows.contains(x, y) {
615            return Spot::Outside;
616        }
617        let index = cx.memory::<RowScroll>().offset + usize::try_from(y - body.y).unwrap_or(0);
618        if index < total { Spot::Row(index) } else { Spot::Free }
619    }
620
621    fn covered(&self, cx: &mut EventCx<'_, Msg>, rect: Rect) -> Vec<usize> {
622        let area = cx.area();
623        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
624        let offset = cx.memory::<RowScroll>().offset;
625        let (top, bottom) = (rect.y.max(body.y), rect.bottom().min(body.bottom()));
626        (top..bottom)
627            .filter_map(|y| usize::try_from(y - body.y).ok())
628            .map(|row| offset + row)
629            .filter(|index| *index < self.rows.len())
630            .collect()
631    }
632
633    fn cursor(&self) -> Option<usize> {
634        self.selected
635    }
636
637    fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
638        Table::select(self, cx, index);
639    }
640
641    fn open(&self, cx: &mut EventCx<'_, Msg>, index: usize, (x, y): (i32, i32)) {
642        if self.activation_is_menu() {
643            let anchor = RowAnchor { row: index, at: Rect::new(x, y, 1, 1), keyboard: false };
644            row_menu::open_as_action(cx, self.menu.as_ref(), &anchor);
645        } else {
646            self.activate(cx, index);
647        }
648    }
649}