right_click_menu.rs

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