1use std::{
18 cell::{Cell, RefCell},
19 collections::HashSet,
20 rc::Rc,
21};
22
23use gpui::{
24 prelude::*, px, AnimationExt, App, IntoElement, ParentElement, Pixels, RenderOnce,
25 SharedString, StatefulInteractiveElement, Styled, Window,
26};
27use herogpui_core::{element_id, Color, FieldVariant, Placement, SelectionMode};
28use herogpui_theme::ActiveTheme;
29
30use crate::{
31 a11y::{self, A11y as _},
32 icons,
33 picker_item::PickerItem,
34 selection::toggle_key,
35 util,
36};
37
38type OnSelectionChange =
39 std::sync::Arc<dyn Fn(&Option<SharedString>, &mut Window, &mut App) + 'static>;
40
41fn format_selected_names(names: &[String]) -> String {
43 match names {
44 [] => String::new(),
45 [name] => name.clone(),
46 [first, second] => format!("{first} and {second}"),
47 _ => format!(
48 "{}, and {}",
49 names[..names.len() - 1].join(", "),
50 names.last().expect("the multiple-item branch is non-empty")
51 ),
52 }
53}
54
55fn in_collection_order(selected: &[SharedString], keys: &[SharedString]) -> Vec<SharedString> {
60 keys.iter()
61 .filter(|key| selected.contains(*key))
62 .cloned()
63 .collect()
64}
65
66fn resolved_keys(
71 items: &[PickerItem],
72 single: &Option<SharedString>,
73 multiple: &[SharedString],
74) -> Vec<SharedString> {
75 items
76 .iter()
77 .map(|item| item.key())
78 .filter(|key| single.as_ref() == Some(*key) || multiple.contains(*key))
79 .cloned()
80 .collect()
81}
82
83#[derive(Clone, Debug, Default)]
90struct SelectSelectionRange {
91 anchor: Option<SharedString>,
92 current: Option<SharedString>,
93 is_all: bool,
94}
95
96fn extend_selection_range(
105 current: &[SharedString],
106 collection: &[SharedString],
107 selectable: &[SharedString],
108 range: &SelectSelectionRange,
109 target: &SharedString,
110) -> Vec<SharedString> {
111 if range.is_all {
112 return vec![target.clone()];
113 }
114 let anchor = range.anchor.as_ref().unwrap_or(target);
115 let previous = range.current.as_ref().unwrap_or(target);
116 let anchor_at = collection.iter().position(|key| key == anchor);
117 let previous_at = collection.iter().position(|key| key == previous);
118 let target_at = collection.iter().position(|key| key == target);
119 let between = |from: Option<usize>, to: Option<usize>| {
120 from.zip(to)
121 .map(|(from, to)| if from <= to { from..=to } else { to..=from })
122 };
123 let mut next: Vec<SharedString> = current.to_vec();
124 if let Some(previous_range) = between(anchor_at, previous_at) {
125 for at in previous_range {
126 let stale = &collection[at];
127 if let Some(held) = next.iter().position(|key| key == stale) {
128 next.remove(held);
129 }
130 }
131 }
132 if let Some(target_range) = between(anchor_at, target_at) {
133 for at in target_range {
134 let key = &collection[at];
135 if selectable.contains(key) && !next.contains(key) {
136 next.push(key.clone());
137 }
138 }
139 }
140 in_collection_order(&next, collection)
141}
142
143fn home_end_registered(modifiers: gpui::Modifiers, macos: bool) -> bool {
156 if macos {
157 !modifiers.control && !modifiers.platform
158 } else {
159 !modifiers.alt && !modifiers.platform
160 }
161}
162
163fn shift_home_end_extends(key_name: &str, control: bool, macos: bool) -> bool {
169 !matches!(key_name, "home" | "end") || (!macos && control)
170}
171
172#[derive(Default)]
177pub struct SelectClearButton {
178 children: Vec<gpui::AnyElement>,
179 on_click: Option<std::sync::Arc<dyn Fn(&gpui::ClickEvent, &mut Window, &mut App)>>,
180}
181
182impl ParentElement for SelectClearButton {
183 fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
184 self.children.extend(elements);
185 }
186}
187
188impl SelectClearButton {
189 pub fn new() -> Self {
191 Self::default()
192 }
193
194 pub fn child(mut self, child: impl IntoElement) -> Self {
196 self.children.push(child.into_any_element());
197 self
198 }
199
200 pub fn on_click(
202 mut self,
203 callback: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
204 ) -> Self {
205 self.on_click = Some(std::sync::Arc::new(callback));
206 self
207 }
208}
209
210#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
212#[derive(IntoElement)]
213pub struct Select {
214 name: Option<SharedString>,
217 id: gpui::ElementId,
218 items: Vec<PickerItem>,
219 selected: Option<SharedString>,
220 is_controlled: bool,
223 default_value: Option<SharedString>,
224 selected_keys: Vec<SharedString>,
226 is_multiple_controlled: bool,
227 default_selected_keys: Vec<SharedString>,
228 selection_mode: SelectionMode,
229 is_open: Option<bool>,
232 default_open: bool,
233 placement: Placement,
234 label: Option<SharedString>,
235 placeholder: SharedString,
236 placeholder_is_set: bool,
239 description: Option<SharedString>,
240 variant: FieldVariant,
241 variant_is_set: bool,
242 is_disabled: bool,
243 is_invalid: bool,
244 should_focus_wrap: bool,
246 row_height: Option<Pixels>,
248 row_padding_x: Option<Pixels>,
250 trigger_text_size: Option<Pixels>,
251 row_text_size: Option<Pixels>,
252 panel_padding: Option<Pixels>,
253 row_padding_y: Option<Pixels>,
255 row_hover_bg: Option<gpui::Hsla>,
257 trigger_hover_bg: Option<gpui::Hsla>,
259 row_font_family: Option<SharedString>,
262 font_family: Option<SharedString>,
264 radius: Option<Pixels>,
267 sections: Vec<(SharedString, SharedString)>,
269 indicator: Option<Box<dyn Fn(bool) -> gpui::AnyElement + 'static>>,
272 #[allow(clippy::type_complexity)]
273 item_leading: Option<Box<dyn Fn(&SharedString, bool) -> Option<gpui::AnyElement> + 'static>>,
274 trigger_indicator: Option<Box<dyn Fn(bool) -> gpui::AnyElement + 'static>>,
278 value_content: Option<Box<dyn Fn(util::SelectionValue<'_>) -> gpui::AnyElement + 'static>>,
281 is_required: bool,
282 disabled_keys: HashSet<SharedString>,
283 full_width: bool,
284 on_open_change: Option<std::sync::Arc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
285 on_selection_change: Option<OnSelectionChange>,
286 clear_buttons: Vec<SelectClearButton>,
287 on_clear: Option<std::sync::Arc<dyn Fn(&mut Window, &mut App)>>,
288 on_selection_change_all:
289 Option<std::sync::Arc<dyn Fn(&[SharedString], &mut Window, &mut App) + 'static>>,
290 field: util::FieldBox,
292 form_state: Rc<RefCell<crate::form::LiveFormFieldState>>,
295 sx: Option<Box<gpui::StyleRefinement>>,
297 recipes: Vec<SharedString>,
298}
299
300impl Select {
301 pub fn clear_button(mut self, button: SelectClearButton) -> Self {
304 self.clear_buttons.push(button);
305 self
306 }
307
308 pub fn on_clear(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self {
310 self.on_clear = Some(std::sync::Arc::new(callback));
311 self
312 }
313
314 pub fn on_change(
316 self,
317 handler: impl Fn(&Option<SharedString>, &mut Window, &mut App) + 'static,
318 ) -> Self {
319 self.on_selection_change(handler)
320 }
321
322 pub fn disabled_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
326 self.disabled_keys = keys.into_iter().collect();
327 self
328 }
329
330 pub fn row_height(mut self, h: impl Into<Pixels>) -> Self {
337 self.row_height = Some(h.into());
338 self
339 }
340
341 pub fn should_focus_wrap(mut self, v: bool) -> Self {
343 self.should_focus_wrap = v;
344 self
345 }
346
347 pub fn section_before(
349 mut self,
350 item: impl Into<SharedString>,
351 label: impl Into<SharedString>,
352 ) -> Self {
353 self.sections.push((item.into(), label.into()));
354 self
355 }
356
357 pub fn indicator(mut self, render: impl Fn(bool) -> gpui::AnyElement + 'static) -> Self {
359 self.indicator = Some(Box::new(render));
360 self
361 }
362
363 pub fn item_leading(
381 mut self,
382 render: impl Fn(&SharedString, bool) -> Option<gpui::AnyElement> + 'static,
383 ) -> Self {
384 self.item_leading = Some(Box::new(render));
385 self
386 }
387
388 pub fn trigger_indicator(
395 mut self,
396 render: impl Fn(bool) -> gpui::AnyElement + 'static,
397 ) -> Self {
398 self.trigger_indicator = Some(Box::new(render));
399 self
400 }
401
402 pub fn value_content(
409 mut self,
410 render: impl Fn(util::SelectionValue<'_>) -> gpui::AnyElement + 'static,
411 ) -> Self {
412 self.value_content = Some(Box::new(render));
413 self
414 }
415
416 pub fn is_invalid(mut self, v: bool) -> Self {
418 self.is_invalid = v;
419 self
420 }
421
422 pub fn is_required(mut self, v: bool) -> Self {
424 self.is_required = v;
425 self
426 }
427
428 pub fn full_width(mut self, v: bool) -> Self {
430 self.full_width = v;
431 self
432 }
433
434 pub fn height(mut self, h: impl Into<Pixels>) -> Self {
437 self.field.height = Some(h.into());
438 self
439 }
440
441 pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
443 self.field.padding_x = Some(p.into());
444 self
445 }
446
447 pub fn padding_y(mut self, p: impl Into<Pixels>) -> Self {
460 self.field.padding_y = Some(p.into());
461 self
462 }
463
464 pub fn is_bare(mut self, v: bool) -> Self {
468 self.field.is_bare = v;
469 self.field.is_bare_is_set = true;
470 self
471 }
472
473 pub fn focus_ring(mut self, v: bool) -> Self {
476 self.field.focus_ring = Some(v);
477 self
478 }
479
480 pub fn trigger_text_size(mut self, value: impl Into<Pixels>) -> Self {
482 self.trigger_text_size = Some(value.into());
483 self
484 }
485
486 pub fn row_text_size(mut self, value: impl Into<Pixels>) -> Self {
488 self.row_text_size = Some(value.into());
489 self
490 }
491
492 pub fn panel_padding(mut self, value: impl Into<Pixels>) -> Self {
494 self.panel_padding = Some(value.into());
495 self
496 }
497
498 pub fn row_padding_x(mut self, p: impl Into<Pixels>) -> Self {
500 self.row_padding_x = Some(p.into());
501 self
502 }
503
504 pub fn row_padding_y(mut self, p: impl Into<Pixels>) -> Self {
506 self.row_padding_y = Some(p.into());
507 self
508 }
509
510 pub fn trigger_hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
517 self.trigger_hover_bg = Some(color.into());
518 self
519 }
520
521 pub fn row_hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
524 self.row_hover_bg = Some(color.into());
525 self
526 }
527
528 pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
533 self.font_family = Some(family.into());
534 self
535 }
536
537 pub fn row_font_family(mut self, family: impl Into<SharedString>) -> Self {
540 self.row_font_family = Some(family.into());
541 self
542 }
543
544 pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
553 self.radius = Some(radius.into());
554 self
555 }
556
557 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
563 util::refine_sx(&mut self.sx, style);
564 self
565 }
566
567 pub fn new(id: impl Into<gpui::ElementId>, items: Vec<PickerItem>) -> Self {
569 Self {
570 name: None,
571 id: id.into(),
572 items,
573 selected: None,
574 is_controlled: false,
575 default_value: None,
576 selected_keys: Vec::new(),
577 is_multiple_controlled: false,
578 default_selected_keys: Vec::new(),
579 selection_mode: SelectionMode::Single,
580 is_open: None,
581 default_open: false,
582 placement: Placement::BottomStart,
583 label: None,
584 placeholder: "Select an item".into(),
585 placeholder_is_set: false,
586 description: None,
587 variant: FieldVariant::Primary,
588 variant_is_set: false,
589 is_disabled: false,
590 is_invalid: false,
591 should_focus_wrap: false,
592 row_height: None,
593 row_padding_x: None,
594 trigger_text_size: None,
595 row_text_size: None,
596 panel_padding: None,
597 row_padding_y: None,
598 row_hover_bg: None,
599 trigger_hover_bg: None,
600 row_font_family: None,
601 font_family: None,
602 item_leading: None,
603 radius: None,
604 field: util::FieldBox::default(),
605 sections: Vec::new(),
606 indicator: None,
607 trigger_indicator: None,
608 value_content: None,
609 is_required: false,
610 disabled_keys: HashSet::new(),
611 full_width: false,
612 on_open_change: None,
613 on_selection_change: None,
614 clear_buttons: Vec::new(),
615 on_clear: None,
616 on_selection_change_all: None,
617 form_state: live_form_state(),
618 sx: None,
619 recipes: Vec::new(),
620 }
621 }
622
623 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
625 self.name = Some(name.into());
626 self
627 }
628
629 pub fn form_field(&self) -> Option<crate::form::FormField> {
653 let name = self.name.clone()?;
654 let selected = if self.is_controlled {
655 self.selected.clone()
656 } else {
657 self.default_value.clone()
658 };
659 let selected_keys = if self.is_multiple_controlled {
660 self.selected_keys.clone()
661 } else {
662 self.default_selected_keys.clone()
663 };
664 sync_select_form(
665 &self.form_state,
666 select_form_value(&self.items, &selected, &selected_keys),
667 self.is_invalid,
668 !self.is_disabled,
669 );
670 Some(
671 crate::form::FormField::live(name, self.form_state.clone())
672 .is_required(self.is_required),
673 )
674 }
675
676 pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
678 self.selection_mode = mode;
679 self
680 }
681
682 pub fn selected_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
685 self.selected_keys = keys.into_iter().collect();
686 self.is_multiple_controlled = true;
687 self
688 }
689
690 pub fn default_selected_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
693 self.default_selected_keys = keys.into_iter().collect();
694 self
695 }
696
697 pub fn on_selection_change_all(
699 mut self,
700 handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
701 ) -> Self {
702 self.on_selection_change_all = Some(std::sync::Arc::new(handler));
703 self
704 }
705
706 pub fn value(mut self, key: Option<SharedString>) -> Self {
709 self.selected = key;
710 self.is_controlled = true;
711 self
712 }
713
714 pub fn default_value(mut self, key: Option<SharedString>) -> Self {
719 self.default_value = key;
720 self
721 }
722
723 pub fn placement(mut self, placement: Placement) -> Self {
725 self.placement = placement;
726 self
727 }
728
729 pub fn is_open(mut self, v: bool) -> Self {
731 self.is_open = Some(v);
732 self
733 }
734 pub fn default_open(mut self, v: bool) -> Self {
739 self.default_open = v;
740 self
741 }
742
743 pub fn label(mut self, l: impl Into<SharedString>) -> Self {
745 self.label = Some(l.into());
746 self
747 }
748
749 pub fn placeholder(mut self, p: impl Into<SharedString>) -> Self {
751 self.placeholder = p.into();
752 self.placeholder_is_set = true;
753 self
754 }
755
756 pub fn description(mut self, d: impl Into<SharedString>) -> Self {
758 self.description = Some(d.into());
759 self
760 }
761
762 pub fn variant(mut self, v: FieldVariant) -> Self {
764 self.variant = v;
765 self.variant_is_set = true;
766 self
767 }
768
769 pub fn recipe(mut self, name: impl Into<SharedString>) -> Self {
772 self.recipes.push(name.into());
773 self
774 }
775
776 pub fn is_disabled(mut self, v: bool) -> Self {
778 self.is_disabled = v;
779 self
780 }
781
782 pub fn on_open_change(mut self, f: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
784 self.on_open_change = Some(std::sync::Arc::new(f));
785 self
786 }
787
788 pub fn on_selection_change(
790 mut self,
791 f: impl Fn(&Option<SharedString>, &mut Window, &mut App) + 'static,
792 ) -> Self {
793 self.on_selection_change = Some(std::sync::Arc::new(f));
794 self
795 }
796}
797
798impl Select {
799 fn value_text_single(&self, selected: &Option<SharedString>) -> SharedString {
800 selected
801 .as_ref()
802 .and_then(|key| self.items.iter().find(|item| item.key() == key))
803 .map_or_else(|| self.placeholder.clone(), |item| item.label().clone())
804 }
805
806 fn value_text_multiple(&self, selected_keys: &[SharedString]) -> SharedString {
807 let names: Vec<String> = self
808 .items
809 .iter()
810 .filter(|item| selected_keys.contains(item.key()))
811 .map(|item| item.label().to_string())
812 .collect();
813 if names.is_empty() {
814 self.placeholder.clone()
815 } else {
816 SharedString::from(format_selected_names(&names))
817 }
818 }
819}
820
821struct SelectControlled {
825 is_open: bool,
826 open_own: Option<gpui::Entity<bool>>,
827 overlay_phase: util::OverlayPhase,
828 dismissal_token: util::OverlayToken,
829 multiple: bool,
830 selected: Option<SharedString>,
831 value_own: Option<gpui::Entity<Option<SharedString>>>,
832 selected_keys: Vec<SharedString>,
833 indices_own: Option<gpui::Entity<Vec<SharedString>>>,
834}
835
836struct SelectFrame {
842 is_open: bool,
843 open_own: Option<gpui::Entity<bool>>,
844 overlay_phase: util::OverlayPhase,
845 dismissal_token: util::OverlayToken,
846 resolved_placement: Rc<Cell<Option<Placement>>>,
847 entry_placement: Placement,
848 multiple: bool,
849 selected: Option<SharedString>,
850 value_own: Option<gpui::Entity<Option<SharedString>>>,
851 selected_keys: Vec<SharedString>,
852 indices_own: Option<gpui::Entity<Vec<SharedString>>>,
853 keys: Vec<SharedString>,
854 focus_handle: gpui::FocusHandle,
855 cursor: gpui::Entity<Option<SharedString>>,
856 cursor_at: Option<usize>,
857 keyboard_press_open: gpui::Entity<Option<bool>>,
858 selection_range: gpui::Entity<SelectSelectionRange>,
859 list_scroll_now: gpui::UniformListScrollHandle,
860 panel_scroll_now: gpui::ScrollHandle,
861 typeahead: gpui::Entity<crate::list_nav::Typeahead>,
862 blur_scope: gpui::FocusHandle,
863 clear_empty: bool,
864 clear_enabled: bool,
865 clear_slots: Vec<util::Interaction>,
866 clear_hovered: bool,
867 clear_selection: SelectAction,
868 anchor_bounds: Rc<Cell<Option<gpui::Bounds<Pixels>>>>,
869 trigger_pressed: Rc<Cell<bool>>,
870}
871
872type SelectAction = std::sync::Arc<dyn Fn(&mut Window, &mut App)>;
873type OnOpenChange = std::sync::Arc<dyn Fn(&bool, &mut Window, &mut App) + 'static>;
874type OnSelectionChangeAll =
875 std::sync::Arc<dyn Fn(&[SharedString], &mut Window, &mut App) + 'static>;
876
877impl RenderOnce for Select {
878 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
879 if !self.placeholder_is_set {
880 self.placeholder = crate::i18n::ui_string(crate::i18n::UiString::SelectPlaceholder, cx);
881 }
882 let (is_open, open_own) = util::controlled(
884 window,
885 cx,
886 element_id::scoped(&self.id, "open"),
887 self.is_open,
888 self.default_open,
889 );
890 let (overlay_phase, dismissal_token) = util::overlay_scope(
891 window,
892 cx,
893 element_id::scoped(&self.id, "overlay"),
894 is_open,
895 true,
896 );
897 let overlay_active = overlay_phase != util::OverlayPhase::Closed;
898 let multiple = self.selection_mode == SelectionMode::Multiple;
899 let (selected, value_own) = if multiple {
900 (None, None)
901 } else {
902 util::controlled(
903 window,
904 cx,
905 element_id::scoped(&self.id, "value"),
906 self.is_controlled.then_some(self.selected.clone()),
907 self.default_value.clone(),
908 )
909 };
910 let (selected_keys, indices_own) = if multiple {
911 util::controlled(
912 window,
913 cx,
914 element_id::scoped(&self.id, "values"),
915 self.is_multiple_controlled.then(|| {
916 crate::selection::normalize_selection(self.selected_keys.clone(), true)
917 }),
918 crate::selection::normalize_selection(self.default_selected_keys.clone(), true),
919 )
920 } else {
921 (Vec::new(), None)
922 };
923 let frame = self.frame(
924 SelectControlled {
925 is_open,
926 open_own,
927 overlay_phase,
928 dismissal_token,
929 multiple,
930 selected,
931 value_own,
932 selected_keys,
933 indices_own,
934 },
935 window,
936 cx,
937 );
938 let colors = cx.colors().clone();
939 let layout = cx.layout().clone();
940 self.apply_theme(&colors, cx);
941 let mut field = self.trigger_field(&frame, &colors, &layout, window, cx);
942
943 if !self.is_disabled {
946 field = self.trigger_keys(field, &frame);
947 }
948
949 let (value_text, value_slot) = self.trigger_value(&frame, &colors);
950 field = field
961 .a11y_named(
962 a11y::Role::Button,
963 &a11y::Name::maybe(self.label.clone()).described(Some(value_text)),
964 )
965 .a11y_expanded(frame.is_open);
966 field = field.child(value_slot);
967 field = self.clear_buttons(field, &frame, &colors, window, cx);
968 field = self.trigger_indicator_slot(field, &frame, &colors, window, cx);
969 field = self.trigger_toggle(field, &frame);
970 if !self.is_disabled {
971 field = util::record_focus_bounds(field, &frame.focus_handle, window, cx);
972 }
973
974 let field = crate::popover::PopoverTriggerMeasure::new(field, frame.anchor_bounds.clone());
980
981 let mut root = self.field_root(field);
982 root = self.root_dismissals(root, &frame);
983 let blur_scope = frame.blur_scope.clone();
984 if overlay_active && !self.items.is_empty() {
985 root = root.child(self.popover(frame, &colors, &layout, window, cx));
986 }
987
988 root = util::apply_sx(root, &self.sx);
989 root.track_focus(&blur_scope)
990 }
991}
992
993impl Select {
994 fn sync_form(&self, controlled: &SelectControlled, window: &mut Window, cx: &mut App) {
997 let SelectControlled {
998 multiple,
999 ref selected,
1000 ref selected_keys,
1001 ref value_own,
1002 ref indices_own,
1003 ..
1004 } = *controlled;
1005 let form_default_keys = if multiple {
1006 let reset_keys = if self.is_multiple_controlled {
1007 self.selected_keys.clone()
1008 } else {
1009 self.default_selected_keys.clone()
1010 };
1011 let slot = window.use_keyed_state(
1012 element_id::scoped(&self.id, "form-default"),
1013 cx,
1014 move |_, _| reset_keys,
1015 );
1016 slot.read(cx).clone()
1017 } else {
1018 Vec::new()
1019 };
1020 sync_select_form(
1021 &self.form_state,
1022 select_form_value(&self.items, selected, selected_keys),
1023 self.is_invalid,
1024 !self.is_disabled,
1025 );
1026 let reset_own = value_own.clone();
1027 let reset_keys_own = indices_own.clone();
1028 let reset_state = Rc::downgrade(&self.form_state);
1029 let reset_change = self
1030 .is_controlled
1031 .then(|| self.on_selection_change.clone())
1032 .flatten();
1033 let reset_change_all = self
1034 .is_multiple_controlled
1035 .then(|| self.on_selection_change_all.clone())
1036 .flatten();
1037 let reset_key = self.default_value.clone();
1038 let reset_items = self.items.clone();
1039 let reset_mode = self.selection_mode;
1040 self.form_state.borrow_mut().restore = (reset_own.is_some()
1041 || (multiple && reset_keys_own.is_some())
1042 || reset_change.is_some()
1043 || (multiple && reset_change_all.is_some()))
1044 .then(|| {
1045 util::shared(move |window: &mut Window, cx: &mut App| {
1046 if reset_mode == SelectionMode::Multiple {
1047 if let Some(state) = reset_state.upgrade() {
1048 state.borrow_mut().value =
1049 select_form_value(&reset_items, &None, &form_default_keys);
1050 }
1051 if let Some(held) = &reset_keys_own {
1052 held.update(cx, |selected, cx| {
1053 *selected = form_default_keys.clone();
1054 cx.notify();
1055 });
1056 }
1057 if let Some(on_change) = &reset_change_all {
1058 on_change(&form_default_keys, window, cx);
1059 }
1060 } else {
1061 if let Some(state) = reset_state.upgrade() {
1062 state.borrow_mut().value = select_form_value(&reset_items, &reset_key, &[]);
1063 }
1064 if let Some(held) = &reset_own {
1065 held.update(cx, |selected, cx| {
1066 *selected = reset_key.clone();
1067 cx.notify();
1068 });
1069 }
1070 if let Some(on_change) = &reset_change {
1071 on_change(&reset_key, window, cx);
1072 }
1073 }
1074 }) as std::sync::Arc<dyn Fn(&mut Window, &mut App)>
1075 });
1076 }
1077
1078 fn frame(
1081 &self,
1082 controlled: SelectControlled,
1083 window: &mut Window,
1084 cx: &mut App,
1085 ) -> SelectFrame {
1086 self.sync_form(&controlled, window, cx);
1087 let SelectControlled {
1088 is_open,
1089 open_own,
1090 overlay_phase,
1091 dismissal_token,
1092 multiple,
1093 selected,
1094 value_own,
1095 selected_keys,
1096 indices_own,
1097 } = controlled;
1098 let (resolved_placement, entry_placement) =
1102 crate::popover::field_placement_feedback(window, cx, &self.id, self.placement);
1103
1104 let keys: Vec<SharedString> = self.items.iter().map(|item| item.key().clone()).collect();
1107
1108 let focus_handle =
1111 window.use_keyed_state(element_id::scoped(&self.id, "focus"), cx, |_, cx| {
1112 cx.focus_handle().tab_stop(true)
1113 });
1114 let focus_handle = focus_handle.read(cx).clone();
1115 self.form_state.borrow_mut().focus = Some(focus_handle.clone());
1116 let cursor = window.use_keyed_state(element_id::scoped(&self.id, "cursor"), cx, |_, _| {
1119 None::<SharedString>
1120 });
1121 let cursor_key = cursor.read(cx).clone();
1122 let cursor_at = cursor_key
1123 .as_ref()
1124 .and_then(|key| keys.iter().position(|k| k == key));
1125 let keyboard_press_open = window.use_keyed_state(
1126 element_id::scoped(&self.id, "keyboard-press"),
1127 cx,
1128 |_, _| None::<bool>,
1129 );
1130 let selection_range =
1134 window.use_keyed_state(element_id::scoped(&self.id, "range"), cx, |_, _| {
1135 SelectSelectionRange::default()
1136 });
1137 let list_scroll =
1142 window.use_keyed_state(element_id::scoped(&self.id, "list-scroll"), cx, |_, _| {
1143 gpui::UniformListScrollHandle::new()
1144 });
1145 let panel_scroll =
1146 window.use_keyed_state(element_id::scoped(&self.id, "panel-scroll"), cx, |_, _| {
1147 gpui::ScrollHandle::new()
1148 });
1149 let list_scroll_now = list_scroll.read(cx).clone();
1150 let panel_scroll_now = panel_scroll.read(cx).clone();
1151 let typeahead =
1154 window.use_keyed_state(element_id::scoped(&self.id, "typed"), cx, |_, _| {
1155 crate::list_nav::Typeahead::default()
1156 });
1157
1158 let blur_base = element_id::scoped(&self.id, "select");
1161 let blur_close_own = open_own.clone();
1162 let blur_open_change = self.on_open_change.clone();
1163 let blur_scope = util::close_on_blur(window, cx, &blur_base, is_open, move |window, cx| {
1164 if let Some(held) = &blur_close_own {
1165 held.update(cx, |v, cx| {
1166 *v = false;
1167 cx.notify();
1168 });
1169 }
1170 if let Some(cb) = &blur_open_change {
1171 cb(&false, window, cx);
1172 }
1173 });
1174
1175 let clear_empty = if multiple {
1178 selected_keys.is_empty()
1179 } else {
1180 selected.is_none()
1181 };
1182 let has_clear = !self.clear_buttons.is_empty();
1183 let clear_enabled = has_clear && !self.is_disabled && !clear_empty;
1184 let clear_slots: Vec<_> = (0..self.clear_buttons.len())
1185 .map(|index| {
1186 util::interaction(
1187 element_id::scoped(&self.id, format!("clear-{index}")),
1188 window,
1189 cx,
1190 )
1191 })
1192 .collect();
1193 let clear_hovered = clear_enabled && clear_slots.iter().any(|slot| slot.read(cx).0);
1194 let clear_own = value_own.clone();
1195 let clear_keys_own = indices_own.clone();
1196 let clear_form = self.form_state.clone();
1197 let clear_single = self.on_selection_change.clone();
1198 let clear_multiple = self.on_selection_change_all.clone();
1199 let clear_callback = self.on_clear.clone();
1200 let clear_range = selection_range.clone();
1201 let clear_open = open_own.clone();
1202 let clear_open_callback = self.on_open_change.clone();
1203 let clear_selection = util::shared(move |window: &mut Window, cx: &mut App| {
1204 if !clear_enabled {
1205 return;
1206 }
1207 if multiple {
1208 if let Some(own) = &clear_keys_own {
1209 clear_form.borrow_mut().value = crate::form::FormValue::Keys(Vec::new());
1210 own.update(cx, |value, cx| {
1211 value.clear();
1212 cx.notify();
1213 });
1214 }
1215 if let Some(callback) = &clear_multiple {
1216 callback(&[], window, cx);
1217 }
1218 } else {
1219 if let Some(own) = &clear_own {
1220 clear_form.borrow_mut().value = crate::form::FormValue::Keys(Vec::new());
1221 own.update(cx, |value, cx| {
1222 *value = None;
1223 cx.notify();
1224 });
1225 }
1226 if let Some(callback) = &clear_single {
1227 callback(&None, window, cx);
1228 }
1229 if is_open {
1231 if let Some(own) = &clear_open {
1232 own.update(cx, |value, cx| {
1233 *value = false;
1234 cx.notify();
1235 });
1236 }
1237 if let Some(callback) = &clear_open_callback {
1238 callback(&false, window, cx);
1239 }
1240 }
1241 }
1242 clear_range.update(cx, |range, _| *range = SelectSelectionRange::default());
1243 if let Some(callback) = &clear_callback {
1244 callback(window, cx);
1245 }
1246 });
1247
1248 let anchor_bounds: Rc<Cell<Option<gpui::Bounds<Pixels>>>> = window
1249 .use_keyed_state(element_id::scoped(&self.id, "anchor-bounds"), cx, |_, _| {
1250 Rc::new(Cell::new(None))
1251 })
1252 .read(cx)
1253 .clone();
1254 let trigger_pressed = Rc::new(Cell::new(false));
1263 SelectFrame {
1264 is_open,
1265 open_own,
1266 overlay_phase,
1267 dismissal_token,
1268 resolved_placement,
1269 entry_placement,
1270 multiple,
1271 selected,
1272 value_own,
1273 selected_keys,
1274 indices_own,
1275 keys,
1276 focus_handle,
1277 cursor,
1278 cursor_at,
1279 keyboard_press_open,
1280 selection_range,
1281 list_scroll_now,
1282 panel_scroll_now,
1283 typeahead,
1284 blur_scope,
1285 clear_empty,
1286 clear_enabled,
1287 clear_slots,
1288 clear_hovered,
1289 clear_selection,
1290 anchor_bounds,
1291 trigger_pressed,
1292 }
1293 }
1294
1295 fn apply_theme(&mut self, colors: &herogpui_theme::ThemeColors, cx: &App) {
1297 let select_theme = cx.theme().components.select.resolve(&self.recipes);
1298 if !self.variant_is_set {
1299 if let Some(variant) = select_theme.variant {
1300 self.variant = variant;
1301 }
1302 }
1303 self.field.height = self.field.height.or(select_theme.height);
1304 self.field.padding_x = self.field.padding_x.or(select_theme.padding_x);
1305 self.field.padding_y = self.field.padding_y.or(select_theme.padding_y);
1306 if !self.field.is_bare_is_set {
1307 if let Some(is_bare) = select_theme.is_bare {
1308 self.field.is_bare = is_bare;
1309 }
1310 }
1311 self.trigger_text_size = self.trigger_text_size.or(select_theme.trigger_text_size);
1312 self.row_height = self.row_height.or(select_theme.row_height);
1313 self.row_padding_x = self.row_padding_x.or(select_theme.row_padding_x);
1314 self.row_padding_y = self.row_padding_y.or(select_theme.row_padding_y);
1315 self.row_text_size = self.row_text_size.or(select_theme.row_text_size);
1316 self.panel_padding = self.panel_padding.or(select_theme.panel_padding);
1317 self.radius = self.radius.or(select_theme.radius);
1318 if self.row_hover_bg.is_none() {
1319 self.row_hover_bg = select_theme.row_hover_bg.map(|color| color.resolve(colors));
1320 }
1321 }
1322
1323 fn trigger_field(
1325 &self,
1326 frame: &SelectFrame,
1327 colors: &herogpui_theme::ThemeColors,
1328 layout: &herogpui_theme::LayoutTheme,
1329 window: &mut Window,
1330 cx: &mut App,
1331 ) -> gpui::Stateful<gpui::Div> {
1332 let SelectFrame {
1333 is_open,
1334 ref focus_handle,
1335 clear_hovered,
1336 ..
1337 } = *frame;
1338 let sem = *cx.role(Color::Accent);
1339 let field_box = self.field;
1341 let (h, text) = (
1342 field_box.resolved_height(),
1343 self.trigger_text_size.unwrap_or(util::FIELD_TEXT),
1344 );
1345
1346 let trigger_id = element_id::scoped(&self.id, "trigger");
1347 let trigger_selector = format!("select-trigger-{}", id_debug(&self.id));
1348 let trigger_radius = util::field_radius(cx);
1349 let trigger_focused = focus_handle.is_focused(window);
1350 let mut field = gpui::div()
1351 .id(trigger_id)
1352 .debug_selector(move || trigger_selector)
1353 .flex()
1354 .items_center()
1355 .justify_between()
1356 .gap(px(8.))
1357 .when_some(self.font_family.clone(), |field, family| field.font_family(family))
1358 .min_h(h)
1359 .when_some(field_box.height, |el, h| el.h(h))
1360 .px(field_box.resolved_padding_x())
1361 .when_some(field_box.padding_y, |el, p| el.py(p))
1362 .relative()
1367 .pr(px(28.))
1368 .text_size(text)
1369 .line_height(px(20.))
1370 .cursor(util::interactive_cursor(cx));
1371
1372 let _border_color = if is_open { sem.color } else { colors.separator };
1373 if !field_box.is_bare {
1377 field = util::apply_field_chrome(field, self.variant, self.is_invalid, false, None, cx);
1378 }
1379 if !field_box.is_bare && field_box.focus_ring.unwrap_or(true) && !self.is_disabled {
1380 field = util::ring_overlay_if_focused(
1384 field,
1385 focus_handle,
1386 true,
1387 trigger_radius,
1388 Vec::new(),
1389 window,
1390 cx,
1391 );
1392 }
1393
1394 if !field_box.is_bare && (self.is_invalid || (trigger_focused && util::focus_visible(cx))) {
1395 field = field.bg(match self.variant {
1396 FieldVariant::Primary => colors.field.focus(),
1397 FieldVariant::Secondary => colors.default.color,
1398 });
1399 }
1400
1401 if self.is_disabled {
1402 field = field.opacity(layout.disabled_opacity);
1403 } else if !field_box.is_bare && !self.is_invalid && !trigger_focused {
1404 let idle_bg = match self.variant {
1405 FieldVariant::Primary => colors.field.background,
1406 FieldVariant::Secondary => colors.default.color,
1407 };
1408 let hover_bg = self.trigger_hover_bg.unwrap_or(match self.variant {
1409 FieldVariant::Primary => colors.field.hover(),
1410 FieldVariant::Secondary => colors.default.hover(),
1412 });
1413 let hover_border = colors.field.border_hover();
1414 field = crate::anim::hover_fade_with_duration_and_easing_suppressed(
1418 field,
1419 element_id::scoped(&self.id, "trigger-hover-fade"),
1420 (idle_bg, hover_bg),
1421 None,
1422 (!clear_hovered).then_some(hover_border),
1423 clear_hovered,
1424 |fill| fill.rounded(trigger_radius),
1425 Some(150),
1426 crate::anim::HoverFadeEasing::EaseSmooth,
1427 window,
1428 cx,
1429 );
1430 }
1431
1432 if self.full_width {
1433 field = field.w_full();
1434 }
1435 field
1436 }
1437
1438 fn trigger_keys(
1440 &self,
1441 field: gpui::Stateful<gpui::Div>,
1442 frame: &SelectFrame,
1443 ) -> gpui::Stateful<gpui::Div> {
1444 let SelectFrame {
1445 is_open,
1446 ref open_own,
1447 multiple,
1448 ref selected,
1449 ref value_own,
1450 ref selected_keys,
1451 ref indices_own,
1452 ref keys,
1453 ref focus_handle,
1454 ref cursor,
1455 ref keyboard_press_open,
1456 ref selection_range,
1457 ref list_scroll_now,
1458 ref panel_scroll_now,
1459 ref typeahead,
1460 clear_enabled,
1461 ref clear_selection,
1462 ..
1463 } = *frame;
1464 let stops: Vec<usize> = (0..self.items.len())
1465 .filter(|i| !self.disabled_keys.contains(&keys[*i]))
1466 .collect();
1467 let collection: Vec<SharedString> = keys.clone();
1471 let selectable: Vec<SharedString> = stops.iter().map(|i| keys[*i].clone()).collect();
1472 let held = cursor.clone();
1473 let wrap = self.should_focus_wrap;
1474 let labels: Vec<String> = self
1476 .items
1477 .iter()
1478 .map(|item| item.label().to_string())
1479 .collect();
1480 let typed = typeahead.clone();
1481 let open_own_keys = open_own.clone();
1482 let value_own_keys = value_own.clone();
1483 let indices_own_keys = indices_own.clone();
1484 let selected_held = selected.clone();
1485 let selected_keys_held = selected_keys.clone();
1486 let form_state_keys = self.form_state.clone();
1487 let on_open_change = self.on_open_change.clone();
1488 let on_select = self.on_selection_change.clone();
1489 let on_select_all = self.on_selection_change_all.clone();
1490 let range_keys = selection_range.clone();
1491 let was_open = is_open;
1492 let virtual_rows = self.row_height.is_some();
1493 let key_list_scroll = list_scroll_now.clone();
1494 let key_panel_scroll = panel_scroll_now.clone();
1495 let fh = focus_handle.clone();
1496 let press_open = keyboard_press_open.clone();
1497 let row_keys = keys.clone();
1498 let clear_keys = clear_selection.clone();
1499 let key_focus = focus_handle.clone();
1500 let handler = SelectTriggerKeys {
1501 stops,
1502 collection,
1503 selectable,
1504 held,
1505 wrap,
1506 labels,
1507 typed,
1508 open_own_keys,
1509 value_own_keys,
1510 indices_own_keys,
1511 selected_held,
1512 selected_keys_held,
1513 form_state_keys,
1514 on_open_change,
1515 on_select,
1516 on_select_all,
1517 range_keys,
1518 was_open,
1519 virtual_rows,
1520 key_list_scroll,
1521 key_panel_scroll,
1522 press_open,
1523 row_keys,
1524 clear_keys,
1525 key_focus,
1526 clear_enabled,
1527 multiple,
1528 };
1529 field
1530 .track_focus(focus_handle)
1531 .key_context("Select")
1532 .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
1533 window.focus(&fh, cx);
1534 })
1535 .on_key_down(move |event, window, cx| handler.on_key_down(event, window, cx))
1536 }
1537
1538 fn trigger_value(
1540 &self,
1541 frame: &SelectFrame,
1542 colors: &herogpui_theme::ThemeColors,
1543 ) -> (SharedString, gpui::AnyElement) {
1544 let SelectFrame {
1545 multiple,
1546 ref selected,
1547 ref selected_keys,
1548 ..
1549 } = *frame;
1550 let value_text = if multiple {
1551 self.value_text_multiple(selected_keys)
1552 } else {
1553 self.value_text_single(selected)
1554 };
1555 let chosen = resolved_keys(&self.items, selected, selected_keys);
1556 let has_value = !chosen.is_empty();
1557 let mut chosen_items: Vec<SharedString> = Vec::with_capacity(chosen.len());
1560 let mut chosen_at: Vec<usize> = Vec::with_capacity(chosen.len());
1561 for (at, item) in self.items.iter().enumerate() {
1562 if chosen.contains(item.key()) {
1563 chosen_items.push(item.label().clone());
1564 chosen_at.push(at);
1565 }
1566 }
1567
1568 let default_children = gpui::div()
1572 .flex_1()
1577 .min_w_0()
1578 .whitespace_normal()
1579 .text_color(if has_value {
1580 colors.foreground
1581 } else {
1582 colors.muted
1583 })
1584 .child(value_text.to_string())
1585 .into_any_element();
1586
1587 let value_slot = match &self.value_content {
1589 Some(render) => {
1590 let names = chosen_items
1591 .iter()
1592 .map(ToString::to_string)
1593 .collect::<Vec<_>>();
1594 let text = format_selected_names(&names);
1595 gpui::div()
1596 .flex_1()
1597 .min_w_0()
1598 .child(render(util::SelectionValue {
1599 selected_items: &chosen_items,
1600 selected_indices: &chosen_at,
1601 selected_keys: Some(&chosen),
1602 selected_text: &text,
1603 is_placeholder: !has_value,
1604 default_children,
1605 }))
1606 .into_any_element()
1607 }
1608 None => default_children,
1609 };
1610 (value_text, value_slot)
1611 }
1612
1613 fn clear_buttons(
1616 &mut self,
1617 mut field: gpui::Stateful<gpui::Div>,
1618 frame: &SelectFrame,
1619 colors: &herogpui_theme::ThemeColors,
1620 window: &mut Window,
1621 cx: &mut App,
1622 ) -> gpui::Stateful<gpui::Div> {
1623 let SelectFrame {
1624 ref focus_handle,
1625 clear_empty,
1626 clear_enabled,
1627 ref clear_slots,
1628 ref clear_selection,
1629 ..
1630 } = *frame;
1631 for (index, (button, slot)) in std::mem::take(&mut self.clear_buttons)
1632 .into_iter()
1633 .zip(clear_slots)
1634 .enumerate()
1635 {
1636 let id = element_id::scoped(&self.id, format!("clear-button-{index}"));
1637 let pressed = clear_enabled && slot.read(cx).1;
1638 let hovered = clear_enabled && slot.read(cx).0;
1639 let scale = if pressed { 0.93 } else { 1. };
1640 let mut visual = gpui::div()
1641 .flex()
1642 .items_center()
1643 .justify_center()
1644 .size(px(20. * scale))
1645 .rounded(px(f32::from(util::small_radius(cx)) * scale))
1646 .when(hovered, |el| el.bg(colors.default.hover()));
1647 visual = if button.children.is_empty() {
1648 visual.child(
1649 gpui::svg()
1650 .w(px(12. * scale))
1651 .h(px(14. * scale))
1652 .path(icons::CLOSE)
1653 .text_color(colors.muted),
1654 )
1655 } else {
1656 visual.children(button.children)
1657 };
1658 let mut opacity = crate::anim::Tween::keyed(
1659 &id,
1660 "opacity",
1661 if clear_empty { 0. } else { 1. },
1662 window,
1663 cx,
1664 );
1665 let reduced = ActiveTheme::reduce_motion(cx);
1667 opacity.snap_if_reduced(reduced || clear_empty);
1668 let visual = if opacity.animates(reduced) {
1669 let from = opacity.from();
1670 let value = opacity.value();
1671 visual
1672 .with_animation(
1673 element_id::scoped(&id, format!("opacity-{}", opacity.generation())),
1674 gpui::Animation::new(std::time::Duration::from_millis(150))
1675 .with_easing(crate::anim::ease_smooth()),
1676 move |el, progress| {
1677 let alpha = from + (1. - from) * progress;
1678 value.set(alpha);
1679 el.opacity(alpha)
1680 },
1681 )
1682 .into_any_element()
1683 } else {
1684 opacity.settle();
1685 visual.opacity(opacity.target()).into_any_element()
1686 };
1687 let target_size = px(24. * scale);
1697 let target_inset = px((20. - 24. * scale) / 2.);
1698 let mut target = gpui::div()
1699 .id(id.clone())
1700 .absolute()
1701 .left(target_inset)
1702 .top(target_inset)
1703 .size(target_size)
1704 .flex()
1705 .items_center()
1706 .justify_center()
1707 .child(visual)
1708 .debug_selector(move || format!("select-clear-{index}"));
1709 if clear_enabled {
1710 let focus = focus_handle.clone();
1711 let clear = clear_selection.clone();
1712 target = util::track_interaction_on_mouse_down(target, slot, move |window, cx| {
1713 window.focus(&focus, cx);
1714 cx.stop_propagation();
1715 })
1716 .cursor(util::interactive_cursor(cx))
1717 .on_mouse_down(gpui::MouseButton::Right, |_, _, cx| cx.stop_propagation())
1718 .on_mouse_down(gpui::MouseButton::Middle, |_, _, cx| cx.stop_propagation())
1719 .on_click(move |event, window, cx| {
1720 cx.stop_propagation();
1721 window.prevent_default();
1722 clear(window, cx);
1723 if let Some(callback) = &button.on_click {
1724 callback(event, window, cx);
1725 }
1726 });
1727 }
1728 field = field.child(
1729 gpui::div()
1730 .relative()
1731 .size(px(20.))
1732 .flex_shrink_0()
1733 .child(target),
1734 );
1735 }
1736 field
1737 }
1738
1739 fn trigger_indicator_slot(
1741 &mut self,
1742 mut field: gpui::Stateful<gpui::Div>,
1743 frame: &SelectFrame,
1744 colors: &herogpui_theme::ThemeColors,
1745 window: &mut Window,
1746 cx: &mut App,
1747 ) -> gpui::Stateful<gpui::Div> {
1748 let is_open = frame.is_open;
1749 let indicator = match self.trigger_indicator.take() {
1754 Some(render) => render(is_open),
1755 None => crate::anim::rotating_indicator_with_duration(
1756 &element_id::scoped(&self.id, "trigger-indicator"),
1757 is_open,
1758 gpui::svg()
1759 .size(px(16.))
1760 .path(icons::CHEVRON_DOWN)
1761 .text_color(colors.muted)
1762 .flex_shrink_0(),
1763 150,
1764 window,
1765 cx,
1766 ),
1767 };
1768 field = field.child(
1769 gpui::div()
1770 .absolute()
1771 .right(px(8.))
1772 .top_0()
1773 .bottom_0()
1774 .w(px(16.))
1775 .flex()
1776 .items_center()
1777 .justify_center()
1778 .child(indicator),
1779 );
1780 field
1781 }
1782
1783 fn trigger_toggle(
1785 &self,
1786 mut field: gpui::Stateful<gpui::Div>,
1787 frame: &SelectFrame,
1788 ) -> gpui::Stateful<gpui::Div> {
1789 let SelectFrame {
1790 is_open,
1791 ref open_own,
1792 multiple,
1793 ref keyboard_press_open,
1794 ref trigger_pressed,
1795 ..
1796 } = *frame;
1797 if !self.is_disabled && (self.on_open_change.is_some() || open_own.is_some()) {
1798 let on_open_change = self.on_open_change.clone();
1799 let own = open_own.clone();
1800 let open = is_open;
1801 let pressed = trigger_pressed.clone();
1802 let keyboard_press_open = keyboard_press_open.clone();
1803 field = field
1804 .capture_any_mouse_down(move |_, _, cx| {
1805 pressed.set(true);
1806 let pressed = pressed.clone();
1807 cx.defer(move |_| pressed.set(false));
1808 })
1809 .on_click(move |event, window, cx| {
1810 let keyboard = matches!(event, gpui::ClickEvent::Keyboard(_));
1814 let press_open = keyboard_press_open.update(cx, |value, _| value.take());
1815 let started_open = if keyboard {
1816 press_open.unwrap_or(open)
1817 } else {
1818 open
1819 };
1820 if multiple && started_open && keyboard {
1821 return;
1822 }
1823 let next_open = !started_open;
1827 if next_open == open {
1828 return;
1829 }
1830 if let Some(held) = &own {
1833 held.update(cx, |v, cx| {
1834 *v = next_open;
1835 cx.notify();
1836 });
1837 }
1838 if let Some(cb) = &on_open_change {
1839 cb(&next_open, window, cx);
1840 }
1841 if !keyboard {
1853 cx.stop_propagation();
1854 }
1855 });
1856 }
1857 field
1858 }
1859
1860 fn field_root(&self, field: impl IntoElement) -> gpui::Div {
1862 let mut root = gpui::div().relative();
1864 root = if self.full_width {
1865 root.w_full()
1866 } else {
1867 root.max_w(px(320.))
1868 };
1869 if self.label.is_some() || self.description.is_some() {
1870 let mut wrapper = gpui::div().flex().flex_col().gap(px(4.)).w_full();
1871 if let Some(label) = &self.label {
1874 wrapper = wrapper.child(
1875 crate::field::Label::new(label.clone())
1876 .is_required(self.is_required)
1877 .is_disabled(self.is_disabled)
1878 .is_invalid(self.is_invalid),
1879 );
1880 }
1881 wrapper = wrapper.child(field);
1882 if !self.is_invalid {
1883 if let Some(desc) = &self.description {
1884 wrapper = wrapper.child(crate::field::Description::new(desc.clone()));
1885 }
1886 }
1887 root = root.child(wrapper);
1888 } else {
1889 root = root.child(field);
1890 }
1891 root
1892 }
1893
1894 fn root_dismissals(&self, mut root: gpui::Div, frame: &SelectFrame) -> gpui::Div {
1896 let SelectFrame {
1897 ref open_own,
1898 ref dismissal_token,
1899 ..
1900 } = *frame;
1901 let overlay_active = frame.overlay_phase != util::OverlayPhase::Closed;
1902 let escape_own = open_own.clone();
1903 let escape_cb = self.on_open_change.clone();
1904 root =
1905 util::dismiss_on_escape_with_token(root, dismissal_token.clone(), move |window, cx| {
1906 if let Some(held) = &escape_own {
1907 held.update(cx, |v, cx| {
1908 *v = false;
1909 cx.notify();
1910 });
1911 }
1912 if let Some(cb) = &escape_cb {
1913 cb(&false, window, cx);
1914 }
1915 util::DismissResult::Handled
1916 });
1917
1918 if overlay_active && self.items.is_empty() {
1919 let dismiss_own = open_own.clone();
1920 let dismiss_cb = self.on_open_change.clone();
1921 root = util::dismiss_on_press_outside_with_token(
1922 root,
1923 dismissal_token.clone(),
1924 move |window, cx| {
1925 if let Some(held) = &dismiss_own {
1926 held.update(cx, |v, cx| {
1927 *v = false;
1928 cx.notify();
1929 });
1930 }
1931 if let Some(cb) = &dismiss_cb {
1932 cb(&false, window, cx);
1933 }
1934 util::DismissResult::Handled
1935 },
1936 );
1937 }
1938 root
1939 }
1940
1941 fn panel_surface(
1944 &self,
1945 base: &str,
1946 base_id: &gpui::ElementId,
1947 radius: Pixels,
1948 panel_scroll_now: &gpui::ScrollHandle,
1949 colors: &herogpui_theme::ThemeColors,
1950 layout: &herogpui_theme::LayoutTheme,
1951 ) -> gpui::Stateful<gpui::Div> {
1952 let list_name = self.label.clone();
1955 gpui::div()
1956 .w_full()
1957 .flex()
1958 .flex_col()
1959 .p(self.panel_padding.unwrap_or(px(6.)))
1960 .bg(colors.overlay.background)
1961 .rounded(radius)
1962 .when_some(layout.overlay_hairline, |el, hairline| {
1966 el.border(layout.border_width).border_color(hairline)
1967 })
1968 .shadow(layout.overlay_shadow.clone())
1969 .id(element_id::scoped(base_id, "scroll"))
1972 .a11y_named(a11y::Role::ListBox, &a11y::Name::maybe(list_name))
1979 .a11y_orientation(herogpui_core::Orientation::Vertical)
1980 .debug_selector({
1981 let base = base.to_owned();
1982 move || format!("{base}-panel")
1983 })
1984 .overflow_y_scroll()
1985 .occlude()
1988 .track_scroll(panel_scroll_now)
1989 .max_h_full()
1994 }
1995
1996 fn popover(
1998 &mut self,
1999 frame: SelectFrame,
2000 colors: &herogpui_theme::ThemeColors,
2001 layout: &herogpui_theme::LayoutTheme,
2002 window: &mut Window,
2003 cx: &mut App,
2004 ) -> gpui::Deferred {
2005 let SelectFrame {
2006 open_own,
2007 overlay_phase,
2008 dismissal_token,
2009 resolved_placement,
2010 entry_placement,
2011 multiple,
2012 selected,
2013 value_own,
2014 selected_keys,
2015 indices_own,
2016 keys,
2017 focus_handle,
2018 cursor,
2019 cursor_at,
2020 selection_range,
2021 list_scroll_now,
2022 panel_scroll_now,
2023 anchor_bounds,
2024 trigger_pressed,
2025 ..
2026 } = frame;
2027 let base = format!("select-list-{}", id_debug(&self.id));
2030 let base_id = element_id::scoped(&self.id, "list");
2031 let options_len = self.items.len();
2032 let panel_interactive = overlay_phase == util::OverlayPhase::Open;
2033 let radius = self.radius.unwrap_or_else(|| util::container_radius(cx));
2036 let panel = self.panel_surface(&base, &base_id, radius, &panel_scroll_now, colors, layout);
2037
2038 let dismiss_own = open_own.clone();
2044 let dismiss_cb = self.on_open_change.clone();
2045 let mut panel =
2046 util::dismiss_on_press_outside_with_token(panel, dismissal_token, move |window, cx| {
2047 if trigger_pressed.get() {
2048 return util::DismissResult::Declined;
2049 }
2050 if let Some(held) = &dismiss_own {
2051 held.update(cx, |v, cx| {
2052 *v = false;
2053 cx.notify();
2054 });
2055 }
2056 if let Some(cb) = &dismiss_cb {
2057 cb(&false, window, cx);
2058 }
2059 util::DismissResult::Handled
2060 });
2061
2062 let row_virtualized = self.row_height.is_some();
2065 let row_disabled_opacity = layout.disabled_opacity;
2066 let items = self.items.clone();
2071 let sections = self.sections.clone();
2072 let opt_disabled_keys = self.disabled_keys.clone();
2073 let collection_rows: Vec<SharedString> = keys.clone();
2076 let selectable_rows: Vec<SharedString> = (0..options_len)
2077 .filter(|i| !self.disabled_keys.contains(&keys[*i]))
2078 .map(|i| keys[i].clone())
2079 .collect();
2080 let range_rows = selection_range;
2081 let cursor_rows = cursor;
2082 let focus_rows = focus_handle;
2083 let indicator: Option<Rc<dyn Fn(bool) -> gpui::AnyElement>> =
2084 self.indicator.take().map(Rc::from);
2085 #[allow(clippy::type_complexity)]
2086 let item_leading: Option<
2087 Rc<dyn Fn(&SharedString, bool) -> Option<gpui::AnyElement>>,
2088 > = self.item_leading.take().map(Rc::from);
2089 let on_change_all = self.on_selection_change_all.clone();
2090 let on_change_one = self.on_selection_change.clone();
2091 let value_own = value_own;
2092 let open_own = open_own;
2093 let form_state_rows = self.form_state.clone();
2094 let on_close = self.on_open_change.clone();
2095 let base_row = base;
2096 let base_row_id = base_id.clone();
2097 let row_text_size = self.row_text_size.unwrap_or(util::FIELD_TEXT);
2098 let row_padding_x = self.row_padding_x.unwrap_or(px(10.));
2099 let row_font_family = self.row_font_family.clone();
2100 let row_padding_y = self.row_padding_y.unwrap_or(px(6.));
2101 let rows = SelectRows {
2102 colors: colors.clone(),
2103 row_hover_bg: self.row_hover_bg,
2104 multiple,
2105 selected,
2106 selected_keys,
2107 cursor_at,
2108 panel_interactive,
2109 options_len,
2110 row_virtualized,
2111 row_disabled_opacity,
2112 items,
2113 sections,
2114 opt_disabled_keys,
2115 collection_rows,
2116 selectable_rows,
2117 range_rows,
2118 cursor_rows,
2119 focus_rows,
2120 indicator,
2121 item_leading,
2122 on_change_all,
2123 on_change_one,
2124 indices_own,
2125 value_own,
2126 open_own,
2127 form_state_rows,
2128 on_close,
2129 base_row,
2130 base_row_id,
2131 row_text_size,
2132 row_padding_x,
2133 row_font_family,
2134 row_padding_y,
2135 };
2136
2137 match self.row_height {
2138 Some(row_height) => {
2148 panel = panel.child(
2149 gpui::uniform_list(
2150 element_id::scoped(&base_id, "rows"),
2151 options_len,
2152 move |range, _window, cx| {
2153 range
2154 .map(|i| rows.row(i, Some(row_height), _window, cx))
2155 .collect::<Vec<_>>()
2156 },
2157 )
2158 .track_scroll(&list_scroll_now)
2159 .with_sizing_behavior(gpui::ListSizingBehavior::Infer)
2160 .w_full(),
2161 );
2162 }
2163 None => {
2164 for i in 0..options_len {
2165 panel = panel.child(rows.row(i, None, window, cx));
2166 }
2167 }
2168 }
2169
2170 let (slide_x, slide_y) = crate::popover::placement_entry_offset(entry_placement);
2171 let zoom = crate::anim::ZoomBox::panel(self.panel_padding.unwrap_or(px(6.)), radius);
2172 let zoom = crate::anim::ZoomBox {
2173 slide_x: (slide_x != 0.0).then(|| px(slide_x)),
2174 slide_y: (slide_y != 0.0).then(|| px(slide_y)),
2175 ..zoom
2176 };
2177 let panel = if overlay_phase == util::OverlayPhase::Exiting {
2178 crate::anim::exiting(
2179 panel,
2180 element_id::scoped(&base_id, "panel-out"),
2181 zoom,
2182 crate::anim::Motion::LIST_OUT,
2183 cx,
2184 )
2185 } else {
2186 crate::anim::entering_zoom(
2187 panel,
2188 element_id::scoped(&base_id, "panel"),
2189 zoom,
2190 crate::anim::Motion::LIST_IN,
2191 cx,
2192 )
2193 };
2194 util::floating(
2199 crate::popover::scrollable_field_popover_with_resolved_placement(
2200 anchor_bounds,
2201 self.placement,
2202 Some(resolved_placement),
2203 panel,
2204 ),
2205 )
2206 }
2207}
2208
2209struct SelectTriggerKeys {
2213 stops: Vec<usize>,
2214 collection: Vec<SharedString>,
2215 selectable: Vec<SharedString>,
2216 held: gpui::Entity<Option<SharedString>>,
2217 wrap: bool,
2218 labels: Vec<String>,
2219 typed: gpui::Entity<crate::list_nav::Typeahead>,
2220 open_own_keys: Option<gpui::Entity<bool>>,
2221 value_own_keys: Option<gpui::Entity<Option<SharedString>>>,
2222 indices_own_keys: Option<gpui::Entity<Vec<SharedString>>>,
2223 selected_held: Option<SharedString>,
2224 selected_keys_held: Vec<SharedString>,
2225 form_state_keys: Rc<RefCell<crate::form::LiveFormFieldState>>,
2226 on_open_change: Option<OnOpenChange>,
2227 on_select: Option<OnSelectionChange>,
2228 on_select_all: Option<OnSelectionChangeAll>,
2229 range_keys: gpui::Entity<SelectSelectionRange>,
2230 was_open: bool,
2231 virtual_rows: bool,
2232 key_list_scroll: gpui::UniformListScrollHandle,
2233 key_panel_scroll: gpui::ScrollHandle,
2234 press_open: gpui::Entity<Option<bool>>,
2235 row_keys: Vec<SharedString>,
2236 clear_keys: SelectAction,
2237 key_focus: gpui::FocusHandle,
2238 clear_enabled: bool,
2239 multiple: bool,
2240}
2241
2242impl SelectTriggerKeys {
2243 fn on_key_down(&self, event: &gpui::KeyDownEvent, window: &mut Window, cx: &mut App) {
2244 let Self {
2245 ref stops,
2246 ref held,
2247 wrap,
2248 ref indices_own_keys,
2249 ref selected_keys_held,
2250 ref form_state_keys,
2251 ref range_keys,
2252 was_open,
2253 ref press_open,
2254 ref row_keys,
2255 ref clear_keys,
2256 ref key_focus,
2257 clear_enabled,
2258 multiple,
2259 ..
2260 } = *self;
2261 if !key_focus.is_focused(window) {
2262 return;
2263 }
2264 let key = event.keystroke.key.as_str();
2265 if !was_open && clear_enabled && matches!(key, "backspace" | "delete") {
2266 cx.stop_propagation();
2267 window.prevent_default();
2268 util::set_focus_visible(true, cx);
2270 clear_keys(window, cx);
2271 return;
2272 }
2273 if matches!(key, "enter" | "space") {
2274 cx.stop_propagation();
2277 if event.is_held {
2278 return;
2279 }
2280 press_open.update(cx, |value, _| *value = Some(was_open));
2281 }
2282 if !was_open {
2283 self.on_closed_key(key, window, cx);
2284 return;
2285 }
2286 let from = held
2287 .read(cx)
2288 .as_ref()
2289 .and_then(|k| row_keys.iter().position(|key| key == k));
2290 let modifiers = event.keystroke.modifiers;
2291 if key == "a"
2300 && modifiers.secondary()
2301 && !modifiers.shift
2302 && !modifiers.alt
2303 && if cfg!(target_os = "macos") {
2304 !modifiers.control
2305 } else {
2306 !modifiers.platform
2307 }
2308 && multiple
2309 {
2310 let all: Vec<SharedString> = stops.iter().map(|i| row_keys[*i].clone()).collect();
2311 let complete = all.iter().all(|key| selected_keys_held.contains(key));
2312 if !complete {
2313 if let Some(held) = &indices_own_keys {
2314 form_state_keys.borrow_mut().value = crate::form::FormValue::Keys(all.clone());
2315 held.update(cx, |selected, cx| {
2316 *selected = all.clone();
2317 cx.notify();
2318 });
2319 range_keys.update(cx, |range, _| {
2320 *range = SelectSelectionRange {
2321 is_all: true,
2322 ..SelectSelectionRange::default()
2323 };
2324 });
2325 }
2326 }
2327 cx.stop_propagation();
2328 return;
2329 }
2330 let page_move = match key {
2343 "pagedown" if from.is_some() => stops.last().copied(),
2344 "pageup" if from.is_some() => stops.first().copied(),
2345 _ => None,
2346 }
2347 .filter(|next| Some(*next) != from);
2348 let next_move = page_move.map_or_else(
2349 || crate::list_nav::resolve(stops, from, key, wrap),
2350 crate::list_nav::Move::To,
2351 );
2352 self.apply_move(next_move, from, key, modifiers, window, cx);
2353 }
2354
2355 fn on_closed_key(&self, key: &str, window: &mut Window, cx: &mut App) {
2358 let Self {
2359 ref labels,
2360 ref stops,
2361 ref typed,
2362 ref open_own_keys,
2363 ref value_own_keys,
2364 ref selected_held,
2365 ref form_state_keys,
2366 ref on_open_change,
2367 ref on_select,
2368 ref row_keys,
2369 multiple,
2370 ..
2371 } = *self;
2372 if matches!(key, "down" | "up") {
2378 if let Some(held) = &open_own_keys {
2379 held.update(cx, |v, cx| {
2380 *v = true;
2381 cx.notify();
2382 });
2383 }
2384 if let Some(cb) = &on_open_change {
2385 cb(&true, window, cx);
2386 }
2387 return;
2388 }
2389 if multiple {
2399 return;
2400 }
2401 if !crate::list_nav::is_typeahead_key(key) {
2402 return;
2403 }
2404 let now = web_time::Instant::now();
2405 let (query, repeat) = typed.update(cx, |t, _| {
2406 let query = t.push(key, now);
2407 (query, t.is_repeat())
2408 });
2409 let selected_at = selected_held
2410 .as_ref()
2411 .and_then(|k| row_keys.iter().position(|key| key == k));
2412 let Some(found) = crate::list_nav::typeahead(labels, stops, selected_at, &query, repeat)
2413 else {
2414 return;
2415 };
2416 let found = row_keys[found].clone();
2417 if let Some(held) = &value_own_keys {
2418 form_state_keys.borrow_mut().value = crate::form::FormValue::Keys(vec![found.clone()]);
2419 held.update(cx, |v, cx| {
2420 *v = Some(found.clone());
2421 cx.notify();
2422 });
2423 }
2424 if let Some(cb) = &on_select {
2425 cb(&Some(found), window, cx);
2426 }
2427 }
2428
2429 fn apply_move(
2434 &self,
2435 next_move: crate::list_nav::Move,
2436 from: Option<usize>,
2437 key: &str,
2438 modifiers: gpui::Modifiers,
2439 window: &mut Window,
2440 cx: &mut App,
2441 ) {
2442 let Self {
2443 ref collection,
2444 ref selectable,
2445 ref held,
2446 ref labels,
2447 ref stops,
2448 ref typed,
2449 ref value_own_keys,
2450 ref indices_own_keys,
2451 ref selected_keys_held,
2452 ref form_state_keys,
2453 ref on_select,
2454 ref on_select_all,
2455 ref range_keys,
2456 virtual_rows,
2457 ref key_list_scroll,
2458 ref key_panel_scroll,
2459 ref row_keys,
2460 multiple,
2461 ..
2462 } = *self;
2463 match next_move {
2464 crate::list_nav::Move::To(at) => {
2465 if matches!(key, "home" | "end")
2472 && !home_end_registered(modifiers, cfg!(target_os = "macos"))
2473 {
2474 return;
2475 }
2476 if matches!(key, "home" | "end") && modifiers.shift && from.is_none() {
2482 return;
2483 }
2484 let next = row_keys[at].clone();
2485 held.update(cx, |v, cx| {
2486 *v = Some(next.clone());
2487 cx.notify();
2488 });
2489 if virtual_rows {
2493 key_list_scroll.scroll_to_item(at, gpui::ScrollStrategy::Center);
2494 } else {
2495 key_panel_scroll.scroll_to_item(at);
2496 }
2497 let exact_shift_navigation =
2511 home_end_registered(modifiers, cfg!(target_os = "macos"));
2512 let extends_selection = multiple
2513 && modifiers.shift
2514 && exact_shift_navigation
2515 && shift_home_end_extends(key, modifiers.control, cfg!(target_os = "macos"))
2516 && (matches!(key, "home" | "end") || Some(at) != from);
2517 if extends_selection {
2518 let range = range_keys.read(cx).clone();
2519 let next_selection = extend_selection_range(
2520 selected_keys_held,
2521 collection,
2522 selectable,
2523 &range,
2524 &next,
2525 );
2526 range_keys.update(cx, |range, _| {
2527 if range.anchor.is_none() {
2528 range.anchor = Some(next.clone());
2529 }
2530 range.current = Some(next);
2531 range.is_all = false;
2532 });
2533 if next_selection != *selected_keys_held {
2541 if let Some(held) = &indices_own_keys {
2542 form_state_keys.borrow_mut().value =
2543 crate::form::FormValue::Keys(next_selection.clone());
2544 held.update(cx, |selected, cx| {
2545 *selected = next_selection.clone();
2546 cx.notify();
2547 });
2548 }
2549 }
2550 if let Some(cb) = &on_select_all {
2551 cb(&next_selection, window, cx);
2552 }
2553 }
2554 }
2555 crate::list_nav::Move::Activate => {
2556 let Some(at) = from else { return };
2559 let key = &row_keys[at];
2560 if multiple {
2561 let added = !selected_keys_held.contains(key);
2562 let mut next = selected_keys_held.clone();
2563 toggle_key(&mut next, key);
2564 let next = in_collection_order(&next, row_keys);
2565 if added {
2570 range_keys.update(cx, |range, _| {
2571 range.anchor = Some(key.clone());
2572 range.current = Some(key.clone());
2573 range.is_all = false;
2574 });
2575 } else {
2576 range_keys.update(cx, |range, _| {
2577 if range.is_all {
2578 *range = SelectSelectionRange::default();
2579 }
2580 });
2581 }
2582 if let Some(held) = &indices_own_keys {
2583 form_state_keys.borrow_mut().value =
2584 crate::form::FormValue::Keys(next.clone());
2585 held.update(cx, |selected, cx| {
2586 *selected = next.clone();
2587 cx.notify();
2588 });
2589 }
2590 if let Some(cb) = &on_select_all {
2591 cb(&next, window, cx);
2592 }
2593 return;
2594 }
2595 if let Some(held) = &value_own_keys {
2596 form_state_keys.borrow_mut().value =
2597 crate::form::FormValue::Keys(vec![key.clone()]);
2598 held.update(cx, |v, cx| {
2599 *v = Some(key.clone());
2600 cx.notify();
2601 });
2602 }
2603 if let Some(cb) = &on_select {
2604 cb(&Some(key.clone()), window, cx);
2605 }
2606 }
2607 crate::list_nav::Move::Ignore => {
2608 if !crate::list_nav::is_typeahead_key(key) {
2613 return;
2614 }
2615 let now = web_time::Instant::now();
2616 let (query, repeat) = typed.update(cx, |t, _| {
2617 let query = t.push(key, now);
2618 (query, t.is_repeat())
2619 });
2620 if let Some(found) = crate::list_nav::typeahead(labels, stops, from, &query, repeat)
2621 {
2622 let found = row_keys[found].clone();
2623 held.update(cx, |v, cx| {
2624 *v = Some(found);
2625 cx.notify();
2626 });
2627 }
2628 }
2629 }
2630 }
2631}
2632
2633struct SelectRows {
2638 colors: herogpui_theme::ThemeColors,
2641 row_hover_bg: Option<gpui::Hsla>,
2642 multiple: bool,
2643 selected: Option<SharedString>,
2644 selected_keys: Vec<SharedString>,
2645 cursor_at: Option<usize>,
2646 panel_interactive: bool,
2647 options_len: usize,
2648 row_virtualized: bool,
2649 row_disabled_opacity: f32,
2650 items: Vec<PickerItem>,
2651 sections: Vec<(SharedString, SharedString)>,
2652 opt_disabled_keys: HashSet<SharedString>,
2653 collection_rows: Vec<SharedString>,
2654 selectable_rows: Vec<SharedString>,
2655 range_rows: gpui::Entity<SelectSelectionRange>,
2656 cursor_rows: gpui::Entity<Option<SharedString>>,
2657 focus_rows: gpui::FocusHandle,
2658 indicator: Option<Rc<dyn Fn(bool) -> gpui::AnyElement>>,
2659 #[allow(clippy::type_complexity)]
2660 item_leading: Option<Rc<dyn Fn(&SharedString, bool) -> Option<gpui::AnyElement>>>,
2661 on_change_all: Option<OnSelectionChangeAll>,
2662 on_change_one: Option<OnSelectionChange>,
2663 indices_own: Option<gpui::Entity<Vec<SharedString>>>,
2664 value_own: Option<gpui::Entity<Option<SharedString>>>,
2665 open_own: Option<gpui::Entity<bool>>,
2666 form_state_rows: Rc<RefCell<crate::form::LiveFormFieldState>>,
2667 on_close: Option<OnOpenChange>,
2668 base_row: String,
2669 base_row_id: gpui::ElementId,
2670 row_text_size: Pixels,
2671 row_padding_x: Pixels,
2672 row_font_family: Option<SharedString>,
2673 row_padding_y: Pixels,
2674}
2675
2676impl SelectRows {
2677 fn row(
2679 &self,
2680 i: usize,
2681 fixed_h: Option<Pixels>,
2682 window: &mut Window,
2683 cx: &mut App,
2684 ) -> gpui::AnyElement {
2685 let Self {
2686 ref colors,
2687 multiple,
2688 ref selected,
2689 ref selected_keys,
2690 cursor_at,
2691 panel_interactive,
2692 options_len,
2693 row_virtualized,
2694 row_disabled_opacity,
2695 ref items,
2696 ref sections,
2697 ref opt_disabled_keys,
2698 ref focus_rows,
2699 ref indicator,
2700 ref item_leading,
2701 ref base_row,
2702 ref base_row_id,
2703 row_text_size,
2704 row_padding_x,
2705 ref row_font_family,
2706 row_padding_y,
2707 ..
2708 } = *self;
2709 let row_muted = colors.muted;
2710 let row_fg = colors.foreground;
2711 let row_hover_bg = self.row_hover_bg.unwrap_or(colors.default.color);
2712 let base = &base_row;
2713 let base_id = &base_row_id;
2714 let opt = &items[i];
2715 let row_key = opt.key().clone();
2716 let focus_click = focus_rows.clone();
2717 let mut rows = Vec::new();
2718 if let Some((_, label)) = sections.iter().find(|(at, _)| at == &row_key) {
2721 rows.push(
2722 gpui::div()
2723 .px(px(8.))
2724 .pt(px(6.))
2725 .pb(px(4.))
2726 .text_size(px(12.))
2727 .line_height(px(16.))
2728 .font_weight(gpui::FontWeight::MEDIUM)
2729 .text_color(row_muted)
2730 .child(label.to_string())
2731 .into_any_element(),
2732 );
2733 }
2734 let is_sel = if multiple {
2735 selected_keys.contains(&row_key)
2736 } else {
2737 selected.as_ref() == Some(&row_key)
2738 };
2739 let has_indicator_slot = indicator.is_some() || is_sel;
2740 let opt_disabled = opt_disabled_keys.contains(&row_key);
2741 let row_selector = format!("{base}-opt-{i}");
2742 let mut item = gpui::div()
2743 .id(element_id::indexed(base_id, "opt", i))
2744 .a11y_named(
2752 a11y::Role::ListBoxOption,
2753 &a11y::Name::labelled(opt.label().clone()),
2754 )
2755 .a11y_selected(is_sel)
2756 .when(row_virtualized, |item| {
2757 item.a11y_set_position(i, options_len)
2758 })
2759 .when(cursor_at == Some(i), |item| item.a11y_active_descendant())
2764 .debug_selector(move || row_selector)
2765 .flex()
2766 .items_center()
2767 .justify_between()
2768 .map(|item| match fixed_h {
2774 Some(row_h) => item.h(row_h),
2775 None => item.min_h(util::FIELD_HEIGHT),
2776 })
2777 .rounded(util::soft_radius(cx))
2778 .px(row_padding_x)
2779 .py(row_padding_y)
2780 .gap(px(12.))
2781 .text_size(row_text_size)
2782 .line_height(px(20.))
2783 .relative()
2788 .when(has_indicator_slot, |row| row.pr(px(28.)));
2789 if let Some(family) = row_font_family.clone() {
2790 item = item.font_family(family);
2791 }
2792
2793 if opt_disabled {
2794 item = item.opacity(row_disabled_opacity);
2795 } else if panel_interactive {
2796 item = item
2797 .cursor(util::interactive_cursor(cx))
2798 .hover(move |s| s.bg(row_hover_bg));
2799 }
2800
2801 item = item.text_color(row_fg);
2804
2805 item = util::with_focus_ring_overlay(
2812 item,
2813 util::shows_focus_ring(cursor_at == Some(i), cx),
2814 true,
2815 util::soft_radius(cx),
2816 Vec::new(),
2817 cx,
2818 );
2819
2820 if let Some(render) = &item_leading {
2830 if let Some(content) = render(&row_key, is_sel) {
2831 item = item.child(gpui::div().flex_none().flex().items_center().child(content));
2832 }
2833 }
2834 let label = gpui::div().flex_1().min_w_0().whitespace_normal();
2835 item = item.child(label.child(opt.label().to_string()));
2836
2837 match &indicator {
2838 Some(render) => {
2839 item = item.child(
2840 gpui::div()
2841 .absolute()
2842 .top_0()
2843 .bottom_0()
2844 .right(px(8.))
2845 .w(px(16.))
2846 .flex()
2847 .items_center()
2848 .justify_center()
2849 .child(render(is_sel)),
2850 );
2851 }
2852 None if is_sel => {
2853 item = item.child(
2854 gpui::div()
2855 .absolute()
2856 .top_0()
2857 .bottom_0()
2858 .right(px(8.))
2859 .w(px(16.))
2860 .flex()
2861 .items_center()
2862 .justify_center()
2863 .child(
2864 gpui::svg()
2865 .size(px(13.))
2866 .path(icons::CHECK)
2867 .text_color(row_fg),
2871 ),
2872 );
2873 }
2874 None => {}
2875 }
2876
2877 if panel_interactive && !opt_disabled {
2883 item = crate::anim::pressed_with_background_ramp(
2884 item,
2885 crate::anim::PressBox {
2886 height: fixed_h.unwrap_or(util::FIELD_HEIGHT),
2887 padding_x: Some(row_padding_x),
2888 width: None,
2889 min_width: None,
2890 text_size: row_text_size,
2891 line_height: px(20.),
2892 gap: px(12.),
2893 radius: util::soft_radius(cx),
2894 shrink_x: false,
2895 scale: crate::anim::PRESSED_SCALE_SUBTLE,
2896 },
2897 None,
2898 crate::anim::LIST_ITEM_PRESS,
2899 None,
2900 window,
2901 cx,
2902 );
2903 }
2904
2905 if panel_interactive && !opt_disabled {
2906 item = self.attach_pick(item, row_key, focus_click);
2907 }
2908
2909 rows.push(item.into_any_element());
2910 gpui::div()
2911 .flex()
2912 .flex_col()
2913 .w_full()
2919 .when_some(fixed_h, |el, h| el.h(h))
2920 .children(rows)
2921 .into_any_element()
2922 }
2923
2924 fn attach_pick(
2927 &self,
2928 mut item: gpui::Stateful<gpui::Div>,
2929 row_key: SharedString,
2930 focus_click: gpui::FocusHandle,
2931 ) -> gpui::Stateful<gpui::Div> {
2932 let Self {
2933 multiple,
2934 ref selected_keys,
2935 ref collection_rows,
2936 ref selectable_rows,
2937 ref range_rows,
2938 ref cursor_rows,
2939 ref on_change_all,
2940 ref on_change_one,
2941 ref indices_own,
2942 ref value_own,
2943 ref open_own,
2944 ref form_state_rows,
2945 ref on_close,
2946 ..
2947 } = *self;
2948 if multiple {
2949 if indices_own.is_some() || on_change_all.is_some() {
2950 let current = selected_keys.clone();
2951 let own = indices_own.clone();
2952 let cb = on_change_all.clone();
2953 let form_state_pick = form_state_rows.clone();
2954 let range_click = range_rows.clone();
2955 let collection_click = collection_rows.clone();
2956 let selectable_click = selectable_rows.clone();
2957 let cursor_click = cursor_rows.clone();
2958 let picked_key = row_key;
2959 item = item.on_click(move |ev, window, cx| {
2960 if !matches!(ev, gpui::ClickEvent::Keyboard(_)) {
2973 cx.stop_propagation();
2974 }
2975 cursor_click.update(cx, |v, cx| {
2980 *v = Some(picked_key.clone());
2981 cx.notify();
2982 });
2983 window.focus(&focus_click, cx);
2988 let mut next = current.clone();
2989 if ev.modifiers().shift {
2995 let range = range_click.read(cx).clone();
2996 next = extend_selection_range(
2997 ¤t,
2998 &collection_click,
2999 &selectable_click,
3000 &range,
3001 &picked_key,
3002 );
3003 range_click.update(cx, |range, _| {
3004 if range.anchor.is_none() {
3005 range.anchor = Some(picked_key.clone());
3006 }
3007 range.current = Some(picked_key.clone());
3008 range.is_all = false;
3009 });
3010 } else {
3011 let added = !next.contains(&picked_key);
3012 toggle_key(&mut next, &picked_key);
3013 next = in_collection_order(&next, &collection_click);
3014 if added {
3015 range_click.update(cx, |range, _| {
3016 range.anchor = Some(picked_key.clone());
3017 range.current = Some(picked_key.clone());
3018 range.is_all = false;
3019 });
3020 } else {
3021 range_click.update(cx, |range, _| {
3022 if range.is_all {
3023 *range = SelectSelectionRange::default();
3024 }
3025 });
3026 }
3027 }
3028 if let Some(held) = &own {
3029 form_state_pick.borrow_mut().value =
3030 crate::form::FormValue::Keys(next.clone());
3031 held.update(cx, |selected, cx| {
3032 *selected = next.clone();
3033 cx.notify();
3034 });
3035 }
3036 if let Some(cb) = &cb {
3037 cb(&next, window, cx);
3038 }
3039 });
3040 }
3041 } else if on_change_one.is_some() || value_own.is_some() || open_own.is_some() {
3042 let on_select = on_change_one.clone();
3043 let on_close = on_close.clone();
3044 let value_own = value_own.clone();
3045 let open_own = open_own.clone();
3046 let form_state_pick = form_state_rows.clone();
3047 let picked_key = row_key;
3048 item = item.on_click(move |ev, window, cx| {
3049 if !matches!(ev, gpui::ClickEvent::Keyboard(_)) {
3054 cx.stop_propagation();
3055 }
3056 if let Some(held) = &value_own {
3059 form_state_pick.borrow_mut().value =
3060 crate::form::FormValue::Keys(vec![picked_key.clone()]);
3061 held.update(cx, |v, cx| {
3062 *v = Some(picked_key.clone());
3063 cx.notify();
3064 });
3065 }
3066 if let Some(held) = &open_own {
3067 held.update(cx, |v, cx| {
3068 *v = false;
3069 cx.notify();
3070 });
3071 }
3072 if let Some(cb) = &on_close {
3083 cb(&false, window, cx);
3084 }
3085 if let Some(f) = &on_select {
3086 f(&Some(picked_key.clone()), window, cx);
3087 }
3088 });
3089 }
3090 item
3091 }
3092}
3093
3094fn live_form_state() -> Rc<RefCell<crate::form::LiveFormFieldState>> {
3095 Rc::new(RefCell::new(crate::form::LiveFormFieldState {
3096 value: crate::form::FormValue::Text(SharedString::default()),
3097 is_invalid: false,
3098 is_successful: true,
3099 focus: None,
3100 restore: None,
3101 }))
3102}
3103
3104fn select_form_value(
3108 items: &[PickerItem],
3109 single: &Option<SharedString>,
3110 multiple: &[SharedString],
3111) -> crate::form::FormValue {
3112 crate::form::FormValue::Keys(resolved_keys(items, single, multiple))
3113}
3114
3115fn sync_select_form(
3116 state: &Rc<RefCell<crate::form::LiveFormFieldState>>,
3117 value: crate::form::FormValue,
3118 is_invalid: bool,
3119 is_successful: bool,
3120) {
3121 let mut state = state.borrow_mut();
3122 state.value = value;
3123 state.is_invalid = is_invalid;
3124 state.is_successful = is_successful;
3125}
3126
3127fn id_debug(id: &gpui::ElementId) -> String {
3128 format!("{id:?}").trim_matches('"').to_owned()
3129}
3130
3131#[cfg(test)]
3132mod tests {
3133 use super::*;
3134
3135 #[test]
3141 fn shift_home_end_extends_only_from_control_outside_macos() {
3142 for key in ["home", "end"] {
3143 assert!(
3144 !shift_home_end_extends(key, true, true),
3145 "macOS registers no Home/End extension"
3146 );
3147 assert!(!shift_home_end_extends(key, false, true));
3148 assert!(
3149 shift_home_end_extends(key, true, false),
3150 "Control+Shift+{key} must extend on Windows and Linux"
3151 );
3152 assert!(
3153 !shift_home_end_extends(key, false, false),
3154 "plain Shift+{key} must only move the cursor"
3155 );
3156 }
3157 }
3158
3159 #[test]
3162 fn arrows_and_pages_skip_the_home_end_gate() {
3163 assert!(shift_home_end_extends("down", false, true));
3164 assert!(shift_home_end_extends("up", true, false));
3165 assert!(shift_home_end_extends("pagedown", false, false));
3166 assert!(shift_home_end_extends("pageup", true, true));
3167 }
3168
3169 #[test]
3170 fn home_end_registration_follows_the_platform_maps() {
3171 let none = gpui::Modifiers::none();
3172 let mut shift = none;
3173 shift.shift = true;
3174 let mut alt = none;
3175 alt.alt = true;
3176 let mut alt_shift = alt;
3177 alt_shift.shift = true;
3178 let mut control = none;
3179 control.control = true;
3180 let mut control_shift = control;
3181 control_shift.shift = true;
3182 let mut platform = none;
3183 platform.platform = true;
3184 let mut platform_shift = platform;
3185 platform_shift.shift = true;
3186 let mut function = none;
3187 function.function = true;
3188 let mut function_alt = function;
3189 function_alt.alt = true;
3190
3191 for modifiers in [none, shift, alt, alt_shift, function, function_alt] {
3192 assert!(
3193 home_end_registered(modifiers, true),
3194 "macOS must register {modifiers:?}"
3195 );
3196 }
3197 for modifiers in [control, control_shift, platform, platform_shift] {
3198 assert!(
3199 !home_end_registered(modifiers, true),
3200 "macOS must not register {modifiers:?}"
3201 );
3202 }
3203 for modifiers in [none, shift, control, control_shift, function] {
3204 assert!(
3205 home_end_registered(modifiers, false),
3206 "Windows and Linux must register {modifiers:?}"
3207 );
3208 }
3209 for modifiers in [alt, alt_shift, function_alt, platform, platform_shift] {
3210 assert!(
3211 !home_end_registered(modifiers, false),
3212 "Windows and Linux must not register {modifiers:?}"
3213 );
3214 }
3215 }
3216
3217 #[test]
3218 fn default_value_text_matches_pinned_select_value() {
3219 let select = Select::new(
3220 "select-default-text",
3221 vec![
3222 PickerItem::new("alpha", "Alpha"),
3223 PickerItem::new("beta", "Beta"),
3224 PickerItem::new("gamma", "Gamma"),
3225 ],
3226 )
3227 .selection_mode(SelectionMode::Multiple)
3228 .selected_keys(["alpha", "beta", "gamma"].map(SharedString::from));
3229
3230 assert_eq!(
3231 select.value_text_multiple(&select.selected_keys),
3232 "Alpha, Beta, and Gamma"
3233 );
3234 assert_eq!(
3235 format_selected_names(&["Alpha".into(), "Beta".into()]),
3236 "Alpha and Beta"
3237 );
3238 assert_eq!(
3239 Select::new("select-default-placeholder", Vec::new()).placeholder,
3240 "Select an item"
3241 );
3242 }
3243
3244 #[test]
3249 fn keys_address_items_and_reads_walk_the_collection() {
3250 let keys = |names: &[&str]| -> Vec<SharedString> {
3251 names.iter().copied().map(SharedString::from).collect()
3252 };
3253 let items = vec![
3254 PickerItem::new("a", "Paris"),
3255 PickerItem::new("b", "London"),
3256 PickerItem::new("c", "Paris"),
3257 ];
3258 let select = Select::new("select-keyed", items).value(Some(SharedString::from("c")));
3260 assert_eq!(
3261 select.value_text_single(&select.selected),
3262 "Paris",
3263 "the key must resolve to its own item, never a same-label sibling"
3264 );
3265 assert_eq!(
3268 in_collection_order(&keys(&["c", "a", "zz"]), &keys(&["a", "b", "c"])),
3269 keys(&["a", "c"]),
3270 "reads must walk the collection and drop unresolvable keys"
3271 );
3272 assert_eq!(
3273 resolved_keys(&select.items, &None, &keys(&["c", "b"])),
3274 keys(&["b", "c"]),
3275 "the resolved set follows the collection's row order"
3276 );
3277 assert_eq!(
3278 resolved_keys(&select.items, &Some(SharedString::from("gone")), &[]),
3279 Vec::<SharedString>::new(),
3280 "a key with no item resolves to no selection"
3281 );
3282 }
3283
3284 #[test]
3290 fn secondary_trigger_and_menu_rows_use_the_pinned_hover_tokens() {
3291 let source = include_str!("select.rs")
3294 .split("#[cfg(test)]")
3295 .next()
3296 .expect("the implementation section is always present");
3297 assert!(
3298 source.contains("FieldVariant::Secondary => colors.default.hover()"),
3299 "the secondary trigger hover must read `colors.default.hover()` \
3300 (pinned `--select-trigger-bg-hover: var(--default-hover)`)"
3301 );
3302 assert!(
3303 source
3304 .contains("let row_hover_bg = self.row_hover_bg.unwrap_or(colors.default.color);"),
3305 "the popup rows must hover the full `bg-default` \
3306 (pinned `.list-box-item:hover`)"
3307 );
3308 assert!(
3309 !source.contains("soft_hover()") && !source.contains("default.soft()"),
3310 "no select surface may hover a soft token"
3311 );
3312 }
3313
3314 #[test]
3315 fn trigger_hover_uses_the_pinned_smooth_transition() {
3316 let source = include_str!("select.rs")
3317 .split("#[cfg(test)]")
3318 .next()
3319 .expect("the implementation section is always present");
3320 assert!(
3321 source.contains("hover_fade_with_duration_and_easing_suppressed")
3322 && source.contains("Some(150)")
3323 && source.contains("HoverFadeEasing::EaseSmooth")
3324 && source.contains("|fill| fill.rounded(trigger_radius)"),
3325 "Select trigger hover must animate with the pinned 150ms smooth \
3326 transition and preserve the trigger radius"
3327 );
3328 assert!(
3329 source.contains("!field_box.is_bare && !self.is_invalid && !trigger_focused"),
3330 "invalid and focused triggers must keep ownership of their chrome"
3331 );
3332 }
3333
3334 #[test]
3335 fn option_rows_use_the_pinned_subtle_press_transition() {
3336 let source = include_str!("select.rs")
3337 .split("#[cfg(test)]")
3338 .next()
3339 .expect("the implementation section is always present");
3340 assert!(
3341 source.contains("crate::anim::pressed_with_background_ramp(")
3342 && source.contains("crate::anim::LIST_ITEM_PRESS")
3343 && source.contains("PRESSED_SCALE_SUBTLE")
3344 && source.contains("radius: util::soft_radius(cx)"),
3345 "Select options must use the shared 98% quart press ramp and keep their row radius"
3346 );
3347 }
3348}
3349
3350crate::util::impl_component_styled!(Select);