1use crate::{
15 menu::{
16 self, Confirm, Cursor, Dismiss, Hit, Item, SelectChild, SelectNext, SelectParent,
17 SelectPrevious,
18 },
19 popover::{self, Popup},
20};
21use gpui::{
22 AnyElement, Context, Div, FocusHandle, Pixels, Point, SharedString, StatefulInteractiveElement,
23 Window, prelude::*,
24};
25use std::rc::Rc;
26use theme::Theme;
27
28pub const KEY_CONTEXT: &str = "ContextMenu";
30
31type OnChoose<V> = Rc<dyn Fn(&mut V, &[usize], &mut Window, &mut Context<V>)>;
33
34pub type Slot<V, K = ()> = fn(&mut V) -> &mut ContextMenu<V, K>;
36
37#[derive(Clone, Copy, Debug, PartialEq)]
39pub enum Place {
40 At(Point<Pixels>),
42 Below,
44 BelowEnd,
46 Above,
48 AboveEnd,
50}
51
52struct Open<V, K> {
53 key: K,
54 place: Place,
55 cursor: Cursor,
56 items: Vec<Item>,
57 on_choose: OnChoose<V>,
58 focus: FocusHandle,
59 back: Option<FocusHandle>,
60}
61
62pub struct ContextMenu<V: 'static, K: 'static = ()>(Popup<Open<V, K>>);
64
65impl<V: 'static, K: 'static> Default for ContextMenu<V, K> {
66 fn default() -> Self {
67 Self(Popup::default())
68 }
69}
70
71impl<V: 'static, K: PartialEq + 'static> ContextMenu<V, K> {
72 pub fn open(
75 &mut self,
76 key: K,
77 place: Place,
78 items: Vec<Item>,
79 on_choose: impl Fn(&mut V, &[usize], &mut Window, &mut Context<V>) + 'static,
80 window: &mut Window,
81 cx: &mut Context<V>,
82 ) {
83 let back = match self.0.as_open() {
86 Some(open) => open.back.clone(),
87 None => window.focused(cx),
88 };
89 let focus = cx.focus_handle();
90 window.focus(&focus, cx);
91 self.0.open(Open {
92 key,
93 place,
94 cursor: Cursor::default(),
95 items,
96 on_choose: Rc::new(on_choose),
97 focus,
98 back,
99 });
100 cx.notify();
101 }
102
103 pub fn toggle<F>(
107 view: &mut V,
108 slot: Slot<V, K>,
109 key: K,
110 place: Place,
111 build: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> (Vec<Item>, F),
112 window: &mut Window,
113 cx: &mut Context<V>,
114 ) where
115 F: Fn(&mut V, &[usize], &mut Window, &mut Context<V>) + 'static,
116 {
117 let menu = slot(view);
118 if menu.0.take_press_was_open() || menu.key() == Some(&key) {
119 Self::close_back(view, slot, window, cx);
120 return;
121 }
122 let (items, on_choose) = build(view, window, cx);
123 slot(view).open(key, place, items, on_choose, window, cx);
124 }
125
126 pub fn note_press(&mut self, key: &K) {
131 self.0.note_trigger_press_matching(|open| open.key == *key);
132 }
133
134 pub fn trigger<E: StatefulInteractiveElement>(
137 el: E,
138 slot: Slot<V, K>,
139 key: K,
140 cx: &Context<V>,
141 ) -> E {
142 el.capture_any_mouse_down(cx.listener(move |view, _: &gpui::MouseDownEvent, _, _| {
144 slot(view).note_press(&key);
145 }))
146 }
147
148 pub fn key(&self) -> Option<&K> {
151 self.0.as_open().map(|open| &open.key)
152 }
153
154 pub fn is_open(&self) -> bool {
155 self.0.is_open()
156 }
157
158 pub fn showing(&self, key: &K) -> bool {
160 self.0.get().is_some_and(|open| open.key == *key)
161 }
162
163 pub fn point_at(&mut self, path: &[usize]) -> bool {
166 self.0
167 .open_mut()
168 .is_some_and(|open| open.cursor.point_at(&open.items, path))
169 }
170
171 fn descend(&mut self) -> bool {
173 self.0
174 .open_mut()
175 .is_some_and(|open| open.cursor.descend(&open.items))
176 }
177
178 fn ascend(&mut self) -> bool {
180 self.0.open_mut().is_some_and(|open| open.cursor.ascend())
181 }
182
183 pub fn clear_cursor(&mut self) {
185 if let Some(open) = self.0.open_mut() {
186 open.cursor.clear();
187 }
188 }
189
190 pub fn close(view: &mut V, slot: Slot<V, K>, cx: &mut Context<V>) {
192 popover::close_popup(view, cx, move |view| &mut slot(view).0);
193 }
194
195 fn close_back(view: &mut V, slot: Slot<V, K>, window: &mut Window, cx: &mut Context<V>) {
197 let back = slot(view).0.as_open().and_then(|open| open.back.clone());
198 Self::close(view, slot, cx);
199 if let Some(back) = back {
200 window.focus(&back, cx);
201 }
202 }
203
204 pub fn card(
208 &self,
209 id: impl Into<SharedString>,
210 slot: Slot<V, K>,
211 window: &mut Window,
212 cx: &mut Context<V>,
213 ) -> Option<Div> {
214 let open = self.0.get()?;
215 let theme = Theme::of(cx).clone();
216 let choose = open.on_choose.clone();
217 let card = menu::card(
218 &theme,
219 id,
220 &open.items,
221 &open.cursor,
222 self.0.closing_since(),
223 window,
224 cx,
225 move |view: &mut V, hit, window, cx| match hit {
226 Hit::Point(path) => {
227 if slot(view).point_at(&path) {
228 cx.notify();
229 }
230 }
231 Hit::Choose(path) => {
232 Self::close_back(view, slot, window, cx);
233 choose(view, &path, window, cx);
234 }
235 Hit::Dismiss => Self::close(view, slot, cx),
236 },
237 )
238 .track_focus(&open.focus)
239 .key_context(KEY_CONTEXT)
240 .on_action(cx.listener(move |view, _: &SelectNext, _, cx| step(view, slot, 1, cx)))
241 .on_action(cx.listener(move |view, _: &SelectPrevious, _, cx| step(view, slot, -1, cx)))
242 .on_action(cx.listener(move |view, _: &SelectChild, _, cx| {
243 if slot(view).descend() {
244 cx.notify();
245 }
246 }))
247 .on_action(cx.listener(move |view, _: &SelectParent, _, cx| {
248 if slot(view).ascend() {
249 cx.notify();
250 }
251 }))
252 .on_action(cx.listener(move |view, _: &Confirm, window, cx| {
253 if slot(view).descend() {
254 cx.notify();
255 return;
256 }
257 let Some((path, choose)) = slot(view)
258 .0
259 .as_open()
260 .and_then(|open| Some((open.cursor.path()?, open.on_choose.clone())))
261 else {
262 return;
263 };
264 Self::close_back(view, slot, window, cx);
265 choose(view, &path, window, cx);
266 }))
267 .on_action(cx.listener(
268 move |view, _: &Dismiss, window, cx| match slot(view).ascend() {
269 true => cx.notify(),
270 false => Self::close_back(view, slot, window, cx),
271 },
272 ));
273 Some(card)
274 }
275
276 pub fn place(&self, id: impl Into<SharedString>, card: impl IntoElement) -> AnyElement {
279 let card = card.into_any_element();
280 let closing = self.0.closing_since();
281 match self.0.get().map(|open| open.place) {
282 Some(Place::At(at)) => popover::menu_at(id, at, card, closing),
283 Some(Place::BelowEnd) => popover::anchored_menu_below_end(id, card, closing),
284 Some(Place::Above) => popover::anchored_menu_above(id, card, closing),
285 Some(Place::AboveEnd) => popover::anchored_menu_above_end(id, card, closing),
286 Some(Place::Below) | None => popover::anchored_menu_below(id, card, closing),
287 }
288 }
289
290 pub fn render(
292 &self,
293 id: impl Into<SharedString>,
294 slot: Slot<V, K>,
295 window: &mut Window,
296 cx: &mut Context<V>,
297 ) -> Option<AnyElement> {
298 let id = id.into();
299 let card = self.card(id.clone(), slot, window, cx)?;
300 Some(self.place(id, card))
301 }
302
303 pub fn render_for(
306 &self,
307 key: &K,
308 id: impl Into<SharedString>,
309 slot: Slot<V, K>,
310 window: &mut Window,
311 cx: &mut Context<V>,
312 ) -> Option<AnyElement> {
313 if !self.showing(key) {
314 return None;
315 }
316 self.render(id, slot, window, cx)
317 }
318}
319
320fn step<V: 'static, K: PartialEq + 'static>(
321 view: &mut V,
322 slot: Slot<V, K>,
323 delta: isize,
324 cx: &mut Context<V>,
325) {
326 if let Some(open) = slot(view).0.open_mut() {
327 open.cursor.step(&open.items, delta);
328 cx.notify();
329 }
330}