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    /// Inclusive `(r1, c1)` to `(r2, c2)`. Always `r1 <= r2 && c1 <= c2`.
17    CellRange(usize, usize, usize, usize),
18    /// Inclusive `[r1, r2]`.
19    RowRange(usize, usize),
20}
21
22impl Selection {
23    /// Returns `(min_row, min_col, max_row, max_col)`. `Selection::None`
24    /// returns `None`.
25    #[must_use]
26    pub fn normalized_bounds(&self) -> Option<(usize, usize, usize, usize)> {
27        match *self {
28            Selection::None => None,
29            Selection::Cell(r, c) => Some((r, c, r, c)),
30            Selection::Row(r) => Some((r, 0, r, usize::MAX)),
31            Selection::Column(c) => Some((0, c, usize::MAX, c)),
32            Selection::CellRange(r1, c1, r2, c2) => {
33                Some((r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2)))
34            }
35            Selection::RowRange(r1, r2) => Some((r1.min(r2), 0, r1.max(r2), usize::MAX)),
36        }
37    }
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41pub enum SortDirection {
42    Ascending,
43    Descending,
44}
45
46/// What a mouse hit-test resolved to.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum HitResult {
49    None,
50    ColumnHeader(usize),
51    SortButton(usize),
52    ColumnBorder(usize),
53    RowHeader(usize),
54    Cell(usize, usize),
55    Corner,
56    ContextMenuItem(usize),
57    VerticalScrollbar,
58    HorizontalScrollbar,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum ScrollbarAxis {
63    Vertical,
64    Horizontal,
65}
66
67/// `true` if the selection visually highlights the given cell.
68#[must_use]
69pub fn is_cell_selected(sel: &Selection, row: usize, col: usize) -> bool {
70    match *sel {
71        Selection::None => false,
72        Selection::Cell(r, c) => r == row && c == col,
73        Selection::CellRange(r1, c1, r2, c2) => {
74            let (rmin, cmin, rmax, cmax) = (r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2));
75            row >= rmin && row <= rmax && col >= cmin && col <= cmax
76        }
77        Selection::Row(r) => r == row,
78        Selection::RowRange(r1, r2) => {
79            let (rmin, rmax) = (r1.min(r2), r1.max(r2));
80            row >= rmin && row <= rmax
81        }
82        Selection::Column(c) => c == col,
83    }
84}
85
86#[must_use]
87pub fn is_row_selected(sel: &Selection, row: usize) -> bool {
88    match *sel {
89        Selection::Row(r) => r == row,
90        Selection::RowRange(r1, r2) => {
91            let (rmin, rmax) = (r1.min(r2), r1.max(r2));
92            row >= rmin && row <= rmax
93        }
94        _ => false,
95    }
96}
97
98#[must_use]
99pub fn is_column_selected(sel: &Selection, col: usize) -> bool {
100    matches!(*sel, Selection::Column(c) if c == col)
101}
102
103/// Convert a screen pointer (in window coordinates) to its corresponding
104/// content-space (i.e. bounds-relative plus scroll offset) coordinates.
105#[must_use]
106pub fn screen_to_content(
107    pos: Point<gpui::Pixels>,
108    bounds_origin: Point<gpui::Pixels>,
109    scroll: Point<gpui::Pixels>,
110) -> (f32, f32) {
111    let sx: f32 = scroll.x.into();
112    let sy: f32 = scroll.y.into();
113    let ox: f32 = bounds_origin.x.into();
114    let oy: f32 = bounds_origin.y.into();
115    let px: f32 = pos.x.into();
116    let py: f32 = pos.y.into();
117    (px - ox + sx, py - oy + sy)
118}
119
120#[cfg(test)]
121#[allow(
122    clippy::unwrap_used,
123    clippy::expect_used,
124    clippy::field_reassign_with_default
125)]
126mod tests {
127    use super::*;
128    use gpui::{px, Pixels};
129
130    fn p(x: f32, y: f32) -> Point<Pixels> {
131        Point { x: px(x), y: px(y) }
132    }
133
134    #[test]
135    fn normalized_bounds_none_is_none() {
136        assert_eq!(Selection::None.normalized_bounds(), None);
137    }
138
139    #[test]
140    fn normalized_bounds_cell_folds_to_single_point() {
141        assert_eq!(
142            Selection::Cell(2, 3).normalized_bounds(),
143            Some((2, 3, 2, 3))
144        );
145    }
146
147    #[test]
148    fn normalized_bounds_row_spans_all_columns() {
149        let (r0, c0, r1, c1) = Selection::Row(4).normalized_bounds().unwrap();
150        assert_eq!(r0, 4);
151        assert_eq!(r1, 4);
152        assert_eq!(c0, 0);
153        assert_eq!(c1, usize::MAX);
154    }
155
156    #[test]
157    fn normalized_bounds_column_spans_all_rows() {
158        let (r0, c0, r1, c1) = Selection::Column(5).normalized_bounds().unwrap();
159        assert_eq!(r0, 0);
160        assert_eq!(r1, usize::MAX);
161        assert_eq!(c0, 5);
162        assert_eq!(c1, 5);
163    }
164
165    #[test]
166    fn normalized_bounds_cell_range_handles_reversed() {
167        assert_eq!(
168            Selection::CellRange(5, 4, 1, 2).normalized_bounds(),
169            Some((1, 2, 5, 4)),
170        );
171    }
172
173    #[test]
174    fn normalized_bounds_row_range_handles_reversed() {
175        let (r0, _c0, r1, c1) = Selection::RowRange(9, 3).normalized_bounds().unwrap();
176        assert_eq!(r0, 3);
177        assert_eq!(r1, 9);
178        assert_eq!(c1, usize::MAX);
179    }
180
181    #[test]
182    fn is_cell_selected_for_all_variants() {
183        assert!(!is_cell_selected(&Selection::None, 0, 0));
184        assert!(is_cell_selected(&Selection::Cell(2, 3), 2, 3));
185        assert!(!is_cell_selected(&Selection::Cell(2, 3), 3, 2));
186
187        assert!(is_cell_selected(&Selection::CellRange(1, 1, 3, 3), 2, 2));
188        assert!(is_cell_selected(&Selection::CellRange(3, 3, 1, 1), 2, 2));
189        assert!(!is_cell_selected(&Selection::CellRange(1, 1, 3, 3), 4, 4));
190
191        assert!(is_cell_selected(&Selection::Row(2), 2, 0));
192        assert!(is_cell_selected(&Selection::Row(2), 2, 99));
193        assert!(!is_cell_selected(&Selection::Row(2), 3, 0));
194
195        assert!(is_cell_selected(&Selection::RowRange(1, 3), 2, 5));
196        assert!(!is_cell_selected(&Selection::RowRange(1, 3), 4, 5));
197        assert!(is_cell_selected(&Selection::RowRange(3, 1), 2, 0));
198
199        assert!(is_cell_selected(&Selection::Column(5), 0, 5));
200        assert!(is_cell_selected(&Selection::Column(5), 99, 5));
201        assert!(!is_cell_selected(&Selection::Column(5), 0, 4));
202    }
203
204    #[test]
205    fn is_row_selected_only_for_row_and_row_range() {
206        assert!(is_row_selected(&Selection::Row(3), 3));
207        assert!(!is_row_selected(&Selection::Row(3), 4));
208        assert!(is_row_selected(&Selection::RowRange(2, 5), 4));
209        assert!(is_row_selected(&Selection::RowRange(5, 2), 4));
210        assert!(!is_row_selected(&Selection::RowRange(2, 5), 6));
211
212        assert!(!is_row_selected(&Selection::Cell(1, 2), 1));
213        assert!(!is_row_selected(&Selection::CellRange(0, 0, 9, 9), 5));
214        assert!(!is_row_selected(&Selection::Column(0), 5));
215        assert!(!is_row_selected(&Selection::None, 0));
216    }
217
218    #[test]
219    fn is_column_selected_only_for_column_variant() {
220        assert!(is_column_selected(&Selection::Column(7), 7));
221        assert!(!is_column_selected(&Selection::Column(7), 8));
222        assert!(!is_column_selected(&Selection::Row(0), 0));
223        assert!(!is_column_selected(&Selection::None, 0));
224        assert!(!is_column_selected(&Selection::CellRange(0, 2, 9, 2), 2));
225    }
226
227    #[test]
228    fn screen_to_content_applies_origin_and_scroll() {
229        let pos = p(50.0, 60.0);
230        let origin = p(10.0, 20.0);
231        let scroll = p(5.0, 7.0);
232        let (cx, cy) = screen_to_content(pos, origin, scroll);
233        assert_eq!(cx, 45.0);
234        assert_eq!(cy, 47.0);
235    }
236
237    #[test]
238    fn screen_to_content_no_offset() {
239        let (cx, cy) = screen_to_content(p(0.0, 0.0), p(0.0, 0.0), p(0.0, 0.0));
240        assert_eq!(cx, 0.0);
241        assert_eq!(cy, 0.0);
242    }
243
244    #[test]
245    fn screen_to_content_handles_negative_above_origin() {
246        // Above-origin and negative-axis positions happen during drag-scroll
247        // and should not panic.
248        let (_, _) = screen_to_content(p(-30.0, -30.0), p(0.0, 0.0), p(0.0, 0.0));
249    }
250}