Skip to main content

ashlar_ui/
selection.rs

1//! An app-owned multi-selection for [`VirtualList`](crate::virtual_list::VirtualList) and
2//! [`VirtualTable`](crate::virtual_list::VirtualTable): a cursor, an anchor and a set of rows.
3//!
4//! Keep it in the app's view state (it serializes) and pass it with `.selection(&mut sel)`.
5//! Click selects one row, Shift-click and Shift+arrows extend from the anchor, ⌘-click (Ctrl-click
6//! off macOS) toggles one row, ⌘A selects every row.
7//!
8//! The set is stored as sorted, disjoint ranges, so "select all" on ten million rows costs one
9//! range, not ten million entries.
10
11use std::ops::Range;
12
13/// A selection of row indices. See the [module docs](self).
14#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15pub struct Selection {
16    /// Sorted, disjoint, non-adjacent, non-empty half-open ranges.
17    ranges: Vec<(usize, usize)>,
18    anchor: Option<usize>,
19    cursor: Option<usize>,
20}
21
22impl Selection {
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Whether `row` is selected (O(log ranges)).
28    pub fn contains(&self, row: usize) -> bool {
29        let i = self.ranges.partition_point(|r| r.1 <= row);
30        self.ranges.get(i).is_some_and(|r| r.0 <= row)
31    }
32    /// Number of selected rows.
33    pub fn len(&self) -> usize {
34        self.ranges.iter().fold(0usize, |n, r| n.saturating_add(r.1.saturating_sub(r.0)))
35    }
36    pub fn is_empty(&self) -> bool {
37        self.ranges.is_empty()
38    }
39    /// The row keyboard navigation moves from (the last row clicked or moved to).
40    pub fn cursor(&self) -> Option<usize> {
41        self.cursor
42    }
43    /// The row Shift ranges start from.
44    pub fn anchor(&self) -> Option<usize> {
45        self.anchor
46    }
47    /// The selected rows as sorted ranges.
48    pub fn ranges(&self) -> impl Iterator<Item = Range<usize>> + '_ {
49        self.ranges.iter().map(|r| r.0..r.1)
50    }
51    /// The selected rows, ascending.
52    pub fn iter(&self) -> impl Iterator<Item = usize> + '_ {
53        self.ranges().flatten()
54    }
55
56    /// Select only `row` (a plain click or arrow key): it becomes the anchor and the cursor.
57    pub fn select_only(&mut self, row: usize) {
58        self.ranges.clear();
59        self.ranges.push((row, row.saturating_add(1)));
60        self.anchor = Some(row);
61        self.cursor = Some(row);
62    }
63    /// Toggle `row` (⌘/Ctrl-click): it becomes the anchor and the cursor.
64    pub fn toggle(&mut self, row: usize) {
65        if self.contains(row) {
66            self.remove(row..row.saturating_add(1));
67        } else {
68            self.insert(row..row.saturating_add(1));
69        }
70        self.anchor = Some(row);
71        self.cursor = Some(row);
72    }
73    /// Select the rows from the anchor to `row` (Shift-click, Shift+arrows), replacing the
74    /// previous selection; the anchor stays, the cursor moves to `row`.
75    pub fn extend_to(&mut self, row: usize) {
76        let anchor = self.anchor.unwrap_or(row);
77        let (lo, hi) = if anchor <= row { (anchor, row) } else { (row, anchor) };
78        self.ranges.clear();
79        self.ranges.push((lo, hi.saturating_add(1)));
80        self.anchor = Some(anchor);
81        self.cursor = Some(row);
82    }
83    /// Select rows `0..rows` (⌘A); the cursor and anchor stay.
84    pub fn select_all(&mut self, rows: usize) {
85        self.ranges.clear();
86        if rows > 0 {
87            self.ranges.push((0, rows));
88        }
89    }
90    /// Deselect everything.
91    pub fn clear(&mut self) {
92        *self = Self::default();
93    }
94    /// Drop rows at or past `rows` (call when the model shrinks).
95    pub fn clamp(&mut self, rows: usize) {
96        self.remove(rows..usize::MAX);
97        self.anchor = self.anchor.filter(|a| *a < rows);
98        self.cursor = self.cursor.filter(|c| *c < rows);
99    }
100
101    /// Add a range of rows.
102    pub fn insert(&mut self, rows: Range<usize>) {
103        if rows.is_empty() {
104            return;
105        }
106        let (mut lo, mut hi) = (rows.start, rows.end);
107        // ranges touching or overlapping [lo, hi) merge into it
108        let first = self.ranges.partition_point(|r| r.1 < lo);
109        let last = self.ranges.partition_point(|r| r.0 <= hi);
110        if let Some(r) = self.ranges.get(first..last) {
111            if let (Some(a), Some(b)) = (r.first(), r.last()) {
112                lo = lo.min(a.0);
113                hi = hi.max(b.1);
114            }
115            self.ranges.splice(first..last, [(lo, hi)]);
116        }
117    }
118    /// Remove a range of rows.
119    pub fn remove(&mut self, rows: Range<usize>) {
120        if rows.is_empty() {
121            return;
122        }
123        let (lo, hi) = (rows.start, rows.end);
124        let first = self.ranges.partition_point(|r| r.1 <= lo);
125        let last = self.ranges.partition_point(|r| r.0 < hi);
126        let Some(hit) = self.ranges.get(first..last) else { return };
127        let mut keep = Vec::with_capacity(2);
128        if let Some(a) = hit.first().filter(|a| a.0 < lo) {
129            keep.push((a.0, lo));
130        }
131        if let Some(b) = hit.last().filter(|b| b.1 > hi) {
132            keep.push((hi, b.1));
133        }
134        self.ranges.splice(first..last, keep);
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn ranges_merge_split_and_count() {
144        let mut s = Selection::new();
145        s.insert(5..8);
146        s.insert(1..3);
147        s.insert(3..5);
148        assert_eq!(s.ranges().collect::<Vec<_>>(), vec![1..8]);
149        s.remove(4..6);
150        assert_eq!(s.ranges().collect::<Vec<_>>(), vec![1..4, 6..8]);
151        assert!(s.contains(3) && !s.contains(4) && s.contains(7) && !s.contains(8));
152        assert_eq!(s.len(), 5);
153        s.toggle(5);
154        assert_eq!(s.ranges().collect::<Vec<_>>(), vec![1..4, 5..8]);
155        s.toggle(5);
156        assert_eq!(s.iter().collect::<Vec<_>>(), vec![1, 2, 3, 6, 7]);
157        s.remove(0..usize::MAX);
158        assert!(s.is_empty());
159    }
160
161    #[test]
162    fn extend_keeps_anchor_and_select_all_is_one_range() {
163        let mut s = Selection::new();
164        s.select_only(10);
165        s.extend_to(7);
166        assert_eq!((s.anchor(), s.cursor(), s.len()), (Some(10), Some(7), 4));
167        s.extend_to(12);
168        assert_eq!(s.ranges().collect::<Vec<_>>(), vec![10..13]);
169        s.select_all(10_000_000);
170        assert_eq!((s.len(), s.ranges().count()), (10_000_000, 1));
171        s.clamp(5);
172        assert_eq!((s.len(), s.anchor(), s.cursor()), (5, None, None));
173        s.insert(usize::MAX - 1..usize::MAX);
174        s.toggle(usize::MAX);
175        let json = serde_json::to_string(&s).unwrap();
176        assert_eq!(serde_json::from_str::<Selection>(&json).unwrap(), s);
177    }
178}