Skip to main content

egui_charts/drawings/services/
selection.rs

1//! Selection Service - manages drawing selection state
2//!
3//! Handles single and multi-select for chart drawings.
4//!
5//! # Features
6//!
7//! - Single click selects, deselects others
8//! - Ctrl/Cmd+click toggles in multi-select mode
9//! - Click on empty area deselects all
10//! - Selected drawings show handles
11
12use std::collections::HashSet;
13
14/// Selection service for managing drawing selection state
15#[derive(Debug, Clone, Default)]
16pub struct SelectionService {
17    /// Currently selected drawing IDs
18    selected: HashSet<usize>,
19    /// Primary selection (most recently clicked)
20    primary: Option<usize>,
21    /// Whether multi-select mode is active
22    multi_select_mode: bool,
23}
24
25impl SelectionService {
26    /// Create a new selection service
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    /// Select a single drawing (deselects all others unless multi-select is active)
32    ///
33    /// Returns true if selection changed.
34    pub fn select(&mut self, id: usize) -> bool {
35        if self.multi_select_mode {
36            // Add to selection
37            let changed = self.selected.insert(id);
38            self.primary = Some(id);
39            changed
40        } else {
41            // Single select mode - replace selection
42            let was_selected = self.selected.contains(&id) && self.selected.len() == 1;
43            self.selected.clear();
44            self.selected.insert(id);
45            self.primary = Some(id);
46            !was_selected
47        }
48    }
49
50    /// Toggle selection of a drawing (for Ctrl+click behavior)
51    ///
52    /// Returns true if selection changed.
53    pub fn toggle(&mut self, id: usize) -> bool {
54        if self.selected.contains(&id) {
55            self.selected.remove(&id);
56            if self.primary == Some(id) {
57                self.primary = self.selected.iter().copied().next();
58            }
59            true
60        } else {
61            self.selected.insert(id);
62            self.primary = Some(id);
63            true
64        }
65    }
66
67    /// Deselect a specific drawing
68    ///
69    /// Returns true if the drawing was selected.
70    pub fn deselect(&mut self, id: usize) -> bool {
71        let removed = self.selected.remove(&id);
72        if self.primary == Some(id) {
73            self.primary = self.selected.iter().copied().next();
74        }
75        removed
76    }
77
78    /// Deselect all drawings
79    ///
80    /// Returns true if any drawings were selected.
81    pub fn deselect_all(&mut self) -> bool {
82        let had_selection = !self.selected.is_empty();
83        self.selected.clear();
84        self.primary = None;
85        had_selection
86    }
87
88    /// Check if a specific drawing is selected
89    #[inline]
90    pub fn is_selected(&self, id: usize) -> bool {
91        self.selected.contains(&id)
92    }
93
94    /// Check if any drawings are selected
95    #[inline]
96    pub fn has_selection(&self) -> bool {
97        !self.selected.is_empty()
98    }
99
100    /// Get the primary (most recently selected) drawing ID
101    #[inline]
102    pub fn primary(&self) -> Option<usize> {
103        self.primary
104    }
105
106    /// Get all selected drawing IDs
107    #[inline]
108    pub fn selected_ids(&self) -> impl Iterator<Item = usize> + '_ {
109        self.selected.iter().copied()
110    }
111
112    /// Get count of selected drawings
113    #[inline]
114    pub fn selection_count(&self) -> usize {
115        self.selected.len()
116    }
117
118    /// Enable/disable multi-select mode
119    pub fn set_multi_select_mode(&mut self, enabled: bool) {
120        self.multi_select_mode = enabled;
121    }
122
123    /// Check if multi-select mode is active
124    #[inline]
125    pub fn is_multi_select_mode(&self) -> bool {
126        self.multi_select_mode
127    }
128
129    /// Select all drawings from a list of IDs
130    pub fn select_all(&mut self, ids: impl IntoIterator<Item = usize>) {
131        for id in ids {
132            self.selected.insert(id);
133            if self.primary.is_none() {
134                self.primary = Some(id);
135            }
136        }
137    }
138
139    /// Remove selection for drawings that no longer exist
140    ///
141    /// Call this after deleting drawings to clean up stale selections.
142    pub fn prune(&mut self, existing_ids: &HashSet<usize>) {
143        self.selected.retain(|id| existing_ids.contains(id));
144        if let Some(p) = self.primary
145            && !existing_ids.contains(&p)
146        {
147            self.primary = self.selected.iter().copied().next();
148        }
149    }
150
151    /// Get single selected drawing ID (legacy compatibility)
152    ///
153    /// Returns None if multiple drawings are selected.
154    pub fn single_selection(&self) -> Option<usize> {
155        if self.selected.len() == 1 {
156            self.selected.iter().copied().next()
157        } else {
158            None
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    /// `select` replaces the selection in single mode and extends it in multi
168    /// mode. Either way the last selected drawing becomes primary, and the
169    /// return value reports whether the selected set changed.
170    #[test]
171    fn select_replaces_or_extends_by_mode() {
172        let cases = [
173            (false, [true, true, true, true, false], vec![2]),
174            (true, [true, true, true, false, false], vec![1, 2, 3]),
175        ];
176        for (multi, changed, selected) in cases {
177            let mut sel = SelectionService::new();
178            sel.set_multi_select_mode(multi);
179
180            let returned = [1, 2, 3, 2, 2].map(|id| sel.select(id));
181
182            assert_eq!(returned, changed, "multi={multi}");
183            let mut ids: Vec<_> = sel.selected_ids().collect();
184            ids.sort_unstable();
185            assert_eq!(ids, selected, "multi={multi}");
186            assert_eq!(sel.primary(), Some(2), "multi={multi}");
187        }
188    }
189
190    #[test]
191    fn test_toggle() {
192        let mut sel = SelectionService::new();
193
194        sel.toggle(1);
195        assert!(sel.is_selected(1));
196
197        sel.toggle(1);
198        assert!(!sel.is_selected(1));
199    }
200
201    #[test]
202    fn test_deselect_all() {
203        let mut sel = SelectionService::new();
204        sel.set_multi_select_mode(true);
205        sel.select(1);
206        sel.select(2);
207
208        assert!(sel.deselect_all());
209        assert!(!sel.has_selection());
210        assert_eq!(sel.primary(), None);
211    }
212
213    #[test]
214    fn test_prune() {
215        let mut sel = SelectionService::new();
216        sel.set_multi_select_mode(true);
217        sel.select(1);
218        sel.select(2);
219        sel.select(3);
220
221        // Drawing 2 was deleted
222        let existing: HashSet<usize> = [1, 3].into_iter().collect();
223        sel.prune(&existing);
224
225        assert!(sel.is_selected(1));
226        assert!(!sel.is_selected(2));
227        assert!(sel.is_selected(3));
228        assert_eq!(sel.selection_count(), 2);
229    }
230
231    #[test]
232    fn test_select_all() {
233        let mut sel = SelectionService::new();
234        sel.select_all([1, 2, 3]);
235
236        let mut ids: Vec<_> = sel.selected_ids().collect();
237        ids.sort_unstable();
238        assert_eq!(ids, [1, 2, 3]);
239        assert_eq!(sel.primary(), Some(1));
240    }
241
242    #[test]
243    fn test_single_selection_legacy() {
244        let mut sel = SelectionService::new();
245
246        // No selection
247        assert_eq!(sel.single_selection(), None);
248
249        // Single selection
250        sel.select(5);
251        assert_eq!(sel.single_selection(), Some(5));
252
253        // Multiple selection
254        sel.set_multi_select_mode(true);
255        sel.select(6);
256        assert_eq!(sel.single_selection(), None);
257    }
258}