Skip to main content

dear_imgui_rs/widget/multi_select/
requests.rs

1use crate::{Id, sys};
2
3use super::basic_selection::BasicSelection;
4use super::storage::MultiSelectIndexStorage;
5
6/// Application-defined item data passed through Dear ImGui's multi-select API.
7pub type MultiSelectUserData = i64;
8
9/// Iteration order requested for a selected range.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum MultiSelectRangeDirection {
12    /// Visit the first item before the last item.
13    Forward,
14    /// Visit the last item before the first item.
15    Backward,
16}
17
18/// An owned selection change requested by Dear ImGui.
19#[derive(Clone, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub enum MultiSelectRequest {
22    /// Select or clear every item in the multi-select scope.
23    SetAll { selected: bool },
24    /// Select or clear an inclusive range of application item data.
25    SetRange {
26        selected: bool,
27        first: MultiSelectUserData,
28        last: MultiSelectUserData,
29        direction: MultiSelectRangeDirection,
30    },
31}
32
33/// An owned copy of the IO produced by `BeginMultiSelect()` or `EndMultiSelect()`.
34///
35/// The native IO is temporary and may be overwritten by the next multi-select scope. This
36/// value contains no native pointers, so it can be stored and applied later.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct MultiSelectResult {
39    requests: Vec<MultiSelectRequest>,
40    range_source_item: Option<MultiSelectUserData>,
41    navigation_item: Option<MultiSelectUserData>,
42    navigation_item_selected: bool,
43    range_source_reset: bool,
44    items_count: usize,
45}
46
47impl MultiSelectResult {
48    /// Selection changes requested by Dear ImGui.
49    #[must_use]
50    pub fn requests(&self) -> &[MultiSelectRequest] {
51        &self.requests
52    }
53
54    /// Source item used for range selection, when one is active.
55    #[must_use]
56    pub fn range_source_item(&self) -> Option<MultiSelectUserData> {
57        self.range_source_item
58    }
59
60    /// Item associated with Dear ImGui's navigation ID, when known.
61    #[must_use]
62    pub fn navigation_item(&self) -> Option<MultiSelectUserData> {
63        self.navigation_item
64    }
65
66    /// Whether the navigation item was selected when this result was captured.
67    #[must_use]
68    pub fn navigation_item_selected(&self) -> bool {
69        self.navigation_item_selected
70    }
71
72    /// Whether the range source was reset before the scope ended.
73    #[must_use]
74    pub fn range_source_reset(&self) -> bool {
75        self.range_source_reset
76    }
77
78    /// Item count supplied when the multi-select scope began.
79    #[must_use]
80    pub fn items_count(&self) -> usize {
81        self.items_count
82    }
83
84    /// Apply these requests to index-addressable selection storage.
85    pub fn apply_requests_indexed<S: MultiSelectIndexStorage>(&self, storage: &mut S) {
86        let items_count = self.items_count.min(storage.len());
87        self.apply_request_indices(items_count, |index, selected| {
88            storage.set_selected(index, selected);
89        });
90    }
91
92    /// Apply these requests to a [`BasicSelection`] using an index-to-ID mapping.
93    pub fn apply_requests_basic<G>(&self, selection: &mut BasicSelection, mut id_at_index: G)
94    where
95        G: FnMut(usize) -> Id,
96    {
97        self.apply_request_indices(self.items_count, |index, selected| {
98            selection.set_selected(id_at_index(index), selected);
99        });
100    }
101
102    fn apply_request_indices(&self, items_count: usize, mut set_selected: impl FnMut(usize, bool)) {
103        for request in &self.requests {
104            match *request {
105                MultiSelectRequest::SetAll { selected } => {
106                    for index in 0..items_count {
107                        set_selected(index, selected);
108                    }
109                }
110                MultiSelectRequest::SetRange {
111                    selected,
112                    first,
113                    last,
114                    direction,
115                } => {
116                    let Some(range) = indexed_range(first, last, items_count) else {
117                        continue;
118                    };
119                    match direction {
120                        MultiSelectRangeDirection::Forward => {
121                            for index in range {
122                                set_selected(index, selected);
123                            }
124                        }
125                        MultiSelectRangeDirection::Backward => {
126                            for index in range.rev() {
127                                set_selected(index, selected);
128                            }
129                        }
130                    }
131                }
132            }
133        }
134    }
135
136    pub(super) fn record_range_source_reset(&mut self, reset: bool) {
137        self.range_source_reset = reset;
138    }
139}
140
141fn indexed_range(
142    first: MultiSelectUserData,
143    last: MultiSelectUserData,
144    items_count: usize,
145) -> Option<std::ops::RangeInclusive<usize>> {
146    let first = usize::try_from(first).ok()?;
147    let last = usize::try_from(last).ok()?;
148    if first > last || first >= items_count {
149        return None;
150    }
151    Some(first..=last.min(items_count.saturating_sub(1)))
152}
153
154pub(super) unsafe fn copy_multi_select_result(
155    io: *const sys::ImGuiMultiSelectIO,
156) -> MultiSelectResult {
157    unsafe {
158        let io = io
159            .as_ref()
160            .expect("Dear ImGui returned a null multi-select IO");
161        let requests = if io.Requests.Data.is_null() || io.Requests.Size <= 0 {
162            Vec::new()
163        } else {
164            usize::try_from(io.Requests.Size)
165                .ok()
166                .map(|len| std::slice::from_raw_parts(io.Requests.Data, len))
167                .unwrap_or_default()
168                .iter()
169                .filter_map(copy_request)
170                .collect()
171        };
172
173        MultiSelectResult {
174            requests,
175            range_source_item: valid_user_data(io.RangeSrcItem),
176            navigation_item: valid_user_data(io.NavIdItem),
177            navigation_item_selected: io.NavIdSelected,
178            range_source_reset: io.RangeSrcReset,
179            items_count: usize::try_from(io.ItemsCount).unwrap_or(0),
180        }
181    }
182}
183
184fn copy_request(request: &sys::ImGuiSelectionRequest) -> Option<MultiSelectRequest> {
185    match request.Type {
186        sys::ImGuiSelectionRequestType_SetAll => Some(MultiSelectRequest::SetAll {
187            selected: request.Selected,
188        }),
189        sys::ImGuiSelectionRequestType_SetRange => Some(MultiSelectRequest::SetRange {
190            selected: request.Selected,
191            first: request.RangeFirstItem,
192            last: request.RangeLastItem,
193            direction: if request.RangeDirection < 0 {
194                MultiSelectRangeDirection::Backward
195            } else {
196                MultiSelectRangeDirection::Forward
197            },
198        }),
199        _ => None,
200    }
201}
202
203fn valid_user_data(value: sys::ImGuiSelectionUserData) -> Option<MultiSelectUserData> {
204    (value != -1).then_some(value)
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    struct RecordingStorage {
212        selected: Vec<bool>,
213        updates: Vec<usize>,
214    }
215
216    impl MultiSelectIndexStorage for RecordingStorage {
217        fn len(&self) -> usize {
218            self.selected.len()
219        }
220
221        fn is_selected(&self, index: usize) -> bool {
222            self.selected[index]
223        }
224
225        fn set_selected(&mut self, index: usize, selected: bool) {
226            self.selected[index] = selected;
227            self.updates.push(index);
228        }
229    }
230
231    fn result(requests: Vec<MultiSelectRequest>, items_count: usize) -> MultiSelectResult {
232        MultiSelectResult {
233            requests,
234            range_source_item: None,
235            navigation_item: None,
236            navigation_item_selected: false,
237            range_source_reset: false,
238            items_count,
239        }
240    }
241
242    #[test]
243    fn indexed_requests_preserve_backward_range_direction() {
244        let requests = result(
245            vec![MultiSelectRequest::SetRange {
246                selected: true,
247                first: 1,
248                last: 3,
249                direction: MultiSelectRangeDirection::Backward,
250            }],
251            5,
252        );
253        let mut storage = RecordingStorage {
254            selected: vec![false; 5],
255            updates: Vec::new(),
256        };
257
258        requests.apply_requests_indexed(&mut storage);
259
260        assert_eq!(storage.updates, vec![3, 2, 1]);
261        assert_eq!(storage.selected, vec![false, true, true, true, false]);
262    }
263
264    #[test]
265    fn indexed_requests_clamp_to_current_storage_and_ignore_invalid_ranges() {
266        let requests = result(
267            vec![
268                MultiSelectRequest::SetAll { selected: true },
269                MultiSelectRequest::SetRange {
270                    selected: false,
271                    first: 2,
272                    last: 99,
273                    direction: MultiSelectRangeDirection::Forward,
274                },
275                MultiSelectRequest::SetRange {
276                    selected: false,
277                    first: -1,
278                    last: 1,
279                    direction: MultiSelectRangeDirection::Forward,
280                },
281            ],
282            8,
283        );
284        let mut storage = RecordingStorage {
285            selected: vec![false; 4],
286            updates: Vec::new(),
287        };
288
289        requests.apply_requests_indexed(&mut storage);
290
291        assert_eq!(storage.selected, vec![true, true, false, false]);
292        assert_eq!(storage.updates, vec![0, 1, 2, 3, 2, 3]);
293    }
294
295    #[test]
296    fn copied_result_does_not_borrow_native_request_storage() {
297        let mut native_request = sys::ImGuiSelectionRequest {
298            Type: sys::ImGuiSelectionRequestType_SetRange,
299            Selected: true,
300            RangeDirection: -1,
301            RangeFirstItem: 1,
302            RangeLastItem: 3,
303        };
304        let mut io = sys::ImGuiMultiSelectIO::default();
305        io.Requests.Data = &mut native_request;
306        io.Requests.Size = 1;
307        io.Requests.Capacity = 1;
308        io.RangeSrcItem = 7;
309        io.NavIdItem = 9;
310        io.NavIdSelected = true;
311        io.ItemsCount = 5;
312
313        let copied = unsafe { copy_multi_select_result(&io) };
314        native_request.Selected = false;
315        native_request.RangeFirstItem = 0;
316        io.RangeSrcItem = 11;
317        assert!(!native_request.Selected);
318        assert_eq!(native_request.RangeFirstItem, 0);
319        assert_eq!(io.RangeSrcItem, 11);
320
321        assert_eq!(
322            copied.requests(),
323            &[MultiSelectRequest::SetRange {
324                selected: true,
325                first: 1,
326                last: 3,
327                direction: MultiSelectRangeDirection::Backward,
328            }]
329        );
330        assert_eq!(copied.range_source_item(), Some(7));
331        assert_eq!(copied.navigation_item(), Some(9));
332        assert!(copied.navigation_item_selected());
333        assert_eq!(copied.items_count(), 5);
334    }
335}