Skip to main content

teksilo_widgets/table_view/
keyboard.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Shared keyboard handler for `TableView` and `TreeTableView`.
5//!
6//! The handler is generic over `RowNavigator` so flat and tree
7//! navigation reuse the same key matrix. Tree-specific arrow-left /
8//! arrow-right collapse/expand semantics fall through automatically
9//! because the trait's default `is_expanded` / `has_children` /
10//! `toggle_expanded` methods are no-ops on a flat table.
11
12use std::rc::Rc;
13use std::time::Duration;
14
15use teksilo_core::event::{EventResponse, Key, WidgetEvent};
16use teksilo_core::signal::Signal;
17use teksilo_core::widget::EventContext;
18use teksilo_data::SelectionMode;
19
20use super::PaneBoundaries;
21use super::body::SharedColumnWidths;
22use super::column::{EditTriggers, TabTraversal};
23use super::row_navigator::RowNavigator;
24use super::selection::{CellSelectionModel, TableSelectionMode};
25use crate::common::row_metrics::SharedRowMetrics;
26use crate::common::type_ahead::TypeAheadState;
27use crate::data_views::RowSelection;
28
29/// Configuration captured from the table at build time and threaded
30/// into the on_key handler. Cheap to clone (signals + Rcs).
31#[derive(Clone)]
32pub(crate) struct KeyHandlerConfig {
33    pub navigator: Rc<dyn RowNavigator>,
34    pub col_count: usize,
35    /// Display position of the tree column — the one hosting the twist and
36    /// indent gutter, and therefore the only column where ArrowLeft/ArrowRight
37    /// collapse/expand instead of moving the cursor.
38    ///
39    /// Resolved per rebuild by the owning widget, because
40    /// [`TreeTableView::tree_column`](crate::TreeTableView::tree_column) names
41    /// a column *id* while user drag-reorder moves its *display* position —
42    /// the two diverge the moment either is used. `TableView` passes `0`: its `FlatNavigator` reports
43    /// `has_children`/`is_expanded` as false and `toggle_expanded` as a no-op,
44    /// so the comparison can never lead anywhere.
45    pub tree_column_display_pos: usize,
46    pub focused_cell: Signal<Option<(usize, usize)>>,
47    pub selection_mode: TableSelectionMode,
48    pub selection: Option<RowSelection>,
49    pub cell_selection: Option<CellSelectionModel>,
50    pub scroll_y: Signal<f32>,
51    pub max_scroll_y: Signal<f32>,
52    pub viewport_height: Rc<std::cell::Cell<f32>>,
53    /// The row-area's absolute (window) rect: row 0's top sits at
54    /// `body_bounds.y` when `scroll_y == 0`. Read to chase the keyboard-focused
55    /// row into any *enclosing* scroll area via
56    /// [`EventContext::ensure_visible`](teksilo_core::widget::EventContext::ensure_visible)
57    /// — the table's own viewport follow is handled by `scroll_y`. Rows are not
58    /// distinct focusable nodes, so the framework's focus-driven follow never
59    /// reveals the selected row in an outer scroller. Populated by each widget's
60    /// `place_children`.
61    pub body_bounds: Rc<std::cell::Cell<teksilo_canvas::Rect>>,
62    /// Row geometry (uniform / exact / auto-measure) — drives the
63    /// PageUp/PageDown focus-row math.
64    pub row_metrics: SharedRowMetrics,
65    pub tab_traversal: TabTraversal,
66    pub editing_cell: Signal<Option<(usize, usize)>>,
67    /// `(row, col_id)` → invoke user's edit hook. The closure resolves
68    /// `col_id` from a display position; we keep it generic over
69    /// `&str` so the keyboard module doesn't need a `Column<T>`
70    /// reference.
71    pub display_col_to_id: Rc<dyn Fn(usize) -> Option<String>>,
72    /// The [`EditTriggers`] in force for the column at a display position —
73    /// the view's set, overridden by the column's own, and `NONE` for a
74    /// non-editable column. Per column rather than one set for the table
75    /// because that is what the caller declares: entering edit mode on a
76    /// column whose delegate has no editor would only confuse the focus /
77    /// dispatch state, and a tree column usually wants different gestures from
78    /// the value columns beside it.
79    pub display_col_triggers: Rc<dyn Fn(usize) -> EditTriggers>,
80    /// Optional: user callback fired when an edit trigger matches.
81    #[allow(clippy::type_complexity)]
82    pub on_cell_edit_request:
83        Option<Rc<dyn Fn(usize, &str, &mut teksilo_core::widget::EventContext)>>,
84    /// Optional: row-activate (Enter) callback.
85    #[allow(clippy::type_complexity)]
86    pub on_row_activate: Option<Rc<dyn Fn(usize, &mut teksilo_core::widget::EventContext)>>,
87    /// Persistent type-ahead buffer (survives the per-keystroke rebuild).
88    pub type_ahead: Rc<TypeAheadState>,
89    /// Type-ahead label resolver: `row -> Some(text)` for a resident row.
90    /// `None` (the option) disables type-ahead.
91    #[allow(clippy::type_complexity)]
92    pub type_ahead_label: Option<Rc<dyn Fn(usize) -> Option<String>>>,
93    /// Reset window for the type-ahead search term.
94    pub type_ahead_timeout: Duration,
95
96    /// Resolved column widths in display order — shared with the row/header
97    /// layout. Read to compute a display column's horizontal extent for
98    /// ensure-column-visible.
99    pub column_widths: SharedColumnWidths,
100    /// Pane partition (Leading/Middle/Trailing) — pinned columns never
101    /// trigger horizontal scrolling, since they're always visible by
102    /// definition. Snapshotted at build like `col_count` (a pinning/order
103    /// change rebuilds the whole table anyway).
104    pub pane_boundaries: PaneBoundaries,
105    pub scroll_x: Signal<f32>,
106    pub max_scroll_x: Signal<f32>,
107    /// Middle-pane viewport width, populated by `place_children` — the
108    /// horizontal analogue of `viewport_height`.
109    pub middle_viewport_width: Rc<std::cell::Cell<f32>>,
110}
111
112/// Build the on_key closure. Captures config by value; the closure is
113/// `'static` and ready to slot into a `HandlerSet`.
114pub(crate) fn build_key_handler(
115    cfg: KeyHandlerConfig,
116) -> impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static {
117    move |event, ctx: &mut EventContext| {
118        let WidgetEvent::KeyDown { key, modifiers, .. } = event else {
119            return EventResponse::Ignored;
120        };
121
122        let row_count = cfg.navigator.row_count();
123        if row_count == 0 || cfg.col_count == 0 {
124            return EventResponse::Ignored;
125        }
126        // The keyboard cursor: the focused cell once the user has navigated or
127        // clicked, else the selected row (a table can be handed a selection
128        // before it is ever focused — a restored last position, a preselected
129        // entry). `None` means "no cursor yet", which is deliberately NOT the
130        // same as `Some((0, 0))`: the directional keys below land ON the near
131        // end cell rather than stepping past it. Collapsing the two is what made
132        // the first ArrowDown skip row 0, the first ArrowUp a dead key
133        // (`prev_row(0)` is `None`), and the first ArrowRight skip column 0.
134        let raw = cfg.focused_cell.get().or_else(|| {
135            cfg.selection
136                .as_ref()
137                .and_then(|s| s.selected_indices().first().copied())
138                .map(|r| (r, 0))
139        });
140        let cursor = raw.map(|(r, c)| (r.min(row_count - 1), c.min(cfg.col_count - 1)));
141        // Anchor for the keys that compute *from* a cell (expand / collapse,
142        // paging, Home/End, activation, editing) rather than step in a
143        // direction.
144        let (row, col) = cursor.unwrap_or((0, 0));
145        // Persist the clamp: if the stored focus was out of range (e.g. rows or
146        // columns were removed since it was set), write the in-bounds cell back
147        // so the focus ring and any later reader don't keep the stale position.
148        if raw.is_some() && raw != Some((row, col)) {
149            cfg.focused_cell.set(Some((row, col)));
150        }
151
152        // Read layout direction live from the dispatch context (a runtime
153        // locale switch dirties the tree but does not rebuild, so a
154        // build-time capture would go stale).
155        let rtl = ctx.is_rtl();
156
157        // Tree-aware collapse / expand (flat impls are no-ops, so this is
158        // safe to evaluate eagerly). The keys follow the visual chevron:
159        // under LTR the collapsed chevron points right (ArrowRight
160        // expands, ArrowLeft collapses); under RTL it points left, so the
161        // two arrows swap.
162        let on_tree_column = col == cfg.tree_column_display_pos;
163        let is_collapse_key = if rtl {
164            matches!(key, Key::ArrowRight)
165        } else {
166            matches!(key, Key::ArrowLeft)
167        };
168        let is_expand_key = if rtl {
169            matches!(key, Key::ArrowLeft)
170        } else {
171            matches!(key, Key::ArrowRight)
172        };
173        if is_collapse_key && on_tree_column && cfg.navigator.is_expanded(row) {
174            cfg.navigator.toggle_expanded(row);
175            return EventResponse::Handled;
176        }
177        if is_expand_key
178            && on_tree_column
179            && cfg.navigator.has_children(row)
180            && !cfg.navigator.is_expanded(row)
181        {
182            cfg.navigator.toggle_expanded(row);
183            return EventResponse::Handled;
184        }
185
186        let viewport_h = cfg.viewport_height.get();
187
188        // Each directional key, with NO cursor yet, lands ON the end cell it
189        // would have entered from — it does not step past it (see `cursor`
190        // above, and the same rule in `ListView` / `TreeView` / `GridView`).
191        // `first_row` / `last_row` (not raw 0 / row_count-1) so a hierarchical
192        // navigator — `TreeTableView` plugs its own in here — enters at a row
193        // that is actually visible.
194        let new_pos: Option<(usize, usize)> = match key {
195            Key::ArrowUp => match cursor {
196                None => cfg.navigator.last_row().map(|r| (r, col)),
197                Some(_) => cfg.navigator.prev_row(row).map(|r| (r, col)),
198            },
199            Key::ArrowDown => match cursor {
200                None => cfg.navigator.first_row().map(|r| (r, col)),
201                Some(_) => cfg.navigator.next_row(row).map(|r| (r, col)),
202            },
203            // Visual-left moves to a higher display index under RTL
204            // (columns run right-to-left), so the two arrows swap their
205            // index delta. The clamps stay tied to the physical edge each
206            // arrow points at. Column 0 is the leading column in both
207            // directions, so a cursor-less entry lands on the column the key
208            // points *away* from: the "next" key on the first column, the
209            // "previous" key on the last.
210            Key::ArrowLeft => {
211                if rtl {
212                    match cursor {
213                        None => Some((row, 0)),
214                        Some(_) => (col + 1 < cfg.col_count).then_some((row, col + 1)),
215                    }
216                } else {
217                    match cursor {
218                        None => Some((row, cfg.col_count - 1)),
219                        // `.then` (lazy) — `col - 1` must not be evaluated at col 0.
220                        Some(_) => (col > 0).then(|| (row, col - 1)),
221                    }
222                }
223            }
224            Key::ArrowRight => {
225                if rtl {
226                    match cursor {
227                        None => Some((row, cfg.col_count - 1)),
228                        Some(_) => (col > 0).then(|| (row, col - 1)),
229                    }
230                } else {
231                    match cursor {
232                        None => Some((row, 0)),
233                        Some(_) => (col + 1 < cfg.col_count).then_some((row, col + 1)),
234                    }
235                }
236            }
237            // Plain Home / End move within the row; with the accelerator
238            // (Ctrl, ⌘ on macOS) they jump to the first / last row of the table.
239            Key::Home if !modifiers.command() => Some((row, 0)),
240            Key::End if !modifiers.command() => Some((row, cfg.col_count - 1)),
241            Key::Home if modifiers.command() => cfg.navigator.first_row().map(|r| (r, 0)),
242            Key::End if modifiers.command() => {
243                cfg.navigator.last_row().map(|r| (r, cfg.col_count - 1))
244            }
245            Key::PageUp => {
246                // Scroll one page; move focus to the row one viewport
247                // above the current row's top (offset-table-driven, so
248                // variable heights page by visual distance, not by a
249                // fixed row count). Guarantee progress even when a
250                // single row is taller than the viewport.
251                let new_y = (cfg.scroll_y.get() - viewport_h).max(0.0);
252                cfg.scroll_y.set(new_y);
253                let r = {
254                    let mut m = cfg.row_metrics.borrow_mut();
255                    m.resize(row_count);
256                    let target_y = (m.row_top(row) - viewport_h).max(0.0);
257                    m.row_at(target_y)
258                };
259                let r = if r == row { row.saturating_sub(1) } else { r };
260                Some((r, col))
261            }
262            Key::PageDown => {
263                let new_y = (cfg.scroll_y.get() + viewport_h).min(cfg.max_scroll_y.get());
264                cfg.scroll_y.set(new_y);
265                let r = {
266                    let mut m = cfg.row_metrics.borrow_mut();
267                    m.resize(row_count);
268                    let target_y = m.row_top(row) + viewport_h;
269                    m.row_at(target_y)
270                };
271                let r = if r == row {
272                    (row + 1).min(row_count - 1)
273                } else {
274                    r.min(row_count - 1)
275                };
276                Some((r, col))
277            }
278            // Ctrl+Tab / Ctrl+Shift+Tab escape the cell grid: return Ignored so
279            // the framework's focus cycling moves to the next / previous widget.
280            // Plain Tab still navigates cells (the `CellsThenRows` trap), but
281            // this gives keyboard users a reliable way out — the same un-trap
282            // affordance `RichTextEditor` leaves to OS focus navigation.
283            //
284            // Literal `ctrl()`, not `command()`: Ctrl+Tab is Ctrl+Tab on macOS
285            // too — ⌘⇥ belongs to the application switcher and never reaches an
286            // app at all.
287            Key::Tab if modifiers.ctrl() => return EventResponse::Ignored,
288            Key::Tab => {
289                if modifiers.shift() {
290                    if col > 0 {
291                        Some((row, col - 1))
292                    } else if let Some(prev) = cfg.navigator.prev_row(row) {
293                        Some((prev, cfg.col_count - 1))
294                    } else if cfg.tab_traversal == TabTraversal::OutOfTable {
295                        return EventResponse::Ignored;
296                    } else {
297                        Some((row, col))
298                    }
299                } else {
300                    if col + 1 < cfg.col_count {
301                        Some((row, col + 1))
302                    } else if let Some(next) = cfg.navigator.next_row(row) {
303                        Some((next, 0))
304                    } else if cfg.tab_traversal == TabTraversal::OutOfTable {
305                        return EventResponse::Ignored;
306                    } else {
307                        Some((row, col))
308                    }
309                }
310            }
311            // Toggles the focused cell/row regardless of Ctrl — this is
312            // already "Ctrl+Space toggles the focused row's selection"
313            // (the Explorer/Finder pairing with Ctrl+Arrow move-only above):
314            // after a Ctrl+Arrow walk away from the selection, Space here
315            // toggles just the cursor's current cell.
316            Key::Space => {
317                toggle_selection(&cfg, row, col);
318                cfg.focused_cell.set(Some((row, col)));
319                return EventResponse::Handled;
320            }
321            Key::Enter => {
322                if let Some(ref f) = cfg.on_row_activate {
323                    f(row, ctx);
324                } else {
325                    toggle_selection(&cfg, row, col);
326                }
327                return EventResponse::Handled;
328            }
329            Key::F2 if (cfg.display_col_triggers)(col).contains(EditTriggers::F2) => {
330                if let Some(col_id) = (cfg.display_col_to_id)(col) {
331                    cfg.editing_cell.set(Some((row, col)));
332                    if let Some(ref f) = cfg.on_cell_edit_request {
333                        f(row, &col_id, ctx);
334                    }
335                    return EventResponse::Handled;
336                }
337                return EventResponse::Ignored;
338            }
339            // Type-to-edit. `Key::Character` only fires for non-letter
340            // printable chars on this platform; letters arrive as the
341            // dedicated `Key::A`..`Key::Z` variants. Match any key that
342            // has a printable char form via `Key::to_char()`. Gated on
343            // the column's `editable` flag so non-editable columns
344            // don't enter edit mode (which would set `editing_cell`
345            // without any actual editor in the cell to receive focus
346            // and follow-up keystrokes).
347            k if (cfg.display_col_triggers)(col).contains(EditTriggers::ANY_KEY)
348                && !modifiers.ctrl()
349                && !modifiers.alt()
350                && !modifiers.super_key()
351                && k.to_char().is_some() =>
352            {
353                if let Some(col_id) = (cfg.display_col_to_id)(col) {
354                    cfg.editing_cell.set(Some((row, col)));
355                    if let Some(ref f) = cfg.on_cell_edit_request {
356                        f(row, &col_id, ctx);
357                    }
358                    // Don't claim Handled — the typed character should
359                    // propagate to the editor that the cell delegate
360                    // swaps in.
361                    return EventResponse::Ignored;
362                }
363                return EventResponse::Ignored;
364            }
365            // Type-ahead: a printable char (no Ctrl/Alt/Super) jumps the
366            // focused row to the next row whose label starts with the
367            // accumulated term. Reached only when the type-to-edit arm above
368            // didn't consume the char (no editor on this column / edit off).
369            k if cfg.type_ahead_label.is_some()
370                && !modifiers.ctrl()
371                && !modifiers.alt()
372                && !modifiers.super_key()
373                && k.to_char().is_some() =>
374            {
375                let c = k.to_char().unwrap();
376                let label = cfg.type_ahead_label.as_ref().unwrap();
377                if let Some(nr) =
378                    cfg.type_ahead
379                        .search(c, row, row_count, cfg.type_ahead_timeout, |i| label(i))
380                {
381                    cfg.focused_cell.set(Some((nr, col)));
382                    apply_selection_extension(&cfg, nr, col, false);
383                    ensure_row_visible(&cfg, nr, row_count, ctx);
384                    ensure_col_visible(&cfg, col);
385                    return EventResponse::Handled;
386                }
387                return EventResponse::Ignored;
388            }
389            // Select all — Ctrl+A, ⌘A on macOS.
390            Key::A if modifiers.command() => {
391                select_all(&cfg, row_count);
392                return EventResponse::Handled;
393            }
394            Key::Escape => {
395                if cfg.editing_cell.get().is_some() {
396                    cfg.editing_cell.set(None);
397                } else {
398                    cfg.focused_cell.set(None);
399                }
400                return EventResponse::Handled;
401            }
402            _ => None,
403        };
404
405        if let Some((nr, nc)) = new_pos {
406            cfg.focused_cell.set(Some((nr, nc)));
407            // Explorer/Finder convention: Ctrl+Arrow (no Shift) repositions
408            // the keyboard cursor without touching selection — the followed
409            // "select the row you land on" behavior is opt-out only via
410            // Ctrl, exactly like plain Arrow's select-follow is opt-in via
411            // nothing (default) and Shift+Arrow's extend is opt-in via
412            // Shift. `Ctrl+Space` (below, `Key::Space`'s `toggle_selection`
413            // already ignores modifiers) then toggles just the cell the
414            // cursor moved to.
415            let is_arrow = matches!(
416                key,
417                Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight
418            );
419            // Literal `ctrl()`, macOS included: ⌘↑/⌘↓ already mean something
420            // else in a Finder list, and this Explorer-style cursor pair has no
421            // ⌘ counterpart — Control keeps it reachable and out of the way.
422            let move_cursor_only = is_arrow && modifiers.ctrl() && !modifiers.shift();
423            if !move_cursor_only {
424                apply_selection_extension(&cfg, nr, nc, modifiers.shift());
425            }
426            ensure_row_visible(&cfg, nr, row_count, ctx);
427            ensure_col_visible(&cfg, nc);
428            return EventResponse::Handled;
429        }
430
431        EventResponse::Ignored
432    }
433}
434
435/// Scroll the viewport so `row` is fully visible — a no-op when it
436/// already is. Gives `TableView` / `TreeTableView` the same
437/// "keyboard-focused row stays on screen" behavior that `ListView` /
438/// `TreeView` already have: every Arrow / Home / End / Ctrl+Home/End /
439/// Tab move that lands on a new row keeps it visible.
440///
441/// `PageUp` / `PageDown` already set `scroll_y` to a page boundary and
442/// pick a focus row at the new viewport edge, so calling this for them
443/// only refines the offset (the chosen row is visible by construction —
444/// no extra jump).
445fn ensure_row_visible(
446    cfg: &KeyHandlerConfig,
447    row: usize,
448    row_count: usize,
449    ctx: &mut EventContext,
450) {
451    let scroll = cfg.scroll_y.get();
452    let new_scroll = {
453        let mut m = cfg.row_metrics.borrow_mut();
454        m.resize(row_count);
455        m.scroll_for_ensure_visible(
456            row,
457            scroll,
458            cfg.viewport_height.get(),
459            cfg.max_scroll_y.get(),
460        )
461    };
462    if (new_scroll - scroll).abs() > f32::EPSILON {
463        cfg.scroll_y.set(new_scroll);
464    }
465    // After keeping the row in the table's OWN viewport, chain the reveal to
466    // any enclosing scroll area (a form/page the table is embedded in).
467    crate::common::row_metrics::chase_row_into_outer_view(
468        ctx,
469        &cfg.row_metrics,
470        cfg.body_bounds.get(),
471        row,
472        new_scroll,
473    );
474}
475
476/// Scroll the Middle pane horizontally so `display_col` is fully visible —
477/// the horizontal analogue of [`ensure_row_visible`]. A no-op for a
478/// Leading/Trailing-pinned column: pinning already guarantees visibility, so
479/// the column can never trigger horizontal scrolling. Unlike rows (via
480/// `RowMetrics`, virtualized over thousands of entries), the column count is
481/// small and already fully resolved in `column_widths`, so a plain linear
482/// scan suffices — no shared "ColumnMetrics" abstraction needed.
483fn ensure_col_visible(cfg: &KeyHandlerConfig, display_col: usize) {
484    let b = cfg.pane_boundaries;
485    if display_col < b.leading_count || display_col >= b.middle_end {
486        return;
487    }
488    let widths = cfg.column_widths.borrow();
489    let Some(w) = widths.get(display_col).copied() else {
490        return;
491    };
492    // Logical x of `display_col` within the *unscrolled* Middle content
493    // strip (offset from the Middle pane's own leading edge) — i.e.
494    // `column_logical_x` with `scroll_x = 0`, restricted to the Middle
495    // pane's own local space (band_width is irrelevant here since Trailing
496    // never enters this branch).
497    let x: f32 = widths[b.leading_count..display_col].iter().sum();
498    drop(widths);
499
500    let viewport_w = cfg.middle_viewport_width.get();
501    let scroll = cfg.scroll_x.get();
502    let max = cfg.max_scroll_x.get();
503    let new_scroll = if x < scroll {
504        x
505    } else if x + w > scroll + viewport_w {
506        (x + w - viewport_w).max(0.0)
507    } else {
508        scroll
509    }
510    .clamp(0.0, max.max(0.0));
511    if (new_scroll - scroll).abs() > f32::EPSILON {
512        cfg.scroll_x.set(new_scroll);
513    }
514}
515
516fn toggle_selection(cfg: &KeyHandlerConfig, row: usize, col: usize) {
517    match cfg.selection_mode {
518        TableSelectionMode::SingleRow | TableSelectionMode::MultiRow => {
519            if let Some(ref s) = cfg.selection {
520                if s.is_selected(row) {
521                    // Toggle off: a Multi selection model can have it
522                    // both ways; Single mode replaces with empty.
523                    if cfg.selection_mode == TableSelectionMode::MultiRow {
524                        s.toggle(row);
525                    } else {
526                        s.clear();
527                    }
528                } else {
529                    s.select(row);
530                }
531            }
532        }
533        TableSelectionMode::SingleCell | TableSelectionMode::MultiCell => {
534            if let Some(ref cs) = cfg.cell_selection {
535                if cs.is_selected(row, col) && cfg.selection_mode == TableSelectionMode::MultiCell {
536                    cs.toggle(row, col);
537                } else {
538                    cs.select(row, col);
539                }
540            }
541        }
542        TableSelectionMode::None => {}
543    }
544}
545
546fn apply_selection_extension(cfg: &KeyHandlerConfig, row: usize, col: usize, shift: bool) {
547    match cfg.selection_mode {
548        TableSelectionMode::MultiRow => {
549            if let Some(ref s) = cfg.selection {
550                if shift && s.mode() == SelectionMode::Multi {
551                    s.extend_to(row);
552                } else {
553                    s.select(row);
554                }
555            }
556        }
557        TableSelectionMode::SingleRow => {
558            if let Some(ref s) = cfg.selection {
559                s.select(row);
560            }
561        }
562        TableSelectionMode::MultiCell => {
563            if let Some(ref cs) = cfg.cell_selection {
564                if shift {
565                    cs.extend_to(row, col);
566                } else {
567                    cs.select(row, col);
568                }
569            }
570        }
571        TableSelectionMode::SingleCell => {
572            if let Some(ref cs) = cfg.cell_selection {
573                cs.select(row, col);
574            }
575        }
576        TableSelectionMode::None => {}
577    }
578}
579
580fn select_all(cfg: &KeyHandlerConfig, row_count: usize) {
581    match cfg.selection_mode {
582        TableSelectionMode::MultiRow => {
583            if let Some(ref s) = cfg.selection {
584                s.select_all(row_count);
585            }
586        }
587        TableSelectionMode::MultiCell => {
588            if let Some(ref cs) = cfg.cell_selection {
589                cs.select_all(row_count, cfg.col_count);
590            }
591        }
592        _ => {}
593    }
594}