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

@@ -13,7 +13,7 @@ macro_rules! atom_manager {
/// Indices into the `Atoms` struct.
#[derive(Copy, Clone, Debug)]
#[allow(non_camel_case_types)]
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
pub enum AtomName {
$($name,)*
}
@@ -34,6 +34,7 @@ macro_rules! atom_manager {
atom_manager! {
// General Use Atoms
CARD32,
STRING,
UTF8_STRING,
WM_CHANGE_STATE,
WM_CLIENT_MACHINE,
@@ -87,11 +88,39 @@ atom_manager! {
XdndDrop,
XdndPosition,
XdndStatus,
XdndActionPrivate,
XdndActionCopy,
XdndSelection,
XdndFinished,
XdndTypeList,
// MIME types for reading selections
TextUriList: b"text/uri-list",
TextPlain: b"text/plain",
TextPlainCharsetUtf8: b"text/plain;charset=utf-8",
TextHtml: b"text/html",
TextHtmlCharsetUtf8: b"text/html;charset=utf-8",
ApplicationRtf: b"application/rtf",
AudioAac: b"audio/aac",
AudioAiff: b"audio/aiff",
AudioFlac: b"audio/flac",
AudioWav: b"audio/wav",
AudioWave: b"audio/wave",
AudioXWav: b"audio/x-wav",
AudioVndWav: b"audio/vnd.wav",
AudioVndWave: b"audio/vnd.wave",
AudioMpeg: b"audio/mpeg",
AudioOgg: b"audio/ogg",
ImageBmp: b"image/bmp",
ImageGif: b"image/gif",
ImageJpeg: b"image/jpeg",
ImagePjpeg: b"image/pjpeg",
ImagePng: b"image/png",
ImageSvg: b"image/svg+xml",
ImageTiff: b"image/tiff",
ImageWebp: b"image/webp",
ImageXIcon: b"image/x-icon",
ImageRaw: b"image/x-panasonic-raw",
None: b"None",
// Miscellaneous Atoms

View File

@@ -1,18 +1,19 @@
use std::collections::VecDeque;
use std::io;
use std::os::raw::*;
use std::path::{Path, PathBuf};
use std::str::Utf8Error;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use dpi::PhysicalPosition;
use percent_encoding::percent_decode;
use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData};
use winit_core::event_loop::AsyncRequestSerial;
use x11rb::protocol::xproto::{self, ConnectionExt};
use crate::atoms::AtomName::None as DndNone;
use crate::atoms::*;
use crate::event_loop::{CookieResultExt, X11Error};
use crate::util;
use crate::xdisplay::XConnection;
use crate::{XWindow, util};
#[derive(Debug, Clone, Copy)]
pub enum DndState {
@@ -21,98 +22,264 @@ pub enum DndState {
}
#[derive(Debug)]
pub enum DndDataParseError {
pub enum UriListParseError {
EmptyData,
InvalidUtf8(#[allow(dead_code)] Utf8Error),
HostnameSpecified(#[allow(dead_code)] String),
UnexpectedProtocol(#[allow(dead_code)] String),
UnresolvablePath(#[allow(dead_code)] io::Error),
Io(#[allow(dead_code)] io::Error),
}
impl From<Utf8Error> for DndDataParseError {
impl From<Utf8Error> for UriListParseError {
fn from(e: Utf8Error) -> Self {
DndDataParseError::InvalidUtf8(e)
UriListParseError::InvalidUtf8(e)
}
}
impl From<io::Error> for DndDataParseError {
impl From<io::Error> for UriListParseError {
fn from(e: io::Error) -> Self {
DndDataParseError::UnresolvablePath(e)
UriListParseError::UnresolvablePath(e)
}
}
#[derive(Debug)]
pub struct SelectionReader {
type_: SelectionType,
data: Vec<u8>,
}
impl TypedData for SelectionReader {
fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
Some(Box::new(io::Cursor::new(self.data.clone())))
}
fn type_(&self) -> &dyn TransferType {
&self.type_
}
fn try_as_string(&self) -> io::Result<String> {
fn invalid_data<E>(err: E) -> io::Error
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
io::Error::new(io::ErrorKind::InvalidData, err)
}
fn decode_utf16_bytes(bytes: &[u8]) -> io::Result<String> {
let utf16 = bytes
.chunks_exact(2)
.map(|chunk| {
let bytes: &[u8; 2] = chunk.try_into().unwrap();
u16::from_ne_bytes(*bytes)
})
.collect::<Vec<_>>();
String::from_utf16(&utf16).map_err(invalid_data)
}
match self.type_.hint() {
Some(TypeHint::Plaintext) | Some(TypeHint::Html) => std::str::from_utf8(&self.data)
.map(|str| str.to_owned())
.map_err(invalid_data)
.or_else(|_| decode_utf16_bytes(&self.data)),
Some(TypeHint::UriList) => String::from_utf8(self.data.clone()).map_err(invalid_data),
_ => Err(io::ErrorKind::InvalidData.into()),
}
}
fn try_as_uris(&self) -> io::Result<Vec<String>> {
if self.type_().hint() != Some(TypeHint::UriList) {
return Err(io::ErrorKind::InvalidData.into());
}
Ok(self
.try_as_string()?
.split(['\n', '\r'])
.filter(|s| !s.is_empty())
.map(ToOwned::to_owned)
.collect())
}
}
#[derive(Debug)]
pub struct DragState {
// Populated by XdndEnter event handler
pub version: c_long,
pub transfer_id: DataTransferId,
pub types: Arc<[SelectionType]>,
// Populated by Xdnd* event handlers
pub source_window: xproto::Window,
// Populated by Xdnd* event handlers
pub target_window: xproto::Window,
// Populated by `fetch_data_transfer`
pub pending_fetch_types: VecDeque<(AsyncRequestSerial, SelectionType)>,
pub finished: Option<(XWindow, XWindow)>,
/// Whether the drag operation is accepted (or `None` if the user never indicated that it's
/// accepted or rejected)
// Populated by `Window::accept_drag`/`Window::reject_drag`.
pub accepted: bool,
}
impl Default for DragState {
fn default() -> Self {
static DATA_TRANSFER_ID: AtomicI64 = AtomicI64::new(0);
Self {
version: Default::default(),
transfer_id: DataTransferId::from_raw(DATA_TRANSFER_ID.fetch_add(1, Ordering::Relaxed)),
types: Default::default(),
source_window: Default::default(),
target_window: Default::default(),
pending_fetch_types: Default::default(),
finished: None,
accepted: Default::default(),
}
}
}
#[derive(Debug)]
pub struct Dnd {
xconn: Arc<XConnection>,
// Populated by XdndEnter event handler
pub version: Option<c_long>,
pub type_list: Option<Vec<xproto::Atom>>,
// Populated by XdndPosition event handler
pub source_window: Option<xproto::Window>,
// Populated by XdndPosition event handler
pub position: PhysicalPosition<f64>,
// Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
pub result: Option<Result<Vec<PathBuf>, DndDataParseError>>,
// Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
pub dragging: bool,
// If `None`, no drag operation is in progress.
state: Option<DragState>,
}
#[derive(Debug)]
pub struct Selection {
types: Arc<[SelectionType]>,
}
impl Selection {
pub(crate) fn new(types: Arc<[SelectionType]>) -> Selection {
Selection { types }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SelectionType {
hint: Option<TypeHint>,
atom: xproto::Atom,
}
impl SelectionType {
pub(crate) fn new(atoms: &Atoms, atom: xproto::Atom) -> Self {
let atom_to_hint = [
// Files
(atoms[TextUriList], TypeHint::UriList),
// Plaintext
(atoms[STRING], TypeHint::Plaintext),
(atoms[UTF8_STRING], TypeHint::Plaintext),
(atoms[TextPlain], TypeHint::Plaintext),
(atoms[TextPlainCharsetUtf8], TypeHint::Plaintext),
// HTML
(atoms[TextHtml], TypeHint::Html),
(atoms[TextHtmlCharsetUtf8], TypeHint::Html),
// RTF
(atoms[ApplicationRtf], TypeHint::Rtf),
// Audio
(atoms[AudioAac], TypeHint::Audio { extension_hint: Some("aac") }),
(atoms[AudioAiff], TypeHint::Audio { extension_hint: Some("aif") }),
(atoms[AudioFlac], TypeHint::Audio { extension_hint: Some("flac") }),
(atoms[AudioVndWav], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioVndWave], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioWav], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioWave], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioXWav], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioOgg], TypeHint::Audio { extension_hint: Some("ogg") }),
(atoms[AudioMpeg], TypeHint::Audio { extension_hint: Some("mp3") }),
// Image
(atoms[ImageBmp], TypeHint::Image { extension_hint: Some("bmp") }),
(atoms[ImageGif], TypeHint::Image { extension_hint: Some("gif") }),
(atoms[ImageJpeg], TypeHint::Image { extension_hint: Some("jpg") }),
(atoms[ImagePjpeg], TypeHint::Image { extension_hint: Some("jpg") }),
(atoms[ImagePng], TypeHint::Image { extension_hint: Some("png") }),
(atoms[ImageRaw], TypeHint::Image { extension_hint: Some("raw") }),
(atoms[ImageSvg], TypeHint::Image { extension_hint: Some("svg") }),
(atoms[ImageTiff], TypeHint::Image { extension_hint: Some("tiff") }),
(atoms[ImageWebp], TypeHint::Image { extension_hint: Some("webp") }),
(atoms[ImageXIcon], TypeHint::Image { extension_hint: Some("ico") }),
];
let hint =
atom_to_hint.iter().find_map(|(haystack, hint)| (*haystack == atom).then_some(*hint));
Self { hint, atom }
}
pub fn atom(&self) -> xproto::Atom {
self.atom
}
}
impl TransferType for SelectionType {
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))
}
}
}
impl DataTransfer for Selection {
fn for_each_available_type<'this>(
&'this self,
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>,
) {
let _ = self.types.iter().map(|mime| mime as &dyn TransferType).try_for_each(func);
}
}
impl Dnd {
pub fn new(xconn: Arc<XConnection>) -> Result<Self, X11Error> {
Ok(Dnd {
xconn,
version: None,
type_list: None,
source_window: None,
position: PhysicalPosition::default(),
result: None,
dragging: false,
})
pub fn new(xconn: Arc<XConnection>) -> Self {
Dnd { xconn, state: None }
}
pub fn reset(&mut self) {
self.version = None;
self.type_list = None;
self.source_window = None;
self.result = None;
self.dragging = false;
pub fn state(&self) -> Option<&DragState> {
self.state.as_ref()
}
pub unsafe fn send_status(
&self,
this_window: xproto::Window,
pub fn state_mut(&mut self) -> Option<&mut DragState> {
self.state.as_mut()
}
pub fn find_type_by_hint(&self, hint: TypeHint) -> Option<&SelectionType> {
self.state.as_ref()?.types.iter().find(|haystack| haystack.hint() == Some(hint))
}
pub fn init_state(
&mut self,
version: c_long,
source_window: xproto::Window,
target_window: xproto::Window,
state: DndState,
) -> Result<(), X11Error> {
let atoms = self.xconn.atoms();
let (accepted, action) = match state {
DndState::Accepted => (1, atoms[XdndActionPrivate]),
DndState::Rejected => (0, atoms[DndNone]),
};
self.xconn
.send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [
this_window,
accepted,
0,
0,
action as _,
])?
.ignore_error();
Ok(())
types: Arc<[SelectionType]>,
) -> &DragState {
self.state.get_or_insert(DragState {
version,
types,
source_window,
target_window,
..Default::default()
})
}
pub unsafe fn send_finished(
&self,
this_window: xproto::Window,
target_window: xproto::Window,
state: DndState,
) -> Result<(), X11Error> {
let atoms = self.xconn.atoms();
let (accepted, action) = match state {
DndState::Accepted => (1, atoms[XdndActionPrivate]),
DndState::Rejected => (0, atoms[DndNone]),
let Some(state) = &self.state else {
return Err(X11Error::UnexpectedNull(
"Drag-and-drop state was not initialized (called `send_finished` before XdndEnter",
));
};
let (accepted, action) =
if state.accepted { (1, atoms[XdndActionCopy]) } else { (0, atoms[DndNone]) };
self.xconn
.send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [
this_window,
@@ -138,54 +305,54 @@ impl Dnd {
)
}
pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) {
pub fn convert_selection(
&self,
window: xproto::Window,
time: xproto::Timestamp,
new_type: xproto::Atom,
) {
let atoms = self.xconn.atoms();
self.xconn
.xcb_connection()
.convert_selection(
window,
atoms[XdndSelection],
atoms[TextUriList],
atoms[XdndSelection],
time,
)
// TODO: We store the converted selection back to `XdndSelection`. We should store to
// some new place so that `XdndSelection` remains untouched.
.convert_selection(window, atoms[XdndSelection], new_type, atoms[XdndSelection], time)
.expect_then_ignore_error("Failed to send XdndSelection event")
}
pub unsafe fn read_data(
pub unsafe fn send_status(
&self,
this_window: xproto::Window,
target_window: xproto::Window,
status: DndState,
) -> Result<(), X11Error> {
let atoms = self.xconn.atoms();
let (accepted, action) = match status {
DndState::Accepted => (1, atoms[XdndActionCopy]),
DndState::Rejected => (0, atoms[DndNone]),
};
self.xconn
.send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [
this_window,
accepted,
0,
0,
action as _,
])?
.ignore_error();
Ok(())
}
pub fn read_data(
&self,
window: xproto::Window,
) -> Result<Vec<c_uchar>, util::GetPropertyError> {
type_: SelectionType,
) -> Result<SelectionReader, util::GetPropertyError> {
let atoms = self.xconn.atoms();
self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList])
}
let type_atom = type_.atom();
let bytes = self.xconn.get_property(window, atoms[XdndSelection], type_atom)?;
pub fn parse_data(&self, data: &mut [c_uchar]) -> Result<Vec<PathBuf>, DndDataParseError> {
if !data.is_empty() {
let mut path_list = Vec::new();
let decoded = percent_decode(data).decode_utf8()?.into_owned();
for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) {
// The format is specified as protocol://host/path
// However, it's typically simply protocol:///path
let path_str = if uri.starts_with("file://") {
let path_str = uri.replace("file://", "");
if !path_str.starts_with('/') {
// A hostname is specified
// Supporting this case is beyond the scope of my mental health
return Err(DndDataParseError::HostnameSpecified(path_str));
}
path_str
} else {
// Only the file protocol is supported
return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned()));
};
let path = Path::new(&path_str).canonicalize()?;
path_list.push(path);
}
Ok(path_list)
} else {
Err(DndDataParseError::EmptyData)
}
Ok(SelectionReader { type_, data: bytes })
}
}

View File

@@ -19,12 +19,13 @@ use tracing::warn;
use winit_common::xkb::Context;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::error::{EventLoopError, RequestError};
use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType};
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
use winit_core::event::{DeviceId, StartCause, WindowEvent};
use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider,
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
DndAction, EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider,
OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
@@ -36,14 +37,17 @@ use x11rb::protocol::{xkb, xproto};
use x11rb::x11_utils::X11Error as LogicalError;
use x11rb::xcb_ffi::ReplyOrIdError;
use crate::atoms::*;
use crate::atoms::{
_NET_WM_PING, _NET_WM_SYNC_REQUEST, ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms,
WM_DELETE_WINDOW,
};
use crate::dnd::Dnd;
use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN};
use crate::ime::{self, Ime, ImeCreationError, ImeSender};
use crate::util::{self, CustomCursor};
use crate::window::{UnownedWindow, Window};
use crate::xdisplay::{XConnection, XError, XNotSupported};
use crate::{XlibErrorHook, ffi, xsettings};
use crate::{Selection, SelectionType, XlibErrorHook, ffi, xsettings};
// Xinput constants not defined in x11rb
pub(crate) const ALL_DEVICES: u16 = 0;
@@ -168,6 +172,7 @@ impl<T> PeekableReceiver<T> {
#[derive(Debug)]
pub struct ActiveEventLoop {
pub(crate) xconn: Arc<XConnection>,
pub(crate) dnd: RefCell<Dnd>,
pub(crate) wm_delete_window: xproto::Atom,
pub(crate) net_wm_ping: xproto::Atom,
pub(crate) net_wm_sync_request: xproto::Atom,
@@ -226,8 +231,7 @@ impl EventLoop {
let net_wm_ping = atoms[_NET_WM_PING];
let net_wm_sync_request = atoms[_NET_WM_SYNC_REQUEST];
let dnd = Dnd::new(Arc::clone(&xconn))
.expect("Failed to call XInternAtoms when initializing drag and drop");
let dnd = Dnd::new(Arc::clone(&xconn)).into();
let (ime_sender, ime_receiver) = mpsc::channel();
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
@@ -342,6 +346,7 @@ impl EventLoop {
let window_target = ActiveEventLoop {
ime,
dnd,
root,
control_flow: Cell::new(ControlFlow::default()),
exit: Cell::new(None),
@@ -368,7 +373,6 @@ impl EventLoop {
let event_processor = EventProcessor {
target: window_target,
dnd,
devices: Default::default(),
randr_event_offset,
ime_receiver,
@@ -621,9 +625,8 @@ impl EventLoop {
fn drain_events<A: ApplicationHandler>(&mut self, app: &mut A) {
let mut xev = MaybeUninit::uninit();
while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } {
let mut xev = unsafe { xev.assume_init() };
self.event_processor.process_event(&mut xev, app);
while let Some(xev) = self.event_processor.poll_one_event(&mut xev) {
self.event_processor.process_event(xev, app);
}
}
@@ -759,6 +762,90 @@ impl RootActiveEventLoop for ActiveEventLoop {
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
let dnd = self.dnd.borrow();
if dnd.state().is_none_or(|state| state.transfer_id != id) {
return Err(RequestError::Ignored);
}
let Some(state) = dnd.state() else {
return Err(RequestError::Ignored);
};
Ok(Box::new(Selection::new(state.types.clone())))
}
fn fetch_data_transfer(
&self,
id: DataTransferId,
type_: &dyn TransferType,
) -> Result<AsyncRequestSerial, RequestError> {
let mut dnd = self.dnd.borrow_mut();
let serial = AsyncRequestSerial::get();
let type_ = type_
.cast_ref::<SelectionType>()
.or_else(|| dnd.find_type_by_hint(type_.hint()?))
.cloned()
.ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?;
let new_convert_selection = {
let Some(state) = dnd.state_mut() else {
return Err(RequestError::Ignored);
};
if state.transfer_id != id {
return Err(RequestError::NotSupported(NotSupportedError::new(
"Unknown data transfer",
)));
}
// If it's non-empty, assume that we're still waiting on some other fetch operation.
// The `SelectionNotify` handler will send a new `convert_selection` event if any
// more are on the stack.
let should_emit_convert_selection = state.pending_fetch_types.is_empty();
let atom = type_.atom();
state.pending_fetch_types.push_back((serial, type_));
should_emit_convert_selection.then_some((
state.target_window,
self.xconn.timestamp(),
atom,
))
};
if let Some((window, time, new_type)) = new_convert_selection {
// This results in the `SelectionNotify` event
dnd.convert_selection(window, time, new_type);
}
Ok(serial)
}
fn set_valid_dnd_actions(
&self,
id: DataTransferId,
actions: &[DndAction],
) -> Result<(), RequestError> {
let mut dnd = self.dnd.borrow_mut();
let Some(state) = dnd.state_mut() else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
if state.transfer_id != id {
return Err(os_error!(UnknownDataTransfer(id)).into());
}
state.accepted = !actions.is_empty();
Ok(())
}
}
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
@@ -767,6 +854,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 {}
pub(crate) struct DeviceInfo<'a> {
xconn: &'a XConnection,
info: *const ffi::XIDeviceInfo,

View File

@@ -1,10 +1,12 @@
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::mem::MaybeUninit;
use std::os::raw::{c_char, c_int, c_long, c_ulong};
use std::slice;
use std::sync::{Arc, Mutex};
use dpi::{PhysicalPosition, PhysicalSize};
use tracing::warn;
use winit_common::xkb::{self, Context, XkbState};
use winit_core::application::ApplicationHandler;
use winit_core::event::{
@@ -12,6 +14,7 @@ use winit_core::event::{
MouseScrollDelta, PointerKind, PointerSource, RawKeyEvent, SurfaceSizeWriter, TouchPhase,
WindowEvent,
};
use winit_core::event_loop::DndAction;
use winit_core::keyboard::ModifiersState;
use winit_core::window::WindowId;
use x11_dl::xinput2::{
@@ -31,7 +34,7 @@ use x11rb::x11_utils::{ExtensionInformation, Serialize};
use xkbcommon_dl::xkb_mod_mask_t;
use crate::atoms::*;
use crate::dnd::{Dnd, DndState};
use crate::dnd::{DndState, SelectionType};
use crate::event_loop::{
ALL_DEVICES, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, DeviceType,
ScrollOrientation, mkdid, mkwid,
@@ -49,7 +52,6 @@ const KEYCODE_OFFSET: u8 = 8;
#[derive(Debug)]
pub struct EventProcessor {
pub dnd: Dnd,
pub ime_receiver: ImeReceiver,
pub ime_event_receiver: ImeEventReceiver,
pub randr_event_offset: u8,
@@ -294,7 +296,10 @@ impl EventProcessor {
unsafe { (self.target.xconn.xlib.XPending)(self.target.xconn.display) != 0 }
}
pub unsafe fn poll_one_event(&mut self, event_ptr: *mut XEvent) -> bool {
pub fn poll_one_event<'a>(
&mut self,
event_ptr: &'a mut MaybeUninit<XEvent>,
) -> Option<&'a mut XEvent> {
// This function is used to poll and remove a single event
// from the Xlib event queue in a non-blocking, atomic way.
// XCheckIfEvent is non-blocking and removes events from queue.
@@ -304,20 +309,21 @@ impl EventProcessor {
unsafe extern "C" fn predicate(
_display: *mut XDisplay,
_event: *mut XEvent,
_arg: *mut c_char,
_filter: *mut c_char,
) -> c_int {
// This predicate always returns "true" (1) to accept all events
1
}
unsafe {
let event_initialized = unsafe {
(self.target.xconn.xlib.XCheckIfEvent)(
self.target.xconn.display,
event_ptr,
event_ptr.as_mut_ptr(),
Some(predicate),
std::ptr::null_mut(),
) != 0
}
};
event_initialized.then(|| unsafe { event_ptr.assume_init_mut() })
}
pub fn init_device(&self, device: xinput::DeviceId) {
@@ -423,21 +429,41 @@ impl EventProcessor {
}
if xev.message_type == atoms[XdndEnter] as c_ulong {
let source_window = xev.data.get_long(0) as xproto::Window;
let flags = xev.data.get_long(1);
let version = flags >> 24;
self.dnd.version = Some(version);
let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1;
if !has_more_types {
let type_list = vec![
xev.data.get_long(2) as xproto::Atom,
xev.data.get_long(3) as xproto::Atom,
xev.data.get_long(4) as xproto::Atom,
];
self.dnd.type_list = Some(type_list);
} else if let Ok(more_types) = unsafe { self.dnd.get_type_list(source_window) } {
self.dnd.type_list = Some(more_types);
}
// Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event`
// never contending the lock.
let transfer_id = {
let mut dnd = self.target.dnd.borrow_mut();
let source_window = xev.data.get_long(0) as xproto::Window;
let flags = xev.data.get_long(1);
let version = flags >> 24;
let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1;
let types: Vec<_> = if !has_more_types {
[
xev.data.get_long(2) as xproto::Atom,
xev.data.get_long(3) as xproto::Atom,
xev.data.get_long(4) as xproto::Atom,
]
.map(|ty_atom| SelectionType::new(atoms, ty_atom))
.into_iter()
.collect()
} else if let Ok(more_types) = unsafe { dnd.get_type_list(source_window) } {
more_types
.into_iter()
.map(|ty_atom| SelectionType::new(atoms, ty_atom))
.collect()
} else {
Default::default()
};
dnd.init_state(version, source_window, window, types.into()).transfer_id
};
app.window_event(&self.target, window_id, WindowEvent::DragEntered {
id: transfer_id,
position: None,
});
return;
}
@@ -464,121 +490,204 @@ impl EventProcessor {
.xconn
.translate_coords(self.target.root, window, x, y)
.expect("Failed to translate window coordinates");
self.dnd.position = PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64);
// By our own state flow, `version` should never be `None` at this point.
let version = self.dnd.version.unwrap_or(5);
// Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event`
// never contending the lock.
let transfer_id = {
let dnd = self.target.dnd.borrow();
let Some(state) = dnd.state() else {
return;
};
// By our own state flow, `state` should never be `None` at this point.
let version = state.version;
// Action is specified in versions 2 and up, though we don't need it anyway.
// let action = xev.data.get_long(4);
let time = if version == 0 {
// In version 0, time isn't specified
x11rb::CURRENT_TIME
} else {
xev.data.get_long(3) as xproto::Timestamp
};
let accepted = if let Some(ref type_list) = self.dnd.type_list {
type_list.contains(&atoms[TextUriList])
} else {
false
};
// Log this timestamp.
self.target.xconn.set_timestamp(time);
if !accepted {
unsafe {
self.dnd
.send_status(window, source_window, DndState::Rejected)
.expect("Failed to send `XdndStatus` message.");
dnd.send_status(
window,
source_window,
if state.accepted { DndState::Accepted } else { DndState::Rejected },
)
.expect("Failed to send `XdndStatus` message.");
}
self.dnd.reset();
return;
}
self.dnd.source_window = Some(source_window);
let time = if version == 0 {
// In version 0, time isn't specified
x11rb::CURRENT_TIME
} else {
xev.data.get_long(3) as xproto::Timestamp
state.transfer_id
};
// Log this timestamp.
self.target.xconn.set_timestamp(time);
app.window_event(&self.target, window_id, WindowEvent::DragPosition {
id: transfer_id,
position: PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64),
// `Copy` is the default. Other actions are possible in X11, but the specification
// does not properly explain how to implement them (only giving a vague description
// of `XdndMove`). For simplicity's sake, we simply do not implement non-copy drag
// on X11.
// See https://www.freedesktop.org/wiki/Specifications/XDND/
proposed_action: Some(DndAction::Copy),
});
// This results in the `SelectionNotify` event below
unsafe {
self.dnd.convert_selection(window, time);
}
unsafe {
self.dnd
.send_status(window, source_window, DndState::Accepted)
.expect("Failed to send `XdndStatus` message.");
}
return;
}
if xev.message_type == atoms[XdndDrop] as c_ulong {
let (source_window, state) = if let Some(source_window) = self.dnd.source_window {
if let Some(Ok(ref path_list)) = self.dnd.result {
let event = WindowEvent::DragDropped {
paths: path_list.iter().map(Into::into).collect(),
position: self.dnd.position,
};
app.window_event(&self.target, window_id, event);
}
(source_window, DndState::Accepted)
} else {
// `source_window` won't be part of our DND state if we already rejected the drop in
// our `XdndPosition` handler.
let source_window = xev.data.get_long(0) as xproto::Window;
(source_window, DndState::Rejected)
let (source_window, transfer_id) = {
let dnd = self.target.dnd.borrow();
let Some(state) = dnd.state() else {
warn!("Received `XdndDrop` without `XdndEnter`");
return;
};
let source_window = state.source_window;
(source_window, state.transfer_id)
};
unsafe {
self.dnd
.send_finished(window, source_window, state)
.expect("Failed to send `XdndFinished` message.");
app.window_event(
&self.target,
window_id,
// TODO
WindowEvent::DragDropped {
id: transfer_id,
// `Copy` is the default. Other actions are possible in X11, but the
// specification does not properly explain how to implement
// them (only giving a vague description of `XdndMove`). For
// simplicity's sake, we simply do not implement non-copy drag
// on X11.
// See https://www.freedesktop.org/wiki/Specifications/XDND/
proposed_action: Some(DndAction::Copy),
},
);
let mut dnd = self.target.dnd.borrow_mut();
if let Some(state) =
dnd.state_mut().filter(|state| !state.pending_fetch_types.is_empty())
{
state.finished = Some((window, source_window));
} else {
unsafe {
dnd.send_finished(window, source_window)
.expect("Failed to send `XdndFinished` message.");
}
}
self.dnd.reset();
return;
}
if xev.message_type == atoms[XdndLeave] as c_ulong {
if self.dnd.dragging {
let event = WindowEvent::DragLeft { position: Some(self.dnd.position) };
app.window_event(&self.target, window_id, event);
}
self.dnd.reset();
let dnd = self.target.dnd.borrow();
let Some(state) = dnd.state() else {
return;
};
app.window_event(&self.target, window_id, WindowEvent::DragLeft {
id: state.transfer_id,
});
}
}
fn selection_notify(&mut self, xev: &XSelectionEvent, app: &mut dyn ApplicationHandler) {
let atoms = self.target.xconn.atoms();
let window = xev.requestor as xproto::Window;
let window_id = mkwid(window);
let xwindow = xev.requestor as xproto::Window;
// Set the timestamp.
self.target.xconn.set_timestamp(xev.time as xproto::Timestamp);
// For now, winit only supports selections for drag-and-drop. This should be changed
// when clipboard support is implemented.
if xev.property != atoms[XdndSelection] as c_ulong {
return;
}
// This is where we receive data from drag and drop
self.dnd.result = None;
if let Ok(mut data) = unsafe { self.dnd.read_data(window) } {
let parse_result = self.dnd.parse_data(&mut data);
let (transfer_id, serial, type_) = {
let Some(state) = self.target.dnd.get_mut().state_mut() else {
return;
};
if let Ok(ref path_list) = parse_result {
let event = if self.dnd.dragging {
WindowEvent::DragMoved { position: self.dnd.position }
} else {
let paths = path_list.iter().map(Into::into).collect();
self.dnd.dragging = true;
WindowEvent::DragEntered { paths, position: self.dnd.position }
let Some((serial, type_)) = state.pending_fetch_types.pop_front() else {
return;
};
// Annoyingly, `xproto::Atom` and `x11_dl::Atom` are different on 64-bit
// but the same on 32-bit, so just casting to `u32` will cause "casting
// to same type" clippy warnings when compiled as 32-bit.
#[cfg(target_pointer_width = "32")]
let target_type = xev.target;
#[cfg(target_pointer_width = "64")]
let target_type = xev.target as u32;
if target_type != type_.atom() {
let get_name = |atom| {
self.target
.xconn
.xcb_connection()
.get_atom_name(atom)
.ok()
.and_then(|cookie| cookie.reply().ok())
.and_then(|reply| String::from_utf8(reply.name).ok())
};
app.window_event(&self.target, window_id, event);
let expected_type_name = get_name(type_.atom());
let found_type_name = get_name(xev.type_ as _);
let extra_context = match (expected_type_name, found_type_name) {
(Some(expected), Some(found)) => {
format!(" (expected {expected}, found {found})")
},
(Some(expected), None) => format!(" (expected {expected})"),
(None, Some(found)) => format!(" (found {found})"),
(None, None) => "".to_string(),
};
warn!(
"Received `SelectionNotify` with unexpected type{extra_context}. Continuing, \
but this may be a bug."
);
}
self.dnd.result = Some(parse_result);
(state.transfer_id, serial, type_)
};
let value = match self.target.dnd.borrow().read_data(xwindow, type_) {
Ok(value) => Arc::new(value),
Err(err) => {
warn!("Failed to read selection: {err}");
return;
},
};
let window_id = mkwid(xwindow);
app.window_event(&self.target, window_id, WindowEvent::DataTransferReceived {
id: transfer_id,
serial,
value,
});
let dnd = self.target.dnd.borrow();
// If we have another fetch pending, request it from the drag source window
if let Some((window, type_)) = dnd.state().and_then(|state| {
state
.pending_fetch_types
.front()
.cloned()
.map(|(_, type_)| (state.target_window, type_))
}) {
dnd.convert_selection(window, self.target.xconn.timestamp(), type_.atom());
} else if let Some((this_window, target_window)) =
dnd.state().and_then(|state| state.finished)
{
unsafe {
dnd.send_finished(this_window, target_window)
.expect("Failed to send `XdndFinished` message.");
}
}
}

View File

@@ -26,6 +26,8 @@ mod window;
mod xdisplay;
mod xsettings;
pub use dnd::{Selection, SelectionReader, SelectionType, UriListParseError};
/// X window type. Maps directly to
/// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html).
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]

View File

@@ -17,6 +17,7 @@ pub enum GetPropertyError {
X11rbError(Arc<ReplyError>),
TypeMismatch(xproto::Atom),
FormatMismatch(c_int),
Unknown,
}
impl GetPropertyError {
@@ -41,6 +42,7 @@ impl fmt::Display for GetPropertyError {
GetPropertyError::X11rbError(err) => err.fmt(f),
GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"),
GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"),
GetPropertyError::Unknown => write!(f, "internal error"),
}
}
}

View File

@@ -31,7 +31,14 @@ use x11rb::protocol::sync::{ConnectionExt as _, Int64};
use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle};
use x11rb::protocol::{randr, xinput};
use crate::atoms::*;
use crate::atoms::{
_GTK_THEME_VARIANT, _NET_ACTIVE_WINDOW, _NET_WM_ICON, _NET_WM_MOVERESIZE, _NET_WM_NAME,
_NET_WM_PID, _NET_WM_PING, _NET_WM_STATE, _NET_WM_STATE_ABOVE, _NET_WM_STATE_BELOW,
_NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_HIDDEN, _NET_WM_STATE_MAXIMIZED_HORZ,
_NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_SYNC_REQUEST, _NET_WM_SYNC_REQUEST_COUNTER,
_NET_WM_WINDOW_TYPE, _XEMBED, AtomName, CARD32, UTF8_STRING, WM_CHANGE_STATE,
WM_CLIENT_MACHINE, WM_DELETE_WINDOW, WM_PROTOCOLS, WM_STATE, XdndAware,
};
use crate::event_loop::{
ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie,
WakeSender, X11Error, xinput_fp1616_to_float,