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 window,
223 cx,
224 move |view: &mut V, hit, window, cx| match hit {
225 Hit::Point(path) => {
226 if slot(view).point_at(&path) {
227 cx.notify();
228 }
229 }
230 Hit::Choose(path) => {
231 Self::close_back(view, slot, window, cx);
232 choose(view, &path, window, cx);
233 }
234 Hit::Dismiss => Self::close(view, slot, cx),
235 },
236 )
237 .track_focus(&open.focus)
238 .key_context(KEY_CONTEXT)
239 .on_action(cx.listener(move |view, _: &SelectNext, _, cx| step(view, slot, 1, cx)))
240 .on_action(cx.listener(move |view, _: &SelectPrevious, _, cx| step(view, slot, -1, cx)))
241 .on_action(cx.listener(move |view, _: &SelectChild, _, cx| {
242 if slot(view).descend() {
243 cx.notify();
244 }
245 }))
246 .on_action(cx.listener(move |view, _: &SelectParent, _, cx| {
247 if slot(view).ascend() {
248 cx.notify();
249 }
250 }))
251 .on_action(cx.listener(move |view, _: &Confirm, window, cx| {
252 if slot(view).descend() {
253 cx.notify();
254 return;
255 }
256 let Some((path, choose)) = slot(view)
257 .0
258 .as_open()
259 .and_then(|open| Some((open.cursor.path()?, open.on_choose.clone())))
260 else {
261 return;
262 };
263 Self::close_back(view, slot, window, cx);
264 choose(view, &path, window, cx);
265 }))
266 .on_action(cx.listener(
267 move |view, _: &Dismiss, window, cx| match slot(view).ascend() {
268 true => cx.notify(),
269 false => Self::close_back(view, slot, window, cx),
270 },
271 ));
272 Some(card)
273 }
274
275 pub fn place(&self, id: impl Into<SharedString>, card: impl IntoElement) -> AnyElement {
278 let card = card.into_any_element();
279 let closing = self.0.closing_since();
280 match self.0.get().map(|open| open.place) {
281 Some(Place::At(at)) => popover::menu_at(id, at, card, closing),
282 Some(Place::BelowEnd) => popover::anchored_menu_below_end(id, card, closing),
283 Some(Place::Above) => popover::anchored_menu_above(id, card, closing),
284 Some(Place::AboveEnd) => popover::anchored_menu_above_end(id, card, closing),
285 Some(Place::Below) | None => popover::anchored_menu_below(id, card, closing),
286 }
287 }
288
289 pub fn render(
291 &self,
292 id: impl Into<SharedString>,
293 slot: Slot<V, K>,
294 window: &mut Window,
295 cx: &mut Context<V>,
296 ) -> Option<AnyElement> {
297 let id = id.into();
298 let card = self.card(id.clone(), slot, window, cx)?;
299 Some(self.place(id, card))
300 }
301
302 pub fn render_for(
305 &self,
306 key: &K,
307 id: impl Into<SharedString>,
308 slot: Slot<V, K>,
309 window: &mut Window,
310 cx: &mut Context<V>,
311 ) -> Option<AnyElement> {
312 if !self.showing(key) {
313 return None;
314 }
315 self.render(id, slot, window, cx)
316 }
317}
318
319fn step<V: 'static, K: PartialEq + 'static>(
320 view: &mut V,
321 slot: Slot<V, K>,
322 delta: isize,
323 cx: &mut Context<V>,
324) {
325 if let Some(open) = slot(view).0.open_mut() {
326 open.cursor.step(&open.items, delta);
327 cx.notify();
328 }
329}