diff --git a/Cargo.toml b/Cargo.toml index d0fb561..4d13334 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,21 @@ readme = "README.md" keywords = ["clipboard", "window", "ui", "gui", "raw-window-handle"] categories = ["gui"] +[features] +# Yoda: put the unix clipboard backends behind opt-in features. Upstream +# pulled both X11 + Wayland unconditionally on unix — pure bloat for +# Wayland-only builds (clipboard_x11 pulls x11rb + its protocol machinery). +# Default keeps both enabled to preserve upstream behaviour; yoda consumers +# pass default-features=false + "wayland" at the dep declaration. +default = ["x11", "wayland"] +x11 = ["dep:clipboard_x11"] +wayland = ["dep:clipboard_wayland"] + [dependencies] raw-window-handle = { version = "0.6", features = ["std"] } thiserror = "1.0" +mime = { path = "./mime" } +dnd = { path = "./dnd" } [target.'cfg(windows)'.dependencies] clipboard-win = { version = "5.0", features = ["std"] } @@ -22,16 +34,12 @@ clipboard-win = { version = "5.0", features = ["std"] } clipboard_macos = { version = "0.1", path = "./macos" } [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten", target_os="ios", target_os="redox"))))'.dependencies] -clipboard_x11 = { version = "0.4.2", path = "./x11" } -clipboard_wayland = { version = "0.2.2", path = "./wayland" } +clipboard_x11 = { version = "0.4.2", path = "./x11", optional = true } +clipboard_wayland = { version = "0.2.2", path = "./wayland", optional = true } [dev-dependencies] rand = "0.8" winit = "0.29" [workspace] -members = [ - "macos", - "wayland", - "x11", -] +members = ["dnd", "macos", "mime", "dnd", "wayland", "x11"] diff --git a/dnd/Cargo.toml b/dnd/Cargo.toml new file mode 100644 index 0000000..868bceb --- /dev/null +++ b/dnd/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dnd" +version = "0.1.0" +edition = "2021" + +[dependencies] +mime = { path = "../mime" } +bitflags = "2.5.0" +raw-window-handle = "0.6" + +[target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten", target_os="ios", target_os="redox"))))'.dependencies] +smithay-clipboard = { path = "../../smithay-clipboard", features = [ + "dnd", +] } +sctk = { package = "smithay-client-toolkit", version = "0.20", default-features = false, features = [ + "calloop", +] } diff --git a/dnd/src/lib.rs b/dnd/src/lib.rs new file mode 100644 index 0000000..66ccd8c --- /dev/null +++ b/dnd/src/lib.rs @@ -0,0 +1,201 @@ +use std::{ + borrow::Cow, + fmt::Debug, + sync::{mpsc::SendError, Arc}, +}; + +use bitflags::bitflags; +use raw_window_handle::HasWindowHandle; + +#[cfg(all( + unix, + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten", + target_os = "redox" + )) +))] +#[path = "platform/linux.rs"] +pub mod platform; + +bitflags! { + // Attributes can be applied to flags types + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct DndAction: u32 { + const Copy = 0b00000001; + const Move = 0b00000010; + const Ask = 0b00000100; + } +} + +#[derive(Debug, Clone)] +pub enum DndEvent { + /// Dnd Offer event with the corresponding destination rectangle ID. + Offer(Option, OfferEvent), + /// Dnd Source event. + Source(SourceEvent), +} + +impl PartialEq for DndEvent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (DndEvent::Offer(a, b), DndEvent::Offer(a2, b2)) => { + a == a2 && b == b2 + } + (DndEvent::Source(a), DndEvent::Source(b)) => a == b, + _ => false, + } + } +} + +#[derive(Debug, Clone)] +pub enum SourceEvent { + /// DnD operation ended. + Finished, + /// DnD Cancelled. + Cancelled, + /// DnD action chosen by the compositor. + Action(DndAction), + /// Mime accepted by destination. + /// If [`None`], no mime types are accepted. + Mime(Option), + /// DnD Dropped. The operation is still ongoing until receiving a + /// [`SourceEvent::Finished`] event. + Dropped, +} + +impl PartialEq for SourceEvent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (SourceEvent::Finished, SourceEvent::Finished) + | (SourceEvent::Cancelled, SourceEvent::Cancelled) + | (SourceEvent::Dropped, SourceEvent::Dropped) => true, + (SourceEvent::Action(a), SourceEvent::Action(b)) => a == b, + (SourceEvent::Mime(a), SourceEvent::Mime(b)) => a == b, + _ => false, + } + } +} + +#[derive(Debug, Clone)] +pub enum OfferEvent { + Enter { + x: f64, + y: f64, + mime_types: Vec, + surface: T, + }, + Motion { + x: f64, + y: f64, + }, + /// The offer is no longer on a DnD destination. + LeaveDestination, + /// The offer has left the surface. + Leave, + /// An offer was dropped + Drop, + /// If the selected action is ASK, the user must be presented with a + /// choice. [`Clipboard::set_action`] should then be called before data + /// can be requested and th DnD operation can be finished. + SelectedAction(DndAction), + Data { + data: Vec, + mime_type: String, + }, +} + +impl PartialEq for OfferEvent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + OfferEvent::Enter { + x, + y, + mime_types, + surface: _, + }, + OfferEvent::Enter { + x: x2, + y: y2, + mime_types: mime_types2, + surface: _, + }, + ) => x == x2 && y == y2 && mime_types == mime_types2, + ( + OfferEvent::Motion { x, y }, + OfferEvent::Motion { x: x2, y: y2 }, + ) => x == x2 && y == y2, + (OfferEvent::LeaveDestination, OfferEvent::LeaveDestination) + | (OfferEvent::Leave, OfferEvent::Leave) + | (OfferEvent::Drop, OfferEvent::Drop) => true, + (OfferEvent::SelectedAction(a), OfferEvent::SelectedAction(b)) => { + a == b + } + ( + OfferEvent::Data { data, mime_type }, + OfferEvent::Data { + data: data2, + mime_type: mime_type2, + }, + ) => data == data2 && mime_type == mime_type2, + _ => false, + } + } +} + +/// A rectangle with a logical location and size relative to a [`DndSurface`] +#[derive(Debug, Default, Clone)] +pub struct Rectangle { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +pub trait Sender { + /// Send an event in the channel + fn send(&self, t: DndEvent) -> Result<(), SendError>>; +} + +/// A rectangle with a logical location and size relative to a [`DndSurface`] +#[derive(Debug, Clone)] +pub struct DndDestinationRectangle { + /// A unique ID + pub id: u128, + /// The rectangle representing this destination. + pub rectangle: Rectangle, + /// Accepted mime types in this rectangle + pub mime_types: Vec>, + /// Accepted actions in this rectangle + pub actions: DndAction, + /// Prefered action in this rectangle + pub preferred: DndAction, +} + +#[derive(Clone)] +pub enum Icon { + Surface(DndSurface), + /// Xrgb8888 or Argb8888 image data with premultiplied alpha + Buffer { + data: Arc>, + width: u32, + height: u32, + transparent: bool, + }, +} + +#[derive(Clone)] +pub struct DndSurface(pub Arc); + +impl Debug for DndSurface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DndSurface").finish() + } +} + +#[derive(Clone)] +pub struct DataWrapper(pub T); diff --git a/dnd/src/platform/linux.rs b/dnd/src/platform/linux.rs new file mode 100644 index 0000000..31a1879 --- /dev/null +++ b/dnd/src/platform/linux.rs @@ -0,0 +1,110 @@ +use std::{borrow::Cow, ffi::c_void, sync::Arc}; + +use crate::{DataWrapper, DndAction, DndSurface, Icon}; +use raw_window_handle::HasWindowHandle; +use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType}; + +impl< + T: mime::AllowedMimeTypes + + std::convert::TryFrom<(std::vec::Vec, String)>, + > AllowedMimeTypes for DataWrapper +{ + fn allowed() -> Cow<'static, [MimeType]> { + T::allowed() + .into_iter() + .map(|s| MimeType::from(Cow::Owned(s.to_string()))) + .collect() + } +} + +impl, String)>> TryFrom<(Vec, MimeType)> + for DataWrapper +{ + type Error = T::Error; + + fn try_from( + (data, mime): (Vec, MimeType), + ) -> Result { + T::try_from((data, mime.to_string())).map(|d| DataWrapper(d)) + } +} + +impl AsMimeTypes for DataWrapper { + fn available(&self) -> Cow<'static, [MimeType]> { + self.0 + .available() + .into_iter() + .map(|m| MimeType::from(Cow::Owned(m.to_string()))) + .collect() + } + + fn as_bytes(&self, mime_type: &MimeType) -> Option> { + self.0.as_bytes(mime_type.as_ref()) + } +} + +impl smithay_clipboard::dnd::RawSurface for DndSurface { + unsafe fn get_ptr(&mut self) -> *mut c_void { + self.0.window_handle().unwrap().get_ptr() + } +} + +impl From + for DndAction +{ + fn from( + action: sctk::reexports::client::protocol::wl_data_device_manager::DndAction, + ) -> Self { + let mut a = DndAction::empty(); + if action.contains(sctk::reexports::client::protocol::wl_data_device_manager::DndAction::Copy) { + a |= DndAction::Copy; + } + if action.contains(sctk::reexports::client::protocol::wl_data_device_manager::DndAction::Move) { + a |= DndAction::Move; + } + if action.contains(sctk::reexports::client::protocol::wl_data_device_manager::DndAction::Ask) { + a |= DndAction::Ask; + } + a + } +} + +impl From + for sctk::reexports::client::protocol::wl_data_device_manager::DndAction +{ + fn from(action: DndAction) -> Self { + let mut a = sctk::reexports::client::protocol::wl_data_device_manager::DndAction::empty(); + if action.contains(DndAction::Copy) { + a |= sctk::reexports::client::protocol::wl_data_device_manager::DndAction::Copy; + } + if action.contains(DndAction::Move) { + a |= sctk::reexports::client::protocol::wl_data_device_manager::DndAction::Move; + } + if action.contains(DndAction::Ask) { + a |= sctk::reexports::client::protocol::wl_data_device_manager::DndAction::Ask; + } + a + } +} + +impl From for smithay_clipboard::dnd::Icon { + fn from(icon: Icon) -> Self { + match icon { + Icon::Surface(surface) => { + smithay_clipboard::dnd::Icon::Surface(surface) + } + Icon::Buffer { + data, + width, + height, + transparent, + } => smithay_clipboard::dnd::Icon::Buf { + data: Arc::try_unwrap(data) + .unwrap_or_else(|d| d.as_ref().clone()), + width, + height, + transparent, + }, + } + } +} diff --git a/dnd/src/platform/mod.rs b/dnd/src/platform/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/examples/big_file.rs b/examples/big_file.rs index f666812..8be0fa2 100644 --- a/examples/big_file.rs +++ b/examples/big_file.rs @@ -1,5 +1,5 @@ use rand::distributions::{Alphanumeric, Distribution}; -use window_clipboard::Clipboard; +use window_clipboard::PlatformClipboard; use winit::{ error::EventLoopError, event::{ElementState, Event, KeyEvent, WindowEvent}, @@ -24,8 +24,8 @@ fn main() -> Result<(), EventLoopError> { .build(&event_loop) .unwrap(); - let mut clipboard = - unsafe { Clipboard::connect(&window) }.expect("Connect to clipboard"); + let mut clipboard = unsafe { PlatformClipboard::connect(&window) } + .expect("Connect to clipboard"); clipboard.write(data.clone()).unwrap(); diff --git a/examples/read.rs b/examples/read.rs index ddbaf8b..8407133 100644 --- a/examples/read.rs +++ b/examples/read.rs @@ -1,4 +1,4 @@ -use window_clipboard::Clipboard; +use window_clipboard::PlatformClipboard; use winit::{ error::EventLoopError, event::{Event, WindowEvent}, @@ -14,8 +14,8 @@ fn main() -> Result<(), EventLoopError> { .build(&event_loop) .unwrap(); - let clipboard = - unsafe { Clipboard::connect(&window) }.expect("Connect to clipboard"); + let clipboard = unsafe { PlatformClipboard::connect(&window) } + .expect("Connect to clipboard"); event_loop.run(move |event, elwt| match event { Event::AboutToWait => { diff --git a/examples/write.rs b/examples/write.rs index 40e6263..49dc505 100644 --- a/examples/write.rs +++ b/examples/write.rs @@ -1,4 +1,4 @@ -use window_clipboard::Clipboard; +use window_clipboard::PlatformClipboard; use winit::{ error::EventLoopError, event::{Event, WindowEvent}, @@ -14,8 +14,8 @@ fn main() -> Result<(), EventLoopError> { .build(&event_loop) .unwrap(); - let mut clipboard = - unsafe { Clipboard::connect(&window) }.expect("Connect to clipboard"); + let mut clipboard = unsafe { PlatformClipboard::connect(&window) } + .expect("Connect to clipboard"); clipboard .write(String::from("Hello, world!")) diff --git a/macos/src/lib.rs b/macos/src/lib.rs index ca9047e..deeb52f 100644 --- a/macos/src/lib.rs +++ b/macos/src/lib.rs @@ -15,11 +15,11 @@ extern crate objc; use objc::runtime::{Class, Object}; -use objc_foundation::{INSArray, INSObject, INSString}; -use objc_foundation::{NSArray, NSDictionary, NSObject, NSString}; +use objc_foundation::{ + INSArray, INSObject, INSString, NSArray, NSDictionary, NSObject, NSString, +}; use objc_id::{Id, Owned}; -use std::error::Error; -use std::mem::transmute; +use std::{error::Error, mem::transmute}; pub struct Clipboard { pasteboard: Id, diff --git a/mime/Cargo.toml b/mime/Cargo.toml new file mode 100644 index 0000000..259b4f0 --- /dev/null +++ b/mime/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mime" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten", target_os="ios", target_os="redox"))))'.dependencies] +smithay-clipboard = { path = "../../smithay-clipboard" } diff --git a/mime/src/lib.rs b/mime/src/lib.rs new file mode 100644 index 0000000..983827d --- /dev/null +++ b/mime/src/lib.rs @@ -0,0 +1,69 @@ +pub mod platform; + +// need a type that can implement traits for storing custom data + +use std::{borrow::Cow, error, fmt}; + +/// Raw data from the clipboard +pub struct ClipboardData(pub Vec, pub String); + +impl AllowedMimeTypes for ClipboardData { + fn allowed() -> Cow<'static, [String]> { + Cow::Owned(vec![]) + } +} + +impl TryFrom<(Vec, String)> for ClipboardData { + type Error = Error; + + fn try_from((data, mime): (Vec, String)) -> Result { + Ok(ClipboardData(data, mime)) + } +} + +/// Data that can be loaded from the clipboard. +pub struct ClipboardLoadData(pub T); + +/// Describes the mime types which are accepted. +pub trait AllowedMimeTypes: + TryFrom<(Vec, String)> + Send + Sync + 'static +{ + /// List allowed mime types for the type to convert from a byte slice. + /// + /// Allowed mime types should be listed in order of decreasing preference, + /// most preferred first. + fn allowed() -> Cow<'static, [String]>; +} + +/// Can be converted to data with the available mime types. +pub trait AsMimeTypes { + /// List available mime types for this data to convert to a byte slice. + fn available(&self) -> Cow<'static, [String]>; + + /// Converts a type to a byte slice for the given mime type if possible. + fn as_bytes(&self, mime_type: &str) -> Option>; +} + +impl AsMimeTypes for Box { + fn available(&self) -> Cow<'static, [String]> { + self.as_ref().available() + } + + fn as_bytes(&self, mime_type: &str) -> Option> { + self.as_ref().as_bytes(mime_type) + } +} + +/// Data that can be stored to the clipboard. +pub struct ClipboardStoreData(pub T); + +#[derive(Debug, Clone, Copy)] +pub struct Error; + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unsupported mime type") + } +} + +impl error::Error for Error {} diff --git a/mime/src/platform/linux.rs b/mime/src/platform/linux.rs new file mode 100644 index 0000000..1da9698 --- /dev/null +++ b/mime/src/platform/linux.rs @@ -0,0 +1,47 @@ +use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType}; + +use crate::{ClipboardLoadData, ClipboardStoreData}; + +impl AsMimeTypes for ClipboardStoreData { + fn available(&self) -> std::borrow::Cow<'static, [MimeType]> { + self.0 + .available() + .into_iter() + .map(|m| MimeType::Other(m.clone().into())) + .collect() + } + + fn as_bytes( + &self, + mime_type: &MimeType, + ) -> Option> { + self.0.as_bytes(mime_type.as_ref()) + } +} + +impl AllowedMimeTypes for ClipboardLoadData { + // TODO select text variants if string matches... + fn allowed() -> std::borrow::Cow<'static, [MimeType]> { + T::allowed() + .into_iter() + .map(|s| MimeType::Other(s.clone().into())) + .collect() + } +} + +impl TryFrom<(Vec, MimeType)> for ClipboardLoadData +where + T: for<'b> TryFrom<(Vec, String)>, + T: 'static, +{ + type Error = crate::Error; + + fn try_from( + (value, mime): (Vec, MimeType), + ) -> Result { + let mime = mime.to_string(); + Ok(ClipboardLoadData( + T::try_from((value, mime)).map_err(|_| crate::Error)?, + )) + } +} diff --git a/mime/src/platform/mod.rs b/mime/src/platform/mod.rs new file mode 100644 index 0000000..10c3ec9 --- /dev/null +++ b/mime/src/platform/mod.rs @@ -0,0 +1,11 @@ +#[cfg(all( + unix, + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten", + target_os = "redox" + )) +))] +pub mod linux; diff --git a/src/dnd/mod.rs b/src/dnd/mod.rs new file mode 100644 index 0000000..b145ca0 --- /dev/null +++ b/src/dnd/mod.rs @@ -0,0 +1,102 @@ +use std::borrow::Cow; + +use ::dnd::{DndAction, DndDestinationRectangle, Sender}; +use dnd::{DndSurface, Icon}; +use mime::{AllowedMimeTypes, AsMimeTypes}; + +pub trait DndProvider { + /// Set up DnD operations for the Clipboard + fn init_dnd( + &self, + _tx: Box + Send + Sync + 'static>, + ) { + } + + /// Start a DnD operation on the given surface with some data + fn start_dnd( + &self, + _internal: bool, + _source_surface: DndSurface, + _icon_surface: Option, + _content: D, + _actions: DndAction, + ) { + } + + /// End the current DnD operation, if there is one + fn end_dnd(&self) {} + + /// Register a surface for receiving DnD offers + /// Rectangles should be provided in order of decreasing priority. + /// This method can be called multiple time for a single surface if the + /// rectangles change. + fn register_dnd_destination( + &self, + _surface: DndSurface, + _rectangles: Vec, + ) { + } + + /// Set the final action after presenting the user with a choice + fn set_action(&self, _action: DndAction) {} + + /// Peek at the contents of a DnD offer + fn peek_offer( + &self, + _mime_type: Option>, + ) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "DnD not supported", + )) + } +} + +impl DndProvider for crate::PlatformClipboard { + fn init_dnd( + &self, + tx: Box + Send + Sync + 'static>, + ) { + self.raw.init_dnd(tx); + } + + fn start_dnd( + &self, + internal: bool, + source_surface: DndSurface, + icon_surface: Option, + content: D, + actions: DndAction, + ) { + self.raw.start_dnd( + internal, + source_surface, + icon_surface, + content, + actions, + ); + } + + fn end_dnd(&self) { + self.raw.end_dnd(); + } + + fn register_dnd_destination( + &self, + surface: DndSurface, + rectangles: Vec, + ) { + self.raw.register_dnd_destination(surface, rectangles); + } + + fn set_action(&self, action: DndAction) { + self.raw.set_action(action); + } + + fn peek_offer( + &self, + mime_type: Option>, + ) -> std::io::Result { + self.raw.peek_offer::(mime_type) + } +} diff --git a/src/lib.rs b/src/lib.rs index a9857e3..54d330f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +pub use mime; + #[cfg(all( unix, not(any( @@ -46,21 +48,26 @@ mod platform; #[path = "platform/dummy.rs"] mod platform; +pub mod dnd; + +use mime::ClipboardStoreData; use raw_window_handle::HasDisplayHandle; use std::error::Error; -pub struct Clipboard { - raw: Box, +pub type Clipboard = PlatformClipboard; + +pub struct PlatformClipboard { + raw: C, } -impl Clipboard { +impl PlatformClipboard { /// Safety: the display handle must be valid for the lifetime of `Clipboard` - pub unsafe fn connect( + pub unsafe fn connect( window: &W, ) -> Result> { - let raw = platform::connect(window)?; - - Ok(Clipboard { raw }) + Ok(PlatformClipboard { + raw: platform::connect(window)?, + }) } pub fn read(&self) -> Result> { @@ -72,14 +79,67 @@ impl Clipboard { } } -impl Clipboard { +impl PlatformClipboard { pub fn read_primary(&self) -> Option>> { self.raw.read_primary() } - pub fn write_primary(&mut self, contents: String) -> Option>> { + pub fn write_primary( + &mut self, + contents: String, + ) -> Option>> { self.raw.write_primary(contents) } + + pub fn read_data(&self) -> Option>> + where + T: mime::AllowedMimeTypes, + { + self.raw.read_data() + } + + pub fn write_data( + &mut self, + contents: ClipboardStoreData, + ) -> Option>> + where + T: mime::AsMimeTypes, + { + self.raw.write_data(contents) + } + + pub fn read_primary_data( + &self, + ) -> Option>> + where + T: mime::AllowedMimeTypes, + { + self.raw.read_primary_data() + } + + pub fn read_primary_raw( + &self, + allowed: Vec, + ) -> Option, String), Box>> { + self.raw.read_primary_raw(allowed) + } + + pub fn read_raw( + &self, + allowed: Vec, + ) -> Option, String), Box>> { + self.raw.read_raw(allowed) + } + + pub fn write_primary_data( + &mut self, + contents: ClipboardStoreData, + ) -> Option>> + where + T: mime::AsMimeTypes, + { + self.raw.write_primary_data(contents) + } } pub trait ClipboardProvider { @@ -91,7 +151,58 @@ pub trait ClipboardProvider { None } - fn write_primary(&mut self, _contents: String) -> Option>> { + fn write_primary( + &mut self, + _contents: String, + ) -> Option>> { + None + } + + fn read_data(&self) -> Option>> + where + T: mime::AllowedMimeTypes, + { + None + } + + fn write_data( + &mut self, + _contents: ClipboardStoreData, + ) -> Option>> + where + T: mime::AsMimeTypes, + { + None + } + + fn read_primary_data(&self) -> Option>> + where + T: mime::AllowedMimeTypes, + { + None + } + + fn read_primary_raw( + &self, + _allowed: Vec, + ) -> Option, String), Box>> { + None + } + + fn read_raw( + &self, + _allowed: Vec, + ) -> Option, String), Box>> { + None + } + + fn write_primary_data( + &mut self, + _contents: ClipboardStoreData, + ) -> Option>> + where + T: mime::AsMimeTypes, + { None } } diff --git a/src/platform/android.rs b/src/platform/android.rs index b0ac8b6..885f80d 100644 --- a/src/platform/android.rs +++ b/src/platform/android.rs @@ -1,12 +1,15 @@ use crate::ClipboardProvider; +use crate::dnd::DndProvider; +use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon}; +use mime::{AllowedMimeTypes, AsMimeTypes}; use raw_window_handle::HasDisplayHandle; -use std::error::Error; +use std::{borrow::Cow, error::Error}; pub fn connect( _window: &W, -) -> Result, Box> { - Ok(Box::new(Clipboard::new()?)) +) -> Result> { + Clipboard::new() } pub struct Clipboard; @@ -36,7 +39,46 @@ impl ClipboardProvider for Clipboard { Err(Box::new(AndroidClipboardError::Unimplemented)) } - fn write(&mut self, contents: String) -> Result<(), Box> { + fn write(&mut self, _contents: String) -> Result<(), Box> { Err(Box::new(AndroidClipboardError::Unimplemented)) } } + +impl DndProvider for Clipboard { + fn init_dnd( + &self, + _tx: Box + Send + Sync + 'static>, + ) { + } + + fn start_dnd( + &self, + _internal: bool, + _source_surface: DndSurface, + _icon_surface: Option, + _content: D, + _actions: DndAction, + ) { + } + + fn end_dnd(&self) {} + + fn register_dnd_destination( + &self, + _surface: DndSurface, + _rectangles: Vec, + ) { + } + + fn set_action(&self, _action: DndAction) {} + + fn peek_offer( + &self, + _mime_type: Option>, + ) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "DnD not supported", + )) + } +} diff --git a/src/platform/dummy.rs b/src/platform/dummy.rs index 8cf7418..c86bcbf 100644 --- a/src/platform/dummy.rs +++ b/src/platform/dummy.rs @@ -1,16 +1,19 @@ -use crate::ClipboardProvider; +use crate::{dnd::DndProvider, ClipboardProvider}; +use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon}; +use mime::{AllowedMimeTypes, AsMimeTypes}; use raw_window_handle::HasDisplayHandle; +use std::borrow::Cow; -struct Dummy; +pub struct Clipboard; -pub fn connect( +pub fn connect( _window: &W, -) -> Result, Box> { - Ok(Box::new(Dummy)) +) -> Result> { + Ok(Clipboard) } -impl ClipboardProvider for Dummy { +impl ClipboardProvider for Clipboard { fn read(&self) -> Result> { Err(Box::new(Error::Unimplemented)) } @@ -23,6 +26,45 @@ impl ClipboardProvider for Dummy { } } +impl DndProvider for Clipboard { + fn init_dnd( + &self, + _tx: Box + Send + Sync + 'static>, + ) { + } + + fn start_dnd( + &self, + _internal: bool, + _source_surface: DndSurface, + _icon_surface: Option, + _content: D, + _actions: DndAction, + ) { + } + + fn end_dnd(&self) {} + + fn register_dnd_destination( + &self, + _surface: DndSurface, + _rectangles: Vec, + ) { + } + + fn set_action(&self, _action: DndAction) {} + + fn peek_offer( + &self, + _mime_type: Option>, + ) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "DnD not supported", + )) + } +} + #[derive(Debug, Clone, Copy, thiserror::Error)] enum Error { #[error("unimplemented")] diff --git a/src/platform/ios.rs b/src/platform/ios.rs index 84ae06a..fd961c7 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -3,10 +3,10 @@ use crate::ClipboardProvider; use raw_window_handle::HasDisplayHandle; use std::error::Error; -pub fn connect( +pub fn connect( _window: &W, -) -> Result, Box> { - Ok(Box::new(Clipboard::new()?)) +) -> Result> { + Clipboard::new() } pub struct Clipboard; diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 12d8c67..755dfb9 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,56 +1,241 @@ -use crate::ClipboardProvider; +use crate::{ + dnd::DndProvider, + mime::{ClipboardLoadData, ClipboardStoreData}, + ClipboardProvider, +}; +use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon}; +use mime::{AllowedMimeTypes, AsMimeTypes}; use raw_window_handle::{HasDisplayHandle, RawDisplayHandle}; -use std::error::Error; +use std::{borrow::Cow, error::Error, sync::Arc}; +use wayland::DndSender; pub use clipboard_wayland as wayland; +#[cfg(feature = "x11")] pub use clipboard_x11 as x11; -pub unsafe fn connect( - window: &W, -) -> Result, Box> { - let clipboard = match window.display_handle()?.as_raw() { - RawDisplayHandle::Wayland(handle) => { - Box::new(wayland::Clipboard::connect(handle.display.as_ptr())) as _ +pub enum Clipboard { + Wayland(wayland::Clipboard), + #[cfg(feature = "x11")] + X11(x11::Clipboard), +} + +impl ClipboardProvider for Clipboard { + fn read(&self) -> Result> { + match self { + Clipboard::Wayland(c) => c.read(), + #[cfg(feature = "x11")] + Clipboard::X11(c) => c.read().map_err(Box::from), + } + } + + fn write(&mut self, contents: String) -> Result<(), Box> { + match self { + Clipboard::Wayland(c) => c.write(contents), + #[cfg(feature = "x11")] + Clipboard::X11(c) => c.write(contents).map_err(Box::from), + } + } + + fn read_primary(&self) -> Option>> { + match self { + Clipboard::Wayland(c) => Some(c.read_primary()), + #[cfg(feature = "x11")] + Clipboard::X11(c) => Some(c.read_primary().map_err(Box::from)), + } + } + + fn write_primary( + &mut self, + contents: String, + ) -> Option>> { + match self { + Clipboard::Wayland(c) => Some(c.write_primary(contents)), + #[cfg(feature = "x11")] + Clipboard::X11(c) => { + Some(c.write_primary(contents).map_err(Box::from)) + } + } + } + + fn read_data(&self) -> Option>> + where + T: mime::AllowedMimeTypes, + { + match self { + Clipboard::Wayland(c) => { + let ret = c.read_data::>(); + Some(ret.map(|ret| ret.0)) + } + #[cfg(feature = "x11")] + Clipboard::X11(_) => None, + } + } + + fn write_data( + &mut self, + contents: ClipboardStoreData, + ) -> Option>> + where + T: mime::AsMimeTypes, + { + match self { + Clipboard::Wayland(c) => { + Some(c.write_data::>(contents)) + } + #[cfg(feature = "x11")] + Clipboard::X11(_) => None, + } + } + + fn read_primary_data(&self) -> Option>> + where + T: mime::AllowedMimeTypes, + { + match self { + Clipboard::Wayland(c) => { + let ret = c.read_primary_data::>(); + Some(ret.map(|ret| ret.0)) + } + #[cfg(feature = "x11")] + Clipboard::X11(_) => None, + } + } + + fn read_primary_raw( + &self, + allowed: Vec, + ) -> Option, String), Box>> { + match self { + Clipboard::Wayland(c) => Some(c.read_primary_raw(allowed)), + #[cfg(feature = "x11")] + Clipboard::X11(_) => None, + } + } + + fn read_raw( + &self, + allowed: Vec, + ) -> Option, String), Box>> { + match self { + Clipboard::Wayland(c) => Some(c.read_raw(allowed)), + #[cfg(feature = "x11")] + Clipboard::X11(_) => None, + } + } + + fn write_primary_data( + &mut self, + contents: ClipboardStoreData, + ) -> Option>> + where + T: mime::AsMimeTypes, + { + match self { + Clipboard::Wayland(c) => { + Some(c.write_primary_data::>(contents)) + } + #[cfg(feature = "x11")] + Clipboard::X11(_) => None, + } + } +} + +impl DndProvider for Clipboard { + fn init_dnd( + &self, + tx: Box + Send + Sync + 'static>, + ) { + match self { + Clipboard::Wayland(c) => c.init_dnd(DndSender(Arc::from(tx))), + #[cfg(feature = "x11")] + Clipboard::X11(_) => {} + } + } + + fn start_dnd( + &self, + internal: bool, + source_surface: DndSurface, + icon_surface: Option, + content: D, + actions: DndAction, + ) { + match self { + Clipboard::Wayland(c) => c.start_dnd( + internal, + source_surface, + icon_surface, + content, + actions, + ), + #[cfg(feature = "x11")] + Clipboard::X11(_) => {} + } + } + + fn end_dnd(&self) { + match self { + Clipboard::Wayland(c) => c.end_dnd(), + #[cfg(feature = "x11")] + Clipboard::X11(_) => {} + } + } + + fn register_dnd_destination( + &self, + surface: DndSurface, + rectangles: Vec, + ) { + match self { + Clipboard::Wayland(c) => { + c.register_dnd_destination(surface, rectangles) + } + #[cfg(feature = "x11")] + Clipboard::X11(_) => {} + } + } + + fn set_action(&self, action: DndAction) { + match self { + Clipboard::Wayland(c) => c.set_action(action), + #[cfg(feature = "x11")] + Clipboard::X11(_) => {} + } + } + + fn peek_offer( + &self, + mime_type: Option>, + ) -> std::io::Result { + match self { + Clipboard::Wayland(c) => c.peek_offer::(mime_type), + #[cfg(feature = "x11")] + Clipboard::X11(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "DnD not supported", + )), + } + } +} + +pub unsafe fn connect( + window: &W, +) -> Result> { + let clipboard = match window.display_handle()?.as_raw() { + RawDisplayHandle::Wayland(handle) => Clipboard::Wayland( + wayland::Clipboard::connect(handle.display.as_ptr()), + ) as _, + #[cfg(feature = "x11")] + _ => Clipboard::X11(x11::Clipboard::connect()?) as _, + #[cfg(not(feature = "x11"))] + _ => { + return Err(Box::from( + "Yoda window_clipboard: X11 feature disabled; \ + non-Wayland display handles are not supported", + )) } - _ => Box::new(x11::Clipboard::connect()?) as _, }; Ok(clipboard) } - -impl ClipboardProvider for wayland::Clipboard { - fn read(&self) -> Result> { - self.read() - } - - fn read_primary(&self) -> Option>> { - Some(self.read_primary()) - } - - fn write(&mut self, contents: String) -> Result<(), Box> { - self.write(contents) - } - - fn write_primary(&mut self, contents: String) -> Option>> { - Some(self.write_primary(contents)) - } -} - -impl ClipboardProvider for x11::Clipboard { - fn read(&self) -> Result> { - self.read().map_err(Box::from) - } - - fn read_primary(&self) -> Option>> { - Some(self.read_primary().map_err(Box::from)) - } - - fn write(&mut self, contents: String) -> Result<(), Box> { - self.write(contents).map_err(Box::from) - } - - fn write_primary(&mut self, contents: String) -> Option>> { - Some(self.write_primary(contents).map_err(Box::from)) - } -} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 5b399e0..d89c446 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,15 +1,58 @@ use crate::ClipboardProvider; +use crate::dnd::DndProvider; +pub(crate) use clipboard_macos::Clipboard; +use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon}; +use mime::{AllowedMimeTypes, AsMimeTypes}; use raw_window_handle::HasDisplayHandle; -use std::error::Error; +use std::{borrow::Cow, error::Error}; -pub fn connect( +pub fn connect( _window: &W, -) -> Result, Box> { - Ok(Box::new(clipboard_macos::Clipboard::new()?)) +) -> Result> { + Clipboard::new() } -impl ClipboardProvider for clipboard_macos::Clipboard { +impl DndProvider for Clipboard { + fn init_dnd( + &self, + _tx: Box + Send + Sync + 'static>, + ) { + } + + fn start_dnd( + &self, + _internal: bool, + _source_surface: DndSurface, + _icon_surface: Option, + _content: D, + _actions: DndAction, + ) { + } + + fn end_dnd(&self) {} + + fn register_dnd_destination( + &self, + _surface: DndSurface, + _rectangles: Vec, + ) { + } + + fn set_action(&self, _action: DndAction) {} + + fn peek_offer( + &self, + _mime_type: Option>, + ) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "DnD not supported", + )) + } +} + +impl ClipboardProvider for Clipboard { fn read(&self) -> Result> { self.read() } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a0c774b..81d1e73 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,14 +1,54 @@ use crate::ClipboardProvider; +use crate::dnd::DndProvider; use clipboard_win::{get_clipboard_string, set_clipboard_string}; +use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon}; +use mime::{AllowedMimeTypes, AsMimeTypes}; use raw_window_handle::HasDisplayHandle; +use std::{borrow::Cow, error::Error}; -use std::error::Error; - -pub fn connect( +pub fn connect( _window: &W, -) -> Result, Box> { - Ok(Box::new(Clipboard)) +) -> Result> { + Ok(Clipboard) +} +impl DndProvider for Clipboard { + fn init_dnd( + &self, + _tx: Box + Send + Sync + 'static>, + ) { + } + + fn start_dnd( + &self, + _internal: bool, + _source_surface: DndSurface, + _icon_surface: Option, + _content: D, + _actions: DndAction, + ) { + } + + fn end_dnd(&self) {} + + fn register_dnd_destination( + &self, + _surface: DndSurface, + _rectangles: Vec, + ) { + } + + fn set_action(&self, _action: DndAction) {} + + fn peek_offer( + &self, + _mime_type: Option>, + ) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "DnD not supported", + )) + } } pub struct Clipboard; diff --git a/wayland/Cargo.toml b/wayland/Cargo.toml index 6853c9d..615f76c 100644 --- a/wayland/Cargo.toml +++ b/wayland/Cargo.toml @@ -10,4 +10,9 @@ documentation = "https://docs.rs/clipboard_wayland" keywords = ["clipboard", "wayland"] [dependencies] -smithay-clipboard = "0.7" + +smithay-clipboard = { path = "../../smithay-clipboard", features = [ + "dnd", +] } +mime = { path = "../mime" } +dnd = { path = "../dnd" } diff --git a/wayland/src/lib.rs b/wayland/src/lib.rs index fdf62c0..298e87c 100644 --- a/wayland/src/lib.rs +++ b/wayland/src/lib.rs @@ -12,12 +12,99 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::error::Error; -use std::ffi::c_void; -use std::sync::{Arc, Mutex}; +use std::{ + borrow::Cow, + error::Error, + ffi::c_void, + sync::{mpsc::SendError, Arc, Mutex}, +}; + +use dnd::{ + DataWrapper, DndAction, DndDestinationRectangle, DndSurface, Sender, +}; +use mime::ClipboardData; +use smithay_clipboard::dnd::{Icon, Rectangle}; +pub use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType}; + +#[derive(Clone)] +pub struct DndSender(pub Arc + 'static + Send + Sync>); + +impl smithay_clipboard::dnd::Sender for DndSender { + fn send( + &self, + event: smithay_clipboard::dnd::DndEvent, + ) -> Result<(), SendError>> + { + _ = self.0.send(match event { + smithay_clipboard::dnd::DndEvent::Offer(id, e) => dnd::DndEvent::Offer( + id, + match e { + smithay_clipboard::dnd::OfferEvent::Enter { + x, + y, + mime_types, + surface, + } => dnd::OfferEvent::Enter { + x, + y, + mime_types: mime_types + .into_iter() + .map(|m| m.to_string()) + .collect(), + surface, + }, + smithay_clipboard::dnd::OfferEvent::Motion { x, y } => { + dnd::OfferEvent::Motion { x, y } + } + smithay_clipboard::dnd::OfferEvent::LeaveDestination => { + dnd::OfferEvent::LeaveDestination + } + smithay_clipboard::dnd::OfferEvent::Leave => { + dnd::OfferEvent::Leave + } + smithay_clipboard::dnd::OfferEvent::Drop => { + dnd::OfferEvent::Drop + } + smithay_clipboard::dnd::OfferEvent::SelectedAction( + action, + ) => dnd::OfferEvent::SelectedAction(action.into()), + smithay_clipboard::dnd::OfferEvent::Data { + data, + mime_type, + } => dnd::OfferEvent::Data { + data, + mime_type: mime_type.to_string(), + }, + }, + ), + smithay_clipboard::dnd::DndEvent::Source(e) => match e { + smithay_clipboard::dnd::SourceEvent::Finished => { + dnd::DndEvent::Source(dnd::SourceEvent::Finished) + } + smithay_clipboard::dnd::SourceEvent::Cancelled => { + dnd::DndEvent::Source(dnd::SourceEvent::Cancelled) + } + smithay_clipboard::dnd::SourceEvent::Action(action) => { + dnd::DndEvent::Source(dnd::SourceEvent::Action( + action.into(), + )) + } + smithay_clipboard::dnd::SourceEvent::Mime(mime) => { + dnd::DndEvent::Source(dnd::SourceEvent::Mime( + mime.map(|m| m.to_string()), + )) + } + smithay_clipboard::dnd::SourceEvent::Dropped => { + dnd::DndEvent::Source(dnd::SourceEvent::Dropped) + } + }, + }); + Ok(()) + } +} pub struct Clipboard { - context: Arc>, + context: Arc>>, } impl Clipboard { @@ -30,22 +117,173 @@ impl Clipboard { } pub fn read(&self) -> Result> { - Ok(self.context.lock().unwrap().load()?) + Ok(self.context.lock().unwrap().load_text()?) } pub fn read_primary(&self) -> Result> { - Ok(self.context.lock().unwrap().load_primary()?) + Ok(self.context.lock().unwrap().load_primary_text()?) } pub fn write(&mut self, data: String) -> Result<(), Box> { + self.context.lock().unwrap().store_text(data); + + Ok(()) + } + + pub fn write_primary( + &mut self, + data: String, + ) -> Result<(), Box> { + self.context.lock().unwrap().store_primary_text(data); + + Ok(()) + } + + pub fn write_data( + &mut self, + data: T, + ) -> Result<(), Box> { self.context.lock().unwrap().store(data); Ok(()) } - pub fn write_primary(&mut self, data: String) -> Result<(), Box> { + pub fn write_primary_data( + &mut self, + data: T, + ) -> Result<(), Box> { self.context.lock().unwrap().store_primary(data); Ok(()) } + + pub fn read_data( + &self, + ) -> Result> { + Ok(self.context.lock().unwrap().load()?) + } + + pub fn read_primary_data( + &self, + ) -> Result> { + Ok(self.context.lock().unwrap().load_primary()?) + } + + pub fn read_primary_raw( + &self, + allowed: Vec, + ) -> Result<(Vec, String), Box> { + Ok(self + .context + .lock() + .unwrap() + .load_primary_mime::>( + allowed + .into_iter() + .map(|s| MimeType::from(Cow::Owned(s))) + .collect::>(), + ) + .map(|d| (d.0 .0, d.0 .1.to_string()))?) + } + + pub fn read_raw( + &self, + allowed: Vec, + ) -> Result<(Vec, String), Box> { + Ok(self + .context + .lock() + .unwrap() + .load_mime::>( + allowed + .into_iter() + .map(|s| MimeType::from(Cow::Owned(s))) + .collect::>(), + ) + .map(|d| (d.0 .0, d.0 .1))?) + } + + pub fn init_dnd(&self, tx: DndSender) { + _ = self.context.lock().unwrap().init_dnd(Box::new(tx)); + } + + /// Start a DnD operation on the given surface with some data + pub fn start_dnd( + &self, + internal: bool, + source_surface: DndSurface, + icon_surface: Option, + content: D, + actions: DndAction, + ) { + _ = self.context.lock().unwrap().start_dnd( + internal, + source_surface, + icon_surface.map(|i| Icon::::from(i)), + DataWrapper(content), + actions.into(), + ); + } + + /// End the current DnD operation, if there is one + pub fn end_dnd(&self) { + _ = self.context.lock().unwrap().end_dnd(); + } + + /// Register a surface for receiving DnD offers + /// Rectangles should be provided in order of decreasing priority. + /// This method can be called multiple time for a single surface if the + /// rectangles change. + pub fn register_dnd_destination( + &self, + surface: DndSurface, + rectangles: Vec, + ) { + _ = self.context.lock().unwrap().register_dnd_destination( + surface, + rectangles + .into_iter() + .map(|r| RectangleWrapper(r).into()) + .collect(), + ); + } + + /// Set the final action after presenting the user with a choice + pub fn set_action(&self, action: DndAction) { + self.context.lock().unwrap().set_action(action.into()); + } + + /// Peek at the contents of a DnD offer + pub fn peek_offer( + &self, + mime_type: Option>, + ) -> std::io::Result { + let d = self + .context + .lock() + .unwrap() + .peek_offer::>(mime_type.map(MimeType::from)); + d.map(|d| d.0) + } +} + +pub struct RectangleWrapper(pub DndDestinationRectangle); + +impl From + for smithay_clipboard::dnd::DndDestinationRectangle +{ + fn from(RectangleWrapper(d): RectangleWrapper) -> Self { + smithay_clipboard::dnd::DndDestinationRectangle { + id: d.id, + rectangle: Rectangle { + x: d.rectangle.x, + y: d.rectangle.y, + width: d.rectangle.width, + height: d.rectangle.height, + }, + mime_types: d.mime_types.into_iter().map(MimeType::from).collect(), + actions: d.actions.into(), + preferred: d.preferred.into(), + } + } } diff --git a/x11/src/error.rs b/x11/src/error.rs index cf04d2f..a08b07a 100644 --- a/x11/src/error.rs +++ b/x11/src/error.rs @@ -1,5 +1,7 @@ -use x11rb::errors::{ConnectError, ConnectionError, ReplyError}; -use x11rb::protocol::xproto::Atom; +use x11rb::{ + errors::{ConnectError, ConnectionError, ReplyError}, + protocol::xproto::Atom, +}; use std::sync::mpsc; diff --git a/x11/src/lib.rs b/x11/src/lib.rs index 0a1e310..f951252 100644 --- a/x11/src/lib.rs +++ b/x11/src/lib.rs @@ -3,17 +3,23 @@ mod error; pub use error::Error; -use x11rb::connection::Connection as _; -use x11rb::errors::ConnectError; -use x11rb::protocol::xproto::{self, Atom, AtomEnum, EventMask, Window}; -use x11rb::protocol::Event; -use x11rb::rust_connection::RustConnection as Connection; -use x11rb::wrapper::ConnectionExt; +use x11rb::{ + connection::Connection as _, + errors::ConnectError, + protocol::{ + xproto::{self, Atom, AtomEnum, EventMask, Window}, + Event, + }, + rust_connection::RustConnection as Connection, + wrapper::ConnectionExt, +}; -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; -use std::thread; -use std::time::{Duration, Instant}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, + thread, + time::{Duration, Instant}, +}; const POLL_DURATION: std::time::Duration = Duration::from_micros(50); @@ -60,13 +66,16 @@ impl Clipboard { self.read_selection(self.reader.atoms.clipboard) } - /// Read the current PRIMARY [`Clipboard`] value. pub fn read_primary(&self) -> Result { self.read_selection(self.reader.atoms.primary) } - fn write_selection(&mut self, selection: Atom, contents: String) -> Result<(), Error> { + fn write_selection( + &mut self, + selection: Atom, + contents: String, + ) -> Result<(), Error> { let target = self.writer.atoms.utf8_string; self.selections @@ -124,9 +133,13 @@ impl Clipboard { selection, target, property, - x11rb::CURRENT_TIME, // FIXME ^ - // Clients should not use CurrentTime for the time argument of a ConvertSelection request. - // Instead, they should use the timestamp of the event that caused the request to be made. + x11rb::CURRENT_TIME, /* FIXME ^ + * Clients should not use CurrentTime for + * the time argument of a ConvertSelection + * request. + * Instead, they should use the timestamp + * of the event that caused the request to + * be made. */ )?; let _ = self.reader.connection.flush()?; @@ -186,8 +199,9 @@ impl Clipboard { continue; }; - // Note that setting the property argument to None indicates that the - // conversion requested could not be made. + // Note that setting the property argument to None indicates + // that the conversion requested could + // not be made. if event.property == AtomEnum::NONE.into() { break; }