Skip to main content

herogpui_components/
selection.rs

1//! The shared `selectionMode` semantics.
2//!
3//! Toggle groups, dropdown menus, tables and pickers all answer the same
4//! question when an item is activated: what is the selection now? Keeping one
5//! implementation means they cannot disagree.
6
7use gpui::SharedString;
8use herogpui_core::{SelectionBehavior, SelectionMode};
9
10/// What an unmodified Escape press does in a selectable collection.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum EscapeKeyBehavior {
13    /// Clear a nonempty selection and consume the key.
14    #[default]
15    ClearSelection,
16    /// Leave selection unchanged and let the key bubble.
17    None,
18}
19
20/// Whether activating an item in `mode` has a selection change to report.
21pub(crate) fn reports_changes(mode: SelectionMode) -> bool {
22    mode != SelectionMode::None
23}
24
25/// Normalizes a keyed selection to the shape of pinned react-stately 3.49.0's
26/// `selectedKeys`: a JS `Set`, which collapses duplicates to the first
27/// insertion and iterates in insertion order. A single-mode selection holds at
28/// most one key — the first the owner (or default) listed.
29pub(crate) fn normalize_selection(keys: Vec<SharedString>, multiple: bool) -> Vec<SharedString> {
30    let mut out: Vec<SharedString> = Vec::with_capacity(keys.len());
31    for key in keys {
32        if out.contains(&key) {
33            continue;
34        }
35        out.push(key);
36        if !multiple {
37            break;
38        }
39    }
40    out
41}
42
43/// Toggles `key` in an ordered selection: removing takes it out in place so
44/// the remaining keys keep their insertion order; adding appends — the
45/// mutation a JS `Set` performs.
46pub(crate) fn toggle_key(selection: &mut Vec<SharedString>, key: &SharedString) {
47    if let Some(at) = selection.iter().position(|k| k == key) {
48        selection.remove(at);
49    } else {
50        selection.push(key.clone());
51    }
52}
53
54/// The selection after activating `key`.
55///
56/// `Single` collapses to just `key` (or clears it, unless
57/// `disallow_empty_selection`); `Multiple` toggles membership; `None` is inert.
58pub fn next_selection(
59    current: &[SharedString],
60    key: &SharedString,
61    mode: SelectionMode,
62    disallow_empty: bool,
63) -> Vec<SharedString> {
64    let is_selected = current.iter().any(|k| k == key);
65    match mode {
66        SelectionMode::None => current.to_vec(),
67        SelectionMode::Single => {
68            if is_selected && !disallow_empty {
69                Vec::new()
70            } else {
71                vec![key.clone()]
72            }
73        }
74        SelectionMode::Multiple => {
75            if is_selected {
76                if disallow_empty && current.len() == 1 {
77                    return current.to_vec();
78                }
79                current.iter().filter(|k| *k != key).cloned().collect()
80            } else {
81                let mut next = current.to_vec();
82                next.push(key.clone());
83                next
84            }
85        }
86    }
87}
88
89/// The selection after an activation with React Stately's
90/// `selectionBehavior` applied.
91///
92/// A single-select collection always uses its single-item toggle rule. In a
93/// multi-select collection, `Toggle` preserves the existing set while
94/// `Replace` selects only the activated key. Callers that received Shift or a
95/// platform non-contiguous modifier should use their range/toggle path before
96/// calling this helper, just as React Aria does.
97pub(crate) fn next_selection_with_behavior(
98    current: &[SharedString],
99    key: &SharedString,
100    mode: SelectionMode,
101    behavior: SelectionBehavior,
102    disallow_empty: bool,
103) -> Vec<SharedString> {
104    match (mode, behavior) {
105        (SelectionMode::Multiple, SelectionBehavior::Replace) => vec![key.clone()],
106        _ => next_selection(current, key, mode, disallow_empty),
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    fn keys(items: &[&str]) -> Vec<SharedString> {
115        items
116            .iter()
117            .map(|s| SharedString::from(s.to_string()))
118            .collect()
119    }
120
121    #[test]
122    fn normalize_keeps_first_insertion_order_and_collapses_duplicates() {
123        assert_eq!(
124            normalize_selection(keys(&["b", "a", "b", "c"]), true),
125            keys(&["b", "a", "c"]),
126            "duplicates collapse to their first insertion; the rest keep \
127             insertion order"
128        );
129        assert_eq!(
130            normalize_selection(keys(&["a", "b"]), false),
131            keys(&["a"]),
132            "single mode keeps only the first listed key"
133        );
134    }
135
136    #[test]
137    fn toggle_removes_in_place_and_appends() {
138        let mut selection = keys(&["a", "b", "c"]);
139        toggle_key(&mut selection, &SharedString::from("b"));
140        assert_eq!(selection, keys(&["a", "c"]));
141        toggle_key(&mut selection, &SharedString::from("d"));
142        assert_eq!(selection, keys(&["a", "c", "d"]));
143    }
144
145    #[test]
146    fn only_selection_modes_report_changes() {
147        assert!(!reports_changes(SelectionMode::None));
148        assert!(reports_changes(SelectionMode::Single));
149        assert!(reports_changes(SelectionMode::Multiple));
150    }
151
152    #[test]
153    fn single_replaces() {
154        let next = next_selection(
155            &keys(&["a"]),
156            &SharedString::from("b"),
157            SelectionMode::Single,
158            false,
159        );
160        assert_eq!(next, keys(&["b"]));
161    }
162
163    #[test]
164    fn single_clears_on_reselect() {
165        let next = next_selection(
166            &keys(&["a"]),
167            &SharedString::from("a"),
168            SelectionMode::Single,
169            false,
170        );
171        assert!(next.is_empty());
172    }
173
174    #[test]
175    fn single_keeps_when_empty_disallowed() {
176        let next = next_selection(
177            &keys(&["a"]),
178            &SharedString::from("a"),
179            SelectionMode::Single,
180            true,
181        );
182        assert_eq!(next, keys(&["a"]));
183    }
184
185    #[test]
186    fn multiple_toggles() {
187        let added = next_selection(
188            &keys(&["a"]),
189            &SharedString::from("b"),
190            SelectionMode::Multiple,
191            false,
192        );
193        assert_eq!(added, keys(&["a", "b"]));
194        let removed = next_selection(
195            &keys(&["a", "b"]),
196            &SharedString::from("a"),
197            SelectionMode::Multiple,
198            false,
199        );
200        assert_eq!(removed, keys(&["b"]));
201    }
202
203    #[test]
204    fn multiple_keeps_last_when_empty_disallowed() {
205        let next = next_selection(
206            &keys(&["a"]),
207            &SharedString::from("a"),
208            SelectionMode::Multiple,
209            true,
210        );
211        assert_eq!(next, keys(&["a"]));
212        let next = next_selection(
213            &keys(&["a", "b"]),
214            &SharedString::from("a"),
215            SelectionMode::Multiple,
216            true,
217        );
218        assert_eq!(next, keys(&["b"]));
219    }
220
221    #[test]
222    fn none_is_inert() {
223        let next = next_selection(
224            &keys(&["a"]),
225            &SharedString::from("b"),
226            SelectionMode::None,
227            false,
228        );
229        assert_eq!(next, keys(&["a"]));
230    }
231
232    #[test]
233    fn replace_behavior_collapses_a_multiple_selection() {
234        let next = next_selection_with_behavior(
235            &keys(&["a", "b"]),
236            &SharedString::from("c"),
237            SelectionMode::Multiple,
238            SelectionBehavior::Replace,
239            false,
240        );
241        assert_eq!(next, keys(&["c"]));
242    }
243
244    #[test]
245    fn replace_behavior_keeps_the_activated_key_selected() {
246        let next = next_selection_with_behavior(
247            &keys(&["a", "b"]),
248            &SharedString::from("b"),
249            SelectionMode::Multiple,
250            SelectionBehavior::Replace,
251            false,
252        );
253        assert_eq!(next, keys(&["b"]));
254    }
255}