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!(bezel_menu, [SelectNext, SelectPrevious, Confirm, Dismiss]);
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub struct MenuIndent(pub Pixels);
42
43impl Default for MenuIndent {
44 fn default() -> Self {
45 Self(px(12.0))
46 }
47}
48
49impl Global for MenuIndent {}
50
51pub(crate) fn indent(cx: &App) -> MenuIndent {
52 cx.try_global::<MenuIndent>().copied().unwrap_or_default()
53}
54
55pub(crate) fn set_indent(indent: MenuIndent, cx: &mut App) {
57 cx.set_global(indent);
58 cx.refresh_windows();
59}
60
61#[derive(Clone, Copy, Debug, PartialEq)]
63pub struct MenuWidth {
64 pub min: Pixels,
67 pub described: Described,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq)]
74pub enum Described {
75 Fixed(Pixels),
77 Fit(Pixels),
79}
80
81impl Default for MenuWidth {
82 fn default() -> Self {
83 Self {
84 min: px(180.0),
85 described: Described::Fixed(px(280.0)),
86 }
87 }
88}
89
90impl Global for MenuWidth {}
91
92pub(crate) fn width(cx: &App) -> MenuWidth {
93 cx.try_global::<MenuWidth>().copied().unwrap_or_default()
94}
95
96pub(crate) fn set_width(width: MenuWidth, cx: &mut App) {
98 cx.set_global(width);
99 cx.refresh_windows();
100}
101
102pub const SEARCH_CONTEXT: &str = "MenuSearch";
105
106pub fn bindings() -> Vec<KeyBinding> {
109 let ctx = Some(SEARCH_CONTEXT);
110 let context = Some(crate::context_menu::KEY_CONTEXT);
111 vec![
112 KeyBinding::new("down", SelectNext, ctx),
113 KeyBinding::new("up", SelectPrevious, ctx),
114 KeyBinding::new("ctrl-n", SelectNext, ctx),
115 KeyBinding::new("ctrl-p", SelectPrevious, ctx),
116 KeyBinding::new("enter", Confirm, ctx),
117 KeyBinding::new("escape", Dismiss, ctx),
118 KeyBinding::new("down", SelectNext, context),
119 KeyBinding::new("up", SelectPrevious, context),
120 KeyBinding::new("enter", Confirm, context),
121 KeyBinding::new("escape", Dismiss, context),
122 ]
123}
124
125pub fn init(cx: &mut App) {
127 cx.bind_keys(bindings());
128}
129
130#[derive(Clone, Debug, PartialEq, Eq)]
136pub enum Item {
137 Action {
138 label: SharedString,
139 description: Option<SharedString>,
146 tooltip: Option<SharedString>,
151 icon: Option<Icon>,
154 indent: usize,
156 keystroke: Option<SharedString>,
161 checked: bool,
163 enabled: bool,
164 },
165 Submenu {
169 label: SharedString,
170 icon: Option<Icon>,
171 indent: usize,
172 enabled: bool,
173 items: Vec<Item>,
174 search: Option<SharedString>,
179 },
180 Segmented {
184 segments: Vec<Segment>,
185 selected: usize,
186 enabled: bool,
187 },
188 Heading(SharedString),
190 Separator,
191}
192
193#[derive(Clone, Debug, PartialEq, Eq)]
195pub struct Segment {
196 pub icon: Icon,
197 pub label: SharedString,
198}
199
200impl Segment {
201 pub fn new(icon: impl Into<Icon>, label: impl Into<SharedString>) -> Self {
202 Self {
203 icon: icon.into(),
204 label: label.into(),
205 }
206 }
207}
208
209impl Item {
210 pub fn action(label: impl Into<SharedString>) -> Self {
211 Item::Action {
212 label: label.into(),
213 description: None,
214 tooltip: None,
215 icon: None,
216 indent: 0,
217 keystroke: None,
218 checked: false,
219 enabled: true,
220 }
221 }
222
223 pub fn submenu(label: impl Into<SharedString>, items: Vec<Item>) -> Self {
224 Item::Submenu {
225 label: label.into(),
226 icon: None,
227 indent: 0,
228 enabled: true,
229 items,
230 search: None,
231 }
232 }
233
234 pub fn searchable(mut self, placeholder: impl Into<SharedString>) -> Self {
237 if let Item::Submenu { search, .. } = &mut self {
238 *search = Some(placeholder.into());
239 }
240 self
241 }
242
243 pub fn segmented(segments: impl IntoIterator<Item = Segment>, selected: usize) -> Self {
244 Item::Segmented {
245 segments: segments.into_iter().collect(),
246 selected,
247 enabled: true,
248 }
249 }
250
251 pub fn with_icon(mut self, icon: impl Into<Icon>) -> Self {
254 match &mut self {
255 Item::Action { icon: slot, .. } | Item::Submenu { icon: slot, .. } => {
256 *slot = Some(icon.into())
257 }
258 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => {}
259 }
260 self
261 }
262
263 pub fn indented(mut self, level: usize) -> Self {
267 match &mut self {
268 Item::Action { indent, .. } | Item::Submenu { indent, .. } => *indent = level,
269 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => {}
270 }
271 self
272 }
273
274 pub fn with_description(mut self, description: impl Into<SharedString>) -> Self {
278 if let Item::Action {
279 description: slot, ..
280 } = &mut self
281 {
282 *slot = Some(description.into());
283 }
284 self
285 }
286
287 pub fn with_long_description(self, description: impl Into<SharedString>) -> Self {
291 let description = description.into();
292 self.with_description(description.clone())
293 .with_tooltip(description)
294 }
295
296 pub fn with_tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
301 if let Item::Action { tooltip: slot, .. } = &mut self {
302 *slot = Some(tooltip.into());
303 }
304 self
305 }
306
307 pub fn with_keystroke(mut self, keystroke: impl Into<SharedString>) -> Self {
310 if let Item::Action {
311 keystroke: slot, ..
312 } = &mut self
313 {
314 *slot = Some(keystroke.into());
315 }
316 self
317 }
318
319 pub fn with_shortcut(self, action: &dyn Action, window: &Window) -> Self {
327 match keys::shortcut(action, window) {
328 Some(keystroke) => self.with_keystroke(keystroke),
329 None => self,
330 }
331 }
332
333 pub fn with_shortcut_in(self, action: &dyn Action, context: &str, window: &Window) -> Self {
338 match keys::shortcut_in(action, context, window) {
339 Some(keystroke) => self.with_keystroke(keystroke),
340 None => self,
341 }
342 }
343
344 pub fn checked(mut self, checked: bool) -> Self {
347 if let Item::Action { checked: slot, .. } = &mut self {
348 *slot = checked;
349 }
350 self
351 }
352
353 pub fn disabled(mut self) -> Self {
354 match &mut self {
355 Item::Action { enabled, .. }
356 | Item::Submenu { enabled, .. }
357 | Item::Segmented { enabled, .. } => *enabled = false,
358 Item::Heading(_) | Item::Separator => {}
359 }
360 self
361 }
362
363 pub fn selectable(&self) -> bool {
367 match self {
368 Item::Action { enabled, .. } => *enabled,
369 Item::Submenu { enabled, items, .. } => *enabled && items.iter().any(Item::selectable),
370 Item::Segmented {
371 enabled, segments, ..
372 } => *enabled && !segments.is_empty(),
373 Item::Heading(_) | Item::Separator => false,
374 }
375 }
376
377 fn has_description(&self) -> bool {
378 matches!(
379 self,
380 Item::Action {
381 description: Some(_),
382 ..
383 }
384 )
385 }
386
387 fn has_icon(&self) -> bool {
388 matches!(
389 self,
390 Item::Action { icon: Some(_), .. } | Item::Submenu { icon: Some(_), .. }
391 )
392 }
393
394 fn landing(&self) -> Option<usize> {
396 match self {
397 Item::Segmented {
398 segments, selected, ..
399 } if !segments.is_empty() => Some((*selected).min(segments.len() - 1)),
400 _ => None,
401 }
402 }
403
404 fn keywords(&self) -> String {
406 match self {
407 Item::Action {
408 label, description, ..
409 } => match description {
410 Some(description) => format!("{label} {description}"),
411 None => label.to_string(),
412 },
413 Item::Submenu { label, .. } => label.to_string(),
414 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => String::new(),
415 }
416 }
417
418 fn search(&self) -> Option<&SharedString> {
420 match self {
421 Item::Submenu { search, .. } => search.as_ref(),
422 _ => None,
423 }
424 }
425
426 fn opens(&self) -> Option<&[Item]> {
428 match self {
429 Item::Submenu {
430 enabled: true,
431 items,
432 ..
433 } => Some(items),
434 _ => None,
435 }
436 }
437}
438
439pub fn at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a Item> {
443 let (&row, above) = path.split_last()?;
444 items_at(items, above)?.get(row)
445}
446
447pub fn items_at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a [Item]> {
451 let mut level = items;
452 for &row in path {
453 level = level.get(row)?.opens()?;
454 }
455 Some(level)
456}
457
458fn open_depth(items: &[Item], open: &[usize]) -> usize {
461 let mut level = items;
462 for (depth, &row) in open.iter().enumerate() {
463 match level.get(row).and_then(Item::opens) {
464 Some(inner) => level = inner,
465 None => return depth,
466 }
467 }
468 open.len()
469}
470
471pub fn next_selectable(items: &[Item], from: Option<usize>, delta: isize) -> Option<usize> {
479 let count = items.len();
480 if count == 0 {
481 return None;
482 }
483 let step = if delta >= 0 { 1 } else { -1 };
484 let wrap = |at: usize| (at as isize + step).rem_euclid(count as isize) as usize;
485 let mut at = match from {
488 None if step > 0 => 0,
489 None => count - 1,
490 Some(at) => wrap(at.min(count - 1)),
491 };
492 for _ in 0..count {
493 if items[at].selectable() {
494 return Some(at);
495 }
496 at = wrap(at);
497 }
498 None
499}
500
501#[derive(Clone, Debug, Default, PartialEq, Eq)]
513pub struct Cursor {
514 open: Vec<usize>,
515 row: Option<usize>,
516 segment: Option<usize>,
517}
518
519impl Cursor {
520 pub fn open(&self) -> &[usize] {
522 &self.open
523 }
524
525 pub fn row(&self) -> Option<usize> {
527 self.row
528 }
529
530 pub fn segment(&self) -> Option<usize> {
532 self.segment
533 }
534
535 pub fn nested(&self) -> bool {
538 !self.open.is_empty()
539 }
540
541 pub fn path(&self) -> Option<Vec<usize>> {
544 let row = self.row?;
545 let mut path = self.open.clone();
546 path.push(row);
547 path.extend(self.segment);
548 Some(path)
549 }
550
551 pub fn clear(&mut self) {
553 self.open.clear();
554 self.row = None;
555 self.segment = None;
556 }
557
558 pub fn lit(&self, depth: usize) -> Option<usize> {
561 match depth.cmp(&self.open.len()) {
562 std::cmp::Ordering::Less => Some(self.open[depth]),
563 std::cmp::Ordering::Equal => self.row,
564 std::cmp::Ordering::Greater => None,
565 }
566 }
567
568 pub fn step(&mut self, root: &[Item], delta: isize) {
570 let Some(items) = items_at(root, &self.open) else {
571 return;
572 };
573 self.row = next_selectable(items, self.row, delta);
574 self.segment = self.row.and_then(|row| items[row].landing());
575 }
576
577 pub fn slide(&mut self, root: &[Item], delta: isize) -> bool {
580 let Some(Item::Segmented { segments, .. }) = self
581 .row
582 .and_then(|row| items_at(root, &self.open)?.get(row))
583 else {
584 return false;
585 };
586 let at = self.segment.unwrap_or(0) as isize + delta.signum();
587 self.segment = Some(at.clamp(0, segments.len() as isize - 1) as usize);
588 true
589 }
590
591 pub fn descend(&mut self, root: &[Item]) -> bool {
595 let Some(row) = self.row else { return false };
596 let Some(inner) = items_at(root, &self.open)
597 .and_then(|items| items.get(row))
598 .and_then(Item::opens)
599 else {
600 return false;
601 };
602 self.row = next_selectable(inner, None, 1);
603 self.segment = self.row.and_then(|row| inner[row].landing());
604 self.open.push(row);
605 true
606 }
607
608 pub fn ascend(&mut self) -> bool {
611 match self.open.pop() {
612 Some(row) => {
613 self.row = Some(row);
614 self.segment = None;
615 true
616 }
617 None => false,
618 }
619 }
620
621 pub fn point_at(&mut self, root: &[Item], path: &[usize]) -> bool {
629 let (open, row, segment) = match path {
630 [] => (Vec::new(), None, None),
631 [above @ .., row, segment]
632 if matches!(
633 at(root, &path[..path.len() - 1]),
634 Some(Item::Segmented { .. })
635 ) =>
636 {
637 (above.to_vec(), Some(*row), Some(*segment))
638 }
639 _ if at(root, path).and_then(Item::opens).is_some() => (path.to_vec(), None, None),
640 [above @ .., row] => (
641 above.to_vec(),
642 Some(*row),
643 at(root, path).and_then(Item::landing),
644 ),
645 };
646 if self.open == open && self.row == row && self.segment == segment {
647 return false;
648 }
649 self.open = open;
650 self.row = row;
651 self.segment = segment;
652 true
653 }
654}
655
656#[derive(Clone, Debug, PartialEq, Eq)]
663pub enum Hit {
664 Point(Vec<usize>),
667 Choose(Vec<usize>),
669 Dismiss,
673}
674
675#[derive(Clone, Copy, Debug, PartialEq, Eq)]
678pub enum Edit {
679 Cut,
680 Copy,
681 Paste,
682 SelectAll,
683}
684
685impl Edit {
686 pub fn at(path: &[usize]) -> Option<Self> {
688 match path {
689 [0] => Some(Edit::Cut),
690 [1] => Some(Edit::Copy),
691 [2] => Some(Edit::Paste),
692 [4] => Some(Edit::SelectAll),
693 _ => None,
694 }
695 }
696}
697
698pub fn edit_items(
701 selected: bool,
702 pasteable: bool,
703 actions: [&dyn Action; 4],
704 window: &Window,
705) -> Vec<Item> {
706 let [cut, copy, paste, select_all] = actions;
707 let enable = |item: Item, on: bool| if on { item } else { item.disabled() };
708 vec![
709 enable(Item::action("Cut").with_shortcut(cut, window), selected),
710 enable(Item::action("Copy").with_shortcut(copy, window), selected),
711 enable(
712 Item::action("Paste").with_shortcut(paste, window),
713 pasteable,
714 ),
715 Item::Separator,
716 Item::action("Select All").with_shortcut(select_all, window),
717 ]
718}
719
720pub fn card<V: 'static>(
739 theme: &Theme,
740 id: impl Into<SharedString>,
741 items: &[Item],
742 cursor: &Cursor,
743 window: &mut Window,
744 cx: &mut Context<V>,
745 on: impl Fn(&mut V, Hit, &mut Window, &mut Context<V>) + 'static,
746) -> gpui::Div {
747 let tree = Tree {
748 id: id.into(),
749 panels: 1 + open_depth(items, cursor.open()),
750 outside: Rc::new(Cell::new((None, 0))),
751 chain: popover::Chain::default(),
752 on: Rc::new(on),
753 };
754 tree.panel(theme, items, cursor, &[], None, window, cx)
755}
756
757struct Query {
761 field: Entity<TextField>,
762 focused: bool,
763 _changed: Subscription,
764}
765
766type Listener<E> = Box<dyn Fn(&E, &mut Window, &mut gpui::App)>;
769
770type Reporter<V> = Rc<dyn Fn(&mut V, Hit, &mut Window, &mut Context<V>)>;
772
773struct Tree<V: 'static> {
775 id: SharedString,
776 panels: usize,
778 outside: Rc<Cell<(Option<Point<Pixels>>, usize)>>,
782 chain: popover::Chain,
784 on: Reporter<V>,
785}
786
787impl<V: 'static> Tree<V> {
788 #[allow(clippy::too_many_arguments)]
789 fn panel(
790 &self,
791 theme: &Theme,
792 items: &[Item],
793 cursor: &Cursor,
794 prefix: &[usize],
795 search: Option<&SharedString>,
796 window: &mut Window,
797 cx: &mut Context<V>,
798 ) -> gpui::Div {
799 let depth = prefix.len();
800 let lit = cursor.lit(depth);
801 let down = cursor.open().get(depth).copied();
802 let rows_id = SharedString::from(format!("{}-rows", row_id(&self.id, prefix)));
803 let Scrolled(handle, shown) = window
804 .use_keyed_state(
805 SharedString::from(format!("{rows_id}-scrolled")),
806 cx,
807 |_, _| Scrolled::default(),
808 )
809 .read(cx)
810 .clone();
811 let query = search.map(|placeholder| self.query(&rows_id, placeholder, window, cx));
812 let text = query
813 .as_ref()
814 .map(|query| query.read(cx).field.read(cx).content().trim().to_owned())
815 .unwrap_or_default();
816 let visible: Vec<usize> = if text.is_empty() {
819 (0..items.len()).collect()
820 } else {
821 let words: Vec<String> = items.iter().map(Item::keywords).collect();
822 popover::filter_indices(&text, &words)
823 .into_iter()
824 .filter(|&row| items[row].selectable())
825 .collect()
826 };
827 let size = handle.bounds().size;
832 if size.height > Pixels::ZERO && shown.get() != Some((lit, size)) {
833 shown.set(Some((lit, size)));
834 if let Some(at) = lit.and_then(|lit| visible.iter().position(|&row| row == lit)) {
835 handle.scroll_to_item(at);
836 }
837 }
838 let inset = px(popover::SNAP) + window.client_inset().unwrap_or(Pixels::ZERO);
839 let cap = window.viewport_size().height - inset * 2.0;
840 let gutter = items.iter().any(Item::has_icon);
843 let MenuIndent(step) = indent(cx);
844 let described = items.iter().any(Item::has_description);
845 let width = width(cx);
846 let rows = div()
847 .id(rows_id.clone())
848 .p(px(popover::MENU_PAD))
849 .children(visible.iter().map(|&row| {
850 let item = &items[row];
851 match item {
852 Item::Separator => return popover::divider(theme).into_any_element(),
853 Item::Heading(label) => {
854 return popover::menu_heading(theme, label.clone()).into_any_element();
855 }
856 _ => {}
857 }
858 let path: Vec<usize> = prefix.iter().copied().chain([row]).collect();
859 if let Item::Segmented {
860 segments,
861 selected,
862 enabled,
863 } = item
864 {
865 let live = cursor.segment().filter(|_| lit == Some(row));
866 return self
867 .segmented(theme, segments, *selected, *enabled, live, &path, cx)
868 .into_any_element();
869 }
870 let id = row_id(&self.id, &path);
871 let (label, icon, indent, enabled) = match item {
872 Item::Action {
873 label,
874 icon,
875 indent,
876 enabled,
877 ..
878 }
879 | Item::Submenu {
880 label,
881 icon,
882 indent,
883 enabled,
884 ..
885 } => (label.clone(), icon.clone(), *indent, *enabled),
886 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => {
887 unreachable!("returned above")
888 }
889 };
890 let (description, hint) = match item {
891 Item::Action {
892 description,
893 tooltip,
894 ..
895 } => (description.clone(), tooltip.clone()),
896 _ => (None, None),
897 };
898 let row = if enabled {
899 popover::menu_row(theme, lit == Some(row), None)
900 .id(id.clone())
901 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
902 .on_click(self.reports(
903 match item {
904 Item::Submenu { .. } => Hit::Point(path.clone()),
907 _ => Hit::Choose(path.clone()),
908 },
909 cx,
910 ))
911 } else {
912 disabled_row(theme).id(id.clone())
913 };
914 let selector = format!("{}/{label}", self.id);
915 let row = row.debug_selector(move || selector);
916 let row = row.when_some(hint, |row, hint| {
917 row.tooltip(move |window, cx| Tooltip::text(hint.clone(), window, cx))
918 });
919 row.when(indent > 0, |row| {
920 row.pl(px(popover::MENU_ROW_INSET) + step * indent as f32)
921 })
922 .when(gutter, |row| row.child(glyph_slot(theme, icon, enabled)))
923 .child(
924 div()
925 .flex_1()
926 .min_w_0()
927 .flex()
928 .flex_col()
929 .child(div().truncate().child(label))
930 .children(
931 description
932 .map(|description| description_line(theme, description, enabled)),
933 ),
934 )
935 .map(|row| match item {
936 Item::Action {
937 keystroke, checked, ..
938 } => row
939 .when(*checked, |row| {
940 row.child(
941 icons::icon(icons::glyph::Check)
942 .size(px(GLYPH))
943 .text_color(theme.text),
944 )
945 })
946 .when_some(keystroke.clone(), |row, keystroke| {
947 row.child(popover::kbd_hint(theme, &keystroke))
948 }),
949 _ => row.child(
950 icons::icon(icons::glyph::ChevronRight)
951 .size(px(GLYPH))
952 .text_color(theme.text_faint),
953 ),
954 })
955 .when_some(
956 item.opens().filter(|_| down == Some(path[depth])),
957 |parent, inner| {
958 let panel = self
959 .panel(theme, inner, cursor, &path, item.search(), window, cx)
960 .into_any_element();
961 parent.relative().child(popover::anchored_submenu(
962 SharedString::from(format!("{id}-panel")),
963 panel,
964 &self.chain,
965 ))
966 },
967 )
968 .into_any_element()
969 }));
970 popover::popover_card(theme)
971 .p_0()
972 .flex()
973 .flex_col()
974 .max_h(cap)
975 .map(|card| match (described, width.described) {
976 (true, Described::Fixed(wide)) => card.w(wide),
977 (true, Described::Fit(most)) => card.min_w(width.min).max_w(most),
978 (false, _) => card.min_w(width.min),
979 })
980 .on_mouse_down_out(self.dismissal(cx))
981 .map(|card| match query {
982 None => card.child(
983 scroll::Viewport::new(rows_id, rows, Axis::Vertical)
984 .track_scroll(&handle)
985 .fill(),
986 ),
987 Some(query) => {
988 let field = query.read(cx).field.clone();
989 let rows = rows.max_h(px(
990 SEARCH_ROWS * popover::menu_row_height() + 2.0 * popover::MENU_PAD
991 ));
992 card.child(
993 div()
994 .key_context(SEARCH_CONTEXT)
995 .px(px(popover::MENU_PAD))
996 .pt(px(popover::MENU_PAD))
997 .map(|el| self.keys(el, items, &visible, lit, prefix, cx))
998 .child(popover::search_line(theme, field.into_any_element())),
999 )
1000 .child(match visible.is_empty() {
1001 true => div()
1002 .px(px(popover::MENU_PAD + popover::MENU_ROW_INSET))
1003 .pb(px(8.0))
1004 .text_style(TextStyle::Body)
1005 .text_color(theme.text_muted)
1006 .child("No matches")
1007 .into_any_element(),
1008 false => scroll::Viewport::new(rows_id, rows, Axis::Vertical)
1009 .track_scroll(&handle)
1010 .into_any_element(),
1011 })
1012 }
1013 })
1014 }
1015
1016 fn query(
1019 &self,
1020 key: &SharedString,
1021 placeholder: &SharedString,
1022 window: &mut Window,
1023 cx: &mut Context<V>,
1024 ) -> Entity<Query> {
1025 let host = cx.entity_id();
1026 let placeholder = placeholder.clone();
1027 let query = window.use_keyed_state(
1028 SharedString::from(format!("{key}-query")),
1029 cx,
1030 move |_, cx| {
1031 let field = cx.new(|cx| crate::search::query_field(placeholder, cx));
1032 let _changed = cx.subscribe(&field, move |_, _, _: &FieldEvent, cx| {
1033 let app: &mut App = cx;
1034 app.notify(host);
1035 });
1036 Query {
1037 field,
1038 focused: false,
1039 _changed,
1040 }
1041 },
1042 );
1043 if !query.read(cx).focused {
1044 query.update(cx, |query, _| query.focused = true);
1045 let handle = query.read(cx).field.focus_handle(cx);
1046 window.focus(&handle, cx);
1047 }
1048 query
1049 }
1050
1051 fn keys(
1054 &self,
1055 el: gpui::Div,
1056 items: &[Item],
1057 shown: &[usize],
1058 lit: Option<usize>,
1059 prefix: &[usize],
1060 cx: &mut Context<V>,
1061 ) -> gpui::Div {
1062 let landable: Rc<Vec<usize>> = Rc::new(
1063 shown
1064 .iter()
1065 .copied()
1066 .filter(|&row| items[row].selectable())
1067 .collect(),
1068 );
1069 let at = lit.and_then(|lit| landable.iter().position(|&row| row == lit));
1070 let path = {
1071 let prefix = prefix.to_vec();
1072 move |row: usize| prefix.iter().copied().chain([row]).collect::<Vec<_>>()
1073 };
1074 let step = |delta: isize| {
1075 let count = landable.len() as isize;
1076 (count > 0).then(|| {
1077 let next = match at {
1078 Some(at) => (at as isize + delta).rem_euclid(count),
1079 None if delta > 0 => 0,
1080 None => count - 1,
1081 };
1082 landable[next as usize]
1083 })
1084 };
1085 let next = step(1).map(&path);
1086 let previous = step(-1).map(&path);
1087 let chosen = at
1088 .map(|at| landable[at])
1089 .or_else(|| landable.first().copied())
1090 .map(|row| match items[row].opens() {
1091 Some(_) => Hit::Point(path(row)),
1092 None => Hit::Choose(path(row)),
1093 });
1094 let on = self.on.clone();
1095 let report = move |hit: Option<Hit>| {
1096 let on = on.clone();
1097 move |view: &mut V, window: &mut Window, cx: &mut Context<V>| {
1098 if let Some(hit) = hit.clone() {
1099 on(view, hit, window, cx);
1100 }
1101 }
1102 };
1103 let (down, up, enter, escape) = (
1104 report(next.map(Hit::Point)),
1105 report(previous.map(Hit::Point)),
1106 report(chosen),
1107 report(Some(Hit::Dismiss)),
1108 );
1109 el.on_action(cx.listener(move |view, _: &SelectNext, window, cx| down(view, window, cx)))
1110 .on_action(
1111 cx.listener(move |view, _: &SelectPrevious, window, cx| up(view, window, cx)),
1112 )
1113 .on_action(cx.listener(move |view, _: &Confirm, window, cx| enter(view, window, cx)))
1114 .on_action(cx.listener(move |view, _: &Dismiss, window, cx| escape(view, window, cx)))
1115 }
1116
1117 #[allow(clippy::too_many_arguments)]
1120 fn segmented(
1121 &self,
1122 theme: &Theme,
1123 segments: &[Segment],
1124 selected: usize,
1125 enabled: bool,
1126 live: Option<usize>,
1127 path: &[usize],
1128 cx: &mut Context<V>,
1129 ) -> gpui::Stateful<gpui::Div> {
1130 div()
1131 .id(row_id(&self.id, path))
1132 .flex()
1133 .flex_row()
1134 .gap(px(popover::MENU_PAD))
1135 .children(segments.iter().enumerate().map(|(at, segment)| {
1136 let path: Vec<usize> = path.iter().copied().chain([at]).collect();
1137 let chosen = at == selected;
1138 let ink = match (enabled, chosen || live == Some(at)) {
1139 (false, _) => theme.text_faint.opacity(0.5),
1140 (true, true) => theme.text,
1141 (true, false) => theme.text_muted,
1142 };
1143 let cell = div()
1144 .id(row_id(&self.id, &path))
1145 .flex_1()
1146 .min_w_0()
1147 .flex()
1148 .flex_col()
1149 .items_center()
1150 .gap(px(4.0))
1151 .px(px(8.0))
1152 .py(px(6.0))
1153 .rounded(px(Theme::inset_radius(
1154 Theme::surface_radius(),
1155 popover::MENU_PAD,
1156 )))
1157 .text_style(TextStyle::Callout)
1158 .text_color(ink)
1159 .when(chosen, |cell| {
1160 cell.bg(theme.element_active)
1161 .font_weight(gpui::FontWeight::MEDIUM)
1162 })
1163 .when(!chosen && live == Some(at), |cell| {
1164 cell.bg(theme.card_selected_bg())
1165 })
1166 .child(
1167 icons::icon(segment.icon.clone())
1168 .size(px(SEGMENT_GLYPH))
1169 .text_color(ink),
1170 )
1171 .child(div().max_w_full().truncate().child(segment.label.clone()));
1172 match enabled {
1173 true => cell
1174 .cursor_pointer()
1175 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
1176 .on_click(self.reports(Hit::Choose(path), cx)),
1177 false => cell,
1178 }
1179 }))
1180 }
1181
1182 fn dismissal(&self, cx: &mut Context<V>) -> Listener<MouseDownEvent> {
1194 let outside = self.outside.clone();
1195 let panels = self.panels;
1196 let on = self.on.clone();
1197 Box::new(
1198 cx.listener(move |view, event: &MouseDownEvent, window, cx| {
1199 let (at, count) = outside.get();
1200 let count = if at == Some(event.position) {
1201 count + 1
1202 } else {
1203 1
1204 };
1205 outside.set((Some(event.position), count));
1206 if count >= panels {
1207 outside.set((None, 0));
1208 on(view, Hit::Dismiss, window, cx);
1209 cx.stop_propagation();
1210 }
1211 }),
1212 )
1213 }
1214
1215 fn reports<E: 'static>(&self, hit: Hit, cx: &mut Context<V>) -> Listener<E> {
1217 let on = self.on.clone();
1218 Box::new(cx.listener(move |view, _: &E, window, cx| on(view, hit.clone(), window, cx)))
1219 }
1220}
1221
1222#[derive(Clone, Default)]
1225struct Scrolled(ScrollHandle, Rc<Cell<Option<Shown>>>);
1226
1227type Shown = (Option<usize>, Size<Pixels>);
1228
1229fn row_id(id: &SharedString, path: &[usize]) -> SharedString {
1232 let mut out = id.to_string();
1233 for row in path {
1234 out.push('-');
1235 out.push_str(&row.to_string());
1236 }
1237 SharedString::from(out)
1238}
1239
1240pub fn glyph_slot(theme: &Theme, icon: Option<Icon>, enabled: bool) -> gpui::Div {
1243 div()
1244 .flex_none()
1245 .size(px(GLYPH))
1246 .flex()
1247 .items_center()
1248 .justify_center()
1249 .children(icon.map(|glyph| {
1250 icons::icon(glyph).size(px(GLYPH)).text_color(if enabled {
1251 theme.text_faint
1252 } else {
1253 theme.text_faint.opacity(0.5)
1254 })
1255 }))
1256}
1257
1258fn description_line(theme: &Theme, description: SharedString, enabled: bool) -> gpui::Div {
1272 div()
1273 .mt(px(2.0))
1274 .truncate()
1275 .text_style(TextStyle::Subheadline)
1276 .text_color(if enabled {
1277 theme.text_muted
1278 } else {
1279 theme.text_faint.opacity(0.5)
1282 })
1283 .child(description)
1284}
1285
1286fn disabled_row(theme: &Theme) -> gpui::Div {
1290 div()
1291 .flex()
1292 .flex_row()
1293 .items_center()
1294 .gap(px(10.0))
1295 .px(px(8.0))
1296 .py(px(popover::MENU_ROW_PAD_Y))
1297 .rounded(px(Theme::inset_radius(
1300 Theme::surface_radius(),
1301 popover::MENU_PAD,
1302 )))
1303 .text_style(TextStyle::Body)
1304 .text_color(theme.text_faint)
1305}