Skip to main content

azul_layout/widgets/
combobox.rs

1//! Combobox widget — an editable text field with a click-toggled drop-down list
2//! of options. A blend of [`crate::widgets::drop_down::DropDown`] (the list of
3//! options + click-to-select-by-index + `on_select` callback) and
4//! [`crate::widgets::text_input::TextInput`] (the editable text field on top: the
5//! user may type a free value, with `get_text_changeset` insertion + backspace
6//! deletion). The open/close show-hide mirrors
7//! [`crate::widgets::popover::Popover`] (an absolutely-positioned panel toggled
8//! via `set_css_property(display)`), but the panel here holds a list of clickable
9//! options rather than a single native menu popup.
10//!
11//! Structure: a `position: relative` wrapper containing a focusable *input field*
12//! (a text node + a drop-down arrow) followed by an absolutely-positioned
13//! *options list*, hidden by default (`display: none`). A single shared
14//! [`RefAny`] holding the [`ComboBoxStateWrapper`] is attached to every callback
15//! (the field's toggle/text-input/key-down handlers and each option's click
16//! handler) so all of them read and mutate the *same* state — clicking the field
17//! flips `open` and shows/hides the list; clicking an option fills the field with
18//! the option's label (`change_node_text`), sets `selected`, closes the list, and
19//! invokes the optional user `on_select(state)` with the new [`ComboBoxState`].
20//! The clicked option's index is derived from its position (counting previous
21//! siblings), exactly like the index-by-position approach used elsewhere.
22//!
23//! TODO2 — type-to-filter is NOT implemented. Live "filter-as-you-type" requires
24//! the option list to be RE-RENDERED (a DOM rebuild) from the typed text on every
25//! keystroke. Azul widget handlers can only patch *live* state through
26//! `info.set_css_property` / `info.change_node_text` (show/hide/restyle/retext an
27//! existing node) — they cannot add/remove DOM nodes, so the visible option set
28//! cannot be re-filtered from a handler with the tools the other widgets use. The
29//! field is therefore genuinely *editable* (you can type a free value, which is
30//! reported in [`ComboBoxState::text`]), and selecting from the *full* list works
31//! — but the list does not shrink as you type. A future revision could rebuild
32//! the list via a full relayout (`Update::RefreshDom`) driven by a user callback
33//! that owns the items, once that is runtime-verifiable.
34//!
35//! TODO2 — like [`Popover`], the list is placed at a fixed offset below the field
36//! (it does not measure the field's height, flip near a screen edge, escape an
37//! `overflow: hidden` ancestor, or raise its z-order — it relies on being the
38//! later sibling to paint on top). There is no click-outside / blur dismissal
39//! (closing on focus-lost races the option click and could swallow the
40//! selection); the list closes on selection or on clicking the field again.
41//!
42//! Key types: [`ComboBox`], [`ComboBoxState`], [`ComboBoxOnSelect`].
43
44use alloc::{string::String, vec::Vec};
45
46use azul_core::{
47    callbacks::{CoreCallback, CoreCallbackData, Update},
48    dom::{
49        Dom, DomVec, EventFilter, FocusEventFilter, HoverEventFilter, IdOrClass, IdOrClass::Class,
50        IdOrClassVec, TabIndex,
51    },
52    refany::{OptionRefAny, RefAny},
53    window::VirtualKeyCode,
54};
55use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
56use azul_css::{
57    props::{
58        basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, StyleFontSize},
59        layout::{LayoutDisplay, LayoutPosition, LayoutFlexGrow, LayoutMinWidth, LayoutFlexDirection, LayoutAlignItems, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutTop, LayoutLeft},
60        property::{CssProperty, *},
61        style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleCursor, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextColor, StyleTextAlign, StyleUserSelect},
62    },
63    impl_option_inner, AzString, StringVec,
64};
65
66use crate::callbacks::{Callback, CallbackInfo};
67
68static COMBOBOX_WRAPPER_CLASS: &[IdOrClass] =
69    &[Class(AzString::from_const_str("__azul-native-combobox"))];
70static COMBOBOX_INPUT_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
71    "__azul-native-combobox-input",
72))];
73static COMBOBOX_TEXT_CLASS: &[IdOrClass] =
74    &[Class(AzString::from_const_str("__azul-native-combobox-text"))];
75static COMBOBOX_ARROW_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
76    "__azul-native-combobox-arrow",
77))];
78static COMBOBOX_LIST_CLASS: &[IdOrClass] =
79    &[Class(AzString::from_const_str("__azul-native-combobox-list"))];
80static COMBOBOX_OPTION_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
81    "__azul-native-combobox-option",
82))];
83
84const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
85const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
86const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
87    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
88
89// ---- layout (logical px) ----
90/// Fixed vertical offset of the list below the wrapper's top edge (a
91/// simplification — see the module-level `TODO2`; the field is ~26px tall).
92const LIST_OFFSET_Y: isize = 28;
93/// Minimum width of the field and the list.
94const MIN_WIDTH: isize = 160;
95const RADIUS: isize = 4;
96const ARROW_FONT_SIZE_PX: isize = 18;
97
98// ---- colours ----
99const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
100const BORDER_COLOR: ColorU = ColorU { r: 172, g: 172, b: 172, a: 255 }; // #acacac
101const BORDER_FOCUS: ColorU = ColorU { r: 66, g: 134, b: 244, a: 255 }; // #4286f4
102const TEXT_COLOR: ColorU = ColorU { r: 51, g: 51, b: 51, a: 255 }; // #333333
103const OPTION_HOVER_BG: ColorU = ColorU { r: 234, g: 244, b: 252, a: 255 }; // #eaf4fc
104
105const WHITE_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(WHITE)];
106const WHITE_BG_VEC: StyleBackgroundContentVec =
107    StyleBackgroundContentVec::from_const_slice(WHITE_BG_ITEMS);
108const OPTION_HOVER_BG_ITEMS: &[StyleBackgroundContent] =
109    &[StyleBackgroundContent::Color(OPTION_HOVER_BG)];
110const OPTION_HOVER_BG_VEC: StyleBackgroundContentVec =
111    StyleBackgroundContentVec::from_const_slice(OPTION_HOVER_BG_ITEMS);
112
113/// Callback invoked when an option is chosen. The [`ComboBoxState`] carries the
114/// new `selected` index and the field `text` (set to the chosen label).
115pub type ComboBoxOnSelectCallbackType = extern "C" fn(RefAny, CallbackInfo, ComboBoxState) -> Update;
116impl_widget_callback!(
117    ComboBoxOnSelect,
118    OptionComboBoxOnSelect,
119    ComboBoxOnSelectCallback,
120    ComboBoxOnSelectCallbackType
121);
122
123azul_core::impl_managed_callback! {
124    wrapper:        ComboBoxOnSelectCallback,
125    info_ty:        CallbackInfo,
126    return_ty:      Update,
127    default_ret:    Update::DoNothing,
128    invoker_static: COMBOBOX_ON_SELECT_INVOKER,
129    invoker_ty:     AzComboBoxOnSelectCallbackInvoker,
130    thunk_fn:       az_combobox_on_select_callback_thunk,
131    setter_fn:      AzApp_setComboBoxOnSelectCallbackInvoker,
132    from_handle_fn: AzComboBoxOnSelectCallback_createFromHostHandle,
133    extra_args:     [ state: ComboBoxState ],
134}
135
136/// An editable filtered-select widget: a text field plus a click-toggled list of
137/// options.
138#[derive(Debug, Clone, PartialEq, Eq)]
139#[repr(C)]
140pub struct ComboBox {
141    /// Runtime state (`open`/`selected`/`text`) plus the item list and the
142    /// optional select callback.
143    pub combo_state: ComboBoxStateWrapper,
144    /// Greyed text shown in the field when no value has been typed/selected.
145    pub placeholder: AzString,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149#[repr(C)]
150pub struct ComboBoxStateWrapper {
151    /// The mutable per-interaction state passed to `on_select`.
152    pub inner: ComboBoxState,
153    /// The full set of selectable options (rendered into the list).
154    pub items: StringVec,
155    /// Optional: function to call when an option is selected.
156    pub on_select: OptionComboBoxOnSelect,
157}
158
159impl Default for ComboBoxStateWrapper {
160    fn default() -> Self {
161        Self {
162            inner: ComboBoxState::default(),
163            items: StringVec::from_const_slice(&[]),
164            on_select: None.into(),
165        }
166    }
167}
168
169/// The live state of a [`ComboBox`]: whether the list is open, the currently
170/// selected index, and the current (editable) field text.
171#[derive(Debug, Clone, PartialEq, Eq)]
172#[repr(C)]
173pub struct ComboBoxState {
174    /// `true` = list shown, `false` (default) = list hidden.
175    pub open: bool,
176    /// Zero-based index of the most recently selected option.
177    pub selected: usize,
178    /// The current text shown in the field (typed or set from a selection).
179    pub text: AzString,
180}
181
182impl Default for ComboBoxState {
183    fn default() -> Self {
184        Self {
185            open: false,
186            selected: 0,
187            text: AzString::from_const_str(""),
188        }
189    }
190}
191
192// ---- styles ----
193
194/// Wrapper: an inline-block positioning context so the absolutely-positioned list
195/// is placed relative to it.
196static COMBOBOX_WRAPPER_STYLE: &[CssPropertyWithConditions] = &[
197    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
198    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
199    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
200    CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(
201        MIN_WIDTH,
202    ))),
203    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
204    CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
205];
206
207/// The clickable, focusable, editable input field (text + arrow).
208static COMBOBOX_INPUT_STYLE: &[CssPropertyWithConditions] = &[
209    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
210    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
211    CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
212    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
213    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Text)),
214    // padding: 3px 4px
215    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(3))),
216    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
217        LayoutPaddingBottom::const_px(3),
218    )),
219    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
220        4,
221    ))),
222    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
223        LayoutPaddingRight::const_px(4),
224    )),
225    // border: 1px solid #acacac
226    CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
227        LayoutBorderTopWidth::const_px(1),
228    )),
229    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
230        LayoutBorderBottomWidth::const_px(1),
231    )),
232    CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
233        LayoutBorderLeftWidth::const_px(1),
234    )),
235    CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
236        LayoutBorderRightWidth::const_px(1),
237    )),
238    CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
239        inner: BorderStyle::Solid,
240    })),
241    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
242        StyleBorderBottomStyle {
243            inner: BorderStyle::Solid,
244        },
245    )),
246    CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
247        inner: BorderStyle::Solid,
248    })),
249    CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
250        StyleBorderRightStyle {
251            inner: BorderStyle::Solid,
252        },
253    )),
254    CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
255        inner: BORDER_COLOR,
256    })),
257    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
258        StyleBorderBottomColor {
259            inner: BORDER_COLOR,
260        },
261    )),
262    CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
263        inner: BORDER_COLOR,
264    })),
265    CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
266        StyleBorderRightColor {
267            inner: BORDER_COLOR,
268        },
269    )),
270    // border-radius: 4px
271    CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
272        StyleBorderTopLeftRadius::const_px(RADIUS),
273    )),
274    CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
275        StyleBorderTopRightRadius::const_px(RADIUS),
276    )),
277    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
278        StyleBorderBottomLeftRadius::const_px(RADIUS),
279    )),
280    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
281        StyleBorderBottomRightRadius::const_px(RADIUS),
282    )),
283    CssPropertyWithConditions::simple(CssProperty::const_background_content(WHITE_BG_VEC)),
284    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
285        inner: TEXT_COLOR,
286    })),
287    // focus: highlight border
288    CssPropertyWithConditions::on_focus(CssProperty::const_border_top_color(StyleBorderTopColor {
289        inner: BORDER_FOCUS,
290    })),
291    CssPropertyWithConditions::on_focus(CssProperty::const_border_bottom_color(
292        StyleBorderBottomColor {
293            inner: BORDER_FOCUS,
294        },
295    )),
296    CssPropertyWithConditions::on_focus(CssProperty::const_border_left_color(StyleBorderLeftColor {
297        inner: BORDER_FOCUS,
298    })),
299    CssPropertyWithConditions::on_focus(CssProperty::const_border_right_color(
300        StyleBorderRightColor {
301            inner: BORDER_FOCUS,
302        },
303    )),
304];
305
306/// The editable text inside the field — takes the remaining horizontal space.
307static COMBOBOX_TEXT_STYLE: &[CssPropertyWithConditions] = &[
308    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
309    CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
310    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
311        LayoutPaddingRight::const_px(4),
312    )),
313];
314
315/// The drop-down arrow icon on the right of the field.
316static COMBOBOX_ARROW_STYLE: &[CssPropertyWithConditions] = &[
317    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
318    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(
319        ARROW_FONT_SIZE_PX,
320    ))),
321    CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
322];
323
324/// Builds the floating options-list style. Only the `display` (open vs closed)
325/// differs; all positioning/visual props are present in both so the runtime
326/// `set_css_property(display)` toggle has everything it needs (mirroring the
327/// popover/accordion approach).
328fn build_list_style(open: bool) -> CssPropertyWithConditionsVec {
329    let display = if open {
330        LayoutDisplay::Block
331    } else {
332        LayoutDisplay::None
333    };
334    CssPropertyWithConditionsVec::from_vec(alloc::vec![
335        CssPropertyWithConditions::simple(CssProperty::const_display(display)),
336        CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
337        CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(LIST_OFFSET_Y))),
338        CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(0))),
339        CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(
340            MIN_WIDTH,
341        ))),
342        // border: 1px solid #acacac
343        CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
344            LayoutBorderTopWidth::const_px(1),
345        )),
346        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
347            LayoutBorderBottomWidth::const_px(1),
348        )),
349        CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
350            LayoutBorderLeftWidth::const_px(1),
351        )),
352        CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
353            LayoutBorderRightWidth::const_px(1),
354        )),
355        CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
356            inner: BorderStyle::Solid,
357        })),
358        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
359            StyleBorderBottomStyle {
360                inner: BorderStyle::Solid,
361            },
362        )),
363        CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
364            inner: BorderStyle::Solid,
365        })),
366        CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
367            StyleBorderRightStyle {
368                inner: BorderStyle::Solid,
369            },
370        )),
371        CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
372            inner: BORDER_COLOR,
373        })),
374        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
375            StyleBorderBottomColor {
376                inner: BORDER_COLOR,
377            },
378        )),
379        CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
380            inner: BORDER_COLOR,
381        })),
382        CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
383            StyleBorderRightColor {
384                inner: BORDER_COLOR,
385            },
386        )),
387        // border-radius: 4px
388        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
389            StyleBorderBottomLeftRadius::const_px(RADIUS),
390        )),
391        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
392            StyleBorderBottomRightRadius::const_px(RADIUS),
393        )),
394        CssPropertyWithConditions::simple(CssProperty::const_background_content(WHITE_BG_VEC)),
395    ])
396}
397
398/// Per-option row style: a padded, pointer-cursor block highlighted on hover.
399static COMBOBOX_OPTION_STYLE: &[CssPropertyWithConditions] = &[
400    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
401    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(6))),
402    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
403        LayoutPaddingBottom::const_px(6),
404    )),
405    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
406        10,
407    ))),
408    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
409        LayoutPaddingRight::const_px(10),
410    )),
411    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
412    CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
413    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
414        inner: TEXT_COLOR,
415    })),
416    CssPropertyWithConditions::on_hover(CssProperty::const_background_content(OPTION_HOVER_BG_VEC)),
417];
418
419impl ComboBox {
420    /// Creates a new combobox with the given options (no callback, nothing typed).
421    #[must_use] pub fn new(items: StringVec) -> Self {
422        Self {
423            combo_state: ComboBoxStateWrapper {
424                inner: ComboBoxState::default(),
425                items,
426                on_select: None.into(),
427            },
428            placeholder: AzString::from_const_str(""),
429        }
430    }
431
432    /// Creates an empty combobox.
433    #[must_use] pub fn create() -> Self {
434        Self::new(StringVec::from_const_slice(&[]))
435    }
436
437    /// Sets the initially-selected option index.
438    #[inline]
439    pub const fn set_selected(&mut self, selected: usize) {
440        self.combo_state.inner.selected = selected;
441    }
442
443    /// Builder-style setter for the initially-selected index.
444    #[inline]
445    #[must_use] pub const fn with_selected(mut self, selected: usize) -> Self {
446        self.set_selected(selected);
447        self
448    }
449
450    /// Sets the initial (editable) field text.
451    #[inline]
452    pub fn set_text(&mut self, text: AzString) {
453        self.combo_state.inner.text = text;
454    }
455
456    /// Builder-style setter for the initial field text.
457    #[inline]
458    #[must_use] pub fn with_text(mut self, text: AzString) -> Self {
459        self.set_text(text);
460        self
461    }
462
463    /// Sets the greyed placeholder shown when the field is empty.
464    #[inline]
465    pub fn set_placeholder(&mut self, placeholder: AzString) {
466        self.placeholder = placeholder;
467    }
468
469    /// Builder-style setter for the placeholder.
470    #[inline]
471    #[must_use] pub fn with_placeholder(mut self, placeholder: AzString) -> Self {
472        self.set_placeholder(placeholder);
473        self
474    }
475
476    /// Sets the callback invoked when an option is selected.
477    #[inline]
478    pub fn set_on_select<C: Into<ComboBoxOnSelectCallback>>(&mut self, data: RefAny, on_select: C) {
479        self.combo_state.on_select = Some(ComboBoxOnSelect {
480            callback: on_select.into(),
481            refany: data,
482        })
483        .into();
484    }
485
486    /// Builder-style setter for the select callback.
487    #[inline]
488    #[must_use] pub fn with_on_select<C: Into<ComboBoxOnSelectCallback>>(
489        mut self,
490        data: RefAny,
491        on_select: C,
492    ) -> Self {
493        self.set_on_select(data, on_select);
494        self
495    }
496
497    /// Replaces `self` with a default (empty) combobox and returns the original.
498    #[inline]
499    #[must_use] pub fn swap_with_default(&mut self) -> Self {
500        let mut s = Self::create();
501        core::mem::swap(&mut s, self);
502        s
503    }
504
505    /// Renders the combobox into a [`Dom`] subtree with the `__azul-native-combobox`
506    /// class.
507    #[must_use] pub fn dom(self) -> Dom {
508        // Initial field text: the typed/selected text if present, else the
509        // placeholder (a simplification — there is no separate placeholder node,
510        // so the placeholder is just the initial label and is replaced on the
511        // first keystroke or selection).
512        let field_text = if self.combo_state.inner.text.as_str().is_empty() {
513            self.placeholder.clone()
514        } else {
515            self.combo_state.inner.text.clone()
516        };
517
518        let open = self.combo_state.inner.open;
519        let items = self.combo_state.items.clone();
520
521        // ONE shared RefAny: the field handlers and every option handler all
522        // read/mutate the same ComboBoxStateWrapper (the text_input shared-state
523        // pattern), so open/selected/text stay in sync across interactions.
524        let state_ref = RefAny::new(self.combo_state);
525
526        let text_node = Dom::create_text(field_text)
527            .with_ids_and_classes(IdOrClassVec::from_const_slice(COMBOBOX_TEXT_CLASS))
528            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(COMBOBOX_TEXT_STYLE));
529
530        let arrow = Dom::create_icon(AzString::from_const_str("arrow_drop_down"))
531            .with_ids_and_classes(IdOrClassVec::from_const_slice(COMBOBOX_ARROW_CLASS))
532            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(COMBOBOX_ARROW_STYLE));
533
534        // The focusable, editable input field. Clicking it toggles the list
535        // (Hover::MouseUp) and focuses it; typing edits the text node
536        // (Focus::TextInput / VirtualKeyDown), mirroring text_input.
537        let field = Dom::create_div()
538            .with_ids_and_classes(IdOrClassVec::from_const_slice(COMBOBOX_INPUT_CLASS))
539            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(COMBOBOX_INPUT_STYLE))
540            .with_tab_index(TabIndex::Auto)
541            .with_callbacks(
542                alloc::vec![
543                    CoreCallbackData {
544                        event: EventFilter::Hover(HoverEventFilter::MouseUp),
545                        callback: CoreCallback {
546                            cb: on_combobox_toggle as usize,
547                            ctx: OptionRefAny::None,
548                        },
549                        refany: state_ref.clone(),
550                    },
551                    CoreCallbackData {
552                        event: EventFilter::Focus(FocusEventFilter::TextInput),
553                        callback: CoreCallback {
554                            cb: on_combobox_text_input as usize,
555                            ctx: OptionRefAny::None,
556                        },
557                        refany: state_ref.clone(),
558                    },
559                    CoreCallbackData {
560                        event: EventFilter::Focus(FocusEventFilter::VirtualKeyDown),
561                        callback: CoreCallback {
562                            cb: on_combobox_key_down as usize,
563                            ctx: OptionRefAny::None,
564                        },
565                        refany: state_ref.clone(),
566                    },
567                ]
568                .into(),
569            )
570            .with_children(DomVec::from_vec(alloc::vec![text_node, arrow]));
571
572        // Build the option rows. Each carries a CLONE of the shared state so its
573        // click handler can mutate selected/open and read the chosen label.
574        let mut option_doms: Vec<Dom> = Vec::with_capacity(items.as_ref().len());
575        for option in items.as_ref() {
576            option_doms.push(
577                Dom::create_text(option.clone())
578                    .with_ids_and_classes(IdOrClassVec::from_const_slice(COMBOBOX_OPTION_CLASS))
579                    .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
580                        COMBOBOX_OPTION_STYLE,
581                    ))
582                    .with_tab_index(TabIndex::Auto)
583                    .with_callbacks(
584                        alloc::vec![CoreCallbackData {
585                            event: EventFilter::Hover(HoverEventFilter::MouseUp),
586                            callback: CoreCallback {
587                                cb: on_combobox_option_click as usize,
588                                ctx: OptionRefAny::None,
589                            },
590                            refany: state_ref.clone(),
591                        }]
592                        .into(),
593                    ),
594            );
595        }
596
597        let list = Dom::create_div()
598            .with_ids_and_classes(IdOrClassVec::from_const_slice(COMBOBOX_LIST_CLASS))
599            .with_css_props(build_list_style(open))
600            .with_children(DomVec::from_vec(option_doms));
601
602        Dom::create_div()
603            .with_ids_and_classes(IdOrClassVec::from_const_slice(COMBOBOX_WRAPPER_CLASS))
604            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(COMBOBOX_WRAPPER_STYLE))
605            // children: [field, list] — the list is the field's next sibling.
606            .with_children(DomVec::from_vec(alloc::vec![field, list]))
607    }
608}
609
610impl Default for ComboBox {
611    fn default() -> Self {
612        Self::create()
613    }
614}
615
616/// Field click handler. The hit node is the field; its next sibling is the list.
617/// Flips `open` on the shared state and shows/hides the list via `display`.
618extern "C" fn on_combobox_toggle(mut data: RefAny, mut info: CallbackInfo) -> Update {
619    let field = info.get_hit_node();
620    let Some(list) = info.get_next_sibling(field) else {
621        return Update::DoNothing;
622    };
623
624    let now_open = {
625        let Some(mut combo) = data.downcast_mut::<ComboBoxStateWrapper>() else {
626            return Update::DoNothing;
627        };
628        combo.inner.open = !combo.inner.open;
629        combo.inner.open
630    };
631
632    // TODO2: shows/hides the list by toggling `display` via set_css_property; the
633    // display:none/block relayout itself is not GUI-verified in this build.
634    let display = if now_open {
635        LayoutDisplay::Block
636    } else {
637        LayoutDisplay::None
638    };
639    info.set_css_property(list, CssProperty::const_display(display));
640
641    Update::DoNothing
642}
643
644/// Field text-input handler — appends the typed character(s) to the editable
645/// field text (mirroring `text_input`). Does NOT re-filter the list (see the
646/// module-level type-to-filter `TODO2`).
647extern "C" fn on_combobox_text_input(data: RefAny, info: CallbackInfo) -> Update {
648    on_combobox_text_input_inner(data, info).unwrap_or(Update::DoNothing)
649}
650
651fn on_combobox_text_input_inner(mut data: RefAny, mut info: CallbackInfo) -> Option<Update> {
652    let field = info.get_hit_node();
653    let text_node = info.get_first_child(field)?;
654
655    let changeset = info.get_text_changeset()?;
656    let inserted_text = changeset.inserted_text.as_str().to_string();
657    if inserted_text.is_empty() {
658        return None;
659    }
660
661    let new_text = {
662        let mut combo = data.downcast_mut::<ComboBoxStateWrapper>()?;
663        let mut s: String = combo.inner.text.as_str().into();
664        s.push_str(&inserted_text);
665        combo.inner.text = s.clone().into();
666        s
667    };
668
669    info.change_node_text(text_node, new_text.into());
670    Some(Update::DoNothing)
671}
672
673/// Field key-down handler — implements backspace deletion (mirroring `text_input`).
674extern "C" fn on_combobox_key_down(data: RefAny, info: CallbackInfo) -> Update {
675    on_combobox_key_down_inner(data, info).unwrap_or(Update::DoNothing)
676}
677
678fn on_combobox_key_down_inner(mut data: RefAny, mut info: CallbackInfo) -> Option<Update> {
679    let field = info.get_hit_node();
680    let text_node = info.get_first_child(field)?;
681
682    let keyboard_state = info.get_current_keyboard_state();
683    let c = keyboard_state.current_virtual_keycode.into_option()?;
684    if c != VirtualKeyCode::Back {
685        return None;
686    }
687
688    let new_text = {
689        let mut combo = data.downcast_mut::<ComboBoxStateWrapper>()?;
690        let mut s: String = combo.inner.text.as_str().into();
691        s.pop();
692        combo.inner.text = s.clone().into();
693        s
694    };
695
696    info.change_node_text(text_node, new_text.into());
697    Some(Update::DoNothing)
698}
699
700/// Option click handler. The hit node is the clicked option; its index is the
701/// number of previous siblings. Its parent is the list; the list's parent is the
702/// wrapper, whose first child is the field, whose first child is the text node.
703/// Fills the field with the option's label, sets `selected`, closes the list, and
704/// invokes the optional user callback.
705extern "C" fn on_combobox_option_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
706    let option = info.get_hit_node();
707
708    // index = number of previous siblings.
709    let mut index = 0usize;
710    let mut cursor = option;
711    while let Some(prev) = info.get_previous_sibling(cursor) {
712        index += 1;
713        cursor = prev;
714    }
715
716    let Some(list) = info.get_parent(option) else {
717        return Update::DoNothing;
718    };
719    let Some(wrapper) = info.get_parent(list) else {
720        return Update::DoNothing;
721    };
722    let Some(field) = info.get_first_child(wrapper) else {
723        return Update::DoNothing;
724    };
725    let Some(text_node) = info.get_first_child(field) else {
726        return Update::DoNothing;
727    };
728
729    let (label, inner, result) = {
730        let Some(mut combo) = data.downcast_mut::<ComboBoxStateWrapper>() else {
731            return Update::DoNothing;
732        };
733        let Some(label) = combo.items.as_ref().get(index).cloned() else {
734            return Update::DoNothing;
735        };
736        combo.inner.selected = index;
737        combo.inner.text = label.clone();
738        combo.inner.open = false;
739        let inner = combo.inner.clone();
740        let combo = &mut *combo;
741        let result = match combo.on_select.as_mut() {
742            Some(ComboBoxOnSelect { callback, refany }) => {
743                (callback.cb)(refany.clone(), info, inner.clone())
744            }
745            None => Update::DoNothing,
746        };
747        (label, inner, result)
748    };
749    drop(inner);
750
751    // Fill the field with the chosen label and close the list.
752    info.change_node_text(text_node, label);
753    info.set_css_property(list, CssProperty::const_display(LayoutDisplay::None));
754
755    result
756}
757
758impl From<ComboBox> for Dom {
759    fn from(c: ComboBox) -> Self {
760        c.dom()
761    }
762}