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