Impl drag-drop action for Windows (#8959)

Small White and Mikayla Maki created

### Description

This is a part of #8809 



https://github.com/zed-industries/zed/assets/14981363/2b085b9d-8b83-4ac7-8b84-07c679760eba




Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>

Change summary

Cargo.lock                                   |  24 +++
Cargo.toml                                   |   3 
crates/gpui/src/platform/windows/platform.rs |  27 ++
crates/gpui/src/platform/windows/window.rs   | 157 +++++++++++++++++++++
typos.toml                                   |   2 
5 files changed, 202 insertions(+), 11 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -12160,6 +12160,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538"
 dependencies = [
  "windows-core",
+ "windows-implement",
+ "windows-interface",
  "windows-targets 0.52.4",
 ]
 
@@ -12173,6 +12175,28 @@ dependencies = [
  "windows-targets 0.52.4",
 ]
 
+[[package]]
+name = "windows-implement"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
 [[package]]
 name = "windows-result"
 version = "0.1.0"

Cargo.toml 🔗

@@ -325,6 +325,7 @@ sys-locale = "0.3.1"
 [workspace.dependencies.windows]
 version = "0.53.0"
 features = [
+    "implement",
     "Wdk_System_SystemServices",
     "Win32_Graphics_Gdi",
     "Win32_Graphics_DirectComposition",
@@ -335,6 +336,8 @@ features = [
     "Win32_System_SystemServices",
     "Win32_System_Time",
     "Win32_Security",
+    "Win32_System_Com",
+    "Win32_System_Com_StructuredStorage",
     "Win32_System_Threading",
     "Win32_System_DataExchange",
     "Win32_System_Ole",

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

@@ -24,19 +24,21 @@ use windows::{
         Foundation::{CloseHandle, BOOL, HANDLE, HWND, LPARAM, TRUE},
         Graphics::DirectComposition::DCompositionWaitForCompositorClock,
         System::{
-            Threading::{CreateEventW, GetCurrentThreadId, INFINITE},
             Time::{GetTimeZoneInformation, TIME_ZONE_ID_INVALID},
+            {
+                Ole::{OleInitialize, OleUninitialize},
+                Threading::{CreateEventW, GetCurrentThreadId, INFINITE},
+            },
         },
         UI::{
             Input::KeyboardAndMouse::GetDoubleClickTime,
             Shell::ShellExecuteW,
             WindowsAndMessaging::{
-                DispatchMessageW, EnumThreadWindows, LoadImageW, MsgWaitForMultipleObjects,
-                PeekMessageW, PostQuitMessage, SetCursor, SystemParametersInfoW, TranslateMessage,
-                HCURSOR, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_IBEAM, IDC_NO, IDC_SIZENS, IDC_SIZEWE,
-                IMAGE_CURSOR, LR_DEFAULTSIZE, LR_SHARED, MSG, PM_REMOVE, QS_ALLINPUT,
-                SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SW_SHOWDEFAULT,
-                SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, WM_QUIT, WM_SETTINGCHANGE,
+                DispatchMessageW, EnumThreadWindows, LoadImageW, PeekMessageW, PostQuitMessage,
+                SetCursor, SystemParametersInfoW, TranslateMessage, HCURSOR, IDC_ARROW, IDC_CROSS,
+                IDC_HAND, IDC_IBEAM, IDC_NO, IDC_SIZENS, IDC_SIZEWE, IMAGE_CURSOR, LR_DEFAULTSIZE,
+                LR_SHARED, MSG, PM_REMOVE, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES,
+                SW_SHOWDEFAULT, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, WM_QUIT, WM_SETTINGCHANGE,
             },
         },
     },
@@ -147,6 +149,9 @@ impl WindowsPlatformSystemSettings {
 
 impl WindowsPlatform {
     pub(crate) fn new() -> Self {
+        unsafe {
+            OleInitialize(None).expect("unable to initialize Windows OLE");
+        }
         let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
         let event = unsafe { CreateEventW(None, false, false, None) }.unwrap();
         let dispatcher = Arc::new(WindowsDispatcher::new(main_sender, event));
@@ -518,6 +523,14 @@ impl Platform for WindowsPlatform {
     }
 }
 
+impl Drop for WindowsPlatform {
+    fn drop(&mut self) {
+        unsafe {
+            OleUninitialize();
+        }
+    }
+}
+
 unsafe fn load_cursor(name: PCWSTR) -> Result<HANDLE> {
     LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED).map_err(|e| anyhow!(e))
 }

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

@@ -7,20 +7,30 @@ use std::{
     cell::{Cell, RefCell},
     ffi::c_void,
     num::NonZeroIsize,
+    path::PathBuf,
     rc::{Rc, Weak},
+    str::FromStr,
     sync::{Arc, Once},
 };
 
 use blade_graphics as gpu;
 use futures::channel::oneshot::Receiver;
 use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
+use smallvec::SmallVec;
 use windows::{
-    core::{w, HSTRING, PCWSTR},
+    core::{implement, w, HSTRING, PCWSTR},
     Win32::{
-        Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, WPARAM},
+        Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, MAX_PATH, POINTL, S_OK, WPARAM},
         Graphics::Gdi::{BeginPaint, EndPaint, InvalidateRect, PAINTSTRUCT},
-        System::SystemServices::{
-            MK_LBUTTON, MK_MBUTTON, MK_RBUTTON, MK_XBUTTON1, MK_XBUTTON2, MODIFIERKEYS_FLAGS,
+        System::{
+            Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL},
+            Ole::{
+                IDropTarget, IDropTarget_Impl, RegisterDragDrop, ReleaseStgMedium, RevokeDragDrop,
+                CF_HDROP, DROPEFFECT, DROPEFFECT_LINK, DROPEFFECT_NONE,
+            },
+            SystemServices::{
+                MK_LBUTTON, MK_MBUTTON, MK_RBUTTON, MK_XBUTTON1, MK_XBUTTON2, MODIFIERKEYS_FLAGS,
+            },
         },
         UI::{
             Input::KeyboardAndMouse::{
@@ -28,6 +38,7 @@ use windows::{
                 VK_F24, VK_HOME, VK_INSERT, VK_LEFT, VK_LWIN, VK_MENU, VK_NEXT, VK_PRIOR,
                 VK_RETURN, VK_RIGHT, VK_RWIN, VK_SHIFT, VK_SPACE, VK_TAB, VK_UP,
             },
+            Shell::{DragQueryFileW, HDROP},
             WindowsAndMessaging::{
                 CreateWindowExW, DefWindowProcW, GetWindowLongPtrW, LoadCursorW, PostQuitMessage,
                 RegisterClassW, SetWindowLongPtrW, SetWindowTextW, ShowWindow, CREATESTRUCTW,
@@ -559,6 +570,14 @@ impl WindowsWindowInner {
         }
         LRESULT(1)
     }
+
+    fn handle_drag_drop(&self, input: PlatformInput) {
+        let mut callbacks = self.callbacks.borrow_mut();
+        let Some(ref mut func) = callbacks.input else {
+            return;
+        };
+        func(input);
+    }
 }
 
 #[derive(Default)]
@@ -576,6 +595,7 @@ struct Callbacks {
 
 pub(crate) struct WindowsWindow {
     inner: Rc<WindowsWindowInner>,
+    drag_drop_handler: IDropTarget,
 }
 
 struct WindowCreateContext {
@@ -640,8 +660,19 @@ impl WindowsWindow {
                 lpparam,
             )
         };
+        let drag_drop_handler = {
+            let inner = context.inner.as_ref().unwrap();
+            let handler = WindowsDragDropHandler(Rc::clone(inner));
+            let drag_drop_handler: IDropTarget = handler.into();
+            unsafe {
+                RegisterDragDrop(inner.hwnd, &drag_drop_handler)
+                    .expect("unable to register drag-drop event")
+            };
+            drag_drop_handler
+        };
         let wnd = Self {
             inner: context.inner.unwrap(),
+            drag_drop_handler,
         };
         platform_inner.window_handles.borrow_mut().insert(handle);
         match options.bounds {
@@ -679,6 +710,14 @@ impl HasDisplayHandle for WindowsWindow {
     }
 }
 
+impl Drop for WindowsWindow {
+    fn drop(&mut self) {
+        unsafe {
+            let _ = RevokeDragDrop(self.inner.hwnd);
+        }
+    }
+}
+
 impl PlatformWindow for WindowsWindow {
     fn bounds(&self) -> WindowBounds {
         WindowBounds::Fixed(Bounds {
@@ -836,6 +875,113 @@ impl PlatformWindow for WindowsWindow {
     }
 }
 
+#[implement(IDropTarget)]
+struct WindowsDragDropHandler(pub Rc<WindowsWindowInner>);
+
+impl IDropTarget_Impl for WindowsDragDropHandler {
+    fn DragEnter(
+        &self,
+        pdataobj: Option<&IDataObject>,
+        _grfkeystate: MODIFIERKEYS_FLAGS,
+        pt: &POINTL,
+        pdweffect: *mut DROPEFFECT,
+    ) -> windows::core::Result<()> {
+        unsafe {
+            let Some(idata_obj) = pdataobj else {
+                log::info!("no dragging file or directory detected");
+                return Ok(());
+            };
+            let config = FORMATETC {
+                cfFormat: CF_HDROP.0,
+                ptd: std::ptr::null_mut() as _,
+                dwAspect: DVASPECT_CONTENT.0,
+                lindex: -1,
+                tymed: TYMED_HGLOBAL.0 as _,
+            };
+            let mut paths = SmallVec::<[PathBuf; 2]>::new();
+            if idata_obj.QueryGetData(&config as _) == S_OK {
+                *pdweffect = DROPEFFECT_LINK;
+                let Ok(mut idata) = idata_obj.GetData(&config as _) else {
+                    return Ok(());
+                };
+                if idata.u.hGlobal.is_invalid() {
+                    return Ok(());
+                }
+                let hdrop = idata.u.hGlobal.0 as *mut HDROP;
+                let file_count = DragQueryFileW(*hdrop, DRAGDROP_GET_FILES_COUNT, None);
+                for file_index in 0..file_count {
+                    let mut buffer = [0u16; MAX_PATH as _];
+                    let filename_length = DragQueryFileW(*hdrop, file_index, None) as usize;
+                    let ret = DragQueryFileW(*hdrop, file_index, Some(&mut buffer));
+                    if ret == 0 {
+                        log::error!("unable to read file name");
+                        continue;
+                    }
+                    if let Ok(file_name) = String::from_utf16(&buffer[0..filename_length]) {
+                        if let Ok(path) = PathBuf::from_str(&file_name) {
+                            paths.push(path);
+                        }
+                    }
+                }
+                ReleaseStgMedium(&mut idata);
+                let input = PlatformInput::FileDrop(crate::FileDropEvent::Entered {
+                    position: Point {
+                        x: Pixels(pt.x as _),
+                        y: Pixels(pt.y as _),
+                    },
+                    paths: crate::ExternalPaths(paths),
+                });
+                self.0.handle_drag_drop(input);
+            } else {
+                *pdweffect = DROPEFFECT_NONE;
+            }
+        }
+        Ok(())
+    }
+
+    fn DragOver(
+        &self,
+        _grfkeystate: MODIFIERKEYS_FLAGS,
+        pt: &POINTL,
+        _pdweffect: *mut DROPEFFECT,
+    ) -> windows::core::Result<()> {
+        let input = PlatformInput::FileDrop(crate::FileDropEvent::Pending {
+            position: Point {
+                x: Pixels(pt.x as _),
+                y: Pixels(pt.y as _),
+            },
+        });
+        self.0.handle_drag_drop(input);
+
+        Ok(())
+    }
+
+    fn DragLeave(&self) -> windows::core::Result<()> {
+        let input = PlatformInput::FileDrop(crate::FileDropEvent::Exited);
+        self.0.handle_drag_drop(input);
+
+        Ok(())
+    }
+
+    fn Drop(
+        &self,
+        _pdataobj: Option<&IDataObject>,
+        _grfkeystate: MODIFIERKEYS_FLAGS,
+        pt: &POINTL,
+        _pdweffect: *mut DROPEFFECT,
+    ) -> windows::core::Result<()> {
+        let input = PlatformInput::FileDrop(crate::FileDropEvent::Submit {
+            position: Point {
+                x: Pixels(pt.x as _),
+                y: Pixels(pt.y as _),
+            },
+        });
+        self.0.handle_drag_drop(input);
+
+        Ok(())
+    }
+}
+
 fn register_wnd_class() -> PCWSTR {
     const CLASS_NAME: PCWSTR = w!("Zed::Window");
 
@@ -923,3 +1069,6 @@ unsafe fn set_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX, dwnewlong:
         SetWindowLongW(hwnd, nindex, dwnewlong as i32) as isize
     }
 }
+
+// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
+const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;

typos.toml 🔗

@@ -16,6 +16,8 @@ extend-exclude = [
     "crates/editor/src/editor_tests.rs",
     # Clojure uses .edn filename extension, which is not a misspelling of "end"
     "crates/languages/src/clojure/config.toml",
+    # Windows likes it's abbreviations
+    "crates/gpui/src/platform/windows/",
 ]
 
 [default]