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