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, AppContext, Bounds, DispatchPhase, Div,
8 EventEmitter, FocusHandle, FocusableView, LayoutId, ManagedView, Manager, MouseButton,
9 MouseDownEvent, Pixels, Point, Render, View, 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> FocusableView for ContextMenu<V> {
28 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
29 self.focus_handle.clone()
30 }
31}
32
33impl<V: Render> EventEmitter<Manager> for ContextMenu<V> {}
34
35impl<V: Render> 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: Render> Render 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(List::new(
109 self.items
110 .iter()
111 .map(|item| match item {
112 ContextMenuItem::Separator(separator) => {
113 ListItem::Separator(separator.clone())
114 }
115 ContextMenuItem::Header(header) => ListItem::Header(header.clone()),
116 ContextMenuItem::Entry(entry, callback) => {
117 let callback = callback.clone();
118 let handle = self.handle.clone();
119 ListItem::Entry(entry.clone().on_click(move |this, cx| {
120 handle.update(cx, |view, cx| callback(view, cx)).ok();
121 cx.emit(Manager::Dismiss);
122 }))
123 }
124 })
125 .collect(),
126 )),
127 )
128 }
129}
130
131pub struct MenuHandle<V: 'static, M: ManagedView> {
132 id: Option<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 id(mut self, id: impl Into<ElementId>) -> Self {
142 self.id = Some(id.into());
143 self
144 }
145
146 pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
147 self.menu_builder = Some(Rc::new(f));
148 self
149 }
150
151 pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
152 self.child_builder = Some(Box::new(|b| f(b).render()));
153 self
154 }
155
156 /// anchor defines which corner of the menu to anchor to the attachment point
157 /// (by default the cursor position, but see attach)
158 pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
159 self.anchor = Some(anchor);
160 self
161 }
162
163 /// attach defines which corner of the handle to attach the menu's anchor to
164 pub fn attach(mut self, attach: AnchorCorner) -> Self {
165 self.attach = Some(attach);
166 self
167 }
168}
169
170pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
171 MenuHandle {
172 id: None,
173 child_builder: None,
174 menu_builder: None,
175 anchor: None,
176 attach: None,
177 }
178}
179
180pub struct MenuHandleState<V, M> {
181 menu: Rc<RefCell<Option<View<M>>>>,
182 position: Rc<RefCell<Point<Pixels>>>,
183 child_layout_id: Option<LayoutId>,
184 child_element: Option<AnyElement<V>>,
185 menu_element: Option<AnyElement<V>>,
186}
187impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
188 type ElementState = MenuHandleState<V, M>;
189
190 fn element_id(&self) -> Option<gpui::ElementId> {
191 Some(self.id.clone().expect("menu_handle must have an id()"))
192 }
193
194 fn layout(
195 &mut self,
196 view_state: &mut V,
197 element_state: Option<Self::ElementState>,
198 cx: &mut crate::ViewContext<V>,
199 ) -> (gpui::LayoutId, Self::ElementState) {
200 let (menu, position) = if let Some(element_state) = element_state {
201 (element_state.menu, element_state.position)
202 } else {
203 (Rc::default(), Rc::default())
204 };
205
206 let mut menu_layout_id = None;
207
208 let menu_element = menu.borrow_mut().as_mut().map(|menu| {
209 let mut overlay = overlay::<V>().snap_to_window();
210 if let Some(anchor) = self.anchor {
211 overlay = overlay.anchor(anchor);
212 }
213 overlay = overlay.position(*position.borrow());
214
215 let mut view = overlay.child(menu.clone()).render();
216 menu_layout_id = Some(view.layout(view_state, cx));
217 view
218 });
219
220 let mut child_element = self
221 .child_builder
222 .take()
223 .map(|child_builder| (child_builder)(menu.borrow().is_some()));
224
225 let child_layout_id = child_element
226 .as_mut()
227 .map(|child_element| child_element.layout(view_state, cx));
228
229 let layout_id = cx.request_layout(
230 &gpui::Style::default(),
231 menu_layout_id.into_iter().chain(child_layout_id),
232 );
233
234 (
235 layout_id,
236 MenuHandleState {
237 menu,
238 position,
239 child_element,
240 child_layout_id,
241 menu_element,
242 },
243 )
244 }
245
246 fn paint(
247 &mut self,
248 bounds: Bounds<gpui::Pixels>,
249 view_state: &mut V,
250 element_state: &mut Self::ElementState,
251 cx: &mut crate::ViewContext<V>,
252 ) {
253 if let Some(child) = element_state.child_element.as_mut() {
254 child.paint(view_state, cx);
255 }
256
257 if let Some(menu) = element_state.menu_element.as_mut() {
258 menu.paint(view_state, cx);
259 return;
260 }
261
262 let Some(builder) = self.menu_builder.clone() else {
263 return;
264 };
265 let menu = element_state.menu.clone();
266 let position = element_state.position.clone();
267 let attach = self.attach.clone();
268 let child_layout_id = element_state.child_layout_id.clone();
269
270 cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
271 if phase == DispatchPhase::Bubble
272 && event.button == MouseButton::Right
273 && bounds.contains_point(&event.position)
274 {
275 cx.stop_propagation();
276 cx.prevent_default();
277
278 let new_menu = (builder)(view_state, cx);
279 let menu2 = menu.clone();
280 cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
281 &Manager::Dismiss => {
282 *menu2.borrow_mut() = None;
283 cx.notify();
284 }
285 })
286 .detach();
287 cx.focus_view(&new_menu);
288 *menu.borrow_mut() = Some(new_menu);
289
290 *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
291 attach
292 .unwrap()
293 .corner(cx.layout_bounds(child_layout_id.unwrap()))
294 } else {
295 cx.mouse_position()
296 };
297 cx.notify();
298 }
299 });
300 }
301}
302
303impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
304 fn render(self) -> AnyElement<V> {
305 AnyElement::new(self)
306 }
307}
308
309#[cfg(feature = "stories")]
310pub use stories::*;
311
312#[cfg(feature = "stories")]
313mod stories {
314 use super::*;
315 use crate::story::Story;
316 use gpui::{actions, Div, Render};
317
318 actions!(PrintCurrentDate, PrintBestFood);
319
320 fn build_menu<V: Render>(
321 cx: &mut ViewContext<V>,
322 header: impl Into<SharedString>,
323 ) -> View<ContextMenu<V>> {
324 let handle = cx.view().clone();
325 ContextMenu::build(cx, |menu, _| {
326 menu.header(header)
327 .separator()
328 .entry(ListEntry::new(Label::new("Print current time")), |v, cx| {
329 println!("dispatching PrintCurrentTime action");
330 cx.dispatch_action(PrintCurrentDate.boxed_clone())
331 })
332 .entry(ListEntry::new(Label::new("Print best food")), |v, cx| {
333 cx.dispatch_action(PrintBestFood.boxed_clone())
334 })
335 })
336 }
337
338 pub struct ContextMenuStory;
339
340 impl Render for ContextMenuStory {
341 type Element = Div<Self>;
342
343 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
344 Story::container(cx)
345 .on_action(|_, _: &PrintCurrentDate, _| {
346 println!("printing unix time!");
347 if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
348 println!("Current Unix time is {:?}", unix_time.as_secs());
349 }
350 })
351 .on_action(|_, _: &PrintBestFood, _| {
352 println!("burrito");
353 })
354 .flex()
355 .flex_row()
356 .justify_between()
357 .child(
358 div()
359 .flex()
360 .flex_col()
361 .justify_between()
362 .child(
363 menu_handle()
364 .id("test2")
365 .child(|is_open| {
366 Label::new(if is_open {
367 "TOP LEFT"
368 } else {
369 "RIGHT CLICK ME"
370 })
371 .render()
372 })
373 .menu(move |_, cx| build_menu(cx, "top left")),
374 )
375 .child(
376 menu_handle()
377 .id("test1")
378 .child(|is_open| {
379 Label::new(if is_open {
380 "BOTTOM LEFT"
381 } else {
382 "RIGHT CLICK ME"
383 })
384 .render()
385 })
386 .anchor(AnchorCorner::BottomLeft)
387 .attach(AnchorCorner::TopLeft)
388 .menu(move |_, cx| build_menu(cx, "bottom left")),
389 ),
390 )
391 .child(
392 div()
393 .flex()
394 .flex_col()
395 .justify_between()
396 .child(
397 menu_handle()
398 .id("test3")
399 .child(|is_open| {
400 Label::new(if is_open {
401 "TOP RIGHT"
402 } else {
403 "RIGHT CLICK ME"
404 })
405 .render()
406 })
407 .anchor(AnchorCorner::TopRight)
408 .menu(move |_, cx| build_menu(cx, "top right")),
409 )
410 .child(
411 menu_handle()
412 .id("test4")
413 .child(|is_open| {
414 Label::new(if is_open {
415 "BOTTOM RIGHT"
416 } else {
417 "RIGHT CLICK ME"
418 })
419 .render()
420 })
421 .anchor(AnchorCorner::BottomRight)
422 .attach(AnchorCorner::TopRight)
423 .menu(move |_, cx| build_menu(cx, "bottom right")),
424 ),
425 )
426 }
427 }
428}