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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user