1use crate::{
10 menu::{self, Confirm, Cursor, Dismiss, Hit, Item, SelectNext, SelectPrevious},
11 popover::{self, Popup},
12};
13use gpui::{AnyElement, Context, FocusHandle, Pixels, Point, SharedString, Window, prelude::*};
14use std::rc::Rc;
15use theme::Theme;
16
17pub const KEY_CONTEXT: &str = "ContextMenu";
19
20type OnChoose<V> = Rc<dyn Fn(&mut V, &[usize], &mut Window, &mut Context<V>)>;
22
23pub type Slot<V> = fn(&mut V) -> &mut ContextMenu;
25
26struct Open {
27 at: Point<Pixels>,
28 cursor: Cursor,
29 items: Vec<Item>,
30 focus: FocusHandle,
31 back: Option<FocusHandle>,
32}
33
34#[derive(Default)]
36pub struct ContextMenu(Popup<Open>);
37
38impl ContextMenu {
39 pub fn open<V: 'static>(
41 &mut self,
42 at: Point<Pixels>,
43 items: Vec<Item>,
44 window: &mut Window,
45 cx: &mut Context<V>,
46 ) {
47 let back = window.focused(cx);
48 let focus = cx.focus_handle();
49 window.focus(&focus, cx);
50 self.0.open(Open {
51 at,
52 cursor: Cursor::default(),
53 items,
54 focus,
55 back,
56 });
57 cx.notify();
58 }
59
60 pub fn is_open(&self) -> bool {
61 self.0.is_open()
62 }
63
64 pub fn render<V: 'static>(
67 &self,
68 id: impl Into<SharedString>,
69 slot: Slot<V>,
70 on_choose: impl Fn(&mut V, &[usize], &mut Window, &mut Context<V>) + 'static,
71 window: &mut Window,
72 cx: &mut Context<V>,
73 ) -> Option<AnyElement> {
74 let open = self.0.get()?;
75 let id = id.into();
76 let on_choose: OnChoose<V> = Rc::new(on_choose);
77 let theme = Theme::of(cx).clone();
78 let choose = on_choose.clone();
79 let card = menu::card(
80 &theme,
81 id.clone(),
82 &open.items,
83 &open.cursor,
84 window,
85 cx,
86 move |view: &mut V, hit, window, cx| match hit {
87 Hit::Point(path) => {
88 if let Some(open) = slot(view).0.open_mut()
89 && open.cursor.point_at(&open.items, &path)
90 {
91 cx.notify();
92 }
93 }
94 Hit::Choose(path) => {
95 close(view, slot, true, window, cx);
96 choose(view, &path, window, cx);
97 }
98 Hit::Dismiss => close(view, slot, false, window, cx),
99 },
100 )
101 .track_focus(&open.focus)
102 .key_context(KEY_CONTEXT)
103 .on_action(cx.listener(move |view, _: &SelectNext, _, cx| step(view, slot, 1, cx)))
104 .on_action(cx.listener(move |view, _: &SelectPrevious, _, cx| step(view, slot, -1, cx)))
105 .on_action(cx.listener(move |view, _: &Confirm, window, cx| {
106 let Some(path) = slot(view).0.get().and_then(|open| open.cursor.path()) else {
107 return;
108 };
109 close(view, slot, true, window, cx);
110 on_choose(view, &path, window, cx);
111 }))
112 .on_action(
113 cx.listener(move |view, _: &Dismiss, window, cx| close(view, slot, true, window, cx)),
114 );
115 Some(popover::menu_at(
116 id,
117 open.at,
118 card.into_any_element(),
119 self.0.closing_since(),
120 ))
121 }
122}
123
124fn step<V: 'static>(view: &mut V, slot: Slot<V>, delta: isize, cx: &mut Context<V>) {
125 if let Some(open) = slot(view).0.open_mut() {
126 open.cursor.step(&open.items, delta);
127 cx.notify();
128 }
129}
130
131fn close<V: 'static>(
134 view: &mut V,
135 slot: Slot<V>,
136 refocus: bool,
137 window: &mut Window,
138 cx: &mut Context<V>,
139) {
140 let back = slot(view).0.get().and_then(|open| open.back.clone());
141 popover::close_popup(view, cx, move |view| &mut slot(view).0);
142 if refocus && let Some(back) = back {
143 window.focus(&back, cx);
144 }
145}