Impl `prompts` and `savefile dialog` on Windows (#9009)

张小白 created

### Description
This is a part of #8809 , and this PR dose not include `open file
dialog`, as I already saw two PRs impl this.



https://github.com/zed-industries/zed/assets/14981363/3223490a-de77-4892-986f-97cf85aec3ae




Release Notes:

- N/A

Change summary

Cargo.toml                                   |  2 
crates/gpui/Cargo.toml                       |  8 +
crates/gpui/src/platform/windows/platform.rs | 66 ++++++++++++++++--
crates/gpui/src/platform/windows/window.rs   | 76 +++++++++++++++++++++
crates/zed/resources/windows/manifest.xml    |  8 ++
5 files changed, 147 insertions(+), 13 deletions(-)

Detailed changes

Cargo.toml 🔗

@@ -329,9 +329,11 @@ features = [
     "Wdk_System_SystemServices",
     "Win32_Graphics_Gdi",
     "Win32_Graphics_DirectComposition",
+    "Win32_UI_Controls",
     "Win32_UI_WindowsAndMessaging",
     "Win32_UI_Input_KeyboardAndMouse",
     "Win32_UI_Shell",
+    "Win32_System_Com",
     "Win32_System_SystemInformation",
     "Win32_System_SystemServices",
     "Win32_System_Time",

crates/gpui/Cargo.toml 🔗

@@ -109,9 +109,13 @@ copypasta = "0.10.1"
 open = "5.0.1"
 ashpd = "0.7.0"
 xcb = { version = "1.3", features = ["as-raw-xcb-connection", "randr", "xkb"] }
-wayland-client= { version = "0.31.2" }
+wayland-client = { version = "0.31.2" }
 wayland-cursor = "0.31.1"
-wayland-protocols = { version = "0.31.2", features = ["client", "staging", "unstable"] }
+wayland-protocols = { version = "0.31.2", features = [
+    "client",
+    "staging",
+    "unstable",
+] }
 wayland-backend = { version = "0.3.3", features = ["client_system"] }
 xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
 as-raw-xcb-connection = "1"

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

@@ -5,6 +5,7 @@ use std::{
     cell::RefCell,
     collections::HashSet,
     ffi::{c_uint, c_void},
+    os::windows::ffi::OsStrExt,
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
@@ -14,7 +15,8 @@ use std::{
 use anyhow::{anyhow, Result};
 use async_task::Runnable;
 use copypasta::{ClipboardContext, ClipboardProvider};
-use futures::channel::oneshot::Receiver;
+use futures::channel::oneshot::{self, Receiver};
+use itertools::Itertools;
 use parking_lot::Mutex;
 use time::UtcOffset;
 use util::{ResultExt, SemanticVersion};
@@ -25,15 +27,17 @@ use windows::{
         Foundation::{CloseHandle, BOOL, HANDLE, HWND, LPARAM, TRUE},
         Graphics::DirectComposition::DCompositionWaitForCompositorClock,
         System::{
+            Com::{CoCreateInstance, CreateBindCtx, CLSCTX_ALL},
+            Ole::{OleInitialize, OleUninitialize},
+            Threading::{CreateEventW, GetCurrentThreadId, INFINITE},
             Time::{GetTimeZoneInformation, TIME_ZONE_ID_INVALID},
-            {
-                Ole::{OleInitialize, OleUninitialize},
-                Threading::{CreateEventW, GetCurrentThreadId, INFINITE},
-            },
         },
         UI::{
             Input::KeyboardAndMouse::GetDoubleClickTime,
-            Shell::ShellExecuteW,
+            Shell::{
+                FileSaveDialog, IFileSaveDialog, IShellItem, SHCreateItemFromParsingName,
+                ShellExecuteW, SIGDN_FILESYSPATH,
+            },
             WindowsAndMessaging::{
                 DispatchMessageW, EnumThreadWindows, LoadImageW, PeekMessageW, PostQuitMessage,
                 SetCursor, SystemParametersInfoW, TranslateMessage, HCURSOR, IDC_ARROW, IDC_CROSS,
@@ -342,9 +346,32 @@ impl Platform for WindowsPlatform {
         unimplemented!()
     }
 
-    // todo(windows)
     fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Option<PathBuf>> {
-        unimplemented!()
+        let directory = directory.to_owned();
+        let (tx, rx) = oneshot::channel();
+        self.foreground_executor()
+            .spawn(async move {
+                unsafe {
+                    let Ok(dialog) = show_savefile_dialog(directory) else {
+                        let _ = tx.send(None);
+                        return;
+                    };
+                    let Ok(_) = dialog.Show(None) else {
+                        let _ = tx.send(None); // user cancel
+                        return;
+                    };
+                    if let Ok(shell_item) = dialog.GetResult() {
+                        if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
+                            let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap())));
+                            return;
+                        }
+                    }
+                    let _ = tx.send(None);
+                }
+            })
+            .detach();
+
+        rx
     }
 
     fn reveal_path(&self, path: &Path) {
@@ -555,3 +582,26 @@ fn open_target(target: &str) {
         }
     }
 }
+
+unsafe fn show_savefile_dialog(directory: PathBuf) -> Result<IFileSaveDialog> {
+    let dialog: IFileSaveDialog = CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)?;
+    let bind_context = CreateBindCtx(0)?;
+    let Ok(full_path) = directory.canonicalize() else {
+        return Ok(dialog);
+    };
+    let dir_str = full_path.into_os_string();
+    if dir_str.is_empty() {
+        return Ok(dialog);
+    }
+    let dir_vec = dir_str.encode_wide().collect_vec();
+    let ret = SHCreateItemFromParsingName(PCWSTR::from_raw(dir_vec.as_ptr()), &bind_context)
+        .inspect_err(|e| log::error!("unable to create IShellItem: {}", e));
+    if ret.is_ok() {
+        let dir_shell_item: IShellItem = ret.unwrap();
+        let _ = dialog
+            .SetFolder(&dir_shell_item)
+            .inspect_err(|e| log::error!("unable to set folder for save file dialog: {}", e));
+    }
+
+    Ok(dialog)
+}

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

@@ -6,6 +6,7 @@ use std::{
     any::Any,
     cell::{Cell, RefCell},
     ffi::c_void,
+    iter::once,
     num::NonZeroIsize,
     path::PathBuf,
     rc::{Rc, Weak},
@@ -14,7 +15,8 @@ use std::{
 };
 
 use blade_graphics as gpu;
-use futures::channel::oneshot::Receiver;
+use futures::channel::oneshot::{self, Receiver};
+use itertools::Itertools;
 use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
 use smallvec::SmallVec;
 use windows::{
@@ -33,6 +35,10 @@ use windows::{
             },
         },
         UI::{
+            Controls::{
+                TaskDialogIndirect, TASKDIALOGCONFIG, TASKDIALOG_BUTTON, TD_ERROR_ICON,
+                TD_INFORMATION_ICON, TD_WARNING_ICON,
+            },
             Input::KeyboardAndMouse::{
                 GetKeyState, VIRTUAL_KEY, VK_BACK, VK_CONTROL, VK_DOWN, VK_END, VK_ESCAPE, VK_F1,
                 VK_F24, VK_HOME, VK_INSERT, VK_LEFT, VK_LWIN, VK_MENU, VK_NEXT, VK_PRIOR,
@@ -778,7 +784,6 @@ impl PlatformWindow for WindowsWindow {
         self.inner.input_handler.take()
     }
 
-    // todo(windows)
     fn prompt(
         &self,
         level: PromptLevel,
@@ -786,7 +791,72 @@ impl PlatformWindow for WindowsWindow {
         detail: Option<&str>,
         answers: &[&str],
     ) -> Option<Receiver<usize>> {
-        unimplemented!()
+        let (done_tx, done_rx) = oneshot::channel();
+        let msg = msg.to_string();
+        let detail_string = match detail {
+            Some(info) => Some(info.to_string()),
+            None => None,
+        };
+        let answers = answers.iter().map(|s| s.to_string()).collect::<Vec<_>>();
+        let handle = self.inner.hwnd;
+        self.inner
+            .platform_inner
+            .foreground_executor
+            .spawn(async move {
+                unsafe {
+                    let mut config;
+                    config = std::mem::zeroed::<TASKDIALOGCONFIG>();
+                    config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as _;
+                    config.hwndParent = handle;
+                    let title;
+                    let main_icon;
+                    match level {
+                        crate::PromptLevel::Info => {
+                            title = windows::core::w!("Info");
+                            main_icon = TD_INFORMATION_ICON;
+                        }
+                        crate::PromptLevel::Warning => {
+                            title = windows::core::w!("Warning");
+                            main_icon = TD_WARNING_ICON;
+                        }
+                        crate::PromptLevel::Critical => {
+                            title = windows::core::w!("Critical");
+                            main_icon = TD_ERROR_ICON;
+                        }
+                    };
+                    config.pszWindowTitle = title;
+                    config.Anonymous1.pszMainIcon = main_icon;
+                    let instruction = msg.encode_utf16().chain(once(0)).collect_vec();
+                    config.pszMainInstruction = PCWSTR::from_raw(instruction.as_ptr());
+                    let hints_encoded;
+                    if let Some(ref hints) = detail_string {
+                        hints_encoded = hints.encode_utf16().chain(once(0)).collect_vec();
+                        config.pszContent = PCWSTR::from_raw(hints_encoded.as_ptr());
+                    };
+                    let mut buttons = Vec::new();
+                    let mut btn_encoded = Vec::new();
+                    for (index, btn_string) in answers.iter().enumerate() {
+                        let encoded = btn_string.encode_utf16().chain(once(0)).collect_vec();
+                        buttons.push(TASKDIALOG_BUTTON {
+                            nButtonID: index as _,
+                            pszButtonText: PCWSTR::from_raw(encoded.as_ptr()),
+                        });
+                        btn_encoded.push(encoded);
+                    }
+                    config.cButtons = buttons.len() as _;
+                    config.pButtons = buttons.as_ptr();
+
+                    config.pfCallback = None;
+                    let mut res = std::mem::zeroed();
+                    let _ = TaskDialogIndirect(&config, Some(&mut res), None, None)
+                        .inspect_err(|e| log::error!("unable to create task dialog: {}", e));
+
+                    let _ = done_tx.send(res as usize);
+                }
+            })
+            .detach();
+
+        Some(done_rx)
     }
 
     // todo(windows)

crates/zed/resources/windows/manifest.xml 🔗

@@ -5,4 +5,12 @@
             <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
         </asmv3:windowsSettings>
     </asmv3:application>
+    <dependency>
+        <dependentAssembly>
+            <assemblyIdentity type='win32' 
+                name='Microsoft.Windows.Common-Controls' 
+                version='6.0.0.0' processorArchitecture='*'   
+                publicKeyToken='6595b64144ccf1df' />
+        </dependentAssembly>
+    </dependency>
 </assembly>