Skip to main content

tui_treelistview/state/
hit.rs

1use std::hash::Hash;
2
3use ratatui::layout::{Position, Rect};
4use smallvec::SmallVec;
5
6use super::TreeListViewState;
7
8/// The region of the latest rendering that contains a coordinate.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum TreeHitRegion {
11    Header,
12    Row,
13    VerticalScrollbar,
14    HorizontalScrollbar,
15}
16
17/// A hit-test result for the most recently rendered layout.
18#[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    /// Resolves a row and column from coordinates in the latest render call.
65    #[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}