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    RowHeader(usize),
77    Cell(usize, usize),
78    Corner,
79    ContextMenuItem(usize),
80    VerticalScrollbar,
81    HorizontalScrollbar,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum ScrollbarAxis {
86    Vertical,
87    Horizontal,
88}
89
90/// `true` if the selection visually highlights the given cell.
91#[must_use]
92pub fn is_cell_selected(sel: &Selection, row: usize, col: usize) -> bool {
93    match *sel {
94        Selection::None => false,
95        Selection::Cell(r, c) => r == row && c == col,
96        Selection::CellRange(r1, c1, r2, c2) => {
97            let (rmin, cmin, rmax, cmax) = (r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2));
98            row >= rmin && row <= rmax && col >= cmin && col <= cmax
99        }
100        Selection::Row(r) => r == row,
101        Selection::RowRange(r1, r2) => {
102            let (rmin, rmax) = (r1.min(r2), r1.max(r2));
103            row >= rmin && row <= rmax
104        }
105        Selection::Column(c) => c == col,
106        Selection::Columns(ref cols) => cols.contains(&col),
107        Selection::Rows(ref rows) => rows.contains(&row),
108        Selection::Cells(ref cells) => cells.contains(&(row, col)),
109    }
110}
111
112#[must_use]
113pub fn is_row_selected(sel: &Selection, row: usize) -> bool {
114    match *sel {
115        Selection::Row(r) => r == row,
116        Selection::RowRange(r1, r2) => {
117            let (rmin, rmax) = (r1.min(r2), r1.max(r2));
118            row >= rmin && row <= rmax
119        }
120        Selection::Rows(ref rows) => rows.contains(&row),
121        _ => false,
122    }
123}
124
125#[must_use]
126pub fn is_column_selected(sel: &Selection, col: usize) -> bool {
127    match *sel {
128        Selection::Column(c) => c == col,
129        Selection::Columns(ref cols) => cols.contains(&col),
130        _ => false,
131    }
132}
133
134/// Convert a screen pointer (in window coordinates) to its corresponding
135/// content-space (i.e. bounds-relative plus scroll offset) coordinates.
136#[must_use]
137pub fn screen_to_content(
138    pos: Point<gpui::Pixels>,
139    bounds_origin: Point<gpui::Pixels>,
140    scroll: Point<gpui::Pixels>,
141) -> (f32, f32) {
142    let sx: f32 = scroll.x.into();
143    let sy: f32 = scroll.y.into();
144    let ox: f32 = bounds_origin.x.into();
145    let oy: f32 = bounds_origin.y.into();
146    let px: f32 = pos.x.into();
147    let py: f32 = pos.y.into();
148    (px - ox + sx, py - oy + sy)
149}
150
151/// Translate an absolute window-space pointer into the grid's OWN coordinate
152/// frame by subtracting the grid's painted `bounds.origin`. Every pointer
153/// value the widget hands to [`GridState`] is normalized through this at the
154/// event boundary, so all stored positions (`click_pos`, `drag_start`,
155/// `last_mouse_pos`, menu/prompt anchors) live in one consistent grid-relative
156/// frame regardless of where the widget is nested in the window. A grid at
157/// window origin (as in the sample app and older tests) is the identity case.
158#[must_use]
159pub fn to_grid_relative(
160    pos: Point<gpui::Pixels>,
161    bounds_origin: Point<gpui::Pixels>,
162) -> Point<gpui::Pixels> {
163    Point {
164        x: pos.x - bounds_origin.x,
165        y: pos.y - bounds_origin.y,
166    }
167}
168
169#[cfg(test)]
170#[allow(
171    clippy::unwrap_used,
172    clippy::expect_used,
173    clippy::field_reassign_with_default
174)]
175mod tests {
176    use super::*;
177    use gpui::{px, Pixels};
178
179    fn p(x: f32, y: f32) -> Point<Pixels> {
180        Point { x: px(x), y: px(y) }
181    }
182
183    #[test]
184    fn normalized_bounds_none_is_none() {
185        assert_eq!(Selection::None.normalized_bounds(), None);
186    }
187
188    #[test]
189    fn normalized_bounds_cell_folds_to_single_point() {
190        assert_eq!(
191            Selection::Cell(2, 3).normalized_bounds(),
192            Some((2, 3, 2, 3))
193        );
194    }
195
196    #[test]
197    fn normalized_bounds_row_spans_all_columns() {
198        let (r0, c0, r1, c1) = Selection::Row(4).normalized_bounds().unwrap();
199        assert_eq!(r0, 4);
200        assert_eq!(r1, 4);
201        assert_eq!(c0, 0);
202        assert_eq!(c1, usize::MAX);
203    }
204
205    #[test]
206    fn normalized_bounds_column_spans_all_rows() {
207        let (r0, c0, r1, c1) = Selection::Column(5).normalized_bounds().unwrap();
208        assert_eq!(r0, 0);
209        assert_eq!(r1, usize::MAX);
210        assert_eq!(c0, 5);
211        assert_eq!(c1, 5);
212    }
213
214    #[test]
215    fn normalized_bounds_cell_range_handles_reversed() {
216        assert_eq!(
217            Selection::CellRange(5, 4, 1, 2).normalized_bounds(),
218            Some((1, 2, 5, 4)),
219        );
220    }
221
222    #[test]
223    fn normalized_bounds_row_range_handles_reversed() {
224        let (r0, _c0, r1, c1) = Selection::RowRange(9, 3).normalized_bounds().unwrap();
225        assert_eq!(r0, 3);
226        assert_eq!(r1, 9);
227        assert_eq!(c1, usize::MAX);
228    }
229
230    #[test]
231    fn is_cell_selected_for_all_variants() {
232        assert!(!is_cell_selected(&Selection::None, 0, 0));
233        assert!(is_cell_selected(&Selection::Cell(2, 3), 2, 3));
234        assert!(!is_cell_selected(&Selection::Cell(2, 3), 3, 2));
235
236        assert!(is_cell_selected(&Selection::CellRange(1, 1, 3, 3), 2, 2));
237        assert!(is_cell_selected(&Selection::CellRange(3, 3, 1, 1), 2, 2));
238        assert!(!is_cell_selected(&Selection::CellRange(1, 1, 3, 3), 4, 4));
239
240        assert!(is_cell_selected(&Selection::Row(2), 2, 0));
241        assert!(is_cell_selected(&Selection::Row(2), 2, 99));
242        assert!(!is_cell_selected(&Selection::Row(2), 3, 0));
243
244        assert!(is_cell_selected(&Selection::RowRange(1, 3), 2, 5));
245        assert!(!is_cell_selected(&Selection::RowRange(1, 3), 4, 5));
246        assert!(is_cell_selected(&Selection::RowRange(3, 1), 2, 0));
247
248        assert!(is_cell_selected(&Selection::Column(5), 0, 5));
249        assert!(is_cell_selected(&Selection::Column(5), 99, 5));
250        assert!(!is_cell_selected(&Selection::Column(5), 0, 4));
251    }
252
253    #[test]
254    fn is_row_selected_only_for_row_and_row_range() {
255        assert!(is_row_selected(&Selection::Row(3), 3));
256        assert!(!is_row_selected(&Selection::Row(3), 4));
257        assert!(is_row_selected(&Selection::RowRange(2, 5), 4));
258        assert!(is_row_selected(&Selection::RowRange(5, 2), 4));
259        assert!(!is_row_selected(&Selection::RowRange(2, 5), 6));
260
261        assert!(!is_row_selected(&Selection::Cell(1, 2), 1));
262        assert!(!is_row_selected(&Selection::CellRange(0, 0, 9, 9), 5));
263        assert!(!is_row_selected(&Selection::Column(0), 5));
264        assert!(!is_row_selected(&Selection::None, 0));
265    }
266
267    #[test]
268    fn is_column_selected_only_for_column_variant() {
269        assert!(is_column_selected(&Selection::Column(7), 7));
270        assert!(!is_column_selected(&Selection::Column(7), 8));
271        assert!(!is_column_selected(&Selection::Row(0), 0));
272        assert!(!is_column_selected(&Selection::None, 0));
273        assert!(!is_column_selected(&Selection::CellRange(0, 2, 9, 2), 2));
274    }
275
276    #[test]
277    fn screen_to_content_applies_origin_and_scroll() {
278        let pos = p(50.0, 60.0);
279        let origin = p(10.0, 20.0);
280        let scroll = p(5.0, 7.0);
281        let (cx, cy) = screen_to_content(pos, origin, scroll);
282        assert_eq!(cx, 45.0);
283        assert_eq!(cy, 47.0);
284    }
285
286    #[test]
287    fn screen_to_content_no_offset() {
288        let (cx, cy) = screen_to_content(p(0.0, 0.0), p(0.0, 0.0), p(0.0, 0.0));
289        assert_eq!(cx, 0.0);
290        assert_eq!(cy, 0.0);
291    }
292
293    #[test]
294    fn screen_to_content_handles_negative_above_origin() {
295        // Above-origin and negative-axis positions happen during drag-scroll
296        // and should not panic.
297        let (_, _) = screen_to_content(p(-30.0, -30.0), p(0.0, 0.0), p(0.0, 0.0));
298    }
299}