Skip to main content

azul_layout/widgets/
drop_down.rs

1//! Native drop-down / select widget.
2//!
3//! Renders a clickable trigger (label + arrow icon) that opens a native
4//! menu popup for item selection.  Depends on [`azul_core::menu`] for
5//! popup rendering.
6
7use azul_core::{
8    callbacks::{CoreCallback, CoreCallbackData, Update},
9    dom::{
10        Dom, DomVec, EventFilter, FocusEventFilter, IdOrClass, IdOrClass::Class, IdOrClassVec,
11        TabIndex,
12    },
13    menu::{Menu, MenuItem, MenuPopupPosition, StringMenuItem},
14    refany::RefAny,
15    window::ContextMenuMouseButton,
16};
17#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
18use azul_css::{
19    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
20    props::{
21        basic::{
22            color::{ColorU, ColorOrSystem},
23            font::{StyleFontFamily, StyleFontFamilyVec},
24            *,
25        },
26        layout::*,
27        property::CssProperty,
28        style::*,
29    },
30    *,
31};
32
33use crate::callbacks::{Callback, CallbackInfo};
34
35// -- Callback type via macro --
36
37/// Callback signature invoked when the user selects a new choice.
38///
39/// The `usize` argument is the zero-based index of the chosen item.
40pub type DropDownOnChoiceChangeCallbackType = extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
41impl_widget_callback!(
42    DropDownOnChoiceChange,
43    OptionDropDownOnChoiceChange,
44    DropDownOnChoiceChangeCallback,
45    DropDownOnChoiceChangeCallbackType
46);
47
48azul_core::impl_managed_callback! {
49    wrapper:        DropDownOnChoiceChangeCallback,
50    info_ty:        CallbackInfo,
51    return_ty:      Update,
52    default_ret:    Update::DoNothing,
53    invoker_static: DROP_DOWN_ON_CHOICE_CHANGE_INVOKER,
54    invoker_ty:     AzDropDownOnChoiceChangeCallbackInvoker,
55    thunk_fn:       az_drop_down_on_choice_change_callback_thunk,
56    setter_fn:      AzApp_setDropDownOnChoiceChangeCallbackInvoker,
57    from_handle_fn: AzDropDownOnChoiceChangeCallback_createFromHostHandle,
58    extra_args:     [ choice_index: usize ],
59}
60
61// -- Font --
62
63const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
64const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
65const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
66    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
67
68// -- Layout constants --
69
70const FONT_SIZE_PX: isize = 13;
71const ARROW_FONT_SIZE_PX: isize = 18;
72const PADDING_HORIZONTAL_PX: isize = 4;
73const PADDING_VERTICAL_PX: isize = 2;
74const LABEL_PADDING_RIGHT_PX: isize = 8;
75const BORDER_WIDTH_PX: isize = 1;
76
77// -- Colors --
78
79const BORDER_NORMAL: ColorU = ColorU { r: 172, g: 172, b: 172, a: 255 };
80const BORDER_HOVER: ColorU = ColorU { r: 126, g: 180, b: 234, a: 255 };
81const BORDER_FOCUS: ColorU = ColorU { r: 86, g: 157, b: 229, a: 255 };
82
83const BG_GRADIENT_TOP: ColorU = ColorU { r: 245, g: 245, b: 245, a: 255 };
84const BG_GRADIENT_BOTTOM: ColorU = ColorU { r: 235, g: 235, b: 235, a: 255 };
85const BG_HOVER_TOP: ColorU = ColorU { r: 234, g: 244, b: 252, a: 255 };
86const BG_HOVER_BOTTOM: ColorU = ColorU { r: 218, g: 236, b: 252, a: 255 };
87const BG_ACTIVE_TOP: ColorU = ColorU { r: 218, g: 236, b: 252, a: 255 };
88const BG_ACTIVE_BOTTOM: ColorU = ColorU { r: 202, g: 226, b: 248, a: 255 };
89
90const NORMAL_BG_ITEMS: &[StyleBackgroundContent] =
91    &[StyleBackgroundContent::LinearGradient(LinearGradient {
92        direction: Direction::FromTo(DirectionCorners {
93            dir_from: DirectionCorner::Top,
94            dir_to: DirectionCorner::Bottom,
95        }),
96        extend_mode: ExtendMode::Clamp,
97        stops: NormalizedLinearColorStopVec::from_const_slice(&[
98            NormalizedLinearColorStop {
99                offset: PercentageValue::const_new(0),
100                color: ColorOrSystem::color(BG_GRADIENT_TOP),
101            },
102            NormalizedLinearColorStop {
103                offset: PercentageValue::const_new(100),
104                color: ColorOrSystem::color(BG_GRADIENT_BOTTOM),
105            },
106        ]),
107    })];
108
109const HOVER_BG_ITEMS: &[StyleBackgroundContent] =
110    &[StyleBackgroundContent::LinearGradient(LinearGradient {
111        direction: Direction::FromTo(DirectionCorners {
112            dir_from: DirectionCorner::Top,
113            dir_to: DirectionCorner::Bottom,
114        }),
115        extend_mode: ExtendMode::Clamp,
116        stops: NormalizedLinearColorStopVec::from_const_slice(&[
117            NormalizedLinearColorStop {
118                offset: PercentageValue::const_new(0),
119                color: ColorOrSystem::color(BG_HOVER_TOP),
120            },
121            NormalizedLinearColorStop {
122                offset: PercentageValue::const_new(100),
123                color: ColorOrSystem::color(BG_HOVER_BOTTOM),
124            },
125        ]),
126    })];
127
128const ACTIVE_BG_ITEMS: &[StyleBackgroundContent] =
129    &[StyleBackgroundContent::LinearGradient(LinearGradient {
130        direction: Direction::FromTo(DirectionCorners {
131            dir_from: DirectionCorner::Top,
132            dir_to: DirectionCorner::Bottom,
133        }),
134        extend_mode: ExtendMode::Clamp,
135        stops: NormalizedLinearColorStopVec::from_const_slice(&[
136            NormalizedLinearColorStop {
137                offset: PercentageValue::const_new(0),
138                color: ColorOrSystem::color(BG_ACTIVE_TOP),
139            },
140            NormalizedLinearColorStop {
141                offset: PercentageValue::const_new(100),
142                color: ColorOrSystem::color(BG_ACTIVE_BOTTOM),
143            },
144        ]),
145    })];
146
147// -- Dropdown wrapper styles (the clickable trigger) --
148
149static DROPDOWN_WRAPPER_STYLE: &[CssPropertyWithConditions] = &[
150    // Layout
151    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineFlex)),
152    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
153    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
154    CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
155    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
156    // Font
157    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(FONT_SIZE_PX))),
158    CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
159    // Padding
160    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(PADDING_HORIZONTAL_PX))),
161    CssPropertyWithConditions::simple(CssProperty::const_padding_right(LayoutPaddingRight::const_px(PADDING_HORIZONTAL_PX))),
162    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(PADDING_VERTICAL_PX))),
163    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(LayoutPaddingBottom::const_px(PADDING_VERTICAL_PX))),
164    // Border
165    CssPropertyWithConditions::simple(CssProperty::const_border_top_width(LayoutBorderTopWidth::const_px(BORDER_WIDTH_PX))),
166    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(LayoutBorderBottomWidth::const_px(BORDER_WIDTH_PX))),
167    CssPropertyWithConditions::simple(CssProperty::const_border_left_width(LayoutBorderLeftWidth::const_px(BORDER_WIDTH_PX))),
168    CssPropertyWithConditions::simple(CssProperty::const_border_right_width(LayoutBorderRightWidth::const_px(BORDER_WIDTH_PX))),
169    CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle { inner: BorderStyle::Solid })),
170    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })),
171    CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle { inner: BorderStyle::Solid })),
172    CssPropertyWithConditions::simple(CssProperty::const_border_right_style(StyleBorderRightStyle { inner: BorderStyle::Solid })),
173    CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor { inner: BORDER_NORMAL })),
174    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(StyleBorderBottomColor { inner: BORDER_NORMAL })),
175    CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor { inner: BORDER_NORMAL })),
176    CssPropertyWithConditions::simple(CssProperty::const_border_right_color(StyleBorderRightColor { inner: BORDER_NORMAL })),
177    // Background
178    CssPropertyWithConditions::simple(CssProperty::const_background_content(
179        StyleBackgroundContentVec::from_const_slice(NORMAL_BG_ITEMS),
180    )),
181    // Hover
182    CssPropertyWithConditions::on_hover(CssProperty::const_border_top_color(StyleBorderTopColor { inner: BORDER_HOVER })),
183    CssPropertyWithConditions::on_hover(CssProperty::const_border_bottom_color(StyleBorderBottomColor { inner: BORDER_HOVER })),
184    CssPropertyWithConditions::on_hover(CssProperty::const_border_left_color(StyleBorderLeftColor { inner: BORDER_HOVER })),
185    CssPropertyWithConditions::on_hover(CssProperty::const_border_right_color(StyleBorderRightColor { inner: BORDER_HOVER })),
186    CssPropertyWithConditions::on_hover(CssProperty::const_background_content(
187        StyleBackgroundContentVec::from_const_slice(HOVER_BG_ITEMS),
188    )),
189    // Active
190    CssPropertyWithConditions::on_active(CssProperty::const_background_content(
191        StyleBackgroundContentVec::from_const_slice(ACTIVE_BG_ITEMS),
192    )),
193    // Focus
194    CssPropertyWithConditions::on_focus(CssProperty::const_border_top_color(StyleBorderTopColor { inner: BORDER_FOCUS })),
195    CssPropertyWithConditions::on_focus(CssProperty::const_border_bottom_color(StyleBorderBottomColor { inner: BORDER_FOCUS })),
196    CssPropertyWithConditions::on_focus(CssProperty::const_border_left_color(StyleBorderLeftColor { inner: BORDER_FOCUS })),
197    CssPropertyWithConditions::on_focus(CssProperty::const_border_right_color(StyleBorderRightColor { inner: BORDER_FOCUS })),
198];
199
200// -- Label text style --
201
202static DROPDOWN_LABEL_STYLE: &[CssPropertyWithConditions] = &[
203    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
204    CssPropertyWithConditions::simple(CssProperty::const_padding_right(LayoutPaddingRight::const_px(LABEL_PADDING_RIGHT_PX))),
205];
206
207// -- Arrow icon style --
208
209static DROPDOWN_ARROW_ICON_STYLE: &[CssPropertyWithConditions] = &[
210    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(ARROW_FONT_SIZE_PX))),
211    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
212];
213
214// ============================================================================
215// Widget struct and API
216// ============================================================================
217
218/// A drop-down / select widget that displays the currently selected item
219/// and opens a native menu popup when focused.
220#[derive(Debug, Clone, PartialEq, Eq)]
221#[repr(C)]
222pub struct DropDown {
223    /// The list of choices presented in the popup menu.
224    pub choices: StringVec,
225    /// Zero-based index of the currently selected choice.
226    pub selected: usize,
227    /// Optional callback invoked when the user picks a different choice.
228    pub on_choice_change: OptionDropDownOnChoiceChange,
229}
230
231impl Default for DropDown {
232    fn default() -> Self {
233        Self {
234            choices: StringVec::from_const_slice(&[]),
235            selected: 0,
236            on_choice_change: None.into(),
237        }
238    }
239}
240
241impl DropDown {
242    /// Creates a new `DropDown` with the given choices and no callback.
243    #[must_use] pub fn new(choices: StringVec) -> Self {
244        Self {
245            choices,
246            selected: 0,
247            on_choice_change: None.into(),
248        }
249    }
250
251    /// Sets the callback invoked when the user selects a different choice.
252    pub fn set_on_choice_change<C: Into<DropDownOnChoiceChangeCallback>>(&mut self, data: RefAny, callback: C) {
253        self.on_choice_change = Some(DropDownOnChoiceChange {
254            callback: callback.into(),
255            refany: data,
256        }).into();
257    }
258
259    /// Builder variant of [`Self::set_on_choice_change`].
260    #[must_use]
261    pub fn with_on_choice_change<C: Into<DropDownOnChoiceChangeCallback>>(mut self, data: RefAny, callback: C) -> Self {
262        self.set_on_choice_change(data, callback);
263        self
264    }
265
266    /// Replaces `self` with the default value and returns the original.
267    #[must_use]
268    pub fn swap_with_default(&mut self) -> Self {
269        let mut m = Self::default();
270        core::mem::swap(&mut m, self);
271        m
272    }
273
274    /// Builds the DOM tree for this drop-down widget.
275    #[must_use] pub fn dom(self) -> Dom {
276        const DROPDOWN_CLASS: &[IdOrClass] =
277            &[Class(AzString::from_const_str("__azul-native-dropdown"))];
278
279        let selected_text = self.choices
280            .as_slice()
281            .get(self.selected)
282            .cloned()
283            .unwrap_or_else(|| AzString::from_const_str(""));
284
285        let refany = RefAny::new(self);
286
287        // Wrapper: focusable trigger that opens popup on focus
288        
289
290        Dom::create_div()
291            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(DROPDOWN_WRAPPER_STYLE))
292            .with_ids_and_classes(IdOrClassVec::from_const_slice(DROPDOWN_CLASS))
293            .with_tab_index(TabIndex::Auto)
294            .with_callbacks(
295                vec![CoreCallbackData {
296                    event: EventFilter::Focus(FocusEventFilter::FocusReceived),
297                    refany,
298                    callback: CoreCallback {
299                        cb: on_dropdown_click as usize,
300                        ctx: azul_core::refany::OptionRefAny::None,
301                    },
302                }]
303                .into(),
304            )
305            .with_children(DomVec::from_vec(vec![
306                // Selected text label wrapped in <p> for proper block formatting
307                Dom::create_p()
308                    .with_css_props(CssPropertyWithConditionsVec::from_const_slice(DROPDOWN_LABEL_STYLE))
309                    .with_children(DomVec::from_vec(vec![
310                        Dom::create_text(selected_text),
311                    ])),
312                // Arrow icon (resolved via Material Icons)
313                Dom::create_icon(AzString::from_const_str("arrow_drop_down"))
314                    .with_css_props(CssPropertyWithConditionsVec::from_const_slice(DROPDOWN_ARROW_ICON_STYLE)),
315            ]))
316    }
317}
318
319// ============================================================================
320// Internal callback data types
321// ============================================================================
322
323struct ChoiceCallbackData {
324    choice_id: usize,
325    on_choice_change: OptionDropDownOnChoiceChange,
326}
327
328// ============================================================================
329// Callbacks
330// ============================================================================
331
332extern "C" fn on_dropdown_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
333    let Some(refany) = refany.downcast_ref::<DropDown>() else {
334        return Update::DoNothing;
335    };
336
337    let menu_items: Vec<MenuItem> = refany
338        .choices
339        .iter()
340        .enumerate()
341        .map(|(idx, choice)| {
342            MenuItem::String(StringMenuItem::create(choice.clone()).with_callback(
343                RefAny::new(ChoiceCallbackData {
344                    choice_id: idx,
345                    on_choice_change: refany.on_choice_change.clone(),
346                }),
347                on_choice_selected as usize,
348            ))
349        })
350        .collect();
351
352    let menu = Menu {
353        items: menu_items.into(),
354        position: MenuPopupPosition::BottomOfHitRect,
355        context_mouse_btn: ContextMenuMouseButton::Right,
356    };
357
358    info.open_menu_for_hit_node(menu);
359    Update::DoNothing
360}
361
362extern "C" fn on_choice_selected(mut refany: RefAny, info: CallbackInfo) -> Update {
363    let Some(mut refany) = refany.downcast_mut::<ChoiceCallbackData>() else {
364        return Update::DoNothing;
365    };
366
367    let choice_id = refany.choice_id;
368
369    match refany.on_choice_change.as_mut() {
370        Some(DropDownOnChoiceChange { refany, callback }) => {
371            (callback.cb)(refany.clone(), info, choice_id)
372        }
373        None => Update::DoNothing,
374    }
375}
376
377impl From<DropDown> for Dom {
378    fn from(b: DropDown) -> Self {
379        b.dom()
380    }
381}