Skip to main content

fui/controls/
combobox.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_option_row_presenter::DropdownOptionRowTemplate;
7use super::internal::selectable_popup_list::{
8    SelectablePopupList, SelectablePopupListOwner, SELECTABLE_POPUP_LIST_PANEL_PADDING,
9};
10use super::internal::text_input_presenter::{
11    TextInputPresenter, TextInputTemplate, TextInputVisualState,
12};
13use super::{DropdownColors, DropdownSizing, TextEditorSurface, TextInput, TextInputColors};
14use crate::bindings::ui;
15use crate::event::{self, TextChangedEventArgs};
16use crate::ffi::{
17    AlignItems, CursorStyle, FlexDirection, KeyEventType, KeyModifier, NodeType, SemanticRole, Unit,
18};
19use crate::focus_adorner;
20use crate::focus_visibility;
21use crate::logger;
22use crate::node::{
23    row, BoxStyleSurface, FlexBox, HasFlexBoxRoot, LayoutSurface, Node, NodeRef, TextNode,
24    WeakFlexBox,
25};
26use crate::signal::SubscriptionGuard;
27use crate::theme::{current_theme, subscribe, Theme};
28use crate::{app, frame_scheduler, ThemeBindable};
29use std::cell::{Cell, RefCell};
30use std::rc::{Rc, Weak};
31
32type ComboBoxChangedCallback = Rc<dyn Fn(crate::controls::ComboBoxChangedEventArgs<ComboBoxItem>)>;
33type ComboBoxTextChangedCallback = Rc<dyn Fn(TextChangedEventArgs)>;
34
35const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
36
37fn strings_equal_ignore_case(left: &str, right: &str) -> bool {
38    left.to_lowercase() == right.to_lowercase()
39}
40
41fn string_contains_ignore_case(value: &str, query: &str) -> bool {
42    value.to_lowercase().contains(&query.to_lowercase())
43}
44
45fn string_starts_with_ignore_case(value: &str, query: &str) -> bool {
46    value.to_lowercase().starts_with(&query.to_lowercase())
47}
48
49fn create_chevron_presenter(
50    template: Option<Rc<dyn DropdownChevronTemplate>>,
51    sizing: Option<DropdownSizing>,
52) -> Rc<dyn DropdownChevronPresenter> {
53    if let Some(template) = template {
54        return template.create(sizing);
55    }
56    if let Some(template) = get_control_templates().and_then(|set| set.dropdown_chevron) {
57        return template.create(sizing);
58    }
59    create_default_dropdown_chevron_presenter(sizing)
60}
61
62fn resolve_text_input_colors(
63    colors: Option<DropdownColors>,
64    theme: &Theme,
65) -> Option<TextInputColors> {
66    let colors = colors?;
67    let input_colors = TextInputColors::new();
68    if colors.has_background() {
69        input_colors.background(colors.background_color());
70    }
71    if colors.has_text_primary() {
72        input_colors.text_primary(colors.text_primary_color());
73    }
74    if colors.has_placeholder() {
75        input_colors.placeholder(colors.placeholder_color());
76    }
77    if colors.has_border() {
78        input_colors.border(colors.border_color());
79    }
80    if colors.has_accent() {
81        input_colors
82            .accent(colors.accent_color())
83            .caret(colors.accent_color());
84    } else {
85        input_colors.caret(theme.colors.accent);
86    }
87    Some(input_colors)
88}
89
90#[derive(Clone, Default)]
91struct ComboBoxEditorPresenter {
92    editor_host: RefCell<Option<TextNode>>,
93    placeholder_host: RefCell<Option<FlexBox>>,
94}
95
96impl TextInputPresenter for ComboBoxEditorPresenter {
97    fn bind(&self, editor_host: TextNode, placeholder_host: FlexBox) {
98        *self.editor_host.borrow_mut() = Some(editor_host);
99        *self.placeholder_host.borrow_mut() = Some(placeholder_host);
100    }
101
102    fn present(
103        &self,
104        _theme: Theme,
105        state: &TextInputVisualState,
106        _colors: Option<TextInputColors>,
107    ) -> crate::PresenterHostStyle {
108        let Some(editor_host) = self.editor_host.borrow().clone() else {
109            return crate::PresenterHostStyle::new();
110        };
111        let Some(placeholder_host) = self.placeholder_host.borrow().clone() else {
112            return crate::PresenterHostStyle::new();
113        };
114        let editable_cursor = if state.enabled {
115            CursorStyle::Text
116        } else {
117            CursorStyle::Default
118        };
119        editor_host.cursor(editable_cursor);
120        placeholder_host
121            .position(0.0, 0.0)
122            .width(100.0, Unit::Percent)
123            .cursor(editable_cursor);
124        crate::PresenterHostStyle::new()
125            .background(0x00000000)
126            .corners(crate::Corners::all(0.0))
127            .border(crate::Border::solid(0.0, 0x00000000))
128            .padding(crate::EdgeInsets::all(0.0))
129            .align_items(AlignItems::Center)
130            .cursor(editable_cursor)
131            .opacity(if state.enabled { 1.0 } else { 0.6 })
132    }
133}
134
135#[derive(Clone)]
136struct ComboBoxEditorTemplate;
137
138impl TextInputTemplate for ComboBoxEditorTemplate {
139    fn create(&self) -> Rc<dyn TextInputPresenter> {
140        Rc::new(ComboBoxEditorPresenter::default())
141    }
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum ComboBoxFilterMode {
146    None,
147    StartsWith,
148    Contains,
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub enum ComboBoxCommitMode {
153    KeepText,
154    RevertToSelection,
155    SelectExactMatch,
156}
157
158#[derive(Clone, Debug, PartialEq, Eq)]
159pub struct ComboBoxItem {
160    pub value: String,
161}
162
163impl ComboBoxItem {
164    pub fn new(value: impl Into<String>) -> Self {
165        Self {
166            value: value.into(),
167        }
168    }
169
170    pub fn from_value(value: impl Into<String>) -> Self {
171        Self::new(value)
172    }
173}
174
175#[derive(Clone)]
176pub struct ComboBox {
177    root: FlexBox,
178    shared: Rc<ComboBoxShared>,
179}
180
181struct ComboBoxShared {
182    self_weak: RefCell<Weak<ComboBoxShared>>,
183    root: WeakFlexBox,
184    editor: TextInput,
185    chevron_host: FlexBox,
186    chevron_template_value: RefCell<Option<Rc<dyn DropdownChevronTemplate>>>,
187    sizing_value: Cell<Option<DropdownSizing>>,
188    colors_value: Cell<Option<DropdownColors>>,
189    chevron_presenter: RefCell<Rc<dyn DropdownChevronPresenter>>,
190    popup_list: SelectablePopupList,
191    items_value: RefCell<Vec<ComboBoxItem>>,
192    filtered_indices: RefCell<Vec<i32>>,
193    open_state: Cell<bool>,
194    popup_pointer_pressed_state: Cell<bool>,
195    pointer_pressed_state: Cell<bool>,
196    hovered_state: Cell<bool>,
197    focused_state: Cell<bool>,
198    wrapper_focused_state: Cell<bool>,
199    editor_focused_state: Cell<bool>,
200    deferred_blur_close_pending_state: Cell<bool>,
201    allow_custom_value: Cell<bool>,
202    auto_complete_value: Cell<bool>,
203    open_on_focus_value: Cell<bool>,
204    stays_open_on_edit_value: Cell<bool>,
205    filter_mode_value: Cell<ComboBoxFilterMode>,
206    commit_mode_value: Cell<ComboBoxCommitMode>,
207    key_filter_token: Cell<u32>,
208    selected_index_value: Cell<i32>,
209    committed_selected_index_value: Cell<i32>,
210    highlighted_index_value: Cell<i32>,
211    text_value: RefCell<String>,
212    popup_panel_color_value: Cell<u32>,
213    popup_panel_background_blur_sigma_value: Cell<f32>,
214    popup_panel_color_overridden: Cell<bool>,
215    popup_panel_background_blur_overridden: Cell<bool>,
216    suppress_editor_changed: Cell<bool>,
217    last_auto_complete_text_value: RefCell<String>,
218    changed_callback: RefCell<Option<ComboBoxChangedCallback>>,
219    text_changed_callback: RefCell<Option<ComboBoxTextChangedCallback>>,
220    theme_guard: RefCell<Option<SubscriptionGuard>>,
221    focus_visibility_guard: RefCell<Option<SubscriptionGuard>>,
222}
223
224thread_local! {
225    static ACTIVE_COMBOBOX: RefCell<Option<Weak<ComboBoxShared>>> = const { RefCell::new(None) };
226    static COMBOBOX_SCROLL_HOOK_REGISTERED: Cell<bool> = const { Cell::new(false) };
227}
228
229impl Default for ComboBox {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235impl ComboBox {
236    pub fn new() -> Self {
237        Self::with_text("")
238    }
239
240    pub fn with_text(text: impl Into<String>) -> Self {
241        Self::with_initial_text(text.into())
242    }
243
244    fn with_initial_text(text: String) -> Self {
245        Self::ensure_scroll_hook();
246        let root = row();
247        root.semantic_role(SemanticRole::ComboBox)
248            .focusable(true, 0)
249            .interactive(true)
250            .cursor(CursorStyle::Text)
251            .flex_direction(FlexDirection::Row)
252            .align_items(AlignItems::Center)
253            .reflect_semantic_disabled_from_enabled()
254            .default_semantic_label("Combo box");
255
256        let editor = TextInput::new();
257        editor
258            .text(text.clone())
259            .template(Rc::new(ComboBoxEditorTemplate));
260        editor.fill_width();
261
262        let chevron_presenter = create_chevron_presenter(None, None);
263        let chevron_host = row();
264        chevron_host
265            .width(32.0, Unit::Pixel)
266            .height(100.0, Unit::Percent)
267            .align_items(AlignItems::Center)
268            .justify_content(crate::ffi::JustifyContent::Center)
269            .child(&chevron_presenter.root());
270
271        let weak_root = root.downgrade();
272        let shared_slot: Rc<RefCell<Option<Weak<ComboBoxShared>>>> = Rc::new(RefCell::new(None));
273        let owner = SelectablePopupListOwner {
274            item_count: {
275                let shared_slot = shared_slot.clone();
276                Rc::new(move || {
277                    shared_slot
278                        .borrow()
279                        .as_ref()
280                        .and_then(Weak::upgrade)
281                        .map(|shared| shared.filtered_indices.borrow().len() as i32)
282                        .unwrap_or(0)
283                })
284            },
285            item_label: {
286                let shared_slot = shared_slot.clone();
287                Rc::new(move |index| {
288                    let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) else {
289                        return String::new();
290                    };
291                    let filtered = shared.filtered_indices.borrow();
292                    let Some(source_index) = filtered.get(index as usize).copied() else {
293                        return String::new();
294                    };
295                    let value = shared
296                        .items_value
297                        .borrow()
298                        .get(source_index as usize)
299                        .map(|item| item.value.clone())
300                        .unwrap_or_default();
301                    value
302                })
303            },
304            item_selected: {
305                let shared_slot = shared_slot.clone();
306                Rc::new(move |index| {
307                    let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) else {
308                        return false;
309                    };
310                    let filtered = shared.filtered_indices.borrow();
311                    filtered.get(index as usize).is_some_and(|source_index| {
312                        *source_index == shared.selected_index_value.get()
313                    })
314                })
315            },
316            enabled: {
317                let weak_root = weak_root.clone();
318                Rc::new(move || {
319                    weak_root
320                        .upgrade()
321                        .is_some_and(|root| root.retained_node_ref().is_enabled_for_routing())
322                })
323            },
324            highlight_index: {
325                let shared_slot = shared_slot.clone();
326                Rc::new(move |index| {
327                    if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
328                        shared.highlight_index(index);
329                    }
330                })
331            },
332            activate_index: {
333                let shared_slot = shared_slot.clone();
334                Rc::new(move |index| {
335                    if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
336                        shared.popup_list_activate_index(index);
337                    }
338                })
339            },
340            pointer_down: {
341                let shared_slot = shared_slot.clone();
342                Rc::new(move |_index| {
343                    if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
344                        shared.popup_pointer_pressed_state.set(true);
345                    }
346                })
347            },
348            pointer_up: {
349                let shared_slot = shared_slot.clone();
350                Rc::new(move |_index| {
351                    if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
352                        shared.popup_list_pointer_up();
353                    }
354                })
355            },
356        };
357        let popup_list = SelectablePopupList::new(owner);
358        let shared = Rc::new(ComboBoxShared {
359            self_weak: RefCell::new(Weak::new()),
360            root: weak_root,
361            editor: editor.clone(),
362            chevron_host: chevron_host.clone(),
363            chevron_template_value: RefCell::new(None),
364            sizing_value: Cell::new(None),
365            colors_value: Cell::new(None),
366            chevron_presenter: RefCell::new(chevron_presenter.clone()),
367            popup_list: popup_list.clone(),
368            items_value: RefCell::new(Vec::new()),
369            filtered_indices: RefCell::new(Vec::new()),
370            open_state: Cell::new(false),
371            popup_pointer_pressed_state: Cell::new(false),
372            pointer_pressed_state: Cell::new(false),
373            hovered_state: Cell::new(false),
374            focused_state: Cell::new(false),
375            wrapper_focused_state: Cell::new(false),
376            editor_focused_state: Cell::new(false),
377            deferred_blur_close_pending_state: Cell::new(false),
378            allow_custom_value: Cell::new(true),
379            auto_complete_value: Cell::new(false),
380            open_on_focus_value: Cell::new(false),
381            stays_open_on_edit_value: Cell::new(true),
382            filter_mode_value: Cell::new(ComboBoxFilterMode::Contains),
383            commit_mode_value: Cell::new(ComboBoxCommitMode::KeepText),
384            key_filter_token: Cell::new(0),
385            selected_index_value: Cell::new(-1),
386            committed_selected_index_value: Cell::new(-1),
387            highlighted_index_value: Cell::new(-1),
388            text_value: RefCell::new(text),
389            popup_panel_color_value: Cell::new(0x00000000),
390            popup_panel_background_blur_sigma_value: Cell::new(DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA),
391            popup_panel_color_overridden: Cell::new(false),
392            popup_panel_background_blur_overridden: Cell::new(false),
393            suppress_editor_changed: Cell::new(false),
394            last_auto_complete_text_value: RefCell::new(String::new()),
395            changed_callback: RefCell::new(None),
396            text_changed_callback: RefCell::new(None),
397            theme_guard: RefCell::new(None),
398            focus_visibility_guard: RefCell::new(None),
399        });
400        *shared.self_weak.borrow_mut() = Rc::downgrade(&shared);
401        *shared_slot.borrow_mut() = Some(Rc::downgrade(&shared));
402        root.retained_node_ref().retain_attachment(shared.clone());
403        shared.rebuild_filtered_indices();
404
405        popup_list.popup_presenter.overlay_node().on_pointer_click({
406            let weak_shared = Rc::downgrade(&shared);
407            move |_event| {
408                if let Some(shared) = weak_shared.upgrade() {
409                    shared.close();
410                }
411            }
412        });
413
414        root.child(&editor)
415            .child(&chevron_host)
416            .child(&popup_list.root);
417
418        let control = Self { root, shared };
419        control.install_visual_subscriptions();
420        control.install_effective_enabled_subscription();
421        control.bind_events();
422        control.shared.sync_semantic_label();
423        control.shared.handle_theme_changed();
424        control
425    }
426
427    fn ensure_scroll_hook() {
428        COMBOBOX_SCROLL_HOOK_REGISTERED.with(|registered| {
429            if registered.get() {
430                return;
431            }
432            registered.set(true);
433            event::register_scroll_hook(|| {
434                ACTIVE_COMBOBOX.with(|slot| {
435                    let Some(shared) = slot.borrow().as_ref().and_then(Weak::upgrade) else {
436                        return;
437                    };
438                    if !shared.is_trigger_visible_in_viewport() {
439                        shared.close();
440                    }
441                });
442            });
443        });
444    }
445
446    fn install_visual_subscriptions(&self) {
447        *self.shared.theme_guard.borrow_mut() = Some(subscribe({
448            let weak_shared = Rc::downgrade(&self.shared);
449            move |_theme| {
450                if let Some(shared) = weak_shared.upgrade() {
451                    shared.handle_theme_changed();
452                }
453            }
454        }));
455        *self.shared.focus_visibility_guard.borrow_mut() = Some(focus_visibility::subscribe({
456            let weak_shared = Rc::downgrade(&self.shared);
457            move |_visible| {
458                if let Some(shared) = weak_shared.upgrade() {
459                    shared.sync_focus_chrome();
460                }
461            }
462        }));
463    }
464
465    fn install_effective_enabled_subscription(&self) {
466        let weak_shared = Rc::downgrade(&self.shared);
467        self.root
468            .retained_node_ref()
469            .on_effective_enabled_changed(Rc::new(move |_enabled| {
470                let Some(shared) = weak_shared.upgrade() else {
471                    return;
472                };
473                shared.editor.enabled(shared.is_enabled());
474                if !shared.is_enabled() {
475                    shared.pointer_pressed_state.set(false);
476                    shared.hovered_state.set(false);
477                    shared.close();
478                }
479                shared.handle_theme_changed();
480            }));
481    }
482
483    fn bind_events(&self) {
484        let shared = self.shared.clone();
485        self.root.on_pointer_enter(move |_event| {
486            if !shared.is_enabled() {
487                return;
488            }
489            shared.hovered_state.set(true);
490            shared.handle_theme_changed();
491        });
492
493        let shared = self.shared.clone();
494        self.root.on_pointer_leave(move |_event| {
495            shared.pointer_pressed_state.set(false);
496            shared.hovered_state.set(false);
497            shared.handle_theme_changed();
498        });
499
500        let shared = self.shared.clone();
501        self.root.on_pointer_down(move |_event| {
502            if !shared.is_enabled() {
503                return;
504            }
505            shared.pointer_pressed_state.set(true);
506            shared.editor.focus_now();
507            shared.handle_theme_changed();
508        });
509
510        let shared = self.shared.clone();
511        self.root.on_pointer_up(move |event| {
512            if !shared.is_enabled() || !shared.pointer_pressed_state.get() {
513                return;
514            }
515            shared.pointer_pressed_state.set(false);
516            shared.toggle_open();
517            shared.handle_theme_changed();
518            event.handled = true;
519        });
520
521        let shared = self.shared.clone();
522        self.root.on_key_down(move |event| {
523            if shared.handle_global_key_event(
524                KeyEventType::Down,
525                event.key.as_str(),
526                event.modifiers,
527            ) {
528                event.handled = true;
529                return;
530            }
531            if !shared.is_enabled() || event.modifiers != 0 {
532                return;
533            }
534            if !shared.open_state.get()
535                && (event.key == "Enter" || event.key == " " || event.key == "ArrowDown")
536            {
537                shared.open();
538                event.handled = true;
539                return;
540            }
541            if !shared.open_state.get() && event.key == "ArrowUp" {
542                shared.open();
543                shared.move_highlight(-1);
544                event.handled = true;
545            }
546        });
547
548        let shared = self.shared.clone();
549        self.root.on_key_up(move |event| {
550            if shared.handle_global_key_event(KeyEventType::Up, event.key.as_str(), event.modifiers)
551            {
552                event.handled = true;
553            }
554        });
555
556        let shared = self.shared.clone();
557        self.root.on_focus_changed(move |event| {
558            shared.wrapper_focused_state.set(event.focused);
559            if !event.focused && !shared.open_state.get() {
560                shared.pointer_pressed_state.set(false);
561            }
562            shared.sync_focused_state();
563        });
564
565        let weak_shared = Rc::downgrade(&self.shared);
566        self.shared.editor.on_changed(move |event| {
567            let Some(shared) = weak_shared.upgrade() else {
568                return;
569            };
570            shared.handle_editor_text_changed(event.text);
571        });
572
573        let weak_shared = Rc::downgrade(&self.shared);
574        self.shared.editor.on_focus_changed(move |event| {
575            let Some(shared) = weak_shared.upgrade() else {
576                return;
577            };
578            shared.handle_editor_focus_changed(event.focused);
579        });
580
581        let weak_shared = Rc::downgrade(&self.shared);
582        self.shared.editor.editor_node().on_key_down(move |event| {
583            let Some(shared) = weak_shared.upgrade() else {
584                return;
585            };
586            if shared.handle_editor_key_down(event.key.as_str(), event.modifiers) {
587                event.handled = true;
588            }
589        });
590        self.shared.editor.editor_node().editor_command_keys(true);
591
592        let weak_shared = Rc::downgrade(&self.shared);
593        self.shared.chevron_host.on_pointer_click(move |event| {
594            let Some(shared) = weak_shared.upgrade() else {
595                return;
596            };
597            if !shared.is_enabled() {
598                return;
599            }
600            shared.toggle_from_chevron();
601            event.handled = true;
602        });
603
604        let weak_shared = Rc::downgrade(&self.shared);
605        self.shared.chevron_host.on_pointer_enter(move |_event| {
606            let Some(shared) = weak_shared.upgrade() else {
607                return;
608            };
609            shared.hovered_state.set(true);
610            shared.handle_theme_changed();
611        });
612
613        let weak_shared = Rc::downgrade(&self.shared);
614        self.shared.chevron_host.on_pointer_leave(move |_event| {
615            let Some(shared) = weak_shared.upgrade() else {
616                return;
617            };
618            shared.pointer_pressed_state.set(false);
619            shared.hovered_state.set(false);
620            shared.handle_theme_changed();
621        });
622
623        let weak_shared = Rc::downgrade(&self.shared);
624        self.shared.chevron_host.on_pointer_down(move |_event| {
625            let Some(shared) = weak_shared.upgrade() else {
626                return;
627            };
628            shared.pointer_pressed_state.set(true);
629            shared.focus_editor_from_chevron();
630            shared.handle_theme_changed();
631        });
632
633        let weak_shared = Rc::downgrade(&self.shared);
634        self.shared.chevron_host.on_pointer_up(move |_event| {
635            let Some(shared) = weak_shared.upgrade() else {
636                return;
637            };
638            shared.pointer_pressed_state.set(false);
639            shared.handle_theme_changed();
640        });
641    }
642
643    pub fn selected_index(&self) -> i32 {
644        self.shared.selected_index_value.get()
645    }
646
647    pub fn value(&self) -> String {
648        self.shared.text_value.borrow().clone()
649    }
650
651    pub fn filtered_count(&self) -> usize {
652        self.shared.filtered_indices.borrow().len()
653    }
654
655    pub fn highlighted_index(&self) -> i32 {
656        self.shared.highlighted_index_value.get()
657    }
658
659    pub fn is_open(&self) -> bool {
660        self.shared.open_state.get()
661    }
662
663    pub fn items<I, S>(&self, items: I) -> &Self
664    where
665        I: IntoIterator<Item = S>,
666        S: Into<String>,
667    {
668        self.shared.close();
669        let mut slot = self.shared.items_value.borrow_mut();
670        slot.clear();
671        slot.extend(items.into_iter().map(ComboBoxItem::new));
672        drop(slot);
673        self.shared.sync_selection_from_text();
674        self.shared.rebuild_filtered_indices();
675        self.shared.popup_list.refresh_panel_layout();
676        self.shared.sync_option_visuals();
677        self.shared.sync_semantic_label();
678        self
679    }
680
681    pub fn text(&self, value: impl Into<String>) -> &Self {
682        self.shared.set_text(value.into(), false);
683        self
684    }
685
686    pub fn placeholder(&self, value: impl Into<String>) -> &Self {
687        self.shared.editor.placeholder(value);
688        self.shared.sync_semantic_label();
689        self
690    }
691
692    pub fn allow_custom(&self, flag: bool) -> &Self {
693        self.shared.allow_custom_value.set(flag);
694        self.shared.sync_selection_from_text();
695        self
696    }
697
698    pub fn auto_complete(&self, flag: bool) -> &Self {
699        self.shared.auto_complete_value.set(flag);
700        self
701    }
702
703    pub fn filter_mode(&self, mode: ComboBoxFilterMode) -> &Self {
704        self.shared.filter_mode_value.set(mode);
705        self.shared.rebuild_filtered_indices();
706        self.shared.refresh_popup_after_filter();
707        self.shared.sync_option_visuals();
708        self
709    }
710
711    pub fn commit_mode(&self, mode: ComboBoxCommitMode) -> &Self {
712        self.shared.commit_mode_value.set(mode);
713        self
714    }
715
716    pub fn open_on_focus(&self, flag: bool) -> &Self {
717        self.shared.open_on_focus_value.set(flag);
718        self
719    }
720
721    pub fn stays_open_on_edit(&self, flag: bool) -> &Self {
722        self.shared.stays_open_on_edit_value.set(flag);
723        self
724    }
725
726    pub fn max_visible_items(&self, count: i32) -> &Self {
727        self.shared.popup_list.max_visible_items(count);
728        self
729    }
730
731    pub fn popup_width(&self, value: f32) -> &Self {
732        self.shared.popup_list.popup_width(value);
733        self
734    }
735
736    pub fn popup_panel_color(&self, color: u32) -> &Self {
737        self.shared.popup_panel_color_overridden.set(true);
738        self.shared.popup_panel_color_value.set(color);
739        self.shared.popup_list.panel_node.bg_color(color);
740        self
741    }
742
743    pub fn popup_panel_background_blur(&self, sigma: f32) -> &Self {
744        self.shared.popup_panel_background_blur_overridden.set(true);
745        if sigma < 0.0 {
746            logger::warn(
747                "Layout",
748                &format!("ComboBox.popupPanelBackgroundBlur() received {sigma}; clamping to 0.0."),
749            );
750        }
751        self.shared
752            .popup_panel_background_blur_sigma_value
753            .set(sigma.max(0.0));
754        self.shared
755            .popup_list
756            .panel_node
757            .background_blur(self.shared.popup_panel_background_blur_sigma_value.get());
758        self
759    }
760
761    pub fn sizing(&self, sizing: DropdownSizing) -> &Self {
762        self.set_sizing(Some(sizing))
763    }
764
765    pub fn clear_sizing(&self) -> &Self {
766        self.set_sizing(None)
767    }
768
769    fn set_sizing(&self, sizing: Option<DropdownSizing>) -> &Self {
770        self.shared.sizing_value.set(sizing);
771        let previous_presenter = self.shared.chevron_presenter.borrow().clone();
772        let next_presenter =
773            create_chevron_presenter(self.shared.chevron_template_value.borrow().clone(), sizing);
774        *self.shared.chevron_presenter.borrow_mut() = next_presenter.clone();
775        self.shared
776            .chevron_host
777            .remove_child(&previous_presenter.root());
778        self.shared.chevron_host.child(&next_presenter.root());
779        previous_presenter.root().dispose();
780        self.shared.popup_list.sizing(sizing);
781        self.shared.handle_theme_changed();
782        self
783    }
784
785    pub fn colors(&self, colors: DropdownColors) -> &Self {
786        self.set_colors(Some(colors))
787    }
788
789    pub fn clear_colors(&self) -> &Self {
790        self.set_colors(None)
791    }
792
793    fn set_colors(&self, colors: Option<DropdownColors>) -> &Self {
794        self.shared.colors_value.set(colors);
795        self.shared.popup_list.colors(colors);
796        self.shared.handle_theme_changed();
797        self
798    }
799
800    pub fn chevron_template(&self, template: Rc<dyn DropdownChevronTemplate>) -> &Self {
801        self.set_chevron_template(Some(template))
802    }
803
804    pub fn clear_chevron_template(&self) -> &Self {
805        self.set_chevron_template(None)
806    }
807
808    fn set_chevron_template(&self, template: Option<Rc<dyn DropdownChevronTemplate>>) -> &Self {
809        *self.shared.chevron_template_value.borrow_mut() = template.clone();
810        let previous_presenter = self.shared.chevron_presenter.borrow().clone();
811        let next_presenter = create_chevron_presenter(template, self.shared.sizing_value.get());
812        *self.shared.chevron_presenter.borrow_mut() = next_presenter.clone();
813        self.shared
814            .chevron_host
815            .remove_child(&previous_presenter.root());
816        self.shared.chevron_host.child(&next_presenter.root());
817        previous_presenter.root().dispose();
818        self.shared.handle_theme_changed();
819        self
820    }
821
822    pub fn option_row_template(&self, template: Rc<dyn DropdownOptionRowTemplate>) -> &Self {
823        self.set_option_row_template(Some(template))
824    }
825
826    pub fn clear_option_row_template(&self) -> &Self {
827        self.set_option_row_template(None)
828    }
829
830    fn set_option_row_template(
831        &self,
832        template: Option<Rc<dyn DropdownOptionRowTemplate>>,
833    ) -> &Self {
834        self.shared.close();
835        self.shared.popup_list.option_row_template(template);
836        self
837    }
838
839    pub fn select_index(&self, index: i32) -> &Self {
840        self.shared.set_selected_index(index, false);
841        self
842    }
843
844    pub fn on_changed(
845        &self,
846        callback: impl Fn(crate::controls::ComboBoxChangedEventArgs<ComboBoxItem>) + 'static,
847    ) -> &Self {
848        *self.shared.changed_callback.borrow_mut() = Some(Rc::new(callback));
849        self
850    }
851
852    pub fn on_text_changed(&self, callback: impl Fn(TextChangedEventArgs) + 'static) -> &Self {
853        *self.shared.text_changed_callback.borrow_mut() = Some(Rc::new(callback));
854        self
855    }
856
857    pub fn focus_now(&self) -> &Self {
858        if self.root.handle() != crate::node::NodeHandle::INVALID {
859            ui::request_focus(self.root.handle().raw());
860        }
861        self
862    }
863
864    pub fn enabled(&self, enabled: bool) -> &Self {
865        self.root.enabled(enabled);
866        if !enabled {
867            self.shared.pointer_pressed_state.set(false);
868            self.shared.hovered_state.set(false);
869            self.shared.close();
870        }
871        self.shared.handle_theme_changed();
872        self
873    }
874}
875
876impl HasFlexBoxRoot for ComboBox {
877    fn flex_box_root(&self) -> &FlexBox {
878        &self.root
879    }
880}
881
882impl ThemeBindable for ComboBox {
883    fn theme_binding_node(&self) -> NodeRef {
884        self.root.retained_node_ref()
885    }
886
887    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
888        let weak_root = self.root.downgrade();
889        let weak_shared = Rc::downgrade(&self.shared);
890        Box::new(move || {
891            Some(ComboBox {
892                root: weak_root.upgrade()?,
893                shared: weak_shared.upgrade()?,
894            })
895        })
896    }
897}
898
899impl Node for ComboBox {
900    fn retained_node_ref(&self) -> NodeRef {
901        self.root.retained_node_ref()
902    }
903
904    fn build_self(&self) {
905        self.root.build_self();
906    }
907
908    fn dispose(&self) {
909        self.shared.close();
910        focus_adorner::hide_owner(&self.root);
911        *self.shared.theme_guard.borrow_mut() = None;
912        *self.shared.focus_visibility_guard.borrow_mut() = None;
913        self.shared.popup_list.dispose();
914        self.root.dispose();
915    }
916}
917
918impl ComboBoxShared {
919    fn is_enabled(&self) -> bool {
920        self.root
921            .upgrade()
922            .is_some_and(|root| root.retained_node_ref().is_enabled_for_routing())
923    }
924
925    fn handle_global_key_event(&self, event_type: KeyEventType, key: &str, modifiers: u32) -> bool {
926        if !self.open_state.get() || modifiers != 0 || event_type != KeyEventType::Down {
927            return false;
928        }
929        match key {
930            "Escape" => {
931                self.close();
932                true
933            }
934            "Enter" => {
935                self.select_highlighted();
936                true
937            }
938            "Home" => {
939                self.highlight_index(0);
940                true
941            }
942            "End" => {
943                self.highlight_index(self.filtered_indices.borrow().len() as i32 - 1);
944                true
945            }
946            "ArrowDown" => {
947                self.move_highlight(1);
948                true
949            }
950            "ArrowUp" => {
951                self.move_highlight(-1);
952                true
953            }
954            _ => false,
955        }
956    }
957
958    fn toggle_from_chevron(&self) {
959        if !self.is_enabled() {
960            return;
961        }
962        self.focus_editor_from_chevron();
963        self.toggle_open();
964        self.handle_theme_changed();
965    }
966
967    fn focus_editor_from_chevron(&self) {
968        self.editor.caret_to_end();
969        self.editor.focus_now();
970        self.editor.caret_to_end();
971    }
972
973    fn toggle_open(&self) {
974        if self.open_state.get() {
975            self.close();
976        } else {
977            self.open();
978        }
979    }
980
981    fn handle_editor_key_down(&self, key: &str, modifiers: u32) -> bool {
982        if !self.is_enabled() {
983            return false;
984        }
985        if modifiers != 0 {
986            return Self::is_text_navigation_key(key, modifiers);
987        }
988        match key {
989            "ArrowDown" => {
990                if !self.open_state.get() {
991                    self.open();
992                } else {
993                    self.move_highlight(1);
994                }
995                true
996            }
997            "ArrowUp" => {
998                if !self.open_state.get() {
999                    self.open();
1000                }
1001                self.move_highlight(-1);
1002                true
1003            }
1004            "Enter" if self.open_state.get() => {
1005                self.select_highlighted();
1006                true
1007            }
1008            "Escape" if self.open_state.get() => {
1009                self.close();
1010                true
1011            }
1012            _ => Self::is_text_navigation_key(key, modifiers),
1013        }
1014    }
1015
1016    fn is_text_navigation_key(key: &str, modifiers: u32) -> bool {
1017        let non_shift_modifiers = modifiers
1018            & ((KeyModifier::Ctrl as u32) | (KeyModifier::Alt as u32) | (KeyModifier::Meta as u32));
1019        non_shift_modifiers == 0
1020            && matches!(
1021                key,
1022                "ArrowLeft"
1023                    | "ArrowRight"
1024                    | "ArrowUp"
1025                    | "ArrowDown"
1026                    | "Home"
1027                    | "End"
1028                    | "PageUp"
1029                    | "PageDown"
1030            )
1031    }
1032
1033    fn handle_editor_text_changed(&self, value: String) {
1034        if self.suppress_editor_changed.get() {
1035            return;
1036        }
1037        let mut next_value = value.clone();
1038        let mut completion_selection: Option<(u32, u32)> = None;
1039        let deleting_text = value.len() < self.text_value.borrow().len();
1040        let should_auto_complete = self.auto_complete_value.get()
1041            && !value.is_empty()
1042            && !deleting_text
1043            && value != *self.last_auto_complete_text_value.borrow();
1044        self.last_auto_complete_text_value.borrow_mut().clear();
1045        if should_auto_complete {
1046            let auto_complete_index = self.find_auto_complete_match(&value);
1047            if auto_complete_index >= 0 {
1048                let completed_value = self.items_value.borrow()[auto_complete_index as usize]
1049                    .value
1050                    .clone();
1051                if completed_value.len() > value.len() {
1052                    let selection_start = value.chars().count() as u32;
1053                    let selection_end = completed_value.chars().count() as u32;
1054                    next_value = completed_value;
1055                    completion_selection = Some((selection_start, selection_end));
1056                    *self.last_auto_complete_text_value.borrow_mut() = value.clone();
1057                }
1058            }
1059        }
1060        if let Some((selection_start, selection_end)) = completion_selection {
1061            self.suppress_editor_changed.set(true);
1062            self.editor.text(next_value.clone());
1063            self.editor.selection_range(selection_start, selection_end);
1064            self.suppress_editor_changed.set(false);
1065        }
1066        *self.text_value.borrow_mut() = next_value.clone();
1067        self.sync_selection_from_text();
1068        self.rebuild_filtered_indices();
1069        if self.filtered_indices.borrow().is_empty() {
1070            self.close();
1071        } else if self.stays_open_on_edit_value.get() {
1072            self.highlighted_index_value.set(0);
1073            if self.open_state.get() {
1074                self.refresh_open_popup();
1075            } else {
1076                self.open();
1077            }
1078        }
1079        self.refresh_popup_after_filter();
1080        self.sync_option_visuals();
1081        self.sync_semantic_label();
1082        self.emit_text_changed(next_value);
1083    }
1084
1085    fn handle_editor_focus_changed(&self, focused: bool) {
1086        self.editor_focused_state.set(focused);
1087        if focused && self.open_on_focus_value.get() {
1088            self.open();
1089        }
1090        if !focused && !self.open_state.get() {
1091            self.commit_current_text();
1092            self.pointer_pressed_state.set(false);
1093        }
1094        self.sync_focused_state();
1095    }
1096
1097    fn sync_focused_state(&self) {
1098        let next_focused = self.wrapper_focused_state.get() || self.editor_focused_state.get();
1099        if !next_focused && !self.popup_pointer_pressed_state.get() {
1100            self.schedule_deferred_blur_close();
1101        }
1102        if self.focused_state.get() == next_focused {
1103            return;
1104        }
1105        self.focused_state.set(next_focused);
1106        self.handle_theme_changed();
1107    }
1108
1109    fn schedule_deferred_blur_close(&self) {
1110        if self.deferred_blur_close_pending_state.get() {
1111            return;
1112        }
1113        self.deferred_blur_close_pending_state.set(true);
1114        let weak = self.self_weak.borrow().clone();
1115        app::after_next_commit(move || {
1116            if let Some(shared) = weak.upgrade() {
1117                shared.fire_deferred_blur_close();
1118            }
1119        });
1120        frame_scheduler::mark_needs_commit();
1121    }
1122
1123    fn fire_deferred_blur_close(&self) {
1124        self.deferred_blur_close_pending_state.set(false);
1125        let next_focused = self.wrapper_focused_state.get() || self.editor_focused_state.get();
1126        if !next_focused && !self.popup_pointer_pressed_state.get() {
1127            self.pointer_pressed_state.set(false);
1128            self.close();
1129        }
1130    }
1131
1132    fn set_text(&self, value: String, emit: bool) {
1133        if *self.text_value.borrow() == value {
1134            return;
1135        }
1136        *self.text_value.borrow_mut() = value.clone();
1137        self.suppress_editor_changed.set(true);
1138        self.editor.text(value.clone());
1139        self.suppress_editor_changed.set(false);
1140        self.sync_selection_from_text();
1141        self.rebuild_filtered_indices();
1142        self.refresh_popup_after_filter();
1143        self.sync_option_visuals();
1144        self.sync_semantic_label();
1145        if emit {
1146            self.emit_text_changed(value);
1147        }
1148    }
1149
1150    fn set_selected_index(&self, index: i32, emit: bool) {
1151        if index == -1 {
1152            self.selected_index_value.set(-1);
1153            self.committed_selected_index_value.set(-1);
1154            self.highlighted_index_value.set(-1);
1155            self.popup_list.set_highlighted_index(-1);
1156            self.sync_semantic_label();
1157            return;
1158        }
1159        let count = self.items_value.borrow().len() as i32;
1160        if count == 0 {
1161            if index != -1 {
1162                logger::warn(
1163                    "Layout",
1164                    &format!(
1165                        "ComboBox.selectIndex() received {index} before any items were assigned."
1166                    ),
1167                );
1168            }
1169            return;
1170        }
1171        let clamped_index = index.clamp(0, count - 1);
1172        if clamped_index != index {
1173            logger::warn(
1174                "Layout",
1175                &format!("ComboBox.selectIndex() received {index}; clamping to {clamped_index}."),
1176            );
1177        }
1178        let changed = self.selected_index_value.get() != clamped_index;
1179        self.selected_index_value.set(clamped_index);
1180        self.committed_selected_index_value.set(clamped_index);
1181        let item = self.items_value.borrow()[clamped_index as usize].clone();
1182        self.set_text(item.value, false);
1183        self.editor.caret_to_end();
1184        self.rebuild_filtered_indices();
1185        let visible_index = self.find_visible_index_for_source_index(clamped_index);
1186        self.highlighted_index_value.set(visible_index);
1187        self.popup_list.set_highlighted_index(visible_index);
1188        self.sync_semantic_label();
1189        if emit && changed {
1190            if let Some(root) = self.root.upgrade() {
1191                root.request_semantic_announcement();
1192            }
1193            self.emit_selection_changed();
1194        }
1195    }
1196
1197    fn sync_selection_from_text(&self) {
1198        let exact_index = self.find_exact_text_match(&self.text_value.borrow());
1199        if exact_index >= 0 {
1200            self.selected_index_value.set(exact_index);
1201            return;
1202        }
1203        if self.allow_custom_value.get() {
1204            self.selected_index_value.set(-1);
1205        }
1206    }
1207
1208    fn commit_current_text(&self) {
1209        match self.commit_mode_value.get() {
1210            ComboBoxCommitMode::KeepText => {}
1211            ComboBoxCommitMode::SelectExactMatch => {
1212                let exact_index = self.find_exact_text_match(&self.text_value.borrow());
1213                if exact_index >= 0 {
1214                    self.set_selected_index(exact_index, true);
1215                }
1216            }
1217            ComboBoxCommitMode::RevertToSelection => {
1218                let committed = self.committed_selected_index_value.get();
1219                if committed >= 0 && committed < self.items_value.borrow().len() as i32 {
1220                    let value = self.items_value.borrow()[committed as usize].value.clone();
1221                    self.set_text(value, true);
1222                    self.editor.caret_to_end();
1223                    self.selected_index_value.set(committed);
1224                }
1225            }
1226        }
1227    }
1228
1229    fn emit_selection_changed(&self) {
1230        let selected_index = self.selected_index_value.get();
1231        if selected_index < 0 {
1232            return;
1233        }
1234        let Some(item) = self
1235            .items_value
1236            .borrow()
1237            .get(selected_index as usize)
1238            .cloned()
1239        else {
1240            return;
1241        };
1242        if let Some(callback) = self.changed_callback.borrow().clone() {
1243            callback(crate::controls::ComboBoxChangedEventArgs {
1244                item,
1245                selected_index,
1246            });
1247        }
1248    }
1249
1250    fn emit_text_changed(&self, value: String) {
1251        if let Some(callback) = self.text_changed_callback.borrow().clone() {
1252            callback(TextChangedEventArgs { text: value });
1253        }
1254    }
1255
1256    fn open(&self) {
1257        let Some(root) = self.root.upgrade() else {
1258            return;
1259        };
1260        if self.open_state.get()
1261            || self.filtered_indices.borrow().is_empty()
1262            || root.handle() == crate::node::NodeHandle::INVALID
1263        {
1264            return;
1265        }
1266        let initial_highlight = if self.selected_index_value.get() >= 0 {
1267            self.find_visible_index_for_source_index(self.selected_index_value.get())
1268        } else {
1269            0
1270        };
1271        self.popup_list.set_highlighted_index(initial_highlight);
1272        self.highlighted_index_value
1273            .set(self.popup_list.highlighted_index());
1274        let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
1275            return;
1276        };
1277        if !self.popup_list.open(
1278            bounds[0],
1279            bounds[1],
1280            bounds[2],
1281            bounds[3],
1282            initial_highlight,
1283        ) {
1284            return;
1285        }
1286        self.highlighted_index_value
1287            .set(self.popup_list.highlighted_index());
1288        self.open_state.set(true);
1289        ACTIVE_COMBOBOX.with(|slot| {
1290            *slot.borrow_mut() = Some(self.self_weak.borrow().clone());
1291        });
1292        ui::set_semantic_expanded(root.handle().raw(), true, true);
1293        root.request_semantic_announcement();
1294        if self.key_filter_token.get() == 0 {
1295            let weak_self = self.self_weak.borrow().clone();
1296            let token = event::push_key_filter(move |event_type, key, modifiers| {
1297                weak_self.upgrade().is_some_and(|shared| {
1298                    shared.handle_global_key_event(event_type, key, modifiers)
1299                })
1300            });
1301            self.key_filter_token.set(token);
1302        }
1303        self.handle_theme_changed();
1304    }
1305
1306    fn close(&self) {
1307        if !self.open_state.get() && !self.popup_list.is_open() {
1308            return;
1309        }
1310        self.deferred_blur_close_pending_state.set(false);
1311        self.popup_pointer_pressed_state.set(false);
1312        self.popup_list.close();
1313        self.open_state.set(false);
1314        ACTIVE_COMBOBOX.with(|slot| {
1315            let should_clear = slot
1316                .borrow()
1317                .as_ref()
1318                .and_then(Weak::upgrade)
1319                .and_then(|active| active.root.upgrade())
1320                .and_then(|active_root| {
1321                    self.root
1322                        .upgrade()
1323                        .map(|root| active_root.handle() == root.handle())
1324                })
1325                .unwrap_or(false);
1326            if should_clear {
1327                slot.borrow_mut().take();
1328            }
1329        });
1330        if let Some(root) = self.root.upgrade() {
1331            ui::set_semantic_expanded(root.handle().raw(), true, false);
1332            root.request_semantic_announcement();
1333        }
1334        let token = self.key_filter_token.replace(0);
1335        if token != 0 {
1336            event::remove_key_filter(token);
1337        }
1338        self.commit_current_text();
1339        self.handle_theme_changed();
1340    }
1341
1342    fn is_trigger_visible_in_viewport(&self) -> bool {
1343        let Some(root) = self.root.upgrade() else {
1344            return true;
1345        };
1346        let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
1347            return true;
1348        };
1349        let x = bounds[0];
1350        let y = bounds[1];
1351        let width = bounds[2];
1352        let height = bounds[3];
1353        if width <= 0.0 || height <= 0.0 {
1354            return true;
1355        }
1356        let right = x + width;
1357        let bottom = y + height;
1358        let mut current = root.retained_node_ref().parent();
1359        while let Some(node) = current {
1360            if node.node_type() == NodeType::ScrollView {
1361                if let Some(scroll_bounds) = ui::get_bounds(node.handle().raw()) {
1362                    let sv_x = scroll_bounds[0];
1363                    let sv_y = scroll_bounds[1];
1364                    let sv_right = sv_x + scroll_bounds[2];
1365                    let sv_bottom = sv_y + scroll_bounds[3];
1366                    return right > sv_x && bottom > sv_y && x < sv_right && y < sv_bottom;
1367                }
1368                break;
1369            }
1370            current = node.parent();
1371        }
1372        let viewport_width = ui::get_viewport_width();
1373        let viewport_height = ui::get_viewport_height();
1374        right > 0.0 && bottom > 0.0 && x < viewport_width && y < viewport_height
1375    }
1376
1377    fn highlight_index(&self, index: i32) {
1378        self.popup_list.highlight_index(index);
1379        self.highlighted_index_value
1380            .set(self.popup_list.highlighted_index());
1381    }
1382
1383    fn move_highlight(&self, delta: i32) {
1384        self.popup_list.move_highlight(delta);
1385        self.highlighted_index_value
1386            .set(self.popup_list.highlighted_index());
1387    }
1388
1389    fn refresh_popup_after_filter(&self) {
1390        if self.open_state.get() {
1391            self.refresh_open_popup();
1392        } else {
1393            self.popup_list.refresh_panel_layout();
1394        }
1395    }
1396
1397    fn refresh_open_popup(&self) {
1398        let Some(root) = self.root.upgrade() else {
1399            self.popup_list.refresh_panel_layout();
1400            return;
1401        };
1402        let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
1403            self.popup_list.refresh_panel_layout();
1404            return;
1405        };
1406        self.popup_list.refresh_open(
1407            bounds[0],
1408            bounds[1],
1409            bounds[2],
1410            bounds[3],
1411            self.highlighted_index_value.get(),
1412        );
1413        self.highlighted_index_value
1414            .set(self.popup_list.highlighted_index());
1415    }
1416
1417    fn select_highlighted(&self) {
1418        let highlighted = self.highlighted_index_value.get();
1419        let source_index = {
1420            let filtered = self.filtered_indices.borrow();
1421            if highlighted < 0 || highlighted >= filtered.len() as i32 {
1422                return;
1423            }
1424            filtered[highlighted as usize]
1425        };
1426        if highlighted < 0 {
1427            return;
1428        }
1429        self.set_selected_index(source_index, true);
1430        self.close();
1431    }
1432
1433    fn rebuild_filtered_indices(&self) {
1434        let mut filtered = self.filtered_indices.borrow_mut();
1435        filtered.clear();
1436        for (index, item) in self.items_value.borrow().iter().enumerate() {
1437            if self.should_include_item(item) {
1438                filtered.push(index as i32);
1439            }
1440        }
1441        if self.highlighted_index_value.get() >= filtered.len() as i32 {
1442            self.highlighted_index_value.set(if filtered.is_empty() {
1443                -1
1444            } else {
1445                filtered.len() as i32 - 1
1446            });
1447        }
1448    }
1449
1450    fn should_include_item(&self, item: &ComboBoxItem) -> bool {
1451        let text = self.text_value.borrow();
1452        if self.filter_mode_value.get() == ComboBoxFilterMode::None || text.is_empty() {
1453            return true;
1454        }
1455        if self.filter_mode_value.get() == ComboBoxFilterMode::StartsWith {
1456            return string_starts_with_ignore_case(&item.value, &text);
1457        }
1458        string_contains_ignore_case(&item.value, &text)
1459    }
1460
1461    fn find_auto_complete_match(&self, text: &str) -> i32 {
1462        for (index, item) in self.items_value.borrow().iter().enumerate() {
1463            if string_starts_with_ignore_case(&item.value, text) {
1464                return index as i32;
1465            }
1466        }
1467        -1
1468    }
1469
1470    fn find_exact_text_match(&self, text: &str) -> i32 {
1471        for (index, item) in self.items_value.borrow().iter().enumerate() {
1472            if strings_equal_ignore_case(&item.value, text) {
1473                return index as i32;
1474            }
1475        }
1476        -1
1477    }
1478
1479    fn find_visible_index_for_source_index(&self, source_index: i32) -> i32 {
1480        for (index, visible) in self.filtered_indices.borrow().iter().enumerate() {
1481            if *visible == source_index {
1482                return index as i32;
1483            }
1484        }
1485        if self.filtered_indices.borrow().is_empty() {
1486            -1
1487        } else {
1488            0
1489        }
1490    }
1491
1492    fn handle_theme_changed(&self) {
1493        let Some(root) = self.root.upgrade() else {
1494            return;
1495        };
1496        let theme = current_theme();
1497        if !self.popup_panel_color_overridden.get() {
1498            self.popup_panel_color_value
1499                .set(theme.context_menu.panel_background);
1500        }
1501        if !self.popup_panel_background_blur_overridden.get() {
1502            self.popup_panel_background_blur_sigma_value
1503                .set(DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA);
1504        }
1505        let sizing = self.sizing_value.get();
1506        let field_height = sizing
1507            .filter(|value| value.has_field_height())
1508            .map(|value| value.field_height_px())
1509            .unwrap_or(32.0);
1510        let chevron_box_size = sizing
1511            .filter(|value| value.has_chevron_box_size())
1512            .map(|value| value.chevron_box_size_px())
1513            .unwrap_or(32.0);
1514        let field_font_size = sizing
1515            .filter(|value| value.has_field_font_size())
1516            .map(|value| value.field_font_size_px())
1517            .unwrap_or(theme.fonts.size_body);
1518        let field_border_width = 2.0;
1519        let field_content_height = (field_height - (field_border_width * 2.0)).max(0.0);
1520        root.cursor(if self.is_enabled() {
1521            CursorStyle::Text
1522        } else {
1523            CursorStyle::Default
1524        })
1525        .corner_radius(0.0)
1526        .border(0.0, 0x00000000)
1527        .padding(0.0, 0.0, 0.0, 0.0)
1528        .bg_color(0x00000000)
1529        .opacity(if self.is_enabled() { 1.0 } else { 0.6 });
1530        let colors = self.colors_value.get();
1531        let field_background = colors
1532            .filter(|value| value.has_background())
1533            .map(|value| value.background_color())
1534            .unwrap_or(theme.colors.surface);
1535        let field_border_color = colors
1536            .filter(|value| value.has_border())
1537            .map(|value| value.border_color())
1538            .unwrap_or(theme.colors.border);
1539        root.height(field_height, Unit::Pixel)
1540            .corner_radius(theme.spacing.sm)
1541            .border(field_border_width, field_border_color)
1542            .padding(16.0, 0.0, 8.0, 0.0)
1543            .bg_color(field_background);
1544        self.editor
1545            .height(field_content_height, Unit::Pixel)
1546            .font_size(field_font_size)
1547            .line_height(field_content_height);
1548        if let Some(colors) = resolve_text_input_colors(colors, &theme) {
1549            self.editor.colors(colors);
1550        } else {
1551            self.editor.clear_colors();
1552        }
1553        self.chevron_host
1554            .width(chevron_box_size, Unit::Pixel)
1555            .height(field_content_height, Unit::Pixel)
1556            .align_items(AlignItems::Center)
1557            .justify_content(crate::ffi::JustifyContent::Center)
1558            .cursor(if self.is_enabled() {
1559                CursorStyle::Pointer
1560            } else {
1561                CursorStyle::Default
1562            });
1563        self.chevron_presenter.borrow().apply(
1564            theme.clone(),
1565            DropdownChevronVisualState::new(
1566                self.open_state.get(),
1567                self.hovered_state.get(),
1568                self.is_enabled(),
1569            ),
1570        );
1571        self.popup_list
1572            .panel_node
1573            .padding(
1574                SELECTABLE_POPUP_LIST_PANEL_PADDING,
1575                SELECTABLE_POPUP_LIST_PANEL_PADDING,
1576                SELECTABLE_POPUP_LIST_PANEL_PADDING,
1577                SELECTABLE_POPUP_LIST_PANEL_PADDING,
1578            )
1579            .corner_radius(theme.spacing.sm)
1580            .bg_color(self.popup_panel_color_value.get())
1581            .border(1.0, theme.context_menu.panel_border_color)
1582            .background_blur(self.popup_panel_background_blur_sigma_value.get())
1583            .drop_shadow(
1584                theme.context_menu.panel_shadow_color,
1585                0.0,
1586                theme.context_menu.shadow_offset_y,
1587                theme.context_menu.shadow_blur,
1588                theme.context_menu.shadow_spread,
1589            );
1590        self.popup_list.popup_scroll_box.bg_color(0x00000000);
1591        self.sync_option_visuals();
1592        self.sync_focus_chrome();
1593    }
1594
1595    fn sync_option_visuals(&self) {
1596        self.popup_list.sync_option_visuals();
1597    }
1598
1599    fn sync_semantic_label(&self) {
1600        if let Some(root) = self.root.upgrade() {
1601            if !self.text_value.borrow().is_empty() {
1602                root.default_semantic_label(self.text_value.borrow().clone());
1603            } else {
1604                root.default_semantic_label("Combo box");
1605            }
1606        }
1607    }
1608
1609    fn sync_focus_chrome(&self) {
1610        let Some(root) = self.root.upgrade() else {
1611            return;
1612        };
1613        if self.focused_state.get()
1614            && self.is_enabled()
1615            && focus_visibility::keyboard_focus_visible()
1616        {
1617            focus_adorner::show_standard(&root, current_theme().spacing.sm);
1618        } else {
1619            focus_adorner::hide_owner(&root);
1620        }
1621    }
1622
1623    fn popup_list_activate_index(&self, index: i32) {
1624        self.highlight_index(index);
1625        self.select_highlighted();
1626    }
1627
1628    fn popup_list_pointer_up(&self) {
1629        self.popup_pointer_pressed_state.set(false);
1630        self.sync_focused_state();
1631    }
1632}