Skip to main content

fui/controls/
dropdown.rs

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}