dear_imgui_rs/widget/multi_select/
requests.rs1use crate::{Id, sys};
2
3use super::basic_selection::BasicSelection;
4use super::storage::MultiSelectIndexStorage;
5
6pub type MultiSelectUserData = i64;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum MultiSelectRangeDirection {
12 Forward,
14 Backward,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub enum MultiSelectRequest {
22 SetAll { selected: bool },
24 SetRange {
26 selected: bool,
27 first: MultiSelectUserData,
28 last: MultiSelectUserData,
29 direction: MultiSelectRangeDirection,
30 },
31}
32
33#[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 #[must_use]
50 pub fn requests(&self) -> &[MultiSelectRequest] {
51 &self.requests
52 }
53
54 #[must_use]
56 pub fn range_source_item(&self) -> Option<MultiSelectUserData> {
57 self.range_source_item
58 }
59
60 #[must_use]
62 pub fn navigation_item(&self) -> Option<MultiSelectUserData> {
63 self.navigation_item
64 }
65
66 #[must_use]
68 pub fn navigation_item_selected(&self) -> bool {
69 self.navigation_item_selected
70 }
71
72 #[must_use]
74 pub fn range_source_reset(&self) -> bool {
75 self.range_source_reset
76 }
77
78 #[must_use]
80 pub fn items_count(&self) -> usize {
81 self.items_count
82 }
83
84 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 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}