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;
36const DEFAULT_ROWS: usize = 12;
37
38actions!(
39 bezel_menu,
40 [
41 SelectNext,
42 SelectPrevious,
43 SelectChild,
44 SelectParent,
45 Confirm,
46 Dismiss
47 ]
48);
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct MenuIndent(pub Pixels);
53
54impl Default for MenuIndent {
55 fn default() -> Self {
56 Self(px(12.0))
57 }
58}
59
60impl Global for MenuIndent {}
61
62pub(crate) fn indent(cx: &App) -> MenuIndent {
63 cx.try_global::<MenuIndent>().copied().unwrap_or_default()
64}
65
66pub(crate) fn set_indent(indent: MenuIndent, cx: &mut App) {
68 cx.set_global(indent);
69 cx.refresh_windows();
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub struct MenuHeight {
74 pub rows: usize,
75}
76
77impl Default for MenuHeight {
78 fn default() -> Self {
79 Self { rows: DEFAULT_ROWS }
80 }
81}
82
83impl MenuHeight {
84 pub fn limit(self, available: Pixels) -> Pixels {
85 px(self.rows.max(1) as f32 * popover::menu_row_height() + 2. * popover::MENU_PAD)
86 .min(available.max(Pixels::ZERO))
87 }
88}
89
90impl Global for MenuHeight {}
91
92pub(crate) fn height(cx: &App) -> MenuHeight {
93 cx.try_global::<MenuHeight>().copied().unwrap_or_default()
94}
95
96pub(crate) fn set_height(height: MenuHeight, cx: &mut App) {
97 cx.set_global(height);
98 cx.refresh_windows();
99}
100
101#[derive(Clone, Copy, Debug, PartialEq)]
103pub struct MenuWidth {
104 pub min: Pixels,
107 pub described: Described,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq)]
114pub enum Described {
115 Fixed(Pixels),
117 Fit(Pixels),
119}
120
121impl Default for MenuWidth {
122 fn default() -> Self {
123 Self {
124 min: px(180.0),
125 described: Described::Fixed(px(280.0)),
126 }
127 }
128}
129
130impl Global for MenuWidth {}
131
132pub(crate) fn width(cx: &App) -> MenuWidth {
133 cx.try_global::<MenuWidth>().copied().unwrap_or_default()
134}
135
136pub(crate) fn set_width(width: MenuWidth, cx: &mut App) {
138 cx.set_global(width);
139 cx.refresh_windows();
140}
141
142pub const SEARCH_CONTEXT: &str = "MenuSearch";
145
146pub fn bindings() -> Vec<KeyBinding> {
149 let ctx = Some(SEARCH_CONTEXT);
150 let context = Some(crate::context_menu::KEY_CONTEXT);
151 vec![
152 KeyBinding::new("down", SelectNext, ctx),
153 KeyBinding::new("up", SelectPrevious, ctx),
154 KeyBinding::new("ctrl-n", SelectNext, ctx),
155 KeyBinding::new("ctrl-p", SelectPrevious, ctx),
156 KeyBinding::new("enter", Confirm, ctx),
157 KeyBinding::new("escape", Dismiss, ctx),
158 KeyBinding::new("down", SelectNext, context),
159 KeyBinding::new("up", SelectPrevious, context),
160 KeyBinding::new("right", SelectChild, context),
161 KeyBinding::new("left", SelectParent, context),
162 KeyBinding::new("enter", Confirm, context),
163 KeyBinding::new("escape", Dismiss, context),
164 ]
165}
166
167pub fn init(cx: &mut App) {
169 cx.bind_keys(bindings());
170}
171
172#[derive(Clone, Debug, PartialEq, Eq)]
178pub enum Item {
179 Action {
180 label: SharedString,
181 description: Option<SharedString>,
188 inline: bool,
192 tooltip: Option<SharedString>,
197 icon: Option<Icon>,
200 indent: usize,
202 keystroke: Option<SharedString>,
207 checked: bool,
209 enabled: bool,
210 },
211 Submenu {
215 label: SharedString,
216 icon: Option<Icon>,
217 indent: usize,
218 enabled: bool,
219 items: Vec<Item>,
220 search: Option<SharedString>,
225 },
226 Segmented {
230 segments: Vec<Segment>,
231 selected: usize,
232 enabled: bool,
233 },
234 Heading(SharedString),
236 Note(SharedString),
239 Separator,
240}
241
242#[derive(Clone, Debug, PartialEq, Eq)]
244pub struct Segment {
245 pub icon: Icon,
246 pub label: SharedString,
247}
248
249impl Segment {
250 pub fn new(icon: impl Into<Icon>, label: impl Into<SharedString>) -> Self {
251 Self {
252 icon: icon.into(),
253 label: label.into(),
254 }
255 }
256}
257
258impl Item {
259 pub fn action(label: impl Into<SharedString>) -> Self {
260 Item::Action {
261 label: label.into(),
262 description: None,
263 inline: false,
264 tooltip: None,
265 icon: None,
266 indent: 0,
267 keystroke: None,
268 checked: false,
269 enabled: true,
270 }
271 }
272
273 pub fn submenu(label: impl Into<SharedString>, items: Vec<Item>) -> Self {
274 Item::Submenu {
275 label: label.into(),
276 icon: None,
277 indent: 0,
278 enabled: true,
279 items,
280 search: None,
281 }
282 }
283
284 pub fn searchable(mut self, placeholder: impl Into<SharedString>) -> Self {
287 if let Item::Submenu { search, .. } = &mut self {
288 *search = Some(placeholder.into());
289 }
290 self
291 }
292
293 pub fn segmented(segments: impl IntoIterator<Item = Segment>, selected: usize) -> Self {
294 Item::Segmented {
295 segments: segments.into_iter().collect(),
296 selected,
297 enabled: true,
298 }
299 }
300
301 pub fn with_icon(mut self, icon: impl Into<Icon>) -> Self {
304 match &mut self {
305 Item::Action { icon: slot, .. } | Item::Submenu { icon: slot, .. } => {
306 *slot = Some(icon.into())
307 }
308 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {}
309 }
310 self
311 }
312
313 pub fn indented(mut self, level: usize) -> Self {
317 match &mut self {
318 Item::Action { indent, .. } | Item::Submenu { indent, .. } => *indent = level,
319 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {}
320 }
321 self
322 }
323
324 pub fn with_description(mut self, description: impl Into<SharedString>) -> Self {
328 if let Item::Action {
329 description: slot, ..
330 } = &mut self
331 {
332 *slot = Some(description.into());
333 }
334 self
335 }
336
337 pub fn with_inline_description(mut self, description: impl Into<SharedString>) -> Self {
341 if let Item::Action {
342 description: slot,
343 inline,
344 ..
345 } = &mut self
346 {
347 *slot = Some(description.into());
348 *inline = true;
349 }
350 self
351 }
352
353 pub fn with_long_description(self, description: impl Into<SharedString>) -> Self {
357 let description = description.into();
358 self.with_description(description.clone())
359 .with_tooltip(description)
360 }
361
362 pub fn with_tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
367 if let Item::Action { tooltip: slot, .. } = &mut self {
368 *slot = Some(tooltip.into());
369 }
370 self
371 }
372
373 pub fn with_keystroke(mut self, keystroke: impl Into<SharedString>) -> Self {
376 if let Item::Action {
377 keystroke: slot, ..
378 } = &mut self
379 {
380 *slot = Some(keystroke.into());
381 }
382 self
383 }
384
385 pub fn with_shortcut(self, action: &dyn Action, window: &Window) -> Self {
393 match keys::shortcut(action, window) {
394 Some(keystroke) => self.with_keystroke(keystroke),
395 None => self,
396 }
397 }
398
399 pub fn with_shortcut_in(self, action: &dyn Action, context: &str, window: &Window) -> Self {
404 match keys::shortcut_in(action, context, window) {
405 Some(keystroke) => self.with_keystroke(keystroke),
406 None => self,
407 }
408 }
409
410 pub fn checked(mut self, checked: bool) -> Self {
413 if let Item::Action { checked: slot, .. } = &mut self {
414 *slot = checked;
415 }
416 self
417 }
418
419 pub fn disabled(mut self, disabled: bool) -> Self {
420 match &mut self {
421 Item::Action { enabled, .. }
422 | Item::Submenu { enabled, .. }
423 | Item::Segmented { enabled, .. } => *enabled = !disabled,
424 Item::Heading(_) | Item::Note(_) | Item::Separator => {}
425 }
426 self
427 }
428
429 pub fn selectable(&self) -> bool {
433 match self {
434 Item::Action { enabled, .. } => *enabled,
435 Item::Submenu { enabled, items, .. } => *enabled && items.iter().any(Item::selectable),
436 Item::Segmented {
437 enabled, segments, ..
438 } => *enabled && !segments.is_empty(),
439 Item::Heading(_) | Item::Note(_) | Item::Separator => false,
440 }
441 }
442
443 fn has_description(&self) -> bool {
444 matches!(
445 self,
446 Item::Action {
447 description: Some(_),
448 ..
449 }
450 )
451 }
452
453 fn has_icon(&self) -> bool {
454 matches!(
455 self,
456 Item::Action { icon: Some(_), .. } | Item::Submenu { icon: Some(_), .. }
457 )
458 }
459
460 fn landing(&self) -> Option<usize> {
462 match self {
463 Item::Segmented {
464 segments, selected, ..
465 } if !segments.is_empty() => Some((*selected).min(segments.len() - 1)),
466 _ => None,
467 }
468 }
469
470 fn keywords(&self) -> String {
472 match self {
473 Item::Action {
474 label, description, ..
475 } => match description {
476 Some(description) => format!("{label} {description}"),
477 None => label.to_string(),
478 },
479 Item::Submenu { label, .. } => label.to_string(),
480 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {
481 String::new()
482 }
483 }
484 }
485
486 fn search(&self) -> Option<&SharedString> {
488 match self {
489 Item::Submenu { search, .. } => search.as_ref(),
490 _ => None,
491 }
492 }
493
494 fn opens(&self) -> Option<&[Item]> {
496 match self {
497 Item::Submenu {
498 enabled: true,
499 items,
500 ..
501 } => Some(items),
502 _ => None,
503 }
504 }
505}
506
507pub fn at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a Item> {
511 let (&row, above) = path.split_last()?;
512 items_at(items, above)?.get(row)
513}
514
515pub fn items_at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a [Item]> {
519 let mut level = items;
520 for &row in path {
521 level = level.get(row)?.opens()?;
522 }
523 Some(level)
524}
525
526fn open_depth(items: &[Item], open: &[usize]) -> usize {
529 let mut level = items;
530 for (depth, &row) in open.iter().enumerate() {
531 match level.get(row).and_then(Item::opens) {
532 Some(inner) => level = inner,
533 None => return depth,
534 }
535 }
536 open.len()
537}
538
539pub fn next_selectable(items: &[Item], from: Option<usize>, delta: isize) -> Option<usize> {
547 let count = items.len();
548 if count == 0 {
549 return None;
550 }
551 let step = if delta >= 0 { 1 } else { -1 };
552 let wrap = |at: usize| (at as isize + step).rem_euclid(count as isize) as usize;
553 let mut at = match from {
556 None if step > 0 => 0,
557 None => count - 1,
558 Some(at) => wrap(at.min(count - 1)),
559 };
560 for _ in 0..count {
561 if items[at].selectable() {
562 return Some(at);
563 }
564 at = wrap(at);
565 }
566 None
567}
568
569#[derive(Clone, Debug, Default, PartialEq, Eq)]
581pub struct Cursor {
582 open: Vec<usize>,
583 row: Option<usize>,
584 segment: Option<usize>,
585}
586
587impl Cursor {
588 pub fn open(&self) -> &[usize] {
590 &self.open
591 }
592
593 pub fn row(&self) -> Option<usize> {
595 self.row
596 }
597
598 pub fn segment(&self) -> Option<usize> {
600 self.segment
601 }
602
603 pub fn nested(&self) -> bool {
606 !self.open.is_empty()
607 }
608
609 pub fn path(&self) -> Option<Vec<usize>> {
612 let row = self.row?;
613 let mut path = self.open.clone();
614 path.push(row);
615 path.extend(self.segment);
616 Some(path)
617 }
618
619 pub fn clear(&mut self) {
621 self.open.clear();
622 self.row = None;
623 self.segment = None;
624 }
625
626 pub fn lit(&self, depth: usize) -> Option<usize> {
629 match depth.cmp(&self.open.len()) {
630 std::cmp::Ordering::Less => Some(self.open[depth]),
631 std::cmp::Ordering::Equal => self.row,
632 std::cmp::Ordering::Greater => None,
633 }
634 }
635
636 pub fn step(&mut self, root: &[Item], delta: isize) {
638 let Some(items) = items_at(root, &self.open) else {
639 return;
640 };
641 self.row = next_selectable(items, self.row, delta);
642 self.segment = self.row.and_then(|row| items[row].landing());
643 }
644
645 pub fn slide(&mut self, root: &[Item], delta: isize) -> bool {
648 let Some(Item::Segmented { segments, .. }) = self
649 .row
650 .and_then(|row| items_at(root, &self.open)?.get(row))
651 else {
652 return false;
653 };
654 let at = self.segment.unwrap_or(0) as isize + delta.signum();
655 self.segment = Some(at.clamp(0, segments.len() as isize - 1) as usize);
656 true
657 }
658
659 pub fn descend(&mut self, root: &[Item]) -> bool {
663 let Some(row) = self.row else { return false };
664 let Some(inner) = items_at(root, &self.open)
665 .and_then(|items| items.get(row))
666 .and_then(Item::opens)
667 else {
668 return false;
669 };
670 self.row = next_selectable(inner, None, 1);
671 self.segment = self.row.and_then(|row| inner[row].landing());
672 self.open.push(row);
673 true
674 }
675
676 pub fn ascend(&mut self) -> bool {
679 match self.open.pop() {
680 Some(row) => {
681 self.row = Some(row);
682 self.segment = None;
683 true
684 }
685 None => false,
686 }
687 }
688
689 pub fn point_at(&mut self, root: &[Item], path: &[usize]) -> bool {
697 let (open, row, segment) = match path {
698 [] => (Vec::new(), None, None),
699 [above @ .., row, segment]
700 if matches!(
701 at(root, &path[..path.len() - 1]),
702 Some(Item::Segmented { .. })
703 ) =>
704 {
705 (above.to_vec(), Some(*row), Some(*segment))
706 }
707 _ if at(root, path).and_then(Item::opens).is_some() => (path.to_vec(), None, None),
708 [above @ .., row] => (
709 above.to_vec(),
710 Some(*row),
711 at(root, path).and_then(Item::landing),
712 ),
713 };
714 if self.open == open && self.row == row && self.segment == segment {
715 return false;
716 }
717 self.open = open;
718 self.row = row;
719 self.segment = segment;
720 true
721 }
722}
723
724#[derive(Clone, Debug, PartialEq, Eq)]
731pub enum Hit {
732 Point(Vec<usize>),
735 Choose(Vec<usize>),
737 Dismiss,
741}
742
743#[derive(Clone, Copy, Debug, PartialEq, Eq)]
746pub enum Edit {
747 Cut,
748 Copy,
749 Paste,
750 SelectAll,
751}
752
753impl Edit {
754 pub fn at(path: &[usize]) -> Option<Self> {
756 match path {
757 [0] => Some(Edit::Cut),
758 [1] => Some(Edit::Copy),
759 [2] => Some(Edit::Paste),
760 [4] => Some(Edit::SelectAll),
761 _ => None,
762 }
763 }
764}
765
766pub fn edit_items(
769 selected: bool,
770 pasteable: bool,
771 actions: [&dyn Action; 4],
772 window: &Window,
773) -> Vec<Item> {
774 let [cut, copy, paste, select_all] = actions;
775 let enable = |item: Item, on: bool| item.disabled(!on);
776 vec![
777 enable(Item::action("Cut").with_shortcut(cut, window), selected),
778 enable(Item::action("Copy").with_shortcut(copy, window), selected),
779 enable(
780 Item::action("Paste").with_shortcut(paste, window),
781 pasteable,
782 ),
783 Item::Separator,
784 Item::action("Select All").with_shortcut(select_all, window),
785 ]
786}
787
788#[allow(clippy::too_many_arguments)]
809pub fn card<V: 'static>(
810 theme: &Theme,
811 id: impl Into<SharedString>,
812 items: &[Item],
813 cursor: &Cursor,
814 closing: Option<web_time::Instant>,
815 window: &mut Window,
816 cx: &mut Context<V>,
817 on: impl Fn(&mut V, Hit, &mut Window, &mut Context<V>) + 'static,
818) -> gpui::Div {
819 let tree = Tree {
820 id: id.into(),
821 closing,
822 panels: 1 + open_depth(items, cursor.open()),
823 outside: Rc::new(Cell::new((None, 0))),
824 chain: popover::Chain::default(),
825 on: Rc::new(on),
826 };
827 tree.panel(theme, items, cursor, &[], None, window, cx)
828}
829
830struct Query {
834 field: Entity<TextField>,
835 focused: bool,
836 _changed: Subscription,
837}
838
839type Listener<E> = Box<dyn Fn(&E, &mut Window, &mut gpui::App)>;
842
843type Reporter<V> = Rc<dyn Fn(&mut V, Hit, &mut Window, &mut Context<V>)>;
845
846struct Tree<V: 'static> {
848 id: SharedString,
849 panels: usize,
851 outside: Rc<Cell<(Option<Point<Pixels>>, usize)>>,
855 chain: popover::Chain,
857 closing: Option<web_time::Instant>,
859 on: Reporter<V>,
860}
861
862impl<V: 'static> Tree<V> {
863 #[allow(clippy::too_many_arguments)]
864 fn panel(
865 &self,
866 theme: &Theme,
867 items: &[Item],
868 cursor: &Cursor,
869 prefix: &[usize],
870 search: Option<&SharedString>,
871 window: &mut Window,
872 cx: &mut Context<V>,
873 ) -> gpui::Div {
874 let depth = prefix.len();
875 let lit = cursor.lit(depth);
876 let down = cursor.open().get(depth).copied();
877 let rows_id = SharedString::from(format!("{}-rows", row_id(&self.id, prefix)));
878 let Scrolled(handle, shown) = window
879 .use_keyed_state(
880 SharedString::from(format!("{rows_id}-scrolled")),
881 cx,
882 |_, _| Scrolled::default(),
883 )
884 .read(cx)
885 .clone();
886 let query = search.map(|placeholder| self.query(&rows_id, placeholder, window, cx));
887 let text = query
888 .as_ref()
889 .map(|query| query.read(cx).field.read(cx).content().trim().to_owned())
890 .unwrap_or_default();
891 let visible: Vec<usize> = if text.is_empty() {
894 (0..items.len()).collect()
895 } else {
896 let words: Vec<String> = items.iter().map(Item::keywords).collect();
897 popover::filter_indices(&text, &words)
898 .into_iter()
899 .filter(|&row| items[row].selectable())
900 .collect()
901 };
902 let size = handle.bounds().size;
907 if size.height > Pixels::ZERO && shown.get() != Some((lit, size)) {
908 shown.set(Some((lit, size)));
909 if let Some(at) = lit.and_then(|lit| visible.iter().position(|&row| row == lit)) {
910 handle.scroll_to_item(at);
911 }
912 }
913 let inset = px(popover::SNAP) + window.client_inset().unwrap_or(Pixels::ZERO);
914 let cap = height(cx).limit(window.viewport_size().height - inset * 2.0);
915 let gutter = items.iter().any(Item::has_icon);
918 let MenuIndent(step) = indent(cx);
919 let described = items.iter().any(Item::has_description);
920 let width = width(cx);
921 let rows = div()
922 .id(rows_id.clone())
923 .p(px(popover::MENU_PAD))
924 .children(visible.iter().map(|&row| {
925 let item = &items[row];
926 match item {
927 Item::Separator => return popover::divider(theme).into_any_element(),
928 Item::Heading(label) => {
929 return popover::menu_heading(theme, label.clone()).into_any_element();
930 }
931 Item::Note(text) => {
932 return popover::menu_note(theme, text.clone()).into_any_element();
933 }
934 _ => {}
935 }
936 let path: Vec<usize> = prefix.iter().copied().chain([row]).collect();
937 if let Item::Segmented {
938 segments,
939 selected,
940 enabled,
941 } = item
942 {
943 let live = cursor.segment().filter(|_| lit == Some(row));
944 return self
945 .segmented(theme, segments, *selected, *enabled, live, &path, cx)
946 .into_any_element();
947 }
948 let id = row_id(&self.id, &path);
949 let (label, icon, indent, enabled) = match item {
950 Item::Action {
951 label,
952 icon,
953 indent,
954 enabled,
955 ..
956 }
957 | Item::Submenu {
958 label,
959 icon,
960 indent,
961 enabled,
962 ..
963 } => (label.clone(), icon.clone(), *indent, *enabled),
964 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {
965 unreachable!("returned above")
966 }
967 };
968 let (description, inline, hint) = match item {
969 Item::Action {
970 description,
971 inline,
972 tooltip,
973 ..
974 } => (description.clone(), *inline, tooltip.clone()),
975 _ => (None, false, None),
976 };
977 let row = if enabled {
978 popover::menu_row(theme, lit == Some(row), None)
979 .id(id.clone())
980 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
981 .on_click(self.reports(
982 match item {
983 Item::Submenu { .. } => Hit::Point(path.clone()),
986 _ => Hit::Choose(path.clone()),
987 },
988 cx,
989 ))
990 } else {
991 disabled_row(theme).id(id.clone())
992 };
993 let selector = format!("{}/{label}", self.id);
994 let row = row.debug_selector(move || selector);
995 let row = row.when_some(hint, |row, hint| {
996 row.tooltip(move |window, cx| Tooltip::text(hint.clone(), window, cx))
997 });
998 row.when(indent > 0, |row| {
999 row.pl(px(popover::MENU_ROW_INSET) + step * indent as f32)
1000 })
1001 .when(gutter, |row| row.child(glyph_slot(theme, icon, enabled)))
1002 .child(
1003 div()
1004 .flex_1()
1005 .min_w_0()
1006 .flex()
1007 .when(!inline, |text| text.flex_col())
1008 .when(inline, |text| text.flex_row().items_baseline().gap(px(8.0)))
1009 .child(
1010 div()
1011 .truncate()
1012 .when(inline, |label| label.flex_none().max_w_full())
1013 .child(label),
1014 )
1015 .children(description.map(|description| {
1016 description_line(theme, description, enabled)
1017 .when(inline, |line| line.mt_0().min_w_0())
1018 })),
1019 )
1020 .map(|row| match item {
1021 Item::Action {
1022 keystroke, checked, ..
1023 } => row
1024 .when(*checked, |row| {
1025 row.child(
1026 icons::icon(icons::glyph::Check)
1027 .size(px(GLYPH))
1028 .text_color(theme.text),
1029 )
1030 })
1031 .when_some(keystroke.clone(), |row, keystroke| {
1032 row.child(popover::kbd_hint(theme, &keystroke))
1033 }),
1034 _ => row.child(
1035 icons::icon(icons::glyph::ChevronRight)
1036 .size(px(GLYPH))
1037 .text_color(theme.text_faint),
1038 ),
1039 })
1040 .when_some(
1041 item.opens().filter(|_| down == Some(path[depth])),
1042 |parent, inner| {
1043 let panel = self
1044 .panel(theme, inner, cursor, &path, item.search(), window, cx)
1045 .into_any_element();
1046 parent.relative().child(popover::anchored_submenu(
1047 SharedString::from(format!("{id}-panel")),
1048 panel,
1049 &self.chain,
1050 self.closing,
1051 ))
1052 },
1053 )
1054 .into_any_element()
1055 }));
1056 popover::popover_card(theme)
1057 .p_0()
1058 .flex()
1059 .flex_col()
1060 .min_h_0()
1061 .max_h(cap)
1062 .map(|card| match (described, width.described) {
1063 (true, Described::Fixed(wide)) => card.w(wide),
1064 (true, Described::Fit(most)) => card.min_w(width.min).max_w(most),
1065 (false, _) => card.min_w(width.min),
1066 })
1067 .when(self.closing.is_none(), |card| {
1068 card.on_mouse_down_out(self.dismissal(cx))
1069 })
1070 .map(|card| match query {
1071 None => card.child(
1072 scroll::Viewport::new(rows_id, rows, Axis::Vertical)
1073 .track_scroll(&handle)
1074 .fill(),
1075 ),
1076 Some(query) => {
1077 let field = query.read(cx).field.clone();
1078 let rows = rows.max_h(px(
1079 SEARCH_ROWS * popover::menu_row_height() + 2.0 * popover::MENU_PAD
1080 ));
1081 card.child(
1082 div()
1083 .key_context(SEARCH_CONTEXT)
1084 .px(px(popover::MENU_PAD))
1085 .pt(px(popover::MENU_PAD))
1086 .map(|el| self.keys(el, items, &visible, lit, prefix, cx))
1087 .child(popover::search_line(theme, field.into_any_element())),
1088 )
1089 .child(match visible.is_empty() {
1090 true => div()
1091 .px(px(popover::MENU_PAD + popover::MENU_ROW_INSET))
1092 .pb(px(8.0))
1093 .text_style(TextStyle::Body)
1094 .text_color(theme.text_muted)
1095 .child("No matches")
1096 .into_any_element(),
1097 false => scroll::Viewport::new(rows_id, rows, Axis::Vertical)
1098 .track_scroll(&handle)
1099 .into_any_element(),
1100 })
1101 }
1102 })
1103 }
1104
1105 fn query(
1108 &self,
1109 key: &SharedString,
1110 placeholder: &SharedString,
1111 window: &mut Window,
1112 cx: &mut Context<V>,
1113 ) -> Entity<Query> {
1114 let host = cx.entity_id();
1115 let placeholder = placeholder.clone();
1116 let query = window.use_keyed_state(
1117 SharedString::from(format!("{key}-query")),
1118 cx,
1119 move |_, cx| {
1120 let field = cx.new(|cx| crate::search::query_field(placeholder, cx));
1121 let _changed = cx.subscribe(&field, move |_, _, _: &FieldEvent, cx| {
1122 let app: &mut App = cx;
1123 app.notify(host);
1124 });
1125 Query {
1126 field,
1127 focused: false,
1128 _changed,
1129 }
1130 },
1131 );
1132 if !query.read(cx).focused {
1133 query.update(cx, |query, _| query.focused = true);
1134 let handle = query.read(cx).field.focus_handle(cx);
1135 window.focus(&handle, cx);
1136 }
1137 query
1138 }
1139
1140 fn keys(
1143 &self,
1144 el: gpui::Div,
1145 items: &[Item],
1146 shown: &[usize],
1147 lit: Option<usize>,
1148 prefix: &[usize],
1149 cx: &mut Context<V>,
1150 ) -> gpui::Div {
1151 let landable: Rc<Vec<usize>> = Rc::new(
1152 shown
1153 .iter()
1154 .copied()
1155 .filter(|&row| items[row].selectable())
1156 .collect(),
1157 );
1158 let at = lit.and_then(|lit| landable.iter().position(|&row| row == lit));
1159 let path = {
1160 let prefix = prefix.to_vec();
1161 move |row: usize| prefix.iter().copied().chain([row]).collect::<Vec<_>>()
1162 };
1163 let step = |delta: isize| {
1164 let count = landable.len() as isize;
1165 (count > 0).then(|| {
1166 let next = match at {
1167 Some(at) => (at as isize + delta).rem_euclid(count),
1168 None if delta > 0 => 0,
1169 None => count - 1,
1170 };
1171 landable[next as usize]
1172 })
1173 };
1174 let next = step(1).map(&path);
1175 let previous = step(-1).map(&path);
1176 let chosen = at
1177 .map(|at| landable[at])
1178 .or_else(|| landable.first().copied())
1179 .map(|row| match items[row].opens() {
1180 Some(_) => Hit::Point(path(row)),
1181 None => Hit::Choose(path(row)),
1182 });
1183 let on = self.on.clone();
1184 let report = move |hit: Option<Hit>| {
1185 let on = on.clone();
1186 move |view: &mut V, window: &mut Window, cx: &mut Context<V>| {
1187 if let Some(hit) = hit.clone() {
1188 on(view, hit, window, cx);
1189 }
1190 }
1191 };
1192 let (down, up, enter, escape) = (
1193 report(next.map(Hit::Point)),
1194 report(previous.map(Hit::Point)),
1195 report(chosen),
1196 report(Some(Hit::Dismiss)),
1197 );
1198 el.on_action(cx.listener(move |view, _: &SelectNext, window, cx| down(view, window, cx)))
1199 .on_action(
1200 cx.listener(move |view, _: &SelectPrevious, window, cx| up(view, window, cx)),
1201 )
1202 .on_action(cx.listener(move |view, _: &Confirm, window, cx| enter(view, window, cx)))
1203 .on_action(cx.listener(move |view, _: &Dismiss, window, cx| escape(view, window, cx)))
1204 }
1205
1206 #[allow(clippy::too_many_arguments)]
1209 fn segmented(
1210 &self,
1211 theme: &Theme,
1212 segments: &[Segment],
1213 selected: usize,
1214 enabled: bool,
1215 live: Option<usize>,
1216 path: &[usize],
1217 cx: &mut Context<V>,
1218 ) -> gpui::Stateful<gpui::Div> {
1219 div()
1220 .id(row_id(&self.id, path))
1221 .flex()
1222 .flex_row()
1223 .gap(px(popover::MENU_PAD))
1224 .children(segments.iter().enumerate().map(|(at, segment)| {
1225 let path: Vec<usize> = path.iter().copied().chain([at]).collect();
1226 let chosen = at == selected;
1227 let ink = match (enabled, chosen || live == Some(at)) {
1228 (false, _) => theme.text_faint.opacity(0.5),
1229 (true, true) => theme.text,
1230 (true, false) => theme.text_muted,
1231 };
1232 let cell = div()
1233 .id(row_id(&self.id, &path))
1234 .flex_1()
1235 .min_w_0()
1236 .flex()
1237 .flex_col()
1238 .items_center()
1239 .gap(px(4.0))
1240 .px(px(8.0))
1241 .py(px(6.0))
1242 .rounded(px(Theme::inset_radius(
1243 Theme::surface_radius(),
1244 popover::MENU_PAD,
1245 )))
1246 .text_style(TextStyle::Callout)
1247 .text_color(ink)
1248 .when(chosen, |cell| {
1249 cell.bg(theme.element_active)
1250 .font_weight(gpui::FontWeight::MEDIUM)
1251 })
1252 .when(!chosen && live == Some(at), |cell| {
1253 cell.bg(theme.card_selected_bg())
1254 })
1255 .child(
1256 icons::icon(segment.icon.clone())
1257 .size(px(SEGMENT_GLYPH))
1258 .text_color(ink),
1259 )
1260 .child(div().max_w_full().truncate().child(segment.label.clone()));
1261 match enabled {
1262 true => cell
1263 .cursor_pointer()
1264 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
1265 .on_click(self.reports(Hit::Choose(path), cx)),
1266 false => cell,
1267 }
1268 }))
1269 }
1270
1271 fn dismissal(&self, cx: &mut Context<V>) -> Listener<MouseDownEvent> {
1283 let outside = self.outside.clone();
1284 let panels = self.panels;
1285 let on = self.on.clone();
1286 Box::new(
1287 cx.listener(move |view, event: &MouseDownEvent, window, cx| {
1288 let (at, count) = outside.get();
1289 let count = if at == Some(event.position) {
1290 count + 1
1291 } else {
1292 1
1293 };
1294 outside.set((Some(event.position), count));
1295 if count >= panels {
1296 outside.set((None, 0));
1297 on(view, Hit::Dismiss, window, cx);
1298 cx.stop_propagation();
1299 }
1300 }),
1301 )
1302 }
1303
1304 fn reports<E: 'static>(&self, hit: Hit, cx: &mut Context<V>) -> Listener<E> {
1306 let on = self.on.clone();
1307 Box::new(cx.listener(move |view, _: &E, window, cx| on(view, hit.clone(), window, cx)))
1308 }
1309}
1310
1311#[derive(Clone, Default)]
1314struct Scrolled(ScrollHandle, Rc<Cell<Option<Shown>>>);
1315
1316type Shown = (Option<usize>, Size<Pixels>);
1317
1318fn row_id(id: &SharedString, path: &[usize]) -> SharedString {
1321 let mut out = id.to_string();
1322 for row in path {
1323 out.push('-');
1324 out.push_str(&row.to_string());
1325 }
1326 SharedString::from(out)
1327}
1328
1329pub fn glyph_slot(theme: &Theme, icon: Option<Icon>, enabled: bool) -> gpui::Div {
1332 div()
1333 .flex_none()
1334 .size(px(GLYPH))
1335 .flex()
1336 .items_center()
1337 .justify_center()
1338 .children(icon.map(|glyph| {
1339 icons::icon(glyph).size(px(GLYPH)).text_color(if enabled {
1340 theme.text_faint
1341 } else {
1342 theme.text_faint.opacity(0.5)
1343 })
1344 }))
1345}
1346
1347fn description_line(theme: &Theme, description: SharedString, enabled: bool) -> gpui::Div {
1361 div()
1362 .mt(px(2.0))
1363 .truncate()
1364 .text_style(TextStyle::Subheadline)
1365 .text_color(if enabled {
1366 theme.text_muted
1367 } else {
1368 theme.text_faint.opacity(0.5)
1371 })
1372 .child(description)
1373}
1374
1375fn disabled_row(theme: &Theme) -> gpui::Div {
1379 div()
1380 .flex()
1381 .flex_row()
1382 .items_center()
1383 .gap(px(10.0))
1384 .px(px(8.0))
1385 .py(px(popover::MENU_ROW_PAD_Y))
1386 .rounded(px(Theme::inset_radius(
1389 Theme::surface_radius(),
1390 popover::MENU_PAD,
1391 )))
1392 .text_style(TextStyle::Body)
1393 .text_color(theme.text_faint)
1394}