Allow creating application menus with submenus

Max Brunsfeld created

Change summary

crates/gpui/src/app.rs                   |   3 
crates/gpui/src/platform/mac/platform.rs | 177 ++++++++++++++++---------
2 files changed, 115 insertions(+), 65 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/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,
+            ));
         }
     }