Skip to main content

fui/controls/
combobox.rs

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