Add "new window" option to the dock menu (#12067)

Conrad Irwin and versecafe created

Fixes: #11651
Co-Authored-By: versecafe <147033096+versecafe@users.noreply.github.com>



Release Notes:

- Added a "New Window" item to the dock menu
([#11651](https://github.com/zed-industries/zed/issues/11651)).

---------

Co-authored-by: versecafe <147033096+versecafe@users.noreply.github.com>

Change summary

crates/gpui/src/app.rs                       |  9 ++
crates/gpui/src/platform.rs                  |  1 
crates/gpui/src/platform/linux/platform.rs   |  3 
crates/gpui/src/platform/mac/platform.rs     | 53 +++++++++++++++++++++
crates/gpui/src/platform/test/platform.rs    |  1 
crates/gpui/src/platform/windows/platform.rs |  1 
crates/zed/src/zed.rs                        |  3 
7 files changed, 66 insertions(+), 5 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -29,8 +29,8 @@ use crate::{
     current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
     AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
     DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
-    Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
-    PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
+    Keystroke, LayoutId, Menu, MenuItem, PathPromptOptions, Pixels, Platform, PlatformDisplay,
+    Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
     SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, View, ViewContext,
     Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
 };
@@ -1167,6 +1167,11 @@ impl AppContext {
         self.platform.set_menus(menus, &self.keymap.borrow());
     }
 
+    /// Sets the right click menu for the app icon in the dock
+    pub fn set_dock_menu(&mut self, menus: Vec<MenuItem>) {
+        self.platform.set_dock_menu(menus, &self.keymap.borrow());
+    }
+
     /// Adds given path to the bottom of the list of recent paths for the application.
     /// The list is usually shown on the application icon's context menu in the dock,
     /// and allows to open the recent files via that context menu.

crates/gpui/src/platform.rs 🔗

@@ -135,6 +135,7 @@ pub(crate) trait Platform: 'static {
     fn on_reopen(&self, callback: Box<dyn FnMut()>);
 
     fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
+    fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
     fn add_recent_document(&self, _path: &Path) {}
     fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
     fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);

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

@@ -34,7 +34,7 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
 use crate::platform::linux::wayland::WaylandClient;
 use crate::{
     px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle,
-    DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, Modifiers,
+    DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers,
     PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
     PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task, WindowAppearance,
     WindowOptions, WindowParams,
@@ -375,6 +375,7 @@ impl<P: LinuxClient + 'static> Platform for P {
 
     // todo(linux)
     fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
+    fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
 
     fn local_timezone(&self) -> UtcOffset {
         UtcOffset::UTC

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

@@ -20,7 +20,7 @@ use cocoa::{
     },
 };
 use core_foundation::{
-    base::{CFType, CFTypeRef, OSStatus, TCFType as _},
+    base::{CFRelease, CFType, CFTypeRef, OSStatus, TCFType as _},
     boolean::CFBoolean,
     data::CFData,
     dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
@@ -120,6 +120,10 @@ unsafe fn build_classes() {
             sel!(menuWillOpen:),
             menu_will_open as extern "C" fn(&mut Object, Sel, id),
         );
+        decl.add_method(
+            sel!(applicationDockMenu:),
+            handle_dock_menu as extern "C" fn(&mut Object, Sel, id) -> id,
+        );
         decl.add_method(
             sel!(application:openURLs:),
             open_urls as extern "C" fn(&mut Object, Sel, id, id),
@@ -147,6 +151,7 @@ pub(crate) struct MacPlatformState {
     menu_actions: Vec<Box<dyn Action>>,
     open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
     finish_launching: Option<Box<dyn FnOnce()>>,
+    dock_menu: Option<id>,
 }
 
 impl Default for MacPlatform {
@@ -174,6 +179,7 @@ impl MacPlatform {
             menu_actions: Default::default(),
             open_urls: None,
             finish_launching: None,
+            dock_menu: None,
         }))
     }
 
@@ -226,6 +232,27 @@ impl MacPlatform {
         application_menu
     }
 
+    unsafe fn create_dock_menu(
+        &self,
+        menu_items: Vec<MenuItem>,
+        delegate: id,
+        actions: &mut Vec<Box<dyn Action>>,
+        keymap: &Keymap,
+    ) -> id {
+        let dock_menu = NSMenu::new(nil);
+        dock_menu.setDelegate_(delegate);
+        for item_config in menu_items {
+            dock_menu.addItem_(Self::create_menu_item(
+                item_config,
+                delegate,
+                actions,
+                keymap,
+            ));
+        }
+
+        dock_menu
+    }
+
     unsafe fn create_menu_item(
         item: MenuItem,
         delegate: id,
@@ -731,6 +758,18 @@ impl Platform for MacPlatform {
         }
     }
 
+    fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {
+        unsafe {
+            let app: id = msg_send![APP_CLASS, sharedApplication];
+            let mut state = self.0.lock();
+            let actions = &mut state.menu_actions;
+            let new = self.create_dock_menu(menu, app.delegate(), actions, keymap);
+            if let Some(old) = state.dock_menu.replace(new) {
+                CFRelease(old as _)
+            }
+        }
+    }
+
     fn add_recent_document(&self, path: &Path) {
         if let Some(path_str) = path.to_str() {
             unsafe {
@@ -1128,6 +1167,18 @@ extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
     }
 }
 
+extern "C" fn handle_dock_menu(this: &mut Object, _: Sel, _: id) -> id {
+    unsafe {
+        let platform = get_mac_platform(this);
+        let mut state = platform.0.lock();
+        if let Some(id) = state.dock_menu {
+            id
+        } else {
+            nil
+        }
+    }
+}
+
 unsafe fn ns_string(string: &str) -> id {
     NSString::alloc(nil).init_str(string).autorelease()
 }

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

@@ -233,6 +233,7 @@ impl Platform for TestPlatform {
     }
 
     fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
+    fn set_dock_menu(&self, _menu: Vec<crate::MenuItem>, _keymap: &Keymap) {}
 
     fn add_recent_document(&self, _paths: &Path) {}
 

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

@@ -450,6 +450,7 @@ impl Platform for WindowsPlatform {
 
     // todo(windows)
     fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
+    fn set_dock_menu(&self, menus: Vec<MenuItem>, keymap: &Keymap) {}
 
     fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
         self.state.borrow_mut().callbacks.app_menu_action = Some(callback);

crates/zed/src/zed.rs 🔗

@@ -10,7 +10,7 @@ use client::ZED_URL_SCHEME;
 use collections::VecDeque;
 use editor::{scroll::Autoscroll, Editor, MultiBuffer};
 use gpui::{
-    actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, PromptLevel,
+    actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem, PromptLevel,
     TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions,
 };
 pub use open_listener::*;
@@ -670,6 +670,7 @@ fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
     load_default_keymap(cx);
     keymap_content.clone().add_to_cx(cx).log_err();
     cx.set_menus(app_menus());
+    cx.set_dock_menu(vec![MenuItem::action("New Window", workspace::NewWindow)])
 }
 
 pub fn load_default_keymap(cx: &mut AppContext) {