Skip to main content

sqlly_datatable/grid/
selection.rs

1//! Pure selection / hit-testing types and helpers. Kept separate from the
2//! stateful widget so paint, input, and copy code can all use the same
3//! predicates without circular dependencies.
4
5use gpui::Point;
6
7/// What is currently selected. Stores display-row indices; after a sort the
8/// "same row" might live at a different position, so callers needing stable
9/// identities should track source rows separately.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum Selection {
12    None,
13    Cell(usize, usize),
14    Row(usize),
15    Column(usize),
16    /// Multiple whole columns. Sorted ascending, deduped, non-empty.
17    Columns(Vec<usize>),
18    /// Inclusive `(r1, c1)` to `(r2, c2)`. Always `r1 <= r2 && c1 <= c2`.
19    CellRange(usize, usize, usize, usize),
20    /// Inclusive `[r1, r2]`.
21    RowRange(usize, usize),
22    /// Multiple whole rows. Sorted ascending, deduped, non-empty.
23    Rows(Vec<usize>),
24    /// Multiple individual cells `(row, col)`. Deduped, non-empty.
25    Cells(Vec<(usize, usize)>),
26}
27
28impl Selection {
29    /// Returns `(min_row, min_col, max_row, max_col)`. `Selection::None`
30    /// returns `None`.
31    #[must_use]
32    pub fn normalized_bounds(&self) -> Option<(usize, usize, usize, usize)> {
33        match *self {
34            Selection::None => None,
35            Selection::Columns(ref cols) => {
36                let min = *cols.iter().min()?;
37                let max = *cols.iter().max()?;
38                Some((0, min, usize::MAX, max))
39            }
40            Selection::Cell(r, c) => Some((r, c, r, c)),
41            Selection::Row(r) => Some((r, 0, r, usize::MAX)),
42            Selection::Column(c) => Some((0, c, usize::MAX, c)),
43            Selection::CellRange(r1, c1, r2, c2) => {
44                Some((r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2)))
45            }
46            Selection::RowRange(r1, r2) => Some((r1.min(r2), 0, r1.max(r2), usize::MAX)),
47            Selection::Rows(ref rows) => {
48                let min = *rows.iter().min()?;
49                let max = *rows.iter().max()?;
50                Some((min, 0, max, usize::MAX))
51            }
52            Selection::Cells(ref cells) => {
53                let rmin = cells.iter().map(|&(r, _)| r).min()?;
54                let rmax = cells.iter().map(|&(r, _)| r).max()?;
55                let cmin = cells.iter().map(|&(_, c)| c).min()?;
56                let cmax = cells.iter().map(|&(_, c)| c).max()?;
57                Some((rmin, cmin, rmax, cmax))
58            }
59        }
60    }
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64pub enum SortDirection {
65    Ascending,
66    Descending,
67}
68
69/// What a mouse hit-test resolved to.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum HitResult {
72    None,
73    ColumnHeader(usize),
74    SortButton(usize),
75    ColumnBorder(usize),
76    /// Expandable section header for a grouped flat-grid row set.
77    GroupHeader(usize),
78    RowHeader(usize),
79    Cell(usize, usize),
80    Corner,
81    ContextMenuItem(usize),
82    VerticalScrollbar,
83    HorizontalScrollbar,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum ScrollbarAxis {
88    Vertical,
89    Horizontal,
90}
91
92/// `true` if the selection visually highlights the given cell.
93#[must_use]
94pub fn is_cell_selected(sel: &Selection, row: usize, col: usize) -> bool {
95    match *sel {
96        Selection::None => false,
97        Selection::Cell(r, c) => r == row && c == col,
98        Selection::CellRange(r1, c1, r2, c2) => {
99            let (rmin, cmin, rmax, cmax) = (r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2));
100            row >= rmin && row <= rmax && col >= cmin && col <= cmax
101        }
102        Selection::Row(r) => r == row,
103        Selection::RowRange(r1, r2) => {
104            let (rmin, rmax) = (r1.min(r2), r1.max(r2));
105            row >= rmin && row <= rmax
106        }
107        Selection::Column(c) => c == col,
108        Selection::Columns(ref cols) => cols.contains(&col),
109        Selection::Rows(ref rows) => rows.contains(&row),
110        Selection::Cells(ref cells) => cells.contains(&(row, col)),
111    }
112}
113
114#[must_use]
115pub fn is_row_selected(sel: &Selection, row: usize) -> bool {
116    match *sel {
117        Selection::Row(r) => r == row,
118        Selection::RowRange(r1, r2) => {
119            let (rmin, rmax) = (r1.min(r2), r1.max(r2));
120            row >= rmin && row <= rmax
121        }
122        Selection::Rows(ref rows) => rows.contains(&row),
123        _ => false,
124    }
125}
126
127#[must_use]
128pub fn is_column_selected(sel: &Selection, col: usize) -> bool {
129    match *sel {
130        Selection::Column(c) => c == col,
131        Selection::Columns(ref cols) => cols.contains(&col),
132        _ => false,
133    }
134}
135
136/// Convert a screen pointer (in window coordinates) to its corresponding
137/// content-space (i.e. bounds-relative plus scroll offset) coordinates.
138#[must_use]
139pub fn screen_to_content(
140    pos: Point<gpui::Pixels>,
141    bounds_origin: Point<gpui::Pixels>,
142    scroll: Point<gpui::Pixels>,
143) -> (f32, f32) {
144    let sx: f32 = scroll.x.into();
145    let sy: f32 = scroll.y.into();
146    let ox: f32 = bounds_origin.x.into();
147    let oy: f32 = bounds_origin.y.into();
148    let px: f32 = pos.x.into();
149    let py: f32 = pos.y.into();
150    (px - ox + sx, py - oy + sy)
151}
152
153/// Translate an absolute window-space pointer into the grid's OWN coordinate
154/// frame by subtracting the grid's painted `bounds.origin`. Every pointer
155/// value the widget hands to [`GridState`] is normalized through this at the
156/// event boundary, so all stored positions (`click_pos`, `drag_start`,
157/// `last_mouse_pos`, menu/prompt anchors) live in one consistent grid-relative
158/// frame regardless of where the widget is nested in the window. A grid at
159/// window origin (as in the sample app and older tests) is the identity case.
160#[must_use]
161pub fn to_grid_relative(
162    pos: Point<gpui::Pixels>,
163    bounds_origin: Point<gpui::Pixels>,
164) -> Point<gpui::Pixels> {
165    Point {
166        x: pos.x - bounds_origin.x,
167        y: pos.y - bounds_origin.y,
168    }
169}
170
171#[cfg(test)]
172#[allow(
173    clippy::unwrap_used,
174    clippy::expect_used,
175    clippy::field_reassign_with_default
176)]
177mod tests {
178    use super::*;
179    use gpui::{px, Pixels};
180
181    fn p(x: f32, y: f32) -> Point<Pixels> {
182        Point { x: px(x), y: px(y) }
183    }
184
185    #[test]
186    fn normalized_bounds_none_is_none() {
187        assert_eq!(Selection::None.normalized_bounds(), None);
188    }
189
190    #[test]
191    fn normalized_bounds_cell_folds_to_single_point() {
192        assert_eq!(
193            Selection::Cell(2, 3).normalized_bounds(),
194            Some((2, 3, 2, 3))
195        );
196    }
197
198    #[test]
199    fn normalized_bounds_row_spans_all_columns() {
200        let (r0, c0, r1, c1) = Selection::Row(4).normalized_bounds().unwrap();
201        assert_eq!(r0, 4);
202        assert_eq!(r1, 4);
203        assert_eq!(c0, 0);
204        assert_eq!(c1, usize::MAX);
205    }
206
207    #[test]
208    fn normalized_bounds_column_spans_all_rows() {
209        let (r0, c0, r1, c1) = Selection::Column(5).normalized_bounds().unwrap();
210        assert_eq!(r0, 0);
211        assert_eq!(r1, usize::MAX);
212        assert_eq!(c0, 5);
213        assert_eq!(c1, 5);
214    }
215
216    #[test]
217    fn normalized_bounds_cell_range_handles_reversed() {
218        assert_eq!(
219            Selection::CellRange(5, 4, 1, 2).normalized_bounds(),
220            Some((1, 2, 5, 4)),
221        );
222    }
223
224    #[test]
225    fn normalized_bounds_row_range_handles_reversed() {
226        let (r0, _c0, r1, c1) = Selection::RowRange(9, 3).normalized_bounds().unwrap();
227        assert_eq!(r0, 3);
228        assert_eq!(r1, 9);
229        assert_eq!(c1, usize::MAX);
230    }
231
232    #[test]
233    fn is_cell_selected_for_all_variants() {
234        assert!(!is_cell_selected(&Selection::None, 0, 0));
235        assert!(is_cell_selected(&Selection::Cell(2, 3), 2, 3));
236        assert!(!is_cell_selected(&Selection::Cell(2, 3), 3, 2));
237
238        assert!(is_cell_selected(&Selection::CellRange(1, 1, 3, 3), 2, 2));
239        assert!(is_cell_selected(&Selection::CellRange(3, 3, 1, 1), 2, 2));
240        assert!(!is_cell_selected(&Selection::CellRange(1, 1, 3, 3), 4, 4));
241
242        assert!(is_cell_selected(&Selection::Row(2), 2, 0));
243        assert!(is_cell_selected(&Selection::Row(2), 2, 99));
244        assert!(!is_cell_selected(&Selection::Row(2), 3, 0));
245
246        assert!(is_cell_selected(&Selection::RowRange(1, 3), 2, 5));
247        assert!(!is_cell_selected(&Selection::RowRange(1, 3), 4, 5));
248        assert!(is_cell_selected(&Selection::RowRange(3, 1), 2, 0));
249
250        assert!(is_cell_selected(&Selection::Column(5), 0, 5));
251        assert!(is_cell_selected(&Selection::Column(5), 99, 5));
252        assert!(!is_cell_selected(&Selection::Column(5), 0, 4));
253    }
254
255    #[test]
256    fn is_row_selected_only_for_row_and_row_range() {
257        assert!(is_row_selected(&Selection::Row(3), 3));
258        assert!(!is_row_selected(&Selection::Row(3), 4));
259        assert!(is_row_selected(&Selection::RowRange(2, 5), 4));
260        assert!(is_row_selected(&Selection::RowRange(5, 2), 4));
261        assert!(!is_row_selected(&Selection::RowRange(2, 5), 6));
262
263        assert!(!is_row_selected(&Selection::Cell(1, 2), 1));
264        assert!(!is_row_selected(&Selection::CellRange(0, 0, 9, 9), 5));
265        assert!(!is_row_selected(&Selection::Column(0), 5));
266        assert!(!is_row_selected(&Selection::None, 0));
267    }
268
269    #[test]
270    fn is_column_selected_only_for_column_variant() {
271        assert!(is_column_selected(&Selection::Column(7), 7));
272        assert!(!is_column_selected(&Selection::Column(7), 8));
273        assert!(!is_column_selected(&Selection::Row(0), 0));
274        assert!(!is_column_selected(&Selection::None, 0));
275        assert!(!is_column_selected(&Selection::CellRange(0, 2, 9, 2), 2));
276    }
277
278    #[test]
279    fn screen_to_content_applies_origin_and_scroll() {
280        let pos = p(50.0, 60.0);
281        let origin = p(10.0, 20.0);
282        let scroll = p(5.0, 7.0);
283        let (cx, cy) = screen_to_content(pos, origin, scroll);
284        assert_eq!(cx, 45.0);
285        assert_eq!(cy, 47.0);
286    }
287
288    #[test]
289    fn screen_to_content_no_offset() {
290        let (cx, cy) = screen_to_content(p(0.0, 0.0), p(0.0, 0.0), p(0.0, 0.0));
291        assert_eq!(cx, 0.0);
292        assert_eq!(cy, 0.0);
293    }
294
295    #[test]
296    fn screen_to_content_handles_negative_above_origin() {
297        // Above-origin and negative-axis positions happen during drag-scroll
298        // and should not panic.
299        let (_, _) = screen_to_content(p(-30.0, -30.0), p(0.0, 0.0), p(0.0, 0.0));
300    }
301}