Skip to main content

glassy_ui/
context_menu.rs

1use std::rc::Rc;
2
3use gpui::{
4    anchored, deferred, div, point, prelude::*, px, AnchoredPositionMode, AnyElement, App,
5    IntoElement, KeyDownEvent, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString,
6    StyleRefinement, Styled, Window,
7};
8
9use crate::compat::StyleCompatExt;
10
11use crate::dropdown_menu::{
12    handle_menu_keydown, initial_highlight, render_panel, set_open, DropdownMenuEntry,
13    DropdownMenuState, MenuPanelContext, OpenChangeHandler,
14};
15use crate::motion::StyledSlot;
16
17/// Pointer-anchored menu. Same items as [`crate::DropdownMenu`], origin at the click.
18#[derive(IntoElement)]
19pub struct ContextMenu {
20    id: SharedString,
21    controlled_open: Option<bool>,
22    default_open: bool,
23    position: Option<Point<Pixels>>,
24    entries: Vec<DropdownMenuEntry>,
25    on_open_change: Option<OpenChangeHandler>,
26    style: StyleRefinement,
27    children: Vec<AnyElement>,
28}
29
30impl ContextMenu {
31    pub fn new(id: impl Into<SharedString>) -> Self {
32        Self {
33            id: id.into(),
34            controlled_open: None,
35            default_open: false,
36            position: None,
37            entries: Vec::new(),
38            on_open_change: None,
39            style: StyleRefinement::default(),
40            children: Vec::new(),
41        }
42    }
43
44    pub fn open(mut self, open: bool) -> Self {
45        self.controlled_open = Some(open);
46        self
47    }
48
49    pub fn default_open(mut self, open: bool) -> Self {
50        self.default_open = open;
51        self
52    }
53
54    /// Local origin used when the menu is shown without a pointer event.
55    pub fn position(mut self, origin: Point<Pixels>) -> Self {
56        self.position = Some(origin);
57        self
58    }
59
60    pub fn entries(mut self, entries: impl IntoIterator<Item = DropdownMenuEntry>) -> Self {
61        self.entries = entries.into_iter().collect();
62        self
63    }
64
65    pub fn on_open_change(
66        mut self,
67        listener: impl Fn(bool, &mut Window, &mut App) + 'static,
68    ) -> Self {
69        self.on_open_change = Some(Rc::new(listener));
70        self
71    }
72}
73
74impl Styled for ContextMenu {
75    fn style(&mut self) -> &mut StyleRefinement {
76        &mut self.style
77    }
78}
79
80impl ParentElement for ContextMenu {
81    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
82        self.children.extend(elements);
83    }
84}
85
86impl RenderOnce for ContextMenu {
87    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
88        let initial_open = self.controlled_open.unwrap_or(self.default_open);
89        let initial_entries = self.entries.clone();
90        let initial_origin = self.position.unwrap_or_else(|| point(px(0.), px(0.)));
91        let state = window.use_keyed_state(self.id.clone(), cx, move |_, cx| DropdownMenuState {
92            focus_handle: cx.focus_handle().tab_stop(true),
93            open: initial_open,
94            highlighted: initial_open
95                .then(|| initial_highlight(&initial_entries))
96                .flatten(),
97            submenu: None,
98            origin: initial_origin,
99            origin_window: false,
100            previous_focus: None,
101        });
102
103        if let Some(controlled_open) = self.controlled_open {
104            if state.read(cx).open != controlled_open {
105                state.update(cx, |menu, _| {
106                    menu.open = controlled_open;
107                    menu.highlighted = controlled_open
108                        .then(|| initial_highlight(&self.entries))
109                        .flatten();
110                    menu.submenu = None;
111                });
112            }
113        }
114        if let Some(position) = self.position {
115            if state.read(cx).origin != position || state.read(cx).origin_window {
116                state.update(cx, |menu, _| {
117                    menu.origin = position;
118                    menu.origin_window = false;
119                });
120            }
121        }
122
123        let open = state.read(cx).open;
124        let highlighted = state.read(cx).highlighted;
125        let submenu = state.read(cx).submenu;
126        let origin = state.read(cx).origin;
127        let origin_window = state.read(cx).origin_window;
128        let focus_handle = state.read(cx).focus_handle.clone();
129        let restore_focus = state
130            .read(cx)
131            .previous_focus
132            .clone()
133            .unwrap_or_else(|| focus_handle.clone());
134
135        let user_change = self.on_open_change.clone();
136        let restore_state = state.clone();
137        let on_open_change: OpenChangeHandler = Rc::new(move |open, window, cx| {
138            if !open {
139                let previous = restore_state.read(cx).previous_focus.clone();
140                if let Some(previous) = previous {
141                    previous.focus(window);
142                }
143            }
144            if let Some(user_change) = &user_change {
145                user_change(open, window, cx);
146            }
147        });
148
149        let click_state = state.clone();
150        let click_entries = self.entries.clone();
151        let click_change = on_open_change.clone();
152        let click_focus = focus_handle.clone();
153        let keyboard_state = state.clone();
154        let keyboard_entries = self.entries.clone();
155        let keyboard_change = on_open_change.clone();
156        let keyboard_restore = restore_focus.clone();
157
158        let target = div()
159            .id(SharedString::from(format!("{}-target", self.id)))
160            .debug_selector({
161                let selector = format!("{}-target", self.id);
162                move || selector.clone()
163            })
164            .relative()
165            .flex()
166            .flex_none()
167            .self_start()
168            .refine_style(&self.style)
169            .on_mouse_down(MouseButton::Right, move |event, window, cx| {
170                let previous = window.focused(cx);
171                click_state.update(cx, |menu, cx| {
172                    menu.origin = event.position;
173                    menu.origin_window = true;
174                    menu.previous_focus = previous;
175                    cx.notify();
176                });
177                set_open(
178                    &click_state,
179                    true,
180                    &click_entries,
181                    Some(&click_change),
182                    window,
183                    cx,
184                );
185                click_focus.focus(window);
186                cx.stop_propagation();
187            })
188            .children(self.children);
189
190        let panel_context = MenuPanelContext {
191            state: state.clone(),
192            root_entries: self.entries.clone(),
193            focus_handle: focus_handle.clone(),
194            on_open_change: Some(on_open_change.clone()),
195        };
196        let panel = render_panel(
197            self.id.clone(),
198            self.entries,
199            highlighted,
200            submenu.map(|submenu| submenu.parent),
201            panel_context,
202            submenu.is_none(),
203            cx,
204        );
205
206        let menu = deferred(
207            anchored()
208                .anchor(gpui::Corner::TopLeft)
209                .position(origin)
210                .position_mode(if origin_window {
211                    AnchoredPositionMode::Window
212                } else {
213                    AnchoredPositionMode::Local
214                })
215                .snap_to_window_with_margin(px(8.))
216                .child(
217                    div()
218                        .track_focus(&focus_handle)
219                        .on_key_down(move |event: &KeyDownEvent, window, cx| {
220                            handle_menu_keydown(
221                                event,
222                                &keyboard_state,
223                                &keyboard_entries,
224                                Some(&keyboard_change),
225                                &keyboard_restore,
226                                window,
227                                cx,
228                            );
229                        })
230                        .child(panel),
231                ),
232        )
233        .with_priority(2);
234
235        target.when(open, |target| target.child(menu))
236    }
237}