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 Separator,
203}
204
205#[derive(Clone, Debug, PartialEq, Eq)]
207pub struct Segment {
208 pub icon: Icon,
209 pub label: SharedString,
210}
211
212impl Segment {
213 pub fn new(icon: impl Into<Icon>, label: impl Into<SharedString>) -> Self {
214 Self {
215 icon: icon.into(),
216 label: label.into(),
217 }
218 }
219}
220
221impl Item {
222 pub fn action(label: impl Into<SharedString>) -> Self {
223 Item::Action {
224 label: label.into(),
225 description: None,
226 tooltip: None,
227 icon: None,
228 indent: 0,
229 keystroke: None,
230 checked: false,
231 enabled: true,
232 }
233 }
234
235 pub fn submenu(label: impl Into<SharedString>, items: Vec<Item>) -> Self {
236 Item::Submenu {
237 label: label.into(),
238 icon: None,
239 indent: 0,
240 enabled: true,
241 items,
242 search: None,
243 }
244 }
245
246 pub fn searchable(mut self, placeholder: impl Into<SharedString>) -> Self {
249 if let Item::Submenu { search, .. } = &mut self {
250 *search = Some(placeholder.into());
251 }
252 self
253 }
254
255 pub fn segmented(segments: impl IntoIterator<Item = Segment>, selected: usize) -> Self {
256 Item::Segmented {
257 segments: segments.into_iter().collect(),
258 selected,
259 enabled: true,
260 }
261 }
262
263 pub fn with_icon(mut self, icon: impl Into<Icon>) -> Self {
266 match &mut self {
267 Item::Action { icon: slot, .. } | Item::Submenu { icon: slot, .. } => {
268 *slot = Some(icon.into())
269 }
270 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => {}
271 }
272 self
273 }
274
275 pub fn indented(mut self, level: usize) -> Self {
279 match &mut self {
280 Item::Action { indent, .. } | Item::Submenu { indent, .. } => *indent = level,
281 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => {}
282 }
283 self
284 }
285
286 pub fn with_description(mut self, description: impl Into<SharedString>) -> Self {
290 if let Item::Action {
291 description: slot, ..
292 } = &mut self
293 {
294 *slot = Some(description.into());
295 }
296 self
297 }
298
299 pub fn with_long_description(self, description: impl Into<SharedString>) -> Self {
303 let description = description.into();
304 self.with_description(description.clone())
305 .with_tooltip(description)
306 }
307
308 pub fn with_tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
313 if let Item::Action { tooltip: slot, .. } = &mut self {
314 *slot = Some(tooltip.into());
315 }
316 self
317 }
318
319 pub fn with_keystroke(mut self, keystroke: impl Into<SharedString>) -> Self {
322 if let Item::Action {
323 keystroke: slot, ..
324 } = &mut self
325 {
326 *slot = Some(keystroke.into());
327 }
328 self
329 }
330
331 pub fn with_shortcut(self, action: &dyn Action, window: &Window) -> Self {
339 match keys::shortcut(action, window) {
340 Some(keystroke) => self.with_keystroke(keystroke),
341 None => self,
342 }
343 }
344
345 pub fn with_shortcut_in(self, action: &dyn Action, context: &str, window: &Window) -> Self {
350 match keys::shortcut_in(action, context, window) {
351 Some(keystroke) => self.with_keystroke(keystroke),
352 None => self,
353 }
354 }
355
356 pub fn checked(mut self, checked: bool) -> Self {
359 if let Item::Action { checked: slot, .. } = &mut self {
360 *slot = checked;
361 }
362 self
363 }
364
365 pub fn disabled(mut self) -> Self {
366 match &mut self {
367 Item::Action { enabled, .. }
368 | Item::Submenu { enabled, .. }
369 | Item::Segmented { enabled, .. } => *enabled = false,
370 Item::Heading(_) | Item::Separator => {}
371 }
372 self
373 }
374
375 pub fn selectable(&self) -> bool {
379 match self {
380 Item::Action { enabled, .. } => *enabled,
381 Item::Submenu { enabled, items, .. } => *enabled && items.iter().any(Item::selectable),
382 Item::Segmented {
383 enabled, segments, ..
384 } => *enabled && !segments.is_empty(),
385 Item::Heading(_) | Item::Separator => false,
386 }
387 }
388
389 fn has_description(&self) -> bool {
390 matches!(
391 self,
392 Item::Action {
393 description: Some(_),
394 ..
395 }
396 )
397 }
398
399 fn has_icon(&self) -> bool {
400 matches!(
401 self,
402 Item::Action { icon: Some(_), .. } | Item::Submenu { icon: Some(_), .. }
403 )
404 }
405
406 fn landing(&self) -> Option<usize> {
408 match self {
409 Item::Segmented {
410 segments, selected, ..
411 } if !segments.is_empty() => Some((*selected).min(segments.len() - 1)),
412 _ => None,
413 }
414 }
415
416 fn keywords(&self) -> String {
418 match self {
419 Item::Action {
420 label, description, ..
421 } => match description {
422 Some(description) => format!("{label} {description}"),
423 None => label.to_string(),
424 },
425 Item::Submenu { label, .. } => label.to_string(),
426 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => String::new(),
427 }
428 }
429
430 fn search(&self) -> Option<&SharedString> {
432 match self {
433 Item::Submenu { search, .. } => search.as_ref(),
434 _ => None,
435 }
436 }
437
438 fn opens(&self) -> Option<&[Item]> {
440 match self {
441 Item::Submenu {
442 enabled: true,
443 items,
444 ..
445 } => Some(items),
446 _ => None,
447 }
448 }
449}
450
451pub fn at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a Item> {
455 let (&row, above) = path.split_last()?;
456 items_at(items, above)?.get(row)
457}
458
459pub fn items_at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a [Item]> {
463 let mut level = items;
464 for &row in path {
465 level = level.get(row)?.opens()?;
466 }
467 Some(level)
468}
469
470fn open_depth(items: &[Item], open: &[usize]) -> usize {
473 let mut level = items;
474 for (depth, &row) in open.iter().enumerate() {
475 match level.get(row).and_then(Item::opens) {
476 Some(inner) => level = inner,
477 None => return depth,
478 }
479 }
480 open.len()
481}
482
483pub fn next_selectable(items: &[Item], from: Option<usize>, delta: isize) -> Option<usize> {
491 let count = items.len();
492 if count == 0 {
493 return None;
494 }
495 let step = if delta >= 0 { 1 } else { -1 };
496 let wrap = |at: usize| (at as isize + step).rem_euclid(count as isize) as usize;
497 let mut at = match from {
500 None if step > 0 => 0,
501 None => count - 1,
502 Some(at) => wrap(at.min(count - 1)),
503 };
504 for _ in 0..count {
505 if items[at].selectable() {
506 return Some(at);
507 }
508 at = wrap(at);
509 }
510 None
511}
512
513#[derive(Clone, Debug, Default, PartialEq, Eq)]
525pub struct Cursor {
526 open: Vec<usize>,
527 row: Option<usize>,
528 segment: Option<usize>,
529}
530
531impl Cursor {
532 pub fn open(&self) -> &[usize] {
534 &self.open
535 }
536
537 pub fn row(&self) -> Option<usize> {
539 self.row
540 }
541
542 pub fn segment(&self) -> Option<usize> {
544 self.segment
545 }
546
547 pub fn nested(&self) -> bool {
550 !self.open.is_empty()
551 }
552
553 pub fn path(&self) -> Option<Vec<usize>> {
556 let row = self.row?;
557 let mut path = self.open.clone();
558 path.push(row);
559 path.extend(self.segment);
560 Some(path)
561 }
562
563 pub fn clear(&mut self) {
565 self.open.clear();
566 self.row = None;
567 self.segment = None;
568 }
569
570 pub fn lit(&self, depth: usize) -> Option<usize> {
573 match depth.cmp(&self.open.len()) {
574 std::cmp::Ordering::Less => Some(self.open[depth]),
575 std::cmp::Ordering::Equal => self.row,
576 std::cmp::Ordering::Greater => None,
577 }
578 }
579
580 pub fn step(&mut self, root: &[Item], delta: isize) {
582 let Some(items) = items_at(root, &self.open) else {
583 return;
584 };
585 self.row = next_selectable(items, self.row, delta);
586 self.segment = self.row.and_then(|row| items[row].landing());
587 }
588
589 pub fn slide(&mut self, root: &[Item], delta: isize) -> bool {
592 let Some(Item::Segmented { segments, .. }) = self
593 .row
594 .and_then(|row| items_at(root, &self.open)?.get(row))
595 else {
596 return false;
597 };
598 let at = self.segment.unwrap_or(0) as isize + delta.signum();
599 self.segment = Some(at.clamp(0, segments.len() as isize - 1) as usize);
600 true
601 }
602
603 pub fn descend(&mut self, root: &[Item]) -> bool {
607 let Some(row) = self.row else { return false };
608 let Some(inner) = items_at(root, &self.open)
609 .and_then(|items| items.get(row))
610 .and_then(Item::opens)
611 else {
612 return false;
613 };
614 self.row = next_selectable(inner, None, 1);
615 self.segment = self.row.and_then(|row| inner[row].landing());
616 self.open.push(row);
617 true
618 }
619
620 pub fn ascend(&mut self) -> bool {
623 match self.open.pop() {
624 Some(row) => {
625 self.row = Some(row);
626 self.segment = None;
627 true
628 }
629 None => false,
630 }
631 }
632
633 pub fn point_at(&mut self, root: &[Item], path: &[usize]) -> bool {
641 let (open, row, segment) = match path {
642 [] => (Vec::new(), None, None),
643 [above @ .., row, segment]
644 if matches!(
645 at(root, &path[..path.len() - 1]),
646 Some(Item::Segmented { .. })
647 ) =>
648 {
649 (above.to_vec(), Some(*row), Some(*segment))
650 }
651 _ if at(root, path).and_then(Item::opens).is_some() => (path.to_vec(), None, None),
652 [above @ .., row] => (
653 above.to_vec(),
654 Some(*row),
655 at(root, path).and_then(Item::landing),
656 ),
657 };
658 if self.open == open && self.row == row && self.segment == segment {
659 return false;
660 }
661 self.open = open;
662 self.row = row;
663 self.segment = segment;
664 true
665 }
666}
667
668#[derive(Clone, Debug, PartialEq, Eq)]
675pub enum Hit {
676 Point(Vec<usize>),
679 Choose(Vec<usize>),
681 Dismiss,
685}
686
687#[derive(Clone, Copy, Debug, PartialEq, Eq)]
690pub enum Edit {
691 Cut,
692 Copy,
693 Paste,
694 SelectAll,
695}
696
697impl Edit {
698 pub fn at(path: &[usize]) -> Option<Self> {
700 match path {
701 [0] => Some(Edit::Cut),
702 [1] => Some(Edit::Copy),
703 [2] => Some(Edit::Paste),
704 [4] => Some(Edit::SelectAll),
705 _ => None,
706 }
707 }
708}
709
710pub fn edit_items(
713 selected: bool,
714 pasteable: bool,
715 actions: [&dyn Action; 4],
716 window: &Window,
717) -> Vec<Item> {
718 let [cut, copy, paste, select_all] = actions;
719 let enable = |item: Item, on: bool| if on { item } else { item.disabled() };
720 vec![
721 enable(Item::action("Cut").with_shortcut(cut, window), selected),
722 enable(Item::action("Copy").with_shortcut(copy, window), selected),
723 enable(
724 Item::action("Paste").with_shortcut(paste, window),
725 pasteable,
726 ),
727 Item::Separator,
728 Item::action("Select All").with_shortcut(select_all, window),
729 ]
730}
731
732pub fn card<V: 'static>(
751 theme: &Theme,
752 id: impl Into<SharedString>,
753 items: &[Item],
754 cursor: &Cursor,
755 window: &mut Window,
756 cx: &mut Context<V>,
757 on: impl Fn(&mut V, Hit, &mut Window, &mut Context<V>) + 'static,
758) -> gpui::Div {
759 let tree = Tree {
760 id: id.into(),
761 panels: 1 + open_depth(items, cursor.open()),
762 outside: Rc::new(Cell::new((None, 0))),
763 chain: popover::Chain::default(),
764 on: Rc::new(on),
765 };
766 tree.panel(theme, items, cursor, &[], None, window, cx)
767}
768
769struct Query {
773 field: Entity<TextField>,
774 focused: bool,
775 _changed: Subscription,
776}
777
778type Listener<E> = Box<dyn Fn(&E, &mut Window, &mut gpui::App)>;
781
782type Reporter<V> = Rc<dyn Fn(&mut V, Hit, &mut Window, &mut Context<V>)>;
784
785struct Tree<V: 'static> {
787 id: SharedString,
788 panels: usize,
790 outside: Rc<Cell<(Option<Point<Pixels>>, usize)>>,
794 chain: popover::Chain,
796 on: Reporter<V>,
797}
798
799impl<V: 'static> Tree<V> {
800 #[allow(clippy::too_many_arguments)]
801 fn panel(
802 &self,
803 theme: &Theme,
804 items: &[Item],
805 cursor: &Cursor,
806 prefix: &[usize],
807 search: Option<&SharedString>,
808 window: &mut Window,
809 cx: &mut Context<V>,
810 ) -> gpui::Div {
811 let depth = prefix.len();
812 let lit = cursor.lit(depth);
813 let down = cursor.open().get(depth).copied();
814 let rows_id = SharedString::from(format!("{}-rows", row_id(&self.id, prefix)));
815 let Scrolled(handle, shown) = window
816 .use_keyed_state(
817 SharedString::from(format!("{rows_id}-scrolled")),
818 cx,
819 |_, _| Scrolled::default(),
820 )
821 .read(cx)
822 .clone();
823 let query = search.map(|placeholder| self.query(&rows_id, placeholder, window, cx));
824 let text = query
825 .as_ref()
826 .map(|query| query.read(cx).field.read(cx).content().trim().to_owned())
827 .unwrap_or_default();
828 let visible: Vec<usize> = if text.is_empty() {
831 (0..items.len()).collect()
832 } else {
833 let words: Vec<String> = items.iter().map(Item::keywords).collect();
834 popover::filter_indices(&text, &words)
835 .into_iter()
836 .filter(|&row| items[row].selectable())
837 .collect()
838 };
839 let size = handle.bounds().size;
844 if size.height > Pixels::ZERO && shown.get() != Some((lit, size)) {
845 shown.set(Some((lit, size)));
846 if let Some(at) = lit.and_then(|lit| visible.iter().position(|&row| row == lit)) {
847 handle.scroll_to_item(at);
848 }
849 }
850 let inset = px(popover::SNAP) + window.client_inset().unwrap_or(Pixels::ZERO);
851 let cap = window.viewport_size().height - inset * 2.0;
852 let gutter = items.iter().any(Item::has_icon);
855 let MenuIndent(step) = indent(cx);
856 let described = items.iter().any(Item::has_description);
857 let width = width(cx);
858 let rows = div()
859 .id(rows_id.clone())
860 .p(px(popover::MENU_PAD))
861 .children(visible.iter().map(|&row| {
862 let item = &items[row];
863 match item {
864 Item::Separator => return popover::divider(theme).into_any_element(),
865 Item::Heading(label) => {
866 return popover::menu_heading(theme, label.clone()).into_any_element();
867 }
868 _ => {}
869 }
870 let path: Vec<usize> = prefix.iter().copied().chain([row]).collect();
871 if let Item::Segmented {
872 segments,
873 selected,
874 enabled,
875 } = item
876 {
877 let live = cursor.segment().filter(|_| lit == Some(row));
878 return self
879 .segmented(theme, segments, *selected, *enabled, live, &path, cx)
880 .into_any_element();
881 }
882 let id = row_id(&self.id, &path);
883 let (label, icon, indent, enabled) = match item {
884 Item::Action {
885 label,
886 icon,
887 indent,
888 enabled,
889 ..
890 }
891 | Item::Submenu {
892 label,
893 icon,
894 indent,
895 enabled,
896 ..
897 } => (label.clone(), icon.clone(), *indent, *enabled),
898 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => {
899 unreachable!("returned above")
900 }
901 };
902 let (description, hint) = match item {
903 Item::Action {
904 description,
905 tooltip,
906 ..
907 } => (description.clone(), tooltip.clone()),
908 _ => (None, None),
909 };
910 let row = if enabled {
911 popover::menu_row(theme, lit == Some(row), None)
912 .id(id.clone())
913 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
914 .on_click(self.reports(
915 match item {
916 Item::Submenu { .. } => Hit::Point(path.clone()),
919 _ => Hit::Choose(path.clone()),
920 },
921 cx,
922 ))
923 } else {
924 disabled_row(theme).id(id.clone())
925 };
926 let selector = format!("{}/{label}", self.id);
927 let row = row.debug_selector(move || selector);
928 let row = row.when_some(hint, |row, hint| {
929 row.tooltip(move |window, cx| Tooltip::text(hint.clone(), window, cx))
930 });
931 row.when(indent > 0, |row| {
932 row.pl(px(popover::MENU_ROW_INSET) + step * indent as f32)
933 })
934 .when(gutter, |row| row.child(glyph_slot(theme, icon, enabled)))
935 .child(
936 div()
937 .flex_1()
938 .min_w_0()
939 .flex()
940 .flex_col()
941 .child(div().truncate().child(label))
942 .children(
943 description
944 .map(|description| description_line(theme, description, enabled)),
945 ),
946 )
947 .map(|row| match item {
948 Item::Action {
949 keystroke, checked, ..
950 } => row
951 .when(*checked, |row| {
952 row.child(
953 icons::icon(icons::glyph::Check)
954 .size(px(GLYPH))
955 .text_color(theme.text),
956 )
957 })
958 .when_some(keystroke.clone(), |row, keystroke| {
959 row.child(popover::kbd_hint(theme, &keystroke))
960 }),
961 _ => row.child(
962 icons::icon(icons::glyph::ChevronRight)
963 .size(px(GLYPH))
964 .text_color(theme.text_faint),
965 ),
966 })
967 .when_some(
968 item.opens().filter(|_| down == Some(path[depth])),
969 |parent, inner| {
970 let panel = self
971 .panel(theme, inner, cursor, &path, item.search(), window, cx)
972 .into_any_element();
973 parent.relative().child(popover::anchored_submenu(
974 SharedString::from(format!("{id}-panel")),
975 panel,
976 &self.chain,
977 ))
978 },
979 )
980 .into_any_element()
981 }));
982 popover::popover_card(theme)
983 .p_0()
984 .flex()
985 .flex_col()
986 .max_h(cap)
987 .map(|card| match (described, width.described) {
988 (true, Described::Fixed(wide)) => card.w(wide),
989 (true, Described::Fit(most)) => card.min_w(width.min).max_w(most),
990 (false, _) => card.min_w(width.min),
991 })
992 .on_mouse_down_out(self.dismissal(cx))
993 .map(|card| match query {
994 None => card.child(
995 scroll::Viewport::new(rows_id, rows, Axis::Vertical)
996 .track_scroll(&handle)
997 .fill(),
998 ),
999 Some(query) => {
1000 let field = query.read(cx).field.clone();
1001 let rows = rows.max_h(px(
1002 SEARCH_ROWS * popover::menu_row_height() + 2.0 * popover::MENU_PAD
1003 ));
1004 card.child(
1005 div()
1006 .key_context(SEARCH_CONTEXT)
1007 .px(px(popover::MENU_PAD))
1008 .pt(px(popover::MENU_PAD))
1009 .map(|el| self.keys(el, items, &visible, lit, prefix, cx))
1010 .child(popover::search_line(theme, field.into_any_element())),
1011 )
1012 .child(match visible.is_empty() {
1013 true => div()
1014 .px(px(popover::MENU_PAD + popover::MENU_ROW_INSET))
1015 .pb(px(8.0))
1016 .text_style(TextStyle::Body)
1017 .text_color(theme.text_muted)
1018 .child("No matches")
1019 .into_any_element(),
1020 false => scroll::Viewport::new(rows_id, rows, Axis::Vertical)
1021 .track_scroll(&handle)
1022 .into_any_element(),
1023 })
1024 }
1025 })
1026 }
1027
1028 fn query(
1031 &self,
1032 key: &SharedString,
1033 placeholder: &SharedString,
1034 window: &mut Window,
1035 cx: &mut Context<V>,
1036 ) -> Entity<Query> {
1037 let host = cx.entity_id();
1038 let placeholder = placeholder.clone();
1039 let query = window.use_keyed_state(
1040 SharedString::from(format!("{key}-query")),
1041 cx,
1042 move |_, cx| {
1043 let field = cx.new(|cx| crate::search::query_field(placeholder, cx));
1044 let _changed = cx.subscribe(&field, move |_, _, _: &FieldEvent, cx| {
1045 let app: &mut App = cx;
1046 app.notify(host);
1047 });
1048 Query {
1049 field,
1050 focused: false,
1051 _changed,
1052 }
1053 },
1054 );
1055 if !query.read(cx).focused {
1056 query.update(cx, |query, _| query.focused = true);
1057 let handle = query.read(cx).field.focus_handle(cx);
1058 window.focus(&handle, cx);
1059 }
1060 query
1061 }
1062
1063 fn keys(
1066 &self,
1067 el: gpui::Div,
1068 items: &[Item],
1069 shown: &[usize],
1070 lit: Option<usize>,
1071 prefix: &[usize],
1072 cx: &mut Context<V>,
1073 ) -> gpui::Div {
1074 let landable: Rc<Vec<usize>> = Rc::new(
1075 shown
1076 .iter()
1077 .copied()
1078 .filter(|&row| items[row].selectable())
1079 .collect(),
1080 );
1081 let at = lit.and_then(|lit| landable.iter().position(|&row| row == lit));
1082 let path = {
1083 let prefix = prefix.to_vec();
1084 move |row: usize| prefix.iter().copied().chain([row]).collect::<Vec<_>>()
1085 };
1086 let step = |delta: isize| {
1087 let count = landable.len() as isize;
1088 (count > 0).then(|| {
1089 let next = match at {
1090 Some(at) => (at as isize + delta).rem_euclid(count),
1091 None if delta > 0 => 0,
1092 None => count - 1,
1093 };
1094 landable[next as usize]
1095 })
1096 };
1097 let next = step(1).map(&path);
1098 let previous = step(-1).map(&path);
1099 let chosen = at
1100 .map(|at| landable[at])
1101 .or_else(|| landable.first().copied())
1102 .map(|row| match items[row].opens() {
1103 Some(_) => Hit::Point(path(row)),
1104 None => Hit::Choose(path(row)),
1105 });
1106 let on = self.on.clone();
1107 let report = move |hit: Option<Hit>| {
1108 let on = on.clone();
1109 move |view: &mut V, window: &mut Window, cx: &mut Context<V>| {
1110 if let Some(hit) = hit.clone() {
1111 on(view, hit, window, cx);
1112 }
1113 }
1114 };
1115 let (down, up, enter, escape) = (
1116 report(next.map(Hit::Point)),
1117 report(previous.map(Hit::Point)),
1118 report(chosen),
1119 report(Some(Hit::Dismiss)),
1120 );
1121 el.on_action(cx.listener(move |view, _: &SelectNext, window, cx| down(view, window, cx)))
1122 .on_action(
1123 cx.listener(move |view, _: &SelectPrevious, window, cx| up(view, window, cx)),
1124 )
1125 .on_action(cx.listener(move |view, _: &Confirm, window, cx| enter(view, window, cx)))
1126 .on_action(cx.listener(move |view, _: &Dismiss, window, cx| escape(view, window, cx)))
1127 }
1128
1129 #[allow(clippy::too_many_arguments)]
1132 fn segmented(
1133 &self,
1134 theme: &Theme,
1135 segments: &[Segment],
1136 selected: usize,
1137 enabled: bool,
1138 live: Option<usize>,
1139 path: &[usize],
1140 cx: &mut Context<V>,
1141 ) -> gpui::Stateful<gpui::Div> {
1142 div()
1143 .id(row_id(&self.id, path))
1144 .flex()
1145 .flex_row()
1146 .gap(px(popover::MENU_PAD))
1147 .children(segments.iter().enumerate().map(|(at, segment)| {
1148 let path: Vec<usize> = path.iter().copied().chain([at]).collect();
1149 let chosen = at == selected;
1150 let ink = match (enabled, chosen || live == Some(at)) {
1151 (false, _) => theme.text_faint.opacity(0.5),
1152 (true, true) => theme.text,
1153 (true, false) => theme.text_muted,
1154 };
1155 let cell = div()
1156 .id(row_id(&self.id, &path))
1157 .flex_1()
1158 .min_w_0()
1159 .flex()
1160 .flex_col()
1161 .items_center()
1162 .gap(px(4.0))
1163 .px(px(8.0))
1164 .py(px(6.0))
1165 .rounded(px(Theme::inset_radius(
1166 Theme::surface_radius(),
1167 popover::MENU_PAD,
1168 )))
1169 .text_style(TextStyle::Callout)
1170 .text_color(ink)
1171 .when(chosen, |cell| {
1172 cell.bg(theme.element_active)
1173 .font_weight(gpui::FontWeight::MEDIUM)
1174 })
1175 .when(!chosen && live == Some(at), |cell| {
1176 cell.bg(theme.card_selected_bg())
1177 })
1178 .child(
1179 icons::icon(segment.icon.clone())
1180 .size(px(SEGMENT_GLYPH))
1181 .text_color(ink),
1182 )
1183 .child(div().max_w_full().truncate().child(segment.label.clone()));
1184 match enabled {
1185 true => cell
1186 .cursor_pointer()
1187 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
1188 .on_click(self.reports(Hit::Choose(path), cx)),
1189 false => cell,
1190 }
1191 }))
1192 }
1193
1194 fn dismissal(&self, cx: &mut Context<V>) -> Listener<MouseDownEvent> {
1206 let outside = self.outside.clone();
1207 let panels = self.panels;
1208 let on = self.on.clone();
1209 Box::new(
1210 cx.listener(move |view, event: &MouseDownEvent, window, cx| {
1211 let (at, count) = outside.get();
1212 let count = if at == Some(event.position) {
1213 count + 1
1214 } else {
1215 1
1216 };
1217 outside.set((Some(event.position), count));
1218 if count >= panels {
1219 outside.set((None, 0));
1220 on(view, Hit::Dismiss, window, cx);
1221 cx.stop_propagation();
1222 }
1223 }),
1224 )
1225 }
1226
1227 fn reports<E: 'static>(&self, hit: Hit, cx: &mut Context<V>) -> Listener<E> {
1229 let on = self.on.clone();
1230 Box::new(cx.listener(move |view, _: &E, window, cx| on(view, hit.clone(), window, cx)))
1231 }
1232}
1233
1234#[derive(Clone, Default)]
1237struct Scrolled(ScrollHandle, Rc<Cell<Option<Shown>>>);
1238
1239type Shown = (Option<usize>, Size<Pixels>);
1240
1241fn row_id(id: &SharedString, path: &[usize]) -> SharedString {
1244 let mut out = id.to_string();
1245 for row in path {
1246 out.push('-');
1247 out.push_str(&row.to_string());
1248 }
1249 SharedString::from(out)
1250}
1251
1252pub fn glyph_slot(theme: &Theme, icon: Option<Icon>, enabled: bool) -> gpui::Div {
1255 div()
1256 .flex_none()
1257 .size(px(GLYPH))
1258 .flex()
1259 .items_center()
1260 .justify_center()
1261 .children(icon.map(|glyph| {
1262 icons::icon(glyph).size(px(GLYPH)).text_color(if enabled {
1263 theme.text_faint
1264 } else {
1265 theme.text_faint.opacity(0.5)
1266 })
1267 }))
1268}
1269
1270fn description_line(theme: &Theme, description: SharedString, enabled: bool) -> gpui::Div {
1284 div()
1285 .mt(px(2.0))
1286 .truncate()
1287 .text_style(TextStyle::Subheadline)
1288 .text_color(if enabled {
1289 theme.text_muted
1290 } else {
1291 theme.text_faint.opacity(0.5)
1294 })
1295 .child(description)
1296}
1297
1298fn disabled_row(theme: &Theme) -> gpui::Div {
1302 div()
1303 .flex()
1304 .flex_row()
1305 .items_center()
1306 .gap(px(10.0))
1307 .px(px(8.0))
1308 .py(px(popover::MENU_ROW_PAD_Y))
1309 .rounded(px(Theme::inset_radius(
1312 Theme::surface_radius(),
1313 popover::MENU_PAD,
1314 )))
1315 .text_style(TextStyle::Body)
1316 .text_color(theme.text_faint)
1317}