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 tooltip: Option<SharedString>,
163 icon: Option<Icon>,
166 indent: usize,
168 keystroke: Option<SharedString>,
173 checked: bool,
175 enabled: bool,
176 },
177 Submenu {
181 label: SharedString,
182 icon: Option<Icon>,
183 indent: usize,
184 enabled: bool,
185 items: Vec<Item>,
186 search: Option<SharedString>,
191 },
192 Segmented {
196 segments: Vec<Segment>,
197 selected: usize,
198 enabled: bool,
199 },
200 Heading(SharedString),
202 Note(SharedString),
205 Separator,
206}
207
208#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct Segment {
211 pub icon: Icon,
212 pub label: SharedString,
213}
214
215impl Segment {
216 pub fn new(icon: impl Into<Icon>, label: impl Into<SharedString>) -> Self {
217 Self {
218 icon: icon.into(),
219 label: label.into(),
220 }
221 }
222}
223
224impl Item {
225 pub fn action(label: impl Into<SharedString>) -> Self {
226 Item::Action {
227 label: label.into(),
228 description: None,
229 tooltip: None,
230 icon: None,
231 indent: 0,
232 keystroke: None,
233 checked: false,
234 enabled: true,
235 }
236 }
237
238 pub fn submenu(label: impl Into<SharedString>, items: Vec<Item>) -> Self {
239 Item::Submenu {
240 label: label.into(),
241 icon: None,
242 indent: 0,
243 enabled: true,
244 items,
245 search: None,
246 }
247 }
248
249 pub fn searchable(mut self, placeholder: impl Into<SharedString>) -> Self {
252 if let Item::Submenu { search, .. } = &mut self {
253 *search = Some(placeholder.into());
254 }
255 self
256 }
257
258 pub fn segmented(segments: impl IntoIterator<Item = Segment>, selected: usize) -> Self {
259 Item::Segmented {
260 segments: segments.into_iter().collect(),
261 selected,
262 enabled: true,
263 }
264 }
265
266 pub fn with_icon(mut self, icon: impl Into<Icon>) -> Self {
269 match &mut self {
270 Item::Action { icon: slot, .. } | Item::Submenu { icon: slot, .. } => {
271 *slot = Some(icon.into())
272 }
273 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {}
274 }
275 self
276 }
277
278 pub fn indented(mut self, level: usize) -> Self {
282 match &mut self {
283 Item::Action { indent, .. } | Item::Submenu { indent, .. } => *indent = level,
284 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {}
285 }
286 self
287 }
288
289 pub fn with_description(mut self, description: impl Into<SharedString>) -> Self {
293 if let Item::Action {
294 description: slot, ..
295 } = &mut self
296 {
297 *slot = Some(description.into());
298 }
299 self
300 }
301
302 pub fn with_long_description(self, description: impl Into<SharedString>) -> Self {
306 let description = description.into();
307 self.with_description(description.clone())
308 .with_tooltip(description)
309 }
310
311 pub fn with_tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
316 if let Item::Action { tooltip: slot, .. } = &mut self {
317 *slot = Some(tooltip.into());
318 }
319 self
320 }
321
322 pub fn with_keystroke(mut self, keystroke: impl Into<SharedString>) -> Self {
325 if let Item::Action {
326 keystroke: slot, ..
327 } = &mut self
328 {
329 *slot = Some(keystroke.into());
330 }
331 self
332 }
333
334 pub fn with_shortcut(self, action: &dyn Action, window: &Window) -> Self {
342 match keys::shortcut(action, window) {
343 Some(keystroke) => self.with_keystroke(keystroke),
344 None => self,
345 }
346 }
347
348 pub fn with_shortcut_in(self, action: &dyn Action, context: &str, window: &Window) -> Self {
353 match keys::shortcut_in(action, context, window) {
354 Some(keystroke) => self.with_keystroke(keystroke),
355 None => self,
356 }
357 }
358
359 pub fn checked(mut self, checked: bool) -> Self {
362 if let Item::Action { checked: slot, .. } = &mut self {
363 *slot = checked;
364 }
365 self
366 }
367
368 pub fn disabled(mut self) -> Self {
369 match &mut self {
370 Item::Action { enabled, .. }
371 | Item::Submenu { enabled, .. }
372 | Item::Segmented { enabled, .. } => *enabled = false,
373 Item::Heading(_) | Item::Note(_) | Item::Separator => {}
374 }
375 self
376 }
377
378 pub fn selectable(&self) -> bool {
382 match self {
383 Item::Action { enabled, .. } => *enabled,
384 Item::Submenu { enabled, items, .. } => *enabled && items.iter().any(Item::selectable),
385 Item::Segmented {
386 enabled, segments, ..
387 } => *enabled && !segments.is_empty(),
388 Item::Heading(_) | Item::Note(_) | Item::Separator => false,
389 }
390 }
391
392 fn has_description(&self) -> bool {
393 matches!(
394 self,
395 Item::Action {
396 description: Some(_),
397 ..
398 }
399 )
400 }
401
402 fn has_icon(&self) -> bool {
403 matches!(
404 self,
405 Item::Action { icon: Some(_), .. } | Item::Submenu { icon: Some(_), .. }
406 )
407 }
408
409 fn landing(&self) -> Option<usize> {
411 match self {
412 Item::Segmented {
413 segments, selected, ..
414 } if !segments.is_empty() => Some((*selected).min(segments.len() - 1)),
415 _ => None,
416 }
417 }
418
419 fn keywords(&self) -> String {
421 match self {
422 Item::Action {
423 label, description, ..
424 } => match description {
425 Some(description) => format!("{label} {description}"),
426 None => label.to_string(),
427 },
428 Item::Submenu { label, .. } => label.to_string(),
429 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {
430 String::new()
431 }
432 }
433 }
434
435 fn search(&self) -> Option<&SharedString> {
437 match self {
438 Item::Submenu { search, .. } => search.as_ref(),
439 _ => None,
440 }
441 }
442
443 fn opens(&self) -> Option<&[Item]> {
445 match self {
446 Item::Submenu {
447 enabled: true,
448 items,
449 ..
450 } => Some(items),
451 _ => None,
452 }
453 }
454}
455
456pub fn at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a Item> {
460 let (&row, above) = path.split_last()?;
461 items_at(items, above)?.get(row)
462}
463
464pub fn items_at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a [Item]> {
468 let mut level = items;
469 for &row in path {
470 level = level.get(row)?.opens()?;
471 }
472 Some(level)
473}
474
475fn open_depth(items: &[Item], open: &[usize]) -> usize {
478 let mut level = items;
479 for (depth, &row) in open.iter().enumerate() {
480 match level.get(row).and_then(Item::opens) {
481 Some(inner) => level = inner,
482 None => return depth,
483 }
484 }
485 open.len()
486}
487
488pub fn next_selectable(items: &[Item], from: Option<usize>, delta: isize) -> Option<usize> {
496 let count = items.len();
497 if count == 0 {
498 return None;
499 }
500 let step = if delta >= 0 { 1 } else { -1 };
501 let wrap = |at: usize| (at as isize + step).rem_euclid(count as isize) as usize;
502 let mut at = match from {
505 None if step > 0 => 0,
506 None => count - 1,
507 Some(at) => wrap(at.min(count - 1)),
508 };
509 for _ in 0..count {
510 if items[at].selectable() {
511 return Some(at);
512 }
513 at = wrap(at);
514 }
515 None
516}
517
518#[derive(Clone, Debug, Default, PartialEq, Eq)]
530pub struct Cursor {
531 open: Vec<usize>,
532 row: Option<usize>,
533 segment: Option<usize>,
534}
535
536impl Cursor {
537 pub fn open(&self) -> &[usize] {
539 &self.open
540 }
541
542 pub fn row(&self) -> Option<usize> {
544 self.row
545 }
546
547 pub fn segment(&self) -> Option<usize> {
549 self.segment
550 }
551
552 pub fn nested(&self) -> bool {
555 !self.open.is_empty()
556 }
557
558 pub fn path(&self) -> Option<Vec<usize>> {
561 let row = self.row?;
562 let mut path = self.open.clone();
563 path.push(row);
564 path.extend(self.segment);
565 Some(path)
566 }
567
568 pub fn clear(&mut self) {
570 self.open.clear();
571 self.row = None;
572 self.segment = None;
573 }
574
575 pub fn lit(&self, depth: usize) -> Option<usize> {
578 match depth.cmp(&self.open.len()) {
579 std::cmp::Ordering::Less => Some(self.open[depth]),
580 std::cmp::Ordering::Equal => self.row,
581 std::cmp::Ordering::Greater => None,
582 }
583 }
584
585 pub fn step(&mut self, root: &[Item], delta: isize) {
587 let Some(items) = items_at(root, &self.open) else {
588 return;
589 };
590 self.row = next_selectable(items, self.row, delta);
591 self.segment = self.row.and_then(|row| items[row].landing());
592 }
593
594 pub fn slide(&mut self, root: &[Item], delta: isize) -> bool {
597 let Some(Item::Segmented { segments, .. }) = self
598 .row
599 .and_then(|row| items_at(root, &self.open)?.get(row))
600 else {
601 return false;
602 };
603 let at = self.segment.unwrap_or(0) as isize + delta.signum();
604 self.segment = Some(at.clamp(0, segments.len() as isize - 1) as usize);
605 true
606 }
607
608 pub fn descend(&mut self, root: &[Item]) -> bool {
612 let Some(row) = self.row else { return false };
613 let Some(inner) = items_at(root, &self.open)
614 .and_then(|items| items.get(row))
615 .and_then(Item::opens)
616 else {
617 return false;
618 };
619 self.row = next_selectable(inner, None, 1);
620 self.segment = self.row.and_then(|row| inner[row].landing());
621 self.open.push(row);
622 true
623 }
624
625 pub fn ascend(&mut self) -> bool {
628 match self.open.pop() {
629 Some(row) => {
630 self.row = Some(row);
631 self.segment = None;
632 true
633 }
634 None => false,
635 }
636 }
637
638 pub fn point_at(&mut self, root: &[Item], path: &[usize]) -> bool {
646 let (open, row, segment) = match path {
647 [] => (Vec::new(), None, None),
648 [above @ .., row, segment]
649 if matches!(
650 at(root, &path[..path.len() - 1]),
651 Some(Item::Segmented { .. })
652 ) =>
653 {
654 (above.to_vec(), Some(*row), Some(*segment))
655 }
656 _ if at(root, path).and_then(Item::opens).is_some() => (path.to_vec(), None, None),
657 [above @ .., row] => (
658 above.to_vec(),
659 Some(*row),
660 at(root, path).and_then(Item::landing),
661 ),
662 };
663 if self.open == open && self.row == row && self.segment == segment {
664 return false;
665 }
666 self.open = open;
667 self.row = row;
668 self.segment = segment;
669 true
670 }
671}
672
673#[derive(Clone, Debug, PartialEq, Eq)]
680pub enum Hit {
681 Point(Vec<usize>),
684 Choose(Vec<usize>),
686 Dismiss,
690}
691
692#[derive(Clone, Copy, Debug, PartialEq, Eq)]
695pub enum Edit {
696 Cut,
697 Copy,
698 Paste,
699 SelectAll,
700}
701
702impl Edit {
703 pub fn at(path: &[usize]) -> Option<Self> {
705 match path {
706 [0] => Some(Edit::Cut),
707 [1] => Some(Edit::Copy),
708 [2] => Some(Edit::Paste),
709 [4] => Some(Edit::SelectAll),
710 _ => None,
711 }
712 }
713}
714
715pub fn edit_items(
718 selected: bool,
719 pasteable: bool,
720 actions: [&dyn Action; 4],
721 window: &Window,
722) -> Vec<Item> {
723 let [cut, copy, paste, select_all] = actions;
724 let enable = |item: Item, on: bool| if on { item } else { item.disabled() };
725 vec![
726 enable(Item::action("Cut").with_shortcut(cut, window), selected),
727 enable(Item::action("Copy").with_shortcut(copy, window), selected),
728 enable(
729 Item::action("Paste").with_shortcut(paste, window),
730 pasteable,
731 ),
732 Item::Separator,
733 Item::action("Select All").with_shortcut(select_all, window),
734 ]
735}
736
737#[allow(clippy::too_many_arguments)]
759pub fn card<V: 'static>(
760 theme: &Theme,
761 id: impl Into<SharedString>,
762 items: &[Item],
763 cursor: &Cursor,
764 closing: Option<web_time::Instant>,
765 window: &mut Window,
766 cx: &mut Context<V>,
767 on: impl Fn(&mut V, Hit, &mut Window, &mut Context<V>) + 'static,
768) -> gpui::Div {
769 let tree = Tree {
770 id: id.into(),
771 closing,
772 panels: 1 + open_depth(items, cursor.open()),
773 outside: Rc::new(Cell::new((None, 0))),
774 chain: popover::Chain::default(),
775 on: Rc::new(on),
776 };
777 tree.panel(theme, items, cursor, &[], None, window, cx)
778}
779
780struct Query {
784 field: Entity<TextField>,
785 focused: bool,
786 _changed: Subscription,
787}
788
789type Listener<E> = Box<dyn Fn(&E, &mut Window, &mut gpui::App)>;
792
793type Reporter<V> = Rc<dyn Fn(&mut V, Hit, &mut Window, &mut Context<V>)>;
795
796struct Tree<V: 'static> {
798 id: SharedString,
799 panels: usize,
801 outside: Rc<Cell<(Option<Point<Pixels>>, usize)>>,
805 chain: popover::Chain,
807 closing: Option<web_time::Instant>,
809 on: Reporter<V>,
810}
811
812impl<V: 'static> Tree<V> {
813 #[allow(clippy::too_many_arguments)]
814 fn panel(
815 &self,
816 theme: &Theme,
817 items: &[Item],
818 cursor: &Cursor,
819 prefix: &[usize],
820 search: Option<&SharedString>,
821 window: &mut Window,
822 cx: &mut Context<V>,
823 ) -> gpui::Div {
824 let depth = prefix.len();
825 let lit = cursor.lit(depth);
826 let down = cursor.open().get(depth).copied();
827 let rows_id = SharedString::from(format!("{}-rows", row_id(&self.id, prefix)));
828 let Scrolled(handle, shown) = window
829 .use_keyed_state(
830 SharedString::from(format!("{rows_id}-scrolled")),
831 cx,
832 |_, _| Scrolled::default(),
833 )
834 .read(cx)
835 .clone();
836 let query = search.map(|placeholder| self.query(&rows_id, placeholder, window, cx));
837 let text = query
838 .as_ref()
839 .map(|query| query.read(cx).field.read(cx).content().trim().to_owned())
840 .unwrap_or_default();
841 let visible: Vec<usize> = if text.is_empty() {
844 (0..items.len()).collect()
845 } else {
846 let words: Vec<String> = items.iter().map(Item::keywords).collect();
847 popover::filter_indices(&text, &words)
848 .into_iter()
849 .filter(|&row| items[row].selectable())
850 .collect()
851 };
852 let size = handle.bounds().size;
857 if size.height > Pixels::ZERO && shown.get() != Some((lit, size)) {
858 shown.set(Some((lit, size)));
859 if let Some(at) = lit.and_then(|lit| visible.iter().position(|&row| row == lit)) {
860 handle.scroll_to_item(at);
861 }
862 }
863 let inset = px(popover::SNAP) + window.client_inset().unwrap_or(Pixels::ZERO);
864 let cap = window.viewport_size().height - inset * 2.0;
865 let gutter = items.iter().any(Item::has_icon);
868 let MenuIndent(step) = indent(cx);
869 let described = items.iter().any(Item::has_description);
870 let width = width(cx);
871 let rows = div()
872 .id(rows_id.clone())
873 .p(px(popover::MENU_PAD))
874 .children(visible.iter().map(|&row| {
875 let item = &items[row];
876 match item {
877 Item::Separator => return popover::divider(theme).into_any_element(),
878 Item::Heading(label) => {
879 return popover::menu_heading(theme, label.clone()).into_any_element();
880 }
881 Item::Note(text) => {
882 return popover::menu_note(theme, text.clone()).into_any_element();
883 }
884 _ => {}
885 }
886 let path: Vec<usize> = prefix.iter().copied().chain([row]).collect();
887 if let Item::Segmented {
888 segments,
889 selected,
890 enabled,
891 } = item
892 {
893 let live = cursor.segment().filter(|_| lit == Some(row));
894 return self
895 .segmented(theme, segments, *selected, *enabled, live, &path, cx)
896 .into_any_element();
897 }
898 let id = row_id(&self.id, &path);
899 let (label, icon, indent, enabled) = match item {
900 Item::Action {
901 label,
902 icon,
903 indent,
904 enabled,
905 ..
906 }
907 | Item::Submenu {
908 label,
909 icon,
910 indent,
911 enabled,
912 ..
913 } => (label.clone(), icon.clone(), *indent, *enabled),
914 Item::Segmented { .. } | Item::Heading(_) | Item::Note(_) | Item::Separator => {
915 unreachable!("returned above")
916 }
917 };
918 let (description, hint) = match item {
919 Item::Action {
920 description,
921 tooltip,
922 ..
923 } => (description.clone(), tooltip.clone()),
924 _ => (None, None),
925 };
926 let row = if enabled {
927 popover::menu_row(theme, lit == Some(row), None)
928 .id(id.clone())
929 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
930 .on_click(self.reports(
931 match item {
932 Item::Submenu { .. } => Hit::Point(path.clone()),
935 _ => Hit::Choose(path.clone()),
936 },
937 cx,
938 ))
939 } else {
940 disabled_row(theme).id(id.clone())
941 };
942 let selector = format!("{}/{label}", self.id);
943 let row = row.debug_selector(move || selector);
944 let row = row.when_some(hint, |row, hint| {
945 row.tooltip(move |window, cx| Tooltip::text(hint.clone(), window, cx))
946 });
947 row.when(indent > 0, |row| {
948 row.pl(px(popover::MENU_ROW_INSET) + step * indent as f32)
949 })
950 .when(gutter, |row| row.child(glyph_slot(theme, icon, enabled)))
951 .child(
952 div()
953 .flex_1()
954 .min_w_0()
955 .flex()
956 .flex_col()
957 .child(div().truncate().child(label))
958 .children(
959 description
960 .map(|description| description_line(theme, description, enabled)),
961 ),
962 )
963 .map(|row| match item {
964 Item::Action {
965 keystroke, checked, ..
966 } => row
967 .when(*checked, |row| {
968 row.child(
969 icons::icon(icons::glyph::Check)
970 .size(px(GLYPH))
971 .text_color(theme.text),
972 )
973 })
974 .when_some(keystroke.clone(), |row, keystroke| {
975 row.child(popover::kbd_hint(theme, &keystroke))
976 }),
977 _ => row.child(
978 icons::icon(icons::glyph::ChevronRight)
979 .size(px(GLYPH))
980 .text_color(theme.text_faint),
981 ),
982 })
983 .when_some(
984 item.opens().filter(|_| down == Some(path[depth])),
985 |parent, inner| {
986 let panel = self
987 .panel(theme, inner, cursor, &path, item.search(), window, cx)
988 .into_any_element();
989 parent.relative().child(popover::anchored_submenu(
990 SharedString::from(format!("{id}-panel")),
991 panel,
992 &self.chain,
993 self.closing,
994 ))
995 },
996 )
997 .into_any_element()
998 }));
999 popover::popover_card(theme)
1000 .p_0()
1001 .flex()
1002 .flex_col()
1003 .max_h(cap)
1004 .map(|card| match (described, width.described) {
1005 (true, Described::Fixed(wide)) => card.w(wide),
1006 (true, Described::Fit(most)) => card.min_w(width.min).max_w(most),
1007 (false, _) => card.min_w(width.min),
1008 })
1009 .on_mouse_down_out(self.dismissal(cx))
1010 .map(|card| match query {
1011 None => card.child(
1012 scroll::Viewport::new(rows_id, rows, Axis::Vertical)
1013 .track_scroll(&handle)
1014 .fill(),
1015 ),
1016 Some(query) => {
1017 let field = query.read(cx).field.clone();
1018 let rows = rows.max_h(px(
1019 SEARCH_ROWS * popover::menu_row_height() + 2.0 * popover::MENU_PAD
1020 ));
1021 card.child(
1022 div()
1023 .key_context(SEARCH_CONTEXT)
1024 .px(px(popover::MENU_PAD))
1025 .pt(px(popover::MENU_PAD))
1026 .map(|el| self.keys(el, items, &visible, lit, prefix, cx))
1027 .child(popover::search_line(theme, field.into_any_element())),
1028 )
1029 .child(match visible.is_empty() {
1030 true => div()
1031 .px(px(popover::MENU_PAD + popover::MENU_ROW_INSET))
1032 .pb(px(8.0))
1033 .text_style(TextStyle::Body)
1034 .text_color(theme.text_muted)
1035 .child("No matches")
1036 .into_any_element(),
1037 false => scroll::Viewport::new(rows_id, rows, Axis::Vertical)
1038 .track_scroll(&handle)
1039 .into_any_element(),
1040 })
1041 }
1042 })
1043 }
1044
1045 fn query(
1048 &self,
1049 key: &SharedString,
1050 placeholder: &SharedString,
1051 window: &mut Window,
1052 cx: &mut Context<V>,
1053 ) -> Entity<Query> {
1054 let host = cx.entity_id();
1055 let placeholder = placeholder.clone();
1056 let query = window.use_keyed_state(
1057 SharedString::from(format!("{key}-query")),
1058 cx,
1059 move |_, cx| {
1060 let field = cx.new(|cx| crate::search::query_field(placeholder, cx));
1061 let _changed = cx.subscribe(&field, move |_, _, _: &FieldEvent, cx| {
1062 let app: &mut App = cx;
1063 app.notify(host);
1064 });
1065 Query {
1066 field,
1067 focused: false,
1068 _changed,
1069 }
1070 },
1071 );
1072 if !query.read(cx).focused {
1073 query.update(cx, |query, _| query.focused = true);
1074 let handle = query.read(cx).field.focus_handle(cx);
1075 window.focus(&handle, cx);
1076 }
1077 query
1078 }
1079
1080 fn keys(
1083 &self,
1084 el: gpui::Div,
1085 items: &[Item],
1086 shown: &[usize],
1087 lit: Option<usize>,
1088 prefix: &[usize],
1089 cx: &mut Context<V>,
1090 ) -> gpui::Div {
1091 let landable: Rc<Vec<usize>> = Rc::new(
1092 shown
1093 .iter()
1094 .copied()
1095 .filter(|&row| items[row].selectable())
1096 .collect(),
1097 );
1098 let at = lit.and_then(|lit| landable.iter().position(|&row| row == lit));
1099 let path = {
1100 let prefix = prefix.to_vec();
1101 move |row: usize| prefix.iter().copied().chain([row]).collect::<Vec<_>>()
1102 };
1103 let step = |delta: isize| {
1104 let count = landable.len() as isize;
1105 (count > 0).then(|| {
1106 let next = match at {
1107 Some(at) => (at as isize + delta).rem_euclid(count),
1108 None if delta > 0 => 0,
1109 None => count - 1,
1110 };
1111 landable[next as usize]
1112 })
1113 };
1114 let next = step(1).map(&path);
1115 let previous = step(-1).map(&path);
1116 let chosen = at
1117 .map(|at| landable[at])
1118 .or_else(|| landable.first().copied())
1119 .map(|row| match items[row].opens() {
1120 Some(_) => Hit::Point(path(row)),
1121 None => Hit::Choose(path(row)),
1122 });
1123 let on = self.on.clone();
1124 let report = move |hit: Option<Hit>| {
1125 let on = on.clone();
1126 move |view: &mut V, window: &mut Window, cx: &mut Context<V>| {
1127 if let Some(hit) = hit.clone() {
1128 on(view, hit, window, cx);
1129 }
1130 }
1131 };
1132 let (down, up, enter, escape) = (
1133 report(next.map(Hit::Point)),
1134 report(previous.map(Hit::Point)),
1135 report(chosen),
1136 report(Some(Hit::Dismiss)),
1137 );
1138 el.on_action(cx.listener(move |view, _: &SelectNext, window, cx| down(view, window, cx)))
1139 .on_action(
1140 cx.listener(move |view, _: &SelectPrevious, window, cx| up(view, window, cx)),
1141 )
1142 .on_action(cx.listener(move |view, _: &Confirm, window, cx| enter(view, window, cx)))
1143 .on_action(cx.listener(move |view, _: &Dismiss, window, cx| escape(view, window, cx)))
1144 }
1145
1146 #[allow(clippy::too_many_arguments)]
1149 fn segmented(
1150 &self,
1151 theme: &Theme,
1152 segments: &[Segment],
1153 selected: usize,
1154 enabled: bool,
1155 live: Option<usize>,
1156 path: &[usize],
1157 cx: &mut Context<V>,
1158 ) -> gpui::Stateful<gpui::Div> {
1159 div()
1160 .id(row_id(&self.id, path))
1161 .flex()
1162 .flex_row()
1163 .gap(px(popover::MENU_PAD))
1164 .children(segments.iter().enumerate().map(|(at, segment)| {
1165 let path: Vec<usize> = path.iter().copied().chain([at]).collect();
1166 let chosen = at == selected;
1167 let ink = match (enabled, chosen || live == Some(at)) {
1168 (false, _) => theme.text_faint.opacity(0.5),
1169 (true, true) => theme.text,
1170 (true, false) => theme.text_muted,
1171 };
1172 let cell = div()
1173 .id(row_id(&self.id, &path))
1174 .flex_1()
1175 .min_w_0()
1176 .flex()
1177 .flex_col()
1178 .items_center()
1179 .gap(px(4.0))
1180 .px(px(8.0))
1181 .py(px(6.0))
1182 .rounded(px(Theme::inset_radius(
1183 Theme::surface_radius(),
1184 popover::MENU_PAD,
1185 )))
1186 .text_style(TextStyle::Callout)
1187 .text_color(ink)
1188 .when(chosen, |cell| {
1189 cell.bg(theme.element_active)
1190 .font_weight(gpui::FontWeight::MEDIUM)
1191 })
1192 .when(!chosen && live == Some(at), |cell| {
1193 cell.bg(theme.card_selected_bg())
1194 })
1195 .child(
1196 icons::icon(segment.icon.clone())
1197 .size(px(SEGMENT_GLYPH))
1198 .text_color(ink),
1199 )
1200 .child(div().max_w_full().truncate().child(segment.label.clone()));
1201 match enabled {
1202 true => cell
1203 .cursor_pointer()
1204 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
1205 .on_click(self.reports(Hit::Choose(path), cx)),
1206 false => cell,
1207 }
1208 }))
1209 }
1210
1211 fn dismissal(&self, cx: &mut Context<V>) -> Listener<MouseDownEvent> {
1223 let outside = self.outside.clone();
1224 let panels = self.panels;
1225 let on = self.on.clone();
1226 Box::new(
1227 cx.listener(move |view, event: &MouseDownEvent, window, cx| {
1228 let (at, count) = outside.get();
1229 let count = if at == Some(event.position) {
1230 count + 1
1231 } else {
1232 1
1233 };
1234 outside.set((Some(event.position), count));
1235 if count >= panels {
1236 outside.set((None, 0));
1237 on(view, Hit::Dismiss, window, cx);
1238 cx.stop_propagation();
1239 }
1240 }),
1241 )
1242 }
1243
1244 fn reports<E: 'static>(&self, hit: Hit, cx: &mut Context<V>) -> Listener<E> {
1246 let on = self.on.clone();
1247 Box::new(cx.listener(move |view, _: &E, window, cx| on(view, hit.clone(), window, cx)))
1248 }
1249}
1250
1251#[derive(Clone, Default)]
1254struct Scrolled(ScrollHandle, Rc<Cell<Option<Shown>>>);
1255
1256type Shown = (Option<usize>, Size<Pixels>);
1257
1258fn row_id(id: &SharedString, path: &[usize]) -> SharedString {
1261 let mut out = id.to_string();
1262 for row in path {
1263 out.push('-');
1264 out.push_str(&row.to_string());
1265 }
1266 SharedString::from(out)
1267}
1268
1269pub fn glyph_slot(theme: &Theme, icon: Option<Icon>, enabled: bool) -> gpui::Div {
1272 div()
1273 .flex_none()
1274 .size(px(GLYPH))
1275 .flex()
1276 .items_center()
1277 .justify_center()
1278 .children(icon.map(|glyph| {
1279 icons::icon(glyph).size(px(GLYPH)).text_color(if enabled {
1280 theme.text_faint
1281 } else {
1282 theme.text_faint.opacity(0.5)
1283 })
1284 }))
1285}
1286
1287fn description_line(theme: &Theme, description: SharedString, enabled: bool) -> gpui::Div {
1301 div()
1302 .mt(px(2.0))
1303 .truncate()
1304 .text_style(TextStyle::Subheadline)
1305 .text_color(if enabled {
1306 theme.text_muted
1307 } else {
1308 theme.text_faint.opacity(0.5)
1311 })
1312 .child(description)
1313}
1314
1315fn disabled_row(theme: &Theme) -> gpui::Div {
1319 div()
1320 .flex()
1321 .flex_row()
1322 .items_center()
1323 .gap(px(10.0))
1324 .px(px(8.0))
1325 .py(px(popover::MENU_ROW_PAD_Y))
1326 .rounded(px(Theme::inset_radius(
1329 Theme::surface_radius(),
1330 popover::MENU_PAD,
1331 )))
1332 .text_style(TextStyle::Body)
1333 .text_color(theme.text_faint)
1334}