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
//! Pointer input for the `/` command palette and the path palette (`@`
//! mentions, or shell-mode Tab completion).
//!
//! The palette renderer reports which of its lines hold which match through
//! [`PaletteFrame::hits`](super::palette::PaletteFrame). A click highlights the
//! row under it; a double click completes it the way Tab does. It never submits
//! the way Enter can, so a stray double click cannot run a command like
//! `/clear`. The wheel over the rows steps the highlight instead of scrolling
//! the transcript.
use std::time::Instant;
use crossterm::event::{MouseButton, MouseEventKind};
use ratatui::layout::{Position, Rect};
use super::{
composer_pointer::{composer_target_at, ChoiceClick, ComposerHit},
palette::{ActivePalette, PaletteRow},
App,
};
impl App {
/// Handles a pointer event over the palette painted into `area` (the
/// frame's `layout.commands`) with row spans `hits`. `false` when no
/// palette row owns the event, so the screen handler keeps its behavior.
pub(super) fn handle_palette_mouse(
&mut self,
kind: MouseEventKind,
hits: &[ComposerHit<PaletteRow>],
area: Rect,
column: u16,
row: u16,
now: Instant,
) -> bool {
match kind {
MouseEventKind::Down(MouseButton::Left) => {
let Some(target) = composer_target_at(hits, area, /*start*/ 0, column, row) else {
return false;
};
self.click_palette_row(target, column, row, now)
}
MouseEventKind::ScrollUp => self.wheel_palette(area, column, row, /*delta*/ -1),
MouseEventKind::ScrollDown => self.wheel_palette(area, column, row, /*delta*/ 1),
MouseEventKind::Down(_)
| MouseEventKind::Up(_)
| MouseEventKind::Drag(_)
| MouseEventKind::Moved
| MouseEventKind::ScrollLeft
| MouseEventKind::ScrollRight => false,
}
}
/// Highlights, or on a double click completes, the palette row `target`.
/// A hit that no longer names a live row of the same palette is ignored
/// and falls through.
fn click_palette_row(
&mut self,
target: PaletteRow,
column: u16,
row: u16,
now: Instant,
) -> bool {
let palette = self.active_palette();
let live = match (target, &palette) {
(PaletteRow::Command(index), Some(ActivePalette::Command(matches))) => {
index < matches.len()
}
(PaletteRow::File(index), Some(ActivePalette::File(matches))) => index < matches.len(),
(PaletteRow::Command(_) | PaletteRow::File(_), Some(_) | None) => false,
};
if !live {
return false;
}
self.input_ui.clear_selection();
self.history.clear_text_selection();
self.history.set_scrollbar_drag(None);
self.screen_selection = None;
self.clear_rail_pointer_state();
self.input_ui.clear_paste_burst();
self.ctrl_c_streak = 0;
let index = match target {
PaletteRow::Command(index) | PaletteRow::File(index) => index,
};
// The index keeps a double click from pairing two rows that share a
// cell after the scrolled window shifts under the first click.
let click = if self
.input_ui
.register_pointer_click(now, column, row, index)
{
ChoiceClick::Double
} else {
ChoiceClick::Single
};
match (palette, click) {
(Some(ActivePalette::Command(_)), ChoiceClick::Single) => {
// Same explicit pick an arrow key records, so Enter honors it.
self.input_ui.move_command_selection(index);
}
(Some(ActivePalette::Command(matches)), ChoiceClick::Double) => {
self.input_ui.move_command_selection(index);
// Mirrors Tab in `handle_command_palette_key`.
if let Some(choice) = matches.get(index) {
self.complete_command_choice(choice);
self.input_ui.set_command_palette_dismissed(false);
self.clamp_command_selection();
}
}
(Some(ActivePalette::File(_)), ChoiceClick::Single) => {
self.input_ui.set_file_selection(index);
}
(Some(ActivePalette::File(matches)), ChoiceClick::Double) => {
self.input_ui.set_file_selection(index);
if let Some(entry) = matches.get(index) {
// The mouse handler is backend-generic and cannot carry an
// anyhow error, so report it where the user is looking.
if let Err(error) = self.apply_file_palette_selection(&entry) {
self.set_status(format!("could not apply palette selection: {error:#}"));
}
}
}
(None, ChoiceClick::Single | ChoiceClick::Double) => {}
}
true
}
/// Steps the palette highlight one row per wheel notch, clamped at both
/// ends, when the pointer is over the palette. `false` elsewhere, so the
/// transcript scrolls.
fn wheel_palette(&mut self, area: Rect, column: u16, row: u16, delta: isize) -> bool {
if !area.contains(Position { x: column, y: row }) {
return false;
}
match self.active_palette() {
Some(ActivePalette::Command(matches)) => {
let last = matches.len().saturating_sub(1);
let current = self.input_ui.command_selection().min(last);
// An explicit pick, like an arrow key, so Enter honors it.
self.input_ui
.move_command_selection(current.saturating_add_signed(delta).min(last));
true
}
Some(ActivePalette::File(matches)) => {
let last = matches.len().saturating_sub(1);
let current = self.input_ui.file_selection().min(last);
self.input_ui
.set_file_selection(current.saturating_add_signed(delta).min(last));
true
}
None => false,
}
}
}
#[cfg(test)]
#[path = "palette_pointer_tests.rs"]
mod tests;