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}