Skip to main content

oxiui_table/
selection.rs

1//! Row selection model supporting single, multi, and range selection.
2
3use std::collections::HashSet;
4
5/// The selection mode of a table.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum SelectionMode {
8    /// Selection disabled.
9    None,
10    /// At most one row selected at a time.
11    Single,
12    /// Any number of rows selectable.
13    Multi,
14}
15
16/// Tracks the set of selected row indices and an anchor for range selection.
17#[derive(Clone, Debug)]
18pub struct SelectionModel {
19    mode: SelectionMode,
20    selected: HashSet<usize>,
21    /// Anchor row for shift-range selection (last primary click).
22    anchor: Option<usize>,
23}
24
25impl SelectionModel {
26    /// Create an empty selection model with the given mode.
27    pub fn new(mode: SelectionMode) -> Self {
28        Self {
29            mode,
30            selected: HashSet::new(),
31            anchor: None,
32        }
33    }
34
35    /// The selection mode.
36    pub fn mode(&self) -> SelectionMode {
37        self.mode
38    }
39
40    /// Number of selected rows.
41    pub fn len(&self) -> usize {
42        self.selected.len()
43    }
44
45    /// Returns `true` if nothing is selected.
46    pub fn is_empty(&self) -> bool {
47        self.selected.is_empty()
48    }
49
50    /// Returns `true` if row `index` is selected.
51    pub fn is_selected(&self, index: usize) -> bool {
52        self.selected.contains(&index)
53    }
54
55    /// Selected indices in ascending order.
56    pub fn selected_sorted(&self) -> Vec<usize> {
57        let mut v: Vec<usize> = self.selected.iter().copied().collect();
58        v.sort_unstable();
59        v
60    }
61
62    /// Clear the entire selection.
63    pub fn clear(&mut self) {
64        self.selected.clear();
65        self.anchor = None;
66    }
67
68    /// Handle a plain click on `index`: selects only that row (and sets the
69    /// range anchor). No-op in [`SelectionMode::None`].
70    pub fn click(&mut self, index: usize) {
71        if self.mode == SelectionMode::None {
72            return;
73        }
74        self.selected.clear();
75        self.selected.insert(index);
76        self.anchor = Some(index);
77    }
78
79    /// Handle a ctrl/cmd-click on `index`: toggles that row's membership without
80    /// affecting others. In [`SelectionMode::Single`] it behaves like a plain
81    /// click; in [`SelectionMode::None`] it is a no-op.
82    pub fn ctrl_click(&mut self, index: usize) {
83        match self.mode {
84            SelectionMode::None => {}
85            SelectionMode::Single => self.click(index),
86            SelectionMode::Multi => {
87                if !self.selected.remove(&index) {
88                    self.selected.insert(index);
89                }
90                self.anchor = Some(index);
91            }
92        }
93    }
94
95    /// Handle a shift-click on `index`: selects the contiguous range from the
96    /// current anchor to `index` (inclusive). Falls back to a plain click if
97    /// there is no anchor or the mode is not multi.
98    pub fn shift_click(&mut self, index: usize) {
99        match (self.mode, self.anchor) {
100            (SelectionMode::Multi, Some(anchor)) => {
101                let (lo, hi) = if anchor <= index {
102                    (anchor, index)
103                } else {
104                    (index, anchor)
105                };
106                self.selected.clear();
107                for i in lo..=hi {
108                    self.selected.insert(i);
109                }
110                // Anchor stays put for chained shift-clicks.
111            }
112            (SelectionMode::None, _) => {}
113            _ => self.click(index),
114        }
115    }
116
117    /// Select every row in `0..row_count` (multi mode only).
118    pub fn select_all(&mut self, row_count: usize) {
119        if self.mode != SelectionMode::Multi {
120            return;
121        }
122        self.selected = (0..row_count).collect();
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn single_click_replaces() {
132        let mut s = SelectionModel::new(SelectionMode::Single);
133        s.click(2);
134        assert!(s.is_selected(2));
135        s.click(5);
136        assert!(s.is_selected(5));
137        assert!(!s.is_selected(2));
138        assert_eq!(s.len(), 1);
139    }
140
141    #[test]
142    fn ctrl_click_toggles_in_multi() {
143        let mut s = SelectionModel::new(SelectionMode::Multi);
144        s.click(1);
145        s.ctrl_click(3);
146        s.ctrl_click(5);
147        assert_eq!(s.selected_sorted(), vec![1, 3, 5]);
148        // Toggle 3 off.
149        s.ctrl_click(3);
150        assert_eq!(s.selected_sorted(), vec![1, 5]);
151    }
152
153    #[test]
154    fn shift_click_selects_range() {
155        let mut s = SelectionModel::new(SelectionMode::Multi);
156        s.click(2); // anchor
157        s.shift_click(5);
158        assert_eq!(s.selected_sorted(), vec![2, 3, 4, 5]);
159        // Shift the other direction from the same anchor.
160        s.shift_click(0);
161        assert_eq!(s.selected_sorted(), vec![0, 1, 2]);
162    }
163
164    #[test]
165    fn select_all_and_clear() {
166        let mut s = SelectionModel::new(SelectionMode::Multi);
167        s.select_all(4);
168        assert_eq!(s.len(), 4);
169        s.clear();
170        assert!(s.is_empty());
171    }
172
173    #[test]
174    fn none_mode_is_inert() {
175        let mut s = SelectionModel::new(SelectionMode::None);
176        s.click(1);
177        s.ctrl_click(2);
178        s.shift_click(3);
179        s.select_all(10);
180        assert!(s.is_empty());
181    }
182
183    #[test]
184    fn single_mode_ctrl_click_behaves_like_click() {
185        let mut s = SelectionModel::new(SelectionMode::Single);
186        s.ctrl_click(2);
187        s.ctrl_click(4);
188        assert_eq!(s.selected_sorted(), vec![4]);
189    }
190}