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

805
winit-wayland/src/dnd.rs Normal file
View File

@@ -0,0 +1,805 @@
//! Types related to drag-and-drop and data transfer on Wayland.
use std::ffi::OsStr;
use std::fmt;
use std::io::{self, BufRead, Cursor, ErrorKind, Write};
use std::ops::{BitOr, Deref};
use std::sync::Arc;
use calloop::PostAction;
use dpi::{LogicalPosition, PhysicalPosition};
use sctk::data_device_manager::WritePipe;
use sctk::data_device_manager::data_device::{DataDeviceData, DataDeviceHandler};
use sctk::data_device_manager::data_offer::{DataOfferHandler, DragOffer};
use sctk::data_device_manager::data_source::{DataSourceHandler, DragSource as SctkDragSource};
use sctk::reexports::client::backend::ObjectId;
use wayland_client::protocol::wl_data_device::WlDataDevice;
use wayland_client::protocol::wl_data_device_manager::DndAction as WlDndAction;
use wayland_client::protocol::wl_data_offer::WlDataOffer;
use wayland_client::protocol::wl_data_source::WlDataSource;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::{Connection, Proxy, QueueHandle};
use winit_core::data_transfer::{
DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData,
};
use winit_core::event::WindowEvent;
use winit_core::event_loop::DndAction;
use winit_core::window::WindowId;
use crate::make_data_transfer_id;
use crate::state::WinitState;
fn encode_uri_list<I>(uri_list: I) -> Vec<u8>
where
I: IntoIterator,
I::Item: AsRef<OsStr>,
{
let mut out = Vec::new();
for uri in uri_list {
out.extend_from_slice(OsStr::new(&uri).as_encoded_bytes());
out.extend_from_slice(b"\r\n");
}
out
}
impl DataSourceHandler for WinitState {
fn accept_mime(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataSource,
_: Option<String>,
) {
// This method isn't a necessary part of the protocol, it's a holdover from the first
// version of DnD in Wayland and now just serves as a hint.
}
fn send_request(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataSource,
mime: String,
fd: WritePipe,
) {
let Some(data) = self.dnd_state.send_drag_data_mut() else {
// TODO: Is there a way to explicitly express that the data was not sent?
return;
};
let mime = MimeType::parse(mime);
let Some(send_data) = data.data_for_type(&mime) else {
return;
};
let mut encoder = match send_data {
SendData::Uris(strings) => Cursor::new(encode_uri_list(strings)),
SendData::String(str) => match mime.parse_charset() {
Ok(Charset::Utf8) => Cursor::new(str.into_bytes()),
Err(e) => {
tracing::error!("{e}");
return;
},
},
SendData::Bytes(binary) => Cursor::new(binary),
};
let _ = self.loop_handle.insert_source(fd, move |_, file, _| {
// Safety: We only mutate `file` in-place and do not replace and drop it.
let file = unsafe { file.get_mut() };
loop {
let Ok(encoded_bytes) = encoder.fill_buf() else {
return PostAction::Remove;
};
match file.write(encoded_bytes) {
Ok(0) => {
break PostAction::Remove;
},
Ok(consumed) => {
encoder.consume(consumed);
},
Err(e) if e.kind() == ErrorKind::WouldBlock => {
break PostAction::Continue;
},
Err(_) => {
break PostAction::Remove;
},
}
}
});
}
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
let Some(current_drag) = self.dnd_state.send_drag() else {
return;
};
let window_id = current_drag.window_id;
let id = current_drag.data_transfer_id;
self.events_sink.push_window_event(WindowEvent::OutgoingDragCanceled { id }, window_id);
}
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
let Some(current_drag) = self.dnd_state.send_drag() else {
return;
};
let window_id = current_drag.window_id;
let id = current_drag.data_transfer_id;
let selected_action = current_drag.selected_action;
self.events_sink.push_window_event(
WindowEvent::OutgoingDragDropped {
id,
action: dnd_action_wl_to_winit(selected_action),
},
window_id,
);
}
fn dnd_finished(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wayland_client::protocol::wl_data_source::WlDataSource,
) {
self.dnd_state.clear_send_drag();
}
fn action(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataSource,
action: WlDndAction,
) {
self.dnd_state.set_target_drag_action(action);
}
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)]
enum Charset {
#[default]
Utf8,
}
/// MIME type as string, with an optional hint detected from the MIME type.
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct MimeType {
mime: Arc<str>,
hint: Option<TypeHint>,
}
// MIME types
// Files
const TEXT_URI_LIST: &str = "text/uri-list";
// Plaintext
const TEXT_PLAIN: &str = "text/plain";
const TEXT_PLAIN_CHARSET_UTF8: &str = "text/plain; charset=utf-8";
// HTML
const TEXT_HTML: &str = "text/html";
const TEXT_HTML_CHARSET_UTF8: &str = "text/html; charset=utf-8";
// RTF
const APPLICATION_RTF: &str = "application/rtf";
// Audio
const AUDIO_AAC: &str = "audio/aac";
const AUDIO_AIFF: &str = "audio/aiff";
const AUDIO_FLAC: &str = "audio/flac";
const AUDIO_WAV: &str = "audio/wav";
const AUDIO_WAVE: &str = "audio/wave";
const AUDIO_X_WAV: &str = "audio/x-wav";
const AUDIO_VND_WAV: &str = "audio/vnd.wav";
const AUDIO_VND_WAVE: &str = "audio/vnd.wave";
const AUDIO_MPEG: &str = "audio/mpeg";
const AUDIO_OGG: &str = "audio/ogg";
// Image
const IMAGE_BMP: &str = "image/bmp";
const IMAGE_GIF: &str = "image/gif";
const IMAGE_JPEG: &str = "image/jpeg";
const IMAGE_PJPEG: &str = "image/pjpeg";
const IMAGE_PNG: &str = "image/png";
const IMAGE_SVG: &str = "image/svg+xml";
const IMAGE_TIFF: &str = "image/tiff";
const IMAGE_WEBP: &str = "image/webp";
const IMAGE_X_ICON: &str = "image/x-icon";
const IMAGE_RAW: &str = "image/x-panasonic-raw";
#[derive(Debug)]
struct UnexpectedCharsetError<'a>(&'a str);
impl std::error::Error for UnexpectedCharsetError<'_> {}
impl fmt::Display for UnexpectedCharsetError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unsupported charset: {}", self.0)
}
}
impl MimeType {
const MIME_HINT_MAP: &[(&str, TypeHint)] = &[
// Files
(TEXT_URI_LIST, TypeHint::UriList),
// Plaintext
(TEXT_PLAIN, TypeHint::Plaintext),
(TEXT_PLAIN_CHARSET_UTF8, TypeHint::Plaintext),
// HTML
(TEXT_HTML, TypeHint::Html),
(TEXT_HTML_CHARSET_UTF8, TypeHint::Html),
// RTF
(APPLICATION_RTF, TypeHint::Rtf),
// Audio
(AUDIO_AAC, TypeHint::Audio { extension_hint: Some("aac") }),
(AUDIO_AIFF, TypeHint::Audio { extension_hint: Some("aif") }),
(AUDIO_FLAC, TypeHint::Audio { extension_hint: Some("flac") }),
(AUDIO_VND_WAV, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_VND_WAVE, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_WAV, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_WAVE, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_X_WAV, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_OGG, TypeHint::Audio { extension_hint: Some("ogg") }),
(AUDIO_MPEG, TypeHint::Audio { extension_hint: Some("mp3") }),
// Image
(IMAGE_BMP, TypeHint::Image { extension_hint: Some("bmp") }),
(IMAGE_GIF, TypeHint::Image { extension_hint: Some("gif") }),
(IMAGE_JPEG, TypeHint::Image { extension_hint: Some("jpg") }),
(IMAGE_PJPEG, TypeHint::Image { extension_hint: Some("jpg") }),
(IMAGE_PNG, TypeHint::Image { extension_hint: Some("png") }),
(IMAGE_RAW, TypeHint::Image { extension_hint: Some("raw") }),
(IMAGE_SVG, TypeHint::Image { extension_hint: Some("svg") }),
(IMAGE_TIFF, TypeHint::Image { extension_hint: Some("tiff") }),
(IMAGE_WEBP, TypeHint::Image { extension_hint: Some("webp") }),
(IMAGE_X_ICON, TypeHint::Image { extension_hint: Some("ico") }),
];
// Returns an iterator so that things like the multiple charsets for plaintext/HTML
// and the multiple ways of expressing .wav work correctly.
pub(crate) fn from_dyn(type_: &dyn TransferType) -> impl Iterator<Item = Self> {
let downcast = type_.cast_ref::<Self>().cloned();
let downcast_failed = downcast.is_none();
// This filter is a bit hacky, but it's the only way to ensure that we always
// return the same type.
let from_hint = downcast_failed
.then_some(
Self::MIME_HINT_MAP
.iter()
.filter(move |(_, haystack)| TransferType::matches(haystack, type_))
.map(move |(mime, _)| Self {
mime: mime.to_string().into(),
hint: type_.hint(),
}),
)
.into_iter()
.flatten();
downcast.into_iter().chain(from_hint)
}
// TODO: We should properly parse MIME types using `mime` or a similar crate.
fn parse_charset(&self) -> Result<Charset, UnexpectedCharsetError<'_>> {
let Some((_, charset)) = self
.mime
.split_once(';')
.and_then(|(_essence, options)| options.split_once("charset="))
else {
return Ok(Default::default());
};
let charset = charset.split_once(',').map(|(first, _)| first).unwrap_or(charset).trim();
if charset == "utf-8" { Ok(Charset::Utf8) } else { Err(UnexpectedCharsetError(charset)) }
}
fn parse(mime: String) -> Self {
let hint = Self::MIME_HINT_MAP
.iter()
.find_map(|(haystack, hint)| (*haystack == &*mime).then_some(*hint))
.or_else(|| {
if mime.starts_with("image/") {
Some(TypeHint::Image { extension_hint: None })
} else if mime.starts_with("audio/") {
Some(TypeHint::Audio { extension_hint: None })
} else {
None
}
});
Self { mime: mime.into(), hint }
}
}
impl fmt::Display for MimeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.mime.fmt(f)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct UnknownTypeHint(pub TypeHint);
impl fmt::Display for UnknownTypeHint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unknown type hint: {:?}", self.0)
}
}
impl TryFrom<TypeHint> for MimeType {
type Error = UnknownTypeHint;
fn try_from(hint: TypeHint) -> Result<Self, Self::Error> {
let mime = Self::MIME_HINT_MAP
.iter()
.find_map(|(mime, haystack)| (*haystack == hint).then_some(*mime))
.ok_or(UnknownTypeHint(hint))?;
Ok(Self { mime: mime.to_owned().into(), hint: Some(hint) })
}
}
impl TransferType for MimeType {
fn hint(&self) -> Option<TypeHint> {
self.hint
}
fn matches(&self, other: &dyn TransferType) -> bool {
if let Some(other_mime) = other.cast_ref::<Self>() {
*self == *other_mime
} else {
// If either hint is `None`, return false
self.hint().is_some_and(|hint| other.hint() == Some(hint))
}
}
}
type BytesResult = Result<Vec<u8>, Arc<io::Error>>;
/// Typed data transfer from another application.
#[derive(Debug)]
pub struct MimeData {
mime_type: MimeType,
result: BytesResult,
}
impl MimeData {
pub(crate) fn new(mime_type: MimeType, result: BytesResult) -> Self {
Self { mime_type, result }
}
fn data(&self) -> io::Result<&[u8]> {
fn arc_to_io_error(arc: Arc<io::Error>) -> io::Error {
io::Error::new(arc.kind(), arc)
}
self.result.as_deref().map_err(|e| arc_to_io_error(e.clone()))
}
}
impl TypedData for MimeData {
fn type_(&self) -> &dyn TransferType {
&self.mime_type
}
fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
let data = self.data().ok()?.to_owned();
Some(Box::new(io::Cursor::new(data)))
}
fn try_as_bytes(&self) -> io::Result<Vec<u8>> {
self.data().map(ToOwned::to_owned)
}
fn try_as_uris(&self) -> io::Result<Vec<String>> {
let data = self.data()?;
Cursor::new(&data)
.lines()
.filter(|result| match result {
Ok(s) => !s.starts_with('#'),
// We want to maintain errors, so the final `collect` returns an error too
Err(_) => true,
})
.collect()
}
fn try_as_string(&self) -> io::Result<String> {
let charset = self.mime_type.parse_charset();
let data = self.data()?;
match charset {
Ok(Charset::Utf8) => String::from_utf8(data.to_vec())
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)),
Err(e) => Err(io::Error::other(e.to_string())),
}
}
}
/// A wrapper around `WlDataOffer`, implementing `DataTransfer`.
#[derive(Debug, Clone)]
pub struct DataOffer {
mime_types: Arc<[MimeType]>,
data: WlDataOffer,
available_actions: WlDndAction,
data_device_id: ObjectId,
serial: u32,
window_id: WindowId,
}
pub(crate) fn dnd_action_winit_to_wl(winit: DndAction) -> WlDndAction {
match winit {
DndAction::Move => WlDndAction::Move,
DndAction::Copy => WlDndAction::Copy,
DndAction::Ask => WlDndAction::Ask,
_ => WlDndAction::empty(),
}
}
pub(crate) fn dnd_action_wl_to_winit(wl: WlDndAction) -> Option<DndAction> {
match wl {
WlDndAction::Move => Some(DndAction::Move),
WlDndAction::Copy => Some(DndAction::Copy),
WlDndAction::Ask => Some(DndAction::Ask),
_ => None,
}
}
impl DataOffer {
pub(crate) fn transfer_id(&self) -> DataTransferId {
make_data_transfer_id(self.data_device_id.clone(), self.serial)
}
pub(crate) fn first_mime_type(&self) -> Option<&MimeType> {
self.mime_types.first()
}
pub(crate) fn serial(&self) -> u32 {
self.serial
}
pub(crate) fn window_id(&self) -> WindowId {
self.window_id
}
pub(crate) fn set_actions(&self, action_set: &[DndAction]) -> bool {
let preferred_action = action_set.iter().find_map(|winit| {
let wl = dnd_action_winit_to_wl(*winit);
self.available_actions.intersects(wl).then_some(wl)
});
let any = preferred_action.is_some();
let all_actions = action_set
.iter()
.copied()
.map(dnd_action_winit_to_wl)
.fold(WlDndAction::empty(), BitOr::bitor);
self.data.set_actions(all_actions, preferred_action.unwrap_or(WlDndAction::empty()));
any
}
pub(crate) fn find_type_dyn<'a>(&'a self, type_: &'a dyn TransferType) -> Option<&'a MimeType> {
match type_.cast_ref::<MimeType>() {
Some(mime_type) => Some(mime_type),
None => {
let hint = type_.hint()?;
self.mime_types.iter().find(|mime_type| {
mime_type.hint().is_some_and(|haystack| haystack.matches(&hint))
})
},
}
}
}
impl Deref for DataOffer {
type Target = WlDataOffer;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl DataTransfer for DataOffer {
fn for_each_available_type<'this>(
&'this self,
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>,
) {
let _ = self.mime_types.iter().map(|mime| mime as &dyn TransferType).try_for_each(func);
}
}
/// Wrapper for [`WlDataSource`], which exposes the types that are advertised by a data
/// transfer operation, along with the data that the source represents
#[derive(Debug)]
pub struct DragSource {
pub(crate) data_transfer_id: DataTransferId,
/// The `WlDataSource` generated from `data`.
///
/// This is stored internally, as if this source is dropped then the
/// drag operation will be cancelled.
_data_source: SctkDragSource,
/// The supplied [`DataTransferSend`].
pub(crate) data: Box<dyn DataTransferSend>,
pub(crate) selected_action: WlDndAction,
pub(crate) window_id: WindowId,
/// (Optionally) an icon for the drag-and-drop operation.
_icon: Option<WlSurface>,
}
impl DragSource {
pub(crate) fn new(
data_transfer_id: DataTransferId,
data_source: SctkDragSource,
data: Box<dyn DataTransferSend>,
icon: Option<WlSurface>,
window_id: WindowId,
) -> Self {
Self {
data_transfer_id,
_data_source: data_source,
data,
selected_action: WlDndAction::None,
window_id,
_icon: icon,
}
}
/// Per-type data to be sent. See [`DataTransferSend`].
pub fn data(&mut self) -> &mut dyn DataTransferSend {
&mut *self.data
}
}
/// The current state of an in-progress drag-and-drop operation.
#[derive(Debug, Default)]
pub struct DndState {
receive_drag: Option<DataOffer>,
send_drag: Option<DragSource>,
}
impl DndState {
pub(crate) fn receive_drag(&self) -> Option<&DataOffer> {
self.receive_drag.as_ref()
}
pub(crate) fn set_send_drag(&mut self, source: DragSource) {
self.send_drag = Some(source);
}
pub(crate) fn send_drag(&self) -> Option<&DragSource> {
self.send_drag.as_ref()
}
pub(crate) fn set_target_drag_action(&mut self, action: WlDndAction) {
if let Some(source) = &mut self.send_drag {
source.selected_action = action;
}
}
/// Returns `true` if a drag operation was in progress, `false` if no drag operation was in
/// progress.
pub(crate) fn clear_send_drag(&mut self) -> bool {
self.send_drag.take().is_some()
}
pub(crate) fn send_drag_data_mut(&mut self) -> Option<&mut dyn DataTransferSend> {
self.send_drag.as_mut().map(|send| send.data())
}
}
impl DataOfferHandler for WinitState {
fn source_actions(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
offer: &mut DragOffer,
actions: WlDndAction,
) {
let _ = actions;
let _ = offer;
let _ = qh;
let _ = conn;
// Not implemented, but required for `DataDeviceHandler`.
}
fn selected_action(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
offer: &mut DragOffer,
actions: WlDndAction,
) {
let _ = actions;
let _ = offer;
let _ = qh;
let _ = conn;
}
}
impl DataDeviceHandler for WinitState {
fn enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
data_device: &WlDataDevice,
x: f64,
y: f64,
wl_surface: &WlSurface,
) {
let Some(data) = data_device.data::<DataDeviceData>() else {
return;
};
let Some(drag) = data.drag_offer() else {
// Selections are not yet implemented
return;
};
let window_id = crate::make_wid(wl_surface);
let current_drag = drag.with_mime_types(|types| DataOffer {
mime_types: types
.iter()
.map(|str| MimeType::parse(str.clone()))
.collect::<Vec<_>>()
.into(),
available_actions: drag.source_actions,
serial: drag.serial,
data_device_id: data_device.id(),
data: drag.inner().clone(),
window_id,
});
current_drag.set_actions(&[]);
let id = current_drag.transfer_id();
self.dnd_state.receive_drag = Some(current_drag);
let scale_factor = self
.windows
.borrow()
.get(&window_id)
.map(|window| window.lock().unwrap().scale_factor())
.unwrap_or(1.);
let position: PhysicalPosition<f64> = LogicalPosition::new(x, y).to_physical(scale_factor);
self.events_sink.push_window_event(
WindowEvent::DragEntered { id, position: Some(position) },
window_id,
);
}
fn leave(&mut self, _: &Connection, _: &QueueHandle<Self>, data_device: &WlDataDevice) {
let Some(data) = data_device.data::<DataDeviceData>() else {
return;
};
if let Some(current_drag) = self.dnd_state.receive_drag() {
self.events_sink.push_window_event(
WindowEvent::DragLeft { id: current_drag.transfer_id() },
current_drag.window_id(),
);
if let Some(receive_drag) = self.dnd_state.receive_drag.take() {
receive_drag.finish();
}
}
if let Some(drag) = data.drag_offer() {
drag.destroy();
}
if let Some(selection) = data.selection_offer() {
selection.destroy();
}
}
fn motion(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
data_device: &WlDataDevice,
x: f64,
y: f64,
) {
let Some(data) = data_device.data::<DataDeviceData>() else {
return;
};
let Some(drag) = data.drag_offer() else {
// Selections (copy/paste) are not yet implemented
return;
};
// `selected_action` should only contain a single flag, but we check with `contains`
// just in case we or the compositor misunderstood the spec.
let proposed_action = if drag.selected_action.contains(WlDndAction::Move) {
Some(DndAction::Move)
} else if drag.selected_action.contains(WlDndAction::Copy) {
Some(DndAction::Copy)
} else if drag.selected_action.contains(WlDndAction::Ask) {
Some(DndAction::Ask)
} else {
None
};
let Some(current_drag) = self.dnd_state.receive_drag() else {
return;
};
let window_id = crate::make_wid(&drag.surface);
let scale_factor = self
.windows
.borrow()
.get(&window_id)
.map(|window| window.lock().unwrap().scale_factor())
.unwrap_or(1.);
let position: PhysicalPosition<f64> = LogicalPosition::new(x, y).to_physical(scale_factor);
self.events_sink.push_window_event(
WindowEvent::DragPosition { id: current_drag.transfer_id(), position, proposed_action },
window_id,
);
}
fn selection(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {
// We don't handle selections right now.
}
fn drop_performed(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
data_device: &WlDataDevice,
) {
let Some(data) = data_device.data::<DataDeviceData>() else {
return;
};
let Some(drag) = data.drag_offer() else {
// Selections (copy/paste) are not yet implemented
return;
};
let Some(current_drag) = self.dnd_state.receive_drag() else {
return;
};
let window_id = crate::make_wid(&drag.surface);
// `selected_action` should only contain a single flag, but we check with `contains`
// just in case we or the compositor misunderstood the spec.
let proposed_action = if drag.selected_action.contains(WlDndAction::Move) {
Some(DndAction::Move)
} else if drag.selected_action.contains(WlDndAction::Copy) {
Some(DndAction::Copy)
} else if drag.selected_action.contains(WlDndAction::Ask) {
Some(DndAction::Ask)
} else {
None
};
self.events_sink.push_window_event(
WindowEvent::DragDropped { id: current_drag.transfer_id(), proposed_action },
window_id,
);
if let Some(receive_drag) = self.dnd_state.receive_drag.take() {
receive_drag.finish();
}
if let Some(drag) = data.drag_offer() {
drag.destroy();
}
if let Some(selection) = data.selection_offer() {
selection.destroy();
}
}
}
sctk::delegate_data_device!(WinitState);

View File

@@ -1,35 +1,46 @@
//! The event-loop routines.
use std::cell::{Cell, RefCell};
use std::io::Result as IOResult;
use std::mem;
use std::io::{self, Read, Result as IOResult};
use std::ops::BitOr;
use std::os::fd::OwnedFd;
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use std::{fmt, mem};
use calloop::PostAction;
use calloop::ping::Ping;
use dpi::LogicalSize;
use rustix::event::{PollFd, PollFlags};
use rustix::pipe::{self, PipeFlags};
use sctk::data_device_manager::{ReadPipe, data_offer};
use sctk::reexports::calloop_wayland_source::WaylandSource;
use sctk::reexports::client::{Connection, QueueHandle, globals};
use sctk::shell::WaylandSurface;
use tracing::warn;
use wayland_client::Proxy;
use wayland_client::protocol::wl_data_device_manager::DndAction as WlDndAction;
use wayland_client::protocol::wl_shm::Format;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::data_transfer::{DataTransfer, DataTransferId, DataTransferSend, TransferType};
use winit_core::error::{EventLoopError, NotSupportedError, OsError, RequestError};
use winit_core::event::{DeviceEvent, StartCause, SurfaceSizeWriter, WindowEvent};
use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
OwnedDisplayHandle as CoreOwnedDisplayHandle,
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
DndAction, DragIcon, OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use winit_core::icon::RgbaIcon;
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
use winit_core::window::Theme;
use crate::dnd::{MimeData, dnd_action_winit_to_wl};
use crate::types::cursor::WaylandCustomCursor;
use crate::{DragSource, MimeType, image_to_buffer, make_data_transfer_id};
mod proxy;
pub mod sink;
@@ -678,8 +689,233 @@ 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 state = self.state.borrow_mut();
let Some(current_drag) = state.dnd_state.receive_drag() else {
return Err(RequestError::Ignored);
};
if current_drag.transfer_id() != id {
return Err(RequestError::Ignored);
}
let Some(mime_type) = current_drag.find_type_dyn(type_) else {
return Err(RequestError::Ignored);
};
let mime_type_str = mime_type.to_string();
// create a pipe
let (readfd, writefd) =
pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK).map_err(|e| os_error!(e))?;
let async_request_serial = AsyncRequestSerial::get();
let mut buffer = Vec::new();
let window_id = current_drag.window_id();
let mut mime_type = Some(mime_type.clone());
let _ = state.loop_handle.insert_source(ReadPipe::from(readfd), move |_, file, state| {
// SAFETY: We do not overwrite the referent of `file`
let file = unsafe { file.get_mut() };
let result = match file.read_to_end(&mut buffer) {
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
return PostAction::Continue;
},
Ok(0) => Ok(mem::take(&mut buffer)),
Ok(_) => {
return PostAction::Continue;
},
Err(e) => Err(Arc::new(e)),
};
state.events_sink.push_window_event(
WindowEvent::DataTransferReceived {
id,
serial: async_request_serial,
// `unwrap` is safe here, as we always return `PostAction::Remove` in this
// branch.
value: Arc::new(MimeData::new(mime_type.take().unwrap(), result)),
},
window_id,
);
PostAction::Remove
});
current_drag.accept(current_drag.serial(), Some(mime_type_str.clone()));
data_offer::receive_to_fd(current_drag, mime_type_str, writefd);
Ok(async_request_serial)
}
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
let state = self.state.borrow();
let Some(state) = state.dnd_state.receive_drag() else {
return Err(RequestError::Ignored);
};
if state.transfer_id() != id {
return Err(RequestError::Ignored);
}
Ok(Box::new(state.clone()))
}
fn set_valid_dnd_actions(
&self,
id: DataTransferId,
actions: &[DndAction],
) -> Result<(), RequestError> {
let state = self.state.borrow();
let Some(state) = state.dnd_state.receive_drag() else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
if state.transfer_id() != id {
return Err(os_error!(UnknownDataTransfer(id)).into());
}
let any_actions = state.set_actions(actions);
let accepted_type =
if any_actions { state.first_mime_type().map(|mime| mime.to_string()) } else { None };
// Some compositors won't even send the "dropped" event if no type
// has been accepted, so we need to accept _something_ here. The
// application can accept further types by fetching the data, but
// this will at least mean that waiting until the drop to start
// fetching data won't prevent the drop from working at all.
state.accept(state.serial(), accepted_type);
Ok(())
}
fn start_drag(
&self,
source: WindowId,
send_data: Box<dyn DataTransferSend>,
action_mask: &[DndAction],
icon: Option<DragIcon>,
) -> Result<DataTransferId, RequestError> {
const NO_POINTER_CAP_ERROR_MSG: &str =
"Tried to initiate drag, but source window does not have the pointer capability";
let mut state = self.state.borrow_mut();
let dnd_actions = action_mask
.iter()
.copied()
.map(dnd_action_winit_to_wl)
.fold(WlDndAction::empty(), BitOr::bitor);
let data_device_manager = state
.data_device_manager_state
.as_ref()
.ok_or(NotSupportedError::new("Tried to initiate drag, but data device not enabled"))?;
let mut mime_types = Vec::new();
send_data.for_each_available_type(&mut |ty_| {
for mime in MimeType::from_dyn(ty_) {
mime_types.push(mime);
}
std::ops::ControlFlow::Continue(())
});
let data_source = data_device_manager.create_drag_and_drop_source(
&self.queue_handle,
mime_types,
dnd_actions,
);
let icon_surface = {
let mut pool = state.image_pool.lock().unwrap();
icon.and_then(|icon| {
let rgba = icon.icon.cast_ref::<RgbaIcon>()?;
let width = rgba.width().try_into().ok()?;
let height = rgba.height().try_into().ok()?;
let buffer =
image_to_buffer(width, height, rgba.buffer(), Format::Argb8888, &mut pool)
.ok()?;
let surface = state.compositor_state.create_surface(&self.queue_handle);
if surface.version() >= 5 {
buffer.attach_to(&surface).ok()?;
surface.offset(icon.offset_x, icon.offset_y);
} else {
surface.attach(Some(buffer.wl_buffer()), icon.offset_x, icon.offset_y);
}
Some(surface)
})
};
// New scope to ensure we drop the locks as soon as possible.
let transfer_id = {
let windows = state.windows.borrow();
let source_window_mutex = windows
.get(&source)
.ok_or(os_error!("Tried to initiate drag, but source window ID was invalid"))?;
let source_window_state = source_window_mutex.lock().unwrap();
let source_surface = source_window_state.window.wl_surface();
let seat = source_window_state
.focused_seats()
.find_map(|seat_id| {
// HACK: How do we get the correct seat for pointers here?
state.seats.get(seat_id).filter(|seat| seat.data_device().is_some())
})
.ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?;
let data_device =
seat.data_device().ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?;
let serial = seat
.pointer_data()
.ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?
.latest_button_serial();
data_source.start_drag(data_device, source_surface, icon_surface.as_ref(), serial);
make_data_transfer_id(data_device.inner().id(), serial)
};
// For some reason, if we commit before starting the drag then the offset isn't applied.
// This doesn't seem to be documented anywhere, and it's possible that it's a bug in KDE.
if let Some(surface) = &icon_surface {
surface.commit();
}
state.dnd_state.set_send_drag(DragSource::new(
transfer_id,
data_source,
send_data,
icon_surface,
source,
));
Ok(transfer_id)
}
}
/// 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 {}
impl ActiveEventLoop {
fn clear_exit(&self) {
self.exit.set(None)

View File

@@ -18,13 +18,16 @@
#![allow(clippy::mutable_key_type)]
use std::ffi::c_void;
use std::hash::BuildHasher;
use std::ptr::NonNull;
use dpi::{LogicalSize, PhysicalSize};
use sctk::reexports::client::Proxy;
use sctk::reexports::client::backend::ObjectId;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::shm::slot::{Buffer, CreateBufferError, SlotPool};
use wayland_client::protocol::wl_shm::Format;
use winit_core::data_transfer::DataTransferId;
use winit_core::event_loop::ActiveEventLoop as CoreActiveEventLoop;
use winit_core::window::{
ActivationToken, PlatformWindowAttributes, Window as CoreWindow, WindowId,
@@ -34,6 +37,7 @@ macro_rules! os_error {
($error:expr) => {{ winit_core::error::OsError::new(line!(), file!(), $error) }};
}
mod dnd;
mod event_loop;
mod output;
mod seat;
@@ -41,6 +45,7 @@ mod state;
mod types;
mod window;
pub use self::dnd::{DataOffer, DragSource, MimeData, MimeType};
pub use self::event_loop::{ActiveEventLoop, EventLoop};
pub use self::window::Window;
@@ -149,6 +154,17 @@ fn make_wid(surface: &WlSurface) -> WindowId {
WindowId::from_raw(surface.id().as_ptr() as usize)
}
/// Create a `DataTransferId` for the given data device and serial.
///
/// It's currently unclear if this will result in the same ID when transferring to the same
/// application.
#[inline]
fn make_data_transfer_id(data_device_id: ObjectId, serial: u32) -> DataTransferId {
const BUILD_HASHER: foldhash::fast::FixedState = foldhash::fast::FixedState::with_seed(0);
DataTransferId::from_raw(BUILD_HASHER.hash_one((data_device_id, serial)) as i64)
}
/// The default routine does floor, but we need round on Wayland.
fn logical_to_physical_rounded(size: LogicalSize<u32>, scale_factor: f64) -> PhysicalSize<u32> {
let width = size.width as f64 * scale_factor;

View File

@@ -3,6 +3,7 @@
use std::sync::Arc;
use foldhash::HashMap;
use sctk::data_device_manager::data_device::DataDevice;
use sctk::reexports::client::backend::ObjectId;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::protocol::wl_touch::WlTouch;
@@ -64,6 +65,9 @@ pub struct WinitSeatState {
/// The hold pointer gesture bound on the seat.
pointer_gesture_hold: Option<ZwpPointerGestureHoldV1>,
/// The drag-and-drop state
data_device: Option<DataDevice>,
/// The keyboard bound on the seat.
keyboard_state: Option<KeyboardState>,
@@ -78,6 +82,14 @@ impl WinitSeatState {
pub fn new() -> Self {
Default::default()
}
pub(crate) fn data_device(&self) -> Option<&DataDevice> {
self.data_device.as_ref()
}
pub(crate) fn pointer_data(&self) -> Option<&WinitPointerData> {
self.pointer.as_ref().and_then(|pointer| pointer.pointer().data())
}
}
impl SeatHandler for WinitState {
@@ -129,6 +141,11 @@ impl SeatHandler for WinitState {
)
.expect("failed to create pointer with present capability.");
seat_state.data_device = self
.data_device_manager_state
.as_ref()
.map(|device| device.get_data_device(queue_handle, &seat));
seat_state.relative_pointer = self.relative_pointer.as_ref().map(|manager| {
manager.get_relative_pointer(
themed_pointer.pointer(),
@@ -225,6 +242,8 @@ impl SeatHandler for WinitState {
pointer_gesture_hold.destroy();
}
seat_state.data_device = None;
if let Some(pointer) = seat_state.pointer.take() {
let pointer_data = pointer.pointer().winit_data();

View File

@@ -22,19 +22,19 @@ use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport;
use sctk::compositor::SurfaceData;
use sctk::globals::GlobalData;
use sctk::seat::SeatState;
use sctk::seat::pointer::{
PointerData, PointerDataExt, PointerEvent, PointerEventKind, PointerHandler,
};
use sctk::seat::SeatState;
use dpi::{LogicalPosition, PhysicalPosition};
use winit_core::event::{
ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource, TouchPhase,
WindowEvent, ButtonSource,
ButtonSource, ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource,
TouchPhase, WindowEvent,
};
use crate::state::WinitState;
use crate::WindowId;
use crate::state::WinitState;
pub mod pointer_gesture;
pub mod relative_pointer;

View File

@@ -3,8 +3,8 @@
use std::ops::Deref;
use sctk::reexports::client::globals::{BindError, GlobalList};
use sctk::reexports::client::{delegate_dispatch, Dispatch};
use sctk::reexports::client::{Connection, QueueHandle};
use sctk::reexports::client::{Dispatch, delegate_dispatch};
use sctk::reexports::protocols::wp::relative_pointer::zv1::{
client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
@@ -12,8 +12,8 @@ use sctk::reexports::protocols::wp::relative_pointer::zv1::{
use sctk::globals::GlobalData;
use winit_core::event::DeviceEvent;
use crate::state::WinitState;
use winit_core::event::DeviceEvent;
/// Wrapper around the relative pointer.
#[derive(Debug)]

View File

@@ -4,6 +4,7 @@ use std::sync::{Arc, Mutex};
use foldhash::HashMap;
use sctk::compositor::{CompositorHandler, CompositorState};
use sctk::data_device_manager::DataDeviceManagerState;
use sctk::output::{OutputHandler, OutputState};
use sctk::reexports::calloop::LoopHandle;
use sctk::reexports::client::backend::ObjectId;
@@ -23,6 +24,7 @@ use sctk::subcompositor::SubcompositorState;
use winit_core::error::OsError;
use crate::WindowId;
use crate::dnd::DndState;
use crate::event_loop::sink::EventSink;
use crate::output::MonitorHandle;
use crate::seat::{
@@ -113,12 +115,18 @@ pub struct WinitState {
/// Viewporter state on the given window.
pub viewporter_state: Option<ViewporterState>,
/// Data device manager state on the given window.
pub data_device_manager_state: Option<DataDeviceManagerState>,
/// Fractional scaling manager.
pub fractional_scaling_manager: Option<FractionalScalingManager>,
/// Blur manager.
pub blur_manager: Option<BgrEffectManager>,
/// Drag-and-drop state.
pub dnd_state: DndState,
/// Loop handle to re-register event sources, such as keyboard repeat.
pub loop_handle: LoopHandle<'static, Self>,
@@ -168,6 +176,17 @@ impl WinitState {
(None, None)
};
let data_device_manager_state = match DataDeviceManagerState::bind(globals, queue_handle) {
Ok(state) => Some(state),
Err(e) => {
tracing::warn!(
"Data device manager not available, clipboard and drag-and-drop disabled: \
{e:?}"
);
None
},
};
let shm = Shm::bind(globals, queue_handle).map_err(|err| os_error!(err))?;
let image_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap()));
@@ -191,9 +210,12 @@ impl WinitState {
window_compositor_updates: Vec::new(),
window_events_sink: Default::default(),
viewporter_state,
data_device_manager_state,
fractional_scaling_manager,
blur_manager: BgrEffectManager::new(globals, queue_handle).ok(),
dnd_state: Default::default(),
seats,
text_input_state: TextInputState::new(globals, queue_handle).ok(),

View File

@@ -243,6 +243,12 @@ impl WindowState {
}
}
// HACK: Currently to get the data device to initiate a drag-and-drop, we iterate through all
// focused seats to find one with a pointer capability. This is definitely wrong.
pub(crate) fn focused_seats(&self) -> impl Iterator<Item = &ObjectId> {
self.seat_focus.iter()
}
/// Apply closure on the given pointer.
fn apply_on_pointer<F: FnMut(&ThemedPointer<WinitPointerData>, &WinitPointerData)>(
&self,