context_menu.rs

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