gpui_component/menu/
context_menu.rs

1use std::{cell::RefCell, rc::Rc};
2
3use gpui::{
4    anchored, deferred, div, prelude::FluentBuilder, px, relative, AnyElement, App, Context,
5    Corner, DismissEvent, Element, ElementId, Entity, Focusable, GlobalElementId,
6    InspectorElementId, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
7    ParentElement, Pixels, Point, Position, Stateful, Style, Subscription, Window,
8};
9
10use crate::menu::popup_menu::PopupMenu;
11
12pub trait ContextMenuExt: ParentElement + Sized {
13    fn context_menu(
14        self,
15        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
16    ) -> Self {
17        self.child(ContextMenu::new("context-menu").menu(f))
18    }
19}
20
21impl<E> ContextMenuExt for Stateful<E> where E: ParentElement {}
22
23/// A context menu that can be shown on right-click.
24pub struct ContextMenu {
25    id: ElementId,
26    menu:
27        Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>,
28    anchor: Corner,
29}
30
31impl ContextMenu {
32    pub fn new(id: impl Into<ElementId>) -> Self {
33        Self {
34            id: id.into(),
35            menu: None,
36            anchor: Corner::TopLeft,
37        }
38    }
39
40    #[must_use]
41    pub fn menu<F>(mut self, builder: F) -> Self
42    where
43        F: Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
44    {
45        self.menu = Some(Box::new(builder));
46        self
47    }
48
49    fn with_element_state<R>(
50        &mut self,
51        id: &GlobalElementId,
52        window: &mut Window,
53        cx: &mut App,
54        f: impl FnOnce(&mut Self, &mut ContextMenuState, &mut Window, &mut App) -> R,
55    ) -> R {
56        window.with_optional_element_state::<ContextMenuState, _>(
57            Some(id),
58            |element_state, window| {
59                let mut element_state = element_state.unwrap().unwrap_or_default();
60                let result = f(self, &mut element_state, window, cx);
61                (result, Some(element_state))
62            },
63        )
64    }
65}
66
67impl IntoElement for ContextMenu {
68    type Element = Self;
69
70    fn into_element(self) -> Self::Element {
71        self
72    }
73}
74
75struct ContextMenuSharedState {
76    menu_view: Option<Entity<PopupMenu>>,
77    open: bool,
78    position: Point<Pixels>,
79    _subscription: Option<Subscription>,
80}
81
82pub struct ContextMenuState {
83    menu_element: Option<AnyElement>,
84    shared_state: Rc<RefCell<ContextMenuSharedState>>,
85}
86
87impl Default for ContextMenuState {
88    fn default() -> Self {
89        Self {
90            menu_element: None,
91            shared_state: Rc::new(RefCell::new(ContextMenuSharedState {
92                menu_view: None,
93                open: false,
94                position: Default::default(),
95                _subscription: None,
96            })),
97        }
98    }
99}
100
101impl Element for ContextMenu {
102    type RequestLayoutState = ContextMenuState;
103    type PrepaintState = ();
104
105    fn id(&self) -> Option<ElementId> {
106        Some(self.id.clone())
107    }
108
109    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
110        None
111    }
112
113    fn request_layout(
114        &mut self,
115        id: Option<&gpui::GlobalElementId>,
116        _: Option<&gpui::InspectorElementId>,
117        window: &mut Window,
118        cx: &mut App,
119    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
120        let mut style = Style::default();
121        // Set the layout style relative to the table view to get same size.
122        style.position = Position::Absolute;
123        style.flex_grow = 1.0;
124        style.flex_shrink = 1.0;
125        style.size.width = relative(1.).into();
126        style.size.height = relative(1.).into();
127
128        let anchor = self.anchor;
129
130        self.with_element_state(
131            id.unwrap(),
132            window,
133            cx,
134            |_, state: &mut ContextMenuState, window, cx| {
135                let (position, open) = {
136                    let shared_state = state.shared_state.borrow();
137                    (shared_state.position, shared_state.open)
138                };
139                let menu_view = state.shared_state.borrow().menu_view.clone();
140                let (menu_element, menu_layout_id) = if open {
141                    let has_menu_item = menu_view
142                        .as_ref()
143                        .map(|menu| !menu.read(cx).is_empty())
144                        .unwrap_or(false);
145
146                    if has_menu_item {
147                        let mut menu_element = deferred(
148                            anchored()
149                                .position(position)
150                                .snap_to_window_with_margin(px(8.))
151                                .anchor(anchor)
152                                .when_some(menu_view, |this, menu| {
153                                    // Focus the menu, so that can be handle the action.
154                                    if !menu.focus_handle(cx).contains_focused(window, cx) {
155                                        menu.focus_handle(cx).focus(window);
156                                    }
157
158                                    this.child(div().occlude().child(menu.clone()))
159                                }),
160                        )
161                        .with_priority(1)
162                        .into_any();
163
164                        let menu_layout_id = menu_element.request_layout(window, cx);
165                        (Some(menu_element), Some(menu_layout_id))
166                    } else {
167                        (None, None)
168                    }
169                } else {
170                    (None, None)
171                };
172
173                let mut layout_ids = vec![];
174                if let Some(menu_layout_id) = menu_layout_id {
175                    layout_ids.push(menu_layout_id);
176                }
177
178                let layout_id = window.request_layout(style, layout_ids, cx);
179
180                (
181                    layout_id,
182                    ContextMenuState {
183                        menu_element,
184
185                        ..Default::default()
186                    },
187                )
188            },
189        )
190    }
191
192    fn prepaint(
193        &mut self,
194        _: Option<&gpui::GlobalElementId>,
195        _: Option<&InspectorElementId>,
196        _: gpui::Bounds<gpui::Pixels>,
197        request_layout: &mut Self::RequestLayoutState,
198        window: &mut Window,
199        cx: &mut App,
200    ) -> Self::PrepaintState {
201        if let Some(menu_element) = &mut request_layout.menu_element {
202            menu_element.prepaint(window, cx);
203        }
204    }
205
206    fn paint(
207        &mut self,
208        id: Option<&gpui::GlobalElementId>,
209        _: Option<&InspectorElementId>,
210        bounds: gpui::Bounds<gpui::Pixels>,
211        request_layout: &mut Self::RequestLayoutState,
212        _: &mut Self::PrepaintState,
213        window: &mut Window,
214        cx: &mut App,
215    ) {
216        if let Some(menu_element) = &mut request_layout.menu_element {
217            menu_element.paint(window, cx);
218        }
219
220        let Some(builder) = self.menu.take() else {
221            return;
222        };
223
224        self.with_element_state(
225            id.unwrap(),
226            window,
227            cx,
228            |_view, state: &mut ContextMenuState, window, _| {
229                let shared_state = state.shared_state.clone();
230
231                // When right mouse click, to build content menu, and show it at the mouse position.
232                window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
233                    if phase.bubble()
234                        && event.button == MouseButton::Right
235                        && bounds.contains(&event.position)
236                    {
237                        {
238                            let mut shared_state = shared_state.borrow_mut();
239                            shared_state.position = event.position;
240                            shared_state.open = true;
241                        }
242
243                        let menu = PopupMenu::build(window, cx, |menu, window, cx| {
244                            (builder)(menu, window, cx)
245                        })
246                        .into_element();
247
248                        let _subscription = window.subscribe(&menu, cx, {
249                            let shared_state = shared_state.clone();
250                            move |_, _: &DismissEvent, window, _| {
251                                shared_state.borrow_mut().open = false;
252                                window.refresh();
253                            }
254                        });
255
256                        shared_state.borrow_mut().menu_view = Some(menu.clone());
257                        shared_state.borrow_mut()._subscription = Some(_subscription);
258                        window.refresh();
259                    }
260                });
261            },
262        );
263    }
264}