context_menu.rs

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