teksilo-widgets 0.9.0

Widget library for Teksilo — over a hundred widgets and layout primitives, from Button to TreeTableView.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech

//! Shared keyboard handler for `TableView` and `TreeTableView`.
//!
//! The handler is generic over `RowNavigator` so flat and tree
//! navigation reuse the same key matrix. Tree-specific arrow-left /
//! arrow-right collapse/expand semantics fall through automatically
//! because the trait's default `is_expanded` / `has_children` /
//! `toggle_expanded` methods are no-ops on a flat table.

use std::rc::Rc;
use std::time::Duration;

use teksilo_core::event::{EventResponse, Key, WidgetEvent};
use teksilo_core::signal::Signal;
use teksilo_core::widget::EventContext;
use teksilo_data::SelectionMode;

use super::PaneBoundaries;
use super::body::SharedColumnWidths;
use super::column::{EditTriggers, TabTraversal};
use super::row_navigator::RowNavigator;
use super::selection::{CellSelectionModel, TableSelectionMode};
use crate::common::row_metrics::SharedRowMetrics;
use crate::common::type_ahead::TypeAheadState;
use crate::data_views::RowSelection;

/// Configuration captured from the table at build time and threaded
/// into the on_key handler. Cheap to clone (signals + Rcs).
#[derive(Clone)]
pub(crate) struct KeyHandlerConfig {
    pub navigator: Rc<dyn RowNavigator>,
    pub col_count: usize,
    /// Display position of the tree column — the one hosting the twist and
    /// indent gutter, and therefore the only column where ArrowLeft/ArrowRight
    /// collapse/expand instead of moving the cursor.
    ///
    /// Resolved per rebuild by the owning widget, because
    /// [`TreeTableView::tree_column`](crate::TreeTableView::tree_column) names
    /// a column *id* while user drag-reorder moves its *display* position —
    /// the two diverge the moment either is used. `TableView` passes `0`: its `FlatNavigator` reports
    /// `has_children`/`is_expanded` as false and `toggle_expanded` as a no-op,
    /// so the comparison can never lead anywhere.
    pub tree_column_display_pos: usize,
    pub focused_cell: Signal<Option<(usize, usize)>>,
    pub selection_mode: TableSelectionMode,
    pub selection: Option<RowSelection>,
    pub cell_selection: Option<CellSelectionModel>,
    pub scroll_y: Signal<f32>,
    pub max_scroll_y: Signal<f32>,
    pub viewport_height: Rc<std::cell::Cell<f32>>,
    /// The row-area's absolute (window) rect: row 0's top sits at
    /// `body_bounds.y` when `scroll_y == 0`. Read to chase the keyboard-focused
    /// row into any *enclosing* scroll area via
    /// [`EventContext::ensure_visible`](teksilo_core::widget::EventContext::ensure_visible)
    /// — the table's own viewport follow is handled by `scroll_y`. Rows are not
    /// distinct focusable nodes, so the framework's focus-driven follow never
    /// reveals the selected row in an outer scroller. Populated by each widget's
    /// `place_children`.
    pub body_bounds: Rc<std::cell::Cell<teksilo_canvas::Rect>>,
    /// Row geometry (uniform / exact / auto-measure) — drives the
    /// PageUp/PageDown focus-row math.
    pub row_metrics: SharedRowMetrics,
    pub tab_traversal: TabTraversal,
    pub editing_cell: Signal<Option<(usize, usize)>>,
    /// `(row, col_id)` → invoke user's edit hook. The closure resolves
    /// `col_id` from a display position; we keep it generic over
    /// `&str` so the keyboard module doesn't need a `Column<T>`
    /// reference.
    pub display_col_to_id: Rc<dyn Fn(usize) -> Option<String>>,
    /// The [`EditTriggers`] in force for the column at a display position —
    /// the view's set, overridden by the column's own, and `NONE` for a
    /// non-editable column. Per column rather than one set for the table
    /// because that is what the caller declares: entering edit mode on a
    /// column whose delegate has no editor would only confuse the focus /
    /// dispatch state, and a tree column usually wants different gestures from
    /// the value columns beside it.
    pub display_col_triggers: Rc<dyn Fn(usize) -> EditTriggers>,
    /// Optional: user callback fired when an edit trigger matches.
    #[allow(clippy::type_complexity)]
    pub on_cell_edit_request:
        Option<Rc<dyn Fn(usize, &str, &mut teksilo_core::widget::EventContext)>>,
    /// Optional: row-activate (Enter) callback.
    #[allow(clippy::type_complexity)]
    pub on_row_activate: Option<Rc<dyn Fn(usize, &mut teksilo_core::widget::EventContext)>>,
    /// Persistent type-ahead buffer (survives the per-keystroke rebuild).
    pub type_ahead: Rc<TypeAheadState>,
    /// Type-ahead label resolver: `row -> Some(text)` for a resident row.
    /// `None` (the option) disables type-ahead.
    #[allow(clippy::type_complexity)]
    pub type_ahead_label: Option<Rc<dyn Fn(usize) -> Option<String>>>,
    /// Reset window for the type-ahead search term.
    pub type_ahead_timeout: Duration,

    /// Resolved column widths in display order — shared with the row/header
    /// layout. Read to compute a display column's horizontal extent for
    /// ensure-column-visible.
    pub column_widths: SharedColumnWidths,
    /// Pane partition (Leading/Middle/Trailing) — pinned columns never
    /// trigger horizontal scrolling, since they're always visible by
    /// definition. Snapshotted at build like `col_count` (a pinning/order
    /// change rebuilds the whole table anyway).
    pub pane_boundaries: PaneBoundaries,
    pub scroll_x: Signal<f32>,
    pub max_scroll_x: Signal<f32>,
    /// Middle-pane viewport width, populated by `place_children` — the
    /// horizontal analogue of `viewport_height`.
    pub middle_viewport_width: Rc<std::cell::Cell<f32>>,
}

/// Build the on_key closure. Captures config by value; the closure is
/// `'static` and ready to slot into a `HandlerSet`.
pub(crate) fn build_key_handler(
    cfg: KeyHandlerConfig,
) -> impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static {
    move |event, ctx: &mut EventContext| {
        let WidgetEvent::KeyDown { key, modifiers, .. } = event else {
            return EventResponse::Ignored;
        };

        let row_count = cfg.navigator.row_count();
        if row_count == 0 || cfg.col_count == 0 {
            return EventResponse::Ignored;
        }
        // The keyboard cursor: the focused cell once the user has navigated or
        // clicked, else the selected row (a table can be handed a selection
        // before it is ever focused — a restored last position, a preselected
        // entry). `None` means "no cursor yet", which is deliberately NOT the
        // same as `Some((0, 0))`: the directional keys below land ON the near
        // end cell rather than stepping past it. Collapsing the two is what made
        // the first ArrowDown skip row 0, the first ArrowUp a dead key
        // (`prev_row(0)` is `None`), and the first ArrowRight skip column 0.
        let raw = cfg.focused_cell.get().or_else(|| {
            cfg.selection
                .as_ref()
                .and_then(|s| s.selected_indices().first().copied())
                .map(|r| (r, 0))
        });
        let cursor = raw.map(|(r, c)| (r.min(row_count - 1), c.min(cfg.col_count - 1)));
        // Anchor for the keys that compute *from* a cell (expand / collapse,
        // paging, Home/End, activation, editing) rather than step in a
        // direction.
        let (row, col) = cursor.unwrap_or((0, 0));
        // Persist the clamp: if the stored focus was out of range (e.g. rows or
        // columns were removed since it was set), write the in-bounds cell back
        // so the focus ring and any later reader don't keep the stale position.
        if raw.is_some() && raw != Some((row, col)) {
            cfg.focused_cell.set(Some((row, col)));
        }

        // Read layout direction live from the dispatch context (a runtime
        // locale switch dirties the tree but does not rebuild, so a
        // build-time capture would go stale).
        let rtl = ctx.is_rtl();

        // Tree-aware collapse / expand (flat impls are no-ops, so this is
        // safe to evaluate eagerly). The keys follow the visual chevron:
        // under LTR the collapsed chevron points right (ArrowRight
        // expands, ArrowLeft collapses); under RTL it points left, so the
        // two arrows swap.
        let on_tree_column = col == cfg.tree_column_display_pos;
        let is_collapse_key = if rtl {
            matches!(key, Key::ArrowRight)
        } else {
            matches!(key, Key::ArrowLeft)
        };
        let is_expand_key = if rtl {
            matches!(key, Key::ArrowLeft)
        } else {
            matches!(key, Key::ArrowRight)
        };
        if is_collapse_key && on_tree_column && cfg.navigator.is_expanded(row) {
            cfg.navigator.toggle_expanded(row);
            return EventResponse::Handled;
        }
        if is_expand_key
            && on_tree_column
            && cfg.navigator.has_children(row)
            && !cfg.navigator.is_expanded(row)
        {
            cfg.navigator.toggle_expanded(row);
            return EventResponse::Handled;
        }

        let viewport_h = cfg.viewport_height.get();

        // Each directional key, with NO cursor yet, lands ON the end cell it
        // would have entered from — it does not step past it (see `cursor`
        // above, and the same rule in `ListView` / `TreeView` / `GridView`).
        // `first_row` / `last_row` (not raw 0 / row_count-1) so a hierarchical
        // navigator — `TreeTableView` plugs its own in here — enters at a row
        // that is actually visible.
        let new_pos: Option<(usize, usize)> = match key {
            Key::ArrowUp => match cursor {
                None => cfg.navigator.last_row().map(|r| (r, col)),
                Some(_) => cfg.navigator.prev_row(row).map(|r| (r, col)),
            },
            Key::ArrowDown => match cursor {
                None => cfg.navigator.first_row().map(|r| (r, col)),
                Some(_) => cfg.navigator.next_row(row).map(|r| (r, col)),
            },
            // Visual-left moves to a higher display index under RTL
            // (columns run right-to-left), so the two arrows swap their
            // index delta. The clamps stay tied to the physical edge each
            // arrow points at. Column 0 is the leading column in both
            // directions, so a cursor-less entry lands on the column the key
            // points *away* from: the "next" key on the first column, the
            // "previous" key on the last.
            Key::ArrowLeft => {
                if rtl {
                    match cursor {
                        None => Some((row, 0)),
                        Some(_) => (col + 1 < cfg.col_count).then_some((row, col + 1)),
                    }
                } else {
                    match cursor {
                        None => Some((row, cfg.col_count - 1)),
                        // `.then` (lazy) — `col - 1` must not be evaluated at col 0.
                        Some(_) => (col > 0).then(|| (row, col - 1)),
                    }
                }
            }
            Key::ArrowRight => {
                if rtl {
                    match cursor {
                        None => Some((row, cfg.col_count - 1)),
                        Some(_) => (col > 0).then(|| (row, col - 1)),
                    }
                } else {
                    match cursor {
                        None => Some((row, 0)),
                        Some(_) => (col + 1 < cfg.col_count).then_some((row, col + 1)),
                    }
                }
            }
            // Plain Home / End move within the row; with the accelerator
            // (Ctrl, ⌘ on macOS) they jump to the first / last row of the table.
            Key::Home if !modifiers.command() => Some((row, 0)),
            Key::End if !modifiers.command() => Some((row, cfg.col_count - 1)),
            Key::Home if modifiers.command() => cfg.navigator.first_row().map(|r| (r, 0)),
            Key::End if modifiers.command() => {
                cfg.navigator.last_row().map(|r| (r, cfg.col_count - 1))
            }
            Key::PageUp => {
                // Scroll one page; move focus to the row one viewport
                // above the current row's top (offset-table-driven, so
                // variable heights page by visual distance, not by a
                // fixed row count). Guarantee progress even when a
                // single row is taller than the viewport.
                let new_y = (cfg.scroll_y.get() - viewport_h).max(0.0);
                cfg.scroll_y.set(new_y);
                let r = {
                    let mut m = cfg.row_metrics.borrow_mut();
                    m.resize(row_count);
                    let target_y = (m.row_top(row) - viewport_h).max(0.0);
                    m.row_at(target_y)
                };
                let r = if r == row { row.saturating_sub(1) } else { r };
                Some((r, col))
            }
            Key::PageDown => {
                let new_y = (cfg.scroll_y.get() + viewport_h).min(cfg.max_scroll_y.get());
                cfg.scroll_y.set(new_y);
                let r = {
                    let mut m = cfg.row_metrics.borrow_mut();
                    m.resize(row_count);
                    let target_y = m.row_top(row) + viewport_h;
                    m.row_at(target_y)
                };
                let r = if r == row {
                    (row + 1).min(row_count - 1)
                } else {
                    r.min(row_count - 1)
                };
                Some((r, col))
            }
            // Ctrl+Tab / Ctrl+Shift+Tab escape the cell grid: return Ignored so
            // the framework's focus cycling moves to the next / previous widget.
            // Plain Tab still navigates cells (the `CellsThenRows` trap), but
            // this gives keyboard users a reliable way out — the same un-trap
            // affordance `RichTextEditor` leaves to OS focus navigation.
            //
            // Literal `ctrl()`, not `command()`: Ctrl+Tab is Ctrl+Tab on macOS
            // too — ⌘⇥ belongs to the application switcher and never reaches an
            // app at all.
            Key::Tab if modifiers.ctrl() => return EventResponse::Ignored,
            Key::Tab => {
                if modifiers.shift() {
                    if col > 0 {
                        Some((row, col - 1))
                    } else if let Some(prev) = cfg.navigator.prev_row(row) {
                        Some((prev, cfg.col_count - 1))
                    } else if cfg.tab_traversal == TabTraversal::OutOfTable {
                        return EventResponse::Ignored;
                    } else {
                        Some((row, col))
                    }
                } else {
                    if col + 1 < cfg.col_count {
                        Some((row, col + 1))
                    } else if let Some(next) = cfg.navigator.next_row(row) {
                        Some((next, 0))
                    } else if cfg.tab_traversal == TabTraversal::OutOfTable {
                        return EventResponse::Ignored;
                    } else {
                        Some((row, col))
                    }
                }
            }
            // Toggles the focused cell/row regardless of Ctrl — this is
            // already "Ctrl+Space toggles the focused row's selection"
            // (the Explorer/Finder pairing with Ctrl+Arrow move-only above):
            // after a Ctrl+Arrow walk away from the selection, Space here
            // toggles just the cursor's current cell.
            Key::Space => {
                toggle_selection(&cfg, row, col);
                cfg.focused_cell.set(Some((row, col)));
                return EventResponse::Handled;
            }
            Key::Enter => {
                if let Some(ref f) = cfg.on_row_activate {
                    f(row, ctx);
                } else {
                    toggle_selection(&cfg, row, col);
                }
                return EventResponse::Handled;
            }
            Key::F2 if (cfg.display_col_triggers)(col).contains(EditTriggers::F2) => {
                if let Some(col_id) = (cfg.display_col_to_id)(col) {
                    cfg.editing_cell.set(Some((row, col)));
                    if let Some(ref f) = cfg.on_cell_edit_request {
                        f(row, &col_id, ctx);
                    }
                    return EventResponse::Handled;
                }
                return EventResponse::Ignored;
            }
            // Type-to-edit. `Key::Character` only fires for non-letter
            // printable chars on this platform; letters arrive as the
            // dedicated `Key::A`..`Key::Z` variants. Match any key that
            // has a printable char form via `Key::to_char()`. Gated on
            // the column's `editable` flag so non-editable columns
            // don't enter edit mode (which would set `editing_cell`
            // without any actual editor in the cell to receive focus
            // and follow-up keystrokes).
            k if (cfg.display_col_triggers)(col).contains(EditTriggers::ANY_KEY)
                && !modifiers.ctrl()
                && !modifiers.alt()
                && !modifiers.super_key()
                && k.to_char().is_some() =>
            {
                if let Some(col_id) = (cfg.display_col_to_id)(col) {
                    cfg.editing_cell.set(Some((row, col)));
                    if let Some(ref f) = cfg.on_cell_edit_request {
                        f(row, &col_id, ctx);
                    }
                    // Don't claim Handled — the typed character should
                    // propagate to the editor that the cell delegate
                    // swaps in.
                    return EventResponse::Ignored;
                }
                return EventResponse::Ignored;
            }
            // Type-ahead: a printable char (no Ctrl/Alt/Super) jumps the
            // focused row to the next row whose label starts with the
            // accumulated term. Reached only when the type-to-edit arm above
            // didn't consume the char (no editor on this column / edit off).
            k if cfg.type_ahead_label.is_some()
                && !modifiers.ctrl()
                && !modifiers.alt()
                && !modifiers.super_key()
                && k.to_char().is_some() =>
            {
                let c = k.to_char().unwrap();
                let label = cfg.type_ahead_label.as_ref().unwrap();
                if let Some(nr) =
                    cfg.type_ahead
                        .search(c, row, row_count, cfg.type_ahead_timeout, |i| label(i))
                {
                    cfg.focused_cell.set(Some((nr, col)));
                    apply_selection_extension(&cfg, nr, col, false);
                    ensure_row_visible(&cfg, nr, row_count, ctx);
                    ensure_col_visible(&cfg, col);
                    return EventResponse::Handled;
                }
                return EventResponse::Ignored;
            }
            // Select all — Ctrl+A, ⌘A on macOS.
            Key::A if modifiers.command() => {
                select_all(&cfg, row_count);
                return EventResponse::Handled;
            }
            Key::Escape => {
                if cfg.editing_cell.get().is_some() {
                    cfg.editing_cell.set(None);
                } else {
                    cfg.focused_cell.set(None);
                }
                return EventResponse::Handled;
            }
            _ => None,
        };

        if let Some((nr, nc)) = new_pos {
            cfg.focused_cell.set(Some((nr, nc)));
            // Explorer/Finder convention: Ctrl+Arrow (no Shift) repositions
            // the keyboard cursor without touching selection — the followed
            // "select the row you land on" behavior is opt-out only via
            // Ctrl, exactly like plain Arrow's select-follow is opt-in via
            // nothing (default) and Shift+Arrow's extend is opt-in via
            // Shift. `Ctrl+Space` (below, `Key::Space`'s `toggle_selection`
            // already ignores modifiers) then toggles just the cell the
            // cursor moved to.
            let is_arrow = matches!(
                key,
                Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight
            );
            // Literal `ctrl()`, macOS included: ⌘↑/⌘↓ already mean something
            // else in a Finder list, and this Explorer-style cursor pair has no
            // ⌘ counterpart — Control keeps it reachable and out of the way.
            let move_cursor_only = is_arrow && modifiers.ctrl() && !modifiers.shift();
            if !move_cursor_only {
                apply_selection_extension(&cfg, nr, nc, modifiers.shift());
            }
            ensure_row_visible(&cfg, nr, row_count, ctx);
            ensure_col_visible(&cfg, nc);
            return EventResponse::Handled;
        }

        EventResponse::Ignored
    }
}

/// Scroll the viewport so `row` is fully visible — a no-op when it
/// already is. Gives `TableView` / `TreeTableView` the same
/// "keyboard-focused row stays on screen" behavior that `ListView` /
/// `TreeView` already have: every Arrow / Home / End / Ctrl+Home/End /
/// Tab move that lands on a new row keeps it visible.
///
/// `PageUp` / `PageDown` already set `scroll_y` to a page boundary and
/// pick a focus row at the new viewport edge, so calling this for them
/// only refines the offset (the chosen row is visible by construction —
/// no extra jump).
fn ensure_row_visible(
    cfg: &KeyHandlerConfig,
    row: usize,
    row_count: usize,
    ctx: &mut EventContext,
) {
    let scroll = cfg.scroll_y.get();
    let new_scroll = {
        let mut m = cfg.row_metrics.borrow_mut();
        m.resize(row_count);
        m.scroll_for_ensure_visible(
            row,
            scroll,
            cfg.viewport_height.get(),
            cfg.max_scroll_y.get(),
        )
    };
    if (new_scroll - scroll).abs() > f32::EPSILON {
        cfg.scroll_y.set(new_scroll);
    }
    // After keeping the row in the table's OWN viewport, chain the reveal to
    // any enclosing scroll area (a form/page the table is embedded in).
    crate::common::row_metrics::chase_row_into_outer_view(
        ctx,
        &cfg.row_metrics,
        cfg.body_bounds.get(),
        row,
        new_scroll,
    );
}

/// Scroll the Middle pane horizontally so `display_col` is fully visible —
/// the horizontal analogue of [`ensure_row_visible`]. A no-op for a
/// Leading/Trailing-pinned column: pinning already guarantees visibility, so
/// the column can never trigger horizontal scrolling. Unlike rows (via
/// `RowMetrics`, virtualized over thousands of entries), the column count is
/// small and already fully resolved in `column_widths`, so a plain linear
/// scan suffices — no shared "ColumnMetrics" abstraction needed.
fn ensure_col_visible(cfg: &KeyHandlerConfig, display_col: usize) {
    let b = cfg.pane_boundaries;
    if display_col < b.leading_count || display_col >= b.middle_end {
        return;
    }
    let widths = cfg.column_widths.borrow();
    let Some(w) = widths.get(display_col).copied() else {
        return;
    };
    // Logical x of `display_col` within the *unscrolled* Middle content
    // strip (offset from the Middle pane's own leading edge) — i.e.
    // `column_logical_x` with `scroll_x = 0`, restricted to the Middle
    // pane's own local space (band_width is irrelevant here since Trailing
    // never enters this branch).
    let x: f32 = widths[b.leading_count..display_col].iter().sum();
    drop(widths);

    let viewport_w = cfg.middle_viewport_width.get();
    let scroll = cfg.scroll_x.get();
    let max = cfg.max_scroll_x.get();
    let new_scroll = if x < scroll {
        x
    } else if x + w > scroll + viewport_w {
        (x + w - viewport_w).max(0.0)
    } else {
        scroll
    }
    .clamp(0.0, max.max(0.0));
    if (new_scroll - scroll).abs() > f32::EPSILON {
        cfg.scroll_x.set(new_scroll);
    }
}

fn toggle_selection(cfg: &KeyHandlerConfig, row: usize, col: usize) {
    match cfg.selection_mode {
        TableSelectionMode::SingleRow | TableSelectionMode::MultiRow => {
            if let Some(ref s) = cfg.selection {
                if s.is_selected(row) {
                    // Toggle off: a Multi selection model can have it
                    // both ways; Single mode replaces with empty.
                    if cfg.selection_mode == TableSelectionMode::MultiRow {
                        s.toggle(row);
                    } else {
                        s.clear();
                    }
                } else {
                    s.select(row);
                }
            }
        }
        TableSelectionMode::SingleCell | TableSelectionMode::MultiCell => {
            if let Some(ref cs) = cfg.cell_selection {
                if cs.is_selected(row, col) && cfg.selection_mode == TableSelectionMode::MultiCell {
                    cs.toggle(row, col);
                } else {
                    cs.select(row, col);
                }
            }
        }
        TableSelectionMode::None => {}
    }
}

fn apply_selection_extension(cfg: &KeyHandlerConfig, row: usize, col: usize, shift: bool) {
    match cfg.selection_mode {
        TableSelectionMode::MultiRow => {
            if let Some(ref s) = cfg.selection {
                if shift && s.mode() == SelectionMode::Multi {
                    s.extend_to(row);
                } else {
                    s.select(row);
                }
            }
        }
        TableSelectionMode::SingleRow => {
            if let Some(ref s) = cfg.selection {
                s.select(row);
            }
        }
        TableSelectionMode::MultiCell => {
            if let Some(ref cs) = cfg.cell_selection {
                if shift {
                    cs.extend_to(row, col);
                } else {
                    cs.select(row, col);
                }
            }
        }
        TableSelectionMode::SingleCell => {
            if let Some(ref cs) = cfg.cell_selection {
                cs.select(row, col);
            }
        }
        TableSelectionMode::None => {}
    }
}

fn select_all(cfg: &KeyHandlerConfig, row_count: usize) {
    match cfg.selection_mode {
        TableSelectionMode::MultiRow => {
            if let Some(ref s) = cfg.selection {
                s.select_all(row_count);
            }
        }
        TableSelectionMode::MultiCell => {
            if let Some(ref cs) = cfg.cell_selection {
                cs.select_all(row_count, cfg.col_count);
            }
        }
        _ => {}
    }
}