right_click_menu.rs

  1use std::{cell::RefCell, rc::Rc};
  2
  3use gpui::{
  4    AnyElement, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity,
  5    Focusable as _, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId,
  6    ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Window, anchored,
  7    deferred, div, px,
  8};
  9
 10pub struct RightClickMenu<M: ManagedView> {
 11    id: ElementId,
 12    child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
 13    menu_builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> Entity<M> + 'static>>,
 14    anchor: Option<Corner>,
 15    attach: Option<Corner>,
 16}
 17
 18impl<M: ManagedView> RightClickMenu<M> {
 19    pub fn menu(mut self, f: impl Fn(&mut Window, &mut App) -> Entity<M> + 'static) -> Self {
 20        self.menu_builder = Some(Rc::new(f));
 21        self
 22    }
 23
 24    pub fn trigger<E: IntoElement + 'static>(mut self, e: E) -> Self {
 25        self.child_builder = Some(Box::new(move |_| e.into_any_element()));
 26        self
 27    }
 28
 29    /// anchor defines which corner of the menu to anchor to the attachment point
 30    /// (by default the cursor position, but see attach)
 31    pub fn anchor(mut self, anchor: Corner) -> Self {
 32        self.anchor = Some(anchor);
 33        self
 34    }
 35
 36    /// attach defines which corner of the handle to attach the menu's anchor to
 37    pub fn attach(mut self, attach: Corner) -> Self {
 38        self.attach = Some(attach);
 39        self
 40    }
 41
 42    fn with_element_state<R>(
 43        &mut self,
 44        global_id: &GlobalElementId,
 45        window: &mut Window,
 46        cx: &mut App,
 47        f: impl FnOnce(&mut Self, &mut MenuHandleElementState<M>, &mut Window, &mut App) -> R,
 48    ) -> R {
 49        window.with_optional_element_state::<MenuHandleElementState<M>, _>(
 50            Some(global_id),
 51            |element_state, window| {
 52                let mut element_state = element_state.unwrap().unwrap_or_default();
 53                let result = f(self, &mut element_state, window, cx);
 54                (result, Some(element_state))
 55            },
 56        )
 57    }
 58}
 59
 60/// Creates a [`RightClickMenu`]
 61pub fn right_click_menu<M: ManagedView>(id: impl Into<ElementId>) -> RightClickMenu<M> {
 62    RightClickMenu {
 63        id: id.into(),
 64        child_builder: None,
 65        menu_builder: None,
 66        anchor: None,
 67        attach: None,
 68    }
 69}
 70
 71pub struct MenuHandleElementState<M> {
 72    menu: Rc<RefCell<Option<Entity<M>>>>,
 73    position: Rc<RefCell<Point<Pixels>>>,
 74}
 75
 76impl<M> Clone for MenuHandleElementState<M> {
 77    fn clone(&self) -> Self {
 78        Self {
 79            menu: Rc::clone(&self.menu),
 80            position: Rc::clone(&self.position),
 81        }
 82    }
 83}
 84
 85impl<M> Default for MenuHandleElementState<M> {
 86    fn default() -> Self {
 87        Self {
 88            menu: Rc::default(),
 89            position: Rc::default(),
 90        }
 91    }
 92}
 93
 94pub struct RequestLayoutState {
 95    child_layout_id: Option<LayoutId>,
 96    child_element: Option<AnyElement>,
 97    menu_element: Option<AnyElement>,
 98}
 99
100pub struct PrepaintState {
101    hitbox: Hitbox,
102    child_bounds: Option<Bounds<Pixels>>,
103}
104
105impl<M: ManagedView> Element for RightClickMenu<M> {
106    type RequestLayoutState = RequestLayoutState;
107    type PrepaintState = PrepaintState;
108
109    fn id(&self) -> Option<ElementId> {
110        Some(self.id.clone())
111    }
112
113    fn request_layout(
114        &mut self,
115        id: Option<&GlobalElementId>,
116        window: &mut Window,
117        cx: &mut App,
118    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
119        self.with_element_state(
120            id.unwrap(),
121            window,
122            cx,
123            |this, element_state, window, cx| {
124                let mut menu_layout_id = None;
125
126                let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| {
127                    let mut anchored = anchored().snap_to_window_with_margin(px(8.));
128                    if let Some(anchor) = this.anchor {
129                        anchored = anchored.anchor(anchor);
130                    }
131                    anchored = anchored.position(*element_state.position.borrow());
132
133                    let mut element = deferred(anchored.child(div().occlude().child(menu.clone())))
134                        .with_priority(1)
135                        .into_any();
136
137                    menu_layout_id = Some(element.request_layout(window, cx));
138                    element
139                });
140
141                let mut child_element = this
142                    .child_builder
143                    .take()
144                    .map(|child_builder| (child_builder)(element_state.menu.borrow().is_some()));
145
146                let child_layout_id = child_element
147                    .as_mut()
148                    .map(|child_element| child_element.request_layout(window, cx));
149
150                let layout_id = window.request_layout(
151                    gpui::Style::default(),
152                    menu_layout_id.into_iter().chain(child_layout_id),
153                    cx,
154                );
155
156                (
157                    layout_id,
158                    RequestLayoutState {
159                        child_element,
160                        child_layout_id,
161                        menu_element,
162                    },
163                )
164            },
165        )
166    }
167
168    fn prepaint(
169        &mut self,
170        _id: Option<&GlobalElementId>,
171        bounds: Bounds<Pixels>,
172        request_layout: &mut Self::RequestLayoutState,
173        window: &mut Window,
174        cx: &mut App,
175    ) -> PrepaintState {
176        let hitbox = window.insert_hitbox(bounds, false);
177
178        if let Some(child) = request_layout.child_element.as_mut() {
179            child.prepaint(window, cx);
180        }
181
182        if let Some(menu) = request_layout.menu_element.as_mut() {
183            menu.prepaint(window, cx);
184        }
185
186        PrepaintState {
187            hitbox,
188            child_bounds: request_layout
189                .child_layout_id
190                .map(|layout_id| window.layout_bounds(layout_id)),
191        }
192    }
193
194    fn paint(
195        &mut self,
196        id: Option<&GlobalElementId>,
197        _bounds: Bounds<gpui::Pixels>,
198        request_layout: &mut Self::RequestLayoutState,
199        prepaint_state: &mut Self::PrepaintState,
200        window: &mut Window,
201        cx: &mut App,
202    ) {
203        self.with_element_state(
204            id.unwrap(),
205            window,
206            cx,
207            |this, element_state, window, cx| {
208                if let Some(mut child) = request_layout.child_element.take() {
209                    child.paint(window, cx);
210                }
211
212                if let Some(mut menu) = request_layout.menu_element.take() {
213                    menu.paint(window, cx);
214                    return;
215                }
216
217                let Some(builder) = this.menu_builder.take() else {
218                    return;
219                };
220
221                let attach = this.attach;
222                let menu = element_state.menu.clone();
223                let position = element_state.position.clone();
224                let child_bounds = prepaint_state.child_bounds;
225
226                let hitbox_id = prepaint_state.hitbox.id;
227                window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
228                    if phase == DispatchPhase::Bubble
229                        && event.button == MouseButton::Right
230                        && hitbox_id.is_hovered(window)
231                    {
232                        cx.stop_propagation();
233                        window.prevent_default();
234
235                        let new_menu = (builder)(window, cx);
236                        let menu2 = menu.clone();
237                        let previous_focus_handle = window.focused(cx);
238
239                        window
240                            .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| {
241                                if modal.focus_handle(cx).contains_focused(window, cx) {
242                                    if let Some(previous_focus_handle) =
243                                        previous_focus_handle.as_ref()
244                                    {
245                                        window.focus(previous_focus_handle);
246                                    }
247                                }
248                                *menu2.borrow_mut() = None;
249                                window.refresh();
250                            })
251                            .detach();
252                        window.focus(&new_menu.focus_handle(cx));
253                        *menu.borrow_mut() = Some(new_menu);
254                        *position.borrow_mut() = if let Some(child_bounds) = child_bounds {
255                            if let Some(attach) = attach {
256                                child_bounds.corner(attach)
257                            } else {
258                                window.mouse_position()
259                            }
260                        } else {
261                            window.mouse_position()
262                        };
263                        window.refresh();
264                    }
265                });
266            },
267        )
268    }
269}
270
271impl<M: ManagedView> IntoElement for RightClickMenu<M> {
272    type Element = Self;
273
274    fn into_element(self) -> Self::Element {
275        self
276    }
277}