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