context_menu.rs

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