tui_treelistview/state/
hit.rs1use std::hash::Hash;
2
3use ratatui::layout::{Position, Rect};
4use smallvec::SmallVec;
5
6use super::TreeListViewState;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum TreeHitRegion {
11 Header,
12 Row,
13 VerticalScrollbar,
14 HorizontalScrollbar,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum TreeHit<Id> {
20 Header {
21 column: Option<usize>,
22 },
23 Row {
24 id: Id,
25 index: usize,
26 column: Option<usize>,
27 },
28 VerticalScrollbar,
29 HorizontalScrollbar,
30}
31
32impl<Id> TreeHit<Id> {
33 #[must_use]
34 pub const fn region(&self) -> TreeHitRegion {
35 match self {
36 Self::Header { .. } => TreeHitRegion::Header,
37 Self::Row { .. } => TreeHitRegion::Row,
38 Self::VerticalScrollbar => TreeHitRegion::VerticalScrollbar,
39 Self::HorizontalScrollbar => TreeHitRegion::HorizontalScrollbar,
40 }
41 }
42}
43
44#[derive(Clone, Copy, Debug, Default)]
45pub struct ColumnHitBox {
46 pub start: u16,
47 pub width: u16,
48}
49
50#[derive(Clone, Debug, Default)]
51pub struct TreeHitMap {
52 pub table: Rect,
53 pub rows: Rect,
54 pub vertical_scrollbar: Option<Rect>,
55 pub horizontal_scrollbar: Option<Rect>,
56 pub range_start: usize,
57 pub range_end: usize,
58 pub horizontal_offset: u16,
59 pub selection_width: u16,
60 pub columns: SmallVec<[ColumnHitBox; 8]>,
61}
62
63impl<Id: Copy + Eq + Hash> TreeListViewState<Id> {
64 #[must_use]
66 pub fn hit_test(&self, position: Position) -> Option<TreeHit<Id>> {
67 if self
68 .hit_map
69 .vertical_scrollbar
70 .is_some_and(|area| contains(area, position))
71 {
72 return Some(TreeHit::VerticalScrollbar);
73 }
74 if self
75 .hit_map
76 .horizontal_scrollbar
77 .is_some_and(|area| contains(area, position))
78 {
79 return Some(TreeHit::HorizontalScrollbar);
80 }
81 if !contains(self.hit_map.table, position) {
82 return None;
83 }
84
85 let column = self.hit_column(position.x);
86 if position.y < self.hit_map.rows.y {
87 return Some(TreeHit::Header { column });
88 }
89 if !contains(self.hit_map.rows, position) {
90 return None;
91 }
92
93 let row = self
94 .hit_map
95 .range_start
96 .saturating_add(position.y.saturating_sub(self.hit_map.rows.y) as usize);
97 if row >= self.hit_map.range_end {
98 return None;
99 }
100 self.projection.nodes().get(row).map(|node| TreeHit::Row {
101 id: node.id(),
102 index: row,
103 column,
104 })
105 }
106
107 fn hit_column(&self, x: u16) -> Option<usize> {
108 let local_x = x.saturating_sub(self.hit_map.table.x);
109 if local_x < self.hit_map.selection_width {
110 return None;
111 }
112 let virtual_x = local_x.saturating_add(self.hit_map.horizontal_offset);
113 self.hit_map.columns.iter().position(|column| {
114 virtual_x >= column.start && virtual_x < column.start.saturating_add(column.width)
115 })
116 }
117}
118
119const fn contains(area: Rect, position: Position) -> bool {
120 position.x >= area.x
121 && position.x < area.x.saturating_add(area.width)
122 && position.y >= area.y
123 && position.y < area.y.saturating_add(area.height)
124}