right_click_menu.rs

  1use std::{cell::RefCell, rc::Rc};
  2
  3use gpui::{
  4    overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, ElementId,
  5    IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
  6    View, VisualContext, WindowContext,
  7};
  8
  9pub struct RightClickMenu<M: ManagedView> {
 10    id: ElementId,
 11    child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
 12    menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
 13    anchor: Option<AnchorCorner>,
 14    attach: Option<AnchorCorner>,
 15}
 16
 17impl<M: ManagedView> RightClickMenu<M> {
 18    pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
 19        self.menu_builder = Some(Rc::new(f));
 20        self
 21    }
 22
 23    pub fn trigger<E: IntoElement + 'static>(mut self, e: E) -> Self {
 24        self.child_builder = Some(Box::new(move |_| e.into_any_element()));
 25        self
 26    }
 27
 28    /// anchor defines which corner of the menu to anchor to the attachment point
 29    /// (by default the cursor position, but see attach)
 30    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
 31        self.anchor = Some(anchor);
 32        self
 33    }
 34
 35    /// attach defines which corner of the handle to attach the menu's anchor to
 36    pub fn attach(mut self, attach: AnchorCorner) -> Self {
 37        self.attach = Some(attach);
 38        self
 39    }
 40}
 41
 42pub fn right_click_menu<M: ManagedView>(id: impl Into<ElementId>) -> RightClickMenu<M> {
 43    RightClickMenu {
 44        id: id.into(),
 45        child_builder: None,
 46        menu_builder: None,
 47        anchor: None,
 48        attach: None,
 49    }
 50}
 51
 52pub struct MenuHandleState<M> {
 53    menu: Rc<RefCell<Option<View<M>>>>,
 54    position: Rc<RefCell<Point<Pixels>>>,
 55    child_layout_id: Option<LayoutId>,
 56    child_element: Option<AnyElement>,
 57    menu_element: Option<AnyElement>,
 58}
 59
 60impl<M: ManagedView> Element for RightClickMenu<M> {
 61    type State = MenuHandleState<M>;
 62
 63    fn layout(
 64        &mut self,
 65        element_state: Option<Self::State>,
 66        cx: &mut WindowContext,
 67    ) -> (gpui::LayoutId, Self::State) {
 68        let (menu, position) = if let Some(element_state) = element_state {
 69            (element_state.menu, element_state.position)
 70        } else {
 71            (Rc::default(), Rc::default())
 72        };
 73
 74        let mut menu_layout_id = None;
 75
 76        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
 77            let mut overlay = overlay().snap_to_window();
 78            if let Some(anchor) = self.anchor {
 79                overlay = overlay.anchor(anchor);
 80            }
 81            overlay = overlay.position(*position.borrow());
 82
 83            let mut element = overlay.child(menu.clone()).into_any();
 84            menu_layout_id = Some(element.layout(cx));
 85            element
 86        });
 87
 88        let mut child_element = self
 89            .child_builder
 90            .take()
 91            .map(|child_builder| (child_builder)(menu.borrow().is_some()));
 92
 93        let child_layout_id = child_element
 94            .as_mut()
 95            .map(|child_element| child_element.layout(cx));
 96
 97        let layout_id = cx.request_layout(
 98            &gpui::Style::default(),
 99            menu_layout_id.into_iter().chain(child_layout_id),
100        );
101
102        (
103            layout_id,
104            MenuHandleState {
105                menu,
106                position,
107                child_element,
108                child_layout_id,
109                menu_element,
110            },
111        )
112    }
113
114    fn paint(
115        self,
116        bounds: Bounds<gpui::Pixels>,
117        element_state: &mut Self::State,
118        cx: &mut WindowContext,
119    ) {
120        if let Some(child) = element_state.child_element.take() {
121            child.paint(cx);
122        }
123
124        if let Some(menu) = element_state.menu_element.take() {
125            menu.paint(cx);
126            return;
127        }
128
129        let Some(builder) = self.menu_builder else {
130            return;
131        };
132        let menu = element_state.menu.clone();
133        let position = element_state.position.clone();
134        let attach = self.attach.clone();
135        let child_layout_id = element_state.child_layout_id.clone();
136
137        cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
138            if phase == DispatchPhase::Bubble
139                && event.button == MouseButton::Right
140                && bounds.contains_point(&event.position)
141            {
142                cx.stop_propagation();
143                cx.prevent_default();
144
145                let new_menu = (builder)(cx);
146                let menu2 = menu.clone();
147                let previous_focus_handle = cx.focused();
148
149                cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
150                    if modal.focus_handle(cx).contains_focused(cx) {
151                        if previous_focus_handle.is_some() {
152                            cx.focus(&previous_focus_handle.as_ref().unwrap())
153                        }
154                    }
155                    *menu2.borrow_mut() = None;
156                    cx.notify();
157                })
158                .detach();
159                cx.focus_view(&new_menu);
160                *menu.borrow_mut() = Some(new_menu);
161
162                *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
163                    attach
164                        .unwrap()
165                        .corner(cx.layout_bounds(child_layout_id.unwrap()))
166                } else {
167                    cx.mouse_position()
168                };
169                cx.notify();
170            }
171        });
172    }
173}
174
175impl<M: ManagedView> IntoElement for RightClickMenu<M> {
176    type Element = Self;
177
178    fn element_id(&self) -> Option<gpui::ElementId> {
179        Some(self.id.clone())
180    }
181
182    fn into_element(self) -> Self::Element {
183        self
184    }
185}