context_menu.rs

  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}