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

@@ -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,