Detailed changes
@@ -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 {
@@ -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 {
@@ -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();
}
@@ -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![],
}]);
@@ -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());
}
@@ -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());
+ }
+}
@@ -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
}
@@ -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());
@@ -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)])]
}
@@ -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
@@ -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,
@@ -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",