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}