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::{
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();

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
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()),
}
}
}

View file

@ -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