Allow menu items to specify arguments for their commands

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/app.rs                   |  12 +-
gpui/src/platform/mac/platform.rs |  30 +++++--
gpui/src/platform/mac/window.rs   |  20 ++++
gpui/src/platform/mod.rs          |   8 +
gpui/src/platform/test.rs         |  11 ++
zed/src/lib.rs                    |   2 
zed/src/main.rs                   |  43 ++++++-----
zed/src/menus.rs                  | 125 +++++++++++++++++---------------
8 files changed, 149 insertions(+), 102 deletions(-)

Detailed changes

gpui/src/app.rs πŸ”—

@@ -69,7 +69,7 @@ pub trait UpdateView {
 
 pub struct Menu<'a> {
     pub name: &'a str,
-    pub items: &'a [MenuItem<'a>],
+    pub items: Vec<MenuItem<'a>>,
 }
 
 pub enum MenuItem<'a> {
@@ -77,6 +77,7 @@ pub enum MenuItem<'a> {
         name: &'a str,
         keystroke: Option<&'a str>,
         action: &'a str,
+        arg: Option<Box<dyn Any>>,
     },
     Separator,
 }
@@ -171,14 +172,14 @@ impl App {
 
     pub fn on_menu_command<F>(self, mut callback: F) -> Self
     where
-        F: 'static + FnMut(&str, &mut MutableAppContext),
+        F: 'static + FnMut(&str, Option<&dyn Any>, &mut MutableAppContext),
     {
         let ctx = self.0.clone();
         self.0
             .borrow()
             .platform
-            .on_menu_command(Box::new(move |command| {
-                callback(command, &mut *ctx.borrow_mut())
+            .on_menu_command(Box::new(move |command, arg| {
+                callback(command, arg, &mut *ctx.borrow_mut())
             }));
         self
     }
@@ -197,7 +198,7 @@ impl App {
         self
     }
 
-    pub fn set_menus(&self, menus: &[Menu]) {
+    pub fn set_menus(&self, menus: Vec<Menu>) {
         self.0.borrow().platform.set_menus(menus);
     }
 
@@ -742,6 +743,7 @@ impl MutableAppContext {
 
     fn open_platform_window(&mut self, window_id: usize) {
         match self.platform.open_window(
+            window_id,
             WindowOptions {
                 bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)),
                 title: "Zed".into(),

gpui/src/platform/mac/platform.rs πŸ”—

@@ -20,6 +20,7 @@ use objc::{
 };
 use ptr::null_mut;
 use std::{
+    any::Any,
     cell::RefCell,
     ffi::{c_void, CStr},
     os::raw::c_char,
@@ -76,7 +77,7 @@ pub struct MacPlatform {
     dispatcher: Arc<Dispatcher>,
     fonts: Arc<FontSystem>,
     callbacks: RefCell<Callbacks>,
-    menu_item_actions: RefCell<Vec<String>>,
+    menu_item_actions: RefCell<Vec<(String, Option<Box<dyn Any>>)>>,
 }
 
 #[derive(Default)]
@@ -84,7 +85,7 @@ struct Callbacks {
     become_active: Option<Box<dyn FnMut()>>,
     resign_active: Option<Box<dyn FnMut()>>,
     event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
-    menu_command: Option<Box<dyn FnMut(&str)>>,
+    menu_command: Option<Box<dyn FnMut(&str, Option<&dyn Any>)>>,
     open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
     finish_launching: Option<Box<dyn FnOnce() -> ()>>,
 }
@@ -99,7 +100,7 @@ impl MacPlatform {
         }
     }
 
-    unsafe fn create_menu_bar(&self, menus: &[Menu]) -> id {
+    unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
         let menu_bar = NSMenu::new(nil).autorelease();
         let mut menu_item_actions = self.menu_item_actions.borrow_mut();
         menu_item_actions.clear();
@@ -107,8 +108,9 @@ impl MacPlatform {
         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_config.name));
+            menu.setTitle_(ns_string(menu_name));
 
             for item_config in menu_config.items {
                 let item;
@@ -121,12 +123,13 @@ impl MacPlatform {
                         name,
                         keystroke,
                         action,
+                        arg,
                     } => {
                         if let Some(keystroke) = keystroke {
                             let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
                                 panic!(
                                     "Invalid keystroke for menu item {}:{} - {:?}",
-                                    menu_config.name, name, err
+                                    menu_name, name, err
                                 )
                             });
 
@@ -161,7 +164,7 @@ impl MacPlatform {
 
                         let tag = menu_item_actions.len() as NSInteger;
                         let _: () = msg_send![item, setTag: tag];
-                        menu_item_actions.push(action.to_string());
+                        menu_item_actions.push((action.to_string(), arg));
                     }
                 }
 
@@ -189,7 +192,7 @@ impl platform::Platform for MacPlatform {
         self.callbacks.borrow_mut().event = Some(callback);
     }
 
-    fn on_menu_command(&self, callback: Box<dyn FnMut(&str)>) {
+    fn on_menu_command(&self, callback: Box<dyn FnMut(&str, Option<&dyn Any>)>) {
         self.callbacks.borrow_mut().menu_command = Some(callback);
     }
 
@@ -231,10 +234,15 @@ impl platform::Platform for MacPlatform {
 
     fn open_window(
         &self,
+        id: usize,
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
     ) -> Result<Box<dyn platform::Window>> {
-        Ok(Box::new(Window::open(options, executor, self.fonts())?))
+        Ok(Box::new(Window::open(id, options, executor, self.fonts())?))
+    }
+
+    fn key_window_id(&self) -> Option<usize> {
+        Window::key_window_id()
     }
 
     fn prompt_for_paths(
@@ -292,7 +300,7 @@ impl platform::Platform for MacPlatform {
         }
     }
 
-    fn set_menus(&self, menus: &[Menu]) {
+    fn set_menus(&self, menus: Vec<Menu>) {
         unsafe {
             let app: id = msg_send![APP_CLASS, sharedApplication];
             app.setMainMenu_(self.create_menu_bar(menus));
@@ -375,8 +383,8 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
         if let Some(callback) = platform.callbacks.borrow_mut().menu_command.as_mut() {
             let tag: NSInteger = msg_send![item, tag];
             let index = tag as usize;
-            if let Some(action) = platform.menu_item_actions.borrow().get(index) {
-                callback(&action);
+            if let Some((action, arg)) = platform.menu_item_actions.borrow().get(index) {
+                callback(action, arg.as_ref().map(Box::as_ref));
             }
         }
     }

gpui/src/platform/mac/window.rs πŸ”—

@@ -7,8 +7,8 @@ use crate::{
 use anyhow::{anyhow, Result};
 use cocoa::{
     appkit::{
-        NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable,
-        NSWindow, NSWindowStyleMask,
+        NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
+        NSViewWidthSizable, NSWindow, NSWindowStyleMask,
     },
     base::{id, nil},
     foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString},
@@ -118,6 +118,7 @@ unsafe fn build_classes() {
 pub struct Window(Rc<RefCell<WindowState>>);
 
 struct WindowState {
+    id: usize,
     native_window: id,
     event_callback: Option<Box<dyn FnMut(Event)>>,
     resize_callback: Option<Box<dyn FnMut(&mut dyn platform::WindowContext)>>,
@@ -131,6 +132,7 @@ struct WindowState {
 
 impl Window {
     pub fn open(
+        id: usize,
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
         fonts: Arc<dyn platform::FontSystem>,
@@ -180,6 +182,7 @@ impl Window {
             }
 
             let window = Self(Rc::new(RefCell::new(WindowState {
+                id,
                 native_window,
                 event_callback: None,
                 resize_callback: None,
@@ -230,6 +233,19 @@ impl Window {
             Ok(window)
         }
     }
+
+    pub fn key_window_id() -> Option<usize> {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let key_window: id = msg_send![app, keyWindow];
+            if key_window.is_null() {
+                None
+            } else {
+                let id = get_window_state(&*key_window).borrow().id;
+                Some(id)
+            }
+        }
+    }
 }
 
 impl Drop for Window {

gpui/src/platform/mod.rs πŸ”—

@@ -20,10 +20,10 @@ use crate::{
 use anyhow::Result;
 use async_task::Runnable;
 pub use event::Event;
-use std::{ops::Range, path::PathBuf, rc::Rc, sync::Arc};
+use std::{any::Any, ops::Range, path::PathBuf, rc::Rc, sync::Arc};
 
 pub trait Platform {
-    fn on_menu_command(&self, callback: Box<dyn FnMut(&str)>);
+    fn on_menu_command(&self, callback: Box<dyn FnMut(&str, Option<&dyn Any>)>);
     fn on_become_active(&self, callback: Box<dyn FnMut()>);
     fn on_resign_active(&self, callback: Box<dyn FnMut()>);
     fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
@@ -36,13 +36,15 @@ pub trait Platform {
     fn activate(&self, ignoring_other_apps: bool);
     fn open_window(
         &self,
+        id: usize,
         options: WindowOptions,
         executor: Rc<executor::Foreground>,
     ) -> Result<Box<dyn Window>>;
+    fn key_window_id(&self) -> Option<usize>;
     fn prompt_for_paths(&self, options: PathPromptOptions) -> Option<Vec<PathBuf>>;
     fn quit(&self);
     fn copy(&self, text: &str);
-    fn set_menus(&self, menus: &[Menu]);
+    fn set_menus(&self, menus: Vec<Menu>);
 }
 
 pub trait Dispatcher: Send + Sync {

gpui/src/platform/test.rs πŸ”—

@@ -1,6 +1,6 @@
 use pathfinder_geometry::vector::Vector2F;
-use std::rc::Rc;
 use std::sync::Arc;
+use std::{any::Any, rc::Rc};
 
 struct Platform {
     dispatcher: Arc<dyn super::Dispatcher>,
@@ -27,7 +27,7 @@ impl Platform {
 }
 
 impl super::Platform for Platform {
-    fn on_menu_command(&self, _: Box<dyn FnMut(&str)>) {}
+    fn on_menu_command(&self, _: Box<dyn FnMut(&str, Option<&dyn Any>)>) {}
 
     fn on_become_active(&self, _: Box<dyn FnMut()>) {}
 
@@ -53,13 +53,18 @@ impl super::Platform for Platform {
 
     fn open_window(
         &self,
+        _: usize,
         options: super::WindowOptions,
         _executor: Rc<super::executor::Foreground>,
     ) -> anyhow::Result<Box<dyn super::Window>> {
         Ok(Box::new(Window::new(options.bounds.size())))
     }
 
-    fn set_menus(&self, _menus: &[crate::Menu]) {}
+    fn key_window_id(&self) -> Option<usize> {
+        None
+    }
+
+    fn set_menus(&self, _menus: Vec<crate::Menu>) {}
 
     fn quit(&self) {}
 

zed/src/lib.rs πŸ”—

@@ -10,6 +10,6 @@ mod test;
 mod time;
 mod timer;
 mod util;
-mod watch;
+pub mod watch;
 pub mod workspace;
 mod worktree;

zed/src/main.rs πŸ”—

@@ -4,7 +4,9 @@ use log::LevelFilter;
 use simplelog::SimpleLogger;
 use std::{fs, path::PathBuf};
 use zed::{
-    assets, editor, file_finder, menus, settings,
+    assets, editor, file_finder, menus,
+    settings::{self, Settings},
+    watch::Receiver,
     workspace::{self, OpenParams},
 };
 
@@ -13,27 +15,28 @@ fn main() {
 
     let app = gpui::App::new(assets::Assets).unwrap();
     let (_, settings_rx) = settings::channel(&app.font_cache()).unwrap();
-    app.set_menus(menus::MENUS);
-    app.on_menu_command({
-        let settings_rx = settings_rx.clone();
-        move |command, ctx| match command {
-            "app:open" => {
-                if let Some(paths) = ctx.platform().prompt_for_paths(PathPromptOptions {
-                    files: true,
-                    directories: true,
-                    multiple: true,
-                }) {
-                    ctx.dispatch_global_action(
-                        "workspace:open_paths",
-                        OpenParams {
-                            paths,
-                            settings: settings_rx.clone(),
-                        },
-                    );
-                }
+    app.set_menus(menus::menus(settings_rx.clone()));
+    app.on_menu_command(move |command, arg, ctx| match command {
+        "app:open" => {
+            if let Some(paths) = ctx.platform().prompt_for_paths(PathPromptOptions {
+                files: true,
+                directories: true,
+                multiple: true,
+            }) {
+                ctx.dispatch_global_action(
+                    "workspace:open_paths",
+                    OpenParams {
+                        paths,
+                        settings: arg
+                            .unwrap()
+                            .downcast_ref::<Receiver<Settings>>()
+                            .unwrap()
+                            .clone(),
+                    },
+                );
             }
-            _ => ctx.dispatch_global_action(command, ()),
         }
+        _ => ctx.dispatch_global_action(command, ()),
     })
     .run(move |ctx| {
         workspace::init(ctx);

zed/src/menus.rs πŸ”—

@@ -1,60 +1,71 @@
+use crate::{settings::Settings, watch::Receiver};
 use gpui::{Menu, MenuItem};
 
 #[cfg(target_os = "macos")]
-pub const MENUS: &'static [Menu] = &[
-    Menu {
-        name: "Zed",
-        items: &[
-            MenuItem::Action {
-                name: "About Zed…",
-                keystroke: None,
-                action: "app:about-zed",
-            },
-            MenuItem::Separator,
-            MenuItem::Action {
-                name: "Quit",
-                keystroke: Some("cmd-q"),
-                action: "app:quit",
-            },
-        ],
-    },
-    Menu {
-        name: "File",
-        items: &[MenuItem::Action {
-            name: "Open…",
-            keystroke: Some("cmd-o"),
-            action: "app:open",
-        }],
-    },
-    Menu {
-        name: "Edit",
-        items: &[
-            MenuItem::Action {
-                name: "Undo",
-                keystroke: Some("cmd-z"),
-                action: "editor:undo",
-            },
-            MenuItem::Action {
-                name: "Redo",
-                keystroke: Some("cmd-Z"),
-                action: "editor:redo",
-            },
-            MenuItem::Separator,
-            MenuItem::Action {
-                name: "Cut",
-                keystroke: Some("cmd-x"),
-                action: "editor:cut",
-            },
-            MenuItem::Action {
-                name: "Copy",
-                keystroke: Some("cmd-c"),
-                action: "editor:copy",
-            },
-            MenuItem::Action {
-                name: "Paste",
-                keystroke: Some("cmd-v"),
-                action: "editor:paste",
-            },
-        ],
-    },
-];
+pub fn menus(settings: Receiver<Settings>) -> Vec<Menu<'static>> {
+    vec![
+        Menu {
+            name: "Zed",
+            items: vec![
+                MenuItem::Action {
+                    name: "About Zed…",
+                    keystroke: None,
+                    action: "app:about-zed",
+                    arg: None,
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Quit",
+                    keystroke: Some("cmd-q"),
+                    action: "app:quit",
+                    arg: None,
+                },
+            ],
+        },
+        Menu {
+            name: "File",
+            items: vec![MenuItem::Action {
+                name: "Open…",
+                keystroke: Some("cmd-o"),
+                action: "app:open",
+                arg: Some(Box::new(settings)),
+            }],
+        },
+        Menu {
+            name: "Edit",
+            items: vec![
+                MenuItem::Action {
+                    name: "Undo",
+                    keystroke: Some("cmd-z"),
+                    action: "editor:undo",
+                    arg: None,
+                },
+                MenuItem::Action {
+                    name: "Redo",
+                    keystroke: Some("cmd-Z"),
+                    action: "editor:redo",
+                    arg: None,
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Cut",
+                    keystroke: Some("cmd-x"),
+                    action: "editor:cut",
+                    arg: None,
+                },
+                MenuItem::Action {
+                    name: "Copy",
+                    keystroke: Some("cmd-c"),
+                    action: "editor:copy",
+                    arg: None,
+                },
+                MenuItem::Action {
+                    name: "Paste",
+                    keystroke: Some("cmd-v"),
+                    action: "editor:paste",
+                    arg: None,
+                },
+            ],
+        },
+    ]
+}