popover_menu.rs

  1use std::{cell::RefCell, rc::Rc};
  2
  3use gpui::{
  4    overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase,
  5    Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent,
  6    ParentElement, Pixels, Point, View, VisualContext, WindowContext,
  7};
  8
  9use crate::{Clickable, Selectable};
 10
 11pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
 12
 13impl<T: IntoElement + Clickable + Selectable + 'static> PopoverTrigger for T {}
 14
 15pub struct PopoverMenu<M: ManagedView> {
 16    id: ElementId,
 17    child_builder: Option<
 18        Box<
 19            dyn FnOnce(
 20                    Rc<RefCell<Option<View<M>>>>,
 21                    Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
 22                ) -> AnyElement
 23                + 'static,
 24        >,
 25    >,
 26    menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
 27    anchor: AnchorCorner,
 28    attach: Option<AnchorCorner>,
 29    offset: Option<Point<Pixels>>,
 30}
 31
 32impl<M: ManagedView> PopoverMenu<M> {
 33    pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
 34        self.menu_builder = Some(Rc::new(f));
 35        self
 36    }
 37
 38    pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
 39        self.child_builder = Some(Box::new(|menu, builder| {
 40            let open = menu.borrow().is_some();
 41            t.selected(open)
 42                .when_some(builder, |el, builder| {
 43                    el.on_click({
 44                        move |_, cx| {
 45                            let new_menu = (builder)(cx);
 46                            let menu2 = menu.clone();
 47                            let previous_focus_handle = cx.focused();
 48
 49                            cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
 50                                if modal.focus_handle(cx).contains_focused(cx) {
 51                                    if previous_focus_handle.is_some() {
 52                                        cx.focus(&previous_focus_handle.as_ref().unwrap())
 53                                    }
 54                                }
 55                                *menu2.borrow_mut() = None;
 56                                cx.notify();
 57                            })
 58                            .detach();
 59                            cx.focus_view(&new_menu);
 60                            *menu.borrow_mut() = Some(new_menu);
 61                        }
 62                    })
 63                })
 64                .into_any_element()
 65        }));
 66        self
 67    }
 68
 69    /// anchor defines which corner of the menu to anchor to the attachment point
 70    /// (by default the cursor position, but see attach)
 71    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
 72        self.anchor = anchor;
 73        self
 74    }
 75
 76    /// attach defines which corner of the handle to attach the menu's anchor to
 77    pub fn attach(mut self, attach: AnchorCorner) -> Self {
 78        self.attach = Some(attach);
 79        self
 80    }
 81
 82    /// offset offsets the position of the content by that many pixels.
 83    pub fn offset(mut self, offset: Point<Pixels>) -> Self {
 84        self.offset = Some(offset);
 85        self
 86    }
 87
 88    fn resolved_attach(&self) -> AnchorCorner {
 89        self.attach.unwrap_or_else(|| match self.anchor {
 90            AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
 91            AnchorCorner::TopRight => AnchorCorner::BottomRight,
 92            AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
 93            AnchorCorner::BottomRight => AnchorCorner::TopRight,
 94        })
 95    }
 96
 97    fn resolved_offset(&self, cx: &WindowContext) -> Point<Pixels> {
 98        self.offset.unwrap_or_else(|| {
 99            // Default offset = 4px padding + 1px border
100            let offset = rems(5. / 16.) * cx.rem_size();
101            match self.anchor {
102                AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)),
103                AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)),
104            }
105        })
106    }
107}
108
109pub fn popover_menu<M: ManagedView>(id: impl Into<ElementId>) -> PopoverMenu<M> {
110    PopoverMenu {
111        id: id.into(),
112        child_builder: None,
113        menu_builder: None,
114        anchor: AnchorCorner::TopLeft,
115        attach: None,
116        offset: None,
117    }
118}
119
120pub struct PopoverMenuState<M> {
121    child_layout_id: Option<LayoutId>,
122    child_element: Option<AnyElement>,
123    child_bounds: Option<Bounds<Pixels>>,
124    menu_element: Option<AnyElement>,
125    menu: Rc<RefCell<Option<View<M>>>>,
126}
127
128impl<M: ManagedView> Element for PopoverMenu<M> {
129    type State = PopoverMenuState<M>;
130
131    fn layout(
132        &mut self,
133        element_state: Option<Self::State>,
134        cx: &mut WindowContext,
135    ) -> (gpui::LayoutId, Self::State) {
136        let mut menu_layout_id = None;
137
138        let (menu, child_bounds) = if let Some(element_state) = element_state {
139            (element_state.menu, element_state.child_bounds)
140        } else {
141            (Rc::default(), None)
142        };
143
144        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
145            let mut overlay = overlay().snap_to_window().anchor(self.anchor);
146
147            if let Some(child_bounds) = child_bounds {
148                overlay = overlay.position(
149                    self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx),
150                );
151            }
152
153            let mut element = overlay.child(menu.clone()).into_any();
154            menu_layout_id = Some(element.layout(cx));
155            element
156        });
157
158        let mut child_element = self
159            .child_builder
160            .take()
161            .map(|child_builder| (child_builder)(menu.clone(), self.menu_builder.clone()));
162
163        let child_layout_id = child_element
164            .as_mut()
165            .map(|child_element| child_element.layout(cx));
166
167        let layout_id = cx.request_layout(
168            &gpui::Style::default(),
169            menu_layout_id.into_iter().chain(child_layout_id),
170        );
171
172        (
173            layout_id,
174            PopoverMenuState {
175                menu,
176                child_element,
177                child_layout_id,
178                menu_element,
179                child_bounds,
180            },
181        )
182    }
183
184    fn paint(
185        &mut self,
186        _: Bounds<gpui::Pixels>,
187        element_state: &mut Self::State,
188        cx: &mut WindowContext,
189    ) {
190        if let Some(mut child) = element_state.child_element.take() {
191            child.paint(cx);
192        }
193
194        if let Some(child_layout_id) = element_state.child_layout_id.take() {
195            element_state.child_bounds = Some(cx.layout_bounds(child_layout_id));
196        }
197
198        if let Some(mut menu) = element_state.menu_element.take() {
199            menu.paint(cx);
200
201            if let Some(child_bounds) = element_state.child_bounds {
202                let interactive_bounds = InteractiveBounds {
203                    bounds: child_bounds,
204                    stacking_order: cx.stacking_order().clone(),
205                };
206
207                // Mouse-downing outside the menu dismisses it, so we don't
208                // want a click on the toggle to re-open it.
209                cx.on_mouse_event(move |e: &MouseDownEvent, phase, cx| {
210                    if phase == DispatchPhase::Bubble
211                        && interactive_bounds.visibly_contains(&e.position, cx)
212                    {
213                        cx.stop_propagation()
214                    }
215                })
216            }
217        }
218    }
219}
220
221impl<M: ManagedView> IntoElement for PopoverMenu<M> {
222    type Element = Self;
223
224    fn element_id(&self) -> Option<gpui::ElementId> {
225        Some(self.id.clone())
226    }
227
228    fn into_element(self) -> Self::Element {
229        self
230    }
231}