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