popover_menu.rs

  1use std::{cell::RefCell, rc::Rc};
  2
  3use gpui::{
  4    anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnchorCorner, AnyElement,
  5    Bounds, DismissEvent, DispatchPhase, Element, ElementId, GlobalElementId, HitboxId,
  6    InteractiveElement, IntoElement, LayoutId, Length, ManagedView, MouseDownEvent, ParentElement,
  7    Pixels, Point, Style, View, VisualContext, WindowContext,
  8};
  9
 10use crate::prelude::*;
 11
 12pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
 13
 14impl<T: IntoElement + Clickable + Selectable + 'static> PopoverTrigger for T {}
 15
 16pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>);
 17
 18impl<M> Clone for PopoverMenuHandle<M> {
 19    fn clone(&self) -> Self {
 20        Self(self.0.clone())
 21    }
 22}
 23
 24impl<M> Default for PopoverMenuHandle<M> {
 25    fn default() -> Self {
 26        Self(Rc::default())
 27    }
 28}
 29
 30struct PopoverMenuHandleState<M> {
 31    menu_builder: Rc<dyn Fn(&mut WindowContext) -> Option<View<M>>>,
 32    menu: Rc<RefCell<Option<View<M>>>>,
 33}
 34
 35impl<M: ManagedView> PopoverMenuHandle<M> {
 36    pub fn show(&self, cx: &mut WindowContext) {
 37        if let Some(state) = self.0.borrow().as_ref() {
 38            show_menu(&state.menu_builder, &state.menu, cx);
 39        }
 40    }
 41
 42    pub fn hide(&self, cx: &mut WindowContext) {
 43        if let Some(state) = self.0.borrow().as_ref() {
 44            if let Some(menu) = state.menu.borrow().as_ref() {
 45                menu.update(cx, |_, cx| cx.emit(DismissEvent));
 46            }
 47        }
 48    }
 49
 50    pub fn toggle(&self, cx: &mut WindowContext) {
 51        if let Some(state) = self.0.borrow().as_ref() {
 52            if state.menu.borrow().is_some() {
 53                self.hide(cx);
 54            } else {
 55                self.show(cx);
 56            }
 57        }
 58    }
 59}
 60
 61pub struct PopoverMenu<M: ManagedView> {
 62    id: ElementId,
 63    child_builder: Option<
 64        Box<
 65            dyn FnOnce(
 66                    Rc<RefCell<Option<View<M>>>>,
 67                    Option<Rc<dyn Fn(&mut WindowContext) -> Option<View<M>> + 'static>>,
 68                ) -> AnyElement
 69                + 'static,
 70        >,
 71    >,
 72    menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> Option<View<M>> + 'static>>,
 73    anchor: AnchorCorner,
 74    attach: Option<AnchorCorner>,
 75    offset: Option<Point<Pixels>>,
 76    trigger_handle: Option<PopoverMenuHandle<M>>,
 77    full_width: bool,
 78}
 79
 80impl<M: ManagedView> PopoverMenu<M> {
 81    /// Returns a new [`PopoverMenu`].
 82    pub fn new(id: impl Into<ElementId>) -> Self {
 83        Self {
 84            id: id.into(),
 85            child_builder: None,
 86            menu_builder: None,
 87            anchor: AnchorCorner::TopLeft,
 88            attach: None,
 89            offset: None,
 90            trigger_handle: None,
 91            full_width: false,
 92        }
 93    }
 94
 95    pub fn full_width(mut self, full_width: bool) -> Self {
 96        self.full_width = full_width;
 97        self
 98    }
 99
100    pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> Option<View<M>> + 'static) -> Self {
101        self.menu_builder = Some(Rc::new(f));
102        self
103    }
104
105    pub fn with_handle(mut self, handle: PopoverMenuHandle<M>) -> Self {
106        self.trigger_handle = Some(handle);
107        self
108    }
109
110    pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
111        self.child_builder = Some(Box::new(|menu, builder| {
112            let open = menu.borrow().is_some();
113            t.selected(open)
114                .when_some(builder, |el, builder| {
115                    el.on_click(move |_, cx| show_menu(&builder, &menu, cx))
116                })
117                .into_any_element()
118        }));
119        self
120    }
121
122    /// anchor defines which corner of the menu to anchor to the attachment point
123    /// (by default the cursor position, but see attach)
124    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
125        self.anchor = anchor;
126        self
127    }
128
129    /// attach defines which corner of the handle to attach the menu's anchor to
130    pub fn attach(mut self, attach: AnchorCorner) -> Self {
131        self.attach = Some(attach);
132        self
133    }
134
135    /// offset offsets the position of the content by that many pixels.
136    pub fn offset(mut self, offset: Point<Pixels>) -> Self {
137        self.offset = Some(offset);
138        self
139    }
140
141    fn resolved_attach(&self) -> AnchorCorner {
142        self.attach.unwrap_or_else(|| match self.anchor {
143            AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
144            AnchorCorner::TopRight => AnchorCorner::BottomRight,
145            AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
146            AnchorCorner::BottomRight => AnchorCorner::TopRight,
147        })
148    }
149
150    fn resolved_offset(&self, cx: &WindowContext) -> Point<Pixels> {
151        self.offset.unwrap_or_else(|| {
152            // Default offset = 4px padding + 1px border
153            let offset = rems_from_px(5.) * cx.rem_size();
154            match self.anchor {
155                AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)),
156                AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)),
157            }
158        })
159    }
160}
161
162fn show_menu<M: ManagedView>(
163    builder: &Rc<dyn Fn(&mut WindowContext) -> Option<View<M>>>,
164    menu: &Rc<RefCell<Option<View<M>>>>,
165    cx: &mut WindowContext,
166) {
167    let Some(new_menu) = (builder)(cx) else {
168        return;
169    };
170    let menu2 = menu.clone();
171    let previous_focus_handle = cx.focused();
172
173    cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
174        if modal.focus_handle(cx).contains_focused(cx) {
175            if let Some(previous_focus_handle) = previous_focus_handle.as_ref() {
176                cx.focus(previous_focus_handle);
177            }
178        }
179        *menu2.borrow_mut() = None;
180        cx.refresh();
181    })
182    .detach();
183    cx.focus_view(&new_menu);
184    *menu.borrow_mut() = Some(new_menu);
185    cx.refresh();
186}
187
188pub struct PopoverMenuElementState<M> {
189    menu: Rc<RefCell<Option<View<M>>>>,
190    child_bounds: Option<Bounds<Pixels>>,
191}
192
193impl<M> Clone for PopoverMenuElementState<M> {
194    fn clone(&self) -> Self {
195        Self {
196            menu: Rc::clone(&self.menu),
197            child_bounds: self.child_bounds,
198        }
199    }
200}
201
202impl<M> Default for PopoverMenuElementState<M> {
203    fn default() -> Self {
204        Self {
205            menu: Rc::default(),
206            child_bounds: None,
207        }
208    }
209}
210
211pub struct PopoverMenuFrameState {
212    child_layout_id: Option<LayoutId>,
213    child_element: Option<AnyElement>,
214    menu_element: Option<AnyElement>,
215}
216
217impl<M: ManagedView> Element for PopoverMenu<M> {
218    type RequestLayoutState = PopoverMenuFrameState;
219    type PrepaintState = Option<HitboxId>;
220
221    fn id(&self) -> Option<ElementId> {
222        Some(self.id.clone())
223    }
224
225    fn request_layout(
226        &mut self,
227        global_id: Option<&GlobalElementId>,
228        cx: &mut WindowContext,
229    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
230        cx.with_element_state(
231            global_id.unwrap(),
232            |element_state: Option<PopoverMenuElementState<M>>, cx| {
233                let element_state = element_state.unwrap_or_default();
234                let mut menu_layout_id = None;
235
236                let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| {
237                    let mut anchored = anchored().snap_to_window().anchor(self.anchor);
238                    if let Some(child_bounds) = element_state.child_bounds {
239                        anchored = anchored.position(
240                            self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx),
241                        );
242                    }
243                    let mut element = deferred(anchored.child(div().occlude().child(menu.clone())))
244                        .with_priority(1)
245                        .into_any();
246
247                    menu_layout_id = Some(element.request_layout(cx));
248                    element
249                });
250
251                let mut child_element = self.child_builder.take().map(|child_builder| {
252                    (child_builder)(element_state.menu.clone(), self.menu_builder.clone())
253                });
254
255                if let Some(trigger_handle) = self.trigger_handle.take() {
256                    if let Some(menu_builder) = self.menu_builder.clone() {
257                        *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState {
258                            menu_builder,
259                            menu: element_state.menu.clone(),
260                        });
261                    }
262                }
263
264                let child_layout_id = child_element
265                    .as_mut()
266                    .map(|child_element| child_element.request_layout(cx));
267
268                let mut style = Style::default();
269                if self.full_width {
270                    style.size = size(relative(1.).into(), Length::Auto);
271                }
272
273                let layout_id =
274                    cx.request_layout(style, menu_layout_id.into_iter().chain(child_layout_id));
275
276                (
277                    (
278                        layout_id,
279                        PopoverMenuFrameState {
280                            child_element,
281                            child_layout_id,
282                            menu_element,
283                        },
284                    ),
285                    element_state,
286                )
287            },
288        )
289    }
290
291    fn prepaint(
292        &mut self,
293        global_id: Option<&GlobalElementId>,
294        _bounds: Bounds<Pixels>,
295        request_layout: &mut Self::RequestLayoutState,
296        cx: &mut WindowContext,
297    ) -> Option<HitboxId> {
298        if let Some(child) = request_layout.child_element.as_mut() {
299            child.prepaint(cx);
300        }
301
302        if let Some(menu) = request_layout.menu_element.as_mut() {
303            menu.prepaint(cx);
304        }
305
306        let hitbox_id = request_layout.child_layout_id.map(|layout_id| {
307            let bounds = cx.layout_bounds(layout_id);
308            cx.with_element_state(global_id.unwrap(), |element_state, _cx| {
309                let mut element_state: PopoverMenuElementState<M> = element_state.unwrap();
310                element_state.child_bounds = Some(bounds);
311                ((), element_state)
312            });
313
314            cx.insert_hitbox(bounds, false).id
315        });
316
317        hitbox_id
318    }
319
320    fn paint(
321        &mut self,
322        _id: Option<&GlobalElementId>,
323        _: Bounds<gpui::Pixels>,
324        request_layout: &mut Self::RequestLayoutState,
325        child_hitbox: &mut Option<HitboxId>,
326        cx: &mut WindowContext,
327    ) {
328        if let Some(mut child) = request_layout.child_element.take() {
329            child.paint(cx);
330        }
331
332        if let Some(mut menu) = request_layout.menu_element.take() {
333            menu.paint(cx);
334
335            if let Some(child_hitbox) = *child_hitbox {
336                // Mouse-downing outside the menu dismisses it, so we don't
337                // want a click on the toggle to re-open it.
338                cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| {
339                    if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(cx) {
340                        cx.stop_propagation()
341                    }
342                })
343            }
344        }
345    }
346}
347
348impl<M: ManagedView> IntoElement for PopoverMenu<M> {
349    type Element = Self;
350
351    fn into_element(self) -> Self::Element {
352        self
353    }
354}