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