gpui: Allow selection of "Services" menu independent of menu title (#34115)

Victor Tran and Mikayla Maki created

Release Notes:

- N/A

---

In the same vein as #29538, the "Services" menu on macOS depended on the
text being exactly "Services", not allowing for i18n of the menu name.

This PR introduces a new menu type called `OsMenu` that defines a
special menu that can be populated by the system. Currently, it takes
one enum value, `ServicesMenu` that tells the system to populate its
contents with the items it would usually populate the "Services" menu
with.

An example of this being used has been implemented in the `set_menus`
example:
`cargo run -p gpui --example set_menus`

---

Point to consider:

In `mac/platform.rs:414` the existing code for setting the "Services"
menu remains for backwards compatibility. Should this remain now that
this new method exists to set the menu, or should it be removed?

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

crates/gpui/examples/set_menus.rs        |  9 +++
crates/gpui/src/platform/app_menu.rs     | 56 ++++++++++++++++++++++++++
crates/gpui/src/platform/mac/platform.rs | 23 +++++++--
crates/title_bar/src/application_menu.rs |  8 +++
crates/zed/src/zed/app_menus.rs          |  5 -
5 files changed, 89 insertions(+), 12 deletions(-)

Detailed changes

crates/gpui/examples/set_menus.rs 🔗

@@ -1,5 +1,6 @@
 use gpui::{
-    App, Application, Context, Menu, MenuItem, Window, WindowOptions, actions, div, prelude::*, rgb,
+    App, Application, Context, Menu, MenuItem, SystemMenuType, Window, WindowOptions, actions, div,
+    prelude::*, rgb,
 };
 
 struct SetMenus;
@@ -27,7 +28,11 @@ fn main() {
         // Add menu items
         cx.set_menus(vec![Menu {
             name: "set_menus".into(),
-            items: vec![MenuItem::action("Quit", Quit)],
+            items: vec![
+                MenuItem::os_submenu("Services", SystemMenuType::Services),
+                MenuItem::separator(),
+                MenuItem::action("Quit", Quit),
+            ],
         }]);
         cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {}))
             .unwrap();

crates/gpui/src/platform/app_menu.rs 🔗

@@ -20,6 +20,34 @@ impl Menu {
     }
 }
 
+/// OS menus are menus that are recognized by the operating system
+/// This allows the operating system to provide specialized items for
+/// these menus
+pub struct OsMenu {
+    /// The name of the menu
+    pub name: SharedString,
+
+    /// The type of menu
+    pub menu_type: SystemMenuType,
+}
+
+impl OsMenu {
+    /// Create an OwnedOsMenu from this OsMenu
+    pub fn owned(self) -> OwnedOsMenu {
+        OwnedOsMenu {
+            name: self.name.to_string().into(),
+            menu_type: self.menu_type,
+        }
+    }
+}
+
+/// The type of system menu
+#[derive(Copy, Clone, Eq, PartialEq)]
+pub enum SystemMenuType {
+    /// The 'Services' menu in the Application menu on macOS
+    Services,
+}
+
 /// The different kinds of items that can be in a menu
 pub enum MenuItem {
     /// A separator between items
@@ -28,6 +56,9 @@ pub enum MenuItem {
     /// A submenu
     Submenu(Menu),
 
+    /// A menu, managed by the system (for example, the Services menu on macOS)
+    SystemMenu(OsMenu),
+
     /// An action that can be performed
     Action {
         /// The name of this menu item
@@ -53,6 +84,14 @@ impl MenuItem {
         Self::Submenu(menu)
     }
 
+    /// Creates a new submenu that is populated by the OS
+    pub fn os_submenu(name: impl Into<SharedString>, menu_type: SystemMenuType) -> Self {
+        Self::SystemMenu(OsMenu {
+            name: name.into(),
+            menu_type,
+        })
+    }
+
     /// Creates a new menu item that invokes an action
     pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
         Self::Action {
@@ -89,10 +128,23 @@ impl MenuItem {
                 action,
                 os_action,
             },
+            MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
         }
     }
 }
 
+/// OS menus are menus that are recognized by the operating system
+/// This allows the operating system to provide specialized items for
+/// these menus
+#[derive(Clone)]
+pub struct OwnedOsMenu {
+    /// The name of the menu
+    pub name: SharedString,
+
+    /// The type of menu
+    pub menu_type: SystemMenuType,
+}
+
 /// A menu of the application, either a main menu or a submenu
 #[derive(Clone)]
 pub struct OwnedMenu {
@@ -111,6 +163,9 @@ pub enum OwnedMenuItem {
     /// A submenu
     Submenu(OwnedMenu),
 
+    /// A menu, managed by the system (for example, the Services menu on macOS)
+    SystemMenu(OwnedOsMenu),
+
     /// An action that can be performed
     Action {
         /// The name of this menu item
@@ -139,6 +194,7 @@ impl Clone for OwnedMenuItem {
                 action: action.boxed_clone(),
                 os_action: *os_action,
             },
+            OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
         }
     }
 }

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -7,9 +7,9 @@ use super::{
 use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
     CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
-    MacDisplay, MacWindow, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay,
-    PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
-    WindowAppearance, WindowParams, hash,
+    MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
+    PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
+    SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
 };
 use anyhow::{Context as _, anyhow};
 use block::ConcreteBlock;
@@ -413,9 +413,20 @@ impl MacPlatform {
                     }
                     item.setSubmenu_(submenu);
                     item.setTitle_(ns_string(&name));
-                    if name == "Services" {
-                        let app: id = msg_send![APP_CLASS, sharedApplication];
-                        app.setServicesMenu_(item);
+                    item
+                }
+                MenuItem::SystemMenu(OsMenu { name, menu_type }) => {
+                    let item = NSMenuItem::new(nil).autorelease();
+                    let submenu = NSMenu::new(nil).autorelease();
+                    submenu.setDelegate_(delegate);
+                    item.setSubmenu_(submenu);
+                    item.setTitle_(ns_string(&name));
+
+                    match menu_type {
+                        SystemMenuType::Services => {
+                            let app: id = msg_send![APP_CLASS, sharedApplication];
+                            app.setServicesMenu_(item);
+                        }
                     }
 
                     item

crates/title_bar/src/application_menu.rs 🔗

@@ -121,8 +121,16 @@ impl ApplicationMenu {
                                     menu.action(name, action)
                                 }
                                 OwnedMenuItem::Submenu(_) => menu,
+                                OwnedMenuItem::SystemMenu(_) => {
+                                    // A system menu doesn't make sense in this context, so ignore it
+                                    menu
+                                }
                             })
                     }
+                    OwnedMenuItem::SystemMenu(_) => {
+                        // A system menu doesn't make sense in this context, so ignore it
+                        menu
+                    }
                 })
         })
     }

crates/zed/src/zed/app_menus.rs 🔗

@@ -35,10 +35,7 @@ pub fn app_menus() -> Vec<Menu> {
                     ],
                 }),
                 MenuItem::separator(),
-                MenuItem::submenu(Menu {
-                    name: "Services".into(),
-                    items: vec![],
-                }),
+                MenuItem::os_submenu("Services", gpui::SystemMenuType::Services),
                 MenuItem::separator(),
                 MenuItem::action("Extensions", zed_actions::Extensions::default()),
                 MenuItem::action("Install CLI", install_cli::Install),