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