Skip to main content

datui_lib/app/
pointer.rs

1//! The mouse, as shortcuts to keys (nothing here changes what a key does): the wheel
2//! presses arrows, a footer key click presses it, a form row click focuses and presses
3//! Space, a tab click selects it, a table or home click moves the cursor (double click:
4//! Enter), a plot click places the crosshair. Header drag moves a column (`H`/`L`),
5//! dragging the gap after sets width (`<`/`>`); header double click sorts (`[`/`]`), gap
6//! double click fits (`=`); right click opens a cell's key menu. Mouse input is never
7//! held: where a key would wait, it is dropped
8//! ([`crate::app::event_pump::EventPump::terminal_mouse`]). Click targets are recorded while
9//! drawing ([`record`]) by the widgets that lay them out.
10
11use std::cell::RefCell;
12use std::time::{Duration, Instant};
13
14use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
15use ratatui::layout::{Position, Rect};
16
17use crate::app::form::{FieldKind, Form};
18use crate::table::CellHit;
19use crate::{App, InputMode, Overlay};
20
21/// Rows a notch of the wheel moves.
22pub const WHEEL_ROWS: usize = 3;
23
24/// Two clicks on one cell this close together are a double click.
25pub const DOUBLE_CLICK: Duration = Duration::from_millis(500);
26
27/// The mouse events the app acts on: a left press, its drag and release, a right
28/// press, and the wheel. Motion with no button down and the middle button are left
29/// alone.
30pub fn wanted(mouse: &MouseEvent) -> bool {
31    matches!(
32        mouse.kind,
33        MouseEventKind::Down(MouseButton::Left | MouseButton::Right)
34            | MouseEventKind::Drag(MouseButton::Left)
35            | MouseEventKind::Up(MouseButton::Left)
36            | MouseEventKind::ScrollDown
37            | MouseEventKind::ScrollUp
38            | MouseEventKind::ScrollLeft
39            | MouseEventKind::ScrollRight
40    )
41}
42
43/// Ask for presses, releases, drag motion and the wheel, SGR-encoded for wide screens;
44/// not `EnableMouseCapture`, which also streams buttonless motion the app would discard.
45/// Undone by `DisableMouseCapture`.
46pub struct EnableMouse;
47
48impl crossterm::Command for EnableMouse {
49    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
50        f.write_str("\x1b[?1000h\x1b[?1002h\x1b[?1006h")
51    }
52
53    #[cfg(windows)]
54    fn execute_winapi(&self) -> std::io::Result<()> {
55        crossterm::Command::execute_winapi(&crossterm::event::EnableMouseCapture)
56    }
57
58    #[cfg(windows)]
59    fn is_ansi_code_supported(&self) -> bool {
60        crossterm::Command::is_ansi_code_supported(&crossterm::event::EnableMouseCapture)
61    }
62}
63
64/// Take the mouse from the terminal when `on` (`display.mouse`, `--mouse`); off, the
65/// terminal keeps it, and selecting text works as it does anywhere.
66pub fn capture(on: bool, out: &mut impl std::io::Write) {
67    if on {
68        let _ = crossterm::execute!(out, EnableMouse);
69    }
70}
71
72/// A form's field as drawn: the form, by its type, and the field, by name. Kept as
73/// text so a frame's regions need not know every form's field type.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct FieldId {
76    form: &'static str,
77    field: String,
78}
79
80impl FieldId {
81    pub fn of<T: Form>(field: T::Field) -> Self {
82        Self {
83            form: std::any::type_name::<T>(),
84            field: format!("{field:?}"),
85        }
86    }
87
88    /// Focus this field in `form` when it is that form's and shown there. Says what
89    /// the field is and whether the click acts on it too.
90    pub fn focus_in<T: Form>(&self, form: &mut T) -> Option<Clicked> {
91        if self.form != std::any::type_name::<T>() {
92            return None;
93        }
94        let (field, kind) = form
95            .fields()
96            .into_iter()
97            .find(|(f, _)| format!("{f:?}") == self.field)?;
98        let focused_already = form.focused() == field;
99        let acts = !form.list_row(field) || focused_already;
100        form.focus(field).then_some(Clicked { kind, acts })
101    }
102}
103
104/// A field a click focused: its kind, and whether the click acts on it as well (a
105/// list row's first click only focuses it: [`Form::list_row`]).
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct Clicked {
108    pub kind: FieldKind,
109    pub acts: bool,
110}
111
112/// What a region of the frame is, for a click.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum Hit {
115    /// A form's row: a click focuses it, then acts as Space.
116    Field(FieldId),
117    /// One of a choice's values drawn side by side, a tab bar's tab: a click focuses
118    /// the field, when there is one, then steps it there with ← / →.
119    Option {
120        field: Option<FieldId>,
121        index: usize,
122        current: usize,
123    },
124    /// A line of an open picker: its place in the list as narrowed, the cursor's, and
125    /// whether the list takes several. A click moves the cursor there and chooses.
126    PickerItem {
127        visible: usize,
128        selected: usize,
129        multi: bool,
130    },
131    /// An open picker's whole area. While one is open only it takes clicks.
132    Picker,
133    /// A row edited in place, such as a filter: while it is, nothing else takes
134    /// clicks.
135    Editor,
136    /// A line of the analysis tools list.
137    Tool(usize),
138    /// Something a key names, a chart kind's tab (`1`-`6`): a click presses it.
139    Key(KeyEvent),
140    /// A dialog's footer chip: a click presses its key, even while a picker or an
141    /// editor has the other clicks, or a question is up.
142    Chip(KeyEvent),
143    /// A line of the context menu.
144    MenuItem(usize),
145    /// The context menu's frame.
146    Menu,
147    /// A dialog that owns the keys, drawn over what came before: a click outside
148    /// it does nothing, and only what is drawn after it, inside it, takes clicks.
149    Modal,
150}
151
152thread_local! {
153    /// The regions of the frame being drawn; `None` outside one, so a widget drawn on
154    /// its own (a test's buffer) records nothing.
155    static DRAWING: RefCell<Option<Vec<(Rect, Hit)>>> = const { RefCell::new(None) };
156}
157
158/// Say what a region of the frame being drawn is. Later regions lie on top.
159pub fn record(rect: Rect, hit: Hit) {
160    if rect.width == 0 || rect.height == 0 {
161        return;
162    }
163    DRAWING.with_borrow_mut(|drawing| {
164        if let Some(regions) = drawing {
165            regions.push((rect, hit));
166        }
167    });
168}
169
170/// Say that `rect` is form `T`'s row for `field`.
171pub fn record_field<T: Form>(rect: Rect, field: T::Field) {
172    record(rect, Hit::Field(FieldId::of::<T>(field)));
173}
174
175/// Record a line's spans that a click picks among: each `(span, hit)` names a span of
176/// `line`, drawn left-aligned from `area.x` on `area.y`. Their places are summed from
177/// the spans drawn, so they agree with the line on screen.
178pub fn record_spans(area: Rect, line: &ratatui::text::Line, spans: Vec<(usize, Hit)>) {
179    let mut x = area.x;
180    let mut at = Vec::with_capacity(line.spans.len());
181    for span in &line.spans {
182        let w = span.width() as u16;
183        at.push((x, w));
184        x = x.saturating_add(w);
185    }
186    for (i, hit) in spans {
187        let Some(&(x, w)) = at.get(i) else { continue };
188        if x >= area.right() {
189            continue;
190        }
191        record(Rect::new(x, area.y, w.min(area.right() - x), 1), hit);
192    }
193}
194
195/// What `draw` records, as a frame would: for a widget's tests.
196#[cfg(test)]
197pub(crate) fn recording(draw: impl FnOnce()) -> Vec<(Rect, Hit)> {
198    begin_recording();
199    draw();
200    end_recording()
201}
202
203fn begin_recording() {
204    DRAWING.with_borrow_mut(|drawing| *drawing = Some(Vec::new()));
205}
206
207fn end_recording() -> Vec<(Rect, Hit)> {
208    DRAWING.with_borrow_mut(|drawing| drawing.take().unwrap_or_default())
209}
210
211/// What a mouse event comes to.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum Pointer {
214    Nothing,
215    /// Press these keys in order, as if typed, as far as typed keys would act now.
216    Keys(Vec<KeyEvent>),
217    /// Move to what was clicked, then press the key if there is one (a double
218    /// click's Enter).
219    Point(Target, Option<KeyEvent>),
220    /// Focus a form's field, when one is named, then press keys: with `act`, the key
221    /// for what the field is ([`act_key`]; `true` for a right click), else `keys`.
222    Form {
223        field: Option<FieldId>,
224        act: Option<bool>,
225        keys: Vec<KeyEvent>,
226    },
227    /// The analysis tools list: put its cursor on this tool and press Enter.
228    Tool(usize),
229    /// Start setting a column's width by dragging the gap after it.
230    Resize {
231        column: String,
232        x: u16,
233    },
234    /// A width dragged to: set it, as `<` and `>` do.
235    Width {
236        column: String,
237        width: u16,
238    },
239    /// A header let go over another column: move it there, as `H` and `L` do.
240    Drop {
241        column: String,
242        onto: String,
243    },
244    /// Open the context menu on this cell, at this point.
245    Menu(CellHit, Position),
246    /// A line of the context menu: close it and press its key.
247    MenuChoose(usize),
248    /// A click outside the open menu: close it, nothing more.
249    CloseMenu,
250    /// Nothing to press, but what is drawn changed (a header carried over another
251    /// column): a frame is due.
252    Redraw,
253}
254
255/// Where a click or the wheel moves the cursor.
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub enum Target {
258    /// A cell of the table as drawn.
259    Table(CellHit),
260    /// A row of the home list, by its place among those listed.
261    HomeRow(usize),
262    /// The home list's selection, moved this many rows and stopped at the ends: the
263    /// arrows there go round, which a wheel must not.
264    HomeStep(isize),
265    /// The chart's crosshair, on the point drawn nearest this column; the plot takes
266    /// the keys, as `x` gives them to it.
267    ChartColumn(u16),
268}
269
270/// A drag in progress, from a press on the table's header.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub enum Drag {
273    /// A header picked up: the column, and the column it is over now.
274    Move { column: String, over: String },
275    /// The gap after a header: the column, where the press was, and its width then.
276    Resize { column: String, x: u16, width: u16 },
277}
278
279/// What the last frame drew where, the last click, and a drag in progress.
280#[derive(Debug, Default)]
281pub struct Pointing {
282    /// The footer's keys and the key each presses.
283    chips: Vec<(Rect, KeyEvent)>,
284    /// The home list: its area and the row on each of its lines.
285    home_list: Option<(Rect, Vec<Option<usize>>)>,
286    /// Everything else the frame said a click can land on, bottom first.
287    hits: Vec<(Rect, Hit)>,
288    last_click: Option<(u16, u16, Instant)>,
289    drag: Option<Drag>,
290    /// Something was handled after the last frame was painted, so what it shows may
291    /// not be what a click there would now land on.
292    changed: bool,
293}
294
295impl Pointing {
296    /// Something was handled: the frame on screen may be out of date.
297    pub fn changed(&mut self) {
298        self.changed = true;
299    }
300
301    /// A frame was painted: the screen shows the app as it is.
302    pub fn painted(&mut self) {
303        self.changed = false;
304    }
305
306    /// Whether the frame on screen shows the app as it is, so the regions recorded
307    /// while drawing it are where a click lands.
308    pub fn on_screen(&self) -> bool {
309        !self.changed
310    }
311
312    /// A frame begins: what it does not draw cannot be clicked.
313    pub fn forget_drawn(&mut self) {
314        self.chips.clear();
315        self.home_list = None;
316        self.hits.clear();
317        begin_recording();
318    }
319
320    /// The frame is drawn: keep what it recorded.
321    pub fn drawn(&mut self) {
322        self.hits = end_recording();
323    }
324
325    /// The footer's keys as drawn. A key that is not one key (`↑↓`) presses nothing.
326    pub fn chips_drawn(&mut self, chips: Vec<(Rect, &str)>) {
327        self.chips = chips
328            .into_iter()
329            .filter_map(|(rect, key)| chip_key(key).map(|key| (rect, key)))
330            .collect();
331    }
332
333    /// The home list as drawn: `lines[i]` is the row on the list's line `i`.
334    pub fn home_list_drawn(&mut self, area: Rect, lines: Vec<Option<usize>>) {
335        self.home_list = Some((area, lines));
336    }
337
338    /// The drag in progress, if any.
339    pub fn drag(&self) -> Option<&Drag> {
340        self.drag.as_ref()
341    }
342
343    fn chip_at(&self, at: Position) -> Option<KeyEvent> {
344        self.chips
345            .iter()
346            .find(|(rect, _)| rect.contains(at))
347            .map(|(_, key)| *key)
348    }
349
350    fn home_row_at(&self, at: Position) -> Option<usize> {
351        let (area, lines) = self.home_list.as_ref()?;
352        if !area.contains(at) {
353            return None;
354        }
355        lines.get(usize::from(at.y - area.y)).copied().flatten()
356    }
357
358    /// What the frame drew at `at`, topmost first. Under a dialog, only what it
359    /// drew; while a picker or an editor is open, only a picker's lines.
360    fn hit_at(&self, at: Position) -> Option<&Hit> {
361        let hits = match self.hits.iter().rposition(|(_, hit)| *hit == Hit::Modal) {
362            Some(i) if !self.hits[i].0.contains(at) => return None,
363            Some(i) => &self.hits[i + 1..],
364            None => &self.hits[..],
365        };
366        let open = hits
367            .iter()
368            .any(|(_, hit)| matches!(hit, Hit::Picker | Hit::Editor));
369        hits.iter()
370            .rev()
371            .filter(|(_, hit)| {
372                !open || matches!(hit, Hit::Picker | Hit::PickerItem { .. } | Hit::Chip(_))
373            })
374            .find(|(rect, _)| rect.contains(at))
375            .map(|(_, hit)| hit)
376    }
377
378    /// Whether the frame has an open picker under `at`.
379    fn over_picker(&self, at: Position) -> bool {
380        self.hits
381            .iter()
382            .any(|(rect, hit)| *hit == Hit::Picker && rect.contains(at))
383    }
384
385    /// Whether a click at `at` is the second of a double click. A third click starts
386    /// over, so a triple click is a double click and then a click.
387    fn second_click(&mut self, at: Position, now: Instant) -> bool {
388        let double = self.last_click.is_some_and(|(x, y, then)| {
389            (x, y) == (at.x, at.y) && now.saturating_duration_since(then) <= DOUBLE_CLICK
390        });
391        self.last_click = if double {
392            None
393        } else {
394            Some((at.x, at.y, now))
395        };
396        double
397    }
398}
399
400/// The key a double click on `column`'s header presses: `[` sorts up by it, `]` turns
401/// that sort down, and `]` again takes it away, as the same key again does.
402fn header_sort_key(state: &crate::table::DataTableState, column: &str) -> char {
403    if state.view_sort_columns() == [column] {
404        ']'
405    } else {
406        '['
407    }
408}
409
410/// The key a chip's label names, when it names one key: `Enter`, `^O`, `q`, `F1`.
411pub fn chip_key(label: &str) -> Option<KeyEvent> {
412    let named = |code| Some(KeyEvent::new(code, KeyModifiers::NONE));
413    match label {
414        "Enter" => return named(KeyCode::Enter),
415        "Esc" => return named(KeyCode::Esc),
416        "Tab" => return named(KeyCode::Tab),
417        "Space" => return named(KeyCode::Char(' ')),
418        "F1" => return named(KeyCode::F(1)),
419        _ => {}
420    }
421    let mut chars = label.chars();
422    match (chars.next(), chars.next(), chars.next()) {
423        (Some('^'), Some(c), None) if c.is_ascii_alphabetic() => Some(KeyEvent::new(
424            KeyCode::Char(c.to_ascii_lowercase()),
425            KeyModifiers::CONTROL,
426        )),
427        // As a terminal reports it: an upper-case letter comes with Shift.
428        (Some(c), None, None) if c.is_ascii_uppercase() => {
429            Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::SHIFT))
430        }
431        (Some(c), None, None) if c.is_ascii_graphic() => named(KeyCode::Char(c)),
432        _ => None,
433    }
434}
435
436fn press(code: KeyCode) -> KeyEvent {
437    KeyEvent::new(code, KeyModifiers::NONE)
438}
439
440/// `delta` presses of `forward`, or of `back` when it is negative.
441fn steps(delta: isize, back: KeyCode, forward: KeyCode) -> Vec<KeyEvent> {
442    let code = if delta < 0 { back } else { forward };
443    vec![press(code); delta.unsigned_abs()]
444}
445
446/// The key a click presses on a field it focused: Space acts on it (toggles,
447/// steps, opens, presses); a right click steps a choice back, as ← does, and only
448/// focuses anything else. A text field only takes focus.
449pub fn act_key(kind: FieldKind, back: bool) -> Option<KeyEvent> {
450    match kind {
451        FieldKind::Text | FieldKind::MultilineText => None,
452        FieldKind::Choice | FieldKind::Picker { multi: false } if back => {
453            Some(press(KeyCode::Left))
454        }
455        _ if back => None,
456        _ => Some(press(KeyCode::Char(' '))),
457    }
458}
459
460/// What a click on `hit` does; `back` for a right click.
461fn click_hit(hit: &Hit, back: bool) -> Pointer {
462    match hit {
463        Hit::Field(id) => Pointer::Form {
464            field: Some(id.clone()),
465            act: Some(back),
466            keys: Vec::new(),
467        },
468        Hit::Option {
469            field,
470            index,
471            current,
472        } => Pointer::Form {
473            field: field.clone(),
474            act: None,
475            keys: steps(
476                *index as isize - *current as isize,
477                KeyCode::Left,
478                KeyCode::Right,
479            ),
480        },
481        Hit::PickerItem {
482            visible,
483            selected,
484            multi,
485        } => {
486            let mut keys = steps(
487                *visible as isize - *selected as isize,
488                KeyCode::Up,
489                KeyCode::Down,
490            );
491            keys.push(press(if *multi {
492                KeyCode::Char(' ')
493            } else {
494                KeyCode::Enter
495            }));
496            Pointer::Keys(keys)
497        }
498        Hit::Tool(i) => Pointer::Tool(*i),
499        Hit::Key(key) | Hit::Chip(key) => Pointer::Keys(vec![*key]),
500        Hit::MenuItem(i) => Pointer::MenuChoose(*i),
501        Hit::Picker | Hit::Editor | Hit::Menu | Hit::Modal => Pointer::Nothing,
502    }
503}
504
505impl App {
506    /// What a mouse event means on the screen last drawn.
507    pub fn pointer(&mut self, mouse: &MouseEvent, now: Instant) -> Pointer {
508        let shift = mouse.modifiers.contains(KeyModifiers::SHIFT);
509        // A menu something else has covered since is gone, as for a key.
510        if self.context_menu.is_some() && !self.menu_showing() {
511            self.close_context_menu();
512        }
513        let at = Position {
514            x: mouse.column,
515            y: mouse.row,
516        };
517        match mouse.kind {
518            MouseEventKind::ScrollDown if shift => self.wheel_across(true),
519            MouseEventKind::ScrollUp if shift => self.wheel_across(false),
520            MouseEventKind::ScrollRight => self.wheel_across(true),
521            MouseEventKind::ScrollLeft => self.wheel_across(false),
522            MouseEventKind::ScrollDown => self.wheel(true, at),
523            MouseEventKind::ScrollUp => self.wheel(false, at),
524            MouseEventKind::Down(MouseButton::Left) => {
525                self.pointer.drag = None;
526                self.click(at, now)
527            }
528            MouseEventKind::Down(MouseButton::Right) => {
529                self.pointer.drag = None;
530                self.right_click(at)
531            }
532            MouseEventKind::Drag(MouseButton::Left) => self.drag_to(at),
533            MouseEventKind::Up(MouseButton::Left) => self.release(),
534            _ => Pointer::Nothing,
535        }
536    }
537
538    /// A key was pressed: a header being carried is put back, and its mark goes.
539    pub fn cancel_drag(&mut self) {
540        self.pointer.drag = None;
541    }
542
543    /// Forget the last click and any drag: it was dropped, so the next is not its
544    /// second, and nothing was picked up.
545    pub fn forget_click(&mut self) {
546        self.pointer.last_click = None;
547        self.pointer.drag = None;
548    }
549
550    /// Move the cursor to what a click or the wheel landed on. A press on a header
551    /// picks the column up, for a drag to move.
552    pub fn point(&mut self, target: &Target) {
553        // As a key does: the line about the last action is stale now.
554        self.flash = None;
555        match target {
556            Target::Table(hit) => {
557                if let Some(state) = self.data_table_state.as_mut() {
558                    state.point_at(hit);
559                }
560                if let (None, Some(column)) = (hit.row, &hit.column) {
561                    self.pointer.drag = Some(Drag::Move {
562                        column: column.clone(),
563                        over: column.clone(),
564                    });
565                }
566            }
567            Target::HomeRow(row) => {
568                self.home.status = None;
569                self.home.select(*row);
570            }
571            Target::HomeStep(rows) => {
572                self.home.status = None;
573                self.home.page_selection(*rows);
574            }
575            Target::ChartColumn(column) => {
576                self.chart.modal.plot_focus = true;
577                self.move_crosshair_to(Some(*column));
578            }
579        }
580    }
581
582    /// Put the cursor on the cell a right click landed on, for the menu. Returns
583    /// whether it is there: rows that moved since they were drawn are not the ones
584    /// clicked, and the menu would act on another cell.
585    pub fn point_for_menu(&mut self, hit: &CellHit) -> bool {
586        self.flash = None;
587        self.data_table_state
588            .as_mut()
589            .is_some_and(|state| state.point_at(hit))
590    }
591
592    /// Focus the field a click landed on, in whichever form drew it.
593    pub fn focus_field(&mut self, id: &FieldId) -> Option<Clicked> {
594        self.flash = None;
595        if let Some(kind) = id.focus_in(&mut self.chart.modal) {
596            // The option rows take the keys back from the plot's crosshair.
597            self.chart.modal.plot_focus = false;
598            return Some(kind);
599        }
600        let shown = id
601            .focus_in(&mut self.export_modal)
602            .or_else(|| id.focus_in(&mut self.copy_modal))
603            .or_else(|| id.focus_in(&mut self.chart.export_modal))
604            .or_else(|| id.focus_in(&mut self.pivot_melt_modal))
605            .or_else(|| id.focus_in(&mut self.sort_filter_modal))
606            .or_else(|| id.focus_in(&mut self.view_modal))
607            .or_else(|| {
608                self.column_forms
609                    .combine
610                    .as_mut()
611                    .and_then(|c| id.focus_in(c))
612            })
613            .or_else(|| self.sample.form.as_mut().and_then(|f| id.focus_in(f)));
614        if shown.is_some() {
615            return shown;
616        }
617        let analysis = &mut self.analysis_modal;
618        let kind = analysis
619            .sample_form
620            .as_mut()
621            .and_then(|f| id.focus_in(f))
622            .or_else(|| {
623                analysis
624                    .quality
625                    .expected_form
626                    .as_mut()
627                    .and_then(|f| id.focus_in(f))
628            })
629            .or_else(|| {
630                analysis
631                    .quality
632                    .intent_form
633                    .as_mut()
634                    .and_then(|f| id.focus_in(f))
635            })?;
636        // A form in the result pane takes the keys once the pane has them.
637        analysis.focus = crate::analysis::analysis_modal::AnalysisFocus::Main;
638        Some(kind)
639    }
640
641    /// Put the analysis tools list's cursor on `tool`, the list focused, as Tab and
642    /// the arrows would.
643    pub fn point_at_tool(&mut self, tool: usize) {
644        self.flash = None;
645        self.analysis_modal.focus = crate::analysis::analysis_modal::AnalysisFocus::Sidebar;
646        self.analysis_modal.sidebar_state.select(Some(tool));
647    }
648
649    /// Start a width drag on `column`, from the width it has now.
650    pub fn start_resize(&mut self, column: String, x: u16) {
651        use crate::widgets::column_widths::{UNSEEN_WIDTH, WidthChoice};
652        let width = self
653            .data_table_state
654            .as_ref()
655            .and_then(|state| match state.width_choice(&column) {
656                WidthChoice::Manual(w) => Some(w),
657                _ => state.on_screen_width(&column),
658            })
659            .unwrap_or(UNSEEN_WIDTH);
660        self.pointer.drag = Some(Drag::Resize { column, x, width });
661    }
662
663    /// Set a column's width by hand, as `<` and `>` do, within their bounds.
664    pub fn set_dragged_width(&mut self, column: String, width: u16) {
665        use crate::widgets::column_widths::{MAX_WIDTH, MIN_WIDTH, WidthChoice};
666        self.flash = None;
667        if let Some(state) = self.data_table_state.as_mut() {
668            let width = width.clamp(MIN_WIDTH, MAX_WIDTH);
669            state.set_width_choices([(column, WidthChoice::Manual(width))]);
670        }
671    }
672
673    /// The home screen is up with nothing over it, so its keys go to it.
674    fn home_has_the_keys(&self) -> bool {
675        self.input_mode == InputMode::Home
676            && !self.help_visible()
677            && !self.error_modal.active
678            && !self.confirmation_modal.active
679    }
680
681    /// The chart view is up with nothing over it: no export dialog, Picker, help or
682    /// modal.
683    fn chart_has_the_keys(&self) -> bool {
684        self.overlay == Overlay::Chart
685            && self.chart.modal.picker.is_none()
686            && !self.help_visible()
687            && !self.error_modal.active
688            && !self.confirmation_modal.active
689    }
690
691    /// A dialog that takes every key is over the screen: the help, an error, a
692    /// question. What is drawn under it takes no clicks.
693    fn dialog_over_all(&self) -> bool {
694        self.help_visible() || self.error_modal.active || self.confirmation_modal.active
695    }
696
697    /// The wheel: the arrows of whatever has the keys. Not where ↑↓ walk a text
698    /// field's history, and not in the prompt for a path on the home screen; over an
699    /// open picker, its list.
700    fn wheel(&self, down: bool, at: Position) -> Pointer {
701        let code = if down { KeyCode::Down } else { KeyCode::Up };
702        let arrows = Pointer::Keys(vec![press(code); WHEEL_ROWS]);
703        if self.context_menu.is_some() {
704            return Pointer::Nothing;
705        }
706        if self.home_has_the_keys() {
707            if self.home.path_input_active {
708                return Pointer::Nothing;
709            }
710            let rows = WHEEL_ROWS as isize;
711            return Pointer::Point(Target::HomeStep(if down { rows } else { -rows }), None);
712        }
713        if !self.dialog_over_all() && self.pointer.over_picker(at) {
714            return arrows;
715        }
716        if self.text_field_focused() {
717            return Pointer::Nothing;
718        }
719        arrows
720    }
721
722    /// The wheel across, or Shift with it: the column cursor, at the table only.
723    /// Elsewhere ←→ switch tabs or step rows, which a sideways scroll should not.
724    fn wheel_across(&self, right: bool) -> Pointer {
725        if !self.in_normal_table_view() || self.data_table_state.is_none() {
726            return Pointer::Nothing;
727        }
728        let code = if right { KeyCode::Right } else { KeyCode::Left };
729        Pointer::Keys(vec![press(code)])
730    }
731
732    fn click(&mut self, at: Position, now: Instant) -> Pointer {
733        if self.context_menu.is_some() {
734            self.pointer.last_click = None;
735            return match self.pointer.hit_at(at) {
736                Some(Hit::MenuItem(i)) => Pointer::MenuChoose(*i),
737                Some(Hit::Menu) => Pointer::Nothing,
738                _ => Pointer::CloseMenu,
739            };
740        }
741        if let Some(key) = self.pointer.chip_at(at) {
742            self.pointer.last_click = None;
743            return Pointer::Keys(vec![key]);
744        }
745        // Over help, an error or a question, only its footer's keys take clicks.
746        if self.dialog_over_all() {
747            self.pointer.last_click = None;
748            return match self.pointer.hit_at(at) {
749                Some(Hit::Chip(key)) => Pointer::Keys(vec![*key]),
750                _ => Pointer::Nothing,
751            };
752        }
753        if let Some(hit) = self.pointer.hit_at(at).cloned() {
754            self.pointer.last_click = None;
755            return click_hit(&hit, false);
756        }
757        let double = self.pointer.second_click(at, now);
758        let enter = double.then(|| press(KeyCode::Enter));
759        if self.home_has_the_keys() {
760            return match self.pointer.home_row_at(at) {
761                Some(row) if !self.home.path_input_active => {
762                    Pointer::Point(Target::HomeRow(row), enter)
763                }
764                _ => Pointer::Nothing,
765            };
766        }
767        if self.chart_has_the_keys()
768            && self.chart.modal.has_crosshair()
769            && self
770                .chart
771                .modal
772                .plot
773                .is_some_and(|plot| plot.graph.contains(at))
774        {
775            return Pointer::Point(Target::ChartColumn(at.x), None);
776        }
777        if self.in_normal_table_view()
778            && let Some(state) = self.data_table_state.as_ref()
779        {
780            if let Some(column) = state.drawn_edge(at.x, at.y) {
781                // A double click on the gap fits the column to its rows, as `=` does
782                // on the cursor's column; the first click started a drag that did not
783                // move.
784                if double {
785                    let hit = CellHit {
786                        row: None,
787                        column: Some(column),
788                    };
789                    return Pointer::Point(Target::Table(hit), Some(press(KeyCode::Char('='))));
790                }
791                return Pointer::Resize { column, x: at.x };
792            }
793            if let Some(hit) = state.drawn_cell(at.x, at.y) {
794                // Enter acts on a row. A double click on a header sorts by it, as
795                // `[` and `]` do: up, then down, then off.
796                let key = match (&hit.row, &hit.column) {
797                    (Some(_), _) => enter,
798                    (None, Some(column)) if double => {
799                        Some(press(KeyCode::Char(header_sort_key(state, column))))
800                    }
801                    (None, _) => None,
802                };
803                return Pointer::Point(Target::Table(hit), key);
804            }
805        }
806        Pointer::Nothing
807    }
808
809    /// A right click: on a table cell, the context menu; on a form's choice, a step
810    /// back. Anywhere else, nothing.
811    fn right_click(&mut self, at: Position) -> Pointer {
812        self.pointer.last_click = None;
813        if self.context_menu.is_some() {
814            return Pointer::CloseMenu;
815        }
816        if self.dialog_over_all() {
817            return Pointer::Nothing;
818        }
819        if let Some(hit) = self.pointer.hit_at(at).cloned() {
820            return match hit {
821                Hit::Field(_) => click_hit(&hit, true),
822                _ => Pointer::Nothing,
823            };
824        }
825        if self.in_normal_table_view()
826            && let Some(hit) = self
827                .data_table_state
828                .as_ref()
829                .and_then(|state| state.drawn_cell(at.x, at.y))
830            && hit.row.is_some()
831            && hit.column.is_some()
832        {
833            return Pointer::Menu(hit, at);
834        }
835        Pointer::Nothing
836    }
837
838    /// The pointer moved with the button down: a header carried over the columns, or
839    /// the gap after one pulled to a width.
840    fn drag_to(&mut self, at: Position) -> Pointer {
841        match self.pointer.drag.clone() {
842            Some(Drag::Move { column, .. }) => {
843                // Off the table's columns it is over itself: let go there, nothing
844                // moves.
845                let over = self
846                    .data_table_state
847                    .as_ref()
848                    .and_then(|state| state.drawn_cell(at.x, at.y))
849                    .and_then(|hit| hit.column)
850                    .unwrap_or_else(|| column.clone());
851                if !matches!(&self.pointer.drag, Some(Drag::Move { over: was, .. }) if *was == over)
852                {
853                    // The drop mark moves: a frame is due.
854                    self.pointer.drag = Some(Drag::Move { column, over });
855                    return Pointer::Redraw;
856                }
857                Pointer::Nothing
858            }
859            Some(Drag::Resize { column, x, width }) => {
860                let width = (i32::from(width) + i32::from(at.x) - i32::from(x)).max(0);
861                Pointer::Width {
862                    column,
863                    width: u16::try_from(width).unwrap_or(u16::MAX),
864                }
865            }
866            None => Pointer::Nothing,
867        }
868    }
869
870    /// The button let go: a header carried to another column moves there.
871    fn release(&mut self) -> Pointer {
872        match self.pointer.drag.take() {
873            Some(Drag::Move { column, over }) if column != over => {
874                Pointer::Drop { column, onto: over }
875            }
876            // Back where it was picked up: the mark it may have shown goes.
877            Some(Drag::Move { .. }) => Pointer::Redraw,
878            _ => Pointer::Nothing,
879        }
880    }
881}
882
883#[cfg(test)]
884mod tests;