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