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