mirror of
https://github.com/rust-windowing/winit.git
synced 2026-08-29 04:40:04 -04:00
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:
@@ -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",
|
||||
|
||||
@@ -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
1615
winit-win32/src/dnd.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user