Skip to main content

leptos_arrow_grid/
selection.rs

1//! Logical selection model for the virtualised grid.
2//!
3//! Tracks selected row indices as `HashSet<u64>`. All operations are pure
4//! (no signals) — the caller wraps in `RwSignal<SelectionState>`.
5
6use std::collections::HashSet;
7
8/// Logical grid selection state.
9#[derive(Clone, Debug, Default)]
10pub struct SelectionState {
11    /// Set of selected absolute row indices.
12    pub selected: HashSet<u64>,
13    /// Anchor for Shift+click range selection.
14    pub anchor: Option<u64>,
15    /// Keyboard cursor position (active row).
16    pub cursor: Option<u64>,
17    /// Whether the user is currently drag-selecting.
18    pub dragging: bool,
19}
20
21impl SelectionState {
22    /// Clear all selection state.
23    pub fn clear(&mut self) {
24        self.selected.clear();
25        self.anchor = None;
26        self.cursor = None;
27        self.dragging = false;
28    }
29
30    /// Select all rows `0..total_rows`.
31    pub fn select_all(&mut self, total_rows: u64) {
32        self.selected = (0..total_rows).collect();
33        self.anchor = Some(0);
34        self.cursor = total_rows.checked_sub(1);
35    }
36
37    /// Check if a row is selected.
38    pub fn is_selected(&self, row: u64) -> bool {
39        self.selected.contains(&row)
40    }
41
42    /// Number of selected rows.
43    pub fn count(&self) -> usize {
44        self.selected.len()
45    }
46
47    /// Handle pointer down on a row.
48    pub fn on_pointer_down(&mut self, row: u64, ctrl: bool, shift: bool, total_rows: u64) {
49        if ctrl {
50            // Toggle single row
51            if !self.selected.remove(&row) {
52                self.selected.insert(row);
53            }
54            self.cursor = Some(row);
55        } else if shift {
56            // Range from anchor to row
57            let from = self.anchor.or(self.cursor).unwrap_or(row);
58            let lo = from.min(row);
59            let hi = from.max(row).min(total_rows.saturating_sub(1));
60            self.selected.clear();
61            for i in lo..=hi {
62                self.selected.insert(i);
63            }
64            self.cursor = Some(row);
65        } else {
66            // Single click — clear and select
67            self.selected.clear();
68            self.selected.insert(row);
69            self.anchor = Some(row);
70            self.cursor = Some(row);
71            self.dragging = true;
72        }
73    }
74
75    /// Handle pointer enter during drag.
76    pub fn on_pointer_enter_drag(&mut self, row: u64, total_rows: u64) {
77        if !self.dragging {
78            return;
79        }
80        let anchor = self.anchor.or(self.cursor).unwrap_or(row);
81        let lo = anchor.min(row);
82        let hi = anchor.max(row).min(total_rows.saturating_sub(1));
83        self.selected.clear();
84        for i in lo..=hi {
85            self.selected.insert(i);
86        }
87        self.cursor = Some(row);
88    }
89
90    /// Handle pointer up — stop dragging.
91    pub fn on_pointer_up(&mut self) {
92        self.dragging = false;
93    }
94
95    /// Handle right-click: if the row is not selected, select only it.
96    /// If already selected, keep selection.
97    pub fn on_context_menu(&mut self, row: u64) {
98        if !self.selected.contains(&row) {
99            self.selected.clear();
100            self.selected.insert(row);
101            self.anchor = Some(row);
102            self.cursor = Some(row);
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn single_click() {
113        let mut s = SelectionState::default();
114        s.on_pointer_down(5, false, false, 100);
115        assert!(s.is_selected(5));
116        assert_eq!(s.count(), 1);
117        assert_eq!(s.anchor, Some(5));
118    }
119
120    #[test]
121    fn ctrl_click_toggle() {
122        let mut s = SelectionState::default();
123        s.on_pointer_down(5, false, false, 100);
124        s.on_pointer_up();
125        s.on_pointer_down(10, true, false, 100);
126        assert!(s.is_selected(5));
127        assert!(s.is_selected(10));
128        assert_eq!(s.count(), 2);
129        // Toggle off
130        s.on_pointer_down(5, true, false, 100);
131        assert!(!s.is_selected(5));
132        assert_eq!(s.count(), 1);
133    }
134
135    #[test]
136    fn shift_click_range() {
137        let mut s = SelectionState::default();
138        s.on_pointer_down(5, false, false, 100);
139        s.on_pointer_up();
140        s.on_pointer_down(10, false, true, 100);
141        assert_eq!(s.count(), 6); // 5,6,7,8,9,10
142        for i in 5..=10 {
143            assert!(s.is_selected(i));
144        }
145    }
146
147    #[test]
148    fn drag_select() {
149        let mut s = SelectionState::default();
150        s.on_pointer_down(5, false, false, 100);
151        s.on_pointer_enter_drag(8, 100);
152        assert_eq!(s.count(), 4); // 5,6,7,8
153        s.on_pointer_up();
154        assert!(!s.dragging);
155    }
156
157    #[test]
158    fn select_all() {
159        let mut s = SelectionState::default();
160        s.select_all(5);
161        assert_eq!(s.count(), 5);
162        assert_eq!(s.anchor, Some(0));
163        assert_eq!(s.cursor, Some(4));
164    }
165
166    #[test]
167    fn context_menu_unselected_drops() {
168        let mut s = SelectionState::default();
169        s.on_pointer_down(5, false, false, 100);
170        s.on_pointer_up();
171        s.on_pointer_down(10, true, false, 100);
172        assert_eq!(s.count(), 2);
173        // Right-click on unselected row 20
174        s.on_context_menu(20);
175        assert_eq!(s.count(), 1);
176        assert!(s.is_selected(20));
177    }
178
179    #[test]
180    fn context_menu_selected_keeps() {
181        let mut s = SelectionState::default();
182        s.on_pointer_down(5, false, false, 100);
183        s.on_pointer_up();
184        s.on_pointer_down(10, true, false, 100);
185        assert_eq!(s.count(), 2);
186        // Right-click on already-selected row 5
187        s.on_context_menu(5);
188        assert_eq!(s.count(), 2);
189    }
190
191    #[test]
192    fn clear() {
193        let mut s = SelectionState::default();
194        s.select_all(100);
195        s.clear();
196        assert_eq!(s.count(), 0);
197        assert!(s.anchor.is_none());
198        assert!(s.cursor.is_none());
199    }
200}