mirror of
https://github.com/rust-windowing/winit.git
synced 2026-08-29 04:40:04 -04:00
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.
262 lines
10 KiB
Rust
262 lines
10 KiB
Rust
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::{error, info, warn};
|
|
use winit::application::ApplicationHandler;
|
|
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"]
|
|
mod fill;
|
|
#[path = "util/tracing.rs"]
|
|
mod tracing;
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
|
tracing::init();
|
|
|
|
let event_loop = EventLoop::new()?;
|
|
|
|
let app = Application::new();
|
|
Ok(event_loop.run_app(app)?)
|
|
}
|
|
|
|
/// Application state and event handling.
|
|
#[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, 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();
|
|
self.surface = Some(surface);
|
|
}
|
|
|
|
fn window_event(
|
|
&mut self,
|
|
event_loop: &dyn ActiveEventLoop,
|
|
window_id: WindowId,
|
|
event: WindowEvent,
|
|
) {
|
|
match event {
|
|
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 => {
|
|
let surface = self.surface.as_mut().unwrap();
|
|
surface.window().pre_present_notify();
|
|
fill::fill(surface);
|
|
},
|
|
WindowEvent::CloseRequested => {
|
|
event_loop.exit();
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
}
|