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    /// Independent flags controlling multi-selection behavior.
25    ///
26    /// The click-selection policy, box-select mode, and scope are represented by
27    /// [`MultiSelectClickPolicy`], [`MultiSelectBoxSelect`], and [`MultiSelectScopeKind`].
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        /// Disable drag-scrolling when box-selecting near edges of the scope.
47        const BOX_SELECT_NO_SCROLL = sys::ImGuiMultiSelectFlags_BoxSelectNoScroll as i32;
48        /// Clear selection when pressing Escape while the scope is focused.
49        const CLEAR_ON_ESCAPE = sys::ImGuiMultiSelectFlags_ClearOnEscape as i32;
50        /// Clear selection when clicking on empty space (void) inside the scope.
51        const CLEAR_ON_CLICK_VOID = sys::ImGuiMultiSelectFlags_ClearOnClickVoid as i32;
52        /// Disable default right-click behavior that selects item before opening a context menu.
53        const NO_SELECT_ON_RIGHT_CLICK =
54            sys::ImGuiMultiSelectFlags_NoSelectOnRightClick as i32;
55    }
56}
57
58/// Box-selection geometry for multi-select scopes.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
60pub enum MultiSelectBoxSelect {
61    /// Same x-position/full-row items.
62    OneDimensional,
63    /// Arbitrary item layout, at a higher clipping cost.
64    TwoDimensional,
65}
66
67impl MultiSelectBoxSelect {
68    #[inline]
69    const fn raw(self) -> i32 {
70        match self {
71            Self::OneDimensional => sys::ImGuiMultiSelectFlags_BoxSelect1d as i32,
72            Self::TwoDimensional => sys::ImGuiMultiSelectFlags_BoxSelect2d as i32,
73        }
74    }
75}
76
77/// Click-selection policy for multi-select scopes.
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub enum MultiSelectClickPolicy {
80    /// Apply selection on mouse down for unselected items and on mouse up for
81    /// selected items.
82    Auto,
83    /// Apply selection on mouse down for any clicked item.
84    ClickAlways,
85    /// Apply selection on mouse release for unselected items.
86    ClickRelease,
87}
88
89impl MultiSelectClickPolicy {
90    #[inline]
91    const fn raw(self) -> i32 {
92        match self {
93            Self::Auto => sys::ImGuiMultiSelectFlags_SelectOnAuto as i32,
94            Self::ClickAlways => sys::ImGuiMultiSelectFlags_SelectOnClickAlways as i32,
95            Self::ClickRelease => sys::ImGuiMultiSelectFlags_SelectOnClickRelease as i32,
96        }
97    }
98}
99
100/// Scope for box-select and clear-on-empty-click behavior.
101#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
102pub enum MultiSelectScopeKind {
103    /// Scope is the whole window.
104    Window,
105    /// Scope is the whole window and enables Dear ImGui's temporary X-axis navigation wrap helper.
106    WindowWithNavWrapX,
107    /// Scope is the rectangle between `BeginMultiSelect()` and `EndMultiSelect()`.
108    Rect,
109}
110
111impl MultiSelectScopeKind {
112    #[inline]
113    const fn raw(self) -> i32 {
114        match self {
115            Self::Window => sys::ImGuiMultiSelectFlags_ScopeWindow as i32,
116            Self::WindowWithNavWrapX => {
117                (sys::ImGuiMultiSelectFlags_ScopeWindow | sys::ImGuiMultiSelectFlags_NavWrapX)
118                    as i32
119            }
120            Self::Rect => sys::ImGuiMultiSelectFlags_ScopeRect as i32,
121        }
122    }
123}
124
125/// Complete multi-select options assembled from independent flags and an
126/// optional single-choice policies.
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
128pub struct MultiSelectOptions {
129    pub flags: MultiSelectFlags,
130    pub click_policy: Option<MultiSelectClickPolicy>,
131    pub box_select: Option<MultiSelectBoxSelect>,
132    pub scope: Option<MultiSelectScopeKind>,
133}
134
135impl MultiSelectOptions {
136    pub const fn new() -> Self {
137        Self {
138            flags: MultiSelectFlags::NONE,
139            click_policy: None,
140            box_select: None,
141            scope: None,
142        }
143    }
144
145    pub fn flags(mut self, flags: MultiSelectFlags) -> Self {
146        self.flags = flags;
147        self
148    }
149
150    pub fn click_policy(mut self, policy: MultiSelectClickPolicy) -> Self {
151        self.click_policy = Some(policy);
152        self
153    }
154
155    pub fn box_select(mut self, mode: MultiSelectBoxSelect) -> Self {
156        self.box_select = Some(mode);
157        self
158    }
159
160    pub fn scope(mut self, scope: MultiSelectScopeKind) -> Self {
161        self.scope = Some(scope);
162        self
163    }
164
165    pub fn bits(self) -> i32 {
166        self.raw()
167    }
168
169    #[inline]
170    pub(crate) fn raw(self) -> i32 {
171        self.flags.bits()
172            | self.click_policy.map_or(0, MultiSelectClickPolicy::raw)
173            | self.box_select.map_or(0, MultiSelectBoxSelect::raw)
174            | self.scope.map_or(0, MultiSelectScopeKind::raw)
175    }
176}
177
178impl Default for MultiSelectOptions {
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184impl From<MultiSelectFlags> for MultiSelectOptions {
185    fn from(flags: MultiSelectFlags) -> Self {
186        Self::new().flags(flags)
187    }
188}
189
190/// Selection container backed by Dear ImGui's `ImGuiSelectionBasicStorage`.
191///
192/// This stores a set of selected `ImGuiID` values using the optimized helper
193/// provided by Dear ImGui. It is suitable when items are naturally identified
194/// by stable IDs (e.g. table rows, tree nodes).
195#[derive(Debug)]
196pub struct BasicSelection {
197    raw: *mut sys::ImGuiSelectionBasicStorage,
198}
199
200impl BasicSelection {
201    /// Create an empty selection storage.
202    pub fn new() -> Self {
203        unsafe {
204            let ptr = sys::ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage();
205            if ptr.is_null() {
206                panic!("ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage() returned null");
207            }
208            Self { raw: ptr }
209        }
210    }
211
212    /// Return the number of selected items.
213    pub fn len(&self) -> usize {
214        unsafe {
215            let size = (*self.raw).Size;
216            if size <= 0 { 0 } else { size as usize }
217        }
218    }
219
220    /// Returns true if the selection is empty.
221    pub fn is_empty(&self) -> bool {
222        self.len() == 0
223    }
224
225    /// Clear the selection set.
226    pub fn clear(&mut self) {
227        unsafe {
228            sys::ImGuiSelectionBasicStorage_Clear(self.raw);
229        }
230    }
231
232    /// Returns true if the given id is selected.
233    pub fn contains(&self, id: crate::Id) -> bool {
234        unsafe { sys::ImGuiSelectionBasicStorage_Contains(self.raw, id.raw()) }
235    }
236
237    /// Set selection state for a given id.
238    pub fn set_selected(&mut self, id: crate::Id, selected: bool) {
239        unsafe {
240            sys::ImGuiSelectionBasicStorage_SetItemSelected(self.raw, id.raw(), selected);
241        }
242    }
243
244    /// Iterate over selected ids.
245    pub fn iter(&self) -> BasicSelectionIter<'_> {
246        BasicSelectionIter {
247            storage: self,
248            it: std::ptr::null_mut(),
249        }
250    }
251
252    /// Expose raw pointer for internal helpers.
253    pub(crate) fn as_raw(&self) -> *mut sys::ImGuiSelectionBasicStorage {
254        self.raw
255    }
256}
257
258impl Default for BasicSelection {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264impl Drop for BasicSelection {
265    fn drop(&mut self) {
266        unsafe {
267            if !self.raw.is_null() {
268                sys::ImGuiSelectionBasicStorage_destroy(self.raw);
269                self.raw = std::ptr::null_mut();
270            }
271        }
272    }
273}
274
275/// Iterator over selected ids stored in [`BasicSelection`].
276pub struct BasicSelectionIter<'a> {
277    storage: &'a BasicSelection,
278    it: *mut std::os::raw::c_void,
279}
280
281impl<'a> Iterator for BasicSelectionIter<'a> {
282    type Item = crate::Id;
283
284    fn next(&mut self) -> Option<Self::Item> {
285        unsafe {
286            let mut out_id: sys::ImGuiID = 0;
287            let has_next = sys::ImGuiSelectionBasicStorage_GetNextSelectedItem(
288                self.storage.as_raw(),
289                &mut self.it,
290                &mut out_id,
291            );
292            if has_next {
293                Some(crate::Id::from(out_id))
294            } else {
295                None
296            }
297        }
298    }
299}
300
301/// Index-based selection storage for multi-select helpers.
302///
303/// Implement this trait for your selection container (e.g. `Vec<bool>`,
304/// `Vec<MyItem { selected: bool }>` or a custom type) to use
305/// [`Ui::multi_select_indexed`].
306pub trait MultiSelectIndexStorage {
307    /// Total number of items in the selection scope.
308    fn len(&self) -> usize;
309
310    /// Returns `true` if the selection scope is empty.
311    fn is_empty(&self) -> bool {
312        self.len() == 0
313    }
314
315    /// Returns whether item at `index` is currently selected.
316    fn is_selected(&self, index: usize) -> bool;
317
318    /// Updates selection state for item at `index`.
319    fn set_selected(&mut self, index: usize, selected: bool);
320
321    /// Optional hint for current selection size.
322    ///
323    /// If provided, this is forwarded to `BeginMultiSelect()` to improve the
324    /// behavior of shortcuts such as `ImGuiMultiSelectFlags_ClearOnEscape`.
325    /// When `None` (default), the size is treated as "unknown".
326    fn selected_count_hint(&self) -> Option<usize> {
327        None
328    }
329}
330
331impl MultiSelectIndexStorage for Vec<bool> {
332    fn len(&self) -> usize {
333        self.len()
334    }
335
336    fn is_selected(&self, index: usize) -> bool {
337        self.get(index).copied().unwrap_or(false)
338    }
339
340    fn set_selected(&mut self, index: usize, selected: bool) {
341        if index < self.len() {
342            self[index] = selected;
343        }
344    }
345
346    fn selected_count_hint(&self) -> Option<usize> {
347        // For typical lists this is cheap enough; callers with large datasets
348        // can implement the trait manually with a more efficient counter.
349        Some(self.iter().filter(|&&b| b).count())
350    }
351}
352
353impl MultiSelectIndexStorage for &mut [bool] {
354    fn len(&self) -> usize {
355        (**self).len()
356    }
357
358    fn is_selected(&self, index: usize) -> bool {
359        self.get(index).copied().unwrap_or(false)
360    }
361
362    fn set_selected(&mut self, index: usize, selected: bool) {
363        if index < self.len() {
364            self[index] = selected;
365        }
366    }
367
368    fn selected_count_hint(&self) -> Option<usize> {
369        Some(self.iter().filter(|&&b| b).count())
370    }
371}
372
373/// Index-based selection storage backed by a key slice + `HashSet` of selected keys.
374///
375/// This is convenient when your application stores selection as a set of
376/// arbitrary keys (e.g. `HashSet<u32>` or `HashSet<MyId>`), but you still
377/// want to drive a multi-select scope using contiguous indices.
378pub struct KeySetSelection<'a, K>
379where
380    K: Eq + std::hash::Hash + Copy,
381{
382    keys: &'a [K],
383    selected: &'a mut HashSet<K>,
384}
385
386impl<'a, K> KeySetSelection<'a, K>
387where
388    K: Eq + std::hash::Hash + Copy,
389{
390    /// Create a new index-based view over a key slice and a selection set.
391    ///
392    /// - `keys`: stable index->key mapping (e.g. your backing array).
393    /// - `selected`: set of currently selected keys.
394    pub fn new(keys: &'a [K], selected: &'a mut HashSet<K>) -> Self {
395        Self { keys, selected }
396    }
397}
398
399impl<'a, K> MultiSelectIndexStorage for KeySetSelection<'a, K>
400where
401    K: Eq + std::hash::Hash + Copy,
402{
403    fn len(&self) -> usize {
404        self.keys.len()
405    }
406
407    fn is_selected(&self, index: usize) -> bool {
408        self.keys
409            .get(index)
410            .map(|k| self.selected.contains(k))
411            .unwrap_or(false)
412    }
413
414    fn set_selected(&mut self, index: usize, selected: bool) {
415        if let Some(&key) = self.keys.get(index) {
416            if selected {
417                self.selected.insert(key);
418            } else {
419                self.selected.remove(&key);
420            }
421        }
422    }
423
424    fn selected_count_hint(&self) -> Option<usize> {
425        Some(self.selected.len())
426    }
427}
428
429/// Apply `ImGuiMultiSelectIO` requests to index-based selection storage.
430///
431/// This mirrors `ImGuiSelectionExternalStorage::ApplyRequests` from Dear ImGui,
432/// but operates on the safe [`MultiSelectIndexStorage`] trait instead of relying
433/// on C callbacks.
434unsafe fn apply_multi_select_requests_indexed<S: MultiSelectIndexStorage>(
435    ms_io: *mut sys::ImGuiMultiSelectIO,
436    storage: &mut S,
437) {
438    unsafe {
439        if ms_io.is_null() {
440            return;
441        }
442
443        let io_ref: &mut sys::ImGuiMultiSelectIO = &mut *ms_io;
444        let items_count = usize::try_from(io_ref.ItemsCount).unwrap_or(0);
445
446        let requests = &mut io_ref.Requests;
447        if requests.Data.is_null() || requests.Size <= 0 {
448            return;
449        }
450
451        let len = match usize::try_from(requests.Size) {
452            Ok(len) => len,
453            Err(_) => return,
454        };
455        let slice = std::slice::from_raw_parts_mut(requests.Data, len);
456
457        for req in slice {
458            if req.Type == sys::ImGuiSelectionRequestType_SetAll {
459                for idx in 0..items_count {
460                    storage.set_selected(idx, req.Selected);
461                }
462            } else if req.Type == sys::ImGuiSelectionRequestType_SetRange {
463                let first = req.RangeFirstItem as i32;
464                let last = req.RangeLastItem as i32;
465                if first < 0 || last < first {
466                    continue;
467                }
468                let last_clamped = std::cmp::min(last as usize, items_count.saturating_sub(1));
469                for idx in first as usize..=last_clamped {
470                    storage.set_selected(idx, req.Selected);
471                }
472            }
473        }
474    }
475}
476
477/// RAII wrapper around `BeginMultiSelect()` / `EndMultiSelect()` for advanced users.
478///
479/// This gives direct, but scoped, access to the underlying `ImGuiMultiSelectIO`
480/// struct. It does not perform any selection updates by itself; you are expected
481/// to call helper methods or use the raw IO to drive your own storage.
482pub struct MultiSelectScope<'ui> {
483    ms_io_begin: *mut sys::ImGuiMultiSelectIO,
484    items_count: i32,
485    _marker: std::marker::PhantomData<&'ui Ui>,
486}
487
488impl<'ui> MultiSelectScope<'ui> {
489    fn new(
490        flags: impl Into<MultiSelectOptions>,
491        selection_size: Option<i32>,
492        items_count: usize,
493    ) -> Self {
494        let options = flags.into();
495        let selection_size_i32 = selection_size.unwrap_or(-1);
496        let items_count_i32 = i32::try_from(items_count).unwrap_or(i32::MAX);
497        let ms_io_begin =
498            unsafe { sys::igBeginMultiSelect(options.raw(), selection_size_i32, items_count_i32) };
499        Self {
500            ms_io_begin,
501            items_count: items_count_i32,
502            _marker: std::marker::PhantomData,
503        }
504    }
505
506    /// Access the IO struct returned by `BeginMultiSelect()`.
507    pub fn begin_io(&self) -> &sys::ImGuiMultiSelectIO {
508        unsafe { &*self.ms_io_begin }
509    }
510
511    /// Mutable access to the IO struct returned by `BeginMultiSelect()`.
512    pub fn begin_io_mut(&mut self) -> &mut sys::ImGuiMultiSelectIO {
513        unsafe { &mut *self.ms_io_begin }
514    }
515
516    /// Apply selection requests from `BeginMultiSelect()` to index-based storage.
517    pub fn apply_begin_requests_indexed<S: MultiSelectIndexStorage>(&mut self, storage: &mut S) {
518        unsafe {
519            apply_multi_select_requests_indexed(self.ms_io_begin, storage);
520        }
521    }
522
523    /// Finalize the multi-select scope and return an IO view for the end state.
524    ///
525    /// This calls `EndMultiSelect()` and returns a `MultiSelectEnd` wrapper
526    /// that can be used to apply the final selection requests.
527    pub fn end(self) -> MultiSelectEnd<'ui> {
528        let ms_io_end = unsafe { sys::igEndMultiSelect() };
529        MultiSelectEnd {
530            ms_io_end,
531            items_count: self.items_count,
532            _marker: std::marker::PhantomData,
533        }
534    }
535}
536
537/// IO view returned after calling `EndMultiSelect()` via [`MultiSelectScope::end`].
538pub struct MultiSelectEnd<'ui> {
539    ms_io_end: *mut sys::ImGuiMultiSelectIO,
540    items_count: i32,
541    _marker: std::marker::PhantomData<&'ui Ui>,
542}
543
544impl<'ui> MultiSelectEnd<'ui> {
545    /// Access the IO struct returned by `EndMultiSelect()`.
546    pub fn io(&self) -> &sys::ImGuiMultiSelectIO {
547        unsafe { &*self.ms_io_end }
548    }
549
550    /// Mutable access to the IO struct returned by `EndMultiSelect()`.
551    pub fn io_mut(&mut self) -> &mut sys::ImGuiMultiSelectIO {
552        unsafe { &mut *self.ms_io_end }
553    }
554
555    /// Apply selection requests from `EndMultiSelect()` to index-based storage.
556    pub fn apply_requests_indexed<S: MultiSelectIndexStorage>(&mut self, storage: &mut S) {
557        unsafe {
558            apply_multi_select_requests_indexed(self.ms_io_end, storage);
559        }
560    }
561
562    /// Apply selection requests from `EndMultiSelect()` to a [`BasicSelection`].
563    pub fn apply_requests_basic<G>(&mut self, selection: &mut BasicSelection, mut id_at_index: G)
564    where
565        G: FnMut(usize) -> crate::Id,
566    {
567        unsafe {
568            apply_multi_select_requests_basic(
569                self.ms_io_end,
570                selection,
571                self.items_count as usize,
572                &mut id_at_index,
573            );
574        }
575    }
576}
577
578impl Ui {
579    /// Low-level entry point: begin a multi-select scope and return a RAII wrapper.
580    ///
581    /// This is the closest safe wrapper to the raw `BeginMultiSelect()` /
582    /// `EndMultiSelect()` pair. It does not drive any selection storage by
583    /// itself; use `begin_io()` / `end().io()` and the helper methods to
584    /// implement custom patterns.
585    pub fn begin_multi_select_raw(
586        &self,
587        flags: impl Into<MultiSelectOptions>,
588        selection_size: Option<i32>,
589        items_count: usize,
590    ) -> MultiSelectScope<'_> {
591        MultiSelectScope::new(flags, selection_size, items_count)
592    }
593    /// Multi-select helper for index-based storage.
594    ///
595    /// This wraps `BeginMultiSelect()` / `EndMultiSelect()` and applies
596    /// selection requests to an index-addressable selection container.
597    ///
598    /// Typical usage:
599    ///
600    /// ```no_run
601    /// # use dear_imgui_rs::*;
602    /// # let mut ctx = Context::create();
603    /// # let ui = ctx.frame();
604    /// let mut selected = vec![false; 128];
605    ///
606    /// ui.multi_select_indexed(&mut selected, MultiSelectOptions::new(), |ui, idx, is_selected| {
607    ///     ui.text(format!(
608    ///         "{} {}",
609    ///         if is_selected { "[x]" } else { "[ ]" },
610    ///         idx
611    ///     ));
612    /// });
613    /// ```
614    ///
615    /// Notes:
616    /// - `storage.len()` defines `items_count`.
617    /// - This helper uses the "external storage" pattern where selection is
618    ///   stored entirely on the application side.
619    /// - Per-item selection toggles can be queried via
620    ///   [`Ui::is_item_toggled_selection`].
621    pub fn multi_select_indexed<S, F>(
622        &self,
623        storage: &mut S,
624        flags: impl Into<MultiSelectOptions>,
625        mut render_item: F,
626    ) where
627        S: MultiSelectIndexStorage,
628        F: FnMut(&Ui, usize, bool),
629    {
630        let options = flags.into();
631        let items_count = storage.len();
632        let selection_size_i32 = storage
633            .selected_count_hint()
634            .and_then(|n| i32::try_from(n).ok())
635            .unwrap_or(-1);
636
637        // Begin multi-select scope.
638        let ms_io_begin = unsafe {
639            sys::igBeginMultiSelect(options.raw(), selection_size_i32, items_count as i32)
640        };
641
642        // Apply SetAll requests (if any) before submitting items.
643        unsafe {
644            apply_multi_select_requests_indexed(ms_io_begin, storage);
645        }
646
647        // Submit items: for each index we set SelectionUserData and let user
648        // draw widgets, passing the current selection state as `is_selected`.
649        for idx in 0..items_count {
650            unsafe {
651                sys::igSetNextItemSelectionUserData(idx as sys::ImGuiSelectionUserData);
652            }
653            let is_selected = storage.is_selected(idx);
654            render_item(self, idx, is_selected);
655        }
656
657        // End scope and apply requests generated during item submission.
658        let ms_io_end = unsafe { sys::igEndMultiSelect() };
659        unsafe {
660            apply_multi_select_requests_indexed(ms_io_end, storage);
661        }
662    }
663
664    /// Multi-select helper for index-based storage inside an active table.
665    ///
666    /// This is a convenience wrapper over [`Ui::multi_select_indexed`] that
667    /// automatically advances table rows and starts each row at column 0.
668    ///
669    /// It expects to be called between `BeginTable`/`EndTable`.
670    pub fn table_multi_select_indexed<S, F>(
671        &self,
672        storage: &mut S,
673        flags: impl Into<MultiSelectOptions>,
674        mut build_row: F,
675    ) where
676        S: MultiSelectIndexStorage,
677        F: FnMut(&Ui, usize, bool),
678    {
679        let options = flags.into();
680        let row_count = storage.len();
681        let selection_size_i32 = storage
682            .selected_count_hint()
683            .and_then(|n| i32::try_from(n).ok())
684            .unwrap_or(-1);
685
686        let ms_io_begin =
687            unsafe { sys::igBeginMultiSelect(options.raw(), selection_size_i32, row_count as i32) };
688
689        unsafe {
690            apply_multi_select_requests_indexed(ms_io_begin, storage);
691        }
692
693        for row in 0..row_count {
694            unsafe {
695                sys::igSetNextItemSelectionUserData(row as sys::ImGuiSelectionUserData);
696            }
697            // Start a new table row and move to first column.
698            self.table_next_row();
699            self.table_next_column();
700
701            let is_selected = storage.is_selected(row);
702            build_row(self, row, is_selected);
703        }
704
705        let ms_io_end = unsafe { sys::igEndMultiSelect() };
706        unsafe {
707            apply_multi_select_requests_indexed(ms_io_end, storage);
708        }
709    }
710
711    /// Multi-select helper using [`BasicSelection`] as underlying storage.
712    ///
713    /// This variant is suitable when items are naturally identified by `ImGuiID`
714    /// (e.g. stable ids for rows or tree nodes).
715    ///
716    /// - `items_count`: number of items in the scope.
717    /// - `id_at_index`: maps `[0, items_count)` to the corresponding item id.
718    /// - `render_item`: called once per index to emit widgets for that item.
719    pub fn multi_select_basic<G, F>(
720        &self,
721        selection: &mut BasicSelection,
722        flags: impl Into<MultiSelectOptions>,
723        items_count: usize,
724        mut id_at_index: G,
725        mut render_item: F,
726    ) where
727        G: FnMut(usize) -> crate::Id,
728        F: FnMut(&Ui, usize, crate::Id, bool),
729    {
730        let options = flags.into();
731        let selection_size_i32 = i32::try_from(selection.len()).unwrap_or(-1);
732
733        let ms_io_begin = unsafe {
734            sys::igBeginMultiSelect(options.raw(), selection_size_i32, items_count as i32)
735        };
736
737        unsafe {
738            apply_multi_select_requests_basic(
739                ms_io_begin,
740                selection,
741                items_count,
742                &mut id_at_index,
743            );
744        }
745
746        for idx in 0..items_count {
747            unsafe {
748                sys::igSetNextItemSelectionUserData(idx as sys::ImGuiSelectionUserData);
749            }
750            let id = id_at_index(idx);
751            let is_selected = selection.contains(id);
752            render_item(self, idx, id, is_selected);
753        }
754
755        let ms_io_end = unsafe { sys::igEndMultiSelect() };
756        unsafe {
757            apply_multi_select_requests_basic(ms_io_end, selection, items_count, &mut id_at_index);
758        }
759    }
760}
761
762/// Apply multi-select requests to a `BasicSelection` using an index→id mapping.
763unsafe fn apply_multi_select_requests_basic<G>(
764    ms_io: *mut sys::ImGuiMultiSelectIO,
765    selection: &mut BasicSelection,
766    items_count: usize,
767    id_at_index: &mut G,
768) where
769    G: FnMut(usize) -> crate::Id,
770{
771    unsafe {
772        if ms_io.is_null() {
773            return;
774        }
775
776        let io_ref: &mut sys::ImGuiMultiSelectIO = &mut *ms_io;
777        let requests = &mut io_ref.Requests;
778        if requests.Data.is_null() || requests.Size <= 0 {
779            return;
780        }
781
782        let len = match usize::try_from(requests.Size) {
783            Ok(len) => len,
784            Err(_) => return,
785        };
786        let slice = std::slice::from_raw_parts_mut(requests.Data, len);
787
788        for req in slice {
789            if req.Type == sys::ImGuiSelectionRequestType_SetAll {
790                for idx in 0..items_count {
791                    let id = id_at_index(idx);
792                    selection.set_selected(id, req.Selected);
793                }
794            } else if req.Type == sys::ImGuiSelectionRequestType_SetRange {
795                let first = req.RangeFirstItem as i32;
796                let last = req.RangeLastItem as i32;
797                if first < 0 || last < first {
798                    continue;
799                }
800                let last_clamped = std::cmp::min(last as usize, items_count.saturating_sub(1));
801                for idx in first as usize..=last_clamped {
802                    let id = id_at_index(idx);
803                    selection.set_selected(id, req.Selected);
804                }
805            }
806        }
807    }
808}