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 PANEL_MIN: f32 = 180.0;
36const PANEL_DESCRIBED: f32 = 280.0;
42
43const SEARCH_ROWS: f32 = 10.0;
45
46actions!(bezel_menu, [SelectNext, SelectPrevious, Confirm, Dismiss]);
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct MenuIndent(pub Pixels);
51
52impl Default for MenuIndent {
53 fn default() -> Self {
54 Self(px(12.0))
55 }
56}
57
58impl Global for MenuIndent {}
59
60pub(crate) fn indent(cx: &App) -> MenuIndent {
61 cx.try_global::<MenuIndent>().copied().unwrap_or_default()
62}
63
64pub(crate) fn set_indent(indent: MenuIndent, cx: &mut App) {
66 cx.set_global(indent);
67 cx.refresh_windows();
68}
69
70pub const SEARCH_CONTEXT: &str = "MenuSearch";
73
74pub fn bindings() -> Vec<KeyBinding> {
77 let ctx = Some(SEARCH_CONTEXT);
78 let context = Some(crate::context_menu::KEY_CONTEXT);
79 vec![
80 KeyBinding::new("down", SelectNext, ctx),
81 KeyBinding::new("up", SelectPrevious, ctx),
82 KeyBinding::new("ctrl-n", SelectNext, ctx),
83 KeyBinding::new("ctrl-p", SelectPrevious, ctx),
84 KeyBinding::new("enter", Confirm, ctx),
85 KeyBinding::new("escape", Dismiss, ctx),
86 KeyBinding::new("down", SelectNext, context),
87 KeyBinding::new("up", SelectPrevious, context),
88 KeyBinding::new("enter", Confirm, context),
89 KeyBinding::new("escape", Dismiss, context),
90 ]
91}
92
93pub fn init(cx: &mut App) {
95 cx.bind_keys(bindings());
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
104pub enum Item {
105 Action {
106 label: SharedString,
107 description: Option<SharedString>,
114 tooltip: Option<SharedString>,
119 icon: Option<Icon>,
122 indent: usize,
124 keystroke: Option<SharedString>,
129 checked: bool,
131 enabled: bool,
132 },
133 Submenu {
137 label: SharedString,
138 icon: Option<Icon>,
139 indent: usize,
140 enabled: bool,
141 items: Vec<Item>,
142 search: Option<SharedString>,
147 },
148 Segmented {
152 segments: Vec<Segment>,
153 selected: usize,
154 enabled: bool,
155 },
156 Heading(SharedString),
158 Separator,
159}
160
161#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct Segment {
164 pub icon: Icon,
165 pub label: SharedString,
166}
167
168impl Segment {
169 pub fn new(icon: impl Into<Icon>, label: impl Into<SharedString>) -> Self {
170 Self {
171 icon: icon.into(),
172 label: label.into(),
173 }
174 }
175}
176
177impl Item {
178 pub fn action(label: impl Into<SharedString>) -> Self {
179 Item::Action {
180 label: label.into(),
181 description: None,
182 tooltip: None,
183 icon: None,
184 indent: 0,
185 keystroke: None,
186 checked: false,
187 enabled: true,
188 }
189 }
190
191 pub fn submenu(label: impl Into<SharedString>, items: Vec<Item>) -> Self {
192 Item::Submenu {
193 label: label.into(),
194 icon: None,
195 indent: 0,
196 enabled: true,
197 items,
198 search: None,
199 }
200 }
201
202 pub fn searchable(mut self, placeholder: impl Into<SharedString>) -> Self {
205 if let Item::Submenu { search, .. } = &mut self {
206 *search = Some(placeholder.into());
207 }
208 self
209 }
210
211 pub fn segmented(segments: impl IntoIterator<Item = Segment>, selected: usize) -> Self {
212 Item::Segmented {
213 segments: segments.into_iter().collect(),
214 selected,
215 enabled: true,
216 }
217 }
218
219 pub fn with_icon(mut self, icon: impl Into<Icon>) -> Self {
222 match &mut self {
223 Item::Action { icon: slot, .. } | Item::Submenu { icon: slot, .. } => {
224 *slot = Some(icon.into())
225 }
226 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => {}
227 }
228 self
229 }
230
231 pub fn indented(mut self, level: usize) -> Self {
235 match &mut self {
236 Item::Action { indent, .. } | Item::Submenu { indent, .. } => *indent = level,
237 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => {}
238 }
239 self
240 }
241
242 pub fn with_description(mut self, description: impl Into<SharedString>) -> Self {
246 if let Item::Action {
247 description: slot, ..
248 } = &mut self
249 {
250 *slot = Some(description.into());
251 }
252 self
253 }
254
255 pub fn with_long_description(self, description: impl Into<SharedString>) -> Self {
259 let description = description.into();
260 self.with_description(description.clone())
261 .with_tooltip(description)
262 }
263
264 pub fn with_tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
269 if let Item::Action { tooltip: slot, .. } = &mut self {
270 *slot = Some(tooltip.into());
271 }
272 self
273 }
274
275 pub fn with_keystroke(mut self, keystroke: impl Into<SharedString>) -> Self {
278 if let Item::Action {
279 keystroke: slot, ..
280 } = &mut self
281 {
282 *slot = Some(keystroke.into());
283 }
284 self
285 }
286
287 pub fn with_shortcut(self, action: &dyn Action, window: &Window) -> Self {
295 match keys::shortcut(action, window) {
296 Some(keystroke) => self.with_keystroke(keystroke),
297 None => self,
298 }
299 }
300
301 pub fn with_shortcut_in(self, action: &dyn Action, context: &str, window: &Window) -> Self {
306 match keys::shortcut_in(action, context, window) {
307 Some(keystroke) => self.with_keystroke(keystroke),
308 None => self,
309 }
310 }
311
312 pub fn checked(mut self, checked: bool) -> Self {
315 if let Item::Action { checked: slot, .. } = &mut self {
316 *slot = checked;
317 }
318 self
319 }
320
321 pub fn disabled(mut self) -> Self {
322 match &mut self {
323 Item::Action { enabled, .. }
324 | Item::Submenu { enabled, .. }
325 | Item::Segmented { enabled, .. } => *enabled = false,
326 Item::Heading(_) | Item::Separator => {}
327 }
328 self
329 }
330
331 pub fn selectable(&self) -> bool {
335 match self {
336 Item::Action { enabled, .. } => *enabled,
337 Item::Submenu { enabled, items, .. } => *enabled && items.iter().any(Item::selectable),
338 Item::Segmented {
339 enabled, segments, ..
340 } => *enabled && !segments.is_empty(),
341 Item::Heading(_) | Item::Separator => false,
342 }
343 }
344
345 fn has_description(&self) -> bool {
346 matches!(
347 self,
348 Item::Action {
349 description: Some(_),
350 ..
351 }
352 )
353 }
354
355 fn has_icon(&self) -> bool {
356 matches!(
357 self,
358 Item::Action { icon: Some(_), .. } | Item::Submenu { icon: Some(_), .. }
359 )
360 }
361
362 fn landing(&self) -> Option<usize> {
364 match self {
365 Item::Segmented {
366 segments, selected, ..
367 } if !segments.is_empty() => Some((*selected).min(segments.len() - 1)),
368 _ => None,
369 }
370 }
371
372 fn keywords(&self) -> String {
374 match self {
375 Item::Action {
376 label, description, ..
377 } => match description {
378 Some(description) => format!("{label} {description}"),
379 None => label.to_string(),
380 },
381 Item::Submenu { label, .. } => label.to_string(),
382 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => String::new(),
383 }
384 }
385
386 fn search(&self) -> Option<&SharedString> {
388 match self {
389 Item::Submenu { search, .. } => search.as_ref(),
390 _ => None,
391 }
392 }
393
394 fn opens(&self) -> Option<&[Item]> {
396 match self {
397 Item::Submenu {
398 enabled: true,
399 items,
400 ..
401 } => Some(items),
402 _ => None,
403 }
404 }
405}
406
407pub fn at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a Item> {
411 let (&row, above) = path.split_last()?;
412 items_at(items, above)?.get(row)
413}
414
415pub fn items_at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a [Item]> {
419 let mut level = items;
420 for &row in path {
421 level = level.get(row)?.opens()?;
422 }
423 Some(level)
424}
425
426fn open_depth(items: &[Item], open: &[usize]) -> usize {
429 let mut level = items;
430 for (depth, &row) in open.iter().enumerate() {
431 match level.get(row).and_then(Item::opens) {
432 Some(inner) => level = inner,
433 None => return depth,
434 }
435 }
436 open.len()
437}
438
439pub fn next_selectable(items: &[Item], from: Option<usize>, delta: isize) -> Option<usize> {
447 let count = items.len();
448 if count == 0 {
449 return None;
450 }
451 let step = if delta >= 0 { 1 } else { -1 };
452 let wrap = |at: usize| (at as isize + step).rem_euclid(count as isize) as usize;
453 let mut at = match from {
456 None if step > 0 => 0,
457 None => count - 1,
458 Some(at) => wrap(at.min(count - 1)),
459 };
460 for _ in 0..count {
461 if items[at].selectable() {
462 return Some(at);
463 }
464 at = wrap(at);
465 }
466 None
467}
468
469#[derive(Clone, Debug, Default, PartialEq, Eq)]
481pub struct Cursor {
482 open: Vec<usize>,
483 row: Option<usize>,
484 segment: Option<usize>,
485}
486
487impl Cursor {
488 pub fn open(&self) -> &[usize] {
490 &self.open
491 }
492
493 pub fn row(&self) -> Option<usize> {
495 self.row
496 }
497
498 pub fn segment(&self) -> Option<usize> {
500 self.segment
501 }
502
503 pub fn nested(&self) -> bool {
506 !self.open.is_empty()
507 }
508
509 pub fn path(&self) -> Option<Vec<usize>> {
512 let row = self.row?;
513 let mut path = self.open.clone();
514 path.push(row);
515 path.extend(self.segment);
516 Some(path)
517 }
518
519 pub fn clear(&mut self) {
521 self.open.clear();
522 self.row = None;
523 self.segment = None;
524 }
525
526 pub fn lit(&self, depth: usize) -> Option<usize> {
529 match depth.cmp(&self.open.len()) {
530 std::cmp::Ordering::Less => Some(self.open[depth]),
531 std::cmp::Ordering::Equal => self.row,
532 std::cmp::Ordering::Greater => None,
533 }
534 }
535
536 pub fn step(&mut self, root: &[Item], delta: isize) {
538 let Some(items) = items_at(root, &self.open) else {
539 return;
540 };
541 self.row = next_selectable(items, self.row, delta);
542 self.segment = self.row.and_then(|row| items[row].landing());
543 }
544
545 pub fn slide(&mut self, root: &[Item], delta: isize) -> bool {
548 let Some(Item::Segmented { segments, .. }) = self
549 .row
550 .and_then(|row| items_at(root, &self.open)?.get(row))
551 else {
552 return false;
553 };
554 let at = self.segment.unwrap_or(0) as isize + delta.signum();
555 self.segment = Some(at.clamp(0, segments.len() as isize - 1) as usize);
556 true
557 }
558
559 pub fn descend(&mut self, root: &[Item]) -> bool {
563 let Some(row) = self.row else { return false };
564 let Some(inner) = items_at(root, &self.open)
565 .and_then(|items| items.get(row))
566 .and_then(Item::opens)
567 else {
568 return false;
569 };
570 self.row = next_selectable(inner, None, 1);
571 self.segment = self.row.and_then(|row| inner[row].landing());
572 self.open.push(row);
573 true
574 }
575
576 pub fn ascend(&mut self) -> bool {
579 match self.open.pop() {
580 Some(row) => {
581 self.row = Some(row);
582 self.segment = None;
583 true
584 }
585 None => false,
586 }
587 }
588
589 pub fn point_at(&mut self, root: &[Item], path: &[usize]) -> bool {
597 let (open, row, segment) = match path {
598 [] => (Vec::new(), None, None),
599 [above @ .., row, segment]
600 if matches!(
601 at(root, &path[..path.len() - 1]),
602 Some(Item::Segmented { .. })
603 ) =>
604 {
605 (above.to_vec(), Some(*row), Some(*segment))
606 }
607 _ if at(root, path).and_then(Item::opens).is_some() => (path.to_vec(), None, None),
608 [above @ .., row] => (
609 above.to_vec(),
610 Some(*row),
611 at(root, path).and_then(Item::landing),
612 ),
613 };
614 if self.open == open && self.row == row && self.segment == segment {
615 return false;
616 }
617 self.open = open;
618 self.row = row;
619 self.segment = segment;
620 true
621 }
622}
623
624#[derive(Clone, Debug, PartialEq, Eq)]
631pub enum Hit {
632 Point(Vec<usize>),
635 Choose(Vec<usize>),
637 Dismiss,
641}
642
643#[derive(Clone, Copy, Debug, PartialEq, Eq)]
646pub enum Edit {
647 Cut,
648 Copy,
649 Paste,
650 SelectAll,
651}
652
653impl Edit {
654 pub fn at(path: &[usize]) -> Option<Self> {
656 match path {
657 [0] => Some(Edit::Cut),
658 [1] => Some(Edit::Copy),
659 [2] => Some(Edit::Paste),
660 [4] => Some(Edit::SelectAll),
661 _ => None,
662 }
663 }
664}
665
666pub fn edit_items(
669 selected: bool,
670 pasteable: bool,
671 actions: [&dyn Action; 4],
672 window: &Window,
673) -> Vec<Item> {
674 let [cut, copy, paste, select_all] = actions;
675 let enable = |item: Item, on: bool| if on { item } else { item.disabled() };
676 vec![
677 enable(Item::action("Cut").with_shortcut(cut, window), selected),
678 enable(Item::action("Copy").with_shortcut(copy, window), selected),
679 enable(
680 Item::action("Paste").with_shortcut(paste, window),
681 pasteable,
682 ),
683 Item::Separator,
684 Item::action("Select All").with_shortcut(select_all, window),
685 ]
686}
687
688pub fn card<V: 'static>(
707 theme: &Theme,
708 id: impl Into<SharedString>,
709 items: &[Item],
710 cursor: &Cursor,
711 window: &mut Window,
712 cx: &mut Context<V>,
713 on: impl Fn(&mut V, Hit, &mut Window, &mut Context<V>) + 'static,
714) -> gpui::Div {
715 let tree = Tree {
716 id: id.into(),
717 panels: 1 + open_depth(items, cursor.open()),
718 outside: Rc::new(Cell::new((None, 0))),
719 chain: popover::Chain::default(),
720 on: Rc::new(on),
721 };
722 tree.panel(theme, items, cursor, &[], None, window, cx)
723}
724
725struct Query {
729 field: Entity<TextField>,
730 focused: bool,
731 _changed: Subscription,
732}
733
734type Listener<E> = Box<dyn Fn(&E, &mut Window, &mut gpui::App)>;
737
738type Reporter<V> = Rc<dyn Fn(&mut V, Hit, &mut Window, &mut Context<V>)>;
740
741struct Tree<V: 'static> {
743 id: SharedString,
744 panels: usize,
746 outside: Rc<Cell<(Option<Point<Pixels>>, usize)>>,
750 chain: popover::Chain,
752 on: Reporter<V>,
753}
754
755impl<V: 'static> Tree<V> {
756 #[allow(clippy::too_many_arguments)]
757 fn panel(
758 &self,
759 theme: &Theme,
760 items: &[Item],
761 cursor: &Cursor,
762 prefix: &[usize],
763 search: Option<&SharedString>,
764 window: &mut Window,
765 cx: &mut Context<V>,
766 ) -> gpui::Div {
767 let depth = prefix.len();
768 let lit = cursor.lit(depth);
769 let down = cursor.open().get(depth).copied();
770 let rows_id = SharedString::from(format!("{}-rows", row_id(&self.id, prefix)));
771 let Scrolled(handle, shown) = window
772 .use_keyed_state(
773 SharedString::from(format!("{rows_id}-scrolled")),
774 cx,
775 |_, _| Scrolled::default(),
776 )
777 .read(cx)
778 .clone();
779 let query = search.map(|placeholder| self.query(&rows_id, placeholder, window, cx));
780 let text = query
781 .as_ref()
782 .map(|query| query.read(cx).field.read(cx).content().trim().to_owned())
783 .unwrap_or_default();
784 let visible: Vec<usize> = if text.is_empty() {
787 (0..items.len()).collect()
788 } else {
789 let words: Vec<String> = items.iter().map(Item::keywords).collect();
790 popover::filter_indices(&text, &words)
791 .into_iter()
792 .filter(|&row| items[row].selectable())
793 .collect()
794 };
795 let size = handle.bounds().size;
800 if size.height > Pixels::ZERO && shown.get() != Some((lit, size)) {
801 shown.set(Some((lit, size)));
802 if let Some(at) = lit.and_then(|lit| visible.iter().position(|&row| row == lit)) {
803 handle.scroll_to_item(at);
804 }
805 }
806 let inset = px(popover::SNAP) + window.client_inset().unwrap_or(Pixels::ZERO);
807 let cap = window.viewport_size().height - inset * 2.0;
808 let gutter = items.iter().any(Item::has_icon);
811 let MenuIndent(step) = indent(cx);
812 let described = items.iter().any(Item::has_description);
813 let rows = div()
814 .id(rows_id.clone())
815 .p(px(popover::MENU_PAD))
816 .children(visible.iter().map(|&row| {
817 let item = &items[row];
818 match item {
819 Item::Separator => return popover::divider(theme).into_any_element(),
820 Item::Heading(label) => {
821 return popover::menu_heading(theme, label.clone()).into_any_element();
822 }
823 _ => {}
824 }
825 let path: Vec<usize> = prefix.iter().copied().chain([row]).collect();
826 if let Item::Segmented {
827 segments,
828 selected,
829 enabled,
830 } = item
831 {
832 let live = cursor.segment().filter(|_| lit == Some(row));
833 return self
834 .segmented(theme, segments, *selected, *enabled, live, &path, cx)
835 .into_any_element();
836 }
837 let id = row_id(&self.id, &path);
838 let (label, icon, indent, enabled) = match item {
839 Item::Action {
840 label,
841 icon,
842 indent,
843 enabled,
844 ..
845 }
846 | Item::Submenu {
847 label,
848 icon,
849 indent,
850 enabled,
851 ..
852 } => (label.clone(), icon.clone(), *indent, *enabled),
853 Item::Segmented { .. } | Item::Heading(_) | Item::Separator => {
854 unreachable!("returned above")
855 }
856 };
857 let (description, hint) = match item {
858 Item::Action {
859 description,
860 tooltip,
861 ..
862 } => (description.clone(), tooltip.clone()),
863 _ => (None, None),
864 };
865 let row = if enabled {
866 popover::menu_row(theme, lit == Some(row), None)
867 .id(id.clone())
868 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
869 .on_click(self.reports(
870 match item {
871 Item::Submenu { .. } => Hit::Point(path.clone()),
874 _ => Hit::Choose(path.clone()),
875 },
876 cx,
877 ))
878 } else {
879 disabled_row(theme).id(id.clone())
880 };
881 let selector = format!("{}/{label}", self.id);
882 let row = row.debug_selector(move || selector);
883 let row = row.when_some(hint, |row, hint| {
884 row.tooltip(move |window, cx| Tooltip::text(hint.clone(), window, cx))
885 });
886 row.when(indent > 0, |row| {
887 row.pl(px(popover::MENU_ROW_INSET) + step * indent as f32)
888 })
889 .when(gutter, |row| row.child(glyph_slot(theme, icon, enabled)))
890 .child(
891 div()
892 .flex_1()
893 .min_w_0()
894 .flex()
895 .flex_col()
896 .child(div().truncate().child(label))
897 .children(
898 description
899 .map(|description| description_line(theme, description, enabled)),
900 ),
901 )
902 .map(|row| match item {
903 Item::Action {
904 keystroke, checked, ..
905 } => row
906 .when(*checked, |row| {
907 row.child(
908 icons::icon(icons::glyph::Check)
909 .size(px(GLYPH))
910 .text_color(theme.text),
911 )
912 })
913 .when_some(keystroke.clone(), |row, keystroke| {
914 row.child(popover::kbd_hint(theme, &keystroke))
915 }),
916 _ => row.child(
917 icons::icon(icons::glyph::ChevronRight)
918 .size(px(GLYPH))
919 .text_color(theme.text_faint),
920 ),
921 })
922 .when_some(
923 item.opens().filter(|_| down == Some(path[depth])),
924 |parent, inner| {
925 let panel = self
926 .panel(theme, inner, cursor, &path, item.search(), window, cx)
927 .into_any_element();
928 parent.relative().child(popover::anchored_submenu(
929 SharedString::from(format!("{id}-panel")),
930 panel,
931 &self.chain,
932 ))
933 },
934 )
935 .into_any_element()
936 }));
937 popover::popover_card(theme)
938 .p_0()
939 .flex()
940 .flex_col()
941 .max_h(cap)
942 .map(|card| match described {
943 true => card.w(px(PANEL_DESCRIBED)),
944 false => card.min_w(px(PANEL_MIN)),
945 })
946 .on_mouse_down_out(self.dismissal(cx))
947 .map(|card| match query {
948 None => card.child(
949 scroll::Viewport::new(rows_id, rows, Axis::Vertical)
950 .track_scroll(&handle)
951 .fill(),
952 ),
953 Some(query) => {
954 let field = query.read(cx).field.clone();
955 let rows = rows.max_h(px(
956 SEARCH_ROWS * popover::menu_row_height() + 2.0 * popover::MENU_PAD
957 ));
958 card.child(
959 div()
960 .key_context(SEARCH_CONTEXT)
961 .px(px(popover::MENU_PAD))
962 .pt(px(popover::MENU_PAD))
963 .map(|el| self.keys(el, items, &visible, lit, prefix, cx))
964 .child(popover::search_line(theme, field.into_any_element())),
965 )
966 .child(match visible.is_empty() {
967 true => div()
968 .px(px(popover::MENU_PAD + popover::MENU_ROW_INSET))
969 .pb(px(8.0))
970 .text_style(TextStyle::Body)
971 .text_color(theme.text_muted)
972 .child("No matches")
973 .into_any_element(),
974 false => scroll::Viewport::new(rows_id, rows, Axis::Vertical)
975 .track_scroll(&handle)
976 .into_any_element(),
977 })
978 }
979 })
980 }
981
982 fn query(
985 &self,
986 key: &SharedString,
987 placeholder: &SharedString,
988 window: &mut Window,
989 cx: &mut Context<V>,
990 ) -> Entity<Query> {
991 let host = cx.entity_id();
992 let placeholder = placeholder.clone();
993 let query = window.use_keyed_state(
994 SharedString::from(format!("{key}-query")),
995 cx,
996 move |_, cx| {
997 let field = cx.new(|cx| crate::search::query_field(placeholder, cx));
998 let _changed = cx.subscribe(&field, move |_, _, _: &FieldEvent, cx| {
999 let app: &mut App = cx;
1000 app.notify(host);
1001 });
1002 Query {
1003 field,
1004 focused: false,
1005 _changed,
1006 }
1007 },
1008 );
1009 if !query.read(cx).focused {
1010 query.update(cx, |query, _| query.focused = true);
1011 let handle = query.read(cx).field.focus_handle(cx);
1012 window.focus(&handle, cx);
1013 }
1014 query
1015 }
1016
1017 fn keys(
1020 &self,
1021 el: gpui::Div,
1022 items: &[Item],
1023 shown: &[usize],
1024 lit: Option<usize>,
1025 prefix: &[usize],
1026 cx: &mut Context<V>,
1027 ) -> gpui::Div {
1028 let landable: Rc<Vec<usize>> = Rc::new(
1029 shown
1030 .iter()
1031 .copied()
1032 .filter(|&row| items[row].selectable())
1033 .collect(),
1034 );
1035 let at = lit.and_then(|lit| landable.iter().position(|&row| row == lit));
1036 let path = {
1037 let prefix = prefix.to_vec();
1038 move |row: usize| prefix.iter().copied().chain([row]).collect::<Vec<_>>()
1039 };
1040 let step = |delta: isize| {
1041 let count = landable.len() as isize;
1042 (count > 0).then(|| {
1043 let next = match at {
1044 Some(at) => (at as isize + delta).rem_euclid(count),
1045 None if delta > 0 => 0,
1046 None => count - 1,
1047 };
1048 landable[next as usize]
1049 })
1050 };
1051 let next = step(1).map(&path);
1052 let previous = step(-1).map(&path);
1053 let chosen = at
1054 .map(|at| landable[at])
1055 .or_else(|| landable.first().copied())
1056 .map(|row| match items[row].opens() {
1057 Some(_) => Hit::Point(path(row)),
1058 None => Hit::Choose(path(row)),
1059 });
1060 let on = self.on.clone();
1061 let report = move |hit: Option<Hit>| {
1062 let on = on.clone();
1063 move |view: &mut V, window: &mut Window, cx: &mut Context<V>| {
1064 if let Some(hit) = hit.clone() {
1065 on(view, hit, window, cx);
1066 }
1067 }
1068 };
1069 let (down, up, enter, escape) = (
1070 report(next.map(Hit::Point)),
1071 report(previous.map(Hit::Point)),
1072 report(chosen),
1073 report(Some(Hit::Dismiss)),
1074 );
1075 el.on_action(cx.listener(move |view, _: &SelectNext, window, cx| down(view, window, cx)))
1076 .on_action(
1077 cx.listener(move |view, _: &SelectPrevious, window, cx| up(view, window, cx)),
1078 )
1079 .on_action(cx.listener(move |view, _: &Confirm, window, cx| enter(view, window, cx)))
1080 .on_action(cx.listener(move |view, _: &Dismiss, window, cx| escape(view, window, cx)))
1081 }
1082
1083 #[allow(clippy::too_many_arguments)]
1086 fn segmented(
1087 &self,
1088 theme: &Theme,
1089 segments: &[Segment],
1090 selected: usize,
1091 enabled: bool,
1092 live: Option<usize>,
1093 path: &[usize],
1094 cx: &mut Context<V>,
1095 ) -> gpui::Stateful<gpui::Div> {
1096 div()
1097 .id(row_id(&self.id, path))
1098 .flex()
1099 .flex_row()
1100 .gap(px(popover::MENU_PAD))
1101 .children(segments.iter().enumerate().map(|(at, segment)| {
1102 let path: Vec<usize> = path.iter().copied().chain([at]).collect();
1103 let chosen = at == selected;
1104 let ink = match (enabled, chosen || live == Some(at)) {
1105 (false, _) => theme.text_faint.opacity(0.5),
1106 (true, true) => theme.text,
1107 (true, false) => theme.text_muted,
1108 };
1109 let cell = div()
1110 .id(row_id(&self.id, &path))
1111 .flex_1()
1112 .min_w_0()
1113 .flex()
1114 .flex_col()
1115 .items_center()
1116 .gap(px(4.0))
1117 .px(px(8.0))
1118 .py(px(6.0))
1119 .rounded(px(Theme::inset_radius(
1120 Theme::surface_radius(),
1121 popover::MENU_PAD,
1122 )))
1123 .text_style(TextStyle::Callout)
1124 .text_color(ink)
1125 .when(chosen, |cell| {
1126 cell.bg(theme.element_active)
1127 .font_weight(gpui::FontWeight::MEDIUM)
1128 })
1129 .when(!chosen && live == Some(at), |cell| {
1130 cell.bg(theme.card_selected_bg())
1131 })
1132 .child(
1133 icons::icon(segment.icon.clone())
1134 .size(px(SEGMENT_GLYPH))
1135 .text_color(ink),
1136 )
1137 .child(div().max_w_full().truncate().child(segment.label.clone()));
1138 match enabled {
1139 true => cell
1140 .cursor_pointer()
1141 .on_mouse_move(self.reports(Hit::Point(path.clone()), cx))
1142 .on_click(self.reports(Hit::Choose(path), cx)),
1143 false => cell,
1144 }
1145 }))
1146 }
1147
1148 fn dismissal(&self, cx: &mut Context<V>) -> Listener<MouseDownEvent> {
1160 let outside = self.outside.clone();
1161 let panels = self.panels;
1162 let on = self.on.clone();
1163 Box::new(
1164 cx.listener(move |view, event: &MouseDownEvent, window, cx| {
1165 let (at, count) = outside.get();
1166 let count = if at == Some(event.position) {
1167 count + 1
1168 } else {
1169 1
1170 };
1171 outside.set((Some(event.position), count));
1172 if count >= panels {
1173 outside.set((None, 0));
1174 on(view, Hit::Dismiss, window, cx);
1175 cx.stop_propagation();
1176 }
1177 }),
1178 )
1179 }
1180
1181 fn reports<E: 'static>(&self, hit: Hit, cx: &mut Context<V>) -> Listener<E> {
1183 let on = self.on.clone();
1184 Box::new(cx.listener(move |view, _: &E, window, cx| on(view, hit.clone(), window, cx)))
1185 }
1186}
1187
1188#[derive(Clone, Default)]
1191struct Scrolled(ScrollHandle, Rc<Cell<Option<Shown>>>);
1192
1193type Shown = (Option<usize>, Size<Pixels>);
1194
1195fn row_id(id: &SharedString, path: &[usize]) -> SharedString {
1198 let mut out = id.to_string();
1199 for row in path {
1200 out.push('-');
1201 out.push_str(&row.to_string());
1202 }
1203 SharedString::from(out)
1204}
1205
1206pub fn glyph_slot(theme: &Theme, icon: Option<Icon>, enabled: bool) -> gpui::Div {
1209 div()
1210 .flex_none()
1211 .size(px(GLYPH))
1212 .flex()
1213 .items_center()
1214 .justify_center()
1215 .children(icon.map(|glyph| {
1216 icons::icon(glyph).size(px(GLYPH)).text_color(if enabled {
1217 theme.text_faint
1218 } else {
1219 theme.text_faint.opacity(0.5)
1220 })
1221 }))
1222}
1223
1224fn description_line(theme: &Theme, description: SharedString, enabled: bool) -> gpui::Div {
1238 div()
1239 .mt(px(2.0))
1240 .truncate()
1241 .text_style(TextStyle::Subheadline)
1242 .text_color(if enabled {
1243 theme.text_muted
1244 } else {
1245 theme.text_faint.opacity(0.5)
1248 })
1249 .child(description)
1250}
1251
1252fn disabled_row(theme: &Theme) -> gpui::Div {
1256 div()
1257 .flex()
1258 .flex_row()
1259 .items_center()
1260 .gap(px(10.0))
1261 .px(px(8.0))
1262 .py(px(popover::MENU_ROW_PAD_Y))
1263 .rounded(px(Theme::inset_radius(
1266 Theme::surface_radius(),
1267 popover::MENU_PAD,
1268 )))
1269 .text_style(TextStyle::Body)
1270 .text_color(theme.text_faint)
1271}