Skip to main content

rgpui_component/native_menu/
mod.rs

1//! A menu rendered natively by the operating system.
2//!
3//! Unlike [`crate::menu::PopupMenu`], which is drawn by GPUI and therefore
4//! clipped to the window bounds, [`NativeMenu`] is rendered by the OS. It can
5//! extend beyond the window 鈥?useful for small windows where a GPUI-drawn popup
6//! menu would otherwise be cut off.
7//!
8//! Items carry a GPUI [`Action`], dispatched via [`Window::dispatch_action`]
9//! when selected 鈥?the same mechanism the application menu bar and key bindings
10//! use. A [`NativeMenu`] can therefore be built directly from GPUI
11//! [`rgpui::MenuItem`]s (see [`NativeMenu::from_menu_items`] /
12//! [`From<rgpui::Menu>`]).
13//!
14//! ```ignore
15//! use rgpui_component::native_menu::NativeMenu;
16//!
17//! NativeMenu::new()
18//!     .menu("Copy", Box::new(Copy))
19//!     .menu("Paste", Box::new(Paste))
20//!     .separator()
21//!     .menu("Delete", Box::new(Delete))
22//!     .show(position, window, cx);
23//! ```
24
25use rgpui::{Action, App, Pixels, Point, SharedString, Window};
26
27#[cfg(target_os = "macos")]
28mod macos;
29#[cfg(target_os = "windows")]
30mod windows;
31
32// Drawn-menu fallback (used on platforms without an OS-native popup, e.g. Linux).
33// Compiled on all platforms because `Root` holds the overlay entity.
34mod fallback;
35pub(crate) use fallback::FallbackMenuOverlay;
36
37enum NativeMenuItem {
38    Separator,
39    Item {
40        label: SharedString,
41        disabled: bool,
42        checked: bool,
43        /// Action dispatched when the item is selected.
44        action: Option<Box<dyn Action>>,
45    },
46    Submenu {
47        label: SharedString,
48        disabled: bool,
49        items: Vec<NativeMenuItem>,
50    },
51}
52
53/// A menu rendered by the operating system.
54///
55/// Build it with the [`NativeMenu::menu`] / [`NativeMenu::separator`] builders,
56/// then call [`NativeMenu::show`] to display it at a position.
57#[derive(Default)]
58pub struct NativeMenu {
59    items: Vec<NativeMenuItem>,
60}
61
62impl NativeMenu {
63    /// Create an empty native menu.
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Append a clickable item that dispatches `action` when selected.
69    pub fn menu(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
70        self.menu_with(label, false, false, Some(action))
71    }
72
73    /// Append an item, controlling its `disabled` state.
74    pub fn menu_with_disabled(
75        self,
76        label: impl Into<SharedString>,
77        disabled: bool,
78        action: Box<dyn Action>,
79    ) -> Self {
80        self.menu_with(label, disabled, false, Some(action))
81    }
82
83    /// Append an item, controlling its `checked` state (a check mark is shown).
84    pub fn menu_with_check(
85        self,
86        label: impl Into<SharedString>,
87        checked: bool,
88        action: Box<dyn Action>,
89    ) -> Self {
90        self.menu_with(label, false, checked, Some(action))
91    }
92
93    fn menu_with(
94        mut self,
95        label: impl Into<SharedString>,
96        disabled: bool,
97        checked: bool,
98        action: Option<Box<dyn Action>>,
99    ) -> Self {
100        self.items.push(NativeMenuItem::Item {
101            label: label.into(),
102            disabled,
103            checked,
104            action,
105        });
106        self
107    }
108
109    /// Append a separator line.
110    pub fn separator(mut self) -> Self {
111        self.items.push(NativeMenuItem::Separator);
112        self
113    }
114
115    /// Append a submenu built from another [`NativeMenu`].
116    pub fn submenu(mut self, label: impl Into<SharedString>, submenu: NativeMenu) -> Self {
117        self.items.push(NativeMenuItem::Submenu {
118            label: label.into(),
119            disabled: false,
120            items: submenu.items,
121        });
122        self
123    }
124
125    /// Whether the menu has no items.
126    pub fn is_empty(&self) -> bool {
127        self.items.is_empty()
128    }
129
130    /// Pop up the menu at `position` (window coordinates, in logical pixels).
131    ///
132    /// The menu is shown without blocking the caller: the OS tracking loop runs
133    /// off GPUI's call stack, so GPUI is not borrowed while it is open. When an
134    /// item is selected, its action is dispatched via [`Window::dispatch_action`].
135    pub fn show(self, position: Point<Pixels>, window: &mut Window, cx: &mut App) {
136        if self.items.is_empty() {
137            return;
138        }
139
140        #[cfg(target_os = "macos")]
141        macos::show(self.items, position, window, cx);
142        #[cfg(target_os = "windows")]
143        windows::show(self.items, position, window, cx);
144        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
145        fallback::show(self.items, position, window, cx);
146    }
147}
148
149/// Reuse an existing GPUI menu definition as a native menu.
150///
151/// `Action`s, separators, submenus, `checked`, and `disabled` are mapped over;
152/// system menus (e.g. macOS Services) have no native popup equivalent and are
153/// skipped.
154impl From<rgpui::Menu> for NativeMenu {
155    fn from(menu: rgpui::Menu) -> Self {
156        let mut native = Self::new();
157        for item in menu.items {
158            match item {
159                rgpui::MenuItem::Separator => native.items.push(NativeMenuItem::Separator),
160                rgpui::MenuItem::Action {
161                    name,
162                    action,
163                    checked,
164                    disabled,
165                    ..
166                } => native.items.push(NativeMenuItem::Item {
167                    label: name,
168                    disabled,
169                    checked,
170                    action: Some(action),
171                }),
172                rgpui::MenuItem::Submenu(submenu) => native.items.push(NativeMenuItem::Submenu {
173                    label: submenu.name.clone(),
174                    disabled: submenu.disabled,
175                    items: Self::from(submenu).items,
176                }),
177                rgpui::MenuItem::SystemMenu(_) => {}
178            }
179        }
180        native
181    }
182}