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
use super::*;
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
/// Where the table landed on screen last frame, for mouse hit-testing.
/// Recorded by `draw_table`; coordinates are terminal cells.
#[derive(Clone, Default)]
pub(crate) struct TableHit {
/// The header row's y.
pub header_y: u16,
/// First data row's y and how many rows are visible.
pub rows_y: u16,
pub rows_h: u16,
/// Inner x extent (inside the borders).
pub x_min: u16,
pub x_max: u16,
/// Per-column `[start, end)` x ranges plus the `display_headers()` index
/// each range shows (columns can be scrolled out of view horizontally).
pub cols: Vec<(u16, u16, usize)>,
}
impl App {
/// Record the table geometry the renderer just used, so clicks can be
/// mapped back to rows and header columns. `cols` are `[start, end)` x
/// ranges tagged with the `display_headers()` index they show.
pub fn record_table_hit(
&self,
header_y: u16,
rows_y: u16,
rows_h: u16,
x_min: u16,
x_max: u16,
cols: Vec<(u16, u16, usize)>,
) {
*self.table_hit.borrow_mut() = Some(TableHit {
header_y,
rows_y,
rows_h,
x_min,
x_max,
cols,
});
}
/// Whether the run loop should keep terminal mouse capture on right now.
/// Document-style views (YAML/describe, diff, events, logs, help) release
/// capture so click-drag uses the terminal's native text selection —
/// nothing in them uses clicks, and scrolling still works because with
/// reporting off terminals translate the wheel into arrow keys in the
/// alternate screen ("alternate scroll"), which these views all handle.
/// The filter-typing overlays keep the same document on screen, so they
/// release too.
pub fn wants_mouse_capture(&self) -> bool {
!matches!(
self.mode,
Mode::Detail
| Mode::Diff
| Mode::Events
| Mode::Logs
| Mode::Help
| Mode::DocFilter
| Mode::LogFilter
)
}
/// Route a mouse event. The wheel is synthesized into the mode's own
/// up/down keys, so scrolling works identically in every view (table,
/// logs, documents, pickers) without a second navigation code path;
/// clicks are table-specific (select a row, sort by a header).
pub fn handle_mouse(&mut self, m: MouseEvent) -> Result<()> {
match m.kind {
MouseEventKind::ScrollUp => self.wheel(KeyCode::Up),
MouseEventKind::ScrollDown => self.wheel(KeyCode::Down),
MouseEventKind::Down(MouseButton::Left) if self.mode == Mode::Table => {
self.table_click(m.column, m.row);
Ok(())
}
_ => Ok(()),
}
}
/// One wheel notch = three steps, like most list UIs.
fn wheel(&mut self, code: KeyCode) -> Result<()> {
for _ in 0..3 {
self.handle_key(KeyEvent::new(code, KeyModifiers::NONE))?;
}
Ok(())
}
fn table_click(&mut self, x: u16, y: u16) {
let Some(hit) = self.table_hit.borrow().clone() else {
return;
};
if x < hit.x_min || x >= hit.x_max {
return;
}
// Header row: sort by the clicked column (again = flip direction).
if y == hit.header_y {
let Some(&(_, _, idx)) = hit.cols.iter().find(|(s, e, _)| x >= *s && x < *e) else {
return;
};
if self.sort_column == Some(idx) {
self.sort_desc = !self.sort_desc;
} else {
self.sort_column = Some(idx);
self.sort_desc = false;
}
self.invalidate_rows();
let label = self.display_headers().get(idx).cloned().unwrap_or_default();
self.flash = format!(
"sort by {label} {}",
if self.sort_desc {
"↓ desc"
} else {
"↑ asc"
}
);
self.flash_err = false;
return;
}
// Data rows: select what was clicked.
if y >= hit.rows_y && y < hit.rows_y + hit.rows_h {
let idx = self.table_state.offset() + (y - hit.rows_y) as usize;
if idx < self.row_count() {
self.table_state.select(Some(idx));
}
}
}
}