windows: Dock menu impl 2 (#26010)

张小白 created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

Cargo.toml                                   |  2 
crates/gpui/src/app.rs                       |  5 +
crates/gpui/src/platform.rs                  |  1 
crates/gpui/src/platform/windows/events.rs   |  1 
crates/gpui/src/platform/windows/platform.rs | 77 +++++++++++++++++++-
crates/zed/src/main.rs                       | 52 ++++++++-----
crates/zed/src/zed/open_listener.rs          |  3 
crates/zed/src/zed/windows_only_instance.rs  | 84 ++++++++++++---------
8 files changed, 163 insertions(+), 62 deletions(-)

Detailed changes

Cargo.toml 🔗

@@ -603,9 +603,11 @@ features = [
 version = "0.58"
 features = [
     "implement",
+    "Foundation_Collections",
     "Foundation_Numerics",
     "Storage",
     "System_Threading",
+    "UI_StartScreen",
     "UI_ViewManagement",
     "Wdk_System_SystemServices",
     "Win32_Globalization",

crates/gpui/src/app.rs 🔗

@@ -1426,6 +1426,11 @@ impl App {
         self.platform.set_dock_menu(menus, &self.keymap.borrow());
     }
 
+    /// Performs the action associated with the given dock menu item, only used on Windows for now.
+    pub fn perform_dock_menu_action(&self, action: usize) {
+        self.platform.perform_dock_menu_action(action);
+    }
+
     /// 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 🔗

@@ -189,6 +189,7 @@ pub(crate) trait Platform: 'static {
     }
 
     fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
+    fn perform_dock_menu_action(&self, _action: usize) {}
     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/windows/events.rs 🔗

@@ -22,6 +22,7 @@ use crate::*;
 pub(crate) const WM_GPUI_CURSOR_STYLE_CHANGED: u32 = WM_USER + 1;
 pub(crate) const WM_GPUI_CLOSE_ONE_WINDOW: u32 = WM_USER + 2;
 pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3;
+pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4;
 
 const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
 const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1;

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

@@ -25,7 +25,10 @@ use windows::{
         System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*},
         UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
     },
-    UI::ViewManagement::UISettings,
+    UI::{
+        StartScreen::{JumpList, JumpListItem},
+        ViewManagement::UISettings,
+    },
 };
 
 use crate::{platform::blade::BladeContext, *};
@@ -49,6 +52,7 @@ pub(crate) struct WindowsPlatform {
 pub(crate) struct WindowsPlatformState {
     callbacks: PlatformCallbacks,
     menus: Vec<OwnedMenu>,
+    dock_menu_actions: Vec<Box<dyn Action>>,
     // NOTE: standard cursor handles don't need to close.
     pub(crate) current_cursor: HCURSOR,
 }
@@ -66,10 +70,12 @@ struct PlatformCallbacks {
 impl WindowsPlatformState {
     fn new() -> Self {
         let callbacks = PlatformCallbacks::default();
+        let dock_menu_actions = Vec::new();
         let current_cursor = load_cursor(CursorStyle::Arrow);
 
         Self {
             callbacks,
+            dock_menu_actions,
             current_cursor,
             menus: Vec::new(),
         }
@@ -184,6 +190,24 @@ impl WindowsPlatform {
         }
     }
 
+    fn handle_dock_action_event(&self, action_idx: usize) {
+        let mut lock = self.state.borrow_mut();
+        if let Some(mut callback) = lock.callbacks.app_menu_action.take() {
+            let Some(action) = lock
+                .dock_menu_actions
+                .get(action_idx)
+                .map(|action| action.boxed_clone())
+            else {
+                lock.callbacks.app_menu_action = Some(callback);
+                log::error!("Dock menu for index {action_idx} not found");
+                return;
+            };
+            drop(lock);
+            callback(&*action);
+            self.state.borrow_mut().callbacks.app_menu_action = Some(callback);
+        }
+    }
+
     // Returns true if the app should quit.
     fn handle_events(&self) -> bool {
         let mut msg = MSG::default();
@@ -191,7 +215,9 @@ impl WindowsPlatform {
             while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
                 match msg.message {
                     WM_QUIT => return true,
-                    WM_GPUI_CLOSE_ONE_WINDOW | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => {
+                    WM_GPUI_CLOSE_ONE_WINDOW
+                    | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD
+                    | WM_GPUI_DOCK_MENU_ACTION => {
                         if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) {
                             return true;
                         }
@@ -227,10 +253,40 @@ impl WindowsPlatform {
                 }
             }
             WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(),
+            WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _),
             _ => unreachable!(),
         }
         false
     }
+
+    fn configure_jump_list(&self, menus: Vec<MenuItem>) -> Result<()> {
+        let jump_list = JumpList::LoadCurrentAsync()?.get()?;
+        let items = jump_list.Items()?;
+        items.Clear()?;
+        let mut actions = Vec::new();
+        for item in menus.into_iter() {
+            let item = match item {
+                MenuItem::Separator => JumpListItem::CreateSeparator()?,
+                MenuItem::Submenu(_) => {
+                    log::error!("Set `MenuItemSubmenu` for dock menu on Windows is not supported.");
+                    continue;
+                }
+                MenuItem::Action { name, action, .. } => {
+                    let idx = actions.len();
+                    actions.push(action.boxed_clone());
+                    let item_args = format!("--dock-action {}", idx);
+                    JumpListItem::CreateWithArguments(
+                        &HSTRING::from(item_args),
+                        &HSTRING::from(name.as_ref()),
+                    )?
+                }
+            };
+            items.Append(&item)?;
+        }
+        jump_list.SaveAsync()?.get()?;
+        self.state.borrow_mut().dock_menu_actions = actions;
+        Ok(())
+    }
 }
 
 impl Platform for WindowsPlatform {
@@ -479,8 +535,9 @@ impl Platform for WindowsPlatform {
         Some(self.state.borrow().menus.clone())
     }
 
-    // todo(windows)
-    fn set_dock_menu(&self, _menus: Vec<MenuItem>, _keymap: &Keymap) {}
+    fn set_dock_menu(&self, menus: Vec<MenuItem>, _keymap: &Keymap) {
+        self.configure_jump_list(menus).log_err();
+    }
 
     fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
         self.state.borrow_mut().callbacks.app_menu_action = Some(callback);
@@ -599,6 +656,18 @@ impl Platform for WindowsPlatform {
     fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
         Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
     }
+
+    fn perform_dock_menu_action(&self, action: usize) {
+        unsafe {
+            PostThreadMessageW(
+                self.main_thread_id_win32,
+                WM_GPUI_DOCK_MENU_ACTION,
+                WPARAM(self.validation_number),
+                LPARAM(action as isize),
+            )
+            .log_err();
+        }
+    }
 }
 
 impl Drop for WindowsPlatform {

crates/zed/src/main.rs 🔗

@@ -217,29 +217,27 @@ fn main() {
 
     let (open_listener, mut open_rx) = OpenListener::new();
 
-    let failed_single_instance_check =
-        if *db::ZED_STATELESS || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
-            false
-        } else {
-            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
-            {
-                crate::zed::listen_for_cli_connections(open_listener.clone()).is_err()
-            }
+    let failed_single_instance_check = if *db::ZED_STATELESS
+        || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev
+    {
+        false
+    } else {
+        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+        {
+            crate::zed::listen_for_cli_connections(open_listener.clone()).is_err()
+        }
 
-            #[cfg(target_os = "windows")]
-            {
-                !crate::zed::windows_only_instance::check_single_instance(
-                    open_listener.clone(),
-                    args.foreground,
-                )
-            }
+        #[cfg(target_os = "windows")]
+        {
+            !crate::zed::windows_only_instance::check_single_instance(open_listener.clone(), &args)
+        }
 
-            #[cfg(target_os = "macos")]
-            {
-                use zed::mac_only_instance::*;
-                ensure_only_instance() != IsOnlyInstance::Yes
-            }
-        };
+        #[cfg(target_os = "macos")]
+        {
+            use zed::mac_only_instance::*;
+            ensure_only_instance() != IsOnlyInstance::Yes
+        }
+    };
     if failed_single_instance_check {
         println!("zed is already running");
         return;
@@ -643,6 +641,11 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
         return;
     }
 
+    if let Some(action_index) = request.dock_menu_action {
+        cx.perform_dock_menu_action(action_index);
+        return;
+    }
+
     if let Some(connection_options) = request.ssh_connection {
         cx.spawn(|mut cx| async move {
             let paths_with_position =
@@ -953,7 +956,14 @@ struct Args {
     /// Run zed in the foreground, only used on Windows, to match the behavior of the behavior on macOS.
     #[arg(long)]
     #[cfg(target_os = "windows")]
+    #[arg(hide = true)]
     foreground: bool,
+
+    /// The dock action to perform. This is used on Windows only.
+    #[arg(long)]
+    #[cfg(target_os = "windows")]
+    #[arg(hide = true)]
+    dock_action: Option<usize>,
 }
 
 #[derive(Clone, Debug)]

crates/zed/src/zed/open_listener.rs 🔗

@@ -34,6 +34,7 @@ pub struct OpenRequest {
     pub open_channel_notes: Vec<(u64, Option<String>)>,
     pub join_channel: Option<u64>,
     pub ssh_connection: Option<SshConnectionOptions>,
+    pub dock_menu_action: Option<usize>,
 }
 
 impl OpenRequest {
@@ -42,6 +43,8 @@ impl OpenRequest {
         for url in urls {
             if let Some(server_name) = url.strip_prefix("zed-cli://") {
                 this.cli_connection = Some(connect_to_cli(server_name)?);
+            } else if let Some(action_index) = url.strip_prefix("zed-dock-action://") {
+                this.dock_menu_action = Some(action_index.parse()?);
             } else if let Some(file) = url.strip_prefix("file://") {
                 this.parse_file_path(file)
             } else if let Some(file) = url.strip_prefix("zed://file") {

crates/zed/src/zed/windows_only_instance.rs 🔗

@@ -1,7 +1,6 @@
 use std::{sync::Arc, thread::JoinHandle};
 
 use anyhow::Context;
-use clap::Parser;
 use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
 use parking_lot::Mutex;
 use release_channel::app_identifier;
@@ -26,23 +25,23 @@ use windows::{
 
 use crate::{Args, OpenListener};
 
-pub fn check_single_instance(opener: OpenListener, run_foreground: bool) -> bool {
+pub fn check_single_instance(opener: OpenListener, args: &Args) -> bool {
     unsafe {
         CreateMutexW(
             None,
             false,
             &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())),
         )
-        .expect("Unable to create instance sync event")
+        .expect("Unable to create instance mutex.")
     };
     let first_instance = unsafe { GetLastError() } != ERROR_ALREADY_EXISTS;
 
     if first_instance {
         // We are the first instance, listen for messages sent from other instances
         std::thread::spawn(move || with_pipe(|url| opener.open_urls(vec![url])));
-    } else if !run_foreground {
+    } else if !args.foreground {
         // We are not the first instance, send args to the first instance
-        send_args_to_instance().log_err();
+        send_args_to_instance(args).log_err();
     }
 
     first_instance
@@ -95,31 +94,45 @@ fn retrieve_message_from_pipe_inner(pipe: HANDLE) -> anyhow::Result<String> {
 }
 
 // This part of code is mostly from crates/cli/src/main.rs
-fn send_args_to_instance() -> anyhow::Result<()> {
-    let Args { paths_or_urls, .. } = Args::parse();
+fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
+    if let Some(dock_menu_action_idx) = args.dock_action {
+        let url = format!("zed-dock-action://{}", dock_menu_action_idx);
+        return write_message_to_instance_pipe(url.as_bytes());
+    }
+
     let (server, server_name) =
         IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
     let url = format!("zed-cli://{server_name}");
 
-    let mut paths = vec![];
-    let mut urls = vec![];
-    for path in paths_or_urls.into_iter() {
-        match std::fs::canonicalize(&path) {
-            Ok(path) => paths.push(path.to_string_lossy().to_string()),
-            Err(error) => {
-                if path.starts_with("zed://")
-                    || path.starts_with("http://")
-                    || path.starts_with("https://")
-                    || path.starts_with("file://")
-                    || path.starts_with("ssh://")
-                {
-                    urls.push(path);
-                } else {
-                    log::error!("error parsing path argument: {}", error);
+    let request = {
+        let mut paths = vec![];
+        let mut urls = vec![];
+        for path in args.paths_or_urls.iter() {
+            match std::fs::canonicalize(&path) {
+                Ok(path) => paths.push(path.to_string_lossy().to_string()),
+                Err(error) => {
+                    if path.starts_with("zed://")
+                        || path.starts_with("http://")
+                        || path.starts_with("https://")
+                        || path.starts_with("file://")
+                        || path.starts_with("ssh://")
+                    {
+                        urls.push(path.clone());
+                    } else {
+                        log::error!("error parsing path argument: {}", error);
+                    }
                 }
             }
         }
-    }
+        CliRequest::Open {
+            paths,
+            urls,
+            wait: false,
+            open_new_workspace: None,
+            env: None,
+        }
+    };
+
     let exit_status = Arc::new(Mutex::new(None));
     let sender: JoinHandle<anyhow::Result<()>> = std::thread::spawn({
         let exit_status = exit_status.clone();
@@ -127,13 +140,7 @@ fn send_args_to_instance() -> anyhow::Result<()> {
             let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
             let (tx, rx) = (handshake.requests, handshake.responses);
 
-            tx.send(CliRequest::Open {
-                paths,
-                urls,
-                wait: false,
-                open_new_workspace: None,
-                env: None,
-            })?;
+            tx.send(request)?;
 
             while let Ok(response) = rx.recv() {
                 match response {
@@ -150,6 +157,15 @@ fn send_args_to_instance() -> anyhow::Result<()> {
         }
     });
 
+    write_message_to_instance_pipe(url.as_bytes())?;
+    sender.join().unwrap()?;
+    if let Some(exit_status) = exit_status.lock().take() {
+        std::process::exit(exit_status);
+    }
+    Ok(())
+}
+
+fn write_message_to_instance_pipe(message: &[u8]) -> anyhow::Result<()> {
     unsafe {
         let pipe = CreateFileW(
             &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())),
@@ -160,14 +176,8 @@ fn send_args_to_instance() -> anyhow::Result<()> {
             FILE_FLAGS_AND_ATTRIBUTES::default(),
             None,
         )?;
-        let message = url.as_bytes();
-        let mut bytes_written = 0;
-        WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?;
+        WriteFile(pipe, Some(message), None, None)?;
         CloseHandle(pipe)?;
     }
-    sender.join().unwrap()?;
-    if let Some(exit_status) = exit_status.lock().take() {
-        std::process::exit(exit_status);
-    }
     Ok(())
 }