use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DragEffect {
None,
Copy,
Move,
}
static MODAL: AtomicBool = AtomicBool::new(false);
pub fn drag_out(hwnd: isize, paths: &[PathBuf], allow_move: bool) -> Result<DragEffect, String> {
platform::drag_out(hwnd, paths, allow_move)
}
pub fn set_modal_active(active: bool) {
MODAL.store(active, Ordering::SeqCst);
}
pub fn is_modal_active() -> bool {
MODAL.load(Ordering::Relaxed)
}
#[cfg(windows)]
mod platform {
use std::cell::Cell;
use std::ffi::c_void;
use std::os::windows::ffi::OsStrExt;
use std::path::PathBuf;
use windows::core::{implement, HRESULT, BOOL, PCWSTR};
use windows::Win32::Foundation::{
GlobalFree, DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, DRAGDROP_S_USEDEFAULTCURSORS,
DV_E_DVASPECT, DV_E_FORMATETC, E_NOTIMPL, E_OUTOFMEMORY, E_POINTER, HGLOBAL, HWND,
OLE_E_ADVISENOTSUPPORTED, OLE_E_NOCONNECTION, POINT, RPC_E_CHANGED_MODE, S_FALSE, S_OK,
};
use windows::Win32::System::Com::{
IAdviseSink, IDataObject, IDataObject_Impl, IEnumFORMATETC, IEnumFORMATETC_Impl, IEnumSTATDATA,
DATADIR_GET, DVASPECT_CONTENT,
FORMATETC, STGMEDIUM, STGMEDIUM_0, TYMED_HGLOBAL,
};
use windows::Win32::System::Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE};
use windows::Win32::System::DataExchange::RegisterClipboardFormatW;
use windows::Win32::System::Ole::{
DoDragDrop, IDropSource, IDropSource_Impl, OleInitialize, ReleaseStgMedium, CF_HDROP, DROPEFFECT,
DROPEFFECT_COPY, DROPEFFECT_MOVE, DROPEFFECT_NONE,
};
use windows::Win32::System::SystemServices::{MK_LBUTTON, MK_RBUTTON, MODIFIERKEYS_FLAGS};
use windows::Win32::UI::Shell::DROPFILES;
use windows::Win32::UI::WindowsAndMessaging::IsWindow;
use crate::flog;
use super::DragEffect;
fn pack_hdrop(paths: &[PathBuf]) -> Vec<u8> {
let header = DROPFILES {
pFiles: std::mem::size_of::<DROPFILES>() as u32,
pt: POINT::default(),
fNC: BOOL(0),
fWide: BOOL(1),
};
let mut bytes = Vec::with_capacity(std::mem::size_of::<DROPFILES>() + 64);
bytes.extend_from_slice(unsafe { as_bytes(&header) });
for path in paths {
for unit in path.as_os_str().encode_wide() {
bytes.extend_from_slice(&unit.to_ne_bytes());
}
bytes.extend_from_slice(&0u16.to_ne_bytes());
}
bytes.extend_from_slice(&0u16.to_ne_bytes());
bytes
}
fn hglobal_from_bytes(bytes: &[u8]) -> Option<HGLOBAL> {
if bytes.is_empty() {
return None;
}
unsafe {
let handle = GlobalAlloc(GMEM_MOVEABLE, bytes.len()).ok()?;
let target = GlobalLock(handle);
if target.is_null() {
let _ = GlobalFree(Some(handle));
return None;
}
std::ptr::copy_nonoverlapping(bytes.as_ptr(), target as *mut u8, bytes.len());
let _ = GlobalUnlock(handle);
Some(handle)
}
}
fn preferred_drop_effect() -> u16 {
static ID: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
*ID.get_or_init(|| register_format("Preferred DropEffect"))
}
fn performed_drop_effect() -> u16 {
static ID: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
*ID.get_or_init(|| register_format("Performed DropEffect"))
}
fn register_format(name: &str) -> u16 {
let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
let id = unsafe { RegisterClipboardFormatW(PCWSTR(wide.as_ptr())) };
(id & 0xffff) as u16
}
fn is_preferred_effect_format(format: &FORMATETC) -> bool {
format.cfFormat == preferred_drop_effect()
&& format.tymed & (TYMED_HGLOBAL.0 as u32) != 0
&& format.dwAspect == DVASPECT_CONTENT.0
}
fn is_hdrop_format(format: &FORMATETC) -> bool {
format.cfFormat == CF_HDROP.0
&& format.tymed & (TYMED_HGLOBAL.0 as u32) != 0
&& format.dwAspect == DVASPECT_CONTENT.0
}
#[implement(IDataObject)]
struct DataObject {
payload: Vec<u8>,
preferred_effect: u32,
}
#[implement(IEnumFORMATETC)]
struct FormatEtcEnum {
total: usize,
index: std::sync::atomic::AtomicUsize,
}
impl FormatEtcEnum {
fn new() -> Self {
Self {
total: 2,
index: std::sync::atomic::AtomicUsize::new(0),
}
}
fn format_at(i: usize) -> FORMATETC {
FORMATETC {
cfFormat: if i == 0 { CF_HDROP.0 } else { preferred_drop_effect() },
ptd: std::ptr::null_mut(),
dwAspect: DVASPECT_CONTENT.0,
lindex: -1,
tymed: TYMED_HGLOBAL.0 as u32,
}
}
}
impl IEnumFORMATETC_Impl for FormatEtcEnum_Impl {
fn Next(&self, celt: u32, rgelt: *mut FORMATETC, fetched: *mut u32) -> HRESULT {
use std::sync::atomic::Ordering::Relaxed;
let mut got = 0u32;
let mut i = self.index.load(Relaxed);
while got < celt && i < self.total {
if !rgelt.is_null() {
unsafe { *rgelt.add(got as usize) = FormatEtcEnum::format_at(i) };
}
got += 1;
i += 1;
}
self.index.store(i, Relaxed);
if !fetched.is_null() {
unsafe { *fetched = got };
}
if got == celt { S_OK } else { S_FALSE }
}
fn Skip(&self, celt: u32) -> windows::core::Result<()> {
use std::sync::atomic::Ordering::Relaxed;
let target = self.index.load(Relaxed).saturating_add(celt as usize);
self.index.store(target, Relaxed);
if target <= self.total {
Ok(())
} else {
Err(S_FALSE.into())
}
}
fn Reset(&self) -> windows::core::Result<()> {
self.index.store(0, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
fn Clone(&self) -> windows::core::Result<IEnumFORMATETC> {
use std::sync::atomic::Ordering::Relaxed;
let clone = FormatEtcEnum::new();
clone.index.store(self.index.load(Relaxed), Relaxed);
Ok(clone.into())
}
}
impl IDataObject_Impl for DataObject_Impl {
fn GetData(&self, format: *const FORMATETC) -> windows::core::Result<STGMEDIUM> {
if format.is_null() {
return Err(E_POINTER.into());
}
let format = unsafe { &*format };
if format.dwAspect != DVASPECT_CONTENT.0 {
return Err(DV_E_DVASPECT.into());
}
if is_preferred_effect_format(format) {
let bytes = self.preferred_effect.to_ne_bytes();
let Some(handle) = hglobal_from_bytes(&bytes) else {
return Err(E_OUTOFMEMORY.into());
};
return Ok(STGMEDIUM {
tymed: TYMED_HGLOBAL.0 as u32,
u: STGMEDIUM_0 { hGlobal: handle },
pUnkForRelease: std::mem::ManuallyDrop::new(None),
});
}
if format.cfFormat != CF_HDROP.0 || format.tymed & (TYMED_HGLOBAL.0 as u32) == 0 {
return Err(DV_E_FORMATETC.into());
}
let Some(handle) = hglobal_from_bytes(&self.payload) else {
flog!("[shell] 拖拽:把 CF_HDROP 载荷拷进全局内存失败");
return Err(E_OUTOFMEMORY.into());
};
Ok(STGMEDIUM {
tymed: TYMED_HGLOBAL.0 as u32,
u: STGMEDIUM_0 { hGlobal: handle },
pUnkForRelease: std::mem::ManuallyDrop::new(None),
})
}
fn GetDataHere(
&self,
_format: *const FORMATETC,
_medium: *mut STGMEDIUM,
) -> windows::core::Result<()> {
Err(E_NOTIMPL.into())
}
fn QueryGetData(&self, format: *const FORMATETC) -> HRESULT {
if format.is_null() {
return E_POINTER;
}
let f = unsafe { &*format };
if is_hdrop_format(f) || is_preferred_effect_format(f) {
S_OK
} else {
S_FALSE
}
}
fn GetCanonicalFormatEtc(
&self,
_format_in: *const FORMATETC,
_format_out: *mut FORMATETC,
) -> HRESULT {
E_NOTIMPL
}
fn SetData(
&self,
_format: *const FORMATETC,
_medium: *const STGMEDIUM,
_release: BOOL,
) -> windows::core::Result<()> {
if _format.is_null() || _medium.is_null() {
return Err(E_POINTER.into());
}
let (format, medium) = unsafe { (&*_format, &*_medium) };
if format.cfFormat != performed_drop_effect() {
return Err(E_NOTIMPL.into());
}
let value = if medium.tymed == TYMED_HGLOBAL.0 as u32 {
let handle = unsafe { medium.u.hGlobal };
let ptr = unsafe { GlobalLock(handle) };
if ptr.is_null() {
0
} else {
let value = unsafe { *(ptr as *const u32) };
let _ = unsafe { GlobalUnlock(handle) };
value
}
} else {
0
};
crate::flog!("[drag] 落点回报所做的效果 = {value}");
if _release.as_bool() {
unsafe { ReleaseStgMedium(_medium as *mut STGMEDIUM) };
}
Ok(())
}
fn EnumFormatEtc(&self, direction: u32) -> windows::core::Result<IEnumFORMATETC> {
if direction != DATADIR_GET.0 as u32 {
return Err(E_NOTIMPL.into());
}
Ok(FormatEtcEnum::new().into())
}
fn DAdvise(
&self,
_format: *const FORMATETC,
_flags: u32,
_sink: windows::core::Ref<'_, IAdviseSink>,
) -> windows::core::Result<u32> {
Err(OLE_E_ADVISENOTSUPPORTED.into())
}
fn DUnadvise(&self, _connection: u32) -> windows::core::Result<()> {
Err(OLE_E_NOCONNECTION.into())
}
fn EnumDAdvise(&self) -> windows::core::Result<IEnumSTATDATA> {
Err(OLE_E_ADVISENOTSUPPORTED.into())
}
}
#[implement(IDropSource)]
struct DropSource {}
impl IDropSource_Impl for DropSource_Impl {
fn QueryContinueDrag(&self, f_escape_pressed: BOOL, keystate: MODIFIERKEYS_FLAGS) -> HRESULT {
if f_escape_pressed.as_bool() {
return DRAGDROP_S_CANCEL;
}
if (keystate & MK_LBUTTON).0 == 0 && (keystate & MK_RBUTTON).0 == 0 {
DRAGDROP_S_DROP
} else {
S_OK
}
}
fn GiveFeedback(&self, _effect: DROPEFFECT) -> HRESULT {
DRAGDROP_S_USEDEFAULTCURSORS
}
}
struct ModalGuard;
impl ModalGuard {
fn enter() -> Self {
super::set_modal_active(true);
Self
}
}
impl Drop for ModalGuard {
fn drop(&mut self) {
super::set_modal_active(false);
}
}
thread_local! {
static OLE_STATE: Cell<u8> = const { Cell::new(0) };
}
fn ensure_ole() -> Result<(), String> {
OLE_STATE.with(|state| match state.get() {
1 => Ok(()),
2 => Err("当前线程在 MTA 里(RPC_E_CHANGED_MODE),OLE 拖放不可用".to_string()),
_ => {
match unsafe { OleInitialize(None) } {
Ok(()) => {
state.set(1);
Ok(())
}
Err(err) if err.code() == RPC_E_CHANGED_MODE => {
state.set(2);
Err("当前线程在 MTA 里(RPC_E_CHANGED_MODE),OLE 拖放不可用".to_string())
}
Err(err) => {
flog!("[shell] OleInitialize 失败:{err}");
Err(format!("初始化 OLE 失败:{err}"))
}
}
}
})
}
pub(super) fn drag_out(
hwnd: isize,
paths: &[PathBuf],
allow_move: bool,
) -> Result<DragEffect, String> {
if paths.is_empty() {
return Err("没有可拖拽的路径".to_string());
}
let window = HWND(hwnd as *mut c_void);
if !unsafe { IsWindow(Some(window)) }.as_bool() {
return Err(format!("窗口句柄无效:0x{hwnd:x}"));
}
ensure_ole()?;
let preferred = if allow_move {
DROPEFFECT_MOVE.0
} else {
DROPEFFECT_COPY.0
};
let data: IDataObject = DataObject {
payload: pack_hdrop(paths),
preferred_effect: preferred,
}
.into();
let source: IDropSource = DropSource {}.into();
let allowed = if allow_move {
DROPEFFECT_COPY | DROPEFFECT_MOVE
} else {
DROPEFFECT_COPY
};
let mut effect = DROPEFFECT_NONE;
let _modal = ModalGuard::enter();
let hr = unsafe { DoDragDrop(&data, &source, allowed, &mut effect) };
if hr == DRAGDROP_S_DROP {
Ok(drag_effect(effect))
} else if hr == DRAGDROP_S_CANCEL {
Ok(DragEffect::None)
} else {
flog!("[shell] DoDragDrop 失败:{hr:?}");
Err(format!("系统拖放失败:{hr:?}"))
}
}
fn drag_effect(effect: DROPEFFECT) -> DragEffect {
if effect == DROPEFFECT_COPY {
DragEffect::Copy
} else if effect == DROPEFFECT_MOVE {
DragEffect::Move
} else {
DragEffect::None
}
}
unsafe fn as_bytes<T>(value: &T) -> &[u8] {
unsafe { std::slice::from_raw_parts(value as *const T as *const u8, std::mem::size_of::<T>()) }
}
#[cfg(test)]
mod tests {
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use windows::Win32::Foundation::GlobalFree;
use windows::Win32::System::Com::{
IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL,
};
use windows::Win32::System::Ole::{ReleaseStgMedium, CF_HDROP};
use windows::Win32::UI::Shell::{DragQueryFileW, HDROP};
use super::*;
fn serial() -> std::sync::MutexGuard<'static, ()> {
crate::shell::menu::SHELL_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn hdrop_format() -> FORMATETC {
FORMATETC {
cfFormat: CF_HDROP.0,
ptd: std::ptr::null_mut(),
dwAspect: DVASPECT_CONTENT.0,
lindex: -1,
tymed: TYMED_HGLOBAL.0 as u32,
}
}
unsafe fn unpack_hdrop(hdrop: HDROP) -> Vec<PathBuf> {
unsafe {
let count = DragQueryFileW(hdrop, u32::MAX, None);
let mut paths = Vec::with_capacity(count as usize);
for index in 0..count {
let len = DragQueryFileW(hdrop, index, None);
if len == 0 {
continue;
}
let mut buffer = vec![0u16; len as usize + 1];
let written = DragQueryFileW(hdrop, index, Some(&mut buffer));
buffer.truncate(written as usize);
paths.push(PathBuf::from(std::ffi::OsString::from_wide(&buffer)));
}
paths
}
}
unsafe fn hdrop_paths(medium: &STGMEDIUM) -> Vec<PathBuf> {
if medium.tymed & (TYMED_HGLOBAL.0 as u32) == 0 {
return Vec::new();
}
let handle = unsafe { medium.u.hGlobal };
if handle.is_invalid() {
return Vec::new();
}
unsafe { unpack_hdrop(HDROP(handle.0)) }
}
#[test]
fn hdrop_pack_unpack_roundtrip() {
let _guard = serial();
let paths = vec![
PathBuf::from(r"C:\Program Files\有 空格 的目录\文件 名.txt"),
PathBuf::from(r"D:\开发\FolderDock\src\shell\drag.rs"),
PathBuf::from(r"E:\带 空格\子 目录\b"),
];
let handle = hglobal_from_bytes(&pack_hdrop(&paths)).expect("分配 HGLOBAL 失败");
let got = unsafe { unpack_hdrop(HDROP(handle.0)) };
let _ = unsafe { GlobalFree(Some(handle)) };
assert_eq!(got, paths);
}
#[test]
fn data_object_get_data_returns_hdrop_medium() {
let _guard = serial();
let paths = vec![
PathBuf::from(r"C:\项目 一\a b\文件.txt"),
PathBuf::from(r"C:\项目 二\two.dll"),
];
let object: IDataObject = DataObject {
payload: pack_hdrop(&paths),
preferred_effect: DROPEFFECT_COPY.0,
}.into();
let mut medium = unsafe { object.GetData(&hdrop_format()) }.expect("GetData(CF_HDROP) 应当成功");
assert_eq!(medium.tymed, TYMED_HGLOBAL.0 as u32);
assert!(!unsafe { medium.u.hGlobal }.is_invalid());
assert!(medium.pUnkForRelease.is_none());
let got = unsafe { hdrop_paths(&medium) };
assert_eq!(got, paths);
unsafe { ReleaseStgMedium(&mut medium) };
let wrong = FORMATETC {
cfFormat: 1,
ptd: std::ptr::null_mut(),
dwAspect: DVASPECT_CONTENT.0,
lindex: -1,
tymed: TYMED_HGLOBAL.0 as u32,
};
assert!(unsafe { object.GetData(&wrong) }.is_err(), "非 CF_HDROP 不应当给数据");
}
}
}
#[cfg(not(windows))]
mod platform {
use std::path::PathBuf;
use super::DragEffect;
pub(super) fn drag_out(
_hwnd: isize,
_paths: &[PathBuf],
_allow_move: bool,
) -> Result<DragEffect, String> {
Err("系统拖放只在 Windows 上实现".to_string())
}
}