1use super::control_template_set::get_control_templates;
2use super::internal::dropdown_chevron_presenter::{
3 create_default_dropdown_chevron_presenter, DropdownChevronPresenter, DropdownChevronTemplate,
4 DropdownChevronVisualState,
5};
6use super::internal::dropdown_field_presenter::{
7 create_default_dropdown_field_presenter, DropdownFieldPresenter, DropdownFieldTemplate,
8 DropdownFieldVisualState,
9};
10use super::internal::dropdown_option_row_presenter::DropdownOptionRowTemplate;
11use super::internal::selectable_popup_list::{
12 SelectablePopupList, SelectablePopupListOwner, SELECTABLE_POPUP_LIST_PANEL_PADDING,
13};
14use super::{DropdownChangedEventArgs, DropdownColors, DropdownSizing};
15use crate::bindings::ui;
16use crate::event::{self, KeyEventArgs};
17use crate::ffi::{AlignItems, CursorStyle, FlexDirection, KeyEventType, NodeType, SemanticRole};
18use crate::focus_adorner;
19use crate::focus_visibility;
20use crate::logger;
21use crate::node::{row, BoxStyleSurface, FlexBox, HasFlexBoxRoot, Node, NodeRef, WeakFlexBox};
22use crate::persisted::{persisted_value_adapter, PersistedInt32Codec};
23use crate::signal::SubscriptionGuard;
24use crate::theme::{current_theme, subscribe};
25use crate::ThemeBindable;
26use std::cell::{Cell, RefCell};
27use std::rc::{Rc, Weak};
28
29type DropdownChangedCallback = Rc<dyn Fn(DropdownChangedEventArgs<DropdownItem>)>;
30
31const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
32
33fn is_activation_key(event: &KeyEventArgs) -> bool {
34 event.key == "Enter" || event.key == " " || event.key == "ArrowDown"
35}
36
37fn create_field_presenter(
38 template: Option<Rc<dyn DropdownFieldTemplate>>,
39 sizing: Option<DropdownSizing>,
40) -> Rc<dyn DropdownFieldPresenter> {
41 if let Some(template) = template {
42 return template.create(sizing);
43 }
44 if let Some(template) = get_control_templates().and_then(|set| set.dropdown_field) {
45 return template.create(sizing);
46 }
47 create_default_dropdown_field_presenter(sizing)
48}
49
50fn create_chevron_presenter(
51 template: Option<Rc<dyn DropdownChevronTemplate>>,
52 sizing: Option<DropdownSizing>,
53) -> Rc<dyn DropdownChevronPresenter> {
54 if let Some(template) = template {
55 return template.create(sizing);
56 }
57 if let Some(template) = get_control_templates().and_then(|set| set.dropdown_chevron) {
58 return template.create(sizing);
59 }
60 create_default_dropdown_chevron_presenter(sizing)
61}
62
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct DropdownItem {
65 pub value: String,
66 pub label: String,
67}
68
69impl DropdownItem {
70 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
71 Self {
72 value: value.into(),
73 label: label.into(),
74 }
75 }
76
77 pub fn from_value(value: impl Into<String>) -> Self {
78 let value = value.into();
79 Self {
80 label: value.clone(),
81 value,
82 }
83 }
84}
85
86#[derive(Clone)]
87pub struct Dropdown {
88 root: FlexBox,
89 shared: Rc<DropdownShared>,
90}
91
92struct DropdownShared {
93 self_weak: RefCell<Weak<DropdownShared>>,
94 root: WeakFlexBox,
95 field_template_value: RefCell<Option<Rc<dyn DropdownFieldTemplate>>>,
96 chevron_template_value: RefCell<Option<Rc<dyn DropdownChevronTemplate>>>,
97 option_row_template_value: RefCell<Option<Rc<dyn DropdownOptionRowTemplate>>>,
98 sizing_value: Cell<Option<DropdownSizing>>,
99 colors_value: Cell<Option<DropdownColors>>,
100 field_presenter: RefCell<Rc<dyn DropdownFieldPresenter>>,
101 chevron_presenter: RefCell<Rc<dyn DropdownChevronPresenter>>,
102 popup_list: SelectablePopupList,
103 items_value: RefCell<Vec<DropdownItem>>,
104 open_state: Cell<bool>,
105 pointer_pressed_state: Cell<bool>,
106 hovered_state: Cell<bool>,
107 focused_state: Cell<bool>,
108 key_filter_token: Cell<u32>,
109 selected_index_value: Cell<i32>,
110 highlighted_index_value: Cell<i32>,
111 popup_panel_color_value: Cell<u32>,
112 popup_panel_background_blur_sigma_value: Cell<f32>,
113 popup_panel_color_overridden: Cell<bool>,
114 popup_panel_background_blur_overridden: Cell<bool>,
115 changed_callback: RefCell<Option<DropdownChangedCallback>>,
116 theme_guard: RefCell<Option<SubscriptionGuard>>,
117 focus_visibility_guard: RefCell<Option<SubscriptionGuard>>,
118}
119
120thread_local! {
121 static ACTIVE_DROPDOWN: RefCell<Option<Weak<DropdownShared>>> = const { RefCell::new(None) };
122 static DROPDOWN_SCROLL_HOOK_REGISTERED: Cell<bool> = const { Cell::new(false) };
123}
124
125impl Default for Dropdown {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131impl Dropdown {
132 pub fn new() -> Self {
133 Self::ensure_scroll_hook();
134 let root = row();
135 root.semantic_role(SemanticRole::ComboBox)
136 .focusable(true, 0)
137 .interactive(true)
138 .cursor(CursorStyle::Pointer)
139 .flex_direction(FlexDirection::Row)
140 .align_items(AlignItems::Center)
141 .reflect_semantic_disabled_from_enabled()
142 .default_semantic_label("Dropdown");
143
144 let weak_root = root.downgrade();
145 let field_presenter = create_field_presenter(None, None);
146 field_presenter.root().fill_width();
147 let chevron_presenter = create_chevron_presenter(None, None);
148 field_presenter
149 .chevron_host()
150 .child(&chevron_presenter.root());
151
152 let shared_slot: Rc<RefCell<Option<Weak<DropdownShared>>>> = Rc::new(RefCell::new(None));
153 let owner = SelectablePopupListOwner {
154 item_count: {
155 let shared_slot = shared_slot.clone();
156 Rc::new(move || {
157 shared_slot
158 .borrow()
159 .as_ref()
160 .and_then(Weak::upgrade)
161 .map(|shared| shared.items_value.borrow().len() as i32)
162 .unwrap_or(0)
163 })
164 },
165 item_label: {
166 let shared_slot = shared_slot.clone();
167 Rc::new(move |index| {
168 shared_slot
169 .borrow()
170 .as_ref()
171 .and_then(Weak::upgrade)
172 .and_then(|shared| shared.items_value.borrow().get(index as usize).cloned())
173 .map(|item| item.label)
174 .unwrap_or_default()
175 })
176 },
177 item_selected: {
178 let shared_slot = shared_slot.clone();
179 Rc::new(move |index| {
180 shared_slot
181 .borrow()
182 .as_ref()
183 .and_then(Weak::upgrade)
184 .is_some_and(|shared| shared.selected_index_value.get() == index)
185 })
186 },
187 enabled: {
188 let weak_root = weak_root.clone();
189 Rc::new(move || {
190 weak_root
191 .upgrade()
192 .is_some_and(|root| root.retained_node_ref().is_enabled_for_routing())
193 })
194 },
195 highlight_index: {
196 let shared_slot = shared_slot.clone();
197 Rc::new(move |index| {
198 if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
199 shared.highlight_index(index);
200 }
201 })
202 },
203 activate_index: {
204 let shared_slot = shared_slot.clone();
205 Rc::new(move |index| {
206 if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
207 shared.popup_list_activate_index(index);
208 }
209 })
210 },
211 pointer_down: Rc::new(move |_index| {}),
212 pointer_up: Rc::new(move |_index| {}),
213 };
214 let popup_list = SelectablePopupList::new(owner);
215 let shared = Rc::new(DropdownShared {
216 self_weak: RefCell::new(Weak::new()),
217 root: weak_root,
218 field_template_value: RefCell::new(None),
219 chevron_template_value: RefCell::new(None),
220 option_row_template_value: RefCell::new(None),
221 sizing_value: Cell::new(None),
222 colors_value: Cell::new(None),
223 field_presenter: RefCell::new(field_presenter.clone()),
224 chevron_presenter: RefCell::new(chevron_presenter.clone()),
225 popup_list: popup_list.clone(),
226 items_value: RefCell::new(Vec::new()),
227 open_state: Cell::new(false),
228 pointer_pressed_state: Cell::new(false),
229 hovered_state: Cell::new(false),
230 focused_state: Cell::new(false),
231 key_filter_token: Cell::new(0),
232 selected_index_value: Cell::new(-1),
233 highlighted_index_value: Cell::new(-1),
234 popup_panel_color_value: Cell::new(0x00000000),
235 popup_panel_background_blur_sigma_value: Cell::new(DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA),
236 popup_panel_color_overridden: Cell::new(false),
237 popup_panel_background_blur_overridden: Cell::new(false),
238 changed_callback: RefCell::new(None),
239 theme_guard: RefCell::new(None),
240 focus_visibility_guard: RefCell::new(None),
241 });
242 *shared.self_weak.borrow_mut() = Rc::downgrade(&shared);
243 *shared_slot.borrow_mut() = Some(Rc::downgrade(&shared));
244 root.retained_node_ref().retain_attachment(shared.clone());
245
246 popup_list.popup_presenter.overlay_node().on_pointer_click({
247 let weak_shared = Rc::downgrade(&shared);
248 move |_event| {
249 if let Some(shared) = weak_shared.upgrade() {
250 shared.close();
251 }
252 }
253 });
254
255 root.child(&field_presenter.root()).child(&popup_list.root);
256
257 let control = Self { root, shared };
258 control.install_visual_subscriptions();
259 control.bind_events();
260 control.shared.sync_value_label();
261 control.shared.handle_theme_changed();
262 control.root.persist_state(persisted_value_adapter(
263 "dropdown-selected-index",
264 PersistedInt32Codec,
265 1,
266 {
267 let shared = control.shared.clone();
268 move || {
269 let value = shared.selected_index_value.get();
270 if value >= 0 {
271 Some(value)
272 } else {
273 None
274 }
275 }
276 },
277 {
278 let shared = control.shared.clone();
279 move |value| {
280 shared.set_selected_index(value, true);
281 }
282 },
283 ));
284 control
285 }
286
287 fn ensure_scroll_hook() {
288 DROPDOWN_SCROLL_HOOK_REGISTERED.with(|registered| {
289 if registered.get() {
290 return;
291 }
292 registered.set(true);
293 event::register_scroll_hook(|| {
294 ACTIVE_DROPDOWN.with(|slot| {
295 let Some(shared) = slot.borrow().as_ref().and_then(Weak::upgrade) else {
296 return;
297 };
298 if !shared.is_trigger_visible_in_viewport() {
299 shared.close();
300 }
301 });
302 });
303 });
304 }
305
306 fn install_visual_subscriptions(&self) {
307 *self.shared.theme_guard.borrow_mut() = Some(subscribe({
308 let weak_shared = Rc::downgrade(&self.shared);
309 move |_theme| {
310 if let Some(shared) = weak_shared.upgrade() {
311 shared.handle_theme_changed();
312 }
313 }
314 }));
315 *self.shared.focus_visibility_guard.borrow_mut() = Some(focus_visibility::subscribe({
316 let weak_shared = Rc::downgrade(&self.shared);
317 move |_visible| {
318 if let Some(shared) = weak_shared.upgrade() {
319 shared.sync_focus_chrome();
320 }
321 }
322 }));
323 }
324
325 fn bind_events(&self) {
326 let shared = self.shared.clone();
327 self.root.on_pointer_enter(move |_event| {
328 if !shared.is_enabled() {
329 return;
330 }
331 shared.hovered_state.set(true);
332 shared.handle_theme_changed();
333 });
334
335 let shared = self.shared.clone();
336 self.root.on_pointer_leave(move |_event| {
337 shared.pointer_pressed_state.set(false);
338 shared.hovered_state.set(false);
339 shared.handle_theme_changed();
340 });
341
342 let shared = self.shared.clone();
343 self.root.on_pointer_down(move |_event| {
344 if !shared.is_enabled() {
345 return;
346 }
347 shared.pointer_pressed_state.set(true);
348 shared.handle_theme_changed();
349 });
350
351 let shared = self.shared.clone();
352 self.root.on_pointer_up(move |event| {
353 if !shared.is_enabled() || !shared.pointer_pressed_state.get() {
354 return;
355 }
356 shared.pointer_pressed_state.set(false);
357 if shared.open_state.get() {
358 shared.close();
359 } else {
360 shared.open();
361 }
362 shared.handle_theme_changed();
363 event.handled = true;
364 });
365
366 let shared = self.shared.clone();
367 self.root.on_key_down(move |event| {
368 if shared.handle_global_key_event(
369 KeyEventType::Down,
370 event.key.as_str(),
371 event.modifiers,
372 ) {
373 event.handled = true;
374 return;
375 }
376 if !shared.is_enabled() || event.modifiers != 0 {
377 return;
378 }
379 if !shared.open_state.get() && is_activation_key(event) {
380 shared.open();
381 event.handled = true;
382 return;
383 }
384 if !shared.open_state.get() && event.key == "ArrowUp" {
385 shared.open();
386 shared.move_highlight(-1);
387 event.handled = true;
388 }
389 });
390
391 let shared = self.shared.clone();
392 self.root.on_key_up(move |event| {
393 if shared.handle_global_key_event(KeyEventType::Up, event.key.as_str(), event.modifiers)
394 {
395 event.handled = true;
396 }
397 });
398
399 let shared = self.shared.clone();
400 self.root.on_focus_changed(move |event| {
401 shared.focused_state.set(event.focused);
402 if !event.focused && !shared.open_state.get() {
403 shared.pointer_pressed_state.set(false);
404 }
405 shared.handle_theme_changed();
406 });
407 }
408
409 pub fn selected_index(&self) -> i32 {
410 self.shared.selected_index_value.get()
411 }
412
413 pub fn items<I>(&self, items: I) -> &Self
414 where
415 I: IntoIterator<Item = DropdownItem>,
416 {
417 self.shared.clear_items();
418 self.shared.items_value.borrow_mut().extend(items);
419 let count = self.shared.items_value.borrow().len() as i32;
420 let selected = self.shared.selected_index_value.get();
421 if selected >= count {
422 self.shared
423 .selected_index_value
424 .set(if count > 0 { 0 } else { -1 });
425 } else if selected < 0 && count > 0 {
426 self.shared.selected_index_value.set(0);
427 }
428 self.shared.popup_list.refresh_panel_layout();
429 self.shared.sync_value_label();
430 self.shared.handle_theme_changed();
431 self
432 }
433
434 pub fn on_changed(
435 &self,
436 callback: impl Fn(DropdownChangedEventArgs<DropdownItem>) + 'static,
437 ) -> &Self {
438 *self.shared.changed_callback.borrow_mut() = Some(Rc::new(callback));
439 self
440 }
441
442 pub fn max_visible_items(&self, count: i32) -> &Self {
443 self.shared.popup_list.max_visible_items(count);
444 self
445 }
446
447 pub fn popup_width(&self, value: f32) -> &Self {
448 self.shared.popup_list.popup_width(value);
449 self
450 }
451
452 pub fn popup_panel_color(&self, color: u32) -> &Self {
453 self.shared.popup_panel_color_overridden.set(true);
454 self.shared.popup_panel_color_value.set(color);
455 self.shared.popup_list.panel_node.bg_color(color);
456 self
457 }
458
459 pub fn popup_panel_background_blur(&self, sigma: f32) -> &Self {
460 self.shared.popup_panel_background_blur_overridden.set(true);
461 if sigma < 0.0 {
462 logger::warn(
463 "Layout",
464 &format!("Dropdown.popupPanelBackgroundBlur() received {sigma}; clamping to 0.0."),
465 );
466 }
467 self.shared
468 .popup_panel_background_blur_sigma_value
469 .set(sigma.max(0.0));
470 self.shared
471 .popup_list
472 .panel_node
473 .background_blur(self.shared.popup_panel_background_blur_sigma_value.get());
474 self
475 }
476
477 pub fn sizing(&self, sizing: DropdownSizing) -> &Self {
478 self.set_sizing(Some(sizing))
479 }
480
481 pub fn clear_sizing(&self) -> &Self {
482 self.set_sizing(None)
483 }
484
485 fn set_sizing(&self, sizing: Option<DropdownSizing>) -> &Self {
486 self.shared.close();
487 self.shared.sizing_value.set(sizing);
488 if self.shared.uses_default_field_presenter() {
489 self.shared.replace_field_presenter(
490 create_field_presenter(self.shared.field_template_value.borrow().clone(), sizing),
491 create_chevron_presenter(
492 self.shared.chevron_template_value.borrow().clone(),
493 sizing,
494 ),
495 );
496 } else if self.shared.uses_default_chevron_presenter() {
497 let previous_presenter = self.shared.chevron_presenter.borrow().clone();
498 let next_presenter = create_chevron_presenter(
499 self.shared.chevron_template_value.borrow().clone(),
500 sizing,
501 );
502 *self.shared.chevron_presenter.borrow_mut() = next_presenter.clone();
503 self.shared
504 .field_presenter
505 .borrow()
506 .chevron_host()
507 .remove_child(&previous_presenter.root());
508 self.shared
509 .field_presenter
510 .borrow()
511 .chevron_host()
512 .child(&next_presenter.root());
513 previous_presenter.root().dispose();
514 }
515 self.shared.popup_list.sizing(sizing);
516 self.shared.sync_value_label();
517 self.shared.handle_theme_changed();
518 self
519 }
520
521 pub fn colors(&self, colors: DropdownColors) -> &Self {
522 self.set_colors(Some(colors))
523 }
524
525 pub fn clear_colors(&self) -> &Self {
526 self.set_colors(None)
527 }
528
529 fn set_colors(&self, colors: Option<DropdownColors>) -> &Self {
530 self.shared.colors_value.set(colors);
531 self.shared.popup_list.colors(colors);
532 self.shared.handle_theme_changed();
533 self
534 }
535
536 pub fn field_template(&self, template: Rc<dyn DropdownFieldTemplate>) -> &Self {
537 self.set_field_template(Some(template))
538 }
539
540 pub fn clear_field_template(&self) -> &Self {
541 self.set_field_template(None)
542 }
543
544 fn set_field_template(&self, template: Option<Rc<dyn DropdownFieldTemplate>>) -> &Self {
545 self.shared.close();
546 *self.shared.field_template_value.borrow_mut() = template.clone();
547 let next_field_presenter = create_field_presenter(template, self.shared.sizing_value.get());
548 let next_chevron_presenter = create_chevron_presenter(
549 self.shared.chevron_template_value.borrow().clone(),
550 self.shared.sizing_value.get(),
551 );
552 self.shared
553 .replace_field_presenter(next_field_presenter, next_chevron_presenter);
554 self.shared.sync_value_label();
555 self.shared.handle_theme_changed();
556 self
557 }
558
559 pub fn chevron_template(&self, template: Rc<dyn DropdownChevronTemplate>) -> &Self {
560 self.set_chevron_template(Some(template))
561 }
562
563 pub fn clear_chevron_template(&self) -> &Self {
564 self.set_chevron_template(None)
565 }
566
567 fn set_chevron_template(&self, template: Option<Rc<dyn DropdownChevronTemplate>>) -> &Self {
568 self.shared.close();
569 *self.shared.chevron_template_value.borrow_mut() = template.clone();
570 let previous_presenter = self.shared.chevron_presenter.borrow().clone();
571 let next_presenter = create_chevron_presenter(template, self.shared.sizing_value.get());
572 *self.shared.chevron_presenter.borrow_mut() = next_presenter.clone();
573 self.shared
574 .field_presenter
575 .borrow()
576 .chevron_host()
577 .remove_child(&previous_presenter.root());
578 self.shared
579 .field_presenter
580 .borrow()
581 .chevron_host()
582 .child(&next_presenter.root());
583 previous_presenter.root().dispose();
584 self.shared.handle_theme_changed();
585 self
586 }
587
588 pub fn option_row_template(&self, template: Rc<dyn DropdownOptionRowTemplate>) -> &Self {
589 self.set_option_row_template(Some(template))
590 }
591
592 pub fn clear_option_row_template(&self) -> &Self {
593 self.set_option_row_template(None)
594 }
595
596 fn set_option_row_template(
597 &self,
598 template: Option<Rc<dyn DropdownOptionRowTemplate>>,
599 ) -> &Self {
600 self.shared.close();
601 *self.shared.option_row_template_value.borrow_mut() = template.clone();
602 self.shared.popup_list.option_row_template(template);
603 self
604 }
605
606 pub fn select_index(&self, index: i32) -> &Self {
607 self.shared.set_selected_index(index, false);
608 self
609 }
610
611 pub fn enabled(&self, enabled: bool) -> &Self {
612 self.root.enabled(enabled);
613 if !enabled {
614 self.shared.pointer_pressed_state.set(false);
615 self.shared.hovered_state.set(false);
616 self.shared.close();
617 }
618 self.shared.handle_theme_changed();
619 self
620 }
621}
622
623impl Node for Dropdown {
624 fn retained_node_ref(&self) -> NodeRef {
625 self.root.retained_node_ref()
626 }
627
628 fn build_self(&self) {
629 self.root.build_self();
630 }
631
632 fn dispose(&self) {
633 self.shared.close();
634 focus_adorner::hide_owner(&self.root);
635 *self.shared.theme_guard.borrow_mut() = None;
636 *self.shared.focus_visibility_guard.borrow_mut() = None;
637 self.shared.popup_list.dispose();
638 self.root.dispose();
639 }
640}
641
642impl HasFlexBoxRoot for Dropdown {
643 fn flex_box_root(&self) -> &FlexBox {
644 &self.root
645 }
646}
647
648impl ThemeBindable for Dropdown {
649 fn theme_binding_node(&self) -> NodeRef {
650 self.root.retained_node_ref()
651 }
652
653 fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
654 let weak_root = self.root.downgrade();
655 let weak_shared = Rc::downgrade(&self.shared);
656 Box::new(move || {
657 Some(Dropdown {
658 root: weak_root.upgrade()?,
659 shared: weak_shared.upgrade()?,
660 })
661 })
662 }
663}
664
665impl DropdownShared {
666 fn is_enabled(&self) -> bool {
667 self.root
668 .upgrade()
669 .is_some_and(|root| root.retained_node_ref().is_enabled_for_routing())
670 }
671
672 fn popup_list_activate_index(&self, index: i32) {
673 if index < 0 || index >= self.items_value.borrow().len() as i32 {
674 return;
675 }
676 self.set_selected_index(index, true);
677 self.close();
678 }
679
680 fn handle_global_key_event(&self, event_type: KeyEventType, key: &str, modifiers: u32) -> bool {
681 if !self.open_state.get() || modifiers != 0 || event_type != KeyEventType::Down {
682 return false;
683 }
684 match key {
685 "Escape" => {
686 self.close();
687 true
688 }
689 "Enter" => {
690 self.select_highlighted();
691 true
692 }
693 "Home" => {
694 self.highlight_index(0);
695 true
696 }
697 "End" => {
698 self.highlight_index(self.items_value.borrow().len() as i32 - 1);
699 true
700 }
701 "ArrowDown" => {
702 self.move_highlight(1);
703 true
704 }
705 "ArrowUp" => {
706 self.move_highlight(-1);
707 true
708 }
709 _ => false,
710 }
711 }
712
713 fn set_selected_index(&self, index: i32, emit: bool) {
714 if index == -1 {
715 self.selected_index_value.set(-1);
716 self.highlighted_index_value.set(-1);
717 self.popup_list.set_highlighted_index(-1);
718 self.sync_value_label();
719 self.handle_theme_changed();
720 return;
721 }
722 let count = self.items_value.borrow().len() as i32;
723 if count == 0 {
724 if index != -1 {
725 logger::warn(
726 "Layout",
727 &format!(
728 "Dropdown.selectIndex() received {index} before any items were assigned."
729 ),
730 );
731 }
732 return;
733 }
734 let clamped_index = index.clamp(0, count - 1);
735 if clamped_index != index {
736 logger::warn(
737 "Layout",
738 &format!("Dropdown.selectIndex() received {index}; clamping to {clamped_index}."),
739 );
740 }
741 let changed = self.selected_index_value.get() != clamped_index;
742 self.selected_index_value.set(clamped_index);
743 self.highlighted_index_value.set(clamped_index);
744 self.popup_list.set_highlighted_index(clamped_index);
745 self.sync_value_label();
746 self.handle_theme_changed();
747 if emit && changed {
748 if let Some(root) = self.root.upgrade() {
749 root.request_semantic_announcement();
750 }
751 self.emit_selection_changed();
752 }
753 }
754
755 fn emit_selection_changed(&self) {
756 let selected_index = self.selected_index_value.get();
757 if selected_index < 0 {
758 return;
759 }
760 let Some(item) = self
761 .items_value
762 .borrow()
763 .get(selected_index as usize)
764 .cloned()
765 else {
766 return;
767 };
768 if let Some(callback) = self.changed_callback.borrow().clone() {
769 callback(DropdownChangedEventArgs {
770 item,
771 selected_index,
772 });
773 }
774 }
775
776 fn open(&self) {
777 if self.open_state.get() || self.items_value.borrow().is_empty() {
778 return;
779 }
780 let Some(root) = self.root.upgrade() else {
781 return;
782 };
783 if root.handle() == crate::node::NodeHandle::INVALID {
784 return;
785 }
786 let initial_highlight = if self.selected_index_value.get() >= 0 {
787 self.selected_index_value.get()
788 } else if !self.items_value.borrow().is_empty() {
789 0
790 } else {
791 -1
792 };
793 self.popup_list.set_highlighted_index(initial_highlight);
794 self.highlighted_index_value
795 .set(self.popup_list.highlighted_index());
796 let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
797 return;
798 };
799 if !self.popup_list.open(
800 bounds[0],
801 bounds[1],
802 bounds[2],
803 bounds[3],
804 initial_highlight,
805 ) {
806 return;
807 }
808 self.highlighted_index_value
809 .set(self.popup_list.highlighted_index());
810 self.open_state.set(true);
811 ACTIVE_DROPDOWN.with(|slot| {
812 *slot.borrow_mut() = Some(self.self_weak.borrow().clone());
813 });
814 ui::set_semantic_expanded(root.handle().raw(), true, true);
815 root.request_semantic_announcement();
816 if self.key_filter_token.get() == 0 {
817 let weak_self = self.self_weak.borrow().clone();
818 let token = event::push_key_filter(move |event_type, key, modifiers| {
819 weak_self.upgrade().is_some_and(|shared| {
820 shared.handle_global_key_event(event_type, key, modifiers)
821 })
822 });
823 self.key_filter_token.set(token);
824 }
825 self.handle_theme_changed();
826 }
827
828 fn close(&self) {
829 if !self.open_state.get() && !self.popup_list.is_open() {
830 return;
831 }
832 self.popup_list.close();
833 self.open_state.set(false);
834 ACTIVE_DROPDOWN.with(|slot| {
835 let should_clear = slot
836 .borrow()
837 .as_ref()
838 .and_then(Weak::upgrade)
839 .and_then(|active| active.root.upgrade())
840 .and_then(|active_root| {
841 self.root
842 .upgrade()
843 .map(|root| active_root.handle() == root.handle())
844 })
845 .unwrap_or(false);
846 if should_clear {
847 slot.borrow_mut().take();
848 }
849 });
850 if let Some(root) = self.root.upgrade() {
851 ui::set_semantic_expanded(root.handle().raw(), true, false);
852 root.request_semantic_announcement();
853 }
854 let token = self.key_filter_token.replace(0);
855 if token != 0 {
856 event::remove_key_filter(token);
857 }
858 self.handle_theme_changed();
859 }
860
861 fn sync_value_label(&self) {
862 let selected_index = self.selected_index_value.get();
863 if let Some(item) = self.items_value.borrow().get(selected_index as usize) {
864 self.field_presenter
865 .borrow()
866 .value_node()
867 .text(item.label.clone());
868 if let Some(root) = self.root.upgrade() {
869 root.default_semantic_label(item.label.clone());
870 }
871 return;
872 }
873 self.field_presenter.borrow().value_node().text("");
874 if let Some(root) = self.root.upgrade() {
875 root.default_semantic_label("Dropdown");
876 }
877 }
878
879 fn highlight_index(&self, index: i32) {
880 self.popup_list.highlight_index(index);
881 self.highlighted_index_value
882 .set(self.popup_list.highlighted_index());
883 }
884
885 fn move_highlight(&self, delta: i32) {
886 if self.popup_list.highlighted_index() < 0 && self.selected_index_value.get() >= 0 {
887 self.popup_list
888 .set_highlighted_index(self.selected_index_value.get());
889 }
890 self.popup_list.move_highlight(delta);
891 self.highlighted_index_value
892 .set(self.popup_list.highlighted_index());
893 }
894
895 fn select_highlighted(&self) {
896 let highlighted = self.highlighted_index_value.get();
897 if highlighted < 0 || highlighted >= self.items_value.borrow().len() as i32 {
898 return;
899 }
900 self.set_selected_index(highlighted, true);
901 self.close();
902 }
903
904 fn clear_items(&self) {
905 self.close();
906 self.items_value.borrow_mut().clear();
907 self.selected_index_value.set(-1);
908 self.highlighted_index_value.set(-1);
909 self.popup_list.clear();
910 }
911
912 fn is_trigger_visible_in_viewport(&self) -> bool {
913 let Some(root) = self.root.upgrade() else {
914 return true;
915 };
916 let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
917 return true;
918 };
919 let x = bounds[0];
920 let y = bounds[1];
921 let width = bounds[2];
922 let height = bounds[3];
923 if width <= 0.0 || height <= 0.0 {
924 return true;
925 }
926 let right = x + width;
927 let bottom = y + height;
928 let mut current = root.retained_node_ref().parent();
929 while let Some(node) = current {
930 if node.node_type() == NodeType::ScrollView {
931 if let Some(scroll_bounds) = ui::get_bounds(node.handle().raw()) {
932 let sv_x = scroll_bounds[0];
933 let sv_y = scroll_bounds[1];
934 let sv_right = sv_x + scroll_bounds[2];
935 let sv_bottom = sv_y + scroll_bounds[3];
936 return right > sv_x && bottom > sv_y && x < sv_right && y < sv_bottom;
937 }
938 break;
939 }
940 current = node.parent();
941 }
942 let viewport_width = ui::get_viewport_width();
943 let viewport_height = ui::get_viewport_height();
944 right > 0.0 && bottom > 0.0 && x < viewport_width && y < viewport_height
945 }
946
947 fn handle_theme_changed(&self) {
948 let Some(root) = self.root.upgrade() else {
949 return;
950 };
951 let theme = current_theme();
952 if !self.popup_panel_color_overridden.get() {
953 self.popup_panel_color_value
954 .set(theme.context_menu.panel_background);
955 }
956 if !self.popup_panel_background_blur_overridden.get() {
957 self.popup_panel_background_blur_sigma_value
958 .set(DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA);
959 }
960 root.cursor(if self.is_enabled() {
961 CursorStyle::Pointer
962 } else {
963 CursorStyle::Default
964 });
965 root.corner_radius(0.0)
966 .border(0.0, 0x00000000)
967 .padding(0.0, 0.0, 0.0, 0.0)
968 .bg_color(0x00000000)
969 .opacity(if self.is_enabled() { 1.0 } else { 0.6 });
970
971 self.field_presenter.borrow().root().fill_width();
972 let selected_label = self
973 .items_value
974 .borrow()
975 .get(self.selected_index_value.get() as usize)
976 .map(|item| item.label.clone())
977 .unwrap_or_default();
978 self.field_presenter.borrow().apply(
979 theme.clone(),
980 &DropdownFieldVisualState::new(
981 self.open_state.get(),
982 self.focused_state.get(),
983 self.is_enabled(),
984 self.pointer_pressed_state.get(),
985 selected_label,
986 ),
987 self.colors_value.get(),
988 );
989 self.chevron_presenter.borrow().apply(
990 theme.clone(),
991 DropdownChevronVisualState::new(
992 self.open_state.get(),
993 self.hovered_state.get(),
994 self.is_enabled(),
995 ),
996 );
997 self.popup_list
998 .panel_node
999 .padding(
1000 SELECTABLE_POPUP_LIST_PANEL_PADDING,
1001 SELECTABLE_POPUP_LIST_PANEL_PADDING,
1002 SELECTABLE_POPUP_LIST_PANEL_PADDING,
1003 SELECTABLE_POPUP_LIST_PANEL_PADDING,
1004 )
1005 .corner_radius(theme.spacing.sm)
1006 .bg_color(self.popup_panel_color_value.get())
1007 .border(1.0, theme.context_menu.panel_border_color)
1008 .background_blur(self.popup_panel_background_blur_sigma_value.get())
1009 .drop_shadow(
1010 theme.context_menu.panel_shadow_color,
1011 0.0,
1012 theme.context_menu.shadow_offset_y,
1013 theme.context_menu.shadow_blur,
1014 theme.context_menu.shadow_spread,
1015 );
1016 self.popup_list.popup_scroll_box.bg_color(0x00000000);
1017 self.popup_list.sync_option_visuals();
1018 self.sync_focus_chrome();
1019 }
1020
1021 fn sync_focus_chrome(&self) {
1022 let Some(root) = self.root.upgrade() else {
1023 return;
1024 };
1025 if self.focused_state.get()
1026 && self.is_enabled()
1027 && focus_visibility::keyboard_focus_visible()
1028 {
1029 focus_adorner::show_standard(&root, current_theme().spacing.sm);
1030 } else {
1031 focus_adorner::hide_owner(&root);
1032 }
1033 }
1034
1035 fn replace_field_presenter(
1036 &self,
1037 next_field_presenter: Rc<dyn DropdownFieldPresenter>,
1038 next_chevron_presenter: Rc<dyn DropdownChevronPresenter>,
1039 ) {
1040 let Some(root) = self.root.upgrade() else {
1041 return;
1042 };
1043 let previous_field_root = self.field_presenter.borrow().root();
1044 *self.field_presenter.borrow_mut() = next_field_presenter.clone();
1045 *self.chevron_presenter.borrow_mut() = next_chevron_presenter.clone();
1046 next_field_presenter.root().fill_width();
1047 next_field_presenter
1048 .chevron_host()
1049 .child(&next_chevron_presenter.root());
1050 root.remove_child(&previous_field_root);
1051 root.child(&next_field_presenter.root());
1052 root.child(&self.popup_list.root);
1053 previous_field_root.dispose();
1054 }
1055
1056 fn uses_default_field_presenter(&self) -> bool {
1057 if self.field_template_value.borrow().is_some() {
1058 return false;
1059 }
1060 let template_set = get_control_templates();
1061 template_set.is_none() || template_set.is_some_and(|set| set.dropdown_field.is_none())
1062 }
1063
1064 fn uses_default_chevron_presenter(&self) -> bool {
1065 if self.chevron_template_value.borrow().is_some() {
1066 return false;
1067 }
1068 let template_set = get_control_templates();
1069 template_set.is_none() || template_set.is_some_and(|set| set.dropdown_chevron.is_none())
1070 }
1071}