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