diff --git a/Cargo.toml b/Cargo.toml index fcd381b..2aacaa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ categories = ["gui"] 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"] } @@ -31,9 +32,4 @@ rand = "0.8" winit = "0.29" [workspace] -members = [ - "macos", - "mime", - "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..202a186 --- /dev/null +++ b/dnd/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dnd" +version = "0.1.0" +edition = "2021" + +[dependencies] +mime = { path = "../mime" } +bitflags = "2.5.0" + +[target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten", target_os="ios", target_os="redox"))))'.dependencies] +smithay-clipboard = { git = "https://github.com/pop-os/smithay-clipboard", branch = "dnd", features = [ + "dnd", +] } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", default-features = false, features = [ + "calloop", +] } diff --git a/dnd/src/lib.rs b/dnd/src/lib.rs new file mode 100644 index 0000000..192acae --- /dev/null +++ b/dnd/src/lib.rs @@ -0,0 +1,126 @@ +use std::{ + borrow::Cow, + ffi::c_void, + sync::{mpsc::SendError, Arc}, +}; + +use bitflags::bitflags; + +#[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)] +pub enum DndEvent { + /// Dnd Offer event with the corresponding destination rectangle ID. + Offer(Option, OfferEvent), + /// Dnd Source event. + Source(SourceEvent), +} + +#[derive(Debug)] +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, +} + +#[derive(Debug)] +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, + }, +} + +/// 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>>; +} + +pub trait RawSurface { + /// # Safety + /// + /// returned pointer must be a valid pointer to the underlying surface, and it must + /// remain valid for as long as `RawSurface` object is alive. + unsafe fn get_ptr(&mut self) -> *mut c_void; +} + +/// 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 struct DndSurface(pub Arc>); + +#[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..bcb7db0 --- /dev/null +++ b/dnd/src/platform/linux.rs @@ -0,0 +1,88 @@ +use std::{borrow::Cow, ffi::c_void, sync::Arc}; + +use crate::{DataWrapper, DndAction, DndSurface}; +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 { + // XXX won't panic because this is only called once before it could be cloned + Arc::get_mut(&mut self.0).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 + } +} diff --git a/dnd/src/platform/mod.rs b/dnd/src/platform/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/mime/Cargo.toml b/mime/Cargo.toml index 74bb6ad..247c364 100644 --- a/mime/Cargo.toml +++ b/mime/Cargo.toml @@ -6,6 +6,4 @@ 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 = { git = "https://github.com/pop-os/smithay-clipboard", tag = "pop-mime-types" } - - +smithay-clipboard = { git = "https://github.com/pop-os/smithay-clipboard", branch = "dnd" } diff --git a/src/dnd/mod.rs b/src/dnd/mod.rs new file mode 100644 index 0000000..c43a76e --- /dev/null +++ b/src/dnd/mod.rs @@ -0,0 +1,102 @@ +use std::borrow::Cow; + +use ::dnd::{DndAction, DndDestinationRectangle, Sender}; +use dnd::DndSurface; +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: Cow<'static, str>, + ) -> 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: Cow<'static, str>, + ) -> std::io::Result { + self.raw.peek_offer::(mime_type) + } +} diff --git a/src/lib.rs b/src/lib.rs index 421b3a1..fd89a71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,8 @@ mod platform; #[path = "platform/dummy.rs"] mod platform; +mod dnd; + use mime::ClipboardStoreData; use raw_window_handle::HasDisplayHandle; use std::error::Error; diff --git a/src/platform/linux.rs b/src/platform/linux.rs index bebf78e..d3cb4f2 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,10 +1,14 @@ use crate::{ + dnd::DndProvider, mime::{ClipboardLoadData, ClipboardStoreData}, ClipboardProvider, }; +use dnd::{DndAction, DndDestinationRectangle, DndSurface}; +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; pub use clipboard_x11 as x11; @@ -125,6 +129,78 @@ impl ClipboardProvider for Clipboard { } } +impl DndProvider for Clipboard { + fn init_dnd( + &self, + tx: Box + Send + Sync + 'static>, + ) { + match self { + Clipboard::Wayland(c) => c.init_dnd(DndSender(Arc::new(tx))), + 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, + ), + Clipboard::X11(_) => {} + } + } + + fn end_dnd(&self) { + match self { + Clipboard::Wayland(c) => c.end_dnd(), + Clipboard::X11(_) => {} + } + } + + fn register_dnd_destination( + &self, + surface: DndSurface, + rectangles: Vec, + ) { + match self { + Clipboard::Wayland(c) => { + c.register_dnd_destination(surface, rectangles) + } + Clipboard::X11(_) => {} + } + } + + fn set_action(&self, action: DndAction) { + match self { + Clipboard::Wayland(c) => c.set_action(action), + Clipboard::X11(_) => {} + } + } + + fn peek_offer( + &self, + mime_type: Cow<'static, str>, + ) -> std::io::Result { + match self { + Clipboard::Wayland(c) => c.peek_offer::(mime_type), + Clipboard::X11(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "DnD not supported", + )), + } + } +} + pub unsafe fn connect( window: &W, ) -> Result> { diff --git a/wayland/Cargo.toml b/wayland/Cargo.toml index 3e28518..6e0c01d 100644 --- a/wayland/Cargo.toml +++ b/wayland/Cargo.toml @@ -10,5 +10,9 @@ documentation = "https://docs.rs/clipboard_wayland" keywords = ["clipboard", "wayland"] [dependencies] -smithay-clipboard = { git = "https://github.com/pop-os/smithay-clipboard", tag = "pop-mime-types" } -mime = { path = "../mime" } \ No newline at end of file + +smithay-clipboard = { git = "https://github.com/pop-os/smithay-clipboard", branch = "dnd", features = [ + "dnd", +] } +mime = { path = "../mime" } +dnd = { path = "../dnd" } diff --git a/wayland/src/lib.rs b/wayland/src/lib.rs index b14586e..94ef900 100644 --- a/wayland/src/lib.rs +++ b/wayland/src/lib.rs @@ -16,13 +16,96 @@ use std::{ borrow::Cow, error::Error, ffi::c_void, - sync::{Arc, Mutex}, + sync::{mpsc::SendError, Arc, Mutex}, }; +use dnd::{ + DataWrapper, DndAction, DndDestinationRectangle, DndSurface, Sender, +}; +use smithay_clipboard::dnd::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 { @@ -120,4 +203,88 @@ impl Clipboard { ) .map(|(d, m)| (d, m.to_string()))?) } + + 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, + 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: Cow<'static, str>, + ) -> std::io::Result { + let d = self + .context + .lock() + .unwrap() + .peek_offer::>(mime_type.into()); + 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(), + } + } }