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