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}