Merge pull request #1203 from zed-industries/themes-in-app-menu

Max Brunsfeld created

Themes in app menu

Change summary

crates/gpui/src/app.rs                      |   3 
crates/gpui/src/elements/keystroke_label.rs |  11 
crates/gpui/src/keymap.rs                   |  34 ++-
crates/gpui/src/platform/mac/platform.rs    | 177 ++++++++++++++--------
crates/zed/src/menus.rs                     |  25 ++-
5 files changed, 156 insertions(+), 94 deletions(-)

Detailed changes

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<dyn Action>,
     },
-    Separator,
 }
 
 #[derive(Clone)]

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 {

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

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -127,89 +127,131 @@ impl MacForegroundPlatform {
         &self,
         menus: Vec<Menu>,
         delegate: id,
+        actions: &mut Vec<Box<dyn Action>>,
         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<Box<dyn Action>>,
+        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<Menu>, 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,
+            ));
         }
     }
 

crates/zed/src/menus.rs 🔗

@@ -15,14 +15,23 @@ pub fn menus() -> Vec<Menu<'static>> {
                     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),