Skip to main content

dear_imgui_rs/widget/
combo.rs

1//! Combo boxes
2//!
3//! Single-selection dropdowns with optional height and popup alignment flags.
4//! Builders provide both string and custom item sources.
5//!
6use std::borrow::Cow;
7
8use crate::sys;
9use crate::ui::Ui;
10use crate::widget::{ComboBoxFlags, ComboBoxHeight, ComboBoxOptions, ComboBoxPreviewMode};
11
12/// # Combo Box Widgets
13impl Ui {
14    /// Creates a combo box and starts appending to it.
15    ///
16    /// Returns `Some(ComboBoxToken)` if the combo box is open. After content has been
17    /// rendered, the token must be ended by calling `.end()`.
18    ///
19    /// Returns `None` if the combo box is not open and no content should be rendered.
20    #[must_use]
21    #[doc(alias = "BeginCombo")]
22    pub fn begin_combo(
23        &self,
24        label: impl AsRef<str>,
25        preview_value: impl AsRef<str>,
26    ) -> Option<ComboBoxToken<'_>> {
27        self.begin_combo_with_flags(label, preview_value, ComboBoxFlags::NONE)
28    }
29
30    /// Creates a combo box with flags and starts appending to it.
31    ///
32    /// Returns `Some(ComboBoxToken)` if the combo box is open. After content has been
33    /// rendered, the token must be ended by calling `.end()`.
34    ///
35    /// Returns `None` if the combo box is not open and no content should be rendered.
36    #[must_use]
37    #[doc(alias = "BeginCombo")]
38    pub fn begin_combo_with_flags(
39        &self,
40        label: impl AsRef<str>,
41        preview_value: impl AsRef<str>,
42        flags: impl Into<ComboBoxOptions>,
43    ) -> Option<ComboBoxToken<'_>> {
44        let options = flags.into();
45        options.validate("Ui::begin_combo_with_flags()");
46        let (label_ptr, preview_ptr) = self.scratch_txt_two(label, preview_value);
47
48        let should_render = unsafe { sys::igBeginCombo(label_ptr, preview_ptr, options.raw()) };
49
50        if should_render {
51            Some(ComboBoxToken::new(self))
52        } else {
53            None
54        }
55    }
56
57    /// Creates a combo box without preview value.
58    ///
59    /// Returns `Some(ComboBoxToken)` if the combo box is open. After content has been
60    /// rendered, the token must be ended by calling `.end()`.
61    ///
62    /// Returns `None` if the combo box is not open and no content should be rendered.
63    #[must_use]
64    #[doc(alias = "BeginCombo")]
65    pub fn begin_combo_no_preview(&self, label: impl AsRef<str>) -> Option<ComboBoxToken<'_>> {
66        self.begin_combo_no_preview_with_flags(label, ComboBoxFlags::NONE)
67    }
68
69    /// Creates a combo box without preview value and with flags.
70    ///
71    /// Returns `Some(ComboBoxToken)` if the combo box is open. After content has been
72    /// rendered, the token must be ended by calling `.end()`.
73    ///
74    /// Returns `None` if the combo box is not open and no content should be rendered.
75    #[must_use]
76    #[doc(alias = "BeginCombo")]
77    pub fn begin_combo_no_preview_with_flags(
78        &self,
79        label: impl AsRef<str>,
80        flags: impl Into<ComboBoxOptions>,
81    ) -> Option<ComboBoxToken<'_>> {
82        let mut options = flags.into();
83        options.preview_mode = ComboBoxPreviewMode::NoPreview;
84        options.validate("Ui::begin_combo_no_preview_with_flags()");
85        let label_ptr = self.scratch_txt(label);
86
87        let should_render =
88            unsafe { sys::igBeginCombo(label_ptr, std::ptr::null(), options.raw()) };
89
90        if should_render {
91            Some(ComboBoxToken::new(self))
92        } else {
93            None
94        }
95    }
96
97    /// Builds a simple combo box for choosing from a slice of values.
98    #[doc(alias = "Combo")]
99    pub fn combo<V, L>(
100        &self,
101        label: impl AsRef<str>,
102        current_item: &mut usize,
103        items: &[V],
104        label_fn: L,
105    ) -> bool
106    where
107        for<'b> L: Fn(&'b V) -> Cow<'b, str>,
108    {
109        let label_fn = &label_fn;
110        let mut result = false;
111        let preview_value = items.get(*current_item).map(label_fn);
112
113        if let Some(combo_token) = self.begin_combo(
114            label,
115            preview_value.as_ref().map(|s| s.as_ref()).unwrap_or(""),
116        ) {
117            for (idx, item) in items.iter().enumerate() {
118                let is_selected = idx == *current_item;
119                if is_selected {
120                    self.set_item_default_focus();
121                }
122
123                let clicked = self.selectable(label_fn(item).as_ref());
124
125                if clicked {
126                    *current_item = idx;
127                    result = true;
128                }
129            }
130            combo_token.end();
131        }
132
133        result
134    }
135
136    /// Builds a simple combo box using an `i32` index (ImGui-style).
137    ///
138    /// This is useful when you want to represent \"no selection\" with `-1`, matching Dear ImGui's
139    /// `Combo()` API.
140    #[doc(alias = "Combo")]
141    pub fn combo_i32<V, L>(
142        &self,
143        label: impl AsRef<str>,
144        current_item: &mut i32,
145        items: &[V],
146        label_fn: L,
147    ) -> bool
148    where
149        for<'b> L: Fn(&'b V) -> Cow<'b, str>,
150    {
151        let label_fn = &label_fn;
152        let mut result = false;
153
154        let preview_value = if *current_item >= 0 {
155            items.get(*current_item as usize).map(|v| label_fn(v))
156        } else {
157            None
158        };
159
160        if let Some(combo_token) = self.begin_combo(
161            label,
162            preview_value.as_ref().map(|s| s.as_ref()).unwrap_or(""),
163        ) {
164            for (idx, item) in items.iter().enumerate() {
165                if idx > i32::MAX as usize {
166                    break;
167                }
168                let idx_i32 = idx as i32;
169                let is_selected = idx_i32 == *current_item;
170                if is_selected {
171                    self.set_item_default_focus();
172                }
173
174                let clicked = self.selectable(label_fn(item).as_ref());
175                if clicked {
176                    *current_item = idx_i32;
177                    result = true;
178                }
179            }
180            combo_token.end();
181        }
182
183        result
184    }
185
186    /// Builds a simple combo box for choosing from a slice of strings
187    #[doc(alias = "Combo")]
188    pub fn combo_simple_string(
189        &self,
190        label: impl AsRef<str>,
191        current_item: &mut usize,
192        items: &[impl AsRef<str>],
193    ) -> bool {
194        self.combo(label, current_item, items, |s| Cow::Borrowed(s.as_ref()))
195    }
196
197    /// Builds a simple combo box for choosing from a slice of strings using an `i32` index.
198    #[doc(alias = "Combo")]
199    pub fn combo_simple_string_i32(
200        &self,
201        label: impl AsRef<str>,
202        current_item: &mut i32,
203        items: &[impl AsRef<str>],
204    ) -> bool {
205        self.combo_i32(label, current_item, items, |s| Cow::Borrowed(s.as_ref()))
206    }
207
208    /// Sets the default focus for the next item
209    pub fn set_item_default_focus(&self) {
210        unsafe {
211            sys::igSetItemDefaultFocus();
212        }
213    }
214}
215
216/// Builder for a combo box widget
217#[derive(Clone, Debug)]
218#[must_use]
219pub struct ComboBox<'ui, Label, Preview = &'static str> {
220    pub label: Label,
221    pub preview_value: Option<Preview>,
222    pub options: ComboBoxOptions,
223    pub ui: &'ui Ui,
224}
225
226impl<'ui, Label: AsRef<str>> ComboBox<'ui, Label> {
227    /// Sets the preview value
228    pub fn preview_value<P: AsRef<str>>(self, preview: P) -> ComboBox<'ui, Label, P> {
229        ComboBox {
230            label: self.label,
231            preview_value: Some(preview),
232            options: self.options,
233            ui: self.ui,
234        }
235    }
236
237    /// Sets the flags
238    pub fn flags(mut self, flags: ComboBoxFlags) -> Self {
239        self.options.flags = flags;
240        self
241    }
242
243    /// Sets the popup height policy.
244    pub fn height(mut self, height: ComboBoxHeight) -> Self {
245        self.options.height = Some(height);
246        self
247    }
248
249    /// Sets the preview/arrow layout.
250    pub fn preview_mode(mut self, mode: ComboBoxPreviewMode) -> Self {
251        self.options.preview_mode = mode;
252        self
253    }
254
255    /// Creates a combo box and starts appending to it.
256    ///
257    /// Returns `Some(ComboBoxToken)` if the combo box is open. After content has been
258    /// rendered, the token must be ended by calling `.end()`.
259    ///
260    /// Returns `None` if the combo box is not open and no content should be rendered.
261    #[must_use]
262    pub fn begin(self) -> Option<ComboBoxToken<'ui>> {
263        self.options.validate("ComboBox::begin()");
264        let (label_ptr, preview_ptr) = self
265            .ui
266            .scratch_txt_with_opt(self.label.as_ref(), self.preview_value.as_deref());
267
268        let should_render =
269            unsafe { sys::igBeginCombo(label_ptr, preview_ptr, self.options.raw()) };
270
271        if should_render {
272            Some(ComboBoxToken::new(self.ui))
273        } else {
274            None
275        }
276    }
277}
278
279/// Tracks a combo box that can be ended by calling `.end()` or by dropping
280#[must_use]
281pub struct ComboBoxToken<'ui> {
282    _ui: &'ui Ui,
283}
284
285impl<'ui> ComboBoxToken<'ui> {
286    /// Creates a new combo box token
287    fn new(ui: &'ui Ui) -> Self {
288        ComboBoxToken { _ui: ui }
289    }
290
291    /// Ends the combo box
292    pub fn end(self) {
293        // The drop implementation will handle the actual ending
294    }
295}
296
297impl<'ui> Drop for ComboBoxToken<'ui> {
298    fn drop(&mut self) {
299        unsafe {
300            sys::igEndCombo();
301        }
302    }
303}