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