From a3b17ffd15b1c2658e8f56a0179c4529e5b46a35 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Jun 2022 17:47:39 -0700 Subject: [PATCH 1/3] Allow creating application menus with submenus --- crates/gpui/src/app.rs | 3 +- crates/gpui/src/platform/mac/platform.rs | 177 +++++++++++++++-------- 2 files changed, 115 insertions(+), 65 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 3c04ea16966f597dee8fd7b00ab2e18f94edbd21..e5b0f71ff3d595f7cfa040ea336d280f831f2dee 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -133,11 +133,12 @@ pub struct Menu<'a> { } pub enum MenuItem<'a> { + Separator, + Submenu(Menu<'a>), Action { name: &'a str, action: Box, }, - Separator, } #[derive(Clone)] diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 2be0cdc454321a7d2b0a2eb2907e27090179a0c5..5dc10c7b57185bd9dc070054c1732ffd18319102 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -127,89 +127,131 @@ impl MacForegroundPlatform { &self, menus: Vec, delegate: id, + actions: &mut Vec>, keystroke_matcher: &keymap::Matcher, ) -> id { - let menu_bar = NSMenu::new(nil).autorelease(); - menu_bar.setDelegate_(delegate); - let mut state = self.0.borrow_mut(); - - state.menu_actions.clear(); + let application_menu = NSMenu::new(nil).autorelease(); + application_menu.setDelegate_(delegate); for menu_config in menus { - let menu_bar_item = NSMenuItem::new(nil).autorelease(); let menu = NSMenu::new(nil).autorelease(); - let menu_name = menu_config.name; - - menu.setTitle_(ns_string(menu_name)); + menu.setTitle_(ns_string(menu_config.name)); menu.setDelegate_(delegate); for item_config in menu_config.items { - let item; + menu.addItem_(self.create_menu_item( + item_config, + delegate, + actions, + keystroke_matcher, + )); + } - match item_config { - MenuItem::Separator => { - item = NSMenuItem::separatorItem(nil); - } - MenuItem::Action { name, action } => { - let mut keystroke = None; - if let Some(binding) = keystroke_matcher - .bindings_for_action_type(action.as_any().type_id()) - .find(|binding| binding.action().eq(action.as_ref())) - { - if binding.keystrokes().len() == 1 { - keystroke = binding.keystrokes().first() + let menu_item = NSMenuItem::new(nil).autorelease(); + menu_item.setSubmenu_(menu); + application_menu.addItem_(menu_item); + + if menu_config.name == "Window" { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setWindowsMenu_(menu); + } + } + + application_menu + } + + unsafe fn create_menu_item( + &self, + item: MenuItem, + delegate: id, + actions: &mut Vec>, + keystroke_matcher: &keymap::Matcher, + ) -> id { + match item { + MenuItem::Separator => NSMenuItem::separatorItem(nil), + MenuItem::Action { name, action } => { + let keystrokes = keystroke_matcher + .bindings_for_action_type(action.as_any().type_id()) + .find(|binding| binding.action().eq(action.as_ref())) + .map(|binding| binding.keystrokes()); + + let item; + if let Some(keystrokes) = keystrokes { + if keystrokes.len() == 1 { + let keystroke = &keystrokes[0]; + let mut mask = NSEventModifierFlags::empty(); + for (modifier, flag) in &[ + (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), + (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), + (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), + ] { + if *modifier { + mask |= *flag; } } - if let Some(keystroke) = keystroke { - let mut mask = NSEventModifierFlags::empty(); - for (modifier, flag) in &[ - (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), - (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), - (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), - ] { - if *modifier { - mask |= *flag; - } + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector("handleGPUIMenuItem:"), + ns_string(key_to_native(&keystroke.key).as_ref()), + ) + .autorelease(); + item.setKeyEquivalentModifierMask_(mask); + } + // For multi-keystroke bindings, render the keystroke as part of the title. + else { + use std::fmt::Write; + + let mut name = format!("{name} ["); + for (i, keystroke) in keystrokes.iter().enumerate() { + if i > 0 { + name.push(' '); } - - item = NSMenuItem::alloc(nil) - .initWithTitle_action_keyEquivalent_( - ns_string(name), - selector("handleGPUIMenuItem:"), - ns_string(key_to_native(&keystroke.key).as_ref()), - ) - .autorelease(); - item.setKeyEquivalentModifierMask_(mask); - } else { - item = NSMenuItem::alloc(nil) - .initWithTitle_action_keyEquivalent_( - ns_string(name), - selector("handleGPUIMenuItem:"), - ns_string(""), - ) - .autorelease(); + write!(&mut name, "{}", keystroke).unwrap(); } - - let tag = state.menu_actions.len() as NSInteger; - let _: () = msg_send![item, setTag: tag]; - state.menu_actions.push(action); + name.push(']'); + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(&name), + selector("handleGPUIMenuItem:"), + ns_string(""), + ) + .autorelease(); } + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector("handleGPUIMenuItem:"), + ns_string(""), + ) + .autorelease(); } - menu.addItem_(item); + let tag = actions.len() as NSInteger; + let _: () = msg_send![item, setTag: tag]; + actions.push(action); + item } - - menu_bar_item.setSubmenu_(menu); - menu_bar.addItem_(menu_bar_item); - - if menu_name == "Window" { - let app: id = msg_send![APP_CLASS, sharedApplication]; - app.setWindowsMenu_(menu); + MenuItem::Submenu(Menu { name, items }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + for item in items { + submenu.addItem_(self.create_menu_item( + item, + delegate, + actions, + keystroke_matcher, + )); + } + item.setSubmenu_(submenu); + item.setTitle_(ns_string(name)); + item } } - - menu_bar } } @@ -270,7 +312,14 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { fn set_menus(&self, menus: Vec, keystroke_matcher: &keymap::Matcher) { unsafe { let app: id = msg_send![APP_CLASS, sharedApplication]; - app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), keystroke_matcher)); + let mut state = self.0.borrow_mut(); + let actions = &mut state.menu_actions; + app.setMainMenu_(self.create_menu_bar( + menus, + app.delegate(), + actions, + keystroke_matcher, + )); } } From 2c61bc2b1fd8ccb4a4164a025a4a294d4709c7be Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Jun 2022 17:48:10 -0700 Subject: [PATCH 2/3] Always use capital letters when rendering a keystroke --- crates/gpui/src/elements/keystroke_label.rs | 11 +++---- crates/gpui/src/keymap.rs | 34 ++++++++++++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/crates/gpui/src/elements/keystroke_label.rs b/crates/gpui/src/elements/keystroke_label.rs index 2cd55e5ff0238a8fc801a801d9b710edaab33db4..0112b548463b450c0b49aa5e1ad4c93f633a5d10 100644 --- a/crates/gpui/src/elements/keystroke_label.rs +++ b/crates/gpui/src/elements/keystroke_label.rs @@ -40,13 +40,10 @@ impl Element for KeystrokeLabel { let mut element = if let Some(keystrokes) = cx.keystrokes_for_action(self.action.as_ref()) { Flex::row() .with_children(keystrokes.iter().map(|keystroke| { - Label::new( - keystroke.to_string().to_uppercase(), - self.text_style.clone(), - ) - .contained() - .with_style(self.container_style) - .boxed() + Label::new(keystroke.to_string(), self.text_style.clone()) + .contained() + .with_style(self.container_style) + .boxed() })) .boxed() } else { diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 786adad704a1a72f7266c59c5a4021ea45ae3a9e..22cb3ba5dea4d357405f2e0d99b1e2efe6242d6a 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -4,7 +4,7 @@ use smallvec::SmallVec; use std::{ any::{Any, TypeId}, collections::{HashMap, HashSet}, - fmt::Debug, + fmt::{Debug, Write}, }; use tree_sitter::{Language, Node, Parser}; @@ -318,28 +318,34 @@ impl Keystroke { impl std::fmt::Display for Keystroke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if self.ctrl { - write!(f, "{}", "^")?; + f.write_char('^')?; } if self.alt { - write!(f, "{}", "⎇")?; + f.write_char('⎇')?; } if self.cmd { - write!(f, "{}", "⌘")?; + f.write_char('⌘')?; } if self.shift { - write!(f, "{}", "⇧")?; + f.write_char('⇧')?; } let key = match self.key.as_str() { - "backspace" => "⌫", - "up" => "↑", - "down" => "↓", - "left" => "←", - "right" => "→", - "tab" => "⇥", - "escape" => "⎋", - key => key, + "backspace" => '⌫', + "up" => '↑', + "down" => '↓', + "left" => '←', + "right" => '→', + "tab" => '⇥', + "escape" => '⎋', + key => { + if key.len() == 1 { + key.chars().next().unwrap().to_ascii_uppercase() + } else { + return f.write_str(key); + } + } }; - write!(f, "{}", key) + f.write_char(key) } } From 21ecbce9b8e5f4e79abdf65967a3a613e9e63bf5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Jun 2022 17:48:50 -0700 Subject: [PATCH 3/3] Add a Zed > Preferences submenu with prefs, bindings, theme --- crates/zed/src/menus.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index ed66953ad8fe1cf2a13db64841043844bbfce623..dfc355660448ce7970059b59e1ae5d15eed811db 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -15,14 +15,23 @@ pub fn menus() -> Vec> { action: Box::new(auto_update::Check), }, MenuItem::Separator, - MenuItem::Action { - name: "Open Settings", - action: Box::new(super::OpenSettings), - }, - MenuItem::Action { - name: "Open Key Bindings", - action: Box::new(super::OpenKeymap), - }, + MenuItem::Submenu(Menu { + name: "Preferences", + items: vec![ + MenuItem::Action { + name: "Open Settings", + action: Box::new(super::OpenSettings), + }, + MenuItem::Action { + name: "Open Key Bindings", + action: Box::new(super::OpenKeymap), + }, + MenuItem::Action { + name: "Select Theme", + action: Box::new(theme_selector::Toggle), + }, + ], + }), MenuItem::Action { name: "Install CLI", action: Box::new(super::InstallCommandLineInterface),