1#![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 #[repr(transparent)]
33 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34 pub struct MultiSelectFlags: i32 {
35 const NONE = sys::ImGuiMultiSelectFlags_None as i32;
37 const SINGLE_SELECT = sys::ImGuiMultiSelectFlags_SingleSelect as i32;
39 const NO_SELECT_ALL = sys::ImGuiMultiSelectFlags_NoSelectAll as i32;
41 const NO_RANGE_SELECT = sys::ImGuiMultiSelectFlags_NoRangeSelect as i32;
43 const NO_AUTO_SELECT = sys::ImGuiMultiSelectFlags_NoAutoSelect as i32;
45 const NO_AUTO_CLEAR = sys::ImGuiMultiSelectFlags_NoAutoClear as i32;
47 const NO_AUTO_CLEAR_ON_RESELECT =
49 sys::ImGuiMultiSelectFlags_NoAutoClearOnReselect as i32;
50 const BOX_SELECT_NO_SCROLL = sys::ImGuiMultiSelectFlags_BoxSelectNoScroll as i32;
52 const CLEAR_ON_ESCAPE = sys::ImGuiMultiSelectFlags_ClearOnEscape as i32;
54 const CLEAR_ON_CLICK_VOID = sys::ImGuiMultiSelectFlags_ClearOnClickVoid as i32;
56 const NO_SELECT_ON_RIGHT_CLICK =
58 sys::ImGuiMultiSelectFlags_NoSelectOnRightClick as i32;
59 }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64pub enum MultiSelectBoxSelect {
65 OneDimensional,
67 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
83pub enum MultiSelectClickPolicy {
84 Auto,
87 ClickAlways,
89 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106pub enum MultiSelectScopeKind {
107 Window,
109 WindowWithNavWrapX,
111 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#[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#[derive(Debug)]
200pub struct BasicSelection {
201 raw: *mut sys::ImGuiSelectionBasicStorage,
202}
203
204impl BasicSelection {
205 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 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 pub fn is_empty(&self) -> bool {
226 self.len() == 0
227 }
228
229 pub fn clear(&mut self) {
231 unsafe {
232 sys::ImGuiSelectionBasicStorage_Clear(self.raw);
233 }
234 }
235
236 pub fn contains(&self, id: crate::Id) -> bool {
238 unsafe { sys::ImGuiSelectionBasicStorage_Contains(self.raw, id.raw()) }
239 }
240
241 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 pub fn iter(&self) -> BasicSelectionIter<'_> {
250 BasicSelectionIter {
251 storage: self,
252 it: std::ptr::null_mut(),
253 }
254 }
255
256 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
279pub 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
305pub trait MultiSelectIndexStorage {
311 fn len(&self) -> usize;
313
314 fn is_empty(&self) -> bool {
316 self.len() == 0
317 }
318
319 fn is_selected(&self, index: usize) -> bool;
321
322 fn set_selected(&mut self, index: usize, selected: bool);
324
325 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 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
377pub 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 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
433unsafe 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
481pub 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 pub fn begin_io(&self) -> &sys::ImGuiMultiSelectIO {
514 unsafe { &*self.ms_io_begin }
515 }
516
517 pub fn begin_io_mut(&mut self) -> &mut sys::ImGuiMultiSelectIO {
519 unsafe { &mut *self.ms_io_begin }
520 }
521
522 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 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
555pub 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 pub fn io(&self) -> &sys::ImGuiMultiSelectIO {
565 unsafe { &*self.ms_io_end }
566 }
567
568 pub fn io_mut(&mut self) -> &mut sys::ImGuiMultiSelectIO {
570 unsafe { &mut *self.ms_io_end }
571 }
572
573 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 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 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 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 scope.apply_begin_requests_indexed(storage);
658
659 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 scope.end().apply_requests_indexed(storage);
671 }
672
673 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 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 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
760unsafe 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}