Files
winit/winit-win32/src/dnd.rs
Olivier Goffart 37d8a503e4 Mark extensible enums in winit-core as #[non_exhaustive]
Event, input source, error, data transfer, native key, IME request,
scroll delta and fullscreen enums can now gain variants without a
breaking change. Backends and examples get wildcard arms where they
matched exhaustively; unknown IME requests are reported as
ImeRequestError::NotSupported and unknown fullscreen modes behave
like Fullscreen::Borderless(None).
2026-07-29 12:23:49 +02:00

1618 lines
59 KiB
Rust

use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::ffi::{OsStr, OsString, c_void};
use std::io;
use std::num::NonZeroU32;
use std::ops::{BitOr, ControlFlow};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::PathBuf;
use std::ptr::NonNull;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{self, AtomicI64, AtomicUsize, Ordering};
use dpi::PhysicalPosition;
use windows_sys::Win32::Foundation::{
DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, DRAGDROP_S_USEDEFAULTCURSORS, DV_E_FORMATETC, E_ABORT,
E_FAIL, E_NOINTERFACE, E_NOTIMPL, E_UNEXPECTED, GlobalFree, HGLOBAL, HWND,
OLE_E_ADVISENOTSUPPORTED, POINT, POINTL, S_FALSE, S_OK,
};
use windows_sys::Win32::Graphics::Gdi::ScreenToClient;
use windows_sys::Win32::System::Com::{
DVASPECT_CONTENT, FORMATETC, STGMEDIUM, TYMED_ENHMF, TYMED_FILE, TYMED_GDI, TYMED_HGLOBAL,
TYMED_MFPICT,
};
use windows_sys::Win32::System::DataExchange::RegisterClipboardFormatW;
use windows_sys::Win32::System::Memory::{
GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock,
};
use windows_sys::Win32::System::Ole::{
CF_HDROP, CF_UNICODETEXT, DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE,
OleDuplicateData, ReleaseStgMedium,
};
use windows_sys::Win32::System::SystemServices::{
MK_LBUTTON, MK_MBUTTON, MK_RBUTTON, MK_XBUTTON1, MK_XBUTTON2,
};
use windows_sys::Win32::UI::Shell::{DROPFILES, DragQueryFileW, HDROP};
use windows_sys::core::{BOOL, GUID, HRESULT, IID_IUnknown};
use winit_core::data_transfer::{
DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData,
};
use winit_core::event::WindowEvent;
use winit_core::event_loop::DndAction;
use winit_core::window::WindowId;
use crate::definitions::{
IDataObject, IDataObjectVtbl, IDropSource, IDropSourceVtbl, IDropTarget, IDropTargetHelper,
IDropTargetHelperVtbl, IDropTargetVtbl, IEnumFORMATETC, IEnumFORMATETCVtbl, IID_IDataObject,
IID_IDropSource, IID_IDropTargetHelper, IID_IEnumFORMATETC, IUnknown, IUnknownVtbl,
};
use crate::event_loop::EventLoopRunner;
use crate::util;
// TODO: Exposing the full native API to client applications is too error-prone so long as
// winit is still manually implementing refcounting and using the win32 APIs. For now, we
// just eagerly read all the data supported by cross-platform type hints on Windows. This
// would be resolved by migrating to `windows-rs`.
#[derive(Debug)]
pub(crate) struct DataObject {
data: HashMap<TypeHint, SendData>,
}
impl DataObject {
unsafe fn from_idataobject(data_obj: *const IDataObject) -> Self {
let mut data = HashMap::new();
if let Some(text) = unsafe { read_unicode_text(data_obj) } {
data.insert(TypeHint::Plaintext, SendData::String(text));
}
if let Some(uris) = unsafe { read_uri_list(data_obj) } {
if !uris.is_empty() {
data.insert(TypeHint::UriList, SendData::Uris(uris));
}
}
if let Some(png) = unsafe { read_png(data_obj) } {
if !png.is_empty() {
data.insert(TypeHint::Image { extension_hint: Some("png") }, SendData::Bytes(png));
}
}
Self { data }
}
fn resolve(&self, requested: TypeHint) -> Option<TypeHint> {
self.data.keys().copied().find(|stored| stored.matches(&requested))
}
}
/// RAII wrapper around an STGMEDIUM returned by IDataObject::GetData, releasing it on drop.
struct StgMedium(STGMEDIUM);
impl StgMedium {
/// Returns `None` if the object doesn't provide the format.
unsafe fn get(data_obj: *const IDataObject, cf_format: u16) -> Option<Self> {
let format = FORMATETC {
cfFormat: cf_format,
ptd: std::ptr::null_mut(),
dwAspect: DVASPECT_CONTENT,
lindex: -1,
tymed: TYMED_HGLOBAL as u32,
};
let mut medium = unsafe { std::mem::zeroed::<STGMEDIUM>() };
let get_data = unsafe { (*(*data_obj).cast::<IDataObjectVtbl>()).GetData };
if unsafe { get_data(data_obj as *mut _, &format, &mut medium) } < 0 {
return None;
}
Some(Self(medium))
}
fn hglobal(&self) -> HGLOBAL {
unsafe { self.0.u.hGlobal }
}
}
impl Drop for StgMedium {
fn drop(&mut self) {
unsafe { ReleaseStgMedium(&mut self.0) };
}
}
unsafe fn read_unicode_text(data_obj: *const IDataObject) -> Option<String> {
let medium = unsafe { StgMedium::get(data_obj, CF_UNICODETEXT) }?;
let hglobal = medium.hglobal();
let ptr = unsafe { GlobalLock(hglobal) };
if ptr.is_null() {
return None;
}
// `CF_UNICODETEXT` is a NUL-terminated UTF-16 string. Cap the scan by the allocation size in
// case the buffer isn't terminated.
let max_units = unsafe { GlobalSize(hglobal) } / std::mem::size_of::<u16>();
let wide = unsafe { std::slice::from_raw_parts(ptr.cast::<u16>(), max_units) };
let len = wide.iter().position(|&c| c == 0).unwrap_or(max_units);
let text = String::from_utf16_lossy(&wide[..len]);
unsafe { GlobalUnlock(hglobal) };
Some(text)
}
unsafe fn read_uri_list(data_obj: *const IDataObject) -> Option<Vec<String>> {
let medium = unsafe { StgMedium::get(data_obj, CF_HDROP) }?;
let hdrop = medium.hglobal() as HDROP;
// The second parameter (0xFFFFFFFF) instructs the function to return the item count.
let item_count = unsafe { DragQueryFileW(hdrop, 0xffff_ffff, std::ptr::null_mut(), 0) };
let mut paths = Vec::with_capacity(item_count as usize);
for i in 0..item_count {
// Query the path length (excluding the NUL terminator), reserve room for it plus the
// terminator, then copy. `set_len` uses the count actually written, so a short copy can
// never expose uninitialized memory.
let character_count = unsafe { DragQueryFileW(hdrop, i, std::ptr::null_mut(), 0) } as usize;
let mut path_buf = Vec::<u16>::with_capacity(character_count + 1);
let copied =
unsafe { DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), character_count as u32 + 1) }
as usize;
unsafe { path_buf.set_len(copied) };
let path = PathBuf::from(OsString::from_wide(&path_buf));
paths.push(
url::Url::from_file_path(&path)
.map(String::from)
.unwrap_or_else(|_| path.to_string_lossy().into_owned()),
);
}
Some(paths)
}
unsafe fn read_png(data_obj: *const IDataObject) -> Option<Vec<u8>> {
let format_name = util::encode_wide("PNG");
let format = unsafe { RegisterClipboardFormatW(format_name.as_ptr()) };
if format == 0 {
return None;
}
let medium = unsafe { StgMedium::get(data_obj, format as u16) }?;
let hglobal = medium.hglobal();
let ptr = unsafe { GlobalLock(hglobal) };
if ptr.is_null() {
return None;
}
let len = unsafe { GlobalSize(hglobal) };
let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) }.to_vec();
unsafe { GlobalUnlock(hglobal) };
Some(bytes)
}
#[derive(Debug)]
pub(crate) struct WinDataTransfer {
data: Arc<DataObject>,
}
impl WinDataTransfer {
pub(crate) fn new(data: Arc<DataObject>) -> Self {
Self { data }
}
}
impl DataTransfer for WinDataTransfer {
fn for_each_available_type<'this>(
&'this self,
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>,
) {
for hint in self.data.data.keys() {
if let ControlFlow::Break(()) = func(hint) {
break;
}
}
}
}
#[derive(Debug)]
pub(crate) struct WinTypedData {
type_: TypeHint,
data: Arc<DataObject>,
}
impl WinTypedData {
pub(crate) fn new(data: Arc<DataObject>, requested: TypeHint) -> Option<Self> {
let type_ = data.resolve(requested)?;
Some(Self { type_, data })
}
}
impl TypedData for WinTypedData {
fn type_(&self) -> &dyn TransferType {
&self.type_
}
fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
match self.data.data.get(&self.type_)? {
SendData::Bytes(bytes) => Some(Box::new(io::Cursor::new(bytes.clone()))),
SendData::String(string) => {
Some(Box::new(io::Cursor::new(string.clone().into_bytes())))
},
// Windows URI drag-and-drop can't be neatly expressed as a binary blob.
SendData::Uris(_) => None,
_ => None,
}
}
fn try_as_uris(&self) -> io::Result<Vec<String>> {
match self.data.data.get(&self.type_) {
Some(SendData::Uris(uris)) => Ok(uris.clone()),
_ => Err(io::ErrorKind::InvalidData.into()),
}
}
fn try_as_string(&self) -> io::Result<String> {
match self.data.data.get(&self.type_) {
Some(SendData::String(string)) => Ok(string.clone()),
_ => Err(io::ErrorKind::InvalidData.into()),
}
}
}
#[repr(C)]
pub struct FileDropHandlerData {
interface: IDropTarget,
refcount: AtomicUsize,
window: HWND,
runner: Rc<EventLoopRunner>,
send_event: Box<dyn Fn(WindowEvent)>,
active_data_transfer_id: Option<DataTransferId>,
// Shell drop-target helper. Lazy-init on first DragEnter; `None` means "not yet created" or
// "creation failed and we're running without a drag image". Forwarding to this is what
// makes the source's `IDragSourceHelper` bitmap actually render under the cursor over our
// own window and any other helper-aware target.
drop_target_helper: Option<NonNull<IDropTargetHelper>>,
}
pub struct FileDropHandler {
data: *mut FileDropHandlerData,
}
#[allow(non_snake_case)]
impl FileDropHandler {
pub(crate) fn new(
window: HWND,
runner: Rc<EventLoopRunner>,
send_event: Box<dyn Fn(WindowEvent)>,
) -> FileDropHandler {
let data = Box::new(FileDropHandlerData {
interface: IDropTarget { lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl },
refcount: AtomicUsize::new(1),
window,
runner,
send_event,
active_data_transfer_id: None,
drop_target_helper: None,
});
FileDropHandler { data: Box::into_raw(data) }
}
/// Lazy-create the shell drop-target helper. Returns `None` if creation failed; callers
/// should treat that as "no drag image" and continue silently - failure is purely cosmetic.
unsafe fn ensure_drop_target_helper(
data: &mut FileDropHandlerData,
) -> Option<NonNull<IDropTargetHelper>> {
use windows_sys::Win32::System::Com::{CLSCTX_ALL, CoCreateInstance};
use windows_sys::Win32::UI::Shell::CLSID_DragDropHelper;
if let Some(helper) = data.drop_target_helper {
return Some(helper);
}
let mut helper: *mut IDropTargetHelper = std::ptr::null_mut();
let hr = unsafe {
CoCreateInstance(
&CLSID_DragDropHelper,
std::ptr::null_mut(),
CLSCTX_ALL,
&IID_IDropTargetHelper,
&mut helper as *mut _ as *mut _,
)
};
if hr < 0 {
return None;
}
let helper = NonNull::new(helper)?;
data.drop_target_helper = Some(helper);
Some(helper)
}
unsafe fn helper_vtbl(helper: NonNull<IDropTargetHelper>) -> &'static IDropTargetHelperVtbl {
unsafe { &*(*(helper.as_ptr() as *mut *const IDropTargetHelperVtbl)) }
}
pub(crate) unsafe fn interface_unchecked_mut(&mut self) -> &mut IDropTarget {
unsafe { &mut (*self.data).interface }
}
// Implement IUnknown
unsafe extern "system" fn QueryInterface(
_this: *mut IUnknown,
_riid: *const GUID,
_ppvObject: *mut *mut c_void,
) -> HRESULT {
// This function doesn't appear to be required for an `IDropTarget`.
// An implementation would be nice however.
// Can't use `unimplemented` here as it's invalid to panic over an FFI boundary.
tracing::warn!("`QueryInterface` called, but it was unimplemented");
E_FAIL
}
unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 {
let drop_handler_data = unsafe { Self::from_interface(this) };
let count = drop_handler_data.refcount.fetch_add(1, Ordering::Relaxed) + 1;
count as u32
}
unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 {
let drop_handler = unsafe { Self::from_interface(this) };
// Release on decrement publishes any writes made through this reference before the
// count is observed by other threads. When we hit zero, fence with Acquire so the
// destructor sees all writes from prior Releases on other threads - the standard
// Arc pattern. Without the fence, dropping the box could race with reads done by
// the last releasing thread on another core.
let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1;
if count == 0 {
atomic::fence(Ordering::Acquire);
// Drop any transfer still in flight (e.g. the window was destroyed mid-drag, so no
// `DragLeave`/`Drop` ever arrived to clean it up).
if let Some(id) = drop_handler.active_data_transfer_id.take() {
drop_handler.runner.remove_data_transfer(id);
}
// Release the shell drop-target helper if we created one.
if let Some(helper) = drop_handler.drop_target_helper {
let vtbl = unsafe { Self::helper_vtbl(helper) };
unsafe {
(vtbl.parent.Release)(helper.as_ptr() as *mut IUnknown);
}
}
// Destroy the underlying data
drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) });
}
count as u32
}
unsafe extern "system" fn DragEnter(
this: *mut IDropTarget,
pDataObj: *const IDataObject,
grfKeyState: u32,
pt: POINTL,
pdwEffect: *mut u32,
) -> HRESULT {
let drop_handler = unsafe { Self::from_interface(this) };
// If this is a self-drop (we initiated the drag from this process), reuse the source's id
// and seed actions from the mask declared at `start_drag` - the app's `DragEntered`
// handler can't call `set_valid_actions` in time because it's buffered until `DoDragDrop`
// returns.
let data_transfer_id = drop_handler
.runner
.source_drag
.get()
.map_or_else(next_data_transfer_id, |info| info.id);
drop_handler.active_data_transfer_id = Some(data_transfer_id);
let wid = WindowId::from_raw(drop_handler.window.addr());
let data = Arc::new(unsafe { DataObject::from_idataobject(pDataObj) });
drop_handler.runner.register_data_transfer(data_transfer_id, wid, data);
let pt_screen = POINT { x: pt.x, y: pt.y };
let mut pt_client = pt_screen;
unsafe {
ScreenToClient(drop_handler.window, &mut pt_client);
}
let position = PhysicalPosition::new(pt_client.x as f64, pt_client.y as f64);
(drop_handler.send_event)(WindowEvent::DragEntered {
id: data_transfer_id,
position: Some(position),
});
// Get actions after the event handler has run, so that we update it based on the user's
// supplied info.
{
let actions = drop_handler.runner.current_drag_actions(data_transfer_id);
let source_allowed = unsafe { pdwEffect.read() };
let new_effect = pick_effect(&actions, grfKeyState, source_allowed);
unsafe {
pdwEffect.write(new_effect);
}
}
// Forward to the shell drop-target helper so any drag image attached by the source's
// IDragSourceHelper renders the bitmap under the cursor while it's over our window.
if let Some(helper) = unsafe { Self::ensure_drop_target_helper(drop_handler) } {
let vtbl = unsafe { Self::helper_vtbl(helper) };
unsafe {
(vtbl.DragEnter)(
helper.as_ptr(),
drop_handler.window,
pDataObj as *mut IDataObject,
&pt_screen,
*pdwEffect,
);
}
}
S_OK
}
unsafe extern "system" fn DragOver(
this: *mut IDropTarget,
grfKeyState: u32,
pt: POINTL,
pdwEffect: *mut u32,
) -> HRESULT {
let drop_handler = unsafe { Self::from_interface(this) };
let Some(data_transfer_id) = drop_handler.active_data_transfer_id else {
unsafe {
pdwEffect.write(DROPEFFECT_NONE);
}
return E_ABORT;
};
let effects = unsafe { pdwEffect.read() };
let pt_screen = POINT { x: pt.x, y: pt.y };
let mut pt_client = pt_screen;
unsafe {
ScreenToClient(drop_handler.window, &mut pt_client);
}
let position = PhysicalPosition::new(pt_client.x as f64, pt_client.y as f64);
let proposed_action = drop_handler.runner.proposed_dnd_action(data_transfer_id, effects);
(drop_handler.send_event)(WindowEvent::DragPosition {
id: data_transfer_id,
position,
proposed_action,
});
// Get actions after the event handler has run, so that we update it based on the user's
// supplied info.
let actions = drop_handler.runner.current_drag_actions(data_transfer_id);
let new_effect = pick_effect(&actions, grfKeyState, effects);
unsafe {
pdwEffect.write(new_effect);
}
if let Some(helper) = drop_handler.drop_target_helper {
let vtbl = unsafe { Self::helper_vtbl(helper) };
unsafe { (vtbl.DragOver)(helper.as_ptr(), &pt_screen, new_effect) };
}
S_OK
}
unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT {
let drop_handler = unsafe { Self::from_interface(this) };
let Some(data_transfer_id) = drop_handler.active_data_transfer_id.take() else {
return E_ABORT;
};
(drop_handler.send_event)(WindowEvent::DragLeft { id: data_transfer_id });
drop_handler.runner.remove_data_transfer(data_transfer_id);
if let Some(helper) = drop_handler.drop_target_helper {
let vtbl = unsafe { Self::helper_vtbl(helper) };
unsafe { (vtbl.DragLeave)(helper.as_ptr()) };
}
S_OK
}
unsafe extern "system" fn Drop(
this: *mut IDropTarget,
pDataObj: *const IDataObject,
grfKeyState: u32,
pt: POINTL,
pdwEffect: *mut u32,
) -> HRESULT {
let drop_handler = unsafe { Self::from_interface(this) };
let Some(data_transfer_id) = drop_handler.active_data_transfer_id.take() else {
unsafe {
*pdwEffect = DROPEFFECT_NONE;
}
return E_ABORT;
};
let effects = unsafe { pdwEffect.read() };
let proposed_action = drop_handler.runner.proposed_dnd_action(data_transfer_id, effects);
let pt_screen = POINT { x: pt.x, y: pt.y };
let mut pt_client = pt_screen;
unsafe {
ScreenToClient(drop_handler.window, &mut pt_client);
}
let pt = pt_client;
let position = PhysicalPosition::new(pt.x as f64, pt.y as f64);
(drop_handler.send_event)(WindowEvent::DragPosition {
id: data_transfer_id,
position,
proposed_action,
});
// New scope to make sure that the `Ref` returned by `current_drag_actions` is dropped
// before we call `remove_data_transfer`.
{
// Get actions after the event handler has run, so that we update it based on the user's
// supplied info.
let actions = drop_handler.runner.current_drag_actions(data_transfer_id);
let source_allowed = unsafe { pdwEffect.read() };
let proposed_action =
drop_handler.runner.proposed_dnd_action(data_transfer_id, effects);
// Negotiate the effect first so we can pick the right outgoing event. If the app
// rejected the drop (e.g. via `set_valid_actions(none())`), `pick_effect` returns
// `DROPEFFECT_NONE`; in that case OLE reports back `effect_out == DROPEFFECT_NONE` to
// the source, so the matching observer-facing event is `DragLeft`, not `DragDropped` -
// otherwise target and source see contradictory outcomes.
let effect = pick_effect(&actions, grfKeyState, source_allowed);
let event = if effect == DROPEFFECT_NONE {
WindowEvent::DragLeft { id: data_transfer_id }
} else {
WindowEvent::DragDropped { id: data_transfer_id, proposed_action }
};
(drop_handler.send_event)(event);
unsafe {
*pdwEffect = effect;
}
if let Some(helper) = drop_handler.drop_target_helper {
let vtbl = unsafe { Self::helper_vtbl(helper) };
unsafe {
(vtbl.Drop)(helper.as_ptr(), pDataObj as *mut IDataObject, &pt_screen, effect);
}
}
}
// External drop: the app's handler dispatched synchronously above and has already read
// the data; safe to release the cache. Self-drop: the handler is buffered and hasn't
// run yet, so defer cleanup until after `dispatch_buffered_events` drains.
if drop_handler.runner.source_drag.get().is_some() {
drop_handler.runner.defer_source_drag_cleanup(data_transfer_id);
} else {
drop_handler.runner.remove_data_transfer(data_transfer_id);
}
S_OK
}
unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut FileDropHandlerData {
unsafe { &mut *(this as *mut _) }
}
}
impl Drop for FileDropHandler {
fn drop(&mut self) {
unsafe {
FileDropHandler::Release(self.data as *mut IUnknown);
}
}
}
static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl {
parent: IUnknownVtbl {
QueryInterface: FileDropHandler::QueryInterface,
AddRef: FileDropHandler::AddRef,
Release: FileDropHandler::Release,
},
DragEnter: FileDropHandler::DragEnter,
DragOver: FileDropHandler::DragOver,
DragLeave: FileDropHandler::DragLeave,
Drop: FileDropHandler::Drop,
};
pub(crate) type DropEffect = u32;
/// Map the app's [`DndActions`] to the win32 `DROPEFFECT_*` bitmask.
pub(crate) fn dnd_action_to_dropeffect_mask(action: DndAction) -> DropEffect {
match action {
DndAction::Move => DROPEFFECT_MOVE,
DndAction::Copy => DROPEFFECT_COPY,
DndAction::Link => DROPEFFECT_LINK,
_ => DROPEFFECT_NONE,
}
}
/// Map the app's [`DndActions`] to the win32 `DROPEFFECT_*` bitmask.
pub(crate) fn dnd_actions_to_dropeffect_mask(actions: &[DndAction]) -> DropEffect {
actions
.iter()
.copied()
.map(dnd_action_to_dropeffect_mask)
.fold(DropEffect::default(), BitOr::bitor)
}
pub(crate) fn drop_effect_to_dnd_action(effect: DropEffect) -> Option<DndAction> {
match effect {
DROPEFFECT_MOVE => Some(DndAction::Move),
DROPEFFECT_COPY => Some(DndAction::Copy),
DROPEFFECT_LINK => Some(DndAction::Link),
_ => None,
}
}
// Intersect the app's valid actions with the source's allowed effects, honoring Ctrl/Shift.
fn pick_effect(actions: &[DndAction], key_state: u32, source_allowed: u32) -> u32 {
const MK_SHIFT: u32 = 0x0004;
const MK_CONTROL: u32 = 0x0008;
// TODO: On macOS, option will disable copy (so if copy has higher precedence than move you can
// select move via option). Is alt expected to act similarly on Windows?
let mut allowed = dnd_actions_to_dropeffect_mask(actions) & source_allowed;
if allowed == 0 {
return DROPEFFECT_NONE;
}
// If holding a modifier would result in no valid values, ignore
// Need to use filter instead of if-let chains for 1.86 compatibility.
if let Some(new_allowed) =
NonZeroU32::new(allowed & !DROPEFFECT_MOVE).filter(|_| key_state & MK_CONTROL != 0)
{
allowed = new_allowed.get();
}
// If holding a modifier would result in no valid values, ignore
// Need to use filter instead of if-let chains for 1.86 compatibility.
if let Some(new_allowed) =
NonZeroU32::new(allowed & !DROPEFFECT_COPY).filter(|_| key_state & MK_SHIFT != 0)
{
allowed = new_allowed.get();
}
for action in actions {
let effect = dnd_action_to_dropeffect_mask(*action);
if (allowed & effect) != 0 {
return effect;
}
}
DROPEFFECT_NONE
}
// ============================================================================
// Source side: providing data and controlling a `DoDragDrop` session.
// ============================================================================
/// Mint a unique [`DataTransferId`] for either a target-side `DragEnter` or a source-side
/// `start_drag` so the two never collide.
pub(crate) fn next_data_transfer_id() -> DataTransferId {
static COUNTER: AtomicI64 = AtomicI64::new(0);
DataTransferId::from_raw(COUNTER.fetch_add(1, Ordering::Relaxed))
}
fn guids_eq(a: &GUID, b: &GUID) -> bool {
a.data1 == b.data1 && a.data2 == b.data2 && a.data3 == b.data3 && a.data4 == b.data4
}
fn register_clipboard_format(name: &str) -> Option<u16> {
let wide = util::encode_wide(name);
let atom = unsafe { RegisterClipboardFormatW(wide.as_ptr()) };
(atom != 0).then_some(atom as u16)
}
// Returns the (cf_format, specialised hint) pairs we can lower `hint` to.
//
// Most hints map 1:1. Wildcard hints like `Image { extension_hint: None }` -
// the cross-platform way to say "any image format" - fan out to one entry per
// concrete format we support, each paired with the specialised hint so
// `data_for_type` is invoked with a concrete extension instead of `None`.
fn cf_formats_for_hint(hint: TypeHint) -> Vec<(u16, TypeHint)> {
fn one(cf: Option<u16>, hint: TypeHint) -> Vec<(u16, TypeHint)> {
cf.map(|cf| vec![(cf, hint)]).unwrap_or_default()
}
match hint {
TypeHint::Plaintext => vec![(CF_UNICODETEXT, hint)],
TypeHint::UriList => vec![(CF_HDROP, hint)],
TypeHint::Html => one(register_clipboard_format("HTML Format"), hint),
TypeHint::Image { extension_hint: Some("png") } => {
one(register_clipboard_format("PNG"), hint)
},
TypeHint::Image { extension_hint: None } => {
// Fan out to every concrete image format we can produce.
let mut out = Vec::new();
if let Some(cf) = register_clipboard_format("PNG") {
out.push((cf, TypeHint::Image { extension_hint: Some("png") }));
}
out
},
_ => Vec::new(),
}
}
/// Duplicate a `STGMEDIUM` for handing out via `GetData` without losing the original.
///
/// Delegates to `OleDuplicateData` which knows how to clone HGLOBAL, HBITMAP, HENHMETAFILE,
/// HMETAFILEPICT and file-name mediums. Returns `None` for tymeds the shell helper doesn't use
/// (interface-based mediums like IStream / IStorage) so we never hand out an aliased pointer
/// the caller would later `Release` once we also drop ours.
unsafe fn duplicate_stgmedium(src: &STGMEDIUM, cf_format: u16) -> Option<STGMEDIUM> {
use windows_sys::Win32::Foundation::HANDLE;
// Pull the handle out of the union by tymed. The shell drag helper only uses HGLOBAL today,
// but forwarding every handle-typed tymed `OleDuplicateData` understands is essentially
// free, so do it. Interface-based tymeds (IStream / IStorage) deliberately fall through -
// duplicating them properly requires AddRef, not OleDuplicateData.
let tymed = src.tymed as i32;
let handle: HANDLE = unsafe {
if tymed == TYMED_HGLOBAL {
src.u.hGlobal as HANDLE
} else if tymed == TYMED_FILE {
src.u.lpszFileName as HANDLE
} else if tymed == TYMED_GDI {
src.u.hBitmap as HANDLE
} else if tymed == TYMED_MFPICT {
src.u.hMetaFilePict as HANDLE
} else if tymed == TYMED_ENHMF {
src.u.hEnhMetaFile as HANDLE
} else {
return None;
}
};
let dup = unsafe { OleDuplicateData(handle, cf_format, 0) };
if dup.is_null() {
return None;
}
let mut out: STGMEDIUM = unsafe { std::mem::zeroed() };
out.tymed = src.tymed;
if tymed == TYMED_HGLOBAL {
out.u.hGlobal = dup as _;
} else if tymed == TYMED_FILE {
out.u.lpszFileName = dup as _;
} else if tymed == TYMED_GDI {
out.u.hBitmap = dup as _;
} else if tymed == TYMED_MFPICT {
out.u.hMetaFilePict = dup as _;
} else if tymed == TYMED_ENHMF {
out.u.hEnhMetaFile = dup as _;
} else {
// Should be unreachable - the same `tymed` matched the first chain above. Return
// `None` rather than panic: this runs across the COM/FFI boundary, where unwinding
// would be UB.
return None;
}
Some(out)
}
fn alloc_hglobal_from(src: &[u8]) -> Option<HGLOBAL> {
let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, src.len()) };
if hglobal.is_null() {
return None;
}
let dst = unsafe { GlobalLock(hglobal) };
if dst.is_null() {
// `GlobalAlloc` succeeded but locking failed - free before bailing so we don't
// leak the moveable handle.
unsafe { GlobalFree(hglobal) };
return None;
}
unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), dst as *mut u8, src.len()) };
unsafe { GlobalUnlock(hglobal) };
Some(hglobal)
}
/// Build the [HTML Clipboard Format] wire bytes from an app-supplied HTML string.
///
/// The format is a UTF-8 buffer with a small text header naming byte offsets into itself. Each
/// offset placeholder is exactly 10 zero-padded decimal digits, which fixes the header at a
/// known constant length and removes the chicken-and-egg between header length and offsets.
///
/// If the input already contains `<!--StartFragment-->` we trust the caller's wrapping;
/// otherwise we wrap it in a minimal `<html><body>` document with fragment markers.
///
/// [HTML Clipboard Format]: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
fn build_html_clipboard_format(html: &str) -> Vec<u8> {
use std::borrow::Cow;
const START_MARKER: &str = "<!--StartFragment-->";
const END_MARKER: &str = "<!--EndFragment-->";
let body: Cow<'_, str> = if html.contains(START_MARKER) {
Cow::Borrowed(html)
} else {
Cow::Owned(format!("<html><body>\r\n{START_MARKER}{html}{END_MARKER}\r\n</body></html>"))
};
const HEADER_LEN: usize = concat!(
"Version:0.9\r\n",
"StartHTML:0000000000\r\n",
"EndHTML:0000000000\r\n",
"StartFragment:0000000000\r\n",
"EndFragment:0000000000\r\n",
)
.len();
let start_html = HEADER_LEN;
let end_html = HEADER_LEN + body.len();
let start_fragment = HEADER_LEN + body.find(START_MARKER).unwrap() + START_MARKER.len();
let end_fragment = HEADER_LEN + body.find(END_MARKER).unwrap();
let header = format!(
"Version:0.9\r\nStartHTML:{start_html:010}\r\nEndHTML:{end_html:010}\r\nStartFragment:\
{start_fragment:010}\r\nEndFragment:{end_fragment:010}\r\n",
);
debug_assert_eq!(header.len(), HEADER_LEN);
let mut buf = Vec::with_capacity(header.len() + body.len());
buf.extend_from_slice(header.as_bytes());
buf.extend_from_slice(body.as_bytes());
buf
}
/// Convert app-supplied [`SendData`] into an `HGLOBAL`-backed [`STGMEDIUM`].
///
/// Callers must have already validated that the `SendData` variant matches the on-the-wire shape
/// the requested clipboard format expects (see `variant_matches_hint` at the `GetData` call site).
unsafe fn send_data_to_stgmedium(data: SendData, hint: TypeHint) -> Option<STGMEDIUM> {
let hglobal = match data {
SendData::String(s) if matches!(hint, TypeHint::Html) => {
// HTML Clipboard Format: UTF-8 with a Version/StartHTML/EndHTML/StartFragment/
// EndFragment header. Targets parse the header before reading the wrapped HTML.
let bytes = build_html_clipboard_format(&s);
alloc_hglobal_from(&bytes)?
},
SendData::String(s) => {
// UTF-16 + NUL - used for `CF_UNICODETEXT` and other text-ish registered formats.
let utf16 = util::encode_wide(&s);
let utf16_bytes =
unsafe { std::slice::from_raw_parts(utf16.as_ptr() as *const u8, utf16.len() * 2) };
alloc_hglobal_from(utf16_bytes)?
},
SendData::Uris(paths) => {
// CF_HDROP: `DROPFILES` header + double-NUL-terminated UTF-16 path list.
let mut wide: Vec<u16> = Vec::new();
for path in paths {
let path_from_uri_owned;
let encoded = if let Some(path_from_uri) =
url::Url::parse(&path).ok().and_then(|url| url.to_file_path().ok())
{
path_from_uri_owned = path_from_uri;
OsStr::new(&path_from_uri_owned).encode_wide()
} else {
OsStr::new(&path).encode_wide()
};
wide.extend(encoded);
wide.push(0);
}
wide.push(0);
let header = DROPFILES {
pFiles: std::mem::size_of::<DROPFILES>() as u32,
pt: POINT { x: 0, y: 0 },
fNC: 0,
fWide: 1,
};
let total = std::mem::size_of::<DROPFILES>() + wide.len() * 2;
let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, total) };
if hglobal.is_null() {
return None;
}
let dst = unsafe { GlobalLock(hglobal) };
if dst.is_null() {
unsafe { GlobalFree(hglobal) };
return None;
}
unsafe {
std::ptr::write_unaligned(dst as *mut DROPFILES, header);
let paths_dst = (dst as *mut u8).add(std::mem::size_of::<DROPFILES>());
std::ptr::copy_nonoverlapping(
wide.as_ptr() as *const u8,
paths_dst,
wide.len() * 2,
);
GlobalUnlock(hglobal);
}
hglobal
},
SendData::Bytes(b) => alloc_hglobal_from(&b)?,
_ => return None,
};
let mut medium = unsafe { std::mem::zeroed::<STGMEDIUM>() };
medium.tymed = TYMED_HGLOBAL as u32;
medium.u.hGlobal = hglobal;
Some(medium)
}
/// True if the `SendData` variant carries the on-the-wire shape OLE expects for `hint`'s mapped
/// clipboard format. Mismatches (e.g. `SendData::String` for a `UriList` hint) would otherwise
/// produce malformed `HGLOBAL` payloads (e.g. UTF-16 text mislabeled as `CF_HDROP`), which the
/// target would parse as wild offsets - memory corruption in the receiving process.
fn variant_matches_hint(hint: TypeHint, data: &SendData) -> bool {
matches!(
(hint, data),
(TypeHint::UriList, SendData::Uris(_))
| (TypeHint::Plaintext | TypeHint::Html | TypeHint::Rtf, SendData::String(_))
| (TypeHint::Image { .. } | TypeHint::Audio { .. }, SendData::Bytes(_))
)
}
// ---- IEnumFORMATETC: a cursor over a precomputed `Vec<FORMATETC>`. -----------
#[repr(C)]
#[allow(non_snake_case)]
struct IEnumFORMATETCInterface {
lpVtbl: *const IEnumFORMATETCVtbl,
}
/// Generates the shared `IUnknown` boilerplate for our hand-rolled COM source-side objects.
///
/// Each such object is a `Box`-allocated `#[repr(C)]` struct whose first field is its COM
/// interface and which carries an `AtomicUsize` `refcount`. The thunks are identical across
/// objects apart from the concrete type and the one extra IID accepted by `QueryInterface`, so
/// the subtle refcount memory ordering lives in exactly one place.
macro_rules! com_iunknown_impl {
($ty:ty, $extra_iid:expr) => {
#[allow(non_snake_case)]
impl $ty {
unsafe fn from_interface<'a, I>(this: *mut I) -> &'a mut Self {
unsafe { &mut *(this as *mut _) }
}
unsafe extern "system" fn QueryInterface(
this: *mut IUnknown,
riid: *const GUID,
ppv: *mut *mut c_void,
) -> HRESULT {
let riid = unsafe { &*riid };
if guids_eq(riid, &IID_IUnknown) || guids_eq(riid, $extra_iid) {
unsafe { *ppv = this as *mut c_void };
unsafe { Self::AddRef(this) };
S_OK
} else {
unsafe { *ppv = std::ptr::null_mut() };
E_NOINTERFACE
}
}
unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 {
let me = unsafe { Self::from_interface(this) };
// Mere refcount bump - Relaxed is enough; the caller already holds a synchronized
// reference to the object.
me.refcount.fetch_add(1, Ordering::Relaxed) as u32 + 1
}
unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 {
let me = unsafe { Self::from_interface(this) };
// Release on decrement publishes any writes made through this reference before the
// count is observed by other threads. When we hit zero, fence with Acquire so the
// destructor sees all writes from prior Releases on other threads - the standard
// Arc pattern. Without the fence, dropping the box could race with reads done by
// the last releasing thread on another core.
let count = me.refcount.fetch_sub(1, Ordering::Release) - 1;
if count == 0 {
atomic::fence(Ordering::Acquire);
drop(unsafe { Box::from_raw(me as *mut Self) });
}
count as u32
}
}
};
}
#[repr(C)]
struct SourceFormatEnumerator {
interface: IEnumFORMATETCInterface,
refcount: AtomicUsize,
formats: Vec<FORMATETC>,
cursor: Cell<usize>,
}
com_iunknown_impl!(SourceFormatEnumerator, &IID_IEnumFORMATETC);
#[allow(non_snake_case)]
impl SourceFormatEnumerator {
fn new_boxed(formats: Vec<FORMATETC>) -> *mut Self {
Box::into_raw(Box::new(Self {
interface: IEnumFORMATETCInterface {
lpVtbl: &SOURCE_ENUM_FORMATETC_VTBL as *const IEnumFORMATETCVtbl,
},
refcount: AtomicUsize::new(1),
formats,
cursor: Cell::new(0),
}))
}
unsafe extern "system" fn Next(
this: *mut IEnumFORMATETC,
celt: u32,
rgelt: *mut FORMATETC,
pcelt_fetched: *mut u32,
) -> HRESULT {
let me = unsafe { Self::from_interface(this) };
let cursor = me.cursor.get();
let to_copy = (celt as usize).min(me.formats.len().saturating_sub(cursor));
for i in 0..to_copy {
unsafe { *rgelt.add(i) = me.formats[cursor + i] };
}
me.cursor.set(cursor + to_copy);
if !pcelt_fetched.is_null() {
unsafe { *pcelt_fetched = to_copy as u32 };
}
if to_copy < celt as usize { S_FALSE } else { S_OK }
}
unsafe extern "system" fn Skip(this: *mut IEnumFORMATETC, celt: u32) -> HRESULT {
let me = unsafe { Self::from_interface(this) };
let new_cursor = me.cursor.get().saturating_add(celt as usize);
if new_cursor > me.formats.len() {
me.cursor.set(me.formats.len());
S_FALSE
} else {
me.cursor.set(new_cursor);
S_OK
}
}
unsafe extern "system" fn Reset(this: *mut IEnumFORMATETC) -> HRESULT {
let me = unsafe { Self::from_interface(this) };
me.cursor.set(0);
S_OK
}
unsafe extern "system" fn Clone(
this: *mut IEnumFORMATETC,
ppenum: *mut *mut IEnumFORMATETC,
) -> HRESULT {
let me = unsafe { Self::from_interface(this) };
let cloned = SourceFormatEnumerator::new_boxed(me.formats.clone());
unsafe { (*cloned).cursor.set(me.cursor.get()) };
unsafe { *ppenum = cloned as *mut IEnumFORMATETC };
S_OK
}
}
static SOURCE_ENUM_FORMATETC_VTBL: IEnumFORMATETCVtbl = IEnumFORMATETCVtbl {
parent: IUnknownVtbl {
QueryInterface: SourceFormatEnumerator::QueryInterface,
AddRef: SourceFormatEnumerator::AddRef,
Release: SourceFormatEnumerator::Release,
},
Next: SourceFormatEnumerator::Next,
Skip: SourceFormatEnumerator::Skip,
Reset: SourceFormatEnumerator::Reset,
Clone: SourceFormatEnumerator::Clone,
};
// ---- IDataObject (source) ---------------------------------------------------
#[repr(C)]
#[allow(non_snake_case)]
struct IDataObjectInterface {
lpVtbl: *const IDataObjectVtbl,
}
/// Owning wrapper around a `STGMEDIUM` that releases the underlying handle on drop.
struct OwnedStgMedium(STGMEDIUM);
impl Drop for OwnedStgMedium {
fn drop(&mut self) {
unsafe { ReleaseStgMedium(&mut self.0) };
}
}
#[repr(C)]
struct SourceDataObjectData {
interface: IDataObjectInterface,
refcount: AtomicUsize,
send_data: RefCell<Box<dyn DataTransferSend>>,
// (cf_format, hint) pairs we advertise to the target.
formats: Vec<(u16, TypeHint)>,
// Formats injected via `SetData` - primarily by `IDragSourceHelper::InitializeFromBitmap`,
// which stores the drag image bits (CFSTR_DRAGIMAGEBITS) and related shell formats here so
// the target-side `IDropTargetHelper` can read them back via `GetData`.
extras: RefCell<Vec<(FORMATETC, OwnedStgMedium)>>,
}
com_iunknown_impl!(SourceDataObjectData, &IID_IDataObject);
#[allow(non_snake_case)]
impl SourceDataObjectData {
fn new_boxed(send_data: Box<dyn DataTransferSend>) -> *mut Self {
let mut formats: Vec<(u16, TypeHint)> = Vec::new();
send_data.for_each_available_type(&mut |ty| {
if let Some(hint) = ty.hint() {
for (cf, specialised) in cf_formats_for_hint(hint) {
if !formats.iter().any(|(c, _)| *c == cf) {
formats.push((cf, specialised));
}
}
}
ControlFlow::Continue(())
});
Box::into_raw(Box::new(Self {
interface: IDataObjectInterface {
lpVtbl: &SOURCE_DATA_OBJECT_VTBL as *const IDataObjectVtbl,
},
refcount: AtomicUsize::new(1),
send_data: RefCell::new(send_data),
formats,
extras: RefCell::new(Vec::new()),
}))
}
unsafe extern "system" fn GetData(
this: *mut IDataObject,
pformatetc_in: *const FORMATETC,
pmedium: *mut STGMEDIUM,
) -> HRESULT {
let me = unsafe { Self::from_interface(this) };
let format = unsafe { &*pformatetc_in };
if (format.tymed & TYMED_HGLOBAL as u32) == 0 {
return DV_E_FORMATETC;
}
// Shell-helper-injected formats live in `extras`; serve those first by duplicating the
// stored HGLOBAL so the caller can `ReleaseStgMedium` independently of our storage.
if let Ok(extras) = me.extras.try_borrow() {
if let Some((stored_fmt, stored)) =
extras.iter().find(|(f, _)| f.cfFormat == format.cfFormat)
{
if (stored_fmt.tymed & format.tymed) != 0 {
if let Some(dup) = unsafe { duplicate_stgmedium(&stored.0, format.cfFormat) } {
unsafe { *pmedium = dup };
return S_OK;
}
return E_FAIL;
}
}
}
let Some(&(_, hint)) = me.formats.iter().find(|&&(cf, _)| cf == format.cfFormat) else {
return DV_E_FORMATETC;
};
// `try_borrow_mut` rather than `borrow_mut`: `data_for_type` is the app's callback and
// may itself reach back into this `IDataObject`. A panic across the `extern "system"`
// boundary would be UB; return `E_UNEXPECTED` instead.
let data = {
let Ok(send_data) = me.send_data.try_borrow_mut() else {
return E_UNEXPECTED;
};
send_data.data_for_type(&hint)
};
let Some(data) = data else {
return DV_E_FORMATETC;
};
if !variant_matches_hint(hint, &data) {
return DV_E_FORMATETC;
}
let Some(medium) = (unsafe { send_data_to_stgmedium(data, hint) }) else {
return E_FAIL;
};
unsafe { *pmedium = medium };
S_OK
}
unsafe extern "system" fn GetDataHere(
_this: *mut IDataObject,
_pformatetc: *const FORMATETC,
_pmedium: *mut STGMEDIUM,
) -> HRESULT {
E_NOTIMPL
}
unsafe extern "system" fn QueryGetData(
this: *mut IDataObject,
pformatetc: *const FORMATETC,
) -> HRESULT {
let me = unsafe { Self::from_interface(this) };
let format = unsafe { &*pformatetc };
if (format.tymed & TYMED_HGLOBAL as u32) == 0 {
return DV_E_FORMATETC;
}
if me.formats.iter().any(|&(cf, _)| cf == format.cfFormat) {
return S_OK;
}
if let Ok(extras) = me.extras.try_borrow() {
if extras.iter().any(|(f, _)| f.cfFormat == format.cfFormat) {
return S_OK;
}
}
S_FALSE
}
unsafe extern "system" fn GetCanonicalFormatEtc(
_this: *mut IDataObject,
_pformatetc_in: *const FORMATETC,
_pformatetc_out: *mut FORMATETC,
) -> HRESULT {
E_NOTIMPL
}
unsafe extern "system" fn SetData(
this: *mut IDataObject,
pformatetc: *const FORMATETC,
pmedium: *const STGMEDIUM,
f_release: BOOL,
) -> HRESULT {
// Primary caller is `IDragSourceHelper::InitializeFromBitmap`, which attaches the drag
// image bits and related shell formats to our data object. We don't interpret them -
// just hold them so `GetData` can hand them back to `IDropTargetHelper`.
let me = unsafe { Self::from_interface(this) };
if pformatetc.is_null() || pmedium.is_null() {
return E_FAIL;
}
let format = unsafe { *pformatetc };
let medium = if f_release != 0 {
// We take ownership of the passed-in medium as-is.
unsafe { *pmedium }
} else {
// Caller retains ownership; we must duplicate.
let Some(dup) = (unsafe { duplicate_stgmedium(&*pmedium, format.cfFormat) }) else {
return E_FAIL;
};
dup
};
let Ok(mut extras) = me.extras.try_borrow_mut() else {
if f_release != 0 {
// We promised to take ownership but can't store - release immediately so we
// don't leak.
let mut m = medium;
unsafe { ReleaseStgMedium(&mut m) };
}
return E_UNEXPECTED;
};
// Replace any earlier entry with the same format - last-write-wins matches what real
// shell apps do and avoids growing the vec unboundedly on repeated SetData calls.
if let Some(slot) = extras.iter_mut().find(|(f, _)| f.cfFormat == format.cfFormat) {
slot.0 = format;
slot.1 = OwnedStgMedium(medium);
} else {
extras.push((format, OwnedStgMedium(medium)));
}
S_OK
}
unsafe extern "system" fn EnumFormatEtc(
this: *mut IDataObject,
dw_direction: u32,
ppenum: *mut *mut IEnumFORMATETC,
) -> HRESULT {
const DATADIR_GET: u32 = 1;
if dw_direction != DATADIR_GET {
return E_NOTIMPL;
}
let me = unsafe { Self::from_interface(this) };
let mut formats: Vec<FORMATETC> = me
.formats
.iter()
.map(|&(cf, _)| FORMATETC {
cfFormat: cf,
ptd: std::ptr::null_mut(),
dwAspect: DVASPECT_CONTENT,
lindex: -1,
tymed: TYMED_HGLOBAL as u32,
})
.collect();
if let Ok(extras) = me.extras.try_borrow() {
for (fmt, _) in extras.iter() {
if !formats.iter().any(|f| f.cfFormat == fmt.cfFormat) {
formats.push(*fmt);
}
}
}
let enumerator = SourceFormatEnumerator::new_boxed(formats);
unsafe { *ppenum = enumerator as *mut IEnumFORMATETC };
S_OK
}
unsafe extern "system" fn DAdvise(
_this: *mut IDataObject,
_pformatetc: *const FORMATETC,
_advf: u32,
_adv_sink: *const crate::definitions::IAdviseSink,
_pdw_connection: *mut u32,
) -> HRESULT {
OLE_E_ADVISENOTSUPPORTED
}
unsafe extern "system" fn DUnadvise(_this: *mut IDataObject, _connection: u32) -> HRESULT {
OLE_E_ADVISENOTSUPPORTED
}
unsafe extern "system" fn EnumDAdvise(
_this: *mut IDataObject,
_ppenum_advise: *const *const crate::definitions::IEnumSTATDATA,
) -> HRESULT {
OLE_E_ADVISENOTSUPPORTED
}
}
static SOURCE_DATA_OBJECT_VTBL: IDataObjectVtbl = IDataObjectVtbl {
parent: IUnknownVtbl {
QueryInterface: SourceDataObjectData::QueryInterface,
AddRef: SourceDataObjectData::AddRef,
Release: SourceDataObjectData::Release,
},
GetData: SourceDataObjectData::GetData,
GetDataHere: SourceDataObjectData::GetDataHere,
QueryGetData: SourceDataObjectData::QueryGetData,
GetCanonicalFormatEtc: SourceDataObjectData::GetCanonicalFormatEtc,
SetData: SourceDataObjectData::SetData,
EnumFormatEtc: SourceDataObjectData::EnumFormatEtc,
DAdvise: SourceDataObjectData::DAdvise,
DUnadvise: SourceDataObjectData::DUnadvise,
EnumDAdvise: SourceDataObjectData::EnumDAdvise,
};
pub(crate) struct SourceDataObject {
data: *mut SourceDataObjectData,
}
impl SourceDataObject {
pub(crate) fn new(send_data: Box<dyn DataTransferSend>) -> Self {
Self { data: SourceDataObjectData::new_boxed(send_data) }
}
pub(crate) fn interface_ptr(&self) -> *mut c_void {
self.data as *mut c_void
}
}
impl Drop for SourceDataObject {
fn drop(&mut self) {
unsafe { SourceDataObjectData::Release(self.data as *mut IUnknown) };
}
}
// ---- IDropSource ------------------------------------------------------------
#[repr(C)]
#[allow(non_snake_case)]
struct IDropSourceInterface {
lpVtbl: *const IDropSourceVtbl,
}
#[repr(C)]
struct DropSourceData {
interface: IDropSourceInterface,
refcount: AtomicUsize,
}
com_iunknown_impl!(DropSourceData, &IID_IDropSource);
#[allow(non_snake_case)]
impl DropSourceData {
fn new_boxed() -> *mut Self {
Box::into_raw(Box::new(Self {
interface: IDropSourceInterface { lpVtbl: &DROP_SOURCE_VTBL as *const IDropSourceVtbl },
refcount: AtomicUsize::new(1),
}))
}
unsafe extern "system" fn QueryContinueDrag(
_this: *mut IDropSource,
escape_pressed: BOOL,
grf_key_state: u32,
) -> HRESULT {
// Drop when no mouse button is pressed - matches Microsoft's documented
// `IDropSource` example and covers right-button / middle-button drags as
// well as the common left-button case. Hardcoding `MK_LBUTTON` would make
// a right-drag (e.g. context-menu drag) never terminate by mouse release.
const ANY_MOUSE_BUTTON: u32 =
MK_LBUTTON | MK_RBUTTON | MK_MBUTTON | MK_XBUTTON1 | MK_XBUTTON2;
if escape_pressed != 0 {
return DRAGDROP_S_CANCEL;
}
if (grf_key_state & ANY_MOUSE_BUTTON) == 0 {
return DRAGDROP_S_DROP;
}
S_OK
}
unsafe extern "system" fn GiveFeedback(_this: *mut IDropSource, _dw_effect: u32) -> HRESULT {
DRAGDROP_S_USEDEFAULTCURSORS
}
}
static DROP_SOURCE_VTBL: IDropSourceVtbl = IDropSourceVtbl {
parent: IUnknownVtbl {
QueryInterface: DropSourceData::QueryInterface,
AddRef: DropSourceData::AddRef,
Release: DropSourceData::Release,
},
QueryContinueDrag: DropSourceData::QueryContinueDrag,
GiveFeedback: DropSourceData::GiveFeedback,
};
pub(crate) struct DropSource {
data: *mut DropSourceData,
}
impl DropSource {
pub(crate) fn new() -> Self {
Self { data: DropSourceData::new_boxed() }
}
pub(crate) fn interface_ptr(&self) -> *mut c_void {
self.data as *mut c_void
}
}
impl Drop for DropSource {
fn drop(&mut self) {
unsafe { DropSourceData::Release(self.data as *mut IUnknown) };
}
}
/// Attach a drag image to `data_object` so the shell renders the app's icon under the cursor
/// during the drag instead of the default no-image cursor.
///
/// `rgba` is the icon's pixel buffer in straight RGBA8 with `width * height * 4` bytes;
/// `offset` matches `DragIcon::offset` - `(0, 0)` sits the cursor at the icon's top-left,
/// `(-w/2, -h/2)` centres the icon on the cursor. Windows expresses this as the offset from
/// the icon's upper-left to the cursor hot spot (sign inverted), so we negate when filling
/// in `SHDRAGIMAGE::ptOffset`.
///
/// Returns `Ok(())` on success. On failure the caller's data object is left untouched and the
/// drag still runs - just without a custom image. We never propagate the error: a missing drag
/// preview is purely cosmetic and shouldn't fail the whole `start_drag`.
pub(crate) unsafe fn apply_drag_image(
data_object: *mut IDataObject,
width: u32,
height: u32,
rgba: &[u8],
offset_x: i32,
offset_y: i32,
) -> Result<(), HRESULT> {
use windows_sys::Win32::Graphics::Gdi::{
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateDIBSection, DIB_RGB_COLORS, DeleteObject, HDC,
};
use windows_sys::Win32::System::Com::{CLSCTX_ALL, CoCreateInstance};
use windows_sys::Win32::UI::Shell::{CLSID_DragDropHelper, SHDRAGIMAGE};
use crate::definitions::{IDragSourceHelper, IDragSourceHelperVtbl, IID_IDragSourceHelper};
if width == 0 || height == 0 || rgba.len() != (width as usize) * (height as usize) * 4 {
return Err(E_FAIL);
}
// Build a top-down 32bpp BGRA DIB. Top-down means negative biHeight; the shell helper
// expects BGRA byte order (B, G, R, A) with premultiplied alpha for smooth compositing.
let mut header: BITMAPINFO = unsafe { std::mem::zeroed() };
header.bmiHeader = BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: width as i32,
biHeight: -(height as i32),
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB,
biSizeImage: width * height * 4,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
biClrImportant: 0,
};
let mut bits_ptr: *mut c_void = std::ptr::null_mut();
let hbitmap = unsafe {
CreateDIBSection(
std::ptr::null_mut::<HDC>() as HDC,
&header,
DIB_RGB_COLORS,
&mut bits_ptr,
std::ptr::null_mut(),
0,
)
};
if hbitmap.is_null() || bits_ptr.is_null() {
return Err(E_FAIL);
}
// Copy RGBA -> premultiplied BGRA so the shell can render the image with smooth alpha
// edges. Without premultiplication, the cursor preview shows a halo on every translucent
// pixel.
let dst = unsafe { std::slice::from_raw_parts_mut(bits_ptr as *mut u8, rgba.len()) };
for (src_px, dst_px) in rgba.chunks_exact(4).zip(dst.chunks_exact_mut(4)) {
let (r, g, b, a) = (src_px[0] as u32, src_px[1] as u32, src_px[2] as u32, src_px[3]);
dst_px[0] = ((b * a as u32) / 255) as u8;
dst_px[1] = ((g * a as u32) / 255) as u8;
dst_px[2] = ((r * a as u32) / 255) as u8;
dst_px[3] = a;
}
let mut helper: *mut IDragSourceHelper = std::ptr::null_mut();
let hr = unsafe {
CoCreateInstance(
&CLSID_DragDropHelper,
std::ptr::null_mut(),
CLSCTX_ALL,
&IID_IDragSourceHelper,
&mut helper as *mut _ as *mut _,
)
};
if hr < 0 || helper.is_null() {
unsafe { DeleteObject(hbitmap as _) };
return Err(hr);
}
// The helper API takes `ptOffset` as the cursor's position inside the image (positive
// values into the image), but cross-platform `DragIcon::offset` is the icon-relative
// offset where the cursor sits (negative values mean the icon extends up/left of the
// cursor). Negate to translate between the two conventions.
let sdi = SHDRAGIMAGE {
sizeDragImage: windows_sys::Win32::Foundation::SIZE { cx: width as i32, cy: height as i32 },
ptOffset: POINT { x: -offset_x, y: -offset_y },
hbmpDragImage: hbitmap,
crColorKey: 0xffff_ffff, // CLR_NONE - use the alpha channel
};
let vtbl = unsafe {
&*((*(helper as *mut *mut IDragSourceHelperVtbl)) as *const IDragSourceHelperVtbl)
};
let init_hr = unsafe { (vtbl.InitializeFromBitmap)(helper, &sdi, data_object) };
let release = vtbl.parent.Release;
unsafe { release(helper as *mut IUnknown) };
if init_hr < 0 {
// On failure the helper did not take ownership of the bitmap - we still own it.
unsafe { DeleteObject(hbitmap as _) };
return Err(init_hr);
}
// On success the helper stores the bitmap on the data object and will delete it later;
// do NOT call DeleteObject here.
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_offset(buf: &[u8], name: &str) -> usize {
let prefix = format!("{name}:");
let head = std::str::from_utf8(buf).unwrap();
let pos = head.find(&prefix).unwrap() + prefix.len();
head[pos..pos + 10].parse().unwrap()
}
#[test]
fn html_clipboard_format_brackets_user_html() {
let html = "<span><strong>Winit</strong> example</span>";
let buf = build_html_clipboard_format(html);
assert!(buf.starts_with(b"Version:0.9\r\n"));
let start_html = parse_offset(&buf, "StartHTML");
let end_html = parse_offset(&buf, "EndHTML");
let doc = std::str::from_utf8(&buf[start_html..end_html]).unwrap();
assert!(doc.starts_with("<html><body>"));
assert!(doc.ends_with("</body></html>"));
let start_fragment = parse_offset(&buf, "StartFragment");
let end_fragment = parse_offset(&buf, "EndFragment");
let fragment = std::str::from_utf8(&buf[start_fragment..end_fragment]).unwrap();
assert_eq!(fragment, html);
}
#[test]
fn html_clipboard_format_preserves_pre_wrapped() {
let pre_wrapped =
"<html><body><!--StartFragment--><p>hi</p><!--EndFragment--></body></html>";
let buf = build_html_clipboard_format(pre_wrapped);
let start_fragment = parse_offset(&buf, "StartFragment");
let end_fragment = parse_offset(&buf, "EndFragment");
let fragment = std::str::from_utf8(&buf[start_fragment..end_fragment]).unwrap();
assert_eq!(fragment, "<p>hi</p>");
}
}