Skip to main content

gpui_cn/menu/
context.rs

1use std::rc::Rc;
2
3use gpui_kit::{
4    AnyElement, App, ElementId, Entity, FocusHandle, InteractiveElement as _, IntoElement, Length,
5    MouseButton, ParentElement, RenderOnce, StyleRefinement, Styled, Window,
6    base::{StyledExt as _, TestSupportExt as _},
7    div,
8};
9
10use super::{MenuAnchor, MenuEntry, MenuState, dropdown::EntriesBuilder, panel::MenuPanels};
11
12/// A menu of commands that a right click on its content opens at the
13/// pointer, as shadcn's `ContextMenu`.
14///
15/// The rows are built as the menu opens, so their enabled state is
16/// current. The menu's top-left corner sits on the pointer, moved into
17/// the window when it would not fit. A right click inside the content
18/// does not reach a context menu around it.
19///
20/// ```
21/// use gpui_cn::{ContextMenu, MenuItem, MenuState};
22/// use gpui_kit::{Entity, IntoElement, ParentElement as _};
23///
24/// fn file(state: &Entity<MenuState>) -> impl IntoElement {
25///     ContextMenu::new("file", state)
26///         .items(|_, _| vec![MenuItem::new("reveal", "Reveal in Finder").into()])
27///         .child("notes.txt")
28/// }
29/// ```
30///
31/// The id keys the rows and the menu's motion, so it must be stable
32/// across frames.
33#[derive(IntoElement)]
34pub struct ContextMenu {
35    id: ElementId,
36    state: Entity<MenuState>,
37    items: Option<EntriesBuilder>,
38    action_context: Option<FocusHandle>,
39    menu_width: Option<Length>,
40    style: StyleRefinement,
41    children: Vec<AnyElement>,
42}
43
44impl ContextMenu {
45    /// A context menu on `state` around its children, with a stable id.
46    pub fn new(id: impl Into<ElementId>, state: &Entity<MenuState>) -> Self {
47        Self {
48            id: id.into(),
49            state: state.clone(),
50            items: None,
51            action_context: None,
52            menu_width: None,
53            style: StyleRefinement::default(),
54            children: Vec::new(),
55        }
56    }
57
58    /// Builds the rows each time the menu opens.
59    pub fn items(
60        mut self,
61        items: impl Fn(&mut Window, &mut App) -> Vec<MenuEntry> + 'static,
62    ) -> Self {
63        self.items = Some(Rc::new(items));
64        self
65    }
66
67    /// The element the items' actions go to, and whose key bindings their
68    /// shortcuts show. Without one they go to the element focused when
69    /// the menu opens, as its key binding would, or up from this element
70    /// when nothing is focused.
71    pub fn action_context(mut self, focus_handle: &FocusHandle) -> Self {
72        self.action_context = Some(focus_handle.clone());
73        self
74    }
75
76    /// A fixed menu width. Without one the menu is as wide as its rows,
77    /// between the theme's menu minimum and maximum.
78    pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
79        self.menu_width = Some(width.into());
80        self
81    }
82}
83
84impl Styled for ContextMenu {
85    fn style(&mut self) -> &mut StyleRefinement {
86        &mut self.style
87    }
88}
89
90impl ParentElement for ContextMenu {
91    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
92        self.children.extend(elements);
93    }
94}
95
96impl RenderOnce for ContextMenu {
97    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
98        let state = self.state;
99        let items = self.items;
100        let context = self.action_context;
101        div()
102            .id(self.id.clone())
103            .test_support()
104            .refine_style(&self.style)
105            .on_mouse_down(MouseButton::Right, {
106                let state = state.clone();
107                move |event, window, cx| {
108                    cx.stop_propagation();
109                    let entries = items
110                        .as_ref()
111                        .map(|items| items(window, cx))
112                        .unwrap_or_default();
113                    if entries.is_empty() {
114                        return;
115                    }
116                    let anchor = MenuAnchor::Point(event.position);
117                    state.update(cx, |state, cx| match &context {
118                        Some(context) => state.open_for(entries, anchor, context, window, cx),
119                        None => state.open(entries, anchor, window, cx),
120                    });
121                }
122            })
123            .children(self.children)
124            .child(MenuPanels::new(self.id, &state).width(self.menu_width))
125    }
126}