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

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>
This commit is contained in:
Victor Tran 2025-08-12 07:10:14 +10:00 committed by GitHub
parent 094e878ccf
commit fa3d0aaed4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 89 additions and 12 deletions

View file

@ -1,5 +1,6 @@
use gpui::{ 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; struct SetMenus;
@ -27,7 +28,11 @@ fn main() {
// Add menu items // Add menu items
cx.set_menus(vec![Menu { cx.set_menus(vec![Menu {
name: "set_menus".into(), 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 {})) cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {}))
.unwrap(); .unwrap();

View file

@ -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 /// The different kinds of items that can be in a menu
pub enum MenuItem { pub enum MenuItem {
/// A separator between items /// A separator between items
@ -28,6 +56,9 @@ pub enum MenuItem {
/// A submenu /// A submenu
Submenu(Menu), Submenu(Menu),
/// A menu, managed by the system (for example, the Services menu on macOS)
SystemMenu(OsMenu),
/// An action that can be performed /// An action that can be performed
Action { Action {
/// The name of this menu item /// The name of this menu item
@ -53,6 +84,14 @@ impl MenuItem {
Self::Submenu(menu) 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 /// Creates a new menu item that invokes an action
pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self { pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
Self::Action { Self::Action {
@ -89,10 +128,23 @@ impl MenuItem {
action, action,
os_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 /// A menu of the application, either a main menu or a submenu
#[derive(Clone)] #[derive(Clone)]
pub struct OwnedMenu { pub struct OwnedMenu {
@ -111,6 +163,9 @@ pub enum OwnedMenuItem {
/// A submenu /// A submenu
Submenu(OwnedMenu), Submenu(OwnedMenu),
/// A menu, managed by the system (for example, the Services menu on macOS)
SystemMenu(OwnedOsMenu),
/// An action that can be performed /// An action that can be performed
Action { Action {
/// The name of this menu item /// The name of this menu item
@ -139,6 +194,7 @@ impl Clone for OwnedMenuItem {
action: action.boxed_clone(), action: action.boxed_clone(),
os_action: *os_action, os_action: *os_action,
}, },
OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
} }
} }
} }

View file

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

View file

@ -121,8 +121,16 @@ impl ApplicationMenu {
menu.action(name, action) menu.action(name, action)
} }
OwnedMenuItem::Submenu(_) => menu, 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
}
}) })
}) })
} }

View file

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