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