Skip to main content

dear_imgui_rs/widget/
multi_select.rs

1//! Multi-select helpers (BeginMultiSelect/EndMultiSelect)
2//!
3//! This module provides a small, safe wrapper around Dear ImGui's multi-select
4//! API introduced in 1.92 (`BeginMultiSelect` / `EndMultiSelect`), following
5//! the "external storage" pattern described in the official docs:
6//! https://github.com/ocornut/imgui/wiki/Multi-Select
7//!
8//! The main entry point is [`Ui::multi_select_indexed`], which:
9//! - wraps `BeginMultiSelect()` / `EndMultiSelect()`
10//! - wires `SetNextItemSelectionUserData()` for each item (index-based)
11//! - applies selection requests to your storage using a simple trait.
12
13#![allow(
14    clippy::cast_possible_truncation,
15    clippy::cast_sign_loss,
16    clippy::as_conversions
17)]
18
19use crate::Ui;
20use crate::sys;
21use std::collections::HashSet;
22
23bitflags::bitflags! {
24    /// Flags controlling multi-selection behavior.
25    ///
26    /// These mirror Dear ImGui's `ImGuiMultiSelectFlags` and control how
27    /// selection works (single vs multi, box-select, keyboard shortcuts, etc).
28    #[repr(transparent)]
29    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30    pub struct MultiSelectFlags: i32 {
31        /// No flags.
32        const NONE = sys::ImGuiMultiSelectFlags_None as i32;
33        /// Single-selection scope. Ctrl/Shift range selection is disabled.
34        const SINGLE_SELECT = sys::ImGuiMultiSelectFlags_SingleSelect as i32;
35        /// Disable `Ctrl+A` "select all" shortcut.
36        const NO_SELECT_ALL = sys::ImGuiMultiSelectFlags_NoSelectAll as i32;
37        /// Disable range selection (Shift+click / Shift+arrow).
38        const NO_RANGE_SELECT = sys::ImGuiMultiSelectFlags_NoRangeSelect as i32;
39        /// Disable automatic selection of newly focused items.
40        const NO_AUTO_SELECT = sys::ImGuiMultiSelectFlags_NoAutoSelect as i32;
41        /// Disable automatic clearing of selection when focus moves within the scope.
42        const NO_AUTO_CLEAR = sys::ImGuiMultiSelectFlags_NoAutoClear as i32;
43        /// Disable automatic clearing when reselecting the same range.
44        const NO_AUTO_CLEAR_ON_RESELECT =
45            sys::ImGuiMultiSelectFlags_NoAutoClearOnReselect as i32;
46        /// Enable 1D box-select (same x, full-width rows).
47        const BOX_SELECT_1D = sys::ImGuiMultiSelectFlags_BoxSelect1d as i32;
48        /// Enable 2D box-select (arbitrary item layout).
49        const BOX_SELECT_2D = sys::ImGuiMultiSelectFlags_BoxSelect2d as i32;
50        /// Disable drag-scrolling when box-selecting near edges of the scope.
51        const BOX_SELECT_NO_SCROLL = sys::ImGuiMultiSelectFlags_BoxSelectNoScroll as i32;
52        /// Clear selection when pressing Escape while the scope is focused.
53        const CLEAR_ON_ESCAPE = sys::ImGuiMultiSelectFlags_ClearOnEscape as i32;
54        /// Clear selection when clicking on empty space (void) inside the scope.
55        const CLEAR_ON_CLICK_VOID = sys::ImGuiMultiSelectFlags_ClearOnClickVoid as i32;
56        /// Scope is the whole window (default).
57        const SCOPE_WINDOW = sys::ImGuiMultiSelectFlags_ScopeWindow as i32;
58        /// Scope is a rectangular region between `BeginMultiSelect()`/`EndMultiSelect()`.
59        const SCOPE_RECT = sys::ImGuiMultiSelectFlags_ScopeRect as i32;
60        /// Apply selection using Dear ImGui's default click behavior.
61        ///
62        /// This maps to `SelectOnAuto`, which upstream now exposes explicitly.
63        const SELECT_ON_AUTO = sys::ImGuiMultiSelectFlags_SelectOnAuto as i32;
64        /// Backward-compatible alias for Dear ImGui's default click behavior.
65        ///
66        /// Upstream renamed `SelectOnClick` to `SelectOnAuto`.
67        const SELECT_ON_CLICK = sys::ImGuiMultiSelectFlags_SelectOnAuto as i32;
68        /// Apply selection on mouse down for any clicked item.
69        const SELECT_ON_CLICK_ALWAYS =
70            sys::ImGuiMultiSelectFlags_SelectOnClickAlways as i32;
71        /// Apply selection on mouse release (allows dragging without altering selection).
72        const SELECT_ON_CLICK_RELEASE =
73            sys::ImGuiMultiSelectFlags_SelectOnClickRelease as i32;
74        /// Enable X-axis navigation wrap helper.
75        const NAV_WRAP_X = sys::ImGuiMultiSelectFlags_NavWrapX as i32;
76        /// Disable default right-click behavior that selects item before opening a context menu.
77        const NO_SELECT_ON_RIGHT_CLICK =
78            sys::ImGuiMultiSelectFlags_NoSelectOnRightClick as i32;
79    }
80}
81
82/// Selection container backed by Dear ImGui's `ImGuiSelectionBasicStorage`.
83///
84/// This stores a set of selected `ImGuiID` values using the optimized helper
85/// provided by Dear ImGui. It is suitable when items are naturally identified
86/// by stable IDs (e.g. table rows, tree nodes).
87#[derive(Debug)]
88pub struct BasicSelection {
89    raw: *mut sys::ImGuiSelectionBasicStorage,
90}
91
92impl BasicSelection {
93    /// Create an empty selection storage.
94    pub fn new() -> Self {
95        unsafe {
96            let ptr = sys::ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage();
97            if ptr.is_null() {
98                panic!("ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage() returned null");
99            }
100            Self { raw: ptr }
101        }
102    }
103
104    /// Return the number of selected items.
105    pub fn len(&self) -> usize {
106        unsafe {
107            let size = (*self.raw).Size;
108            if size <= 0 { 0 } else { size as usize }
109        }
110    }
111
112    /// Returns true if the selection is empty.
113    pub fn is_empty(&self) -> bool {
114        self.len() == 0
115    }
116
117    /// Clear the selection set.
118    pub fn clear(&mut self) {
119        unsafe {
120            sys::ImGuiSelectionBasicStorage_Clear(self.raw);
121        }
122    }
123
124    /// Returns true if the given id is selected.
125    pub fn contains(&self, id: crate::Id) -> bool {
126        unsafe { sys::ImGuiSelectionBasicStorage_Contains(self.raw, id.raw()) }
127    }
128
129    /// Set selection state for a given id.
130    pub fn set_selected(&mut self, id: crate::Id, selected: bool) {
131        unsafe {
132            sys::ImGuiSelectionBasicStorage_SetItemSelected(self.raw, id.raw(), selected);
133        }
134    }
135
136    /// Iterate over selected ids.
137    pub fn iter(&self) -> BasicSelectionIter<'_> {
138        BasicSelectionIter {
139            storage: self,
140            it: std::ptr::null_mut(),
141        }
142    }
143
144    /// Expose raw pointer for internal helpers.
145    pub(crate) fn as_raw(&self) -> *mut sys::ImGuiSelectionBasicStorage {
146        self.raw
147    }
148}
149
150impl Default for BasicSelection {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl Drop for BasicSelection {
157    fn drop(&mut self) {
158        unsafe {
159            if !self.raw.is_null() {
160                sys::ImGuiSelectionBasicStorage_destroy(self.raw);
161                self.raw = std::ptr::null_mut();
162            }
163        }
164    }
165}
166
167/// Iterator over selected ids stored in [`BasicSelection`].
168pub struct BasicSelectionIter<'a> {
169    storage: &'a BasicSelection,
170    it: *mut std::os::raw::c_void,
171}
172
173impl<'a> Iterator for BasicSelectionIter<'a> {
174    type Item = crate::Id;
175
176    fn next(&mut self) -> Option<Self::Item> {
177        unsafe {
178            let mut out_id: sys::ImGuiID = 0;
179            let has_next = sys::ImGuiSelectionBasicStorage_GetNextSelectedItem(
180                self.storage.as_raw(),
181                &mut self.it,
182                &mut out_id,
183            );
184            if has_next {
185                Some(crate::Id::from(out_id))
186            } else {
187                None
188            }
189        }
190    }
191}
192
193/// Index-based selection storage for multi-select helpers.
194///
195/// Implement this trait for your selection container (e.g. `Vec<bool>`,
196/// `Vec<MyItem { selected: bool }>` or a custom type) to use
197/// [`Ui::multi_select_indexed`].
198pub trait MultiSelectIndexStorage {
199    /// Total number of items in the selection scope.
200    fn len(&self) -> usize;
201
202    /// Returns `true` if the selection scope is empty.
203    fn is_empty(&self) -> bool {
204        self.len() == 0
205    }
206
207    /// Returns whether item at `index` is currently selected.
208    fn is_selected(&self, index: usize) -> bool;
209
210    /// Updates selection state for item at `index`.
211    fn set_selected(&mut self, index: usize, selected: bool);
212
213    /// Optional hint for current selection size.
214    ///
215    /// If provided, this is forwarded to `BeginMultiSelect()` to improve the
216    /// behavior of shortcuts such as `ImGuiMultiSelectFlags_ClearOnEscape`.
217    /// When `None` (default), the size is treated as "unknown".
218    fn selected_count_hint(&self) -> Option<usize> {
219        None
220    }
221}
222
223impl MultiSelectIndexStorage for Vec<bool> {
224    fn len(&self) -> usize {
225        self.len()
226    }
227
228    fn is_selected(&self, index: usize) -> bool {
229        self.get(index).copied().unwrap_or(false)
230    }
231
232    fn set_selected(&mut self, index: usize, selected: bool) {
233        if index < self.len() {
234            self[index] = selected;
235        }
236    }
237
238    fn selected_count_hint(&self) -> Option<usize> {
239        // For typical lists this is cheap enough; callers with large datasets
240        // can implement the trait manually with a more efficient counter.
241        Some(self.iter().filter(|&&b| b).count())
242    }
243}
244
245impl MultiSelectIndexStorage for &mut [bool] {
246    fn len(&self) -> usize {
247        (**self).len()
248    }
249
250    fn is_selected(&self, index: usize) -> bool {
251        self.get(index).copied().unwrap_or(false)
252    }
253
254    fn set_selected(&mut self, index: usize, selected: bool) {
255        if index < self.len() {
256            self[index] = selected;
257        }
258    }
259
260    fn selected_count_hint(&self) -> Option<usize> {
261        Some(self.iter().filter(|&&b| b).count())
262    }
263}
264
265/// Index-based selection storage backed by a key slice + `HashSet` of selected keys.
266///
267/// This is convenient when your application stores selection as a set of
268/// arbitrary keys (e.g. `HashSet<u32>` or `HashSet<MyId>`), but you still
269/// want to drive a multi-select scope using contiguous indices.
270pub struct KeySetSelection<'a, K>
271where
272    K: Eq + std::hash::Hash + Copy,
273{
274    keys: &'a [K],
275    selected: &'a mut HashSet<K>,
276}
277
278impl<'a, K> KeySetSelection<'a, K>
279where
280    K: Eq + std::hash::Hash + Copy,
281{
282    /// Create a new index-based view over a key slice and a selection set.
283    ///
284    /// - `keys`: stable index->key mapping (e.g. your backing array).
285    /// - `selected`: set of currently selected keys.
286    pub fn new(keys: &'a [K], selected: &'a mut HashSet<K>) -> Self {
287        Self { keys, selected }
288    }
289}
290
291impl<'a, K> MultiSelectIndexStorage for KeySetSelection<'a, K>
292where
293    K: Eq + std::hash::Hash + Copy,
294{
295    fn len(&self) -> usize {
296        self.keys.len()
297    }
298
299    fn is_selected(&self, index: usize) -> bool {
300        self.keys
301            .get(index)
302            .map(|k| self.selected.contains(k))
303            .unwrap_or(false)
304    }
305
306    fn set_selected(&mut self, index: usize, selected: bool) {
307        if let Some(&key) = self.keys.get(index) {
308            if selected {
309                self.selected.insert(key);
310            } else {
311                self.selected.remove(&key);
312            }
313        }
314    }
315
316    fn selected_count_hint(&self) -> Option<usize> {
317        Some(self.selected.len())
318    }
319}
320
321/// Apply `ImGuiMultiSelectIO` requests to index-based selection storage.
322///
323/// This mirrors `ImGuiSelectionExternalStorage::ApplyRequests` from Dear ImGui,
324/// but operates on the safe [`MultiSelectIndexStorage`] trait instead of relying
325/// on C callbacks.
326unsafe fn apply_multi_select_requests_indexed<S: MultiSelectIndexStorage>(
327    ms_io: *mut sys::ImGuiMultiSelectIO,
328    storage: &mut S,
329) {
330    unsafe {
331        if ms_io.is_null() {
332            return;
333        }
334
335        let io_ref: &mut sys::ImGuiMultiSelectIO = &mut *ms_io;
336        let items_count = usize::try_from(io_ref.ItemsCount).unwrap_or(0);
337
338        let requests = &mut io_ref.Requests;
339        if requests.Data.is_null() || requests.Size <= 0 {
340            return;
341        }
342
343        let len = match usize::try_from(requests.Size) {
344            Ok(len) => len,
345            Err(_) => return,
346        };
347        let slice = std::slice::from_raw_parts_mut(requests.Data, len);
348
349        for req in slice {
350            if req.Type == sys::ImGuiSelectionRequestType_SetAll {
351                for idx in 0..items_count {
352                    storage.set_selected(idx, req.Selected);
353                }
354            } else if req.Type == sys::ImGuiSelectionRequestType_SetRange {
355                let first = req.RangeFirstItem as i32;
356                let last = req.RangeLastItem as i32;
357                if first < 0 || last < first {
358                    continue;
359                }
360                let last_clamped = std::cmp::min(last as usize, items_count.saturating_sub(1));
361                for idx in first as usize..=last_clamped {
362                    storage.set_selected(idx, req.Selected);
363                }
364            }
365        }
366    }
367}
368
369/// RAII wrapper around `BeginMultiSelect()` / `EndMultiSelect()` for advanced users.
370///
371/// This gives direct, but scoped, access to the underlying `ImGuiMultiSelectIO`
372/// struct. It does not perform any selection updates by itself; you are expected
373/// to call helper methods or use the raw IO to drive your own storage.
374pub struct MultiSelectScope<'ui> {
375    ms_io_begin: *mut sys::ImGuiMultiSelectIO,
376    items_count: i32,
377    _marker: std::marker::PhantomData<&'ui Ui>,
378}
379
380impl<'ui> MultiSelectScope<'ui> {
381    fn new(flags: MultiSelectFlags, selection_size: Option<i32>, items_count: usize) -> Self {
382        let selection_size_i32 = selection_size.unwrap_or(-1);
383        let items_count_i32 = i32::try_from(items_count).unwrap_or(i32::MAX);
384        let ms_io_begin =
385            unsafe { sys::igBeginMultiSelect(flags.bits(), selection_size_i32, items_count_i32) };
386        Self {
387            ms_io_begin,
388            items_count: items_count_i32,
389            _marker: std::marker::PhantomData,
390        }
391    }
392
393    /// Access the IO struct returned by `BeginMultiSelect()`.
394    pub fn begin_io(&self) -> &sys::ImGuiMultiSelectIO {
395        unsafe { &*self.ms_io_begin }
396    }
397
398    /// Mutable access to the IO struct returned by `BeginMultiSelect()`.
399    pub fn begin_io_mut(&mut self) -> &mut sys::ImGuiMultiSelectIO {
400        unsafe { &mut *self.ms_io_begin }
401    }
402
403    /// Apply selection requests from `BeginMultiSelect()` to index-based storage.
404    pub fn apply_begin_requests_indexed<S: MultiSelectIndexStorage>(&mut self, storage: &mut S) {
405        unsafe {
406            apply_multi_select_requests_indexed(self.ms_io_begin, storage);
407        }
408    }
409
410    /// Finalize the multi-select scope and return an IO view for the end state.
411    ///
412    /// This calls `EndMultiSelect()` and returns a `MultiSelectEnd` wrapper
413    /// that can be used to apply the final selection requests.
414    pub fn end(self) -> MultiSelectEnd<'ui> {
415        let ms_io_end = unsafe { sys::igEndMultiSelect() };
416        MultiSelectEnd {
417            ms_io_end,
418            items_count: self.items_count,
419            _marker: std::marker::PhantomData,
420        }
421    }
422}
423
424/// IO view returned after calling `EndMultiSelect()` via [`MultiSelectScope::end`].
425pub struct MultiSelectEnd<'ui> {
426    ms_io_end: *mut sys::ImGuiMultiSelectIO,
427    items_count: i32,
428    _marker: std::marker::PhantomData<&'ui Ui>,
429}
430
431impl<'ui> MultiSelectEnd<'ui> {
432    /// Access the IO struct returned by `EndMultiSelect()`.
433    pub fn io(&self) -> &sys::ImGuiMultiSelectIO {
434        unsafe { &*self.ms_io_end }
435    }
436
437    /// Mutable access to the IO struct returned by `EndMultiSelect()`.
438    pub fn io_mut(&mut self) -> &mut sys::ImGuiMultiSelectIO {
439        unsafe { &mut *self.ms_io_end }
440    }
441
442    /// Apply selection requests from `EndMultiSelect()` to index-based storage.
443    pub fn apply_requests_indexed<S: MultiSelectIndexStorage>(&mut self, storage: &mut S) {
444        unsafe {
445            apply_multi_select_requests_indexed(self.ms_io_end, storage);
446        }
447    }
448
449    /// Apply selection requests from `EndMultiSelect()` to a [`BasicSelection`].
450    pub fn apply_requests_basic<G>(&mut self, selection: &mut BasicSelection, mut id_at_index: G)
451    where
452        G: FnMut(usize) -> crate::Id,
453    {
454        unsafe {
455            apply_multi_select_requests_basic(
456                self.ms_io_end,
457                selection,
458                self.items_count as usize,
459                &mut id_at_index,
460            );
461        }
462    }
463}
464
465impl Ui {
466    /// Low-level entry point: begin a multi-select scope and return a RAII wrapper.
467    ///
468    /// This is the closest safe wrapper to the raw `BeginMultiSelect()` /
469    /// `EndMultiSelect()` pair. It does not drive any selection storage by
470    /// itself; use `begin_io()` / `end().io()` and the helper methods to
471    /// implement custom patterns.
472    pub fn begin_multi_select_raw(
473        &self,
474        flags: MultiSelectFlags,
475        selection_size: Option<i32>,
476        items_count: usize,
477    ) -> MultiSelectScope<'_> {
478        MultiSelectScope::new(flags, selection_size, items_count)
479    }
480    /// Multi-select helper for index-based storage.
481    ///
482    /// This wraps `BeginMultiSelect()` / `EndMultiSelect()` and applies
483    /// selection requests to an index-addressable selection container.
484    ///
485    /// Typical usage:
486    ///
487    /// ```no_run
488    /// # use dear_imgui_rs::*;
489    /// # let mut ctx = Context::create();
490    /// # let ui = ctx.frame();
491    /// let mut selected = vec![false; 128];
492    ///
493    /// ui.multi_select_indexed(&mut selected, MultiSelectFlags::NONE, |ui, idx, is_selected| {
494    ///     ui.text(format!(
495    ///         "{} {}",
496    ///         if is_selected { "[x]" } else { "[ ]" },
497    ///         idx
498    ///     ));
499    /// });
500    /// ```
501    ///
502    /// Notes:
503    /// - `storage.len()` defines `items_count`.
504    /// - This helper uses the "external storage" pattern where selection is
505    ///   stored entirely on the application side.
506    /// - Per-item selection toggles can be queried via
507    ///   [`Ui::is_item_toggled_selection`].
508    pub fn multi_select_indexed<S, F>(
509        &self,
510        storage: &mut S,
511        flags: MultiSelectFlags,
512        mut render_item: F,
513    ) where
514        S: MultiSelectIndexStorage,
515        F: FnMut(&Ui, usize, bool),
516    {
517        let items_count = storage.len();
518        let selection_size_i32 = storage
519            .selected_count_hint()
520            .and_then(|n| i32::try_from(n).ok())
521            .unwrap_or(-1);
522
523        // Begin multi-select scope.
524        let ms_io_begin = unsafe {
525            sys::igBeginMultiSelect(flags.bits(), selection_size_i32, items_count as i32)
526        };
527
528        // Apply SetAll requests (if any) before submitting items.
529        unsafe {
530            apply_multi_select_requests_indexed(ms_io_begin, storage);
531        }
532
533        // Submit items: for each index we set SelectionUserData and let user
534        // draw widgets, passing the current selection state as `is_selected`.
535        for idx in 0..items_count {
536            unsafe {
537                sys::igSetNextItemSelectionUserData(idx as sys::ImGuiSelectionUserData);
538            }
539            let is_selected = storage.is_selected(idx);
540            render_item(self, idx, is_selected);
541        }
542
543        // End scope and apply requests generated during item submission.
544        let ms_io_end = unsafe { sys::igEndMultiSelect() };
545        unsafe {
546            apply_multi_select_requests_indexed(ms_io_end, storage);
547        }
548    }
549
550    /// Multi-select helper for index-based storage inside an active table.
551    ///
552    /// This is a convenience wrapper over [`Ui::multi_select_indexed`] that
553    /// automatically advances table rows and starts each row at column 0.
554    ///
555    /// It expects to be called between `BeginTable`/`EndTable`.
556    pub fn table_multi_select_indexed<S, F>(
557        &self,
558        storage: &mut S,
559        flags: MultiSelectFlags,
560        mut build_row: F,
561    ) where
562        S: MultiSelectIndexStorage,
563        F: FnMut(&Ui, usize, bool),
564    {
565        let row_count = storage.len();
566        let selection_size_i32 = storage
567            .selected_count_hint()
568            .and_then(|n| i32::try_from(n).ok())
569            .unwrap_or(-1);
570
571        let ms_io_begin =
572            unsafe { sys::igBeginMultiSelect(flags.bits(), selection_size_i32, row_count as i32) };
573
574        unsafe {
575            apply_multi_select_requests_indexed(ms_io_begin, storage);
576        }
577
578        for row in 0..row_count {
579            unsafe {
580                sys::igSetNextItemSelectionUserData(row as sys::ImGuiSelectionUserData);
581            }
582            // Start a new table row and move to first column.
583            self.table_next_row();
584            self.table_next_column();
585
586            let is_selected = storage.is_selected(row);
587            build_row(self, row, is_selected);
588        }
589
590        let ms_io_end = unsafe { sys::igEndMultiSelect() };
591        unsafe {
592            apply_multi_select_requests_indexed(ms_io_end, storage);
593        }
594    }
595
596    /// Multi-select helper using [`BasicSelection`] as underlying storage.
597    ///
598    /// This variant is suitable when items are naturally identified by `ImGuiID`
599    /// (e.g. stable ids for rows or tree nodes).
600    ///
601    /// - `items_count`: number of items in the scope.
602    /// - `id_at_index`: maps `[0, items_count)` to the corresponding item id.
603    /// - `render_item`: called once per index to emit widgets for that item.
604    pub fn multi_select_basic<G, F>(
605        &self,
606        selection: &mut BasicSelection,
607        flags: MultiSelectFlags,
608        items_count: usize,
609        mut id_at_index: G,
610        mut render_item: F,
611    ) where
612        G: FnMut(usize) -> crate::Id,
613        F: FnMut(&Ui, usize, crate::Id, bool),
614    {
615        let selection_size_i32 = i32::try_from(selection.len()).unwrap_or(-1);
616
617        let ms_io_begin = unsafe {
618            sys::igBeginMultiSelect(flags.bits(), selection_size_i32, items_count as i32)
619        };
620
621        unsafe {
622            apply_multi_select_requests_basic(
623                ms_io_begin,
624                selection,
625                items_count,
626                &mut id_at_index,
627            );
628        }
629
630        for idx in 0..items_count {
631            unsafe {
632                sys::igSetNextItemSelectionUserData(idx as sys::ImGuiSelectionUserData);
633            }
634            let id = id_at_index(idx);
635            let is_selected = selection.contains(id);
636            render_item(self, idx, id, is_selected);
637        }
638
639        let ms_io_end = unsafe { sys::igEndMultiSelect() };
640        unsafe {
641            apply_multi_select_requests_basic(ms_io_end, selection, items_count, &mut id_at_index);
642        }
643    }
644}
645
646/// Apply multi-select requests to a `BasicSelection` using an index→id mapping.
647unsafe fn apply_multi_select_requests_basic<G>(
648    ms_io: *mut sys::ImGuiMultiSelectIO,
649    selection: &mut BasicSelection,
650    items_count: usize,
651    id_at_index: &mut G,
652) where
653    G: FnMut(usize) -> crate::Id,
654{
655    unsafe {
656        if ms_io.is_null() {
657            return;
658        }
659
660        let io_ref: &mut sys::ImGuiMultiSelectIO = &mut *ms_io;
661        let requests = &mut io_ref.Requests;
662        if requests.Data.is_null() || requests.Size <= 0 {
663            return;
664        }
665
666        let len = match usize::try_from(requests.Size) {
667            Ok(len) => len,
668            Err(_) => return,
669        };
670        let slice = std::slice::from_raw_parts_mut(requests.Data, len);
671
672        for req in slice {
673            if req.Type == sys::ImGuiSelectionRequestType_SetAll {
674                for idx in 0..items_count {
675                    let id = id_at_index(idx);
676                    selection.set_selected(id, req.Selected);
677                }
678            } else if req.Type == sys::ImGuiSelectionRequestType_SetRange {
679                let first = req.RangeFirstItem as i32;
680                let last = req.RangeLastItem as i32;
681                if first < 0 || last < first {
682                    continue;
683                }
684                let last_clamped = std::cmp::min(last as usize, items_count.saturating_sub(1));
685                for idx in first as usize..=last_clamped {
686                    let id = id_at_index(idx);
687                    selection.set_selected(id, req.Selected);
688                }
689            }
690        }
691    }
692}