popover_menu.rs

  1#![allow(missing_docs)]
  2
  3use std::{cell::RefCell, rc::Rc};
  4
  5use gpui::{
  6    anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, AnyView, App,
  7    Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable as _,
  8    GlobalElementId, HitboxId, InteractiveElement, IntoElement, LayoutId, Length, ManagedView,
  9    MouseDownEvent, ParentElement, Pixels, Point, Style, Window,
 10};
 11
 12use crate::prelude::*;
 13
 14pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {}
 15
 16impl<T: IntoElement + Clickable + Toggleable + 'static> PopoverTrigger for T {}
 17
 18impl<T: Clickable> Clickable for gpui::AnimationElement<T>
 19where
 20    T: Clickable + 'static,
 21{
 22    fn on_click(
 23        self,
 24        handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
 25    ) -> Self {
 26        self.map_element(|e| e.on_click(handler))
 27    }
 28
 29    fn cursor_style(self, cursor_style: gpui::CursorStyle) -> Self {
 30        self.map_element(|e| e.cursor_style(cursor_style))
 31    }
 32}
 33
 34impl<T: Toggleable> Toggleable for gpui::AnimationElement<T>
 35where
 36    T: Toggleable + 'static,
 37{
 38    fn toggle_state(self, selected: bool) -> Self {
 39        self.map_element(|e| e.toggle_state(selected))
 40    }
 41}
 42
 43pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>);
 44
 45impl<M> Clone for PopoverMenuHandle<M> {
 46    fn clone(&self) -> Self {
 47        Self(self.0.clone())
 48    }
 49}
 50
 51impl<M> Default for PopoverMenuHandle<M> {
 52    fn default() -> Self {
 53        Self(Rc::default())
 54    }
 55}
 56
 57struct PopoverMenuHandleState<M> {
 58    menu_builder: Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
 59    menu: Rc<RefCell<Option<Entity<M>>>>,
 60    on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
 61}
 62
 63impl<M: ManagedView> PopoverMenuHandle<M> {
 64    pub fn show(&self, window: &mut Window, cx: &mut App) {
 65        if let Some(state) = self.0.borrow().as_ref() {
 66            show_menu(
 67                &state.menu_builder,
 68                &state.menu,
 69                state.on_open.clone(),
 70                window,
 71                cx,
 72            );
 73        }
 74    }
 75
 76    pub fn hide(&self, cx: &mut App) {
 77        if let Some(state) = self.0.borrow().as_ref() {
 78            if let Some(menu) = state.menu.borrow().as_ref() {
 79                menu.update(cx, |_, cx| cx.emit(DismissEvent));
 80            }
 81        }
 82    }
 83
 84    pub fn toggle(&self, window: &mut Window, cx: &mut App) {
 85        if let Some(state) = self.0.borrow().as_ref() {
 86            if state.menu.borrow().is_some() {
 87                self.hide(cx);
 88            } else {
 89                self.show(window, cx);
 90            }
 91        }
 92    }
 93
 94    pub fn is_deployed(&self) -> bool {
 95        self.0
 96            .borrow()
 97            .as_ref()
 98            .map_or(false, |state| state.menu.borrow().as_ref().is_some())
 99    }
100
101    pub fn is_focused(&self, window: &Window, cx: &App) -> bool {
102        self.0.borrow().as_ref().map_or(false, |state| {
103            state
104                .menu
105                .borrow()
106                .as_ref()
107                .map_or(false, |model| model.focus_handle(cx).is_focused(window))
108        })
109    }
110}
111
112pub struct PopoverMenu<M: ManagedView> {
113    id: ElementId,
114    child_builder: Option<
115        Box<
116            dyn FnOnce(
117                    Rc<RefCell<Option<Entity<M>>>>,
118                    Option<Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>> + 'static>>,
119                ) -> AnyElement
120                + 'static,
121        >,
122    >,
123    menu_builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>> + 'static>>,
124    anchor: Corner,
125    attach: Option<Corner>,
126    offset: Option<Point<Pixels>>,
127    trigger_handle: Option<PopoverMenuHandle<M>>,
128    on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
129    full_width: bool,
130}
131
132impl<M: ManagedView> PopoverMenu<M> {
133    /// Returns a new [`PopoverMenu`].
134    pub fn new(id: impl Into<ElementId>) -> Self {
135        Self {
136            id: id.into(),
137            child_builder: None,
138            menu_builder: None,
139            anchor: Corner::TopLeft,
140            attach: None,
141            offset: None,
142            trigger_handle: None,
143            on_open: None,
144            full_width: false,
145        }
146    }
147
148    pub fn full_width(mut self, full_width: bool) -> Self {
149        self.full_width = full_width;
150        self
151    }
152
153    pub fn menu(
154        mut self,
155        f: impl Fn(&mut Window, &mut App) -> Option<Entity<M>> + 'static,
156    ) -> Self {
157        self.menu_builder = Some(Rc::new(f));
158        self
159    }
160
161    pub fn with_handle(mut self, handle: PopoverMenuHandle<M>) -> Self {
162        self.trigger_handle = Some(handle);
163        self
164    }
165
166    pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
167        let on_open = self.on_open.clone();
168        self.child_builder = Some(Box::new(move |menu, builder| {
169            let open = menu.borrow().is_some();
170            t.toggle_state(open)
171                .when_some(builder, |el, builder| {
172                    el.on_click(move |_event, window, cx| {
173                        show_menu(&builder, &menu, on_open.clone(), window, cx)
174                    })
175                })
176                .into_any_element()
177        }));
178        self
179    }
180
181    pub fn trigger_with_tooltip<T: PopoverTrigger + ButtonCommon>(
182        mut self,
183        t: T,
184        tooltip_builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
185    ) -> Self {
186        let on_open = self.on_open.clone();
187        self.child_builder = Some(Box::new(move |menu, builder| {
188            let open = menu.borrow().is_some();
189            t.toggle_state(open)
190                .when_some(builder, |el, builder| {
191                    el.on_click(move |_, window, cx| {
192                        show_menu(&builder, &menu, on_open.clone(), window, cx)
193                    })
194                    .when(!open, |t| {
195                        t.tooltip(move |window, cx| tooltip_builder(window, cx))
196                    })
197                })
198                .into_any_element()
199        }));
200        self
201    }
202
203    /// anchor defines which corner of the menu to anchor to the attachment point
204    /// (by default the cursor position, but see attach)
205    pub fn anchor(mut self, anchor: Corner) -> Self {
206        self.anchor = anchor;
207        self
208    }
209
210    /// attach defines which corner of the handle to attach the menu's anchor to
211    pub fn attach(mut self, attach: Corner) -> Self {
212        self.attach = Some(attach);
213        self
214    }
215
216    /// offset offsets the position of the content by that many pixels.
217    pub fn offset(mut self, offset: Point<Pixels>) -> Self {
218        self.offset = Some(offset);
219        self
220    }
221
222    /// attach something upon opening the menu
223    pub fn on_open(mut self, on_open: Rc<dyn Fn(&mut Window, &mut App)>) -> Self {
224        self.on_open = Some(on_open);
225        self
226    }
227
228    fn resolved_attach(&self) -> Corner {
229        self.attach.unwrap_or(match self.anchor {
230            Corner::TopLeft => Corner::BottomLeft,
231            Corner::TopRight => Corner::BottomRight,
232            Corner::BottomLeft => Corner::TopLeft,
233            Corner::BottomRight => Corner::TopRight,
234        })
235    }
236
237    fn resolved_offset(&self, window: &mut Window) -> Point<Pixels> {
238        self.offset.unwrap_or_else(|| {
239            // Default offset = 4px padding + 1px border
240            let offset = rems_from_px(5.) * window.rem_size();
241            match self.anchor {
242                Corner::TopRight | Corner::BottomRight => point(offset, px(0.)),
243                Corner::TopLeft | Corner::BottomLeft => point(-offset, px(0.)),
244            }
245        })
246    }
247}
248
249fn show_menu<M: ManagedView>(
250    builder: &Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
251    menu: &Rc<RefCell<Option<Entity<M>>>>,
252    on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
253    window: &mut Window,
254    cx: &mut App,
255) {
256    let Some(new_menu) = (builder)(window, cx) else {
257        return;
258    };
259    let menu2 = menu.clone();
260    let previous_focus_handle = window.focused(cx);
261
262    window
263        .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| {
264            if modal.focus_handle(cx).contains_focused(window, cx) {
265                if let Some(previous_focus_handle) = previous_focus_handle.as_ref() {
266                    window.focus(previous_focus_handle);
267                }
268            }
269            *menu2.borrow_mut() = None;
270            window.refresh();
271        })
272        .detach();
273    window.focus(&new_menu.focus_handle(cx));
274    *menu.borrow_mut() = Some(new_menu);
275    window.refresh();
276
277    if let Some(on_open) = on_open {
278        on_open(window, cx);
279    }
280}
281
282pub struct PopoverMenuElementState<M> {
283    menu: Rc<RefCell<Option<Entity<M>>>>,
284    child_bounds: Option<Bounds<Pixels>>,
285}
286
287impl<M> Clone for PopoverMenuElementState<M> {
288    fn clone(&self) -> Self {
289        Self {
290            menu: Rc::clone(&self.menu),
291            child_bounds: self.child_bounds,
292        }
293    }
294}
295
296impl<M> Default for PopoverMenuElementState<M> {
297    fn default() -> Self {
298        Self {
299            menu: Rc::default(),
300            child_bounds: None,
301        }
302    }
303}
304
305pub struct PopoverMenuFrameState<M: ManagedView> {
306    child_layout_id: Option<LayoutId>,
307    child_element: Option<AnyElement>,
308    menu_element: Option<AnyElement>,
309    menu_handle: Rc<RefCell<Option<Entity<M>>>>,
310}
311
312impl<M: ManagedView> Element for PopoverMenu<M> {
313    type RequestLayoutState = PopoverMenuFrameState<M>;
314    type PrepaintState = Option<HitboxId>;
315
316    fn id(&self) -> Option<ElementId> {
317        Some(self.id.clone())
318    }
319
320    fn request_layout(
321        &mut self,
322        global_id: Option<&GlobalElementId>,
323        window: &mut Window,
324        cx: &mut App,
325    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
326        window.with_element_state(
327            global_id.unwrap(),
328            |element_state: Option<PopoverMenuElementState<M>>, window| {
329                let element_state = element_state.unwrap_or_default();
330                let mut menu_layout_id = None;
331
332                let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| {
333                    let offset = self.resolved_offset(window);
334                    let mut anchored = anchored()
335                        .snap_to_window_with_margin(px(8.))
336                        .anchor(self.anchor)
337                        .offset(offset);
338                    if let Some(child_bounds) = element_state.child_bounds {
339                        anchored =
340                            anchored.position(child_bounds.corner(self.resolved_attach()) + offset);
341                    }
342                    let mut element = deferred(anchored.child(div().occlude().child(menu.clone())))
343                        .with_priority(1)
344                        .into_any();
345
346                    menu_layout_id = Some(element.request_layout(window, cx));
347                    element
348                });
349
350                let mut child_element = self.child_builder.take().map(|child_builder| {
351                    (child_builder)(element_state.menu.clone(), self.menu_builder.clone())
352                });
353
354                if let Some(trigger_handle) = self.trigger_handle.take() {
355                    if let Some(menu_builder) = self.menu_builder.clone() {
356                        *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState {
357                            menu_builder,
358                            menu: element_state.menu.clone(),
359                            on_open: self.on_open.clone(),
360                        });
361                    }
362                }
363
364                let child_layout_id = child_element
365                    .as_mut()
366                    .map(|child_element| child_element.request_layout(window, cx));
367
368                let mut style = Style::default();
369                if self.full_width {
370                    style.size = size(relative(1.).into(), Length::Auto);
371                }
372
373                let layout_id = window.request_layout(
374                    style,
375                    menu_layout_id.into_iter().chain(child_layout_id),
376                    cx,
377                );
378
379                (
380                    (
381                        layout_id,
382                        PopoverMenuFrameState {
383                            child_element,
384                            child_layout_id,
385                            menu_element,
386                            menu_handle: element_state.menu.clone(),
387                        },
388                    ),
389                    element_state,
390                )
391            },
392        )
393    }
394
395    fn prepaint(
396        &mut self,
397        global_id: Option<&GlobalElementId>,
398        _bounds: Bounds<Pixels>,
399        request_layout: &mut Self::RequestLayoutState,
400        window: &mut Window,
401        cx: &mut App,
402    ) -> Option<HitboxId> {
403        if let Some(child) = request_layout.child_element.as_mut() {
404            child.prepaint(window, cx);
405        }
406
407        if let Some(menu) = request_layout.menu_element.as_mut() {
408            menu.prepaint(window, cx);
409        }
410
411        request_layout.child_layout_id.map(|layout_id| {
412            let bounds = window.layout_bounds(layout_id);
413            window.with_element_state(global_id.unwrap(), |element_state, _cx| {
414                let mut element_state: PopoverMenuElementState<M> = element_state.unwrap();
415                element_state.child_bounds = Some(bounds);
416                ((), element_state)
417            });
418
419            window.insert_hitbox(bounds, false).id
420        })
421    }
422
423    fn paint(
424        &mut self,
425        _id: Option<&GlobalElementId>,
426        _: Bounds<gpui::Pixels>,
427        request_layout: &mut Self::RequestLayoutState,
428        child_hitbox: &mut Option<HitboxId>,
429        window: &mut Window,
430        cx: &mut App,
431    ) {
432        if let Some(mut child) = request_layout.child_element.take() {
433            child.paint(window, cx);
434        }
435
436        if let Some(mut menu) = request_layout.menu_element.take() {
437            menu.paint(window, cx);
438
439            if let Some(child_hitbox) = *child_hitbox {
440                let menu_handle = request_layout.menu_handle.clone();
441                // Mouse-downing outside the menu dismisses it, so we don't
442                // want a click on the toggle to re-open it.
443                window.on_mouse_event(move |_: &MouseDownEvent, phase, window, cx| {
444                    if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(window) {
445                        if let Some(menu) = menu_handle.borrow().as_ref() {
446                            menu.update(cx, |_, cx| {
447                                cx.emit(DismissEvent);
448                            });
449                        }
450                        cx.stop_propagation();
451                    }
452                })
453            }
454        }
455    }
456}
457
458impl<M: ManagedView> IntoElement for PopoverMenu<M> {
459    type Element = Self;
460
461    fn into_element(self) -> Self::Element {
462        self
463    }
464}