context_menu.rs

  1use std::cell::RefCell;
  2use std::rc::Rc;
  3
  4use crate::{prelude::*, v_stack, Label, List};
  5use crate::{ListItem, ListSeparator, ListSubHeader};
  6use gpui::{
  7    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase,
  8    Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton,
  9    MouseDownEvent, Pixels, Point, Render, View, VisualContext,
 10};
 11
 12pub enum ContextMenuItem {
 13    Separator,
 14    Header(SharedString),
 15    Entry(
 16        SharedString,
 17        Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>,
 18    ),
 19}
 20
 21pub struct ContextMenu {
 22    items: Vec<ContextMenuItem>,
 23    focus_handle: FocusHandle,
 24}
 25
 26impl FocusableView for ContextMenu {
 27    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
 28        self.focus_handle.clone()
 29    }
 30}
 31
 32impl EventEmitter<DismissEvent> for ContextMenu {}
 33
 34impl ContextMenu {
 35    pub fn build(
 36        cx: &mut WindowContext,
 37        f: impl FnOnce(Self, &mut WindowContext) -> Self,
 38    ) -> View<Self> {
 39        // let handle = cx.view().downgrade();
 40        cx.build_view(|cx| {
 41            f(
 42                Self {
 43                    items: Default::default(),
 44                    focus_handle: cx.focus_handle(),
 45                },
 46                cx,
 47            )
 48        })
 49    }
 50
 51    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
 52        self.items.push(ContextMenuItem::Header(title.into()));
 53        self
 54    }
 55
 56    pub fn separator(mut self) -> Self {
 57        self.items.push(ContextMenuItem::Separator);
 58        self
 59    }
 60
 61    pub fn entry(
 62        mut self,
 63        label: impl Into<SharedString>,
 64        on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
 65    ) -> Self {
 66        self.items
 67            .push(ContextMenuItem::Entry(label.into(), Rc::new(on_click)));
 68        self
 69    }
 70
 71    pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
 72        // todo: add the keybindings to the list entry
 73        self.entry(label.into(), move |_, cx| {
 74            cx.dispatch_action(action.boxed_clone())
 75        })
 76    }
 77
 78    pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 79        // todo!()
 80        cx.emit(DismissEvent::Dismiss);
 81    }
 82
 83    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
 84        cx.emit(DismissEvent::Dismiss);
 85    }
 86}
 87
 88impl Render for ContextMenu {
 89    type Element = Div;
 90
 91    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
 92        div().elevation_2(cx).flex().flex_row().child(
 93            v_stack()
 94                .min_w(px(200.))
 95                .track_focus(&self.focus_handle)
 96                .on_mouse_down_out(
 97                    cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)),
 98                )
 99                // .on_action(ContextMenu::select_first)
100                // .on_action(ContextMenu::select_last)
101                // .on_action(ContextMenu::select_next)
102                // .on_action(ContextMenu::select_prev)
103                .on_action(cx.listener(ContextMenu::confirm))
104                .on_action(cx.listener(ContextMenu::cancel))
105                .flex_none()
106                // .bg(cx.theme().colors().elevated_surface_background)
107                // .border()
108                // .border_color(cx.theme().colors().border)
109                .child(
110                    List::new().children(self.items.iter().map(|item| match item {
111                        ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
112                        ContextMenuItem::Header(header) => {
113                            ListSubHeader::new(header.clone()).into_any_element()
114                        }
115                        ContextMenuItem::Entry(entry, callback) => {
116                            let callback = callback.clone();
117                            let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
118
119                            ListItem::new(entry.clone())
120                                .child(Label::new(entry.clone()))
121                                .on_click(move |event, cx| {
122                                    callback(event, cx);
123                                    dismiss(event, cx)
124                                })
125                                .into_any_element()
126                        }
127                    })),
128                ),
129        )
130    }
131}
132
133pub struct MenuHandle<M: ManagedView> {
134    id: ElementId,
135    child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
136    menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
137    anchor: Option<AnchorCorner>,
138    attach: Option<AnchorCorner>,
139}
140
141impl<M: ManagedView> MenuHandle<M> {
142    pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
143        self.menu_builder = Some(Rc::new(f));
144        self
145    }
146
147    pub fn child<R: IntoElement>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
148        self.child_builder = Some(Box::new(|b| f(b).into_element().into_any()));
149        self
150    }
151
152    /// anchor defines which corner of the menu to anchor to the attachment point
153    /// (by default the cursor position, but see attach)
154    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
155        self.anchor = Some(anchor);
156        self
157    }
158
159    /// attach defines which corner of the handle to attach the menu's anchor to
160    pub fn attach(mut self, attach: AnchorCorner) -> Self {
161        self.attach = Some(attach);
162        self
163    }
164}
165
166pub fn menu_handle<M: ManagedView>(id: impl Into<ElementId>) -> MenuHandle<M> {
167    MenuHandle {
168        id: id.into(),
169        child_builder: None,
170        menu_builder: None,
171        anchor: None,
172        attach: None,
173    }
174}
175
176pub struct MenuHandleState<M> {
177    menu: Rc<RefCell<Option<View<M>>>>,
178    position: Rc<RefCell<Point<Pixels>>>,
179    child_layout_id: Option<LayoutId>,
180    child_element: Option<AnyElement>,
181    menu_element: Option<AnyElement>,
182}
183impl<M: ManagedView> Element for MenuHandle<M> {
184    type State = MenuHandleState<M>;
185
186    fn layout(
187        &mut self,
188        element_state: Option<Self::State>,
189        cx: &mut WindowContext,
190    ) -> (gpui::LayoutId, Self::State) {
191        let (menu, position) = if let Some(element_state) = element_state {
192            (element_state.menu, element_state.position)
193        } else {
194            (Rc::default(), Rc::default())
195        };
196
197        let mut menu_layout_id = None;
198
199        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
200            let mut overlay = overlay().snap_to_window();
201            if let Some(anchor) = self.anchor {
202                overlay = overlay.anchor(anchor);
203            }
204            overlay = overlay.position(*position.borrow());
205
206            let mut element = overlay.child(menu.clone()).into_any();
207            menu_layout_id = Some(element.layout(cx));
208            element
209        });
210
211        let mut child_element = self
212            .child_builder
213            .take()
214            .map(|child_builder| (child_builder)(menu.borrow().is_some()));
215
216        let child_layout_id = child_element
217            .as_mut()
218            .map(|child_element| child_element.layout(cx));
219
220        let layout_id = cx.request_layout(
221            &gpui::Style::default(),
222            menu_layout_id.into_iter().chain(child_layout_id),
223        );
224
225        (
226            layout_id,
227            MenuHandleState {
228                menu,
229                position,
230                child_element,
231                child_layout_id,
232                menu_element,
233            },
234        )
235    }
236
237    fn paint(
238        self,
239        bounds: Bounds<gpui::Pixels>,
240        element_state: &mut Self::State,
241        cx: &mut WindowContext,
242    ) {
243        if let Some(child) = element_state.child_element.take() {
244            child.paint(cx);
245        }
246
247        if let Some(menu) = element_state.menu_element.take() {
248            menu.paint(cx);
249            return;
250        }
251
252        let Some(builder) = self.menu_builder else {
253            return;
254        };
255        let menu = element_state.menu.clone();
256        let position = element_state.position.clone();
257        let attach = self.attach.clone();
258        let child_layout_id = element_state.child_layout_id.clone();
259
260        cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
261            if phase == DispatchPhase::Bubble
262                && event.button == MouseButton::Right
263                && bounds.contains_point(&event.position)
264            {
265                cx.stop_propagation();
266                cx.prevent_default();
267
268                let new_menu = (builder)(cx);
269                let menu2 = menu.clone();
270                cx.subscribe(&new_menu, move |modal, e, cx| match e {
271                    &DismissEvent::Dismiss => {
272                        *menu2.borrow_mut() = None;
273                        cx.notify();
274                    }
275                })
276                .detach();
277                cx.focus_view(&new_menu);
278                *menu.borrow_mut() = Some(new_menu);
279
280                *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
281                    attach
282                        .unwrap()
283                        .corner(cx.layout_bounds(child_layout_id.unwrap()))
284                } else {
285                    cx.mouse_position()
286                };
287                cx.notify();
288            }
289        });
290    }
291}
292
293impl<M: ManagedView> IntoElement for MenuHandle<M> {
294    type Element = Self;
295
296    fn element_id(&self) -> Option<gpui::ElementId> {
297        Some(self.id.clone())
298    }
299
300    fn into_element(self) -> Self::Element {
301        self
302    }
303}