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
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, aligned with `display_headers()`.
pub cols: Vec<(u16, u16)>,
}
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 aligned with `display_headers()`.
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)>,
) {
*self.table_hit.borrow_mut() = Some(TableHit {
header_y,
rows_y,
rows_h,
x_min,
x_max,
cols,
});
}
/// 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().position(|(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));
}
}
}
}