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 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, Space adds or takes out the cursor's row and a plain click selects
228    /// that one row. A right click on a selected row keeps the selection for its menu; on another
229    /// row it makes that row the selection first.
230    #[must_use]
231    pub fn multi_select(mut self, selected: &[usize], message: impl Fn(Vec<usize>) -> Msg + 'static) -> Self {
232        self.picking.chosen = selected.to_vec();
233        self.picking.on_choose = Some(Box::new(message));
234        self
235    }
236
237    /// Lets a drag from the free space below the rows draw a box: the rows it covers become the
238    /// selection while it is drawn, or join it when Ctrl was held at the press, and a click there
239    /// without a drag clears the selection. The box is a tone laid over the cells it covers, never
240    /// a frame. It needs [`multi_select`](Self::multi_select) and does nothing without it.
241    #[must_use]
242    pub fn box_select(mut self, on: bool) -> Self {
243        self.picking.box_select = on;
244        self
245    }
246
247    /// Lets rows be dragged onto other rows, such as files onto a folder: `accepts(index)` tells
248    /// whether a row takes drops and `message(RowDrop)` asks the application to move the rows.
249    ///
250    /// A drag carries the pressed row, or the whole [selection](Self::multi_select) when it is
251    /// pressed on one of its rows; a click on a selected row without a drag makes it the one
252    /// selected row on release. The row under the pointer takes the accent tone while it can take
253    /// the drag. A release anywhere else, or on one of the dragged rows, does nothing. With
254    /// [`Click::Single`] a row activates on release rather than on press, so pressing a row to
255    /// drag it does not activate it.
256    #[must_use]
257    pub fn droppable(
258        mut self,
259        message: impl Fn(RowDrop) -> Msg + 'static,
260        accepts: impl Fn(usize) -> bool + 'static,
261    ) -> Self {
262        self.picking.dropping = Some((Box::new(message), Box::new(accepts)));
263        self
264    }
265
266    /// A drop released with Ctrl held asks for a copy with `message` instead of the move of
267    /// [`droppable`](Self::droppable), the way a file explorer copies. A terminal that does not
268    /// report Ctrl with the pointer always moves. It does nothing without `droppable`.
269    #[must_use]
270    pub fn on_copy_drop(mut self, message: impl Fn(RowDrop) -> Msg + 'static) -> Self {
271        self.picking.copy_drop = Some(Box::new(message));
272        self
273    }
274
275    /// The cells the rows have to themselves: the scrollbar column is not part of a row.
276    fn rows_width(area: Rect, overflows: bool) -> u16 {
277        area.width.saturating_sub(u16::from(overflows))
278    }
279
280    /// Offers `event` to the row menu. A right press picks the row under the pointer and makes it
281    /// the selection unless it is checked, because a menu on a checked row acts on the checked
282    /// rows, which the application knows about.
283    fn menu_event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
284        let area = cx.area();
285        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
286        let total = self.rows.len();
287        let visible = usize::from(body.height);
288        let overflows = total > visible;
289        row_menu::event(
290            cx,
291            event,
292            self.menu.as_ref(),
293            total,
294            |cx, x, y| {
295                if y < body.y || x >= area.x + i32::from(Self::rows_width(area, overflows)) {
296                    return None;
297                }
298                let offset = cx.memory::<RowScroll>().offset;
299                let row = usize::try_from(y - body.y).ok().map(|row| offset + row).filter(|row| *row < total)?;
300                let checked = self.checked.as_ref().is_some_and(|checked| checked.get(row).copied().unwrap_or(false));
301                if self.picking.is_multi() && !self.picking.is_chosen(row) {
302                    self.picking.select_one(cx, self, row);
303                } else if !checked && !self.picking.is_multi() {
304                    self.select(cx, row);
305                }
306                Some(RowAnchor { row, at: Rect::new(x, y, 1, 1), keyboard: false })
307            },
308            |cx| self.selected_anchor(cx),
309        )
310    }
311
312    /// Where the menu of the selected row unfolds from for the keyboard: below the whole row,
313    /// scrolled into view first.
314    fn selected_anchor(&self, cx: &mut EventCx<'_, Msg>) -> Option<RowAnchor> {
315        let area = cx.area();
316        let body_y = area.y + 1;
317        let total = self.rows.len();
318        let visible = usize::from(area.height.saturating_sub(1));
319        let overflows = total > visible;
320        let row = self.selected.filter(|row| *row < total)?;
321        let memory = cx.memory::<RowScroll>();
322        if row < memory.offset {
323            memory.offset = row;
324        } else if visible > 0 && row >= memory.offset + visible {
325            memory.offset = row + 1 - visible;
326        }
327        let y = body_y + i32::try_from(row - memory.offset).unwrap_or(0);
328        let at = Rect::new(area.x, y, Self::rows_width(area, overflows), 1);
329        Some(RowAnchor { row, at, keyboard: true })
330    }
331
332    /// Whether Enter and a click open the row's menu rather than the row.
333    fn activation_is_menu(&self) -> bool {
334        self.menu_on_activate && self.menu.is_some()
335    }
336
337    fn lead(&self) -> u16 {
338        LEAD + if self.checked.is_some() { MARK } else { 0 }
339    }
340
341    fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
342        if Some(index) != self.selected
343            && let Some(message) = &self.on_select
344        {
345            cx.emit(message(index));
346        }
347    }
348
349    fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
350        let Some(message) = &self.on_activate else {
351            return false;
352        };
353        cx.memory::<RowScroll>().flashed = Some(index);
354        cx.flash();
355        cx.emit(message(index));
356        true
357    }
358
359    fn toggle(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
360        match (&self.checked, &self.on_toggle) {
361            (Some(_), Some(message)) => {
362                cx.emit(message(index));
363                true
364            }
365            _ => false,
366        }
367    }
368
369    fn request_sort(&self, cx: &mut EventCx<'_, Msg>, column: usize, direction: SortDirection) -> bool {
370        match &self.on_sort {
371            Some(message) if self.columns.get(column).is_some_and(|c| c.sortable) => {
372                cx.emit(message(column, direction));
373                true
374            }
375            _ => false,
376        }
377    }
378
379    /// Sorting after a click on `column`'s title: the other direction when it is already sorted.
380    fn click_sort(&self, column: usize) -> SortDirection {
381        match self.sort {
382            Some((sorted, direction)) if sorted == column => direction.reversed(),
383            _ => SortDirection::Ascending,
384        }
385    }
386
387    /// Scrolls overflowing columns one column forward or back. Returns whether the columns
388    /// overflow at all, so the key or press is used even at an end.
389    fn scroll_columns(cx: &mut EventCx<'_, Msg>, forward: bool) -> bool {
390        let memory = cx.memory::<TableMemory>();
391        if memory.max_column_offset == 0 {
392            return false;
393        }
394        memory.column_offset = if forward {
395            (memory.column_offset + 1).min(memory.max_column_offset)
396        } else {
397            memory.column_offset.saturating_sub(1)
398        };
399        true
400    }
401
402    /// Which way the header's scroll arrow at column `x` scrolls, when one is drawn there: the
403    /// back arrow in the first cell while columns are hidden on the left, the forward arrow in
404    /// the last cell while columns are hidden on the right.
405    fn scroll_arrow_at(cx: &mut EventCx<'_, Msg>, area: Rect, x: i32) -> Option<bool> {
406        let memory = cx.memory::<TableMemory>();
407        if x == area.x && memory.column_offset > 0 {
408            Some(false)
409        } else if x == area.right() - 1 && memory.more {
410            Some(true)
411        } else {
412            None
413        }
414    }
415}
416
417impl<Msg: 'static> Widget<Msg> for Table<Msg> {
418    fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
419        let rows = self.rows.len().max(1) + 1;
420        let widths =
421            self.columns.iter().fold(0u16, |sum, c| sum.saturating_add(c.title_width()).saturating_add(COLUMN_GAP));
422        Size::new(widths.saturating_add(self.lead() + 1), clamp_u16(i32::try_from(rows).unwrap_or(i32::MAX)))
423            .min(available)
424    }
425
426    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
427        if area.is_empty() {
428            return;
429        }
430        cx.register_hit(area);
431        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
432        let total = self.rows.len();
433        let visible = usize::from(body.height);
434        let overflows = total > visible;
435        let lead = self.lead();
436        let room = area.width.saturating_sub(lead + u16::from(overflows));
437
438        let (placed, column_offset, more) = {
439            let memory = cx.memory::<TableMemory>();
440            let widest = if self.columns.iter().any(|c| c.width == ColumnWidth::Fit) {
441                self.widest_cells(memory)
442            } else {
443                vec![0; self.columns.len()]
444            };
445            let (widths, overflow) = self.widths(&widest, room);
446            // Columns that scroll sideways keep the forward arrow's cell and one cell of air before
447            // it free, like the back arrow and the lead on the left, so the arrow never covers or
448            // touches a title or a value. A scrollbar column already gives the arrow its cell.
449            let arrow = if overflow { 2 - u16::from(overflows) } else { 0 };
450            let room = room.saturating_sub(arrow);
451            let max_offset = if overflow { Self::max_offset(&widths, room) } else { 0 };
452            memory.max_column_offset = max_offset;
453            memory.column_offset = memory.column_offset.min(max_offset);
454            let placed = Self::place(&widths, memory.column_offset, area.x + i32::from(lead), room);
455            let more =
456                placed.last().is_some_and(|last| last.column + 1 < widths.len() || last.width < widths[last.column]);
457            memory.placed.clone_from(&placed);
458            memory.more = more;
459            (placed, memory.column_offset, more)
460        };
461        self.paint_header(cx, area, &placed, column_offset, more);
462
463        if total == 0 {
464            let faint = cx.style("list-header", None, &[]).text();
465            let budget = area.width.saturating_sub(LEAD);
466            cx.text(area.x + i32::from(LEAD), body.y, &self.empty, faint, budget);
467            return;
468        }
469        let focused = cx.is_focused();
470        let pressed = cx.is_pressed();
471        // An open row menu takes the pointer: only the row it acts on stays raised, so the menu
472        // and the row it belongs to are read together.
473        let menu_row = row_menu::open_row(cx, self.menu.as_ref());
474        if menu_row.is_some() {
475            cx.request_overlay(area);
476        }
477        let offset = cx.memory::<RowScroll>().follow(self.selected, total, visible);
478        let row_width = Self::rows_width(area, overflows);
479        let rows_rect = Rect::new(area.x, body.y, row_width, body.height);
480        // The row a drag is over takes the accent tone when it can take what is dragged.
481        let target = row_pointer::dragged(cx).and_then(|((x, y), carried)| {
482            let index = offset + usize::try_from(y - body.y).ok()?;
483            (rows_rect.contains(x, y) && index < total && self.picking.takes_drop(&carried, index)).then_some(index)
484        });
485        for (row, index) in (offset..total).take(visible).enumerate() {
486            let rect = Rect::new(area.x, body.y + i32::try_from(row).unwrap_or(0), row_width, 1);
487            self.paint_row(cx, rect, index, &placed, RowPaint { focused, pressed, menu_row, target });
488        }
489        if let Some(drawn) = row_pointer::drawn_box(cx) {
490            select_box::paint(cx, drawn, rows_rect);
491        }
492        rows::paint_scrollbar(cx, body, total, offset, None);
493    }
494
495    fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
496        row_menu::paint(cx, self.menu.as_ref(), anchor);
497    }
498
499    fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
500        if self.menu_event(cx, event) {
501            return true;
502        }
503        let area = cx.area();
504        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
505        let total = self.rows.len();
506        match event {
507            Event::Key(key) => {
508                if let Some(step) = Step::from_key(key) {
509                    let Some(target) = step.apply(self.selected, total, usize::from(body.height)) else {
510                        return false;
511                    };
512                    if self.picking.is_multi() {
513                        self.picking.select_one(cx, self, target);
514                    } else {
515                        self.select(cx, target);
516                    }
517                    return true;
518                }
519                if key.is_plain(Key::Left) || key.is_plain(Key::Right) {
520                    return Self::scroll_columns(cx, key.is_plain(Key::Right));
521                }
522                if key.is_plain(Key::Enter) {
523                    if self.activation_is_menu() {
524                        return self
525                            .selected_anchor(cx)
526                            .is_some_and(|anchor| row_menu::open_as_action(cx, self.menu.as_ref(), &anchor));
527                    }
528                    return self.selected.is_some_and(|index| self.activate(cx, index));
529                }
530                if key.is_plain(Key::Space) {
531                    let Some(index) = self.selected else { return false };
532                    return self.picking.toggle(cx, self, index) || self.toggle(cx, index) || self.activate(cx, index);
533                }
534                let shift_s = KeyChord { key: Key::Char('s'), mods: Modifiers { shift: true, ..Modifiers::default() } };
535                if self.on_sort.is_some() && (key.is_plain(Key::Char('s')) || key.chord == shift_s) {
536                    return match (key.chord == shift_s, self.sort) {
537                        (true, Some((column, direction))) => self.request_sort(cx, column, direction.reversed()),
538                        (true, None) => false,
539                        (false, current) => {
540                            let start = current.map_or(0, |(column, _)| column + 1);
541                            let count = self.columns.len();
542                            let next = (0..count)
543                                .map(|step| (start + step) % count.max(1))
544                                .find(|i| self.columns[*i].sortable);
545                            next.is_some_and(|column| self.request_sort(cx, column, SortDirection::Ascending))
546                        }
547                    };
548                }
549                false
550            }
551            Event::Mouse(mouse) => {
552                if rows::scroll_mouse(cx, mouse, body, total) {
553                    return true;
554                }
555                if mouse.kind == MouseKind::Down(MouseButton::Left) {
556                    if mouse.y == area.y {
557                        if let Some(forward) = Self::scroll_arrow_at(cx, area, mouse.x) {
558                            return Self::scroll_columns(cx, forward);
559                        }
560                        let placed = cx.memory::<TableMemory>().placed.clone();
561                        let Some(place) = placed.iter().find(|place| Self::spans(place, mouse.x)) else {
562                            return false;
563                        };
564                        return self.request_sort(cx, place.column, self.click_sort(place.column));
565                    }
566                    if self.checked.is_some()
567                        && mouse.x < area.x + i32::from(LEAD + MARK)
568                        && let Spot::Row(index) = self.spot(cx, mouse.x, mouse.y)
569                        && self.toggle(cx, index)
570                    {
571                        return true;
572                    }
573                }
574                self.picking.mouse(cx, mouse, self).unwrap_or(false)
575            }
576            _ => false,
577        }
578    }
579
580    fn focusable(&self) -> bool {
581        !self.rows.is_empty()
582    }
583}
584
585impl<Msg: 'static> PickedRows<Msg> for Table<Msg> {
586    fn spot(&self, cx: &mut EventCx<'_, Msg>, x: i32, y: i32) -> Spot {
587        let area = cx.area();
588        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
589        let total = self.rows.len();
590        let overflows = total > usize::from(body.height);
591        let rows = Rect::new(area.x, body.y, Self::rows_width(area, overflows), body.height);
592        if !rows.contains(x, y) {
593            return Spot::Outside;
594        }
595        let index = cx.memory::<RowScroll>().offset + usize::try_from(y - body.y).unwrap_or(0);
596        if index < total { Spot::Row(index) } else { Spot::Free }
597    }
598
599    fn covered(&self, cx: &mut EventCx<'_, Msg>, rect: Rect) -> Vec<usize> {
600        let area = cx.area();
601        let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
602        let offset = cx.memory::<RowScroll>().offset;
603        let (top, bottom) = (rect.y.max(body.y), rect.bottom().min(body.bottom()));
604        (top..bottom)
605            .filter_map(|y| usize::try_from(y - body.y).ok())
606            .map(|row| offset + row)
607            .filter(|index| *index < self.rows.len())
608            .collect()
609    }
610
611    fn cursor(&self) -> Option<usize> {
612        self.selected
613    }
614
615    fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
616        Table::select(self, cx, index);
617    }
618
619    fn open(&self, cx: &mut EventCx<'_, Msg>, index: usize, (x, y): (i32, i32)) {
620        if self.activation_is_menu() {
621            let anchor = RowAnchor { row: index, at: Rect::new(x, y, 1, 1), keyboard: false };
622            row_menu::open_as_action(cx, self.menu.as_ref(), &anchor);
623        } else {
624            self.activate(cx, index);
625        }
626    }
627}