1use std::{
32 cell::{Cell, RefCell},
33 collections::HashMap,
34 rc::{Rc, Weak},
35 sync::Arc,
36};
37
38use gpui::{
39 div, prelude::*, px, App, Entity, InteractiveElement, IntoElement, Pixels, RenderOnce,
40 SharedString, Styled, Window,
41};
42use herogpui_core::{element_id, FieldVariant, Placement, SelectionMode};
43use herogpui_theme::ActiveTheme;
44
45use crate::{
46 a11y::{self, A11y as _},
47 icons,
48 input::{Input, InputState},
49 matches::{empty_matches, MatchesCache},
50 picker_item::PickerItem,
51 selection::{normalize_selection, toggle_key},
52 util,
53};
54
55const TRIGGER_MIN_WIDTH: Pixels = px(180.);
59
60#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
65pub enum MenuTrigger {
66 #[default]
68 Focus,
69 Input,
71 Manual,
73}
74
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
81pub enum ComboBoxFormValue {
82 #[default]
84 Key,
85 Text,
87}
88
89type OnSelectionChange = Arc<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>;
90type OnOpenChange = Arc<dyn Fn(&bool, &mut Window, &mut App) + 'static>;
91type ComboBoxFormState = Rc<RefCell<crate::form::LiveFormFieldState>>;
92
93thread_local! {
94 static COMBO_BOX_FORM_STATES: RefCell<HashMap<u64, Weak<RefCell<crate::form::LiveFormFieldState>>>> =
95 RefCell::new(HashMap::new());
96}
97
98fn combo_box_form_state(entity_id: u64) -> ComboBoxFormState {
99 COMBO_BOX_FORM_STATES.with(|states| {
100 let mut states = states.borrow_mut();
101 if let Some(state) = states.get(&entity_id).and_then(|state| state.upgrade()) {
102 return state;
103 }
104 let state = Rc::new(RefCell::new(crate::form::LiveFormFieldState {
105 value: crate::form::FormValue::Keys(Vec::new()),
106 is_invalid: false,
107 is_successful: true,
108 focus: None,
109 restore: None,
110 }));
111 states.insert(entity_id, Rc::downgrade(&state));
112 state
113 })
114}
115
116fn form_submits_text(form_value: Option<ComboBoxFormValue>, allows_custom_value: bool) -> bool {
120 allows_custom_value || form_value == Some(ComboBoxFormValue::Text)
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131struct FocusOpen {
132 can_open: bool,
133 was_open: bool,
134}
135
136#[derive(Clone, Debug, PartialEq, Eq)]
144struct ComboCursor {
145 key: SharedString,
146 hidden_query: Option<String>,
147}
148
149fn cursor_for(rows: &[PickerItem], index: usize, hidden_query: Option<String>) -> ComboCursor {
150 ComboCursor {
151 key: rows[index].key().clone(),
152 hidden_query,
153 }
154}
155
156fn cursor_position(rows: &[PickerItem], cursor: &ComboCursor) -> Option<usize> {
157 rows.iter().position(|item| item.key() == &cursor.key)
158}
159
160fn label_of_key<'a>(items: &'a [PickerItem], key: &SharedString) -> Option<&'a SharedString> {
163 items
164 .iter()
165 .find(|item| item.key() == key)
166 .map(|item| item.label())
167}
168
169fn compute_matches(
175 items: &[PickerItem],
176 query: &str,
177 max_items: usize,
178 full: bool,
179 filter: Option<&Arc<dyn Fn(&str, &str) -> bool + 'static>>,
180) -> Vec<PickerItem> {
181 if full {
182 return items.iter().take(max_items).cloned().collect();
183 }
184 if let Some(filter) = filter {
185 return items
186 .iter()
187 .filter(|item| filter(item.label(), query))
188 .take(max_items)
189 .cloned()
190 .collect();
191 }
192 if query.is_empty() {
193 return items.iter().take(max_items).cloned().collect();
194 }
195 let lowered = query.to_lowercase();
196 items
197 .iter()
198 .filter(|item| item.label().to_lowercase().contains(&lowered))
199 .take(max_items)
200 .cloned()
201 .collect()
202}
203
204#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
206#[derive(IntoElement)]
207pub struct ComboBox {
208 state: Entity<InputState>,
209 items: Vec<PickerItem>,
210 is_open: Option<bool>,
213 default_open: bool,
214 placement: Placement,
215 label: Option<SharedString>,
216 placeholder: Option<SharedString>,
217 description: Option<SharedString>,
218 error_message: Option<SharedString>,
219 variant: FieldVariant,
220 menu_trigger: MenuTrigger,
221 filter: Option<Arc<dyn Fn(&str, &str) -> bool + 'static>>,
224 allows_custom_value: bool,
225 max_items: usize,
226 full_width: bool,
227 min_width: Option<Pixels>,
229 field: util::FieldBox,
231 is_disabled: bool,
232 is_invalid: bool,
233 is_required: bool,
234 is_read_only: bool,
235 auto_focus: bool,
237 row_height: Option<Pixels>,
239 row_padding_x: Option<Pixels>,
241 row_padding_y: Option<Pixels>,
243 row_hover_bg: Option<gpui::Hsla>,
245 row_font_family: Option<SharedString>,
249 font_family: Option<SharedString>,
251 radius: Option<Pixels>,
254 validate: Option<crate::validation::Validator<str>>,
256 validation_behavior: Option<crate::form::ValidationBehavior>,
258 allows_empty_collection: bool,
260 name: Option<SharedString>,
262 form_value: Option<ComboBoxFormValue>,
265 should_focus_wrap: bool,
267 sections: Vec<(SharedString, SharedString)>,
269 indicator: Option<Box<dyn Fn(bool) -> gpui::AnyElement + 'static>>,
272 disabled_keys: std::collections::HashSet<SharedString>,
273 selection_mode: SelectionMode,
274 selected_keys: Vec<SharedString>,
279 is_controlled: bool,
284 selected_key_sync: bool,
288 value_content: Option<Box<dyn Fn(util::SelectionValue<'_>) -> gpui::AnyElement + 'static>>,
290 default_value: Option<Vec<SharedString>>,
292 default_input_value: Option<SharedString>,
294 value: Option<SharedString>,
297 on_selection_change_all: Option<Arc<dyn Fn(&[SharedString], &mut Window, &mut App) + 'static>>,
298 on_input_change: Option<Arc<dyn Fn(&str, &mut Window, &mut App) + 'static>>,
299 on_selection_change: Option<OnSelectionChange>,
300 on_open_change: Option<OnOpenChange>,
301 form_state: ComboBoxFormState,
302 sx: Option<Box<gpui::StyleRefinement>>,
304}
305
306impl ComboBox {
307 pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
309 self.selection_mode = mode;
310 self
311 }
312
313 pub fn default_input_value(mut self, text: impl Into<SharedString>) -> Self {
323 self.default_input_value = Some(text.into());
324 self
325 }
326
327 pub fn default_value(
329 mut self,
330 keys: impl IntoIterator<Item = impl Into<SharedString>>,
331 ) -> Self {
332 self.default_value = Some(keys.into_iter().map(Into::into).collect());
333 self
334 }
335
336 pub fn selected_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
340 self.selected_keys = keys.into_iter().collect();
341 self.is_controlled = true;
342 self
343 }
344
345 pub fn on_selection_change_all(
351 mut self,
352 handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
353 ) -> Self {
354 self.on_selection_change_all = Some(Arc::new(handler));
355 self
356 }
357
358 pub fn selected_key(mut self, key: impl Into<String>) -> Self {
365 let key = SharedString::from(key.into());
366 self.selected_keys = if key.is_empty() {
367 Vec::new()
368 } else {
369 vec![key]
370 };
371 self.is_controlled = true;
372 self.selected_key_sync = true;
373 self
374 }
375
376 pub fn value(self, value: impl Into<String>) -> Self {
379 self.input_value(value)
380 }
381
382 pub fn disabled_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
386 self.disabled_keys = keys.into_iter().collect();
387 self
388 }
389
390 pub fn row_height(mut self, h: impl Into<Pixels>) -> Self {
397 self.row_height = Some(h.into());
398 self
399 }
400
401 pub fn auto_focus(mut self, v: bool) -> Self {
403 self.auto_focus = v;
404 self
405 }
406
407 pub fn validate(mut self, f: impl Fn(&str) -> Option<SharedString> + 'static) -> Self {
410 self.validate = Some(Arc::new(f));
411 self
412 }
413
414 pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
417 self.validation_behavior = Some(behavior);
418 self
419 }
420
421 pub fn allows_empty_collection(mut self, v: bool) -> Self {
423 self.allows_empty_collection = v;
424 self
425 }
426
427 pub fn should_focus_wrap(mut self, v: bool) -> Self {
429 self.should_focus_wrap = v;
430 self
431 }
432
433 pub fn section_before(
435 mut self,
436 item: impl Into<SharedString>,
437 label: impl Into<SharedString>,
438 ) -> Self {
439 self.sections.push((item.into(), label.into()));
440 self
441 }
442
443 pub fn indicator(mut self, render: impl Fn(bool) -> gpui::AnyElement + 'static) -> Self {
445 self.indicator = Some(Box::new(render));
446 self
447 }
448
449 pub fn value_content(
456 mut self,
457 render: impl Fn(util::SelectionValue<'_>) -> gpui::AnyElement + 'static,
458 ) -> Self {
459 self.value_content = Some(Box::new(render));
460 self
461 }
462
463 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
465 self.name = Some(name.into());
466 self
467 }
468
469 pub fn form_value(mut self, form_value: ComboBoxFormValue) -> Self {
473 self.form_value = Some(form_value);
474 self
475 }
476
477 pub fn form_field(&self) -> Option<crate::form::FormField> {
486 let name = self.name.clone()?;
487 Some(
488 crate::form::FormField::live_text(name, self.form_state.clone(), self.state.clone())
489 .is_required(self.is_required),
490 )
491 }
492
493 pub fn is_read_only(mut self, v: bool) -> Self {
495 self.is_read_only = v;
496 self
497 }
498
499 pub fn input_value(mut self, value: impl Into<String>) -> Self {
509 self.value = Some(SharedString::from(value.into()));
510 self
511 }
512
513 pub fn on_input_change(
516 mut self,
517 handler: impl Fn(&str, &mut Window, &mut App) + 'static,
518 ) -> Self {
519 self.on_input_change = Some(Arc::new(handler));
520 self
521 }
522
523 pub fn on_change(
527 self,
528 handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
529 ) -> Self {
530 self.on_selection_change(handler)
531 }
532
533 pub fn new(state: Entity<InputState>, items: Vec<PickerItem>) -> Self {
535 let form_state = combo_box_form_state(state.entity_id().as_u64());
536 Self {
537 state,
538 items,
539 is_open: None,
540 default_open: false,
541 placement: Placement::BottomStart,
542 label: None,
543 placeholder: None,
544 description: None,
545 error_message: None,
546 variant: FieldVariant::Primary,
547 menu_trigger: MenuTrigger::Focus,
548 filter: None,
549 allows_custom_value: false,
550 max_items: 8,
551 full_width: false,
552 min_width: None,
553 is_disabled: false,
554 is_invalid: false,
555 is_required: false,
556 is_read_only: false,
557 auto_focus: false,
558 row_height: None,
559 row_padding_x: None,
560 row_padding_y: None,
561 row_hover_bg: None,
562 row_font_family: None,
563 font_family: None,
564 radius: None,
565 field: util::FieldBox::default(),
566 validate: None,
567 validation_behavior: None,
568 allows_empty_collection: false,
569 name: None,
570 form_value: None,
571 should_focus_wrap: false,
572 sections: Vec::new(),
573 indicator: None,
574 disabled_keys: std::collections::HashSet::new(),
575 selection_mode: SelectionMode::Single,
576 selected_keys: Vec::new(),
577 is_controlled: false,
578 selected_key_sync: false,
579 value_content: None,
580 default_value: None,
581 default_input_value: None,
582 value: None,
583 on_selection_change_all: None,
584 on_input_change: None,
585 on_selection_change: None,
586 on_open_change: None,
587 form_state,
588 sx: None,
589 }
590 }
591
592 pub fn placement(mut self, placement: Placement) -> Self {
594 self.placement = placement;
595 self
596 }
597
598 pub fn is_open(mut self, v: bool) -> Self {
600 self.is_open = Some(v);
601 self
602 }
603 pub fn default_open(mut self, v: bool) -> Self {
608 self.default_open = v;
609 self
610 }
611
612 pub fn label(mut self, text: impl Into<SharedString>) -> Self {
614 self.label = Some(text.into());
615 self
616 }
617
618 pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
620 self.placeholder = Some(text.into());
621 self
622 }
623
624 pub fn description(mut self, text: impl Into<SharedString>) -> Self {
626 self.description = Some(text.into());
627 self
628 }
629
630 pub fn error_message(mut self, text: impl Into<SharedString>) -> Self {
632 self.error_message = Some(text.into());
633 self
634 }
635
636 pub fn variant(mut self, variant: FieldVariant) -> Self {
638 self.variant = variant;
639 self
640 }
641
642 pub fn menu_trigger(mut self, trigger: MenuTrigger) -> Self {
644 self.menu_trigger = trigger;
645 self
646 }
647
648 pub fn filter(mut self, f: impl Fn(&str, &str) -> bool + 'static) -> Self {
655 self.filter = Some(Arc::new(f));
656 self
657 }
658
659 pub fn allows_custom_value(mut self, v: bool) -> Self {
662 self.allows_custom_value = v;
663 self
664 }
665
666 pub fn max_items(mut self, n: usize) -> Self {
668 self.max_items = n.max(1);
669 self
670 }
671
672 pub fn full_width(mut self, v: bool) -> Self {
674 self.full_width = v;
675 self
676 }
677
678 pub fn min_width(mut self, width: impl Into<Pixels>) -> Self {
683 self.min_width = Some(width.into());
684 self
685 }
686
687 pub fn height(mut self, h: impl Into<Pixels>) -> Self {
689 self.field.height = Some(h.into());
690 self
691 }
692
693 pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
696 self.field.padding_x = Some(p.into());
697 self
698 }
699
700 pub fn is_bare(mut self, v: bool) -> Self {
703 self.field.is_bare = v;
704 self.field.is_bare_is_set = true;
705 self
706 }
707
708 pub fn focus_ring(mut self, v: bool) -> Self {
711 self.field.focus_ring = Some(v);
712 self
713 }
714
715 pub fn row_padding_x(mut self, p: impl Into<Pixels>) -> Self {
717 self.row_padding_x = Some(p.into());
718 self
719 }
720
721 pub fn row_padding_y(mut self, p: impl Into<Pixels>) -> Self {
723 self.row_padding_y = Some(p.into());
724 self
725 }
726
727 pub fn row_hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
729 self.row_hover_bg = Some(color.into());
730 self
731 }
732
733 pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
740 self.font_family = Some(family.into());
741 self
742 }
743
744 pub fn row_font_family(mut self, family: impl Into<SharedString>) -> Self {
747 self.row_font_family = Some(family.into());
748 self
749 }
750
751 pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
760 self.radius = Some(radius.into());
761 self
762 }
763
764 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
770 util::refine_sx(&mut self.sx, style);
771 self
772 }
773
774 pub fn is_disabled(mut self, v: bool) -> Self {
776 self.is_disabled = v;
777 self
778 }
779
780 pub fn is_invalid(mut self, v: bool) -> Self {
782 self.is_invalid = v;
783 self
784 }
785
786 pub fn is_required(mut self, v: bool) -> Self {
788 self.is_required = v;
789 self
790 }
791
792 pub fn on_selection_change(
798 mut self,
799 handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
800 ) -> Self {
801 self.on_selection_change = Some(Arc::new(handler));
802 self
803 }
804
805 pub fn on_open_change(
807 mut self,
808 handler: impl Fn(&bool, &mut Window, &mut App) + 'static,
809 ) -> Self {
810 self.on_open_change = Some(Arc::new(handler));
811 self
812 }
813}
814
815type ComboAction = Arc<dyn Fn(&mut Window, &mut App)>;
816type OnSelectionChangeAll = Arc<dyn Fn(&[SharedString], &mut Window, &mut App) + 'static>;
817type OnInputChange = Arc<dyn Fn(&str, &mut Window, &mut App) + 'static>;
818type ComboFilter = Arc<dyn Fn(&str, &str) -> bool + 'static>;
819
820struct ComboControlled {
824 entity_id: u64,
825 base_id: gpui::ElementId,
826 items: Rc<[PickerItem]>,
827 multiple: bool,
828 default_selection: Vec<SharedString>,
829 selection_own: Option<Entity<Vec<SharedString>>>,
830 open_state: bool,
831 open_own: Option<Entity<bool>>,
832 overlay_phase: util::OverlayPhase,
833 dismissal_token: util::OverlayToken,
834}
835
836struct ComboFrame {
842 entity_id: u64,
843 base_id: gpui::ElementId,
844 items: Rc<[PickerItem]>,
845 multiple: bool,
846 selection_own: Option<Entity<Vec<SharedString>>>,
847 open_state: bool,
848 open_own: Option<Entity<bool>>,
849 overlay_phase: util::OverlayPhase,
850 dismissal_token: util::OverlayToken,
851 resolved_placement: Rc<Cell<Option<Placement>>>,
852 entry_placement: Placement,
853 anchor_bounds: Rc<Cell<Option<gpui::Bounds<Pixels>>>>,
854 colors: herogpui_theme::ThemeColors,
855 layout: herogpui_theme::LayoutTheme,
856 container_radius: Pixels,
857 close_open: ComboAction,
858 raw_query: String,
859 is_invalid: bool,
860 focus_handle: gpui::FocusHandle,
861 show_all_items: Entity<bool>,
862 display_full_collection: bool,
863 cursor: Entity<Option<ComboCursor>>,
864 matches: Rc<[PickerItem]>,
865 focus_open: Option<Entity<FocusOpen>>,
866 inside_pressed: Rc<Cell<bool>>,
867 cursor_at: Option<usize>,
868 list_scroll_now: crate::VirtualListHandle,
869 panel_scroll_now: gpui::ScrollHandle,
870 commit_value: ComboAction,
871 blur_scope: util::FocusLeave,
872 blur_focus: gpui::FocusHandle,
873}
874
875impl RenderOnce for ComboBox {
876 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
877 let entity_id = self.state.entity_id().as_u64();
882 let base_id = gpui::ElementId::named_usize("combobox", entity_id as usize);
883 let items: Rc<[PickerItem]> = std::mem::take(&mut self.items).into();
886
887 let seed = self.value.clone().or(self.default_input_value.clone());
892 if let Some(text) = seed {
893 let state = self.state.clone();
894 util::seed_once(
895 window,
896 cx,
897 element_id::scoped(&base_id, "default-text"),
898 move |cx| {
899 state.update(cx, |s, cx| {
900 s.set_value(text.to_string());
901 cx.notify();
902 });
903 },
904 );
905 }
906
907 let multiple = self.selection_mode == SelectionMode::Multiple;
913 self.selected_keys = normalize_selection(self.selected_keys.clone(), multiple);
914 let default_selection =
915 normalize_selection(self.default_value.clone().unwrap_or_default(), multiple);
916 let (selection, selection_own) = util::controlled(
917 window,
918 cx,
919 element_id::scoped(&base_id, "selection"),
920 self.is_controlled.then(|| self.selected_keys.clone()),
921 default_selection.clone(),
922 );
923 self.selected_keys = selection;
924
925 let (open_state, open_own) = util::controlled(
927 window,
928 cx,
929 element_id::scoped(&base_id, "open"),
930 self.is_open,
931 self.default_open,
932 );
933 let (overlay_phase, dismissal_token) = util::overlay_scope(
934 window,
935 cx,
936 element_id::scoped(&base_id, "overlay"),
937 open_state,
938 true,
939 );
940 let overlay_active = overlay_phase != util::OverlayPhase::Closed;
941 let frame = self.frame(
942 ComboControlled {
943 entity_id,
944 base_id,
945 items,
946 multiple,
947 default_selection,
948 selection_own,
949 open_state,
950 open_own,
951 overlay_phase,
952 dismissal_token,
953 },
954 window,
955 cx,
956 );
957 let trigger = self.trigger(&frame, window, cx);
958 let mut input = self.input(&frame, trigger);
959 let ComboFrame {
960 entity_id,
961 ref base_id,
962 ref anchor_bounds,
963 ref inside_pressed,
964 ..
965 } = frame;
966 let inside_pressed_for_group = inside_pressed.clone();
976 let field_selector = format!("combobox-field-{entity_id}");
977 input = input.field_anchor(anchor_bounds.clone(), field_selector);
978 let input_group = div()
979 .id(element_id::scoped(base_id, "field"))
980 .a11y(a11y::Role::Group)
990 .relative()
991 .capture_any_mouse_down(move |_, _, cx| {
992 inside_pressed_for_group.set(true);
993 let pressed = inside_pressed_for_group.clone();
994 cx.defer(move |_| pressed.set(false));
995 })
996 .child(input.render(window, cx));
997 let mut root = div()
998 .when(!self.full_width, |e| {
999 e.min_w(self.min_width.unwrap_or(TRIGGER_MIN_WIDTH))
1000 })
1001 .relative()
1002 .flex()
1003 .flex_col()
1004 .gap(px(4.))
1005 .when_some(self.font_family.clone(), |e, family| e.font_family(family));
1008 let value_content = self.value_row(&frame);
1009
1010 if !self.is_disabled && !self.is_read_only {
1016 root = self.root_keys(root, &frame);
1017 }
1018
1019 let show_list = overlay_active
1020 && !self.is_disabled
1021 && (!frame.matches.is_empty() || self.allows_empty_collection);
1022 root = self.root_dismissals(root, &frame, overlay_active && !show_list);
1023 root = root.child(input_group);
1028 let blur_focus = frame.blur_focus.clone();
1029 if show_list {
1030 root = root.child(self.popover(frame, cx));
1031 }
1032
1033 root = util::apply_sx(root, &self.sx);
1037 root.when_some(value_content, |root, value| root.child(value))
1038 .track_focus(&blur_focus)
1039 }
1040}
1041
1042impl ComboBox {
1043 fn frame(&self, controlled: ComboControlled, window: &mut Window, cx: &mut App) -> ComboFrame {
1046 let ComboControlled {
1047 ref base_id,
1048 ref items,
1049 open_state,
1050 ref open_own,
1051 ..
1052 } = controlled;
1053 let (resolved_placement, entry_placement) =
1057 crate::popover::field_placement_feedback(window, cx, base_id, self.placement);
1058
1059 let anchor_bounds: Rc<Cell<Option<gpui::Bounds<Pixels>>>> = window
1061 .use_keyed_state(element_id::scoped(base_id, "anchor-bounds"), cx, |_, _| {
1062 Rc::new(Cell::new(None))
1063 })
1064 .read(cx)
1065 .clone();
1066 let colors = cx.colors().clone();
1067 let layout = cx.layout().clone();
1068 let container_radius = self.radius.unwrap_or_else(|| util::container_radius(cx));
1072 let close_open = util::shared({
1073 let own = open_own.clone();
1074 let callback = self.on_open_change.clone();
1075 move |window: &mut Window, cx: &mut App| {
1076 let is_open = own.as_ref().map_or(open_state, |held| *held.read(cx));
1077 if !is_open {
1078 return;
1079 }
1080 if let Some(held) = &own {
1081 held.update(cx, |value, cx| {
1082 *value = false;
1083 cx.notify();
1084 });
1085 }
1086 if let Some(callback) = &callback {
1087 callback(&false, window, cx);
1088 }
1089 }
1090 });
1091 let raw_query = self.state.read(cx).value().to_owned();
1092 let is_invalid = self.is_invalid || self.error_message.is_some();
1093 let focus_handle = self.state.read(cx).focus_handle.clone();
1094
1095 self.sync_form(&controlled, &raw_query, &focus_handle, window, cx);
1096
1097 let show_all_items =
1098 window.use_keyed_state(element_id::scoped(base_id, "show-all-items"), cx, |_, _| {
1099 false
1100 });
1101 let last_query =
1102 window.use_keyed_state(element_id::scoped(base_id, "last-query"), cx, |_, _| {
1103 raw_query.clone()
1104 });
1105 if *last_query.read(cx) != raw_query {
1106 last_query.update(cx, |value, _| value.clone_from(&raw_query));
1107 show_all_items.update(cx, |value, _| *value = false);
1108 }
1109 let display_full_collection =
1110 *show_all_items.read(cx) || (self.menu_trigger == MenuTrigger::Manual && !open_state);
1111
1112 let cursor = window.use_keyed_state(element_id::scoped(base_id, "cursor"), cx, |_, _| {
1116 None::<ComboCursor>
1117 });
1118
1119 let matches = self.matches(
1120 &controlled,
1121 &raw_query,
1122 display_full_collection,
1123 &cursor,
1124 window,
1125 cx,
1126 );
1127 let focus_open = self.focus_open(&controlled, &show_all_items, window, cx);
1128
1129 let inside_pressed = Rc::new(Cell::new(false));
1135
1136 let stale_cursor = cursor.read(cx).as_ref().is_some_and(|focused| {
1137 let visible = cursor_position(&matches, focused).is_some();
1138 let retained_hidden = focused.hidden_query.as_deref() == Some(raw_query.as_str())
1139 && items.iter().any(|item| item.key() == &focused.key);
1140 self.disabled_keys.contains(&focused.key) || (!visible && !retained_hidden)
1141 });
1142 if stale_cursor {
1143 cursor.update(cx, |value, _| *value = None);
1144 }
1145 let cursor_at = cursor
1146 .read(cx)
1147 .as_ref()
1148 .and_then(|focused| cursor_position(&matches, focused));
1149 let list_scroll =
1152 window.use_keyed_state(element_id::scoped(base_id, "list-scroll"), cx, |_, _| {
1153 crate::VirtualListHandle::uniform(0)
1154 });
1155 let panel_scroll =
1156 window.use_keyed_state(element_id::scoped(base_id, "panel-scroll"), cx, |_, _| {
1157 gpui::ScrollHandle::new()
1158 });
1159 let list_scroll_now = list_scroll.read(cx).clone();
1160 let panel_scroll_now = panel_scroll.read(cx).clone();
1161
1162 let commit_value = self.commit_action(&controlled);
1163 let blur_close = close_open.clone();
1164 let blur_commit = commit_value.clone();
1165 let blur_scope =
1166 util::on_focus_leave(window, cx, base_id, !self.is_disabled, move |window, cx| {
1167 blur_commit(window, cx);
1168 blur_close(window, cx);
1169 });
1170 let blur_focus = blur_scope.focus_handle();
1171
1172 let ComboControlled {
1173 entity_id,
1174 base_id,
1175 items,
1176 multiple,
1177 selection_own,
1178 open_state,
1179 open_own,
1180 overlay_phase,
1181 dismissal_token,
1182 ..
1183 } = controlled;
1184 ComboFrame {
1185 entity_id,
1186 base_id,
1187 items,
1188 multiple,
1189 selection_own,
1190 open_state,
1191 open_own,
1192 overlay_phase,
1193 dismissal_token,
1194 resolved_placement,
1195 entry_placement,
1196 anchor_bounds,
1197 colors,
1198 layout,
1199 container_radius,
1200 close_open,
1201 raw_query,
1202 is_invalid,
1203 focus_handle,
1204 show_all_items,
1205 display_full_collection,
1206 cursor,
1207 matches,
1208 focus_open,
1209 inside_pressed,
1210 cursor_at,
1211 list_scroll_now,
1212 panel_scroll_now,
1213 commit_value,
1214 blur_scope,
1215 blur_focus,
1216 }
1217 }
1218
1219 fn sync_form(
1222 &self,
1223 controlled: &ComboControlled,
1224 raw_query: &str,
1225 focus_handle: &gpui::FocusHandle,
1226 window: &mut Window,
1227 cx: &mut App,
1228 ) {
1229 let ComboControlled {
1230 ref base_id,
1231 ref items,
1232 ref default_selection,
1233 ref selection_own,
1234 ..
1235 } = *controlled;
1236 let form_text = form_submits_text(self.form_value, self.allows_custom_value);
1243 {
1244 let mut form = self.form_state.borrow_mut();
1245 form.value = if form_text {
1246 crate::form::FormValue::Text(SharedString::from(raw_query))
1247 } else {
1248 crate::form::FormValue::Keys(self.selected_keys.clone())
1249 };
1250 form.focus = Some(focus_handle.clone());
1251 }
1252
1253 if self.selected_key_sync {
1259 let applied_key =
1260 window.use_keyed_state(element_id::scoped(base_id, "applied-key"), cx, |_, _| {
1261 None::<SharedString>
1262 });
1263 let owned_key = self.selected_keys.first().cloned().unwrap_or_default();
1264 let first_apply = applied_key.read(cx).is_none();
1265 if applied_key.read(cx).clone() != Some(owned_key.clone()) {
1266 if !(first_apply && (self.default_input_value.is_some() || self.value.is_some())) {
1272 let label = label_of_key(items, &owned_key).cloned().unwrap_or_default();
1273 self.state.update(cx, |state, cx| {
1274 state.set_value(label.to_string());
1275 cx.notify();
1276 });
1277 }
1278 applied_key.update(cx, |v, _| *v = Some(owned_key));
1279 }
1280 }
1281
1282 let restore_own = selection_own.clone();
1288 let restore_state = Rc::downgrade(&self.form_state);
1289 let restore_input = self.state.clone();
1290 let restore_items = items.clone();
1291 let restore_default = default_selection.clone();
1292 let restore_input_change = self.on_input_change.clone();
1293 let restore_all = self.on_selection_change_all.clone();
1294 self.form_state.borrow_mut().restore = (restore_own.is_some() || restore_all.is_some())
1295 .then(|| {
1296 util::shared(move |window: &mut Window, cx: &mut App| {
1297 let default_text = restore_default
1298 .first()
1299 .and_then(|key| label_of_key(&restore_items, key))
1300 .cloned()
1301 .unwrap_or_default();
1302 if let Some(state) = restore_state.upgrade() {
1303 state.borrow_mut().value =
1304 crate::form::FormValue::Keys(restore_default.clone());
1305 }
1306 let input_changed = restore_input.read(cx).value() != default_text.as_ref();
1307 restore_input.update(cx, |state, cx| {
1308 state.set_value(default_text.to_string());
1309 cx.notify();
1310 });
1311 if input_changed {
1312 if let Some(cb) = &restore_input_change {
1313 cb(&default_text, window, cx);
1314 }
1315 }
1316 if let Some(held) = &restore_own {
1317 let keys = restore_default.clone();
1318 held.update(cx, |v, cx| {
1319 *v = keys;
1320 cx.notify();
1321 });
1322 }
1323 if let Some(cb) = &restore_all {
1324 cb(&restore_default, window, cx);
1325 }
1326 }) as Arc<dyn Fn(&mut Window, &mut App)>
1327 });
1328 }
1329
1330 fn matches(
1333 &self,
1334 controlled: &ComboControlled,
1335 raw_query: &str,
1336 display_full_collection: bool,
1337 cursor: &Entity<Option<ComboCursor>>,
1338 window: &mut Window,
1339 cx: &mut App,
1340 ) -> Rc<[PickerItem]> {
1341 let ComboControlled {
1342 ref base_id,
1343 ref items,
1344 overlay_phase,
1345 ..
1346 } = *controlled;
1347 let overlay_active = overlay_phase != util::OverlayPhase::Closed;
1348 let matches_cache =
1359 window.use_keyed_state(element_id::scoped(base_id, "matches"), cx, |_, _| {
1360 MatchesCache::default()
1361 });
1362 let retained_matches = window.use_keyed_state(
1367 element_id::scoped(base_id, "retained-matches"),
1368 cx,
1369 |_, _| empty_matches(),
1370 );
1371 let consume_matches = overlay_active
1372 || cursor.read(cx).is_some()
1373 || (self.allows_custom_value && !raw_query.is_empty());
1374 let matches: Rc<[PickerItem]> = if overlay_phase == util::OverlayPhase::Exiting {
1375 retained_matches.read(cx).clone()
1376 } else if !consume_matches {
1377 empty_matches()
1378 } else {
1379 let filter = self.filter.clone();
1380 match &filter {
1381 Some(f) => Rc::from(compute_matches(
1382 items,
1383 raw_query,
1384 self.max_items,
1385 display_full_collection,
1386 Some(f),
1387 )),
1388 None if display_full_collection => Rc::from(compute_matches(
1389 items,
1390 raw_query,
1391 self.max_items,
1392 true,
1393 None,
1394 )),
1395 None if raw_query.is_empty() => Rc::from(compute_matches(
1396 items,
1397 raw_query,
1398 self.max_items,
1399 false,
1400 None,
1401 )),
1402 None => matches_cache.update(cx, |cache, _| {
1403 cache.get(items.clone(), raw_query, self.max_items, |items| {
1404 compute_matches(items, raw_query, self.max_items, false, None)
1405 })
1406 }),
1407 }
1408 };
1409 if overlay_phase == util::OverlayPhase::Open {
1410 retained_matches.update(cx, |value, _| *value = matches.clone());
1411 }
1412 matches
1413 }
1414
1415 fn focus_open(
1417 &self,
1418 controlled: &ComboControlled,
1419 show_all_items: &Entity<bool>,
1420 window: &mut Window,
1421 cx: &mut App,
1422 ) -> Option<Entity<FocusOpen>> {
1423 let ComboControlled {
1424 ref base_id,
1425 ref items,
1426 open_state,
1427 ref open_own,
1428 ..
1429 } = *controlled;
1430 let focus_open =
1438 if self.menu_trigger == MenuTrigger::Focus && !self.is_disabled && !self.is_read_only {
1439 Some(window.use_keyed_state(
1440 element_id::scoped(base_id, "focus-open"),
1441 cx,
1442 |_, _| FocusOpen {
1443 can_open: true,
1444 was_open: false,
1445 },
1446 ))
1447 } else {
1448 None
1449 };
1450 if let Some(focus_open) = &focus_open {
1451 let focused = self.state.read(cx).focus_handle.is_focused(window);
1452 let mut held = *focus_open.read(cx);
1453 let mut now_open = open_state;
1454 if focused && !open_state && held.was_open {
1455 held.can_open = false;
1458 } else if focused && !open_state && held.can_open {
1459 let can_show = !items.is_empty() || self.allows_empty_collection;
1463 if can_show {
1464 show_all_items.update(cx, |v, _| *v = true);
1465 if let Some(open) = &open_own {
1469 open.update(cx, |v, cx| {
1470 *v = true;
1471 cx.notify();
1472 });
1473 }
1474 if let Some(cb) = &self.on_open_change {
1475 cb(&true, window, cx);
1476 }
1477 held.can_open = false;
1478 now_open = true;
1479 }
1480 }
1481 if !focused {
1482 held.can_open = true;
1484 }
1485 held.was_open = now_open;
1486 focus_open.update(cx, |v, _| *v = held);
1487 }
1488 focus_open
1489 }
1490
1491 fn commit_action(&self, controlled: &ComboControlled) -> ComboAction {
1493 let ComboControlled {
1494 ref items,
1495 multiple,
1496 ref selection_own,
1497 ..
1498 } = *controlled;
1499 util::shared({
1503 let state = self.state.clone();
1504 let selection_own = selection_own.clone();
1505 let selected = self.selected_keys.clone();
1506 let items = items.clone();
1507 let selection_change = self.on_selection_change_all.clone();
1508 let input_change = self.on_input_change.clone();
1509 let allows_custom = self.allows_custom_value;
1510 move |window: &mut Window, cx: &mut App| {
1511 let current = state.read(cx).value().to_owned();
1512 let selected_text = selected
1513 .first()
1514 .and_then(|key| label_of_key(&items, key))
1515 .cloned()
1516 .unwrap_or_default();
1517
1518 if allows_custom {
1519 if !multiple && current != selected_text && !selected.is_empty() {
1523 if let Some(held) = &selection_own {
1524 held.update(cx, |value, cx| {
1525 value.clear();
1526 cx.notify();
1527 });
1528 }
1529 if let Some(callback) = &selection_change {
1530 callback(&[], window, cx);
1531 }
1532 }
1533 return;
1534 }
1535
1536 let committed = if multiple {
1537 String::new()
1538 } else {
1539 selected_text.to_string()
1540 };
1541 if current != committed {
1542 state.update(cx, |state, cx| {
1543 state.set_value(committed.clone());
1544 cx.notify();
1545 });
1546 if let Some(callback) = &input_change {
1547 callback(&committed, window, cx);
1548 }
1549 }
1550 }
1551 })
1552 }
1553
1554 fn trigger(
1556 &self,
1557 frame: &ComboFrame,
1558 window: &mut Window,
1559 cx: &mut App,
1560 ) -> gpui::Stateful<gpui::Div> {
1561 let ComboFrame {
1562 ref base_id,
1563 open_state,
1564 ref open_own,
1565 ref colors,
1566 ref close_open,
1567 ref focus_handle,
1568 ref show_all_items,
1569 ref focus_open,
1570 ..
1571 } = *frame;
1572 let on_open_change = self.on_open_change.clone();
1573 let is_open = open_state;
1574 let trigger_hover_fg = colors.field.foreground;
1580 let trigger_indicator = crate::anim::rotating_indicator_with_duration(
1581 &element_id::scoped(base_id, "trigger-indicator"),
1582 is_open,
1583 gpui::svg()
1584 .size(util::FIELD_ICON)
1585 .path(icons::CHEVRON_DOWN)
1586 .text_color(colors.muted)
1587 .when(!self.is_disabled && !self.is_read_only, |glyph| {
1588 glyph.hover(move |st| st.text_color(trigger_hover_fg))
1589 }),
1590 150,
1591 window,
1592 cx,
1593 );
1594 let mut trigger = div()
1595 .id(element_id::scoped(base_id, "trigger"))
1596 .a11y(a11y::Role::Button)
1604 .a11y_expanded(is_open)
1605 .flex()
1606 .items_center()
1607 .justify_center()
1608 .flex_shrink_0()
1609 .size(px(20.))
1610 .rounded(px(6.))
1611 .text_color(colors.muted)
1612 .child(trigger_indicator);
1613 if !self.is_disabled && !self.is_read_only {
1614 trigger = trigger
1615 .cursor(util::interactive_cursor(cx))
1616 .hover(move |s| s.text_color(trigger_hover_fg));
1617 if on_open_change.is_some() || open_own.is_some() {
1618 let own = open_own.clone();
1619 let focus_open = focus_open.clone();
1620 let show_all_items = show_all_items.clone();
1621 let close = close_open.clone();
1622 let focus_handle = focus_handle.clone();
1623 trigger = trigger
1624 .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
1628 if let Some(focus_open) = &focus_open {
1629 focus_open.update(cx, |v, _| v.can_open = false);
1630 }
1631 window.focus(&focus_handle, cx);
1632 cx.stop_propagation();
1633 })
1634 .on_click(move |_, window, cx| {
1635 if is_open {
1636 close(window, cx);
1637 return;
1638 }
1639 show_all_items.update(cx, |v, _| *v = true);
1640 if let Some(held) = &own {
1641 held.update(cx, |v, cx| {
1642 *v = true;
1643 cx.notify();
1644 });
1645 }
1646 if let Some(cb) = &on_open_change {
1647 cb(&true, window, cx);
1648 }
1649 });
1650 }
1651 }
1652 trigger
1653 }
1654
1655 fn input(&self, frame: &ComboFrame, trigger: gpui::Stateful<gpui::Div>) -> Input {
1658 let ComboFrame {
1659 ref items,
1660 ref selection_own,
1661 open_state,
1662 ref open_own,
1663 ref close_open,
1664 ref raw_query,
1665 is_invalid,
1666 ref show_all_items,
1667 ref cursor,
1668 ref focus_open,
1669 ..
1670 } = *frame;
1671 let cursor_on_change = cursor.clone();
1672 let validate = self.validate.clone();
1673 let mut input = Input::new(self.state.clone())
1674 .variant(self.variant)
1675 .is_disabled(self.is_disabled)
1676 .is_invalid(is_invalid)
1677 .is_required(self.is_required)
1678 .is_read_only(self.is_read_only)
1679 .auto_focus(self.auto_focus)
1680 .when_some(self.font_family.clone(), |i, family| i.font_family(family))
1681 .when_some(self.validation_behavior, |i, b| i.validation_behavior(b))
1682 .when_some(validate, |i, f| i.validate(move |v| f(v)))
1683 .end_content(trigger);
1684 input = input.with_field_box(self.field);
1685 let input_change = self.on_input_change.clone();
1690 let open_own_on_change = open_own.clone();
1691 let open_change_cb = self.on_open_change.clone();
1692 let focus_open_on_change = focus_open.clone();
1693 let show_all_items_on_change = show_all_items.clone();
1694 let items_on_change = items.clone();
1695 let filter_on_change = self.filter.clone();
1696 let menu_trigger_on_change = self.menu_trigger;
1697 let allows_empty_collection = self.allows_empty_collection;
1698 let was_open = open_state;
1699 let selection_own_on_change = selection_own.clone();
1700 let selected_on_change = self.selected_keys.clone();
1701 let selection_mode_on_change = self.selection_mode;
1702 let selection_change_on_input = self.on_selection_change_all.clone();
1703 let input_value_on_change = raw_query.clone();
1704 let close_on_empty = close_open.clone();
1705 input = input.on_change(move |text, window, cx| {
1706 if let Some(cb) = &input_change {
1707 cb(text, window, cx);
1708 }
1709 cursor_on_change.update(cx, |v, _| *v = None);
1710
1711 if selection_mode_on_change == SelectionMode::Single
1712 && !input_value_on_change.is_empty()
1713 && text.is_empty()
1714 && !selected_on_change.is_empty()
1715 {
1716 if let Some(held) = &selection_own_on_change {
1717 held.update(cx, |value, cx| {
1718 value.clear();
1719 cx.notify();
1720 });
1721 }
1722 if let Some(cb) = &selection_change_on_input {
1723 cb(&[], window, cx);
1724 }
1725 }
1726
1727 let is_open = open_own_on_change
1728 .as_ref()
1729 .map_or(was_open, |held| *held.read(cx));
1730
1731 let closing_after_pick = was_open && !is_open;
1738 let has_matches = if closing_after_pick {
1739 true
1740 } else {
1741 match &filter_on_change {
1742 Some(f) => items_on_change.iter().any(|item| f(item.label(), text)),
1743 None if text.is_empty() => !items_on_change.is_empty(),
1744 None => {
1745 let query = text.to_lowercase();
1746 items_on_change
1747 .iter()
1748 .any(|item| item.label().to_lowercase().contains(&query))
1749 }
1750 }
1751 };
1752 let can_show = has_matches || allows_empty_collection;
1753
1754 if !is_open && menu_trigger_on_change != MenuTrigger::Manual && can_show {
1755 if let Some(focus_open) = &focus_open_on_change {
1756 focus_open.update(cx, |v, _| v.can_open = false);
1757 }
1758 if let Some(held) = &open_own_on_change {
1759 held.update(cx, |v, cx| {
1760 *v = true;
1761 cx.notify();
1762 });
1763 }
1764 if let Some(cb) = &open_change_cb {
1765 cb(&true, window, cx);
1766 }
1767 } else if is_open && !can_show {
1768 close_on_empty(window, cx);
1769 }
1770
1771 show_all_items_on_change.update(cx, |v, cx| {
1772 *v = false;
1773 cx.notify();
1774 });
1775 if menu_trigger_on_change == MenuTrigger::Manual {
1776 cx.refresh_windows();
1777 }
1778 });
1779
1780 if self.full_width {
1781 input = input.full_width();
1782 }
1783 if let Some(label) = self.label.clone() {
1784 input = input.label(label);
1785 }
1786 if let Some(placeholder) = self.placeholder.clone() {
1787 input = input.placeholder(placeholder);
1788 }
1789 if is_invalid {
1790 if let Some(message) = self.error_message.clone() {
1791 input = input.error_message(message);
1792 }
1793 } else if let Some(description) = self.description.clone() {
1794 input = input.description(description);
1795 }
1796 input
1797 }
1798
1799 fn value_row(&mut self, frame: &ComboFrame) -> Option<gpui::AnyElement> {
1801 let ComboFrame {
1802 entity_id,
1803 ref items,
1804 ref colors,
1805 ..
1806 } = *frame;
1807 let mut value_content = None;
1814 if let Some(render) = self.value_content.take() {
1815 let mut labels: Vec<SharedString> = Vec::new();
1816 let mut indices: Vec<usize> = Vec::new();
1817 for key in &self.selected_keys {
1818 if let Some((index, item)) =
1819 items.iter().enumerate().find(|(_, it)| it.key() == key)
1820 {
1821 labels.push(item.label().clone());
1822 indices.push(index);
1823 }
1824 }
1825 let text = labels
1826 .iter()
1827 .map(ToString::to_string)
1828 .collect::<Vec<_>>()
1829 .join(", ");
1830 let value_selector = format!("combobox-value-{entity_id}");
1831 let default_children = div()
1832 .debug_selector(move || value_selector)
1833 .w_full()
1834 .min_w_0()
1835 .whitespace_normal()
1836 .text_size(util::FIELD_TEXT)
1837 .line_height(px(20.))
1838 .text_color(colors.field.foreground)
1839 .child(text.clone())
1840 .into_any_element();
1841 value_content = Some(render(util::SelectionValue {
1842 selected_items: &labels,
1843 selected_indices: &indices,
1844 selected_keys: Some(&self.selected_keys),
1845 selected_text: &text,
1846 is_placeholder: labels.is_empty(),
1847 default_children,
1848 }));
1849 }
1850 value_content
1851 }
1852
1853 fn root_keys(&self, root: gpui::Div, frame: &ComboFrame) -> gpui::Div {
1856 let ComboFrame {
1857 ref items,
1858 multiple,
1859 ref selection_own,
1860 open_state,
1861 ref open_own,
1862 ref close_open,
1863 ref raw_query,
1864 ref show_all_items,
1865 display_full_collection,
1866 ref cursor,
1867 ref matches,
1868 ref list_scroll_now,
1869 ref panel_scroll_now,
1870 ref blur_scope,
1871 ..
1872 } = *frame;
1873 let key_rows: Rc<[PickerItem]> =
1874 if open_state || (self.allows_custom_value && !raw_query.is_empty()) {
1875 Rc::clone(matches)
1876 } else {
1877 items
1878 .iter()
1879 .take(self.max_items)
1880 .cloned()
1881 .collect::<Vec<_>>()
1882 .into()
1883 };
1884 let stops: Vec<usize> = (0..key_rows.len())
1885 .filter(|i| {
1886 key_rows
1887 .get(*i)
1888 .is_some_and(|item| !self.disabled_keys.contains(item.key()))
1889 })
1890 .collect();
1891 let held = cursor.clone();
1892 let wrap = self.should_focus_wrap;
1893 let virtual_rows = self.row_height.is_some();
1894 let key_list_scroll = list_scroll_now.clone();
1895 let key_panel_scroll = panel_scroll_now.clone();
1896 let rows = key_rows;
1897 let state = self.state.clone();
1898 let allows_custom_value = self.allows_custom_value;
1899 let was_open = open_state;
1900 let show_all_items = show_all_items.clone();
1901 let on_selection_change = self.on_selection_change.clone();
1902 let on_selection_change_all = self.on_selection_change_all.clone();
1903 let on_input_change = self.on_input_change.clone();
1904 let selected_now = self.selected_keys.clone();
1905 let key_query = raw_query.clone();
1906 let key_display_full = display_full_collection;
1907 let key_items = items.clone();
1908 let key_filter = self.filter.clone();
1909 let key_max_items = self.max_items;
1910 let key_disabled = self.disabled_keys.clone();
1911 let key_menu_trigger = self.menu_trigger;
1912 let open_own_keys = open_own.clone();
1913 let on_open_change = self.on_open_change.clone();
1914 let key_selection_own = selection_own.clone();
1915 let key_close = close_open.clone();
1916 let key_blur = blur_scope.clone();
1917 let key_multiple = multiple;
1918 let handler = ComboKeys {
1919 rows,
1920 stops,
1921 held,
1922 wrap,
1923 virtual_rows,
1924 key_list_scroll,
1925 key_panel_scroll,
1926 state,
1927 allows_custom_value,
1928 was_open,
1929 show_all_items,
1930 on_selection_change,
1931 on_selection_change_all,
1932 on_input_change,
1933 selected_now,
1934 key_query,
1935 key_display_full,
1936 key_items,
1937 key_filter,
1938 key_max_items,
1939 key_disabled,
1940 key_menu_trigger,
1941 open_own_keys,
1942 on_open_change,
1943 key_selection_own,
1944 key_close,
1945 key_blur,
1946 key_multiple,
1947 };
1948 root.on_key_down(move |event, window, cx| handler.on_key_down(event, window, cx))
1949 }
1950
1951 fn root_dismissals(
1953 &self,
1954 mut root: gpui::Div,
1955 frame: &ComboFrame,
1956 dismiss_without_list: bool,
1957 ) -> gpui::Div {
1958 let ComboFrame {
1959 ref dismissal_token,
1960 ref close_open,
1961 ref commit_value,
1962 ref blur_scope,
1963 ..
1964 } = *frame;
1965 let escape_close = close_open.clone();
1966 root =
1967 util::dismiss_on_escape_with_token(root, dismissal_token.clone(), move |window, cx| {
1968 escape_close(window, cx);
1969 util::DismissResult::Handled
1970 });
1971 if dismiss_without_list {
1972 let dismiss_close = close_open.clone();
1973 let dismiss_commit = commit_value.clone();
1974 let dismiss_blur = blur_scope.clone();
1975 root = util::dismiss_on_press_outside_with_token(
1976 root,
1977 dismissal_token.clone(),
1978 move |window, cx| {
1979 dismiss_blur.consume(cx);
1980 dismiss_commit(window, cx);
1981 dismiss_close(window, cx);
1982 util::DismissResult::Handled
1983 },
1984 );
1985 }
1986 root
1987 }
1988
1989 fn panel_surface(&self, frame: &ComboFrame) -> gpui::Stateful<gpui::Div> {
1992 let ComboFrame {
1993 entity_id,
1994 ref base_id,
1995 overlay_phase,
1996 ref colors,
1997 ref layout,
1998 container_radius,
1999 ref panel_scroll_now,
2000 ..
2001 } = *frame;
2002 let panel_selector = format!("combobox-panel-{entity_id}");
2003 div()
2004 .id(element_id::scoped(base_id, "panel"))
2005 .a11y(a11y::Role::ListBox)
2010 .a11y_orientation(herogpui_core::Orientation::Vertical)
2011 .debug_selector(move || panel_selector)
2012 .w_full()
2013 .flex()
2014 .flex_col()
2015 .gap(px(2.))
2016 .p(px(4.))
2017 .overflow_y_scroll()
2018 .when(overlay_phase == util::OverlayPhase::Open, |el| el.occlude())
2023 .track_scroll(panel_scroll_now)
2024 .max_h_full()
2029 .rounded(container_radius)
2030 .bg(colors.overlay.background)
2031 .when_some(layout.overlay_hairline, |el, hairline| {
2035 el.border(layout.border_width).border_color(hairline)
2036 })
2037 .text_color(colors.overlay.foreground)
2038 .when(
2039 !layout.overlay_shadow.is_empty(),
2040 |e: gpui::Stateful<gpui::Div>| e.shadow(layout.overlay_shadow.clone()),
2041 )
2042 }
2043
2044 fn popover(&mut self, frame: ComboFrame, cx: &mut App) -> gpui::Deferred {
2046 let panel = self.panel_surface(&frame);
2047 let ComboFrame {
2048 entity_id,
2049 base_id,
2050 multiple,
2051 selection_own,
2052 open_state,
2053 open_own,
2054 overlay_phase,
2055 dismissal_token,
2056 resolved_placement,
2057 entry_placement,
2058 anchor_bounds,
2059 colors,
2060 layout,
2061 container_radius,
2062 close_open,
2063 cursor,
2064 matches,
2065 inside_pressed,
2066 cursor_at,
2067 list_scroll_now,
2068 commit_value,
2069 blur_scope,
2070 ..
2071 } = frame;
2072 let dismiss_close = close_open.clone();
2078 let dismiss_commit = commit_value.clone();
2079 let dismiss_blur = blur_scope;
2080 let mut panel =
2081 util::dismiss_on_press_outside_with_token(panel, dismissal_token, move |window, cx| {
2082 if inside_pressed.get() {
2083 return util::DismissResult::Declined;
2084 }
2085 dismiss_blur.consume(cx);
2086 dismiss_commit(window, cx);
2087 dismiss_close(window, cx);
2088 util::DismissResult::Handled
2089 });
2090
2091 if matches.is_empty() {
2092 let message = if self.allows_custom_value {
2095 "Press Enter to use this value"
2096 } else {
2097 "No matching options"
2098 };
2099 panel = panel.child(
2100 div()
2101 .px(px(8.))
2102 .py(px(6.))
2103 .text_size(util::FIELD_TEXT)
2104 .line_height(px(20.))
2105 .text_color(colors.muted)
2106 .child(message),
2107 );
2108 }
2109
2110 let matches_len = matches.len();
2115 let rows = matches;
2116 let sections = self.sections.clone();
2117 let row_disabled_keys = self.disabled_keys.clone();
2118 let row_read_only = self.is_read_only;
2119 let row_selected_keys = self.selected_keys.clone();
2120 let indicator: Option<Rc<dyn Fn(bool) -> gpui::AnyElement>> =
2121 self.indicator.take().map(Rc::from);
2122 let on_change_all = self.on_selection_change_all.clone();
2123 let row_input_change = self.on_input_change.clone();
2124 let on_change_one = self.on_selection_change.clone();
2125 let row_state = self.state.clone();
2126 let row_cursor = cursor;
2127 let selection_own = selection_own;
2128 let row_open_own = open_own;
2129 let row_open_state = open_state;
2130 let row_close_open = close_open.clone();
2131 let row_disabled_opacity = layout.disabled_opacity;
2132 let row_base = element_id::scoped(&base_id, "item");
2135 let row_virtualized = self.row_height.is_some();
2138 let panel_interactive = overlay_phase == util::OverlayPhase::Open;
2139 let row_count = rows.len();
2140 let row_padding_x = self.row_padding_x.unwrap_or(px(8.));
2141 let row_font_family = self.row_font_family.clone();
2142 let row_padding_y = self.row_padding_y.unwrap_or(px(6.));
2143 let rows = ComboRows {
2144 entity_id,
2145 multiple,
2146 cursor_at,
2147 colors,
2148 row_hover_bg: self.row_hover_bg,
2149 rows,
2150 sections,
2151 row_disabled_keys,
2152 row_read_only,
2153 row_selected_keys,
2154 indicator,
2155 on_change_all,
2156 row_input_change,
2157 on_change_one,
2158 row_state,
2159 row_cursor,
2160 selection_own,
2161 row_open_own,
2162 row_open_state,
2163 row_close_open,
2164 row_disabled_opacity,
2165 row_base,
2166 row_virtualized,
2167 panel_interactive,
2168 row_count,
2169 row_padding_x,
2170 row_font_family,
2171 row_padding_y,
2172 };
2173
2174 match self.row_height {
2175 Some(row_height) => {
2185 let handle = list_scroll_now;
2188 if handle.item_count() != matches_len {
2189 handle.splice(0..handle.item_count(), matches_len);
2190 }
2191 panel = panel.child(crate::VirtualList::new(
2192 element_id::scoped(&base_id, "rows"),
2193 &handle,
2194 move |i, _window, cx| rows.row(i, Some(row_height), cx),
2195 ));
2196 }
2197 None => {
2198 for index in 0..matches_len {
2199 panel = panel.child(rows.row(index, None, cx));
2200 }
2201 }
2202 }
2203
2204 let (slide_x, slide_y) = crate::popover::placement_entry_offset(entry_placement);
2205 let zoom = crate::anim::ZoomBox::panel(px(4.), container_radius).padding_x(px(4.));
2206 let zoom = crate::anim::ZoomBox {
2207 slide_x: (slide_x != 0.0).then(|| px(slide_x)),
2208 slide_y: (slide_y != 0.0).then(|| px(slide_y)),
2209 ..zoom
2210 };
2211 let panel = if overlay_phase == util::OverlayPhase::Exiting {
2212 crate::anim::exiting(
2213 panel,
2214 element_id::scoped(&base_id, "anim-out"),
2215 zoom,
2216 crate::anim::Motion::LIST_OUT,
2217 cx,
2218 )
2219 } else {
2220 crate::anim::entering_zoom(
2221 panel,
2222 element_id::scoped(&base_id, "anim"),
2223 zoom,
2224 crate::anim::Motion::LIST_IN,
2225 cx,
2226 )
2227 };
2228 util::floating(
2232 crate::popover::scrollable_field_popover_with_resolved_placement(
2233 anchor_bounds,
2234 self.placement,
2235 Some(resolved_placement),
2236 panel,
2237 ),
2238 )
2239 }
2240}
2241
2242struct ComboKeys {
2246 rows: Rc<[PickerItem]>,
2247 stops: Vec<usize>,
2248 held: Entity<Option<ComboCursor>>,
2249 wrap: bool,
2250 virtual_rows: bool,
2251 key_list_scroll: crate::VirtualListHandle,
2252 key_panel_scroll: gpui::ScrollHandle,
2253 state: Entity<InputState>,
2254 allows_custom_value: bool,
2255 was_open: bool,
2256 show_all_items: Entity<bool>,
2257 on_selection_change: Option<OnSelectionChange>,
2258 on_selection_change_all: Option<OnSelectionChangeAll>,
2259 on_input_change: Option<OnInputChange>,
2260 selected_now: Vec<SharedString>,
2261 key_query: String,
2262 key_display_full: bool,
2263 key_items: Rc<[PickerItem]>,
2264 key_filter: Option<ComboFilter>,
2265 key_max_items: usize,
2266 key_disabled: std::collections::HashSet<SharedString>,
2267 key_menu_trigger: MenuTrigger,
2268 open_own_keys: Option<Entity<bool>>,
2269 on_open_change: Option<OnOpenChange>,
2270 key_selection_own: Option<Entity<Vec<SharedString>>>,
2271 key_close: ComboAction,
2272 key_blur: util::FocusLeave,
2273 key_multiple: bool,
2274}
2275
2276impl ComboKeys {
2277 fn on_key_down(&self, event: &gpui::KeyDownEvent, window: &mut Window, cx: &mut App) {
2278 let rows = Rc::clone(&self.rows);
2281 let Self {
2282 ref stops,
2283 ref held,
2284 wrap,
2285 ref state,
2286 allows_custom_value,
2287 was_open,
2288 ref show_all_items,
2289 ref on_selection_change_all,
2290 ref on_input_change,
2291 ref selected_now,
2292 ref key_query,
2293 key_display_full,
2294 ref key_items,
2295 ref key_filter,
2296 key_max_items,
2297 ref key_disabled,
2298 key_menu_trigger,
2299 ref open_own_keys,
2300 ref key_selection_own,
2301 ref key_close,
2302 key_multiple,
2303 ..
2304 } = *self;
2305 let key = event.keystroke.key.as_str();
2306 let is_open = open_own_keys
2307 .as_ref()
2308 .map_or(was_open, |held| *held.read(cx));
2309 let stale_cursor = held.read(cx).as_ref().is_some_and(|focused| {
2310 let current_query = state.read(cx).value().to_owned();
2311 let display_full = (current_query == *key_query && *show_all_items.read(cx))
2312 || (key_menu_trigger == MenuTrigger::Manual && !is_open);
2313 let current_rows: Rc<[PickerItem]> =
2314 if !(is_open || (allows_custom_value && !current_query.is_empty())) {
2315 key_items
2316 .iter()
2317 .take(key_max_items)
2318 .cloned()
2319 .collect::<Vec<_>>()
2320 .into()
2321 } else if current_query == *key_query
2322 && is_open == was_open
2323 && display_full == key_display_full
2324 {
2325 Rc::clone(&rows)
2326 } else {
2327 compute_matches(
2328 key_items,
2329 ¤t_query,
2330 key_max_items,
2331 display_full,
2332 key_filter.as_ref(),
2333 )
2334 .into()
2335 };
2336 let visible = cursor_position(¤t_rows, focused).is_some();
2337 let retained_hidden = focused.hidden_query.as_deref() == Some(current_query.as_str())
2338 && key_items.iter().any(|item| item.key() == &focused.key);
2339 key_disabled.contains(&focused.key) || (!visible && !retained_hidden)
2340 });
2341 if stale_cursor {
2342 held.update(cx, |value, _| *value = None);
2343 }
2344 if allows_custom_value
2358 && key == "enter"
2359 && !state.read(cx).value().is_empty()
2360 && held
2361 .read(cx)
2362 .as_ref()
2363 .is_none_or(|focused| stale_cursor || cursor_position(&rows, focused).is_none())
2364 {
2365 let selected_label = selected_now
2366 .first()
2367 .and_then(|item| label_of_key(key_items, item))
2368 .cloned()
2369 .unwrap_or_default();
2370 if !key_multiple && selected_label != state.read(cx).value() {
2371 let had_selection = !selected_now.is_empty();
2372 if let Some(held) = &key_selection_own {
2373 held.update(cx, |v, cx| {
2374 v.clear();
2375 cx.notify();
2376 });
2377 }
2378 if had_selection {
2379 if let Some(cb) = &on_selection_change_all {
2380 cb(&[], window, cx);
2381 }
2382 }
2383 }
2384 held.update(cx, |v, _| *v = None);
2385 key_close(window, cx);
2386 if is_open {
2392 cx.stop_propagation();
2393 }
2394 return;
2395 }
2396 if key == "enter" && (stale_cursor || held.read(cx).is_none()) {
2397 let reset_value = if key_multiple {
2398 String::new()
2399 } else {
2400 selected_now
2401 .first()
2402 .and_then(|item| label_of_key(key_items, item))
2403 .cloned()
2404 .unwrap_or_default()
2405 .to_string()
2406 };
2407 let input_changed = state.read(cx).value() != reset_value;
2408 state.update(cx, |value, cx| {
2409 value.set_value(reset_value.clone());
2410 cx.notify();
2411 });
2412 if input_changed {
2413 if let Some(cb) = &on_input_change {
2414 cb(&reset_value, window, cx);
2415 }
2416 }
2417 key_close(window, cx);
2418 if is_open || stale_cursor || input_changed {
2424 cx.stop_propagation();
2425 }
2426 return;
2427 }
2428 let from = held
2429 .read(cx)
2430 .as_ref()
2431 .and_then(|focused| cursor_position(&rows, focused));
2432 let nav_key = if key == "tab" && is_open && held.read(cx).is_some() {
2433 "enter"
2434 } else {
2435 key
2436 };
2437 let page_move = match nav_key {
2450 "pagedown" if is_open && from.is_some() => stops.last().copied(),
2451 "pageup" if is_open && from.is_some() => stops.first().copied(),
2452 _ => None,
2453 }
2454 .filter(|next| Some(*next) != from);
2455 let next_move = page_move.map_or_else(
2456 || crate::list_nav::resolve(stops, from, nav_key, wrap),
2457 crate::list_nav::Move::To,
2458 );
2459 self.apply_move(next_move, key, window, cx);
2460 }
2461
2462 fn apply_move(
2465 &self,
2466 next_move: crate::list_nav::Move,
2467 key: &str,
2468 window: &mut Window,
2469 cx: &mut App,
2470 ) {
2471 let Self {
2472 ref rows,
2473 ref held,
2474 virtual_rows,
2475 ref key_list_scroll,
2476 ref key_panel_scroll,
2477 ref state,
2478 was_open,
2479 ref show_all_items,
2480 ref on_selection_change,
2481 ref on_selection_change_all,
2482 ref on_input_change,
2483 ref selected_now,
2484 ref key_items,
2485 ref open_own_keys,
2486 ref on_open_change,
2487 ref key_selection_own,
2488 ref key_close,
2489 ref key_blur,
2490 key_multiple,
2491 ..
2492 } = *self;
2493 match next_move {
2494 crate::list_nav::Move::To(next) => {
2495 let next_cursor = Some(cursor_for(rows, next, None));
2496 held.update(cx, |v, cx| {
2497 *v = next_cursor;
2498 cx.notify();
2499 });
2500 if virtual_rows {
2501 key_list_scroll.scroll_to_item(next, crate::VirtualListScroll::Center);
2502 } else {
2503 key_panel_scroll.scroll_to_item(next);
2504 }
2505 if let Some(held) = &open_own_keys {
2507 held.update(cx, |v, cx| {
2508 *v = true;
2509 cx.notify();
2510 });
2511 }
2512 if !was_open {
2513 show_all_items.update(cx, |v, _| *v = true);
2514 if let Some(cb) = &on_open_change {
2515 cb(&true, window, cx);
2516 }
2517 }
2518 }
2519 crate::list_nav::Move::Activate => {
2520 let Some(item_key) = held.read(cx).as_ref().map(|focused| focused.key.clone())
2526 else {
2527 return;
2528 };
2529 let item_label = label_of_key(key_items, &item_key)
2530 .cloned()
2531 .unwrap_or_default();
2532 if key == "enter" {
2536 cx.stop_propagation();
2537 }
2538 if key_multiple {
2539 let had_query = !state.read(cx).value().is_empty();
2540 state.update(cx, |st, cx| {
2541 st.set_value(String::new());
2542 cx.notify();
2543 });
2544 held.update(cx, |v, _| {
2545 if let Some(focused) = v {
2546 focused.hidden_query = Some(String::new());
2547 }
2548 });
2549 let mut next = selected_now.clone();
2550 toggle_key(&mut next, &item_key);
2551 if let Some(held) = &key_selection_own {
2552 let next = next.clone();
2553 held.update(cx, |v, cx| {
2554 *v = next;
2555 cx.notify();
2556 });
2557 }
2558 if let Some(cb) = &on_selection_change_all {
2559 cb(&next, window, cx);
2560 }
2561 if had_query {
2562 if let Some(cb) = &on_input_change {
2563 cb("", window, cx);
2564 }
2565 }
2566 return;
2567 }
2568 key_close(window, cx);
2574 state.update(cx, |st, cx| {
2575 st.set_value(item_label.to_string());
2576 cx.notify();
2577 });
2578 if let Some(held) = &key_selection_own {
2581 let next = vec![item_key.clone()];
2582 held.update(cx, |v, cx| {
2583 *v = next;
2584 cx.notify();
2585 });
2586 }
2587 held.update(cx, |v, _| *v = None);
2588 if key == "tab" {
2589 key_blur.consume(cx);
2590 }
2591 if selected_now.first() != Some(&item_key) {
2592 if let Some(cb) = &on_selection_change_all {
2593 cb(std::slice::from_ref(&item_key), window, cx);
2594 }
2595 }
2596 if let Some(cb) = &on_selection_change {
2597 cb(&item_key, window, cx);
2598 }
2599 }
2600 crate::list_nav::Move::Ignore => {}
2601 }
2602 }
2603}
2604
2605struct ComboRows {
2610 entity_id: u64,
2611 multiple: bool,
2612 cursor_at: Option<usize>,
2613 colors: herogpui_theme::ThemeColors,
2614 row_hover_bg: Option<gpui::Hsla>,
2615 rows: Rc<[PickerItem]>,
2616 sections: Vec<(SharedString, SharedString)>,
2617 row_disabled_keys: std::collections::HashSet<SharedString>,
2618 row_read_only: bool,
2619 row_selected_keys: Vec<SharedString>,
2620 indicator: Option<Rc<dyn Fn(bool) -> gpui::AnyElement>>,
2621 on_change_all: Option<OnSelectionChangeAll>,
2622 row_input_change: Option<OnInputChange>,
2623 on_change_one: Option<OnSelectionChange>,
2624 row_state: Entity<InputState>,
2625 row_cursor: Entity<Option<ComboCursor>>,
2626 selection_own: Option<Entity<Vec<SharedString>>>,
2627 row_open_own: Option<Entity<bool>>,
2628 row_open_state: bool,
2629 row_close_open: ComboAction,
2630 row_disabled_opacity: f32,
2631 row_base: gpui::ElementId,
2634 row_virtualized: bool,
2635 panel_interactive: bool,
2636 row_count: usize,
2637 row_padding_x: Pixels,
2638 row_font_family: Option<SharedString>,
2639 row_padding_y: Pixels,
2640}
2641
2642impl ComboRows {
2643 fn row(&self, index: usize, fixed_h: Option<Pixels>, cx: &mut App) -> gpui::AnyElement {
2645 let Self {
2646 entity_id,
2647 multiple,
2648 cursor_at,
2649 ref colors,
2650 ref rows,
2651 ref sections,
2652 ref row_disabled_keys,
2653 row_read_only,
2654 ref row_selected_keys,
2655 ref indicator,
2656 row_disabled_opacity,
2657 ref row_base,
2658 row_virtualized,
2659 row_count,
2660 row_padding_x,
2661 ref row_font_family,
2662 row_padding_y,
2663 ..
2664 } = *self;
2665 let row_muted = colors.muted;
2666 let row_hover_bg = self.row_hover_bg.unwrap_or(colors.default.color);
2667 let row_focus = colors.focus;
2668 let row_accent = colors.accent.color;
2669 let item = &rows[index];
2670 let mut head: Vec<gpui::AnyElement> = Vec::new();
2673 let done = |head: Vec<gpui::AnyElement>, row: gpui::AnyElement| {
2674 div()
2675 .flex()
2676 .flex_col()
2677 .when_some(fixed_h, |el, h| el.h(h).w_full())
2678 .children(head)
2679 .child(row)
2680 .into_any_element()
2681 };
2682 if let Some((_, label)) = sections.iter().find(|(at, _)| at == item.key()) {
2684 head.push(
2685 div()
2686 .px(px(8.))
2687 .pt(px(6.))
2688 .pb(px(4.))
2689 .text_size(px(12.))
2690 .line_height(px(16.))
2691 .font_weight(gpui::FontWeight::MEDIUM)
2692 .text_color(row_muted)
2693 .child(label.to_string())
2694 .into_any_element(),
2695 );
2696 }
2697 let item_disabled = row_disabled_keys.contains(item.key());
2700 let hover_bg = row_hover_bg;
2701 let row_selector = format!("combobox-{entity_id}-item-{}", item.key());
2702 let row_selected = row_selected_keys.contains(item.key());
2703 let has_indicator_slot = indicator.is_some() || (multiple && row_selected);
2704 let mut row = div()
2705 .id(element_id::scoped(row_base, item.key().clone()))
2706 .a11y_named(
2713 a11y::Role::ListBoxOption,
2714 &a11y::Name::labelled(item.label().clone()),
2715 )
2716 .a11y_selected(row_selected_keys.contains(item.key()))
2717 .when(cursor_at == Some(index), |row| row.a11y_active_descendant())
2718 .when(row_virtualized, |row| {
2719 row.a11y_set_position(index, row_count)
2720 })
2721 .debug_selector(move || row_selector)
2722 .min_h(util::FIELD_HEIGHT)
2724 .px(row_padding_x)
2725 .py(row_padding_y)
2726 .gap(px(12.))
2727 .rounded(util::soft_radius(cx))
2728 .flex()
2729 .items_center()
2730 .justify_between()
2731 .text_size(util::FIELD_TEXT)
2732 .line_height(px(20.))
2733 .relative()
2738 .when(has_indicator_slot, |row| row.pr(px(28.)));
2739 if let Some(family) = row_font_family.clone() {
2740 row = row.font_family(family);
2741 }
2742
2743 if item_disabled {
2744 row = row.opacity(row_disabled_opacity);
2745 } else if !row_read_only {
2746 row = row
2747 .cursor(util::interactive_cursor(cx))
2748 .hover(move |s| s.bg(hover_bg));
2749 }
2750
2751 if util::shows_focus_ring(cursor_at == Some(index), cx) {
2753 row = row.border_2().border_color(row_focus);
2754 }
2755
2756 let label = div().flex_1().min_w_0().whitespace_normal();
2762 row = row.child(label.child(item.label().to_string()));
2763
2764 match &indicator {
2768 Some(render) => {
2769 row = row.child(
2770 div()
2771 .absolute()
2772 .top_0()
2773 .bottom_0()
2774 .right(px(8.))
2775 .w(px(16.))
2776 .flex()
2777 .items_center()
2778 .justify_center()
2779 .child(render(row_selected)),
2780 );
2781 }
2782 None if multiple && row_selected => {
2783 row = row.child(
2784 div()
2785 .absolute()
2786 .top_0()
2787 .bottom_0()
2788 .right(px(8.))
2789 .w(px(16.))
2790 .flex()
2791 .items_center()
2792 .justify_center()
2793 .child(
2794 gpui::svg()
2795 .size(px(13.))
2796 .path(icons::CHECK)
2797 .text_color(row_accent),
2798 ),
2799 );
2800 }
2801 None => {}
2802 }
2803
2804 if item_disabled || row_read_only {
2805 return done(head, row.into_any_element());
2806 }
2807
2808 if multiple {
2810 row = self.attach_toggle(row, index);
2811 return done(head, row.into_any_element());
2812 }
2813
2814 row = self.attach_pick(row, index);
2818
2819 done(head, row.into_any_element())
2820 }
2821
2822 fn attach_toggle(
2824 &self,
2825 mut row: gpui::Stateful<gpui::Div>,
2826 index: usize,
2827 ) -> gpui::Stateful<gpui::Div> {
2828 let rows = Rc::clone(&self.rows);
2829 let item = &rows[index];
2830 let Self {
2831 panel_interactive,
2832 ref row_selected_keys,
2833 ref on_change_all,
2834 ref row_input_change,
2835 ref row_state,
2836 ref row_cursor,
2837 ref selection_own,
2838 ..
2839 } = *self;
2840 if panel_interactive && (on_change_all.is_some() || selection_own.is_some()) {
2841 let cb = on_change_all.clone();
2842 let own = selection_own.clone();
2843 let current = row_selected_keys.clone();
2844 let value = item.key().clone();
2845 let state = row_state.clone();
2846 let cursor = row_cursor.clone();
2847 let next_cursor = cursor_for(&rows, index, Some(String::new()));
2848 let input_change = row_input_change.clone();
2849 row = row.on_click(move |_, window, cx| {
2850 let had_query = !state.read(cx).value().is_empty();
2851 let focus_handle = state.read(cx).focus_handle.clone();
2852 focus_handle.focus(window, cx);
2853 state.update(cx, |st, cx| {
2854 st.set_value(String::new());
2855 cx.notify();
2856 });
2857 cursor.update(cx, |v, _| *v = Some(next_cursor.clone()));
2858 let mut next = current.clone();
2859 toggle_key(&mut next, &value);
2860 if let Some(held) = &own {
2863 let set = next.clone();
2864 held.update(cx, |v, cx| {
2865 *v = set;
2866 cx.notify();
2867 });
2868 }
2869 if let Some(cb) = &cb {
2870 cb(&next, window, cx);
2871 }
2872 if had_query {
2873 if let Some(cb) = &input_change {
2874 cb("", window, cx);
2875 }
2876 }
2877 });
2878 }
2879 row
2880 }
2881
2882 fn attach_pick(
2884 &self,
2885 mut row: gpui::Stateful<gpui::Div>,
2886 index: usize,
2887 ) -> gpui::Stateful<gpui::Div> {
2888 let item = &self.rows[index];
2889 let Self {
2890 panel_interactive,
2891 ref row_selected_keys,
2892 ref on_change_all,
2893 ref on_change_one,
2894 ref row_state,
2895 ref selection_own,
2896 ref row_open_own,
2897 row_open_state,
2898 ref row_close_open,
2899 ..
2900 } = *self;
2901 let value = item.key().clone();
2902 let label = item.label().clone();
2903 let state = row_state.clone();
2904 let on_selection_change = on_change_one.clone();
2905 let on_selection_change_all = on_change_all.clone();
2906 let selection_changed = row_selected_keys.first() != Some(&value);
2907 let own = selection_own.clone();
2908 let open_own = row_open_own.clone();
2909 let close_open = row_close_open.clone();
2910 if panel_interactive {
2911 row = row.on_click(move |_, window, cx| {
2912 let is_open = open_own
2913 .as_ref()
2914 .map_or(row_open_state, |held| *held.read(cx));
2915 if !is_open {
2916 return;
2917 }
2918 close_open(window, cx);
2921 state.update(cx, |s, cx| {
2922 s.set_value(label.to_string());
2923 cx.notify();
2924 });
2925 if let Some(held) = &own {
2928 let next = vec![value.clone()];
2929 held.update(cx, |v, cx| {
2930 *v = next;
2931 cx.notify();
2932 });
2933 }
2934 if selection_changed {
2935 if let Some(cb) = &on_selection_change_all {
2936 cb(std::slice::from_ref(&value), window, cx);
2937 }
2938 }
2939 if let Some(cb) = &on_selection_change {
2940 cb(&value, window, cx);
2941 }
2942 });
2943 }
2944 row
2945 }
2946}
2947
2948#[cfg(test)]
2952mod hover_tokens {
2953 #[test]
2954 fn the_trigger_hover_recolours_the_text_and_fills_nothing() {
2955 let source = include_str!("combo_box.rs")
2958 .split("#[cfg(test)]")
2959 .next()
2960 .expect("the implementation section is always present");
2961 assert!(
2962 source.contains(".hover(move |s| s.text_color(trigger_hover_fg))"),
2963 "the trigger hover must recolour the text to `text-field-foreground` \
2964 (pinned `.combo-box__trigger:hover`)"
2965 );
2966 assert!(
2967 !source.contains("let hover_bg = colors.default.hover();"),
2968 "the trigger hover must not fill a background \
2969 (pinned `.combo-box__trigger` is `bg-transparent`)"
2970 );
2971 }
2972}
2973
2974crate::util::impl_component_styled!(ComboBox);