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