Create an API for assigning the menubar contents

Max Brunsfeld created

Change summary

gpui/src/app.rs                 |  18 +++
gpui/src/platform/mac/app.rs    |   8 +
gpui/src/platform/mac/runner.rs | 159 +++++++++++++++++++++++-----------
gpui/src/platform/mod.rs        |   7 +
gpui/src/platform/test.rs       |   2 
zed/src/lib.rs                  |   1 
zed/src/main.rs                 |  24 +++-
zed/src/menus.rs                |  52 +++++++++++
zed/src/workspace/mod.rs        |   5 +
9 files changed, 216 insertions(+), 60 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -66,6 +66,20 @@ pub trait UpdateView {
         F: FnOnce(&mut T, &mut ViewContext<T>) -> S;
 }
 
+pub struct Menu<'a> {
+    pub name: &'a str,
+    pub items: &'a [MenuItem<'a>],
+}
+
+pub enum MenuItem<'a> {
+    Action {
+        name: &'a str,
+        keystroke: Option<&'a str>,
+        action: &'a str,
+    },
+    Separator,
+}
+
 #[derive(Clone)]
 pub struct App(Rc<RefCell<MutableAppContext>>);
 
@@ -365,6 +379,10 @@ impl MutableAppContext {
         &self.ctx
     }
 
+    pub fn platform(&self) -> Arc<dyn platform::App> {
+        self.platform.clone()
+    }
+
     pub fn foreground_executor(&self) -> &Rc<executor::Foreground> {
         &self.foreground
     }

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

@@ -2,6 +2,7 @@ use super::{BoolExt as _, Dispatcher, FontSystem, Window};
 use crate::{executor, platform};
 use anyhow::Result;
 use cocoa::{appkit::NSApplication, base::nil};
+use objc::{msg_send, sel, sel_impl};
 use std::{rc::Rc, sync::Arc};
 
 pub struct App {
@@ -41,4 +42,11 @@ impl platform::App for App {
     fn fonts(&self) -> Arc<dyn platform::FontSystem> {
         self.fonts.clone()
     }
+
+    fn quit(&self) {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let _: () = msg_send![app, terminate: nil];
+        }
+    }
 }

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

@@ -1,11 +1,11 @@
-use crate::platform::Event;
+use crate::{keymap::Keystroke, platform::Event, Menu, MenuItem};
 use cocoa::{
     appkit::{
-        NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, NSMenu,
-        NSMenuItem, NSWindow,
+        NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
+        NSEventModifierFlags, NSMenu, NSMenuItem, NSWindow,
     },
     base::{id, nil, selector},
-    foundation::{NSArray, NSAutoreleasePool, NSString},
+    foundation::{NSArray, NSAutoreleasePool, NSInteger, NSString},
 };
 use ctor::ctor;
 use objc::{
@@ -53,6 +53,10 @@ unsafe fn build_classes() {
             sel!(applicationDidResignActive:),
             did_resign_active as extern "C" fn(&mut Object, Sel, id),
         );
+        decl.add_method(
+            sel!(handleGPUIMenuItem:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
         decl.add_method(
             sel!(application:openFiles:),
             open_files as extern "C" fn(&mut Object, Sel, id, id),
@@ -68,12 +72,89 @@ pub struct Runner {
     resign_active_callback: Option<Box<dyn FnMut()>>,
     event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
     open_files_callback: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
+    menu_command_callback: Option<Box<dyn FnMut(&str)>>,
+    menu_item_actions: Vec<String>,
 }
 
 impl Runner {
     pub fn new() -> Self {
         Default::default()
     }
+
+    unsafe fn create_menu_bar(&mut self, menus: &[Menu]) -> id {
+        let menu_bar = NSMenu::new(nil).autorelease();
+        self.menu_item_actions.clear();
+
+        for menu_config in menus {
+            let menu_bar_item = NSMenuItem::new(nil).autorelease();
+            let menu = NSMenu::new(nil).autorelease();
+
+            menu.setTitle_(ns_string(menu_config.name));
+
+            for item_config in menu_config.items {
+                let item;
+
+                match item_config {
+                    MenuItem::Separator => {
+                        item = NSMenuItem::separatorItem(nil);
+                    }
+                    MenuItem::Action {
+                        name,
+                        keystroke,
+                        action,
+                    } => {
+                        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
+                                )
+                            });
+
+                            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(&keystroke.key),
+                                )
+                                .autorelease();
+                            item.setKeyEquivalentModifierMask_(mask);
+                        } else {
+                            item = NSMenuItem::alloc(nil)
+                                .initWithTitle_action_keyEquivalent_(
+                                    ns_string(name),
+                                    selector("handleGPUIMenuItem:"),
+                                    ns_string(""),
+                                )
+                                .autorelease();
+                        }
+
+                        let tag = self.menu_item_actions.len() as NSInteger;
+                        let _: () = msg_send![item, setTag: tag];
+                        self.menu_item_actions.push(action.to_string());
+                    }
+                }
+
+                menu.addItem_(item);
+            }
+
+            menu_bar_item.setSubmenu_(menu);
+            menu_bar.addItem_(menu_bar_item);
+        }
+
+        menu_bar
+    }
 }
 
 impl crate::platform::Runner for Runner {
@@ -82,6 +163,11 @@ impl crate::platform::Runner for Runner {
         self
     }
 
+    fn on_menu_command<F: 'static + FnMut(&str)>(mut self, callback: F) -> Self {
+        self.menu_command_callback = Some(Box::new(callback));
+        self
+    }
+
     fn on_become_active<F: 'static + FnMut()>(mut self, callback: F) -> Self {
         log::info!("become active");
         self.become_active_callback = Some(Box::new(callback));
@@ -103,6 +189,14 @@ impl crate::platform::Runner for Runner {
         self
     }
 
+    fn set_menus(mut self, menus: &[Menu]) -> Self {
+        unsafe {
+            let app: id = msg_send![APP_CLASS, sharedApplication];
+            app.setMainMenu_(self.create_menu_bar(menus));
+        }
+        self
+    }
+
     fn run(self) {
         unsafe {
             let self_ptr = Box::into_raw(Box::new(self));
@@ -114,7 +208,6 @@ impl crate::platform::Runner for Runner {
             app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
             (*app).set_ivar(RUNNER_IVAR, self_ptr as *mut c_void);
             (*app_delegate).set_ivar(RUNNER_IVAR, self_ptr as *mut c_void);
-            app.setMainMenu_(create_menu_bar());
             app.setDelegate_(app_delegate);
             app.run();
             pool.drain();
@@ -190,51 +283,17 @@ extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
     }
 }
 
-unsafe fn create_menu_bar() -> id {
-    let menu_bar = NSMenu::new(nil).autorelease();
-
-    // App menu
-    let app_menu_item = NSMenuItem::alloc(nil)
-        .initWithTitle_action_keyEquivalent_(
-            ns_string("Application"),
-            Sel::from_ptr(ptr::null()),
-            ns_string(""),
-        )
-        .autorelease();
-    let quit_item = NSMenuItem::alloc(nil)
-        .initWithTitle_action_keyEquivalent_(
-            ns_string("Quit"),
-            selector("terminate:"),
-            ns_string("q\0"),
-        )
-        .autorelease();
-    let app_menu = NSMenu::new(nil).autorelease();
-    app_menu.addItem_(quit_item);
-    app_menu_item.setSubmenu_(app_menu);
-    menu_bar.addItem_(app_menu_item);
-
-    // File menu
-    let file_menu_item = NSMenuItem::alloc(nil)
-        .initWithTitle_action_keyEquivalent_(
-            ns_string("File"),
-            Sel::from_ptr(ptr::null()),
-            ns_string(""),
-        )
-        .autorelease();
-    let open_item = NSMenuItem::alloc(nil)
-        .initWithTitle_action_keyEquivalent_(
-            ns_string("Open"),
-            selector("openDocument:"),
-            ns_string("o\0"),
-        )
-        .autorelease();
-    let file_menu = NSMenu::new(nil).autorelease();
-    file_menu.setTitle_(ns_string("File"));
-    file_menu.addItem_(open_item);
-    file_menu_item.setSubmenu_(file_menu);
-    menu_bar.addItem_(file_menu_item);
-
-    menu_bar
+extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
+    unsafe {
+        let runner = get_runner(this);
+        if let Some(callback) = runner.menu_command_callback.as_mut() {
+            let tag: NSInteger = msg_send![item, tag];
+            let index = tag as usize;
+            if let Some(action) = runner.menu_item_actions.get(index) {
+                callback(&action);
+            }
+        }
+    }
 }
 
 unsafe fn ns_string(string: &str) -> id {

gpui/src/platform/mod.rs 🔗

@@ -15,7 +15,7 @@ use crate::{
         vector::Vector2F,
     },
     text_layout::Line,
-    Scene,
+    Menu, Scene,
 };
 use anyhow::Result;
 use async_task::Runnable;
@@ -23,11 +23,13 @@ pub use event::Event;
 use std::{ops::Range, path::PathBuf, rc::Rc, sync::Arc};
 
 pub trait Runner {
-    fn on_finish_launching<F: 'static + FnOnce()>(self, callback: F) -> Self where;
+    fn on_finish_launching<F: 'static + FnOnce()>(self, callback: F) -> Self;
+    fn on_menu_command<F: 'static + FnMut(&str)>(self, callback: F) -> Self;
     fn on_become_active<F: 'static + FnMut()>(self, callback: F) -> Self;
     fn on_resign_active<F: 'static + FnMut()>(self, callback: F) -> Self;
     fn on_event<F: 'static + FnMut(Event) -> bool>(self, callback: F) -> Self;
     fn on_open_files<F: 'static + FnMut(Vec<PathBuf>)>(self, callback: F) -> Self;
+    fn set_menus(self, menus: &[Menu]) -> Self;
     fn run(self);
 }
 
@@ -40,6 +42,7 @@ pub trait App {
         executor: Rc<executor::Foreground>,
     ) -> Result<Box<dyn Window>>;
     fn fonts(&self) -> Arc<dyn FontSystem>;
+    fn quit(&self);
 }
 
 pub trait Dispatcher: Send + Sync {

gpui/src/platform/test.rs 🔗

@@ -46,6 +46,8 @@ impl super::App for App {
     fn fonts(&self) -> std::sync::Arc<dyn super::FontSystem> {
         self.fonts.clone()
     }
+
+    fn quit(&self) {}
 }
 
 impl Window {

zed/src/lib.rs 🔗

@@ -1,6 +1,7 @@
 pub mod assets;
 pub mod editor;
 pub mod file_finder;
+pub mod menus;
 mod operation_queue;
 pub mod settings;
 mod sum_tree;

zed/src/main.rs 🔗

@@ -4,7 +4,7 @@ use log::LevelFilter;
 use simplelog::SimpleLogger;
 use std::{fs, path::PathBuf};
 use zed::{
-    assets, editor, file_finder, settings,
+    assets, editor, file_finder, menus, settings,
     workspace::{self, OpenParams},
 };
 
@@ -14,10 +14,18 @@ fn main() {
     let app = gpui::App::new(assets::Assets).unwrap();
     let (_, settings_rx) = settings::channel(&app.font_cache()).unwrap();
 
-    {
-        let mut app = app.clone();
-        platform::runner()
-            .on_finish_launching(move || {
+    platform::runner()
+        .set_menus(menus::MENUS)
+        .on_menu_command({
+            let app = app.clone();
+            move |command| {
+                log::info!("menu command: {}", command);
+                app.dispatch_global_action(command, ())
+            }
+        })
+        .on_finish_launching({
+            let mut app = app.clone();
+            move || {
                 workspace::init(&mut app);
                 editor::init(&mut app);
                 file_finder::init(&mut app);
@@ -36,9 +44,9 @@ fn main() {
                         },
                     );
                 }
-            })
-            .run();
-    }
+            }
+        })
+        .run();
 }
 
 fn init_logger() {

zed/src/menus.rs 🔗

@@ -0,0 +1,52 @@
+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: "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",
+            },
+        ],
+    },
+];

zed/src/workspace/mod.rs 🔗

@@ -14,6 +14,7 @@ use std::path::PathBuf;
 
 pub fn init(app: &mut App) {
     app.add_global_action("workspace:open_paths", open_paths);
+    app.add_global_action("app:quit", quit);
     pane::init(app);
     workspace_view::init(app);
 }
@@ -50,6 +51,10 @@ fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
     app.add_window(|ctx| WorkspaceView::new(workspace, params.settings.clone(), ctx));
 }
 
+fn quit(_: &(), app: &mut MutableAppContext) {
+    app.platform().quit();
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;