1use std::cell::RefCell;
2use std::rc::Rc;
3
4use crate::prelude::*;
5use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
6use gpui::{
7 overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
8 FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
9 VisualContext, WeakView,
10};
11
12pub enum ContextMenuItem<V> {
13 Separator(ListSeparator),
14 Header(ListSubHeader),
15 Entry(
16 ListEntry<ContextMenu<V>>,
17 Rc<dyn Fn(&mut V, &mut ViewContext<V>)>,
18 ),
19}
20
21pub struct ContextMenu<V> {
22 items: Vec<ContextMenuItem<V>>,
23 focus_handle: FocusHandle,
24 handle: WeakView<V>,
25}
26
27impl<V: Render> ManagedView for ContextMenu<V> {
28 fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
29 self.focus_handle.clone()
30 }
31}
32
33impl<V: Render> ContextMenu<V> {
34 pub fn build(
35 cx: &mut ViewContext<V>,
36 f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
37 ) -> View<Self> {
38 let handle = cx.view().downgrade();
39 cx.build_view(|cx| {
40 f(
41 Self {
42 handle,
43 items: Default::default(),
44 focus_handle: cx.focus_handle(),
45 },
46 cx,
47 )
48 })
49 }
50
51 pub fn header(mut self, title: impl Into<SharedString>) -> Self {
52 self.items
53 .push(ContextMenuItem::Header(ListSubHeader::new(title)));
54 self
55 }
56
57 pub fn separator(mut self) -> Self {
58 self.items.push(ContextMenuItem::Separator(ListSeparator));
59 self
60 }
61
62 pub fn entry(
63 mut self,
64 view: ListEntry<Self>,
65 on_click: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
66 ) -> Self {
67 self.items
68 .push(ContextMenuItem::Entry(view, Rc::new(on_click)));
69 self
70 }
71
72 pub fn action(self, view: ListEntry<Self>, action: Box<dyn Action>) -> Self {
73 // todo: add the keybindings to the list entry
74 self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone()))
75 }
76
77 pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
78 // todo!()
79 cx.emit(Dismiss);
80 }
81
82 pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
83 cx.emit(Dismiss);
84 }
85}
86
87impl<V: Render> Render for ContextMenu<V> {
88 type Element = Div<Self>;
89
90 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
91 div().elevation_2(cx).flex().flex_row().child(
92 v_stack()
93 .min_w(px(200.))
94 .track_focus(&self.focus_handle)
95 .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
96 // .on_action(ContextMenu::select_first)
97 // .on_action(ContextMenu::select_last)
98 // .on_action(ContextMenu::select_next)
99 // .on_action(ContextMenu::select_prev)
100 .on_action(ContextMenu::confirm)
101 .on_action(ContextMenu::cancel)
102 .flex_none()
103 // .bg(cx.theme().colors().elevated_surface_background)
104 // .border()
105 // .border_color(cx.theme().colors().border)
106 .child(List::new(
107 self.items
108 .iter()
109 .map(|item| match item {
110 ContextMenuItem::Separator(separator) => {
111 ListItem::Separator(separator.clone())
112 }
113 ContextMenuItem::Header(header) => ListItem::Header(header.clone()),
114 ContextMenuItem::Entry(entry, callback) => {
115 let callback = callback.clone();
116 let handle = self.handle.clone();
117 ListItem::Entry(entry.clone().on_click(move |this, cx| {
118 handle.update(cx, |view, cx| callback(view, cx)).ok();
119 cx.emit(Dismiss);
120 }))
121 }
122 })
123 .collect(),
124 )),
125 )
126 }
127}
128
129pub struct MenuHandle<V: 'static, M: ManagedView> {
130 id: Option<ElementId>,
131 child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
132 menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
133
134 anchor: Option<AnchorCorner>,
135 attach: Option<AnchorCorner>,
136}
137
138impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
139 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
140 self.id = Some(id.into());
141 self
142 }
143
144 pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
145 self.menu_builder = Some(Rc::new(f));
146 self
147 }
148
149 pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
150 self.child_builder = Some(Box::new(|b| f(b).render()));
151 self
152 }
153
154 /// anchor defines which corner of the menu to anchor to the attachment point
155 /// (by default the cursor position, but see attach)
156 pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
157 self.anchor = Some(anchor);
158 self
159 }
160
161 /// attach defines which corner of the handle to attach the menu's anchor to
162 pub fn attach(mut self, attach: AnchorCorner) -> Self {
163 self.attach = Some(attach);
164 self
165 }
166}
167
168pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
169 MenuHandle {
170 id: None,
171 child_builder: None,
172 menu_builder: None,
173 anchor: None,
174 attach: None,
175 }
176}
177
178pub struct MenuHandleState<V, M> {
179 menu: Rc<RefCell<Option<View<M>>>>,
180 position: Rc<RefCell<Point<Pixels>>>,
181 child_layout_id: Option<LayoutId>,
182 child_element: Option<AnyElement<V>>,
183 menu_element: Option<AnyElement<V>>,
184}
185impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
186 type ElementState = MenuHandleState<V, M>;
187
188 fn element_id(&self) -> Option<gpui::ElementId> {
189 Some(self.id.clone().expect("menu_handle must have an id()"))
190 }
191
192 fn layout(
193 &mut self,
194 view_state: &mut V,
195 element_state: Option<Self::ElementState>,
196 cx: &mut crate::ViewContext<V>,
197 ) -> (gpui::LayoutId, Self::ElementState) {
198 let (menu, position) = if let Some(element_state) = element_state {
199 (element_state.menu, element_state.position)
200 } else {
201 (Rc::default(), Rc::default())
202 };
203
204 let mut menu_layout_id = None;
205
206 let menu_element = menu.borrow_mut().as_mut().map(|menu| {
207 let mut overlay = overlay::<V>().snap_to_window();
208 if let Some(anchor) = self.anchor {
209 overlay = overlay.anchor(anchor);
210 }
211 overlay = overlay.position(*position.borrow());
212
213 let mut view = overlay.child(menu.clone()).render();
214 menu_layout_id = Some(view.layout(view_state, cx));
215 view
216 });
217
218 let mut child_element = self
219 .child_builder
220 .take()
221 .map(|child_builder| (child_builder)(menu.borrow().is_some()));
222
223 let child_layout_id = child_element
224 .as_mut()
225 .map(|child_element| child_element.layout(view_state, cx));
226
227 let layout_id = cx.request_layout(
228 &gpui::Style::default(),
229 menu_layout_id.into_iter().chain(child_layout_id),
230 );
231
232 (
233 layout_id,
234 MenuHandleState {
235 menu,
236 position,
237 child_element,
238 child_layout_id,
239 menu_element,
240 },
241 )
242 }
243
244 fn paint(
245 &mut self,
246 bounds: Bounds<gpui::Pixels>,
247 view_state: &mut V,
248 element_state: &mut Self::ElementState,
249 cx: &mut crate::ViewContext<V>,
250 ) {
251 if let Some(child) = element_state.child_element.as_mut() {
252 child.paint(view_state, cx);
253 }
254
255 if let Some(menu) = element_state.menu_element.as_mut() {
256 menu.paint(view_state, cx);
257 return;
258 }
259
260 let Some(builder) = self.menu_builder.clone() else {
261 return;
262 };
263 let menu = element_state.menu.clone();
264 let position = element_state.position.clone();
265 let attach = self.attach.clone();
266 let child_layout_id = element_state.child_layout_id.clone();
267
268 cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
269 if phase == DispatchPhase::Bubble
270 && event.button == MouseButton::Right
271 && bounds.contains_point(&event.position)
272 {
273 cx.stop_propagation();
274 cx.prevent_default();
275
276 let new_menu = (builder)(view_state, cx);
277 let menu2 = menu.clone();
278 cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
279 &Dismiss => {
280 *menu2.borrow_mut() = None;
281 cx.notify();
282 }
283 })
284 .detach();
285 cx.focus_view(&new_menu);
286 *menu.borrow_mut() = Some(new_menu);
287
288 *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
289 attach
290 .unwrap()
291 .corner(cx.layout_bounds(child_layout_id.unwrap()))
292 } else {
293 cx.mouse_position()
294 };
295 cx.notify();
296 }
297 });
298 }
299}
300
301impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
302 fn render(self) -> AnyElement<V> {
303 AnyElement::new(self)
304 }
305}
306
307#[cfg(feature = "stories")]
308pub use stories::*;
309
310#[cfg(feature = "stories")]
311mod stories {
312 use super::*;
313 use crate::story::Story;
314 use gpui::{actions, Div, Render};
315
316 actions!(PrintCurrentDate, PrintBestFood);
317
318 fn build_menu<V: Render>(
319 cx: &mut ViewContext<V>,
320 header: impl Into<SharedString>,
321 ) -> View<ContextMenu<V>> {
322 let handle = cx.view().clone();
323 ContextMenu::build(cx, |menu, _| {
324 menu.header(header)
325 .separator()
326 .entry(ListEntry::new(Label::new("Print current time")), |v, cx| {
327 println!("dispatching PrintCurrentTime action");
328 cx.dispatch_action(PrintCurrentDate.boxed_clone())
329 })
330 .entry(ListEntry::new(Label::new("Print best food")), |v, cx| {
331 cx.dispatch_action(PrintBestFood.boxed_clone())
332 })
333 })
334 }
335
336 pub struct ContextMenuStory;
337
338 impl Render for ContextMenuStory {
339 type Element = Div<Self>;
340
341 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
342 Story::container(cx)
343 .on_action(|_, _: &PrintCurrentDate, _| {
344 println!("printing unix time!");
345 if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
346 println!("Current Unix time is {:?}", unix_time.as_secs());
347 }
348 })
349 .on_action(|_, _: &PrintBestFood, _| {
350 println!("burrito");
351 })
352 .flex()
353 .flex_row()
354 .justify_between()
355 .child(
356 div()
357 .flex()
358 .flex_col()
359 .justify_between()
360 .child(
361 menu_handle()
362 .id("test2")
363 .child(|is_open| {
364 Label::new(if is_open {
365 "TOP LEFT"
366 } else {
367 "RIGHT CLICK ME"
368 })
369 .render()
370 })
371 .menu(move |_, cx| build_menu(cx, "top left")),
372 )
373 .child(
374 menu_handle()
375 .id("test1")
376 .child(|is_open| {
377 Label::new(if is_open {
378 "BOTTOM LEFT"
379 } else {
380 "RIGHT CLICK ME"
381 })
382 .render()
383 })
384 .anchor(AnchorCorner::BottomLeft)
385 .attach(AnchorCorner::TopLeft)
386 .menu(move |_, cx| build_menu(cx, "bottom left")),
387 ),
388 )
389 .child(
390 div()
391 .flex()
392 .flex_col()
393 .justify_between()
394 .child(
395 menu_handle()
396 .id("test3")
397 .child(|is_open| {
398 Label::new(if is_open {
399 "TOP RIGHT"
400 } else {
401 "RIGHT CLICK ME"
402 })
403 .render()
404 })
405 .anchor(AnchorCorner::TopRight)
406 .menu(move |_, cx| build_menu(cx, "top right")),
407 )
408 .child(
409 menu_handle()
410 .id("test4")
411 .child(|is_open| {
412 Label::new(if is_open {
413 "BOTTOM RIGHT"
414 } else {
415 "RIGHT CLICK ME"
416 })
417 .render()
418 })
419 .anchor(AnchorCorner::BottomRight)
420 .attach(AnchorCorner::TopRight)
421 .menu(move |_, cx| build_menu(cx, "bottom right")),
422 ),
423 )
424 }
425 }
426}