mirror of
https://github.com/rust-windowing/winit.git
synced 2026-09-02 06:40:06 -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:
805
winit-wayland/src/dnd.rs
Normal file
805
winit-wayland/src/dnd.rs
Normal 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);
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user