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