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

@@ -1,10 +1,18 @@
use std::error::Error;
use std::path::PathBuf;
use std::sync::Arc;
use image::imageops::FilterType;
use image::{DynamicImage, GenericImageView, RgbImage};
use softbuffer::{Context, Surface};
use tracing::info;
use tracing::{error, info, warn};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::data_transfer::{DataTransferId, DataTransferSendBuilder, SendData, TypeHint};
use winit::event::{ButtonSource, MouseButton, WindowEvent};
use winit::event_loop::{
ActiveEventLoop, AsyncRequestSerial, DndAction, DragIcon, EventLoop, OwnedDisplayHandle,
};
use winit::icon::{Icon, RgbaIcon};
use winit::window::{Window, WindowAttributes, WindowId};
#[path = "util/fill.rs"]
@@ -17,20 +25,55 @@ fn main() -> Result<(), Box<dyn Error>> {
let event_loop = EventLoop::new()?;
let app = Application::default();
let app = Application::new();
Ok(event_loop.run_app(app)?)
}
/// Application state and event handling.
#[derive(Default, Debug)]
#[derive(Debug)]
struct Application {
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
last_dnd_fetch: Option<AsyncRequestSerial>,
last_drag_start: Option<DataTransferId>,
drag_icon: (Icon, i32, i32),
drag_image_data: Arc<RgbImage>,
}
const DRAG_IMAGE: &[u8] = include_bytes!("data/icon.png");
impl Application {
fn new() -> Self {
let drag_icon = load_icon(DRAG_IMAGE);
let drag_image_data = Arc::new(image::load_from_memory(DRAG_IMAGE).unwrap().into_rgb8());
Self {
surface: None,
last_dnd_fetch: None,
last_drag_start: None,
drag_icon,
drag_image_data,
}
}
}
fn load_icon(bytes: &[u8]) -> (Icon, i32, i32) {
let (icon_rgba, icon_width, icon_height) = {
let image = image::load_from_memory(bytes).unwrap().into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
(
RgbaIcon::new(icon_rgba, icon_width, icon_height).expect("Failed to open icon").into(),
-(icon_width as i32) / 2,
-(icon_height as i32) / 2,
)
}
impl ApplicationHandler for Application {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
let window_attributes =
WindowAttributes::default().with_title("Drag and drop files on me!");
WindowAttributes::default().with_title("Drag and drop files, text or HTML onto me!");
let window = event_loop.create_window(window_attributes).unwrap();
let context = Context::new(event_loop.owned_display_handle()).unwrap();
let surface = Surface::new(&context, window).unwrap();
@@ -40,14 +83,168 @@ impl ApplicationHandler for Application {
fn window_event(
&mut self,
event_loop: &dyn ActiveEventLoop,
_window_id: WindowId,
window_id: WindowId,
event: WindowEvent,
) {
match event {
WindowEvent::DragLeft { .. }
| WindowEvent::DragEntered { .. }
| WindowEvent::DragMoved { .. }
| WindowEvent::DragDropped { .. } => {
WindowEvent::PointerButton { button: ButtonSource::Mouse(button), state, .. }
if button == MouseButton::Left && state.is_pressed() =>
{
let (icon, offset_x, offset_y) = self.drag_icon.clone();
// In a real application, you probably wouldn't advertise so many types.
// Depending on platform and destination application, different options may be
// chosen.
let result = event_loop.start_drag(
window_id,
DataTransferSendBuilder::new(self.drag_image_data.clone())
.with_type(TypeHint::UriList, |_, _| {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let root = manifest_dir.parent().unwrap();
let this_file = root.join(file!());
let icon_file = this_file.parent().unwrap().join("data/icon.png");
SendData::from_file_paths([icon_file])
})
.with_type(TypeHint::Plaintext, |_, _| Some("Winit example".to_string()))
.with_type(TypeHint::Html, |_, _| {
Some("<span><strong>Winit</strong> example</span>".to_string())
})
// You can advertise a `TypeHint` that can match many types, and switch
// inside the callback. For example, this will match any image type.
// This may be desirable on some platforms which restrict the set of
// image types that can be sent.
.with_type(TypeHint::Image { extension_hint: None }, |image, ty| {
let hint = ty.hint()?;
match hint {
TypeHint::Image { extension_hint } => {
let image = DynamicImage::from((**image).clone());
let (w, h) = image.dimensions();
let image = image
.resize(w * 8, h * 8, FilterType::Gaussian)
.into_rgb8();
let ext = extension_hint.unwrap_or("png");
info!("Destination requested image as {ext}, converting...");
let format = image::ImageFormat::from_extension(ext)?;
let mut out_buf = Vec::new();
let mut out_writer = std::io::Cursor::new(&mut out_buf);
image.write_to(&mut out_writer, format).ok()?;
Some(out_buf)
},
_ => None,
}
})
.build(),
&[DndAction::Move, DndAction::Copy],
Some(DragIcon { icon, offset_x, offset_y }),
);
self.last_drag_start = result.ok();
},
WindowEvent::DragLeft { .. } => {
info!("{event:?}");
},
WindowEvent::DataTransferReceived { ref value, serial, .. } => {
assert_eq!(self.last_dnd_fetch, Some(serial));
match value.type_().hint() {
Some(TypeHint::Plaintext | TypeHint::Html) => {
let Ok(text) = value.try_as_string() else {
return;
};
info!("{text:?}");
},
Some(TypeHint::UriList) => {
let Ok(uris) = value.try_as_uris() else {
return;
};
info!("URIs: {uris:#?}");
// If you only want to support dropping files, rather than arbitrary URIs,
// you can use the `try_as_file_paths` helper method.
let Ok(uris_as_paths) = value.try_as_file_paths() else {
return;
};
info!("URIs as file paths: {uris_as_paths:#?}");
},
Some(TypeHint::Image { extension_hint: ext }) => {
let Ok(bytes) = value.try_as_bytes() else {
return;
};
let format = ext.and_then(image::ImageFormat::from_extension);
let reader = std::io::Cursor::new(&bytes[..]);
let reader = match format {
Some(fmt) => image::ImageReader::with_format(reader, fmt),
None => image::ImageReader::new(reader),
};
match reader.decode() {
Ok(image) => {
let width = image.width();
let height = image.height();
info!("Received image ({width}x{height})");
},
Err(err) => {
warn!("Failed to decode image: {err}");
},
}
},
_ => {
unreachable!("Received a type we didn't ask for!");
},
}
},
WindowEvent::DragPosition { .. } => {
info!("{event:?}");
},
WindowEvent::DragDropped { .. } => {
info!("{event:?}");
},
WindowEvent::DragEntered { id, .. } => {
info!("{event:?}");
let data_transfer = match event_loop.data_transfer(id) {
Ok(dt) => dt,
Err(e) => {
error!("{e}");
return;
},
};
info!("Types: {:#?}", data_transfer.available_types());
let readable_image_types = image::ImageFormat::all()
.filter(|fmt| fmt.reading_enabled())
.filter_map(|fmt| {
let ext = fmt.extensions_str().first()?;
Some(TypeHint::Image { extension_hint: Some(ext) })
});
let mut valid_types = readable_image_types.chain([
TypeHint::Html,
TypeHint::UriList,
TypeHint::Plaintext,
]);
let valid_type = valid_types.find(|ty| data_transfer.has_type(ty));
let Some(type_) = valid_type else {
event_loop.set_valid_dnd_actions(id, &[]).unwrap();
return;
};
event_loop.set_valid_dnd_actions(id, &[DndAction::Move, DndAction::Copy]).unwrap();
self.last_dnd_fetch = event_loop.fetch_data_transfer(id, &type_).ok();
},
WindowEvent::OutgoingDragDropped { .. } => {
info!("{event:?}");
},
WindowEvent::OutgoingDragCanceled { .. } => {
info!("{event:?}");
},
WindowEvent::RedrawRequested => {