//! Windows drag backend: OLE `DoDragDrop` with `CF_HDROP`. //! //! Implements the three COM interfaces that Windows requires to act as a //! drag source for files: //! //! - **`IDropSource`** — controls drag continuation (escape cancels, mouse-up drops). //! - **`IDataObject`** — serves clipboard data; we provide a single `CF_HDROP` //! `HGLOBAL` containing a `DROPFILES` header followed by null-terminated //! UTF-16 file paths. //! - **`IEnumFORMATETC`** — enumerates available clipboard formats (just CF_HDROP). //! //! `DoDragDrop` is a blocking/modal call — it runs a nested message loop //! and returns only when the user drops or cancels. The `DRAG_ACTIVE` guard //! in the parent module is cleared synchronously after it returns. use std::os::windows::ffi::OsStrExt; use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; use tracing::{debug, warn}; use windows::core::*; use windows::Win32::Foundation::*; use windows::Win32::System::Com::*; // STGMEDIUM and STGMEDIUM_0 are in Win32::System::Com (re-exported via Com::*) use windows::Win32::System::Memory::*; use windows::Win32::System::Ole::*; use windows::Win32::System::SystemServices::MODIFIERKEYS_FLAGS; const CF_HDROP_VALUE: u16 = 15; const MK_LBUTTON: u32 = 0x0001; // ---------- DROPFILES header ---------- /// Win32 DROPFILES structure. Defined here to avoid pulling in the Shell feature. /// Layout is ABI-stable: 20-byte header followed by null-terminated UTF-16 paths. #[repr(C)] struct DropFilesHeader { /// Byte offset from the start of this struct to the first file path. p_files: u32, pt_x: i32, pt_y: i32, /// Non-client area flag (unused, zero). f_nc: i32, /// 1 = paths are UTF-16. Must be 1 for Unicode paths. f_wide: i32, } fn hdrop_formatetc() -> FORMATETC { FORMATETC { cfFormat: CF_HDROP_VALUE, ptd: std::ptr::null_mut(), dwAspect: DVASPECT_CONTENT.0 as u32, lindex: -1, tymed: TYMED_HGLOBAL.0 as u32, } } // ---------- Build CF_HDROP HGLOBAL ---------- /// Allocate a moveable HGLOBAL containing a DROPFILES header + UTF-16 paths. /// The buffer ends with a double-null terminator (first null ends the last /// path, second null ends the list). GMEM_ZEROINIT handles the trailing null. /// /// # Safety /// Caller must free the returned HGLOBAL via `GlobalFree` when done. /// All pointer arithmetic stays within the `total_bytes` allocation. unsafe fn build_hdrop(paths: &[PathBuf]) -> Result<(HGLOBAL, usize)> { let header_size = std::mem::size_of::(); let wide_paths: Vec> = paths .iter() .map(|p| p.as_os_str().encode_wide().chain(std::iter::once(0)).collect()) .collect(); let total_wide: usize = wide_paths.iter().map(|w| w.len()).sum::() + 1; let total_bytes = header_size + total_wide * 2; let h = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, total_bytes)?; let ptr = GlobalLock(h); if ptr.is_null() { let _ = GlobalFree(Some(h)); return Err(Error::from(E_OUTOFMEMORY)); } let ptr = ptr as *mut u8; let header = ptr as *mut DropFilesHeader; (*header).p_files = header_size as u32; (*header).f_wide = 1; let mut offset = header_size; for wide in &wide_paths { let byte_len = wide.len() * 2; std::ptr::copy_nonoverlapping(wide.as_ptr() as *const u8, ptr.add(offset), byte_len); offset += byte_len; } let _ = GlobalUnlock(h); Ok((h, total_bytes)) } // ---------- IDropSource ---------- /// Minimal drag source: cancel on Escape, drop on mouse-up, default cursors. #[implement(IDropSource)] struct DropSource; impl IDropSource_Impl for DropSource_Impl { fn QueryContinueDrag(&self, fescapepressed: BOOL, grfkeystate: MODIFIERKEYS_FLAGS) -> HRESULT { if fescapepressed.as_bool() { DRAGDROP_S_CANCEL } else if grfkeystate.0 & MK_LBUTTON == 0 { DRAGDROP_S_DROP } else { S_OK } } fn GiveFeedback(&self, _dweffect: DROPEFFECT) -> HRESULT { DRAGDROP_S_USEDEFAULTCURSORS } } // ---------- IEnumFORMATETC ---------- /// Enumerates a single clipboard format (CF_HDROP). #[implement(IEnumFORMATETC)] struct HDropFormatEnum { pos: AtomicU32, } impl IEnumFORMATETC_Impl for HDropFormatEnum_Impl { fn Next( &self, celt: u32, rgelt: *mut FORMATETC, pceltfetched: *mut u32, ) -> HRESULT { let pos = self.pos.load(Ordering::Relaxed); if pos >= 1 || celt == 0 { if !pceltfetched.is_null() { // SAFETY: COM contract — pceltfetched is a valid caller-allocated // out-parameter when non-null (null-checked above). unsafe { *pceltfetched = 0 }; } return S_FALSE; } self.pos.store(1, Ordering::Relaxed); // SAFETY: COM contract — rgelt points to a caller-allocated array of at // least `celt` FORMATETC elements. pceltfetched is valid when non-null. unsafe { *rgelt = hdrop_formatetc(); if !pceltfetched.is_null() { *pceltfetched = 1; } } S_OK } fn Skip(&self, celt: u32) -> windows_core::Result<()> { self.pos.fetch_add(celt, Ordering::Relaxed); Ok(()) } fn Reset(&self) -> windows_core::Result<()> { self.pos.store(0, Ordering::Relaxed); Ok(()) } fn Clone(&self) -> Result { let e: IEnumFORMATETC = HDropFormatEnum { pos: AtomicU32::new(self.pos.load(Ordering::Relaxed)), } .into(); Ok(e) } } // ---------- IDataObject ---------- /// Serves a single CF_HDROP payload. Clones the HGLOBAL on each `GetData` /// call so the caller owns its copy. Frees the original on drop. #[implement(IDataObject)] struct FileDataObject { hdrop: HGLOBAL, size: usize, } impl Drop for FileDataObject { fn drop(&mut self) { // SAFETY: self.hdrop was allocated by build_hdrop via GlobalAlloc and // has not been freed yet (Drop runs exactly once). unsafe { let _ = GlobalFree(Some(self.hdrop)); } } } impl IDataObject_Impl for FileDataObject_Impl { fn GetData(&self, pformatetcin: *const FORMATETC) -> Result { // SAFETY: COM contract — pformatetcin is a valid non-null pointer to a // caller-owned FORMATETC. let fmt = unsafe { &*pformatetcin }; if fmt.cfFormat != CF_HDROP_VALUE { return Err(Error::from(DV_E_FORMATETC)); } // SAFETY: self.hdrop is valid (not freed until Drop). New HGLOBAL is // allocated at the same size. Copy stays within bounds (self.size bytes). // The returned STGMEDIUM transfers ownership of `h` to the caller. unsafe { let h = GlobalAlloc(GMEM_MOVEABLE, self.size)?; let src = GlobalLock(self.hdrop); if src.is_null() { let _ = GlobalFree(Some(h)); return Err(Error::from(E_OUTOFMEMORY)); } let dst = GlobalLock(h); if dst.is_null() { let _ = GlobalUnlock(self.hdrop); let _ = GlobalFree(Some(h)); return Err(Error::from(E_OUTOFMEMORY)); } std::ptr::copy_nonoverlapping(src as *const u8, dst as *mut u8, self.size); let _ = GlobalUnlock(self.hdrop); let _ = GlobalUnlock(h); Ok(STGMEDIUM { tymed: TYMED_HGLOBAL.0 as u32, u: STGMEDIUM_0 { hGlobal: h }, pUnkForRelease: std::mem::ManuallyDrop::new(None), }) } } fn GetDataHere(&self, _: *const FORMATETC, _: *mut STGMEDIUM) -> Result<()> { Err(Error::from(E_NOTIMPL)) } fn QueryGetData(&self, pformatetc: *const FORMATETC) -> HRESULT { // SAFETY: COM contract — pformatetc is a valid non-null pointer. let fmt = unsafe { &*pformatetc }; if fmt.cfFormat == CF_HDROP_VALUE { S_OK } else { DV_E_FORMATETC } } fn GetCanonicalFormatEtc(&self, _: *const FORMATETC, pformatetcout: *mut FORMATETC) -> HRESULT { // SAFETY: COM contract guarantees `pformatetcout` is a valid non-null pointer // allocated by the caller. We only write `ptd` to indicate no device target. unsafe { (*pformatetcout).ptd = std::ptr::null_mut(); } DATA_S_SAMEFORMATETC } fn SetData(&self, _: *const FORMATETC, _: *const STGMEDIUM, _: BOOL) -> Result<()> { Err(Error::from(E_NOTIMPL)) } fn EnumFormatEtc(&self, dwdirection: u32) -> Result { if dwdirection != DATADIR_GET.0 as u32 { return Err(Error::from(E_NOTIMPL)); } let e: IEnumFORMATETC = HDropFormatEnum { pos: AtomicU32::new(0), } .into(); Ok(e) } fn DAdvise(&self, _: *const FORMATETC, _: u32, _: Ref) -> Result { Err(Error::from(OLE_E_ADVISENOTSUPPORTED)) } fn DUnadvise(&self, _: u32) -> Result<()> { Err(Error::from(OLE_E_ADVISENOTSUPPORTED)) } fn EnumDAdvise(&self) -> Result { Err(Error::from(OLE_E_ADVISENOTSUPPORTED)) } } // ---------- Entry point ---------- /// Start a blocking OLE drag-drop session. Returns `true` if the user /// completed the drop (as opposed to cancelling or dragging to a /// non-accepting target). pub(super) fn begin_drag_session(paths: &[PathBuf]) -> bool { // SAFETY: OleInitialize/OleUninitialize bracket the session. build_hdrop is // called with valid paths and its HGLOBAL is owned by FileDataObject (freed // on drop). COM objects are constructed via safe `into()`. DoDragDrop is the // standard OLE drag entry point — it blocks until the user drops or cancels. unsafe { // OleInitialize is the superset of CoInitializeEx — required for DoDragDrop. // Ok(()) means we initialized, Err with S_FALSE means already initialized (fine). let ole_result = OleInitialize(None); let we_initialized = ole_result.is_ok(); if !we_initialized { // Check if it's the benign "already initialized" case if let Err(ref e) = ole_result { if e.code() != S_FALSE { warn!(?ole_result, "OleInitialize failed"); return false; } } } let result = (|| -> Result { let (hdrop, size) = build_hdrop(paths)?; let data_object: IDataObject = FileDataObject { hdrop, size }.into(); let drop_source: IDropSource = DropSource.into(); let mut effect = DROPEFFECT(0); let hr = DoDragDrop(&data_object, &drop_source, DROPEFFECT_COPY, &mut effect); debug!(?hr, ?effect, "DoDragDrop completed"); Ok(hr == DRAGDROP_S_DROP) })(); if we_initialized { OleUninitialize(); } match result { Ok(dropped) => dropped, Err(e) => { warn!(%e, "Drag failed"); false } } } }