gpui_component/menu/
dropdown_menu.rs

1use std::rc::Rc;
2
3use gpui::{
4    Context, Corner, DismissEvent, ElementId, Entity, Focusable, InteractiveElement, IntoElement,
5    RenderOnce, SharedString, StyleRefinement, Styled, Window,
6};
7
8use crate::{button::Button, menu::PopupMenu, popover::Popover, Selectable};
9
10/// A dropdown menu trait for buttons and other interactive elements
11pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
12    /// Create a dropdown menu with the given items, anchored to the TopLeft corner
13    fn dropdown_menu(
14        self,
15        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
16    ) -> DropdownMenuPopover<Self> {
17        self.dropdown_menu_with_anchor(Corner::TopLeft, f)
18    }
19
20    /// Create a dropdown menu with the given items, anchored to the given corner
21    fn dropdown_menu_with_anchor(
22        mut self,
23        anchor: impl Into<Corner>,
24        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
25    ) -> DropdownMenuPopover<Self> {
26        let style = self.style().clone();
27        let id = self.interactivity().element_id.clone();
28
29        DropdownMenuPopover::new(id.unwrap_or(0.into()), anchor, self, f).trigger_style(style)
30    }
31}
32
33impl DropdownMenu for Button {}
34
35#[derive(IntoElement)]
36pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
37    id: ElementId,
38    style: StyleRefinement,
39    anchor: Corner,
40    trigger: T,
41    builder: Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>,
42}
43
44impl<T> DropdownMenuPopover<T>
45where
46    T: Selectable + IntoElement + 'static,
47{
48    fn new(
49        id: ElementId,
50        anchor: impl Into<Corner>,
51        trigger: T,
52        builder: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
53    ) -> Self {
54        Self {
55            id: SharedString::from(format!("dropdown-menu:{:?}", id)).into(),
56            style: StyleRefinement::default(),
57            anchor: anchor.into(),
58            trigger,
59            builder: Rc::new(builder),
60        }
61    }
62
63    /// Set the anchor corner for the dropdown menu popover.
64    pub fn anchor(mut self, anchor: impl Into<Corner>) -> Self {
65        self.anchor = anchor.into();
66        self
67    }
68
69    /// Set the style refinement for the dropdown menu trigger.
70    fn trigger_style(mut self, style: StyleRefinement) -> Self {
71        self.style = style;
72        self
73    }
74}
75
76#[derive(Default)]
77struct DropdownMenuState {
78    menu: Option<Entity<PopupMenu>>,
79}
80
81impl<T> RenderOnce for DropdownMenuPopover<T>
82where
83    T: Selectable + IntoElement + 'static,
84{
85    fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
86        let builder = self.builder.clone();
87        let menu_state =
88            window.use_keyed_state(self.id.clone(), cx, |_, _| DropdownMenuState::default());
89
90        Popover::new(SharedString::from(format!("popover:{}", self.id)))
91            .appearance(false)
92            .overlay_closable(false)
93            .trigger(self.trigger)
94            .trigger_style(self.style)
95            .anchor(self.anchor)
96            .content(move |_, window, cx| {
97                // Here is special logic to only create the PopupMenu once and reuse it.
98                // Because this `content` will called in every time render, so we need to store the menu
99                // in state to avoid recreating at every render.
100                //
101                // And we also need to rebuild the menu when it is dismissed, to rebuild menu items
102                // dynamically for support `dropdown_menu` method, so we listen for DismissEvent below.
103                let menu = match menu_state.read(cx).menu.clone() {
104                    Some(menu) => menu,
105                    None => {
106                        let builder = builder.clone();
107                        let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
108                            builder(menu, window, cx)
109                        });
110                        menu_state.update(cx, |state, _| {
111                            state.menu = Some(menu.clone());
112                        });
113                        menu.focus_handle(cx).focus(window);
114
115                        // Listen for dismiss events from the PopupMenu to close the popover.
116                        let popover_state = cx.entity();
117                        window
118                            .subscribe(&menu, cx, {
119                                let menu_state = menu_state.clone();
120                                move |_, _: &DismissEvent, window, cx| {
121                                    popover_state.update(cx, |state, cx| {
122                                        state.dismiss(window, cx);
123                                    });
124                                    menu_state.update(cx, |state, _| {
125                                        state.menu = None;
126                                    });
127                                }
128                            })
129                            .detach();
130
131                        menu.clone()
132                    }
133                };
134
135                menu.clone()
136            })
137    }
138}