gpui/platform/app_menu.rs
1use crate::{Action, App, Platform, SharedString};
2
3/// A menu in the application's menu bar, or a submenu inside another menu.
4///
5/// Build a menu with [`Menu::new`] and fill it with [`Menu::items`]. Pass the menus to
6/// [`App::set_menus`] to show them. Each call to `set_menus` replaces the whole menu bar.
7/// Only macOS draws the menu bar from these menus. Linux and Windows store them, and
8/// [`App::get_menus`] returns them, but they don't draw a menu bar.
9///
10/// Picking an [`MenuItem::Action`] dispatches its action through [`App::dispatch_action`].
11/// An [`App::on_action`] handler for that action runs for the menu item and for its key
12/// binding alike.
13///
14/// # Examples
15///
16/// ```standalone_crate
17/// use std::{cell::Cell, rc::Rc};
18///
19/// use gpui::{App, Menu, MenuItem, OsAction, SystemMenuType, TestApp, actions};
20///
21/// actions!(app_menu_example, [Preferences, Undo]);
22///
23/// fn set_up_menus(cx: &mut App, preferences_opened: Rc<Cell<bool>>) {
24/// // A menu item and a key binding for the same action share one handler.
25/// cx.on_action(move |_: &Preferences, _cx| preferences_opened.set(true));
26/// cx.on_action(|_: &Undo, _cx| {
27/// // Undo the last edit here.
28/// });
29/// cx.set_menus([
30/// Menu::new("App").items([
31/// MenuItem::os_submenu("Services", SystemMenuType::Services),
32/// MenuItem::separator(),
33/// MenuItem::action("Preferences...", Preferences),
34/// ]),
35/// Menu::new("Edit").items([MenuItem::os_action("Undo", Undo, OsAction::Undo)]),
36/// ]);
37/// }
38///
39/// # let opened = Rc::new(Cell::new(false));
40/// # let mut app = TestApp::new();
41/// # app.update(|cx| set_up_menus(cx, opened.clone()));
42/// // Picking the menu item dispatches its action. The test harness has no menu bar,
43/// // so it dispatches the action directly.
44/// # app.update(|cx| cx.dispatch_action(&Preferences));
45/// # assert!(opened.get());
46/// ```
47pub struct Menu {
48 /// The title of the menu, as shown in the menu bar.
49 pub name: SharedString,
50
51 /// The entries of the menu, in display order.
52 pub items: Vec<MenuItem>,
53
54 /// Whether the menu is disabled. On macOS, a disabled submenu can't be opened.
55 pub disabled: bool,
56}
57
58impl Menu {
59 /// Creates an empty menu with the given title.
60 ///
61 /// Add entries with [`Self::items`].
62 pub fn new(name: impl Into<SharedString>) -> Self {
63 Self {
64 name: name.into(),
65 items: vec![],
66 disabled: false,
67 }
68 }
69
70 /// Replaces the menu's entries with `items`, and returns the menu.
71 ///
72 /// The previous entries are discarded.
73 pub fn items(mut self, items: impl IntoIterator<Item = MenuItem>) -> Self {
74 self.items = items.into_iter().collect();
75 self
76 }
77
78 /// Sets whether the menu is disabled, and returns the menu.
79 pub fn disabled(mut self, disabled: bool) -> Self {
80 self.disabled = disabled;
81 self
82 }
83
84 /// Converts this menu into an [`OwnedMenu`], the form that [`App::get_menus`] returns.
85 ///
86 /// Platforms call this when they store the menu bar set with [`App::set_menus`], so you
87 /// rarely need it.
88 pub fn owned(self) -> OwnedMenu {
89 OwnedMenu {
90 name: self.name.to_string().into(),
91 items: self.items.into_iter().map(|item| item.owned()).collect(),
92 disabled: self.disabled,
93 }
94 }
95}
96
97/// A submenu that the operating system fills in, such as the Services menu on macOS.
98///
99/// Create one with [`MenuItem::os_submenu`]. The operating system decides what the menu
100/// contains.
101pub struct OsMenu {
102 /// The title of the menu.
103 pub name: SharedString,
104
105 /// Which system menu to show.
106 pub menu_type: SystemMenuType,
107}
108
109impl OsMenu {
110 /// Converts this menu into an [`OwnedOsMenu`].
111 pub fn owned(self) -> OwnedOsMenu {
112 OwnedOsMenu {
113 name: self.name.to_string().into(),
114 menu_type: self.menu_type,
115 }
116 }
117}
118
119/// The kinds of menu that the operating system fills in.
120#[derive(Copy, Clone, Eq, PartialEq)]
121pub enum SystemMenuType {
122 /// The Services menu in the application menu on macOS, which lists the services that
123 /// other apps provide.
124 Services,
125}
126
127/// An entry in a [`Menu`]: a separator, a submenu, a system menu, or an action.
128///
129/// Build entries with the constructors on this type, such as [`MenuItem::action`] and
130/// [`MenuItem::separator`].
131pub enum MenuItem {
132 /// A horizontal line that separates groups of entries.
133 Separator,
134
135 /// A menu nested inside another menu.
136 Submenu(Menu),
137
138 /// A submenu that the operating system fills in, such as the Services menu on macOS.
139 SystemMenu(OsMenu),
140
141 /// An entry that dispatches an action when the user picks it.
142 Action {
143 /// The label of the entry.
144 name: SharedString,
145
146 /// The action dispatched when the entry is picked.
147 action: Box<dyn Action>,
148
149 /// The standard command this entry stands for, if any. See [`OsAction`].
150 os_action: Option<OsAction>,
151
152 /// Whether the entry shows a checkmark. On macOS, the checkmark is shown when this is
153 /// `true`.
154 checked: bool,
155
156 /// Whether the entry is disabled. On macOS, a disabled entry is greyed out and can't be
157 /// picked.
158 disabled: bool,
159 },
160}
161
162impl MenuItem {
163 /// Returns a separator entry.
164 pub fn separator() -> Self {
165 Self::Separator
166 }
167
168 /// Returns an entry that opens `menu` as a submenu.
169 pub fn submenu(menu: Menu) -> Self {
170 Self::Submenu(menu)
171 }
172
173 /// Returns a submenu titled `name` that the operating system fills in.
174 ///
175 /// Use it for menus like the macOS Services menu, which the system builds itself.
176 pub fn os_submenu(name: impl Into<SharedString>, menu_type: SystemMenuType) -> Self {
177 Self::SystemMenu(OsMenu {
178 name: name.into(),
179 menu_type,
180 })
181 }
182
183 /// Returns an entry titled `name` that dispatches `action` when the user picks it.
184 ///
185 /// Use [`Self::os_action`] instead when the entry is a standard command, such as Copy
186 /// or Undo.
187 pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
188 Self::Action {
189 name: name.into(),
190 action: Box::new(action),
191 os_action: None,
192 checked: false,
193 disabled: false,
194 }
195 }
196
197 /// Returns an entry like [`Self::action`], tagged with the standard command it stands for.
198 ///
199 /// On macOS, Cut, Copy, Paste and Select All are sent to the responder chain, so the
200 /// focused text view handles them. Undo, Redo and entries without an `OsAction` dispatch
201 /// `action` as usual.
202 pub fn os_action(
203 name: impl Into<SharedString>,
204 action: impl Action,
205 os_action: OsAction,
206 ) -> Self {
207 Self::Action {
208 name: name.into(),
209 action: Box::new(action),
210 os_action: Some(os_action),
211 checked: false,
212 disabled: false,
213 }
214 }
215
216 /// Converts this entry into an [`OwnedMenuItem`].
217 pub fn owned(self) -> OwnedMenuItem {
218 match self {
219 MenuItem::Separator => OwnedMenuItem::Separator,
220 MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()),
221 MenuItem::Action {
222 name,
223 action,
224 os_action,
225 checked,
226 disabled,
227 } => OwnedMenuItem::Action {
228 name: name.into(),
229 action,
230 os_action,
231 checked,
232 disabled,
233 },
234 MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
235 }
236 }
237
238 /// Sets whether the entry shows a checkmark, and returns the entry.
239 ///
240 /// Has an effect only on [`MenuItem::Action`]. Other kinds of entry ignore it.
241 pub fn checked(mut self, checked: bool) -> Self {
242 match &mut self {
243 MenuItem::Action { checked: old, .. } => {
244 *old = checked;
245 }
246 _ => {}
247 }
248 self
249 }
250
251 /// Returns whether the entry shows a checkmark.
252 ///
253 /// Returns `false` for every kind of entry except [`MenuItem::Action`].
254 #[inline]
255 pub fn is_checked(&self) -> bool {
256 match self {
257 MenuItem::Action { checked, .. } => *checked,
258 _ => false,
259 }
260 }
261
262 /// Sets whether the entry is disabled, and returns the entry.
263 ///
264 /// Has an effect on [`MenuItem::Action`] and [`MenuItem::Submenu`]. Other kinds of entry
265 /// ignore it.
266 pub fn disabled(mut self, disabled: bool) -> Self {
267 match &mut self {
268 MenuItem::Action { disabled: old, .. } => {
269 *old = disabled;
270 }
271 MenuItem::Submenu(submenu) => {
272 submenu.disabled = disabled;
273 }
274 _ => {}
275 }
276 self
277 }
278
279 /// Returns whether the entry is disabled.
280 ///
281 /// Returns `false` for separators and system menus.
282 #[inline]
283 pub fn is_disabled(&self) -> bool {
284 match self {
285 MenuItem::Action { disabled, .. } => *disabled,
286 MenuItem::Submenu(submenu) => submenu.disabled,
287 _ => false,
288 }
289 }
290}
291
292/// An [`OsMenu`] in the form that [`App::get_menus`] returns.
293#[derive(Clone)]
294pub struct OwnedOsMenu {
295 /// The title of the menu.
296 pub name: SharedString,
297
298 /// Which system menu to show.
299 pub menu_type: SystemMenuType,
300}
301
302/// A [`Menu`] in the form that [`App::get_menus`] returns.
303///
304/// Its entries are [`OwnedMenuItem`]s. The data is the same as the [`Menu`] that was set.
305#[derive(Clone)]
306pub struct OwnedMenu {
307 /// The title of the menu, as shown in the menu bar.
308 pub name: SharedString,
309
310 /// The entries of the menu, in display order.
311 pub items: Vec<OwnedMenuItem>,
312
313 /// Whether the menu is disabled.
314 pub disabled: bool,
315}
316
317/// A [`MenuItem`] in the form that [`App::get_menus`] returns.
318pub enum OwnedMenuItem {
319 /// A horizontal line that separates groups of entries.
320 Separator,
321
322 /// A menu nested inside another menu.
323 Submenu(OwnedMenu),
324
325 /// A submenu that the operating system fills in.
326 SystemMenu(OwnedOsMenu),
327
328 /// An entry that dispatches an action when the user picks it.
329 Action {
330 /// The label of the entry.
331 name: String,
332
333 /// The action dispatched when the entry is picked.
334 action: Box<dyn Action>,
335
336 /// The standard command this entry stands for, if any. See [`OsAction`].
337 os_action: Option<OsAction>,
338
339 /// Whether the entry shows a checkmark.
340 checked: bool,
341
342 /// Whether the entry is disabled.
343 disabled: bool,
344 },
345}
346
347impl Clone for OwnedMenuItem {
348 fn clone(&self) -> Self {
349 match self {
350 OwnedMenuItem::Separator => OwnedMenuItem::Separator,
351 OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()),
352 OwnedMenuItem::Action {
353 name,
354 action,
355 os_action,
356 checked,
357 disabled,
358 } => OwnedMenuItem::Action {
359 name: name.clone(),
360 action: action.boxed_clone(),
361 os_action: *os_action,
362 checked: *checked,
363 disabled: *disabled,
364 },
365 OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
366 }
367 }
368}
369
370// TODO: As part of the global selections refactor, these should
371// be moved to GPUI-provided actions that make this association
372// without leaking the platform details to GPUI users
373
374/// A standard command that the operating system knows about, such as Copy or Undo.
375///
376/// Tag a menu entry with one of these using [`MenuItem::os_action`], so the operating system can
377/// route the command the way it does for other apps. On macOS, the entry's action is still
378/// dispatched for Undo and Redo, and the system handles Cut, Copy, Paste and Select All.
379#[derive(Copy, Clone, Eq, PartialEq)]
380pub enum OsAction {
381 /// The Cut command.
382 Cut,
383
384 /// The Copy command.
385 Copy,
386
387 /// The Paste command.
388 Paste,
389
390 /// The Select All command.
391 SelectAll,
392
393 /// The Undo command.
394 Undo,
395
396 /// The Redo command.
397 Redo,
398}
399
400pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
401 platform.on_will_open_app_menu(Box::new({
402 let cx = cx.to_async();
403 move || {
404 if let Some(app) = cx.app.upgrade() {
405 app.borrow_mut().update(|cx| cx.clear_pending_keystrokes());
406 }
407 }
408 }));
409
410 platform.on_validate_app_menu_command(Box::new({
411 let cx = cx.to_async();
412 move |action| {
413 cx.app
414 .upgrade()
415 .map(|app| app.borrow_mut().update(|cx| cx.is_action_available(action)))
416 .unwrap_or(false)
417 }
418 }));
419
420 platform.on_app_menu_action(Box::new({
421 let cx = cx.to_async();
422 move |action| {
423 if let Some(app) = cx.app.upgrade() {
424 app.borrow_mut().update(|cx| cx.dispatch_action(action));
425 }
426 }
427 }));
428}
429
430#[cfg(test)]
431mod tests {
432 use crate::Menu;
433
434 #[test]
435 fn test_menu() {
436 let menu = Menu::new("App")
437 .items(vec![
438 crate::MenuItem::action("Action 1", gpui::NoAction),
439 crate::MenuItem::separator(),
440 ])
441 .disabled(true);
442
443 assert_eq!(menu.name.as_ref(), "App");
444 assert_eq!(menu.items.len(), 2);
445 assert!(menu.disabled);
446 }
447
448 #[test]
449 fn test_menu_item_builder() {
450 use super::MenuItem;
451
452 let item = MenuItem::action("Test Action", gpui::NoAction);
453 assert_eq!(
454 match &item {
455 MenuItem::Action { name, .. } => name.as_ref(),
456 _ => unreachable!(),
457 },
458 "Test Action"
459 );
460 assert!(matches!(
461 item,
462 MenuItem::Action {
463 checked: false,
464 disabled: false,
465 ..
466 }
467 ));
468
469 assert!(
470 MenuItem::action("Test Action", gpui::NoAction)
471 .checked(true)
472 .is_checked()
473 );
474 assert!(
475 MenuItem::action("Test Action", gpui::NoAction)
476 .disabled(true)
477 .is_disabled()
478 );
479
480 let submenu = MenuItem::submenu(super::Menu {
481 name: "Submenu".into(),
482 items: vec![],
483 disabled: true,
484 });
485 assert_eq!(
486 match &submenu {
487 MenuItem::Submenu(menu) => menu.name.as_ref(),
488 _ => unreachable!(),
489 },
490 "Submenu"
491 );
492 assert!(!submenu.is_checked());
493 assert!(submenu.is_disabled());
494 }
495}