Skip to main content

datui_lib/table/
drawn.rs

1//! Where the last frame drew the table, so a click can be told what it landed on.
2
3use ratatui::layout::Rect;
4
5use super::DataTableState;
6
7/// The table as the last frame drew it: what a click on it lands on.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub(crate) struct DrawnTable {
10    /// The whole table, rail and header included.
11    pub(crate) area: Rect,
12    pub(crate) header: u16,
13    /// The first row drawn and how many under the header.
14    pub(crate) start_row: usize,
15    pub(crate) rows: usize,
16    pub(crate) columns: DrawnColumns,
17}
18
19/// Each column drawn: its cells across, `[from, to)`, and its name.
20pub type DrawnColumns = Vec<(u16, u16, String)>;
21
22/// What a click on the table lands on: the row on screen, counted from the top (none
23/// on the header), and the column (none on the rail or the row numbers).
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct CellHit {
26    pub row: Option<usize>,
27    pub column: Option<String>,
28}
29
30impl DataTableState {
31    /// Forget where the table was drawn: a frame that does not draw it leaves nothing
32    /// there to click.
33    pub fn forget_drawn(&mut self) {
34        self.drawn = None;
35    }
36
37    /// What the cell at `(x, y)` showed in the last frame. `None` off the table, or
38    /// below its last row.
39    pub fn drawn_cell(&self, x: u16, y: u16) -> Option<CellHit> {
40        let drawn = self.drawn.as_ref()?;
41        if !drawn.area.contains(ratatui::layout::Position { x, y }) {
42            return None;
43        }
44        let below_header = usize::from(y - drawn.area.y).checked_sub(usize::from(drawn.header));
45        let row = match below_header {
46            Some(row) if row >= drawn.rows => return None,
47            row => row,
48        };
49        let column = drawn
50            .columns
51            .iter()
52            .find(|(from, to, _)| (*from..*to).contains(&x))
53            .map(|(_, _, name)| name.clone());
54        Some(CellHit { row, column })
55    }
56
57    /// The column whose right edge the header cell at `(x, y)` is: the first cell
58    /// of the gap after a column's last, where a drag resizes it.
59    pub fn drawn_edge(&self, x: u16, y: u16) -> Option<String> {
60        let drawn = self.drawn.as_ref()?;
61        let header = drawn.area.y..drawn.area.y + drawn.header;
62        if !header.contains(&y) || !drawn.area.contains(ratatui::layout::Position { x, y }) {
63            return None;
64        }
65        if let Some(name) = Self::right_edge(drawn, x) {
66            return Some(name);
67        }
68        if drawn
69            .columns
70            .iter()
71            .any(|(from, to, _)| (*from..*to).contains(&x))
72        {
73            return None;
74        }
75        drawn
76            .columns
77            .iter()
78            .find(|(_, to, _)| *to == x)
79            .map(|(_, _, name)| name.clone())
80    }
81
82    /// The edge of a column that reaches the table's right side, filled or cut
83    /// there: no gap follows it, so its last header cell is its edge.
84    fn right_edge(drawn: &DrawnTable, x: u16) -> Option<String> {
85        let (_, to, name) = drawn.columns.iter().max_by_key(|(_, to, _)| *to)?;
86        (*to >= drawn.area.right() && x + 1 == *to).then(|| name.clone())
87    }
88
89    /// The header rows as drawn and each column's cells across, `[from, to)`: where a
90    /// header drag draws its drop mark.
91    pub fn drawn_header(&self) -> Option<(Rect, DrawnColumns)> {
92        let drawn = self.drawn.as_ref()?;
93        Some((
94            Rect {
95                height: drawn.header.min(drawn.area.height),
96                ..drawn.area
97            },
98            drawn.columns.clone(),
99        ))
100    }
101
102    /// Put the cursor on what a click landed on: the row, when the rows drawn are
103    /// still the view's, and the column. Returns whether it is there now, row and
104    /// column both.
105    pub fn point_at(&mut self, hit: &CellHit) -> bool {
106        let mut landed = true;
107        if let Some(row) = hit.row {
108            if let Some(drawn) = self.drawn.as_ref()
109                && drawn.start_row == self.view.start_row
110                && row < drawn.rows
111            {
112                self.table_state.select(Some(row));
113            } else {
114                landed = false;
115            }
116        }
117        if let Some(name) = &hit.column {
118            self.set_current_column(name);
119            landed &= self.current_column() == Some(name.as_str());
120        }
121        landed
122    }
123}