Skip to main content

fui/controls/
dropdown.rs

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}