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