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