context_menu.rs

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