Skip to main content

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}