Skip to main content

kanban_core/
selection.rs

1//! Generic selection state utilities.
2//!
3//! Provides a reusable single-selection state machine that can be used
4//! by any UI framework for list selection.
5
6/// State for single-item selection in a list.
7#[derive(Clone, Debug, Default)]
8pub struct SelectionState {
9    selected_index: Option<usize>,
10}
11
12impl SelectionState {
13    /// Create a new selection state with no selection.
14    pub fn new() -> Self {
15        Self {
16            selected_index: None,
17        }
18    }
19
20    /// Get the currently selected index.
21    pub fn get(&self) -> Option<usize> {
22        self.selected_index
23    }
24
25    /// Set the selected index.
26    pub fn set(&mut self, index: Option<usize>) {
27        self.selected_index = index;
28    }
29
30    /// Clear the selection.
31    pub fn clear(&mut self) {
32        self.selected_index = None;
33    }
34
35    /// Move selection to the next item.
36    pub fn next(&mut self, max_count: usize) {
37        if max_count == 0 {
38            return;
39        }
40        self.selected_index = Some(match self.selected_index {
41            Some(idx) => (idx + 1).min(max_count - 1),
42            None => 0,
43        });
44    }
45
46    /// Move selection to the previous item.
47    pub fn prev(&mut self) {
48        self.selected_index = Some(match self.selected_index {
49            Some(idx) => idx.saturating_sub(1),
50            None => 0,
51        });
52    }
53
54    /// Auto-select the first item if nothing is selected and items exist.
55    pub fn auto_select_first_if_empty(&mut self, has_items: bool) {
56        if self.selected_index.is_none() && has_items {
57            self.selected_index = Some(0);
58        }
59    }
60
61    /// Jump to the first item.
62    pub fn jump_to_first(&mut self) {
63        self.selected_index = Some(0);
64    }
65
66    /// Jump to the last item.
67    pub fn jump_to_last(&mut self, len: usize) {
68        if len > 0 {
69            self.selected_index = Some(len - 1);
70        }
71    }
72
73    /// Check if an index is selected.
74    pub fn is_selected(&self, index: usize) -> bool {
75        self.selected_index == Some(index)
76    }
77
78    /// Check if anything is selected.
79    pub fn has_selection(&self) -> bool {
80        self.selected_index.is_some()
81    }
82
83    /// Clamp selection to valid range after list size changes.
84    pub fn clamp(&mut self, max_count: usize) {
85        if let Some(idx) = self.selected_index {
86            if max_count == 0 {
87                self.selected_index = None;
88            } else if idx >= max_count {
89                self.selected_index = Some(max_count - 1);
90            }
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_new_selection_is_empty() {
101        let selection = SelectionState::new();
102        assert!(selection.get().is_none());
103        assert!(!selection.has_selection());
104    }
105
106    #[test]
107    fn test_set_and_get() {
108        let mut selection = SelectionState::new();
109        selection.set(Some(5));
110        assert_eq!(selection.get(), Some(5));
111        assert!(selection.has_selection());
112    }
113
114    #[test]
115    fn test_clear() {
116        let mut selection = SelectionState::new();
117        selection.set(Some(5));
118        selection.clear();
119        assert!(selection.get().is_none());
120    }
121
122    #[test]
123    fn test_next() {
124        let mut selection = SelectionState::new();
125
126        // From None
127        selection.next(5);
128        assert_eq!(selection.get(), Some(0));
129
130        // Normal increment
131        selection.next(5);
132        assert_eq!(selection.get(), Some(1));
133
134        // At boundary
135        selection.set(Some(4));
136        selection.next(5);
137        assert_eq!(selection.get(), Some(4));
138    }
139
140    #[test]
141    fn test_prev() {
142        let mut selection = SelectionState::new();
143
144        // From None
145        selection.prev();
146        assert_eq!(selection.get(), Some(0));
147
148        // Normal decrement
149        selection.set(Some(3));
150        selection.prev();
151        assert_eq!(selection.get(), Some(2));
152
153        // At boundary
154        selection.set(Some(0));
155        selection.prev();
156        assert_eq!(selection.get(), Some(0));
157    }
158
159    #[test]
160    fn test_auto_select_first() {
161        let mut selection = SelectionState::new();
162
163        // With items
164        selection.auto_select_first_if_empty(true);
165        assert_eq!(selection.get(), Some(0));
166
167        // Already selected - no change
168        selection.set(Some(5));
169        selection.auto_select_first_if_empty(true);
170        assert_eq!(selection.get(), Some(5));
171
172        // No items
173        let mut selection2 = SelectionState::new();
174        selection2.auto_select_first_if_empty(false);
175        assert!(selection2.get().is_none());
176    }
177
178    #[test]
179    fn test_jump_to_first_last() {
180        let mut selection = SelectionState::new();
181
182        selection.set(Some(5));
183        selection.jump_to_first();
184        assert_eq!(selection.get(), Some(0));
185
186        selection.jump_to_last(10);
187        assert_eq!(selection.get(), Some(9));
188
189        // Empty list
190        selection.jump_to_last(0);
191        assert_eq!(selection.get(), Some(9)); // No change
192    }
193
194    #[test]
195    fn test_is_selected() {
196        let mut selection = SelectionState::new();
197        selection.set(Some(3));
198
199        assert!(selection.is_selected(3));
200        assert!(!selection.is_selected(0));
201        assert!(!selection.is_selected(5));
202    }
203
204    #[test]
205    fn test_clamp() {
206        let mut selection = SelectionState::new();
207        selection.set(Some(10));
208
209        // Clamp to smaller size
210        selection.clamp(5);
211        assert_eq!(selection.get(), Some(4));
212
213        // Clamp to empty
214        selection.clamp(0);
215        assert!(selection.get().is_none());
216
217        // Clamp when within range - no change
218        selection.set(Some(3));
219        selection.clamp(10);
220        assert_eq!(selection.get(), Some(3));
221    }
222
223    #[test]
224    fn test_next_empty_list() {
225        let mut selection = SelectionState::new();
226        selection.next(0);
227        assert!(selection.get().is_none());
228    }
229}