Skip to main content

datui_lib/
pointer.rs

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