New drag and drop API (#4571)

This commit implements a new API for drag and drop, with a `DataTransfer` type which abstracts over the various clipboard/drag and drop APIs across different platforms. I built this on top of #2429 although admittedly I ended up removing pretty much all of their work while I was reworking the design.

This is being built in order to help support [drag-and-drop work](https://github.com/slint-ui/slint/issues/1967) in Slint's winit backend. As part of that work, I did extensive research on how drag-and-drop and clipboard APIs are implemented across different platforms, and wrote a (still WIP) research document that can be found [here](https://gist.github.com/eira-fransham/06750cf8d25ade08d362a0ca8dfafe06).

The new API is inspired by the browser's [`DataTransfer`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) API.
This commit is contained in:
Eira Fransham
2026-07-16 14:48:12 +02:00
committed by GitHub
parent 066c091b56
commit 156433eb91
44 changed files with 5905 additions and 594 deletions

View File

@@ -19,6 +19,7 @@ rwh_06.workspace = true
serde = { workspace = true, optional = true }
smol_str.workspace = true
tracing.workspace = true
url.workspace = true
winit-core.workspace = true
# Platform-specific
@@ -32,7 +33,9 @@ windows-sys = { workspace = true, features = [
"Win32_Media",
"Win32_System_Com_StructuredStorage",
"Win32_System_Com",
"Win32_System_DataExchange",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_Security",
"Win32_System_SystemInformation",

View File

@@ -3,8 +3,9 @@
use std::ffi::c_void;
use windows_sys::Win32::Foundation::{HWND, POINTL};
use windows_sys::Win32::Foundation::{HWND, POINT, POINTL};
use windows_sys::Win32::System::Com::{FORMATETC, STGMEDIUM};
use windows_sys::Win32::UI::Shell::SHDRAGIMAGE;
use windows_sys::core::{BOOL, GUID, HRESULT};
pub type IUnknown = *mut c_void;
@@ -37,7 +38,7 @@ pub struct IDataObjectVtbl {
pformatetc: *const FORMATETC,
pmedium: *mut STGMEDIUM,
) -> HRESULT,
QueryGetData:
pub QueryGetData:
unsafe extern "system" fn(This: *mut IDataObject, pformatetc: *const FORMATETC) -> HRESULT,
pub GetCanonicalFormatEtc: unsafe extern "system" fn(
This: *mut IDataObject,
@@ -47,7 +48,7 @@ pub struct IDataObjectVtbl {
pub SetData: unsafe extern "system" fn(
This: *mut IDataObject,
pformatetc: *const FORMATETC,
pformatetcOut: *const FORMATETC,
pmedium: *const STGMEDIUM,
fRelease: BOOL,
) -> HRESULT,
pub EnumFormatEtc: unsafe extern "system" fn(
@@ -69,6 +70,81 @@ pub struct IDataObjectVtbl {
) -> HRESULT,
}
#[repr(C)]
pub struct IEnumFORMATETCVtbl {
pub parent: IUnknownVtbl,
pub Next: unsafe extern "system" fn(
This: *mut IEnumFORMATETC,
celt: u32,
rgelt: *mut FORMATETC,
pceltFetched: *mut u32,
) -> HRESULT,
pub Skip: unsafe extern "system" fn(This: *mut IEnumFORMATETC, celt: u32) -> HRESULT,
pub Reset: unsafe extern "system" fn(This: *mut IEnumFORMATETC) -> HRESULT,
pub Clone: unsafe extern "system" fn(
This: *mut IEnumFORMATETC,
ppenum: *mut *mut IEnumFORMATETC,
) -> HRESULT,
}
pub type IDragSourceHelper = *mut c_void;
#[repr(C)]
pub struct IDragSourceHelperVtbl {
pub parent: IUnknownVtbl,
pub InitializeFromBitmap: unsafe extern "system" fn(
This: *mut IDragSourceHelper,
pshdi: *const SHDRAGIMAGE,
pDataObject: *mut IDataObject,
) -> HRESULT,
pub InitializeFromWindow: unsafe extern "system" fn(
This: *mut IDragSourceHelper,
hwnd: HWND,
ppt: *const POINT,
pDataObject: *mut IDataObject,
) -> HRESULT,
}
pub type IDropTargetHelper = *mut c_void;
#[repr(C)]
pub struct IDropTargetHelperVtbl {
pub parent: IUnknownVtbl,
pub DragEnter: unsafe extern "system" fn(
This: *mut IDropTargetHelper,
hwndTarget: HWND,
pDataObject: *mut IDataObject,
ppt: *const POINT,
dwEffect: u32,
) -> HRESULT,
pub DragLeave: unsafe extern "system" fn(This: *mut IDropTargetHelper) -> HRESULT,
pub DragOver: unsafe extern "system" fn(
This: *mut IDropTargetHelper,
ppt: *const POINT,
dwEffect: u32,
) -> HRESULT,
pub Drop: unsafe extern "system" fn(
This: *mut IDropTargetHelper,
pDataObject: *mut IDataObject,
ppt: *const POINT,
dwEffect: u32,
) -> HRESULT,
pub Show: unsafe extern "system" fn(This: *mut IDropTargetHelper, fShow: BOOL) -> HRESULT,
}
pub type IDropSource = *mut c_void;
#[repr(C)]
pub struct IDropSourceVtbl {
pub parent: IUnknownVtbl,
pub QueryContinueDrag: unsafe extern "system" fn(
This: *mut IDropSource,
fEscapePressed: BOOL,
grfKeyState: u32,
) -> HRESULT,
pub GiveFeedback: unsafe extern "system" fn(This: *mut IDropSource, dwEffect: u32) -> HRESULT,
}
#[repr(C)]
pub struct IDropTargetVtbl {
pub parent: IUnknownVtbl,
@@ -130,6 +206,22 @@ pub struct ITaskbarList2 {
pub lpVtbl: *const ITaskbarList2Vtbl,
}
/// Defined in `objidl.h`.
pub const IID_IDataObject: GUID = GUID::from_u128(0x0000010e_0000_0000_c000_000000000046);
/// Defined in `oleidl.h`.
pub const IID_IDropSource: GUID = GUID::from_u128(0x00000121_0000_0000_c000_000000000046);
/// Defined in `objidl.h`.
pub const IID_IEnumFORMATETC: GUID = GUID::from_u128(0x00000103_0000_0000_c000_000000000046);
/// Defined in `shobjidl_core.h`.
pub const IID_IDragSourceHelper: GUID = GUID::from_u128(0xde5bf786_477a_11d2_839d_00c04fd918d0);
/// Defined in `shobjidl_core.h`.
pub const IID_IDropTargetHelper: GUID = GUID::from_u128(0x4657278b_411b_11d2_839a_00c04fd918d0);
/// Defined in `shobjidl_core.h`.
pub const CLSID_TaskbarList: GUID = GUID {
data1: 0x56fdf344,
data2: 0xfd6d,
@@ -137,6 +229,7 @@ pub const CLSID_TaskbarList: GUID = GUID {
data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90],
};
/// Defined in `shobjidl_core.h`.
pub const IID_ITaskbarList: GUID = GUID {
data1: 0x56fdf342,
data2: 0xfd6d,
@@ -144,6 +237,7 @@ pub const IID_ITaskbarList: GUID = GUID {
data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90],
};
/// Defined in `shobjidl_core.h`.
pub const IID_ITaskbarList2: GUID = GUID {
data1: 0x602d4995,
data2: 0xb13a,

1615
winit-win32/src/dnd.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,240 +0,0 @@
use std::ffi::{OsString, c_void};
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::ptr;
use std::sync::atomic::{AtomicUsize, Ordering};
use dpi::PhysicalPosition;
use tracing::debug;
use windows_sys::Win32::Foundation::{DV_E_FORMATETC, HWND, POINT, POINTL, S_OK};
use windows_sys::Win32::Graphics::Gdi::ScreenToClient;
use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL};
use windows_sys::Win32::System::Ole::{CF_HDROP, DROPEFFECT_COPY, DROPEFFECT_NONE};
use windows_sys::Win32::UI::Shell::{DragFinish, DragQueryFileW, HDROP};
use windows_sys::core::{GUID, HRESULT};
use winit_core::event::WindowEvent;
use crate::definitions::{
IDataObject, IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknown, IUnknownVtbl,
};
#[repr(C)]
pub struct FileDropHandlerData {
pub interface: IDropTarget,
refcount: AtomicUsize,
window: HWND,
send_event: Box<dyn Fn(WindowEvent)>,
cursor_effect: u32,
valid: bool, /* If the currently hovered item is not valid there must not be any
* `DragLeft` emitted */
}
pub struct FileDropHandler {
pub data: *mut FileDropHandlerData,
}
#[allow(non_snake_case)]
impl FileDropHandler {
pub(crate) fn new(window: HWND, 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,
send_event,
cursor_effect: DROPEFFECT_NONE,
valid: false,
});
FileDropHandler { data: Box::into_raw(data) }
}
// Implement IUnknown
pub 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.
unimplemented!();
}
pub 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::Release) + 1;
count as u32
}
pub unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 {
let drop_handler = unsafe { Self::from_interface(this) };
let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1;
if count == 0 {
// Destroy the underlying data
drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) });
}
count as u32
}
pub 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) };
let mut pt = POINT { x: pt.x, y: pt.y };
unsafe {
ScreenToClient(drop_handler.window, &mut pt);
}
let position = PhysicalPosition::new(pt.x as f64, pt.y as f64);
let mut paths = Vec::new();
let hdrop = unsafe { Self::iterate_filenames(pDataObj, |path| paths.push(path)) };
drop_handler.valid = hdrop.is_some();
if drop_handler.valid {
(drop_handler.send_event)(WindowEvent::DragEntered { paths, position });
}
drop_handler.cursor_effect =
if drop_handler.valid { DROPEFFECT_COPY } else { DROPEFFECT_NONE };
unsafe {
*pdwEffect = drop_handler.cursor_effect;
}
S_OK
}
pub unsafe extern "system" fn DragOver(
this: *mut IDropTarget,
_grfKeyState: u32,
pt: POINTL,
pdwEffect: *mut u32,
) -> HRESULT {
let drop_handler = unsafe { Self::from_interface(this) };
if drop_handler.valid {
let mut pt = POINT { x: pt.x, y: pt.y };
unsafe {
ScreenToClient(drop_handler.window, &mut pt);
}
let position = PhysicalPosition::new(pt.x as f64, pt.y as f64);
(drop_handler.send_event)(WindowEvent::DragMoved { position });
}
unsafe {
*pdwEffect = drop_handler.cursor_effect;
}
S_OK
}
pub unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT {
let drop_handler = unsafe { Self::from_interface(this) };
if drop_handler.valid {
(drop_handler.send_event)(WindowEvent::DragLeft { position: None });
}
S_OK
}
pub 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) };
if drop_handler.valid {
let mut pt = POINT { x: pt.x, y: pt.y };
unsafe {
ScreenToClient(drop_handler.window, &mut pt);
}
let position = PhysicalPosition::new(pt.x as f64, pt.y as f64);
let mut paths = Vec::new();
let hdrop = unsafe { Self::iterate_filenames(pDataObj, |path| paths.push(path)) };
(drop_handler.send_event)(WindowEvent::DragDropped { paths, position });
if let Some(hdrop) = hdrop {
unsafe {
DragFinish(hdrop);
}
}
}
unsafe {
*pdwEffect = drop_handler.cursor_effect;
}
S_OK
}
unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut FileDropHandlerData {
unsafe { &mut *(this as *mut _) }
}
unsafe fn iterate_filenames<F>(data_obj: *const IDataObject, mut callback: F) -> Option<HDROP>
where
F: FnMut(PathBuf),
{
let drop_format = FORMATETC {
cfFormat: CF_HDROP,
ptd: ptr::null_mut(),
dwAspect: DVASPECT_CONTENT,
lindex: -1,
tymed: TYMED_HGLOBAL as u32,
};
let mut medium = unsafe { std::mem::zeroed() };
let get_data_fn = unsafe { (*(*data_obj).cast::<IDataObjectVtbl>()).GetData };
let get_data_result = unsafe { get_data_fn(data_obj as *mut _, &drop_format, &mut medium) };
if get_data_result >= 0 {
let hdrop = unsafe { medium.u.hGlobal as HDROP };
// The second parameter (0xFFFFFFFF) instructs the function to return the item count
let item_count = unsafe { DragQueryFileW(hdrop, 0xffffffff, ptr::null_mut(), 0) };
for i in 0..item_count {
// Get the length of the path string NOT including the terminating null character.
// Previously, this was using a fixed size array of MAX_PATH length, but the
// Windows API allows longer paths under certain circumstances.
let character_count =
unsafe { DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize };
let str_len = character_count + 1;
// Fill path_buf with the null-terminated file name
let mut path_buf = Vec::with_capacity(str_len);
unsafe {
DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32);
path_buf.set_len(str_len);
}
callback(OsString::from_wide(&path_buf[0..character_count]).into());
}
Some(hdrop)
} else if get_data_result == DV_E_FORMATETC {
// If the dropped item is not a file this error will occur.
// In this case it is OK to return without taking further action.
debug!("Error occurred while processing dropped/hovered item: item is not a file.");
None
} else {
debug!("Unexpected error occurred while processing dropped/hovered item.");
None
}
}
}
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,
};

View File

@@ -63,6 +63,9 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
};
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor, CustomCursorSource};
use winit_core::data_transfer::{
DataTransfer, DataTransferId, DataTransferSend, TransferType, TypedData,
};
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
use winit_core::event::{
DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TabletToolButton,
@@ -70,8 +73,8 @@ use winit_core::event::{
};
use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider,
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
DndAction, DragIcon, EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider,
OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use winit_core::keyboard::ModifiersState;
@@ -82,8 +85,9 @@ pub(super) use self::runner::{Event, EventLoopRunner};
use super::SelectedCursor;
use super::window::set_skip_taskbar;
use crate::dark_mode::try_theme;
use crate::dnd::{DropSource, FileDropHandler, SourceDataObject, WinDataTransfer, WinTypedData};
use crate::dpi::{become_dpi_aware, dpi_to_scale_factor};
use crate::drop_handler::FileDropHandler;
use crate::event_loop::runner::PendingDrag;
use crate::icon::WinCursor;
use crate::ime::ImeContext;
use crate::keyboard::KeyEventBuilder;
@@ -478,6 +482,105 @@ impl RootActiveEventLoop for ActiveEventLoop {
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
fn fetch_data_transfer(
&self,
id: DataTransferId,
type_: &dyn TransferType,
) -> Result<AsyncRequestSerial, RequestError> {
let Some(state) = self.0.drag_state(id) else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
let hint = type_.hint().ok_or(RequestError::Ignored)?;
let typed_data = WinTypedData::new(state.data.clone(), hint)
.map(|value| Arc::new(value) as Arc<dyn TypedData>)
.ok_or(RequestError::Ignored)?;
let serial = AsyncRequestSerial::get();
self.0.send_event(Event::Window {
window_id: state.window_id,
event: WindowEvent::DataTransferReceived { id, serial, value: typed_data },
});
Ok(serial)
}
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
let Some(state) = self.0.drag_state(id) else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
Ok(Box::new(WinDataTransfer::new(state.data.clone())))
}
fn set_valid_dnd_actions(
&self,
id: DataTransferId,
actions: &[DndAction],
) -> Result<(), RequestError> {
let mut state = self.0.drag_state.borrow_mut();
let Some(state) = state.as_mut().filter(|s| s.id == id) else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
state.actions = actions.to_vec();
Ok(())
}
fn start_drag(
&self,
source: WindowId,
send_data: Box<dyn DataTransferSend>,
allowed_actions: &[DndAction],
icon: Option<DragIcon>,
) -> Result<DataTransferId, RequestError> {
let allowed_effects = crate::dnd::dnd_actions_to_dropeffect_mask(allowed_actions);
// Win32 would happily run a modal `DoDragDrop` with `allowed_effects == 0`, but every
// target would see "no action allowed" and the drag would end in a guaranteed cancel
// after burning a full modal pump. Fail fast instead - the caller asked for a drag
// they explicitly refuse to allow.
if allowed_effects == 0 {
return Err(
NotSupportedError::new("start_drag called with an empty action mask").into()
);
}
let id = crate::dnd::next_data_transfer_id();
let data_object = SourceDataObject::new(send_data);
let drop_source = DropSource::new();
// Attach a drag preview if the app supplied one. Cosmetic failures must not abort the
// drag - the gesture still works, just without a custom image - so log and move on.
if let Some(icon) = icon {
if let Some(rgba) = icon.icon.cast_ref::<winit_core::icon::RgbaIcon>() {
let result = unsafe {
crate::dnd::apply_drag_image(
data_object.interface_ptr() as *mut _,
rgba.width(),
rgba.height(),
rgba.buffer(),
icon.offset_x,
icon.offset_y,
)
};
if let Err(hr) = result {
tracing::warn!("Failed to attach drag image: hr=0x{hr:08x}");
}
} else {
tracing::warn!("DragIcon::icon must be an RgbaIcon on win32; ignoring");
}
}
self.0.pending_drag.replace(Some(PendingDrag {
window_id: source,
id,
data_object,
drop_source,
allowed_effects,
}));
Ok(id)
}
}
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
@@ -487,6 +590,19 @@ impl rwh_06::HasDisplayHandle for ActiveEventLoop {
}
}
/// An operation was attempted on a data transfer ID, but that ID was invalid.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct UnknownDataTransfer(pub DataTransferId);
impl fmt::Display for UnknownDataTransfer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.0.into_raw();
write!(f, "Unknown data transfer with ID {id}")
}
}
impl std::error::Error for UnknownDataTransfer {}
#[derive(Clone)]
pub(crate) struct OwnedDisplayHandle;
@@ -1177,6 +1293,14 @@ unsafe fn public_window_callback_inner(
result = ProcResult::Value(0);
},
WM_PAINT if userdata.event_loop_runner.source_drag.get().is_some() => {
// While a source-side drag is in flight, the app handler is on the stack (we're
// inside `start_drag` -> `DoDragDrop`), so we can neither dispatch `RedrawRequested`
// nor keep re-arming via `RDW_INTERNALPAINT` (that would spin in OLE's modal loop).
// Let `DefWindowProcW` validate the region and show stale content for the duration of
// the drag; the next real paint happens once `DoDragDrop` returns.
result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) });
},
WM_PAINT => {
userdata.window_state_lock().redraw_requested =
userdata.event_loop_runner.should_buffer();
@@ -2352,6 +2476,13 @@ unsafe fn public_window_callback_inner(
.catch_unwind(callback)
.unwrap_or_else(|| result = ProcResult::Value(-1));
// We execute a new drag operation here instead of immediately starting it in
// `ActiveEventLoop::start_drag`. `DoDragDrop` is blocking and synchronous, so if we started
// it inside the event loop then an internal drag operation would be re-entrant and the
// application would not be able to handle the incoming messages. This is after the application
// has had a chance to handle mouse events.
userdata.event_loop_runner.try_execute_drag_drop();
match result {
ProcResult::DefWindowProc(wparam) => unsafe { DefWindowProcW(window, msg, wparam, lparam) },
ProcResult::Value(val) => val,

View File

@@ -1,5 +1,5 @@
use std::any::Any;
use std::cell::{Cell, RefCell};
use std::cell::{Cell, Ref, RefCell};
use std::collections::VecDeque;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
@@ -7,18 +7,49 @@ use std::time::Instant;
use std::{fmt, mem, panic};
use dpi::PhysicalSize;
use windows_sys::Win32::Foundation::HWND;
use windows_sys::Win32::Foundation::{DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, HWND};
use windows_sys::Win32::System::Ole::{
DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, DoDragDrop,
};
use winit_core::application::ApplicationHandler;
use winit_core::data_transfer::DataTransferId;
use winit_core::event::{DeviceEvent, DeviceId, StartCause, SurfaceSizeWriter, WindowEvent};
use winit_core::event_loop::ActiveEventLoop as RootActiveEventLoop;
use winit_core::event_loop::{ActiveEventLoop as RootActiveEventLoop, DndAction};
use winit_core::window::WindowId;
use super::{ActiveEventLoop, ControlFlow, EventLoopThreadExecutor};
use crate::dnd::{DataObject, DropEffect, DropSource, SourceDataObject, drop_effect_to_dnd_action};
use crate::event_loop::{GWL_USERDATA, WindowData};
use crate::util::get_window_long;
type EventHandler = Cell<Option<&'static mut (dyn ApplicationHandler + 'static)>>;
/// State for the single drag-and-drop transfer currently in flight (OLE guarantees at most one
/// active drag per process).
#[derive(Debug)]
pub(super) struct DragState {
pub(super) id: DataTransferId,
pub(super) window_id: WindowId,
pub(super) data: Arc<DataObject>,
pub(super) actions: Vec<DndAction>,
}
pub(super) struct PendingDrag {
pub(super) window_id: WindowId,
pub(super) data_object: SourceDataObject,
pub(super) drop_source: DropSource,
pub(super) allowed_effects: DropEffect,
pub(super) id: DataTransferId,
}
/// Set while `DoDragDrop` is on the call stack - i.e., this process is the source of an active
/// drag. The target-side `IDropTarget` checks this to recognize self-drops and reuse the source's
/// id + allowed actions instead of waiting for the (buffered) app `DragEntered` handler.
#[derive(Copy, Clone)]
pub(crate) struct SourceDrag {
pub(crate) id: DataTransferId,
}
pub(crate) struct EventLoopRunner {
pub(super) thread_id: u32,
@@ -37,6 +68,29 @@ pub(crate) struct EventLoopRunner {
event_handler: Rc<EventHandler>,
event_buffer: RefCell<VecDeque<Event>>,
/// The currently in-flight drag transfer, if any, alive between `DragEntered` and
/// `DragLeft`/`DragDropped`.
pub(super) drag_state: RefCell<Option<DragState>>,
/// `Some(_)` while `start_drag` has `DoDragDrop` on the call stack.
pub(crate) source_drag: Cell<Option<SourceDrag>>,
/// `DoDragDrop` is blocking and synchronous, so we wait until after the application returns
/// control to winit before actually calling into the OS to initiate the drag. This prevents
/// the event loop from being re-entrant if we are doing an internal drag operation, since if
/// we handled this inside `ActiveEventLoop::start_drag` then all the `WindowEvent::Drag*`
/// events would be buffered until `DoDragDrop` returns, preventing the application from
/// handling those messages.
pub(super) pending_drag: RefCell<Option<PendingDrag>>,
/// For self-drops, target-side `IDropTarget::Drop` can't release the cached `DragState`
/// before its `DragDropped` `WindowEvent` is delivered - the event is buffered (the outer app
/// handler holds `event_handler` for the duration of `DoDragDrop`) and `data_transfer(id)`
/// would return `UnknownDataTransfer` if cleanup ran synchronously. So we stash the id here
/// and drain it at the end of `dispatch_buffered_events`, after the app's buffered handler
/// has had its chance to read the data.
pending_source_drag_cleanup: Cell<Option<DataTransferId>>,
panic_error: Cell<Option<PanicError>>,
}
@@ -87,9 +141,122 @@ impl EventLoopRunner {
last_events_cleared: Cell::new(Instant::now()),
event_handler: Rc::new(Cell::new(None)),
event_buffer: RefCell::new(VecDeque::new()),
drag_state: RefCell::new(None),
source_drag: Cell::new(None),
pending_drag: RefCell::new(None),
pending_source_drag_cleanup: Cell::new(None),
}
}
pub(super) fn try_execute_drag_drop(self: &Rc<Self>) {
let Some(PendingDrag { data_object, drop_source, id, allowed_effects, window_id }) =
self.pending_drag.take()
else {
return;
};
// Make the drag visible to our own target-side `IDropTarget` so it can recognize
// self-drops and reuse this id + action mask without going through the (buffered) app
// handler. The guard ensures the flag is cleared on any exit path - if anything between
// here and `DoDragDrop`'s return panics, the stale flag would otherwise permanently
// disable `WM_PAINT` dispatch and misclassify all future external drags as self-drops.
struct ClearOnDrop<'a>(&'a Cell<Option<SourceDrag>>);
impl Drop for ClearOnDrop<'_> {
fn drop(&mut self) {
self.0.set(None);
}
}
self.source_drag.set(Some(SourceDrag { id }));
let _guard = ClearOnDrop(&self.source_drag);
let mut effect_out: u32 = DROPEFFECT_NONE;
let hr = unsafe {
DoDragDrop(
data_object.interface_ptr(),
drop_source.interface_ptr(),
allowed_effects,
&mut effect_out,
)
};
if hr == DRAGDROP_S_DROP {
let action = drop_effect_to_dnd_action(effect_out);
self.send_event(Event::Window {
window_id,
event: WindowEvent::OutgoingDragDropped { id, action },
});
} else if hr == DRAGDROP_S_CANCEL {
self.send_event(Event::Window {
window_id,
event: WindowEvent::OutgoingDragCanceled { id },
});
} else {
tracing::error!("DoDragDrop failed: 0x{hr:08x}");
return;
}
// Both `DRAGDROP_S_DROP` and `DRAGDROP_S_CANCEL` are success codes for us - the app
// will hear about the outcome via the buffered `DragDropped`/`DragLeft` events
// (target-side translates `effect_out == DROPEFFECT_NONE` to `DragLeft`).
// Log the negotiated effect so cross-process drops, which have no target-side event
// in this process, leave a debuggable trace of what action the remote target performed.
tracing::trace!(
"DoDragDrop completed: hr=0x{hr:08x} effect_out={effect_out} (COPY={DROPEFFECT_COPY}, \
MOVE={DROPEFFECT_MOVE}, LINK={DROPEFFECT_LINK})",
);
}
pub(crate) fn defer_source_drag_cleanup(&self, id: DataTransferId) {
self.pending_source_drag_cleanup.set(Some(id));
}
pub(crate) fn register_data_transfer(
&self,
id: DataTransferId,
window_id: WindowId,
data: Arc<DataObject>,
) {
// By default, no actions have been set as valid by the target.
*self.drag_state.borrow_mut() =
Some(DragState { id, window_id, data, actions: Default::default() });
}
pub(crate) fn remove_data_transfer(&self, id: DataTransferId) {
let mut state = self.drag_state.borrow_mut();
if state.as_ref().is_some_and(|s| s.id == id) {
*state = None;
}
}
pub(super) fn drag_state(&self, id: DataTransferId) -> Option<Ref<'_, DragState>> {
Ref::filter_map(self.drag_state.borrow(), |state| state.as_ref().filter(|s| s.id == id))
.ok()
}
pub(crate) fn current_drag_actions(&self, id: DataTransferId) -> Ref<'_, [DndAction]> {
Ref::map(self.drag_state.borrow(), |state| {
state.as_ref().filter(|s| s.id == id).map(|s| &s.actions[..]).unwrap_or_default()
})
}
pub(crate) fn proposed_dnd_action(
&self,
id: DataTransferId,
effects: DropEffect,
) -> Option<DndAction> {
self.current_drag_actions(id).iter().copied().find(|action| {
let effect = match action {
DndAction::Move => DROPEFFECT_MOVE,
DndAction::Copy => DROPEFFECT_COPY,
DndAction::Link => DROPEFFECT_LINK,
_ => return false,
};
(effects & effect) != 0
})
}
/// Associate the application's event handler with the runner.
///
/// # Safety
@@ -138,12 +305,20 @@ impl EventLoopRunner {
last_events_cleared: _,
event_handler,
event_buffer: _,
drag_state,
source_drag,
pending_drag,
pending_source_drag_cleanup,
} = self;
interrupt_msg_dispatch.set(false);
runner_state.set(RunnerState::Uninitialized);
panic_error.set(None);
exit.set(None);
event_handler.set(None);
drag_state.take();
source_drag.set(None);
pending_drag.take();
pending_source_drag_cleanup.set(None);
}
}
@@ -285,6 +460,11 @@ impl EventLoopRunner {
None => break,
}
}
// The app's buffered `DragDropped` handler (if any) has now had its chance to call
// `data_transfer(id)`; safe to release the cached `DragState` for a deferred self-drop.
if let Some(id) = self.pending_source_drag_cleanup.take() {
self.remove_data_transfer(id);
}
}
/// Dispatch control flow events (`NewEvents`, `AboutToWait`, and

View File

@@ -8,8 +8,8 @@
mod util;
mod dark_mode;
mod definitions;
mod dnd;
mod dpi;
mod drop_handler;
mod event_loop;
mod icon;
mod ime;

View File

@@ -61,8 +61,8 @@ use crate::dark_mode::try_theme;
use crate::definitions::{
CLSID_TaskbarList, IID_ITaskbarList, IID_ITaskbarList2, ITaskbarList, ITaskbarList2,
};
use crate::dnd::FileDropHandler;
use crate::dpi::{dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi};
use crate::drop_handler::FileDropHandler;
use crate::event_loop::{self, ActiveEventLoop, DESTROY_MSG_ID, Event, EventLoopRunner};
use crate::icon::{IconType, WinCursor};
use crate::ime::ImeContext;
@@ -1191,6 +1191,7 @@ impl InitData<'_> {
let window_state = {
let window_state = WindowState::new(
&self.attributes,
&self.win_attributes,
scale_factor,
current_theme,
self.attributes.preferred_theme,
@@ -1230,15 +1231,16 @@ impl InitData<'_> {
let file_drop_runner = self.runner.clone();
let window_id = win.id();
let file_drop_handler = FileDropHandler::new(
let mut file_drop_handler = FileDropHandler::new(
win.window.hwnd(),
self.runner.clone(),
Box::new(move |event| {
file_drop_runner.send_event(Event::Window { window_id, event })
}),
);
let handler_interface_ptr =
unsafe { &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void };
unsafe { file_drop_handler.interface_unchecked_mut() as *mut _ as *mut c_void };
assert_eq!(unsafe { RegisterDragDrop(win.window.hwnd(), handler_interface_ptr) }, S_OK);
Some(file_drop_handler)

View File

@@ -22,7 +22,7 @@ use winit_core::keyboard::ModifiersState;
use winit_core::monitor::Fullscreen;
use winit_core::window::{ImeCapabilities, Theme, WindowAttributes};
use crate::{SelectedCursor, event_loop, util};
use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util};
/// Contains information about states and the window that the callback is going to use.
#[derive(Debug)]
@@ -154,6 +154,7 @@ pub enum ImeState {
impl WindowState {
pub(crate) fn new(
attributes: &WindowAttributes,
_win_attributes: &WindowAttributesWindows,
scale_factor: f64,
current_theme: Theme,
preferred_theme: Option<Theme>,