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:
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user