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