gpui: Add disabled state to app menu items (#44191)

Continue #39876 to add `disabled` state to `MenuItem`.

Make this change for #44047 ready to have disabled state.

1. Add `disabled` state to `MenuItem`.
2. Add some builder methods to `Menu` and `MenuItem`.
3. Improve `set_menus` method to receive a `impl IntoIterator<Item =
Menu>`.

Release Notes:

- N/A

--

<img width="294" height="204" alt="image"
src="https://github.com/user-attachments/assets/688e0db8-6c4e-4f9b-a832-8228db0e95d8"
/>

```bash
cargo run -p gpui --example set_menus
```
This commit is contained in:
Jason Lee authored and GitHub committed 2026-03-20 11:00:00 +00:00
1 parent 2728ccb8ac
commit d8fea89fce
12 files changed
+236 -83

No files matched your search

+1
View File
@@ -181,6 +181,7 @@ fn run_example() {
cx.set_menus(vec![Menu {
name: "Image".into(),
items: vec![MenuItem::action("Quit", Quit)],
disabled: false,
}]);
let window_options = WindowOptions {
+1 -4
View File
@@ -273,10 +273,7 @@ fn run_example() {
cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
cx.set_menus(vec![Menu {
name: "Image Gallery".into(),
items: vec![MenuItem::action("Quit", Quit)],
}]);
cx.set_menus([Menu::new("Image Gallery").items([MenuItem::action("Quit", Quit)])]);
let window_options = WindowOptions {
titlebar: Some(TitlebarOptions {
+24 -18
View File
@@ -2,7 +2,7 @@
use gpui::{
App, Context, Global, Menu, MenuItem, SharedString, SystemMenuType, Window, WindowOptions,
actions, div, prelude::*, rgb,
actions, div, prelude::*,
};
use gpui_platform::application;
@@ -12,12 +12,12 @@ impl Render for SetMenus {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.bg(rgb(0x2e7d32))
.bg(gpui::white())
.size_full()
.justify_center()
.items_center()
.text_xl()
.text_color(rgb(0xffffff))
.text_color(gpui::black())
.child("Set Menus Example")
}
}
@@ -28,7 +28,8 @@ fn run_example() {
// Bring the menu bar to the foreground (so you can see the menu bar)
cx.activate(true);
// Register the `quit` function so it can be referenced by the `MenuItem::action` in the menu bar
// Register the `quit` function so it can be referenced
// by the `MenuItem::action` in the menu bar
cx.on_action(quit);
cx.on_action(toggle_check);
// Add menu items
@@ -91,19 +92,24 @@ impl Global for AppState {}
fn set_app_menus(cx: &mut App) {
let app_state = cx.global::<AppState>();
cx.set_menus(vec![Menu {
name: "set_menus".into(),
items: vec![
MenuItem::os_submenu("Services", SystemMenuType::Services),
MenuItem::separator(),
MenuItem::action(ViewMode::List, ToggleCheck)
.checked(app_state.view_mode == ViewMode::List),
MenuItem::action(ViewMode::Grid, ToggleCheck)
.checked(app_state.view_mode == ViewMode::Grid),
MenuItem::separator(),
MenuItem::action("Quit", Quit),
],
}]);
cx.set_menus([Menu::new("set_menus").items([
MenuItem::os_submenu("Services", SystemMenuType::Services),
MenuItem::separator(),
MenuItem::action("Disabled Item", gpui::NoAction).disabled(true),
MenuItem::submenu(Menu::new("Disabled Submenu").disabled(true)),
MenuItem::separator(),
MenuItem::action("List Mode", ToggleCheck).checked(app_state.view_mode == ViewMode::List),
MenuItem::submenu(
Menu::new("Mode").items([
MenuItem::action(ViewMode::List, ToggleCheck)
.checked(app_state.view_mode == ViewMode::List),
MenuItem::action(ViewMode::Grid, ToggleCheck)
.checked(app_state.view_mode == ViewMode::Grid),
]),
),
MenuItem::separator(),
MenuItem::action("Quit", Quit),
])]);
}
// Associate actions using the `actions!` macro (or `Action` derive macro)
@@ -111,7 +117,7 @@ actions!(set_menus, [Quit, ToggleCheck]);
// Define the quit function that is registered with the App
fn quit(_: &Quit, cx: &mut App) {
println!("Gracefully quitting the application . . .");
println!("Gracefully quitting the application...");
cx.quit();
}
+1
View File
@@ -350,6 +350,7 @@ fn run_example() {
application().run(|cx: &mut App| {
cx.set_menus(vec![Menu {
name: "GPUI Typography".into(),
disabled: false,
items: vec![],
}]);
+2 -1
View File
@@ -2072,7 +2072,8 @@ impl App {
}
/// Sets the menu bar for this application. This will replace any existing menu bar.
pub fn set_menus(&self, menus: Vec<Menu>) {
pub fn set_menus(&self, menus: impl IntoIterator<Item = Menu>) {
let menus: Vec<Menu> = menus.into_iter().collect();
self.platform.set_menus(menus, &self.keymap.borrow());
}
+149 -12
View File
@@ -7,14 +7,39 @@ pub struct Menu {
/// The items in the menu
pub items: Vec<MenuItem>,
/// Whether this menu is disabled
pub disabled: bool,
}
impl Menu {
/// Create a new Menu with the given name
pub fn new(name: impl Into<SharedString>) -> Self {
Self {
name: name.into(),
items: vec![],
disabled: false,
}
}
/// Set items to be in this menu
pub fn items(mut self, items: impl IntoIterator<Item = MenuItem>) -> Self {
self.items = items.into_iter().collect();
self
}
/// Set whether this menu is disabled
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Create an OwnedMenu from this Menu
pub fn owned(self) -> OwnedMenu {
OwnedMenu {
name: self.name.to_string().into(),
items: self.items.into_iter().map(|item| item.owned()).collect(),
disabled: self.disabled,
}
}
}
@@ -72,6 +97,9 @@ pub enum MenuItem {
/// Whether this action is checked
checked: bool,
/// Whether this action is disabled
disabled: bool,
},
}
@@ -101,6 +129,7 @@ impl MenuItem {
action: Box::new(action),
os_action: None,
checked: false,
disabled: false,
}
}
@@ -115,6 +144,7 @@ impl MenuItem {
action: Box::new(action),
os_action: Some(os_action),
checked: false,
disabled: false,
}
}
@@ -128,11 +158,13 @@ impl MenuItem {
action,
os_action,
checked,
disabled,
} => OwnedMenuItem::Action {
name: name.into(),
action,
os_action,
checked,
disabled,
},
MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
}
@@ -142,19 +174,49 @@ impl MenuItem {
///
/// Only for [`MenuItem::Action`], otherwise, will be ignored
pub fn checked(mut self, checked: bool) -> Self {
match &mut self {
MenuItem::Action { checked: old, .. } => {
*old = checked;
}
_ => {}
}
self
}
/// Returns whether this menu item is checked
///
/// Only for [`MenuItem::Action`], otherwise, returns false
#[inline]
pub fn is_checked(&self) -> bool {
match self {
MenuItem::Action {
action,
os_action,
name,
..
} => MenuItem::Action {
name,
action,
os_action,
checked,
},
_ => self,
MenuItem::Action { checked, .. } => *checked,
_ => false,
}
}
/// Set whether this menu item is disabled
pub fn disabled(mut self, disabled: bool) -> Self {
match &mut self {
MenuItem::Action { disabled: old, .. } => {
*old = disabled;
}
MenuItem::Submenu(submenu) => {
submenu.disabled = disabled;
}
_ => {}
}
self
}
/// Returns whether this menu item is disabled
///
/// Only for [`MenuItem::Action`] and [`MenuItem::Submenu`], otherwise, returns false
#[inline]
pub fn is_disabled(&self) -> bool {
match self {
MenuItem::Action { disabled, .. } => *disabled,
MenuItem::Submenu(submenu) => submenu.disabled,
_ => false,
}
}
}
@@ -179,6 +241,9 @@ pub struct OwnedMenu {
/// The items in the menu
pub items: Vec<OwnedMenuItem>,
/// Whether this menu is disabled
pub disabled: bool,
}
/// The different kinds of items that can be in a menu
@@ -206,6 +271,9 @@ pub enum OwnedMenuItem {
/// Whether this action is checked
checked: bool,
/// Whether this action is disabled
disabled: bool,
},
}
@@ -219,11 +287,13 @@ impl Clone for OwnedMenuItem {
action,
os_action,
checked,
disabled,
} => OwnedMenuItem::Action {
name: name.clone(),
action: action.boxed_clone(),
os_action: *os_action,
checked: *checked,
disabled: *disabled,
},
OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
}
@@ -287,3 +357,70 @@ pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
}
}));
}
#[cfg(test)]
mod tests {
use crate::Menu;
#[test]
fn test_menu() {
let menu = Menu::new("App")
.items(vec![
crate::MenuItem::action("Action 1", gpui::NoAction),
crate::MenuItem::separator(),
])
.disabled(true);
assert_eq!(menu.name.as_ref(), "App");
assert_eq!(menu.items.len(), 2);
assert!(menu.disabled);
}
#[test]
fn test_menu_item_builder() {
use super::MenuItem;
let item = MenuItem::action("Test Action", gpui::NoAction);
assert_eq!(
match &item {
MenuItem::Action { name, .. } => name.as_ref(),
_ => unreachable!(),
},
"Test Action"
);
assert!(matches!(
item,
MenuItem::Action {
checked: false,
disabled: false,
..
}
));
assert!(
MenuItem::action("Test Action", gpui::NoAction)
.checked(true)
.is_checked()
);
assert!(
MenuItem::action("Test Action", gpui::NoAction)
.disabled(true)
.is_disabled()
);
let submenu = MenuItem::submenu(super::Menu {
name: "Submenu".into(),
items: vec![],
disabled: true,
});
assert_eq!(
match &submenu {
MenuItem::Submenu(menu) => menu.name.as_ref(),
_ => unreachable!(),
},
"Submenu"
);
assert!(!submenu.is_checked());
assert!(submenu.is_disabled());
}
}
+10 -3
View File
@@ -7,8 +7,8 @@ use block::ConcreteBlock;
use cocoa::{
appkit::{
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSSavePanel,
NSVisualEffectState, NSVisualEffectView, NSWindow,
NSControl as _, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel,
NSSavePanel, NSVisualEffectState, NSVisualEffectView, NSWindow,
},
base::{BOOL, NO, YES, id, nil, selector},
foundation::{
@@ -297,6 +297,7 @@ impl MacPlatform {
action,
os_action,
checked,
disabled,
} => {
// Note that this is intentionally using earlier bindings, whereas typically
// later ones take display precedence. See the discussion on
@@ -394,13 +395,18 @@ impl MacPlatform {
if *checked {
item.setState_(NSVisualEffectState::Active);
}
item.setEnabled_(!disabled);
let tag = actions.len() as NSInteger;
let _: () = msg_send![item, setTag: tag];
actions.push(action.boxed_clone());
item
}
MenuItem::Submenu(Menu { name, items }) => {
MenuItem::Submenu(Menu {
name,
items,
disabled,
}) => {
let item = NSMenuItem::new(nil).autorelease();
let submenu = NSMenu::new(nil).autorelease();
submenu.setDelegate_(delegate);
@@ -408,6 +414,7 @@ impl MacPlatform {
submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap));
}
item.setSubmenu_(submenu);
item.setEnabled_(!disabled);
item.setTitle_(ns_string(name));
item
}
+1 -9
View File
@@ -35,15 +35,7 @@ fn main() {
cx.activate(true);
cx.on_action(quit);
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
cx.set_menus(vec![Menu {
name: "Zed".into(),
items: vec![MenuItem::Action {
name: "Quit".into(),
action: Box::new(Quit),
os_action: None,
checked: false,
}],
}]);
cx.set_menus([Menu::new("Zed").items([MenuItem::action("Quit", Quit)])]);
let livekit_url = std::env::var("LIVEKIT_URL").unwrap_or("http://localhost:7880".into());
let livekit_key = std::env::var("LIVEKIT_KEY").unwrap_or("devkey".into());
+1 -4
View File
@@ -3,8 +3,5 @@ use gpui::{Menu, MenuItem};
pub fn app_menus() -> Vec<Menu> {
use crate::actions::Quit;
vec![Menu {
name: "Storybook".into(),
items: vec![MenuItem::action("Quit", Quit)],
}]
vec![Menu::new("Storybook").items([MenuItem::action("Quit", Quit)])]
}
+5 -2
View File
@@ -114,8 +114,9 @@ impl ApplicationMenu {
name,
action,
checked,
disabled,
..
} => menu.action_checked(name, action, checked),
} => menu.action_checked_with_disabled(name, action, checked, disabled),
OwnedMenuItem::Submenu(submenu) => {
submenu
.items
@@ -126,8 +127,10 @@ impl ApplicationMenu {
name,
action,
checked,
disabled,
..
} => menu.action_checked(name, action, checked),
} => menu
.action_checked_with_disabled(name, action, checked, disabled),
OwnedMenuItem::Submenu(_) => menu,
OwnedMenuItem::SystemMenu(_) => {
// A system menu doesn't make sense in this context, so ignore it
+11 -1
View File
@@ -692,10 +692,20 @@ impl ContextMenu {
}
pub fn action_checked(
self,
label: impl Into<SharedString>,
action: Box<dyn Action>,
checked: bool,
) -> Self {
self.action_checked_with_disabled(label, action, checked, false)
}
pub fn action_checked_with_disabled(
mut self,
label: impl Into<SharedString>,
action: Box<dyn Action>,
checked: bool,
disabled: bool,
) -> Self {
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: if checked {
@@ -718,7 +728,7 @@ impl ContextMenu {
icon_position: IconPosition::End,
icon_size: IconSize::Small,
icon_color: None,
disabled: false,
disabled,
documentation_aside: None,
end_slot_icon: None,
end_slot_title: None,
+30 -29
View File
@@ -31,6 +31,7 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
MenuItem::action("Toggle All Docks", workspace::ToggleAllDocks),
MenuItem::submenu(Menu {
name: "Editor Layout".into(),
disabled: false,
items: vec![
MenuItem::action("Split Up", workspace::SplitUp::default()),
MenuItem::action("Split Down", workspace::SplitDown::default()),
@@ -60,39 +61,31 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
vec![
Menu {
name: "Zed".into(),
disabled: false,
items: vec![
MenuItem::action("About Zed", zed_actions::About),
MenuItem::action("Check for Updates", auto_update::Check),
MenuItem::separator(),
MenuItem::submenu(Menu {
name: "Settings".into(),
items: vec![
MenuItem::action("Open Settings", zed_actions::OpenSettings),
MenuItem::action("Open Settings File", super::OpenSettingsFile),
MenuItem::action("Open Project Settings", zed_actions::OpenProjectSettings),
MenuItem::action(
"Open Project Settings File",
super::OpenProjectSettingsFile,
),
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
MenuItem::separator(),
MenuItem::action("Open Keymap", zed_actions::OpenKeymap),
MenuItem::action("Open Keymap File", zed_actions::OpenKeymapFile),
MenuItem::action(
"Open Default Key Bindings",
zed_actions::OpenDefaultKeymap,
),
MenuItem::separator(),
MenuItem::action(
"Select Theme...",
zed_actions::theme_selector::Toggle::default(),
),
MenuItem::action(
"Select Icon Theme...",
zed_actions::icon_theme_selector::Toggle::default(),
),
],
}),
MenuItem::submenu(Menu::new("Settings").items([
MenuItem::action("Open Settings", zed_actions::OpenSettings),
MenuItem::action("Open Settings File", super::OpenSettingsFile),
MenuItem::action("Open Project Settings", zed_actions::OpenProjectSettings),
MenuItem::action("Open Project Settings File", super::OpenProjectSettingsFile),
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
MenuItem::separator(),
MenuItem::action("Open Keymap", zed_actions::OpenKeymap),
MenuItem::action("Open Keymap File", zed_actions::OpenKeymapFile),
MenuItem::action("Open Default Key Bindings", zed_actions::OpenDefaultKeymap),
MenuItem::separator(),
MenuItem::action(
"Select Theme...",
zed_actions::theme_selector::Toggle::default(),
),
MenuItem::action(
"Select Icon Theme...",
zed_actions::icon_theme_selector::Toggle::default(),
),
])),
MenuItem::separator(),
#[cfg(target_os = "macos")]
MenuItem::os_submenu("Services", gpui::SystemMenuType::Services),
@@ -113,6 +106,7 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
},
Menu {
name: "File".into(),
disabled: false,
items: vec![
MenuItem::action("New", workspace::NewFile),
MenuItem::action("New Window", workspace::NewWindow),
@@ -160,6 +154,7 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
},
Menu {
name: "Edit".into(),
disabled: false,
items: vec![
MenuItem::os_action("Undo", editor::actions::Undo, OsAction::Undo),
MenuItem::os_action("Redo", editor::actions::Redo, OsAction::Redo),
@@ -180,6 +175,7 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
},
Menu {
name: "Selection".into(),
disabled: false,
items: vec![
MenuItem::os_action(
"Select All",
@@ -227,10 +223,12 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
},
Menu {
name: "View".into(),
disabled: false,
items: view_items,
},
Menu {
name: "Go".into(),
disabled: false,
items: vec![
MenuItem::action("Back", workspace::GoBack),
MenuItem::action("Forward", workspace::GoForward),
@@ -262,6 +260,7 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
},
Menu {
name: "Run".into(),
disabled: false,
items: vec![
MenuItem::action(
"Spawn Task",
@@ -286,6 +285,7 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
},
Menu {
name: "Window".into(),
disabled: false,
items: vec![
MenuItem::action("Minimize", super::Minimize),
MenuItem::action("Zoom", super::Zoom),
@@ -294,6 +294,7 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
},
Menu {
name: "Help".into(),
disabled: false,
items: vec![
MenuItem::action(
"View Release Notes Locally",