1use 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)] use 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
35pub 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
61const 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
68const 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
77const 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
147static DROPDOWN_WRAPPER_STYLE: &[CssPropertyWithConditions] = &[
150 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 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(FONT_SIZE_PX))),
158 CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
159 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 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 CssPropertyWithConditions::simple(CssProperty::const_background_content(
179 StyleBackgroundContentVec::from_const_slice(NORMAL_BG_ITEMS),
180 )),
181 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 CssPropertyWithConditions::on_active(CssProperty::const_background_content(
191 StyleBackgroundContentVec::from_const_slice(ACTIVE_BG_ITEMS),
192 )),
193 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
200static 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
207static 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#[derive(Debug, Clone, PartialEq, Eq)]
221#[repr(C)]
222pub struct DropDown {
223 pub choices: StringVec,
225 pub selected: usize,
227 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 #[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 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 #[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 #[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 #[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 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 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 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
319struct ChoiceCallbackData {
324 choice_id: usize,
325 on_choice_change: OptionDropDownOnChoiceChange,
326}
327
328extern "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}