1use crate::{
15 icons,
16 input::{FieldEvent, TextField},
17 keys, popover, scroll,
18 tooltip::Tooltip,
19};
20use gpui::{
21 Action, App, Axis, Context, Entity, Focusable as _, Global, KeyBinding, MouseDownEvent, Pixels,
22 Point, ScrollHandle, SharedString, Size, Subscription, Window, actions, div, prelude::*, px,
23};
24use icons::Icon;
25use std::{cell::Cell, rc::Rc};
26use theme::{TextStyle, Theme, Typeset};
27
28const GLYPH: f32 = 13.0;
30
31const SEGMENT_GLYPH: f32 = 16.0;
33
34const SEARCH_ROWS: f32 = 10.0;
36
37actions!(
38 bezel_menu,
39 [
40 SelectNext,
41 SelectPrevious,
42 SelectChild,
43 SelectParent,
44 Confirm,
45 Dismiss
46 ]
47);
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct MenuIndent(pub Pixels);
52
53impl Default for MenuIndent {
54 fn default() -> Self {
55 Self(px(12.0))
56 }
57}
58
59impl Global for MenuIndent {}
60
61pub(crate) fn indent(cx: &App) -> MenuIndent {
62 cx.try_global::<MenuIndent>().copied().unwrap_or_default()
63}
64
65pub(crate) fn set_indent(indent: MenuIndent, cx: &mut App) {
67 cx.set_global(indent);
68 cx.refresh_windows();
69}
70
71#[derive(Clone, Copy, Debug, PartialEq)]
73pub struct MenuWidth {
74 pub min: Pixels,
77 pub described: Described,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq)]
84pub enum Described {
85 Fixed(Pixels),
87 Fit(Pixels),
89}
90
91impl Default for MenuWidth {
92 fn default() -> Self {
93 Self {
94 min: px(180.0),
95 described: Described::Fixed(px(280.0)),
96 }
97 }
98}
99
100impl Global for MenuWidth {}
101
102pub(crate) fn width(cx: &App) -> MenuWidth {
103 cx.try_global::<MenuWidth>().copied().unwrap_or_default()
104}
105
106pub(crate) fn set_width(width: MenuWidth, cx: &mut App) {
108 cx.set_global(width);
109 cx.refresh_windows();
110}
111
112pub const SEARCH_CONTEXT: &str = "MenuSearch";
115
116pub fn bindings() -> Vec<KeyBinding> {
119 let ctx = Some(SEARCH_CONTEXT);
120 let context = Some(crate::context_menu::KEY_CONTEXT);
121 vec![
122 KeyBinding::new("down", SelectNext, ctx),
123 KeyBinding::new("up", SelectPrevious, ctx),
124 KeyBinding::new("ctrl-n", SelectNext, ctx),
125 KeyBinding::new("ctrl-p", SelectPrevious, ctx),
126 KeyBinding::new("enter", Confirm, ctx),
127 KeyBinding::new("escape", Dismiss, ctx),
128 KeyBinding::new("down", SelectNext, context),
129 KeyBinding::new("up", SelectPrevious, context),
130 KeyBinding::new("right", SelectChild, context),
131 KeyBinding::new("left", SelectParent, context),
132 KeyBinding::new("enter", Confirm, context),
133 KeyBinding::new("escape", Dismiss, context),
134 ]
135}
136
137pub fn init(cx: &mut App) {
139 cx.bind_keys(bindings());
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
148pub enum Item {
149 Action {
150 label: SharedString,
151 description: Option<SharedString>,
158 inline: bool,
162 tooltip: Option<SharedString>,
167 icon: Option<Icon>,
170 indent: usize,
172 keystroke: Option<SharedString>,
177 checked: bool,
179 enabled: bool,
180 },
181 Submenu {
185 label: SharedString,
186 icon: Option<Icon>,
187 indent: usize,
188 enabled: bool,
189 items: Vec<Item>,
190 search: Option<SharedString>,
195 },
196 Segmented {
200 segments: Vec<Segment>,
201 selected: usize,
202 enabled: bool,
203 },
204 Heading(SharedString),
206 Note(SharedString),
209 Separator,
210}
211
212#[derive(Clone, Debug, PartialEq, Eq)]
214pub struct Segment {
215 pub icon: Icon,
216 pub label: SharedString,
217}
218
219impl Segment {
220 pub fn new(icon: impl Into<Icon>, label: impl Into<SharedString>) -> Self {
221 Self {
222 icon: icon.into(),
223 label: label.into(),
224 }
225 }
226}
227
228impl Item {
229 pub fn action(label: impl Into<SharedString>) -> Self {
230 Item::Action {
231 label: label.into(),
232 description: None,
233 inline: false,
234 tooltip: None,
235 icon: None,
236 indent: 0,
237 keystroke: None,
238 checked: false,
239 enabled: true,
240 }
241 }
242
243 pub fn submenu(label: impl Into<SharedString>, items: Vec<Item>) -> Self {
244 Item::Submenu {
245 label: label.into(),
246 icon: None,
247 indent: 0,
248 enabled: true,
249 items,
250 search: None,
251 }
252 }
253
254 pub fn searchable(mut self, placeholder: impl Into<SharedString>) -> Self {
257 if let Item::Submenu { search, .. } = &mut self {
258 *search = Some(placeholder.into());
259 }
260 self
261 }
262
263 pub fn segmented(segments: impl IntoIterator<Item = Segment>, selected: usize) -> Self {
264 Item::Segmented {
265 segments: segments.into_iter().collect(),
266 selected,
267 enabled: true,
268 }
269 }
270
271 pub fn with_icon(mut self, icon: impl Into<Icon>) -> Self {
274 match &mut self {
275 Item::Action { icon: slot, .. } | Item::Submenu { icon: slot, .. } => {
276 *slot = Some(icon.into())
277 }
278 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {}
279 }
280 self
281 }
282
283 pub fn indented(mut self, level: usize) -> Self {
287 match &mut self {
288 Item::Action { indent, .. } | Item::Submenu { indent, .. } => *indent = level,
289 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {}
290 }
291 self
292 }
293
294 pub fn with_description(mut self, description: impl Into<SharedString>) -> Self {
298 if let Item::Action {
299 description: slot, ..
300 } = &mut self
301 {
302 *slot = Some(description.into());
303 }
304 self
305 }
306
307 pub fn with_inline_description(mut self, description: impl Into<SharedString>) -> Self {
311 if let Item::Action {
312 description: slot,
313 inline,
314 ..
315 } = &mut self
316 {
317 *slot = Some(description.into());
318 *inline = true;
319 }
320 self
321 }
322
323 pub fn with_long_description(self, description: impl Into<SharedString>) -> Self {
327 let description = description.into();
328 self.with_description(description.clone())
329 .with_tooltip(description)
330 }
331
332 pub fn with_tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
337 if let Item::Action { tooltip: slot, .. } = &mut self {
338 *slot = Some(tooltip.into());
339 }
340 self
341 }
342
343 pub fn with_keystroke(mut self, keystroke: impl Into<SharedString>) -> Self {
346 if let Item::Action {
347 keystroke: slot, ..
348 } = &mut self
349 {
350 *slot = Some(keystroke.into());
351 }
352 self
353 }
354
355 pub fn with_shortcut(self, action: &dyn Action, window: &Window) -> Self {
363 match keys::shortcut(action, window) {
364 Some(keystroke) => self.with_keystroke(keystroke),
365 None => self,
366 }
367 }
368
369 pub fn with_shortcut_in(self, action: &dyn Action, context: &str, window: &Window) -> Self {
374 match keys::shortcut_in(action, context, window) {
375 Some(keystroke) => self.with_keystroke(keystroke),
376 None => self,
377 }
378 }
379
380 pub fn checked(mut self, checked: bool) -> Self {
383 if let Item::Action { checked: slot, .. } = &mut self {
384 *slot = checked;
385 }
386 self
387 }
388
389 pub fn disabled(mut self, disabled: bool) -> Self {
390 match &mut self {
391 Item::Action { enabled, .. }
392 | Item::Submenu { enabled, .. }
393 | Item::Segmented { enabled, .. } => *enabled = !disabled,
394 Item::Heading(_) | Item::Note(_) | Item::Separator => {}
395 }
396 self
397 }
398
399 pub fn selectable(&self) -> bool {
403 match self {
404 Item::Action { enabled, .. } => *enabled,
405 Item::Submenu { enabled, items, .. } => *enabled && items.iter().any(Item::selectable),
406 Item::Segmented {
407 enabled, segments, ..
408 } => *enabled && !segments.is_empty(),
409 Item::Heading(_) | Item::Note(_) | Item::Separator => false,
410 }
411 }
412
413 fn has_description(&self) -> bool {
414 matches!(
415 self,
416 Item::Action {
417 description: Some(_),
418 ..
419 }
420 )
421 }
422
423 fn has_icon(&self) -> bool {
424 matches!(
425 self,
426 Item::Action { icon: Some(_), .. } | Item::Submenu { icon: Some(_), .. }
427 )
428 }
429
430 fn landing(&self) -> Option<usize> {
432 match self {
433 Item::Segmented {
434 segments, selected, ..
435 } if !segments.is_empty() => Some((*selected).min(segments.len() - 1)),
436 _ => None,
437 }
438 }
439
440 fn keywords(&self) -> String {
442 match self {
443 Item::Action {
444 label, description, ..
445 } => match description {
446 Some(description) => format!("{label} {description}"),
447 None => label.to_string(),
448 },
449 Item::Submenu { label, .. } => label.to_string(),
450 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {
451 String::new()
452 }
453 }
454 }
455
456 fn search(&self) -> Option<&SharedString> {
458 match self {
459 Item::Submenu { search, .. } => search.as_ref(),
460 _ => None,
461 }
462 }
463
464 fn opens(&self) -> Option<&[Item]> {
466 match self {
467 Item::Submenu {
468 enabled: true,
469 items,
470 ..
471 } => Some(items),
472 _ => None,
473 }
474 }
475}
476
477pub fn at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a Item> {
481 let (&row, above) = path.split_last()?;
482 items_at(items, above)?.get(row)
483}
484
485pub fn items_at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a [Item]> {
489 let mut level = items;
490 for &row in path {
491 level = level.get(row)?.opens()?;
492 }
493 Some(level)
494}
495
496fn open_depth(items: &[Item], open: &[usize]) -> usize {
499 let mut level = items;
500 for (depth, &row) in open.iter().enumerate() {
501 match level.get(row).and_then(Item::opens) {
502 Some(inner) => level = inner,
503 None => return depth,
504 }
505 }
506 open.len()
507}
508
509pub fn next_selectable(items: &[Item], from: Option<usize>, delta: isize) -> Option<usize> {
517 let count = items.len();
518 if count == 0 {
519 return None;
520 }
521 let step = if delta >= 0 { 1 } else { -1 };
522 let wrap = |at: usize| (at as isize + step).rem_euclid(count as isize) as usize;
523 let mut at = match from {
526 None if step > 0 => 0,
527 None => count - 1,
528 Some(at) => wrap(at.min(count - 1)),
529 };
530 for _ in 0..count {
531 if items[at].selectable() {
532 return Some(at);
533 }
534 at = wrap(at);
535 }
536 None
537}
538
539#[derive(Clone, Debug, Default, PartialEq, Eq)]
551pub struct Cursor {
552 open: Vec<usize>,
553 row: Option<usize>,
554 segment: Option<usize>,
555}
556
557impl Cursor {
558 pub fn open(&self) -> &[usize] {
560 &self.open
561 }
562
563 pub fn row(&self) -> Option<usize> {
565 self.row
566 }
567
568 pub fn segment(&self) -> Option<usize> {
570 self.segment
571 }
572
573 pub fn nested(&self) -> bool {
576 !self.open.is_empty()
577 }
578
579 pub fn path(&self) -> Option<Vec<usize>> {
582 let row = self.row?;
583 let mut path = self.open.clone();
584 path.push(row);
585 path.extend(self.segment);
586 Some(path)
587 }
588
589 pub fn clear(&mut self) {
591 self.open.clear();
592 self.row = None;
593 self.segment = None;
594 }
595
596 pub fn lit(&self, depth: usize) -> Option<usize> {
599 match depth.cmp(&self.open.len()) {
600 std::cmp::Ordering::Less => Some(self.open[depth]),
601 std::cmp::Ordering::Equal => self.row,
602 std::cmp::Ordering::Greater => None,
603 }
604 }
605
606 pub fn step(&mut self, root: &[Item], delta: isize) {
608 let Some(items) = items_at(root, &self.open) else {
609 return;
610 };
611 self.row = next_selectable(items, self.row, delta);
612 self.segment = self.row.and_then(|row| items[row].landing());
613 }
614
615 pub fn slide(&mut self, root: &[Item], delta: isize) -> bool {
618 let Some(Item::Segmented { segments, .. }) = self
619 .row
620 .and_then(|row| items_at(root, &self.open)?.get(row))
621 else {
622 return false;
623 };
624 let at = self.segment.unwrap_or(0) as isize + delta.signum();
625 self.segment = Some(at.clamp(0, segments.len() as isize - 1) as usize);
626 true
627 }
628
629 pub fn descend(&mut self, root: &[Item]) -> bool {
633 let Some(row) = self.row else { return false };
634 let Some(inner) = items_at(root, &self.open)
635 .and_then(|items| items.get(row))
636 .and_then(Item::opens)
637 else {
638 return false;
639 };
640 self.row = next_selectable(inner, None, 1);
641 self.segment = self.row.and_then(|row| inner[row].landing());
642 self.open.push(row);
643 true
644 }
645
646 pub fn ascend(&mut self) -> bool {
649 match self.open.pop() {
650 Some(row) => {
651 self.row = Some(row);
652 self.segment = None;
653 true
654 }
655 None => false,
656 }
657 }
658
659 pub fn point_at(&mut self, root: &[Item], path: &[usize]) -> bool {
667 let (open, row, segment) = match path {
668 [] => (Vec::new(), None, None),
669 [above @ .., row, segment]
670 if matches!(
671 at(root, &path[..path.len() - 1]),
672 Some(Item::Segmented { .. })
673 ) =>
674 {
675 (above.to_vec(), Some(*row), Some(*segment))
676 }
677 _ if at(root, path).and_then(Item::opens).is_some() => (path.to_vec(), None, None),
678 [above @ .., row] => (
679 above.to_vec(),
680 Some(*row),
681 at(root, path).and_then(Item::landing),
682 ),
683 };
684 if self.open == open && self.row == row && self.segment == segment {
685 return false;
686 }
687 self.open = open;
688 self.row = row;
689 self.segment = segment;
690 true
691 }
692}
693
694#[derive(Clone, Debug, PartialEq, Eq)]
701pub enum Hit {
702 Point(Vec<usize>),
705 Choose(Vec<usize>),
707 Dismiss,
711}
712
713#[derive(Clone, Copy, Debug, PartialEq, Eq)]
716pub enum Edit {
717 Cut,
718 Copy,
719 Paste,
720 SelectAll,
721}
722
723impl Edit {
724 pub fn at(path: &[usize]) -> Option<Self> {
726 match path {
727 [0] => Some(Edit::Cut),
728 [1] => Some(Edit::Copy),
729 [2] => Some(Edit::Paste),
730 [4] => Some(Edit::SelectAll),
731 _ => None,
732 }
733 }
734}
735
736pub fn edit_items(
739 selected: bool,
740 pasteable: bool,
741 actions: [&dyn Action; 4],
742 window: &Window,
743) -> Vec<Item> {
744 let [cut, copy, paste, select_all] = actions;
745 let enable = |item: Item, on: bool| item.disabled(!on);
746 vec![
747 enable(Item::action("Cut").with_shortcut(cut, window), selected),
748 enable(Item::action("Copy").with_shortcut(copy, window), selected),
749 enable(
750 Item::action("Paste").with_shortcut(paste, window),
751 pasteable,
752 ),
753 Item::Separator,
754 Item::action("Select All").with_shortcut(select_all, window),
755 ]
756}
757
758#[allow(clippy::too_many_arguments)]
780pub fn card<V: 'static>(
781 theme: &Theme,
782 id: impl Into<SharedString>,
783 items: &[Item],
784 cursor: &Cursor,
785 closing: Option<web_time::Instant>,
786 window: &mut Window,
787 cx: &mut Context<V>,
788 on: impl Fn(&mut V, Hit, &mut Window, &mut Context<V>) + 'static,
789) -> gpui::Div {
790 let tree = Tree {
791 id: id.into(),
792 closing,
793 panels: 1 + open_depth(items, cursor.open()),
794 outside: Rc::new(Cell::new((None, 0))),
795 chain: popover::Chain::default(),
796 on: Rc::new(on),
797 };
798 tree.panel(theme, items, cursor, &[], None, window, cx)
799}
800
801struct Query {
805 field: Entity<TextField>,
806 focused: bool,
807 _changed: Subscription,
808}
809
810type Listener<E> = Box<dyn Fn(&E, &mut Window, &mut gpui::App)>;
813
814type Reporter<V> = Rc<dyn Fn(&mut V, Hit, &mut Window, &mut Context<V>)>;
816
817struct Tree<V: 'static> {
819 id: SharedString,
820 panels: usize,
822 outside: Rc<Cell<(Option<Point<Pixels>>, usize)>>,
826 chain: popover::Chain,
828 closing: Option<web_time::Instant>,
830 on: Reporter<V>,
831}
832
833impl<V: 'static> Tree<V> {
834 #[allow(clippy::too_many_arguments)]
835 fn panel(
836 &self,
837 theme: &Theme,
838 items: &[Item],
839 cursor: &Cursor,
840 prefix: &[usize],
841 search: Option<&SharedString>,
842 window: &mut Window,
843 cx: &mut Context<V>,
844 ) -> gpui::Div {
845 let depth = prefix.len();
846 let lit = cursor.lit(depth);
847 let down = cursor.open().get(depth).copied();
848 let rows_id = SharedString::from(format!("{}-rows", row_id(&self.id, prefix)));
849 let Scrolled(handle, shown) = window
850 .use_keyed_state(
851 SharedString::from(format!("{rows_id}-scrolled")),
852 cx,
853 |_, _| Scrolled::default(),
854 )
855 .read(cx)
856 .clone();
857 let query = search.map(|placeholder| self.query(&rows_id, placeholder, window, cx));
858 let text = query
859 .as_ref()
860 .map(|query| query.read(cx).field.read(cx).content().trim().to_owned())
861 .unwrap_or_default();
862 let visible: Vec<usize> = if text.is_empty() {
865 (0..items.len()).collect()
866 } else {
867 let words: Vec<String> = items.iter().map(Item::keywords).collect();
868 popover::filter_indices(&text, &words)
869 .into_iter()
870 .filter(|&row| items[row].selectable())
871 .collect()
872 };
873 let size = handle.bounds().size;
878 if size.height > Pixels::ZERO && shown.get() != Some((lit, size)) {
879 shown.set(Some((lit, size)));
880 if let Some(at) = lit.and_then(|lit| visible.iter().position(|&row| row == lit)) {
881 handle.scroll_to_item(at);
882 }
883 }
884 let inset = px(popover::SNAP) + window.client_inset().unwrap_or(Pixels::ZERO);
885 let cap = window.viewport_size().height - inset * 2.0;
886 let gutter = items.iter().any(Item::has_icon);
889 let MenuIndent(step) = indent(cx);
890 let described = items.iter().any(Item::has_description);
891 let width = width(cx);
892 let rows = div()
893 .id(rows_id.clone())
894 .p(px(popover::MENU_PAD))
895 .children(visible.iter().map(|&row| {
896 let item = &items[row];
897 match item {
898 Item::Separator => return popover::divider(theme).into_any_element(),
899 Item::Heading(label) => {
900 return popover::menu_heading(theme, label.clone()).into_any_element();
901 }
902 Item::Note(text) => {
903 return popover::menu_note(theme, text.clone()).into_any_element();
904 }
905 _ => {}
906 }
907 let path: Vec<usize> = prefix.iter().copied().chain([row]).collect();
908 if let Item::Segmented {
909 segments,
910 selected,
911 enabled,
912 } = item
913 {
914 let live = cursor.segment().filter(|_| lit == Some(row));
915 return self
916 .segmented(theme, segments, *selected, *enabled, live, &path, cx)
917 .into_any_element();
918 }
919 let id = row_id(&self.id, &path);
920 let (label, icon, indent, enabled) = match item {
921 Item::Action {
922 label,
923 icon,
924 indent,
925 enabled,
926 ..
927 }
928 | Item::Submenu {
929 label,
930 icon,
931 indent,
932 enabled,
933 ..
934 } => (label.clone(), icon.clone(), *indent, *enabled),
935 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {
936 unreachable!("returned above")
937 }
938 };
939 let (description, inline, hint) = match item {
940 Item::Action {
941 description,
942 inline,
943 tooltip,
944 ..
945 } => (description.clone(), *inline, tooltip.clone()),
946 _ => (None, false, None),
947 };
948 let row = if enabled {
949 popover::menu_row(theme, lit == Some(row), None)
950 .id(id.clone())
951 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
952 .on_click(self.reports(
953 match item {
954 Item::Submenu { .. } => Hit::Point(path.clone()),
957 _ => Hit::Choose(path.clone()),
958 },
959 cx,
960 ))
961 } else {
962 disabled_row(theme).id(id.clone())
963 };
964 let selector = format!("{}/{label}", self.id);
965 let row = row.debug_selector(move || selector);
966 let row = row.when_some(hint, |row, hint| {
967 row.tooltip(move |window, cx| Tooltip::text(hint.clone(), window, cx))
968 });
969 row.when(indent > 0, |row| {
970 row.pl(px(popover::MENU_ROW_INSET) + step * indent as f32)
971 })
972 .when(gutter, |row| row.child(glyph_slot(theme, icon, enabled)))
973 .child(
974 div()
975 .flex_1()
976 .min_w_0()
977 .flex()
978 .when(!inline, |text| text.flex_col())
979 .when(inline, |text| text.flex_row().items_baseline().gap(px(8.0)))
980 .child(
981 div()
982 .truncate()
983 .when(inline, |label| label.flex_none().max_w_full())
984 .child(label),
985 )
986 .children(description.map(|description| {
987 description_line(theme, description, enabled)
988 .when(inline, |line| line.mt_0().min_w_0())
989 })),
990 )
991 .map(|row| match item {
992 Item::Action {
993 keystroke, checked, ..
994 } => row
995 .when(*checked, |row| {
996 row.child(
997 icons::icon(icons::glyph::Check)
998 .size(px(GLYPH))
999 .text_color(theme.text),
1000 )
1001 })
1002 .when_some(keystroke.clone(), |row, keystroke| {
1003 row.child(popover::kbd_hint(theme, &keystroke))
1004 }),
1005 _ => row.child(
1006 icons::icon(icons::glyph::ChevronRight)
1007 .size(px(GLYPH))
1008 .text_color(theme.text_faint),
1009 ),
1010 })
1011 .when_some(
1012 item.opens().filter(|_| down == Some(path[depth])),
1013 |parent, inner| {
1014 let panel = self
1015 .panel(theme, inner, cursor, &path, item.search(), window, cx)
1016 .into_any_element();
1017 parent.relative().child(popover::anchored_submenu(
1018 SharedString::from(format!("{id}-panel")),
1019 panel,
1020 &self.chain,
1021 self.closing,
1022 ))
1023 },
1024 )
1025 .into_any_element()
1026 }));
1027 popover::popover_card(theme)
1028 .p_0()
1029 .flex()
1030 .flex_col()
1031 .max_h(cap)
1032 .map(|card| match (described, width.described) {
1033 (true, Described::Fixed(wide)) => card.w(wide),
1034 (true, Described::Fit(most)) => card.min_w(width.min).max_w(most),
1035 (false, _) => card.min_w(width.min),
1036 })
1037 .when(self.closing.is_none(), |card| {
1038 card.on_mouse_down_out(self.dismissal(cx))
1039 })
1040 .map(|card| match query {
1041 None => card.child(
1042 scroll::Viewport::new(rows_id, rows, Axis::Vertical)
1043 .track_scroll(&handle)
1044 .fill(),
1045 ),
1046 Some(query) => {
1047 let field = query.read(cx).field.clone();
1048 let rows = rows.max_h(px(
1049 SEARCH_ROWS * popover::menu_row_height() + 2.0 * popover::MENU_PAD
1050 ));
1051 card.child(
1052 div()
1053 .key_context(SEARCH_CONTEXT)
1054 .px(px(popover::MENU_PAD))
1055 .pt(px(popover::MENU_PAD))
1056 .map(|el| self.keys(el, items, &visible, lit, prefix, cx))
1057 .child(popover::search_line(theme, field.into_any_element())),
1058 )
1059 .child(match visible.is_empty() {
1060 true => div()
1061 .px(px(popover::MENU_PAD + popover::MENU_ROW_INSET))
1062 .pb(px(8.0))
1063 .text_style(TextStyle::Body)
1064 .text_color(theme.text_muted)
1065 .child("No matches")
1066 .into_any_element(),
1067 false => scroll::Viewport::new(rows_id, rows, Axis::Vertical)
1068 .track_scroll(&handle)
1069 .into_any_element(),
1070 })
1071 }
1072 })
1073 }
1074
1075 fn query(
1078 &self,
1079 key: &SharedString,
1080 placeholder: &SharedString,
1081 window: &mut Window,
1082 cx: &mut Context<V>,
1083 ) -> Entity<Query> {
1084 let host = cx.entity_id();
1085 let placeholder = placeholder.clone();
1086 let query = window.use_keyed_state(
1087 SharedString::from(format!("{key}-query")),
1088 cx,
1089 move |_, cx| {
1090 let field = cx.new(|cx| crate::search::query_field(placeholder, cx));
1091 let _changed = cx.subscribe(&field, move |_, _, _: &FieldEvent, cx| {
1092 let app: &mut App = cx;
1093 app.notify(host);
1094 });
1095 Query {
1096 field,
1097 focused: false,
1098 _changed,
1099 }
1100 },
1101 );
1102 if !query.read(cx).focused {
1103 query.update(cx, |query, _| query.focused = true);
1104 let handle = query.read(cx).field.focus_handle(cx);
1105 window.focus(&handle, cx);
1106 }
1107 query
1108 }
1109
1110 fn keys(
1113 &self,
1114 el: gpui::Div,
1115 items: &[Item],
1116 shown: &[usize],
1117 lit: Option<usize>,
1118 prefix: &[usize],
1119 cx: &mut Context<V>,
1120 ) -> gpui::Div {
1121 let landable: Rc<Vec<usize>> = Rc::new(
1122 shown
1123 .iter()
1124 .copied()
1125 .filter(|&row| items[row].selectable())
1126 .collect(),
1127 );
1128 let at = lit.and_then(|lit| landable.iter().position(|&row| row == lit));
1129 let path = {
1130 let prefix = prefix.to_vec();
1131 move |row: usize| prefix.iter().copied().chain([row]).collect::<Vec<_>>()
1132 };
1133 let step = |delta: isize| {
1134 let count = landable.len() as isize;
1135 (count > 0).then(|| {
1136 let next = match at {
1137 Some(at) => (at as isize + delta).rem_euclid(count),
1138 None if delta > 0 => 0,
1139 None => count - 1,
1140 };
1141 landable[next as usize]
1142 })
1143 };
1144 let next = step(1).map(&path);
1145 let previous = step(-1).map(&path);
1146 let chosen = at
1147 .map(|at| landable[at])
1148 .or_else(|| landable.first().copied())
1149 .map(|row| match items[row].opens() {
1150 Some(_) => Hit::Point(path(row)),
1151 None => Hit::Choose(path(row)),
1152 });
1153 let on = self.on.clone();
1154 let report = move |hit: Option<Hit>| {
1155 let on = on.clone();
1156 move |view: &mut V, window: &mut Window, cx: &mut Context<V>| {
1157 if let Some(hit) = hit.clone() {
1158 on(view, hit, window, cx);
1159 }
1160 }
1161 };
1162 let (down, up, enter, escape) = (
1163 report(next.map(Hit::Point)),
1164 report(previous.map(Hit::Point)),
1165 report(chosen),
1166 report(Some(Hit::Dismiss)),
1167 );
1168 el.on_action(cx.listener(move |view, _: &SelectNext, window, cx| down(view, window, cx)))
1169 .on_action(
1170 cx.listener(move |view, _: &SelectPrevious, window, cx| up(view, window, cx)),
1171 )
1172 .on_action(cx.listener(move |view, _: &Confirm, window, cx| enter(view, window, cx)))
1173 .on_action(cx.listener(move |view, _: &Dismiss, window, cx| escape(view, window, cx)))
1174 }
1175
1176 #[allow(clippy::too_many_arguments)]
1179 fn segmented(
1180 &self,
1181 theme: &Theme,
1182 segments: &[Segment],
1183 selected: usize,
1184 enabled: bool,
1185 live: Option<usize>,
1186 path: &[usize],
1187 cx: &mut Context<V>,
1188 ) -> gpui::Stateful<gpui::Div> {
1189 div()
1190 .id(row_id(&self.id, path))
1191 .flex()
1192 .flex_row()
1193 .gap(px(popover::MENU_PAD))
1194 .children(segments.iter().enumerate().map(|(at, segment)| {
1195 let path: Vec<usize> = path.iter().copied().chain([at]).collect();
1196 let chosen = at == selected;
1197 let ink = match (enabled, chosen || live == Some(at)) {
1198 (false, _) => theme.text_faint.opacity(0.5),
1199 (true, true) => theme.text,
1200 (true, false) => theme.text_muted,
1201 };
1202 let cell = div()
1203 .id(row_id(&self.id, &path))
1204 .flex_1()
1205 .min_w_0()
1206 .flex()
1207 .flex_col()
1208 .items_center()
1209 .gap(px(4.0))
1210 .px(px(8.0))
1211 .py(px(6.0))
1212 .rounded(px(Theme::inset_radius(
1213 Theme::surface_radius(),
1214 popover::MENU_PAD,
1215 )))
1216 .text_style(TextStyle::Callout)
1217 .text_color(ink)
1218 .when(chosen, |cell| {
1219 cell.bg(theme.element_active)
1220 .font_weight(gpui::FontWeight::MEDIUM)
1221 })
1222 .when(!chosen && live == Some(at), |cell| {
1223 cell.bg(theme.card_selected_bg())
1224 })
1225 .child(
1226 icons::icon(segment.icon.clone())
1227 .size(px(SEGMENT_GLYPH))
1228 .text_color(ink),
1229 )
1230 .child(div().max_w_full().truncate().child(segment.label.clone()));
1231 match enabled {
1232 true => cell
1233 .cursor_pointer()
1234 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
1235 .on_click(self.reports(Hit::Choose(path), cx)),
1236 false => cell,
1237 }
1238 }))
1239 }
1240
1241 fn dismissal(&self, cx: &mut Context<V>) -> Listener<MouseDownEvent> {
1253 let outside = self.outside.clone();
1254 let panels = self.panels;
1255 let on = self.on.clone();
1256 Box::new(
1257 cx.listener(move |view, event: &MouseDownEvent, window, cx| {
1258 let (at, count) = outside.get();
1259 let count = if at == Some(event.position) {
1260 count + 1
1261 } else {
1262 1
1263 };
1264 outside.set((Some(event.position), count));
1265 if count >= panels {
1266 outside.set((None, 0));
1267 on(view, Hit::Dismiss, window, cx);
1268 cx.stop_propagation();
1269 }
1270 }),
1271 )
1272 }
1273
1274 fn reports<E: 'static>(&self, hit: Hit, cx: &mut Context<V>) -> Listener<E> {
1276 let on = self.on.clone();
1277 Box::new(cx.listener(move |view, _: &E, window, cx| on(view, hit.clone(), window, cx)))
1278 }
1279}
1280
1281#[derive(Clone, Default)]
1284struct Scrolled(ScrollHandle, Rc<Cell<Option<Shown>>>);
1285
1286type Shown = (Option<usize>, Size<Pixels>);
1287
1288fn row_id(id: &SharedString, path: &[usize]) -> SharedString {
1291 let mut out = id.to_string();
1292 for row in path {
1293 out.push('-');
1294 out.push_str(&row.to_string());
1295 }
1296 SharedString::from(out)
1297}
1298
1299pub fn glyph_slot(theme: &Theme, icon: Option<Icon>, enabled: bool) -> gpui::Div {
1302 div()
1303 .flex_none()
1304 .size(px(GLYPH))
1305 .flex()
1306 .items_center()
1307 .justify_center()
1308 .children(icon.map(|glyph| {
1309 icons::icon(glyph).size(px(GLYPH)).text_color(if enabled {
1310 theme.text_faint
1311 } else {
1312 theme.text_faint.opacity(0.5)
1313 })
1314 }))
1315}
1316
1317fn description_line(theme: &Theme, description: SharedString, enabled: bool) -> gpui::Div {
1331 div()
1332 .mt(px(2.0))
1333 .truncate()
1334 .text_style(TextStyle::Subheadline)
1335 .text_color(if enabled {
1336 theme.text_muted
1337 } else {
1338 theme.text_faint.opacity(0.5)
1341 })
1342 .child(description)
1343}
1344
1345fn disabled_row(theme: &Theme) -> gpui::Div {
1349 div()
1350 .flex()
1351 .flex_row()
1352 .items_center()
1353 .gap(px(10.0))
1354 .px(px(8.0))
1355 .py(px(popover::MENU_ROW_PAD_Y))
1356 .rounded(px(Theme::inset_radius(
1359 Theme::surface_radius(),
1360 popover::MENU_PAD,
1361 )))
1362 .text_style(TextStyle::Body)
1363 .text_color(theme.text_faint)
1364}