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

            
7
use 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
use 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

            
32
use 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.
39
pub type DropDownOnChoiceChangeCallbackType = extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
40
impl_widget_callback!(
41
    DropDownOnChoiceChange,
42
    OptionDropDownOnChoiceChange,
43
    DropDownOnChoiceChangeCallback,
44
    DropDownOnChoiceChangeCallbackType
45
);
46

            
47
azul_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

            
62
const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
63
const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
64
const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
65
    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
66

            
67
// -- Colors --
68

            
69
const BORDER_NORMAL: ColorU = ColorU { r: 172, g: 172, b: 172, a: 255 };
70
const BORDER_HOVER: ColorU = ColorU { r: 126, g: 180, b: 234, a: 255 };
71
const BORDER_FOCUS: ColorU = ColorU { r: 86, g: 157, b: 229, a: 255 };
72

            
73
const BG_GRADIENT_TOP: ColorU = ColorU { r: 245, g: 245, b: 245, a: 255 };
74
const BG_GRADIENT_BOTTOM: ColorU = ColorU { r: 235, g: 235, b: 235, a: 255 };
75
const BG_HOVER_TOP: ColorU = ColorU { r: 234, g: 244, b: 252, a: 255 };
76
const BG_HOVER_BOTTOM: ColorU = ColorU { r: 218, g: 236, b: 252, a: 255 };
77
const BG_ACTIVE_TOP: ColorU = ColorU { r: 218, g: 236, b: 252, a: 255 };
78
const BG_ACTIVE_BOTTOM: ColorU = ColorU { r: 202, g: 226, b: 248, a: 255 };
79

            
80
const 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

            
99
const 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

            
118
const 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

            
139
static 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

            
192
static 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

            
199
static 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)]
212
pub 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

            
221
impl 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

            
231
impl 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

            
311
struct ChoiceCallbackData {
312
    choice_id: usize,
313
    on_choice_change: OptionDropDownOnChoiceChange,
314
}
315

            
316
// ============================================================================
317
// Callbacks
318
// ============================================================================
319

            
320
extern "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

            
351
extern "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

            
367
impl From<DropDown> for Dom {
368
    fn from(b: DropDown) -> Dom {
369
        b.dom()
370
    }
371
}