use ratatui::layout::Rect;
use crate::tui::app::TuiApp;
use crate::tui::input::{TuiMouseAction, TuiMouseEvent, TuiMouseEventKind};
use crate::tui::layout;
use crate::tui::model::TuiScreen;
use crate::tui::view::ViewOptions;
pub(crate) fn hit_test(
app: &TuiApp,
_options: ViewOptions,
area: Rect,
mouse: TuiMouseEvent,
) -> Option<TuiMouseAction> {
let layout = layout::frame(area);
let point = (mouse.column, mouse.row);
if matches!(mouse.kind, TuiMouseEventKind::LeftDown)
&& layout::rect_contains(layout.header, point)
{
return hit_header_tab(layout.header, point);
}
if !layout::rect_contains(layout.body, point) {
return None;
}
match mouse.kind {
TuiMouseEventKind::ScrollUp => Some(TuiMouseAction::ScrollUp),
TuiMouseEventKind::ScrollDown => Some(TuiMouseAction::ScrollDown),
TuiMouseEventKind::LeftDown => match app.screen {
TuiScreen::Map => hit_map_row(app, layout.body, point),
TuiScreen::Treemap => hit_treemap_tile(app, layout.body, point),
TuiScreen::Types | TuiScreen::Extensions => {
hit_distribution_row(app, layout.body, point)
}
_ => None,
},
}
}
fn hit_header_tab(area: Rect, point: (u16, u16)) -> Option<TuiMouseAction> {
layout::header_tab_rects(area)
.into_iter()
.find_map(|(rect, screen)| {
layout::rect_contains(rect, point).then_some(TuiMouseAction::SwitchScreen(screen))
})
}
fn hit_map_row(app: &TuiApp, area: Rect, point: (u16, u16)) -> Option<TuiMouseAction> {
let chunks = layout::workbench_body(area);
layout::table_row_at(chunks.primary, point, app.visible_rows().len())
.map(TuiMouseAction::SelectMapRow)
}
fn hit_distribution_row(app: &TuiApp, area: Rect, point: (u16, u16)) -> Option<TuiMouseAction> {
let chunks = layout::workbench_body(area);
layout::table_row_at(chunks.primary, point, app.distribution_rows().len())
.map(TuiMouseAction::SelectDistributionRow)
}
fn hit_treemap_tile(app: &TuiApp, area: Rect, point: (u16, u16)) -> Option<TuiMouseAction> {
let chunks = layout::workbench_body(area);
let tile_area = layout::bordered_inner(chunks.primary);
if !layout::rect_contains(tile_area, point) {
return None;
}
let rows = app.visible_rows();
layout::treemap_tiles(&rows, tile_area)
.into_iter()
.find_map(|tile| {
layout::rect_contains(tile.rect, point)
.then_some(tile.row_index)
.flatten()
.map(TuiMouseAction::SelectMapRow)
})
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use rebecca::core::disk_map::{
DiskMapEntry, DiskMapEntryKind, DiskMapGroup, DiskMapGroupKind, DiskMapMetrics,
DiskMapReport, DiskMapRoot, DiskMapRootStatus,
};
use rebecca::core::disk_session::DiskMapSession;
use rebecca::core::plan::{EstimateProvenance, EstimateSource};
use rebecca::core::scan::ScanBackendKind;
use super::*;
use crate::tui::input::TuiKey;
#[test]
fn hit_test_header_tab_switches_to_treemap() {
let app = test_app();
let action = hit_test(
&app,
view_options(),
Rect::new(0, 0, 100, 30),
mouse_left(18, 0),
);
assert_eq!(
action,
Some(TuiMouseAction::SwitchScreen(TuiScreen::Treemap))
);
}
#[test]
fn hit_test_map_row_selects_visible_row() {
let app = test_app();
let action = hit_test(
&app,
view_options(),
Rect::new(0, 0, 100, 30),
mouse_left(2, 2),
);
assert_eq!(action, Some(TuiMouseAction::SelectMapRow(0)));
}
#[test]
fn hit_test_distribution_row_selects_distribution_row() {
let mut app = test_app();
app.handle_key(TuiKey::Char('x'));
let action = hit_test(
&app,
view_options(),
Rect::new(0, 0, 100, 30),
mouse_left(2, 2),
);
assert_eq!(action, Some(TuiMouseAction::SelectDistributionRow(0)));
}
#[test]
fn hit_test_treemap_tile_selects_map_row() {
let mut app = test_app();
app.handle_key(TuiKey::Char('4'));
let action = hit_test(
&app,
view_options(),
Rect::new(0, 0, 100, 30),
mouse_left(2, 3),
);
assert_eq!(action, Some(TuiMouseAction::SelectMapRow(0)));
}
#[test]
fn hit_test_wheel_moves_active_selection() {
let app = test_app();
let action = hit_test(
&app,
view_options(),
Rect::new(0, 0, 100, 30),
TuiMouseEvent {
column: 2,
row: 3,
kind: TuiMouseEventKind::ScrollDown,
},
);
assert_eq!(action, Some(TuiMouseAction::ScrollDown));
}
fn view_options() -> ViewOptions {
ViewOptions {
width: 100,
visual_bars: true,
color: true,
}
}
fn mouse_left(column: u16, row: u16) -> TuiMouseEvent {
TuiMouseEvent {
column,
row,
kind: TuiMouseEventKind::LeftDown,
}
}
fn test_app() -> TuiApp {
TuiApp::from_session(
DiskMapSession::from_report(test_report()),
ScanBackendKind::PortableRecursive,
100,
)
}
fn test_report() -> DiskMapReport {
let root = PathBuf::from("/tmp");
DiskMapReport {
roots: vec![DiskMapRoot {
path: root.clone(),
status: DiskMapRootStatus::Scanned,
metrics: metrics(100, 2, 1),
estimate_source: EstimateSource::FreshScan,
estimate_provenance: EstimateProvenance::default(),
reason: None,
}],
totals: metrics(100, 2, 1),
top_entries: vec![
DiskMapEntry {
path: root.join("cache"),
root: root.clone(),
kind: DiskMapEntryKind::Directory,
depth: 1,
logical_bytes: 60,
allocated_bytes: None,
unique_logical_bytes: Some(60),
unique_allocated_bytes: None,
files: 1,
directories: 1,
estimate_source: EstimateSource::FreshScan,
estimate_provenance: EstimateProvenance::default(),
cleanup_advice: None,
},
DiskMapEntry {
path: root.join("notes.tmp"),
root,
kind: DiskMapEntryKind::File,
depth: 1,
logical_bytes: 40,
allocated_bytes: None,
unique_logical_bytes: Some(40),
unique_allocated_bytes: None,
files: 1,
directories: 0,
estimate_source: EstimateSource::FreshScan,
estimate_provenance: EstimateProvenance::default(),
cleanup_advice: None,
},
],
groups: vec![DiskMapGroup {
kind: DiskMapGroupKind::Extension,
key: ".tmp".to_string(),
label: ".tmp".to_string(),
metrics: metrics(40, 1, 0),
}],
diagnostic_summary: Default::default(),
diagnostics: Vec::new(),
}
}
fn metrics(logical_bytes: u64, files: u64, directories: u64) -> DiskMapMetrics {
DiskMapMetrics {
logical_bytes,
allocated_bytes: None,
unique_logical_bytes: Some(logical_bytes),
unique_allocated_bytes: None,
files,
directories,
}
}
}