diff --git a/Cargo.toml b/Cargo.toml index cb7992b..4b208ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,12 @@ rust-version = "1.65.0" [dependencies] libc = "0.2.149" -sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop"] } -wayland-backend = { version = "0.3.0", default_features = false, features = ["client_system"] } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", default-features = false, features = ["calloop"] } +wayland-backend = { version = "0.3.3", default_features = false, features = ["client_system"] } [dev-dependencies] dirs = "5.0.1" -sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop", "xkbcommon"] } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", default-features = false, features = ["calloop", "xkbcommon"] } thiserror = "1.0.57" url = "2.5.0" diff --git a/examples/clipboard.rs b/examples/clipboard.rs index 557e531..de751c1 100644 --- a/examples/clipboard.rs +++ b/examples/clipboard.rs @@ -11,6 +11,7 @@ use sctk::output::{OutputHandler, OutputState}; use sctk::reexports::calloop::{EventLoop, LoopHandle}; use sctk::reexports::calloop_wayland_source::WaylandSource; use sctk::reexports::client::globals::registry_queue_init; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::protocol::{wl_keyboard, wl_output, wl_seat, wl_shm, wl_surface}; use sctk::reexports::client::{Connection, Proxy, QueueHandle}; @@ -26,7 +27,8 @@ use sctk::{ delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers, }; -use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType}; +use smithay_clipboard::dnd::{DndDestinationRectangle, Rectangle}; +use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType, ALLOWED_TEXT_MIME_TYPES}; use smithay_clipboard::{Clipboard, SimpleClipboard}; use thiserror::Error; use url::Url; @@ -66,12 +68,22 @@ fn main() { let pool = SlotPool::new(MIN_DIM_SIZE * MIN_DIM_SIZE * 4, &shm).expect("Failed to create pool"); let (tx, rx) = sctk::reexports::calloop::channel::sync_channel(10); - clipboard.init_dnd(Box::new(tx)); + clipboard.init_dnd(Box::new(tx)).expect("Failed to set up DnD"); - event_loop.handle().insert_source(rx, |event, _, state| { + _ = event_loop.handle().insert_source(rx, |event, _, _state| { dbg!(event); }); + clipboard.register_dnd_destination(window.wl_surface().clone(), vec![ + DndDestinationRectangle { + id: 0, + rectangle: Rectangle { x: 0., y: 0., width: 256., height: 256. }, + mime_types: ALLOWED_TEXT_MIME_TYPES.iter().map(|m| Cow::from(m.to_string())).collect(), + actions: DndAction::all(), + preferred: DndAction::Copy, + }, + ]); + let mut simple_window = SimpleWindow { registry_state: RegistryState::new(&globals), seat_state: SeatState::new(&globals, &queue_handle), @@ -367,6 +379,7 @@ impl KeyboardHandler for SimpleWindow { _: &wl_keyboard::WlKeyboard, _serial: u32, _modifiers: Modifiers, + _: u32, ) { } } diff --git a/src/dnd/mod.rs b/src/dnd/mod.rs index 9a67276..0e90ce7 100644 --- a/src/dnd/mod.rs +++ b/src/dnd/mod.rs @@ -1,16 +1,22 @@ +use std::borrow::Cow; use std::ffi::c_void; use std::fmt::Debug; use std::sync::mpsc::SendError; use sctk::reexports::calloop; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::{Connection, Proxy}; use wayland_backend::client::{InvalidId, ObjectId}; +use crate::mime::AsMimeTypes; use crate::Clipboard; +pub mod state; + +#[derive(Clone)] pub struct DndSurface { - pub surface: WlSurface, + pub(crate) surface: WlSurface, pub s: T, } @@ -46,9 +52,100 @@ pub trait Sender { fn send(&self, t: DndEvent) -> Result<(), SendError>>; } +#[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 + /// [`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_actions`] 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 [`Surface`] +#[derive(Debug, Default, Clone)] +pub struct Rectangle { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +impl Rectangle { + fn contains(&self, x: f64, y: f64) -> bool { + self.x <= x && self.x + self.width >= x && self.y <= y && self.y + self.height >= y + } +} + +#[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, +} + +pub enum DndRequest { + /// Init DnD + InitDnD(Box + Send>), + /// Register a surface for receiving Dnd events. + Surface(DndSurface, Vec), + /// Start a Dnd operation with the given source surface and data. + StartDnd { + source: DndSurface, + icon: Option>, + content: Box, + }, + /// Set the DnD action chosen by the user. + SetAction(DndAction), +} + #[derive(Debug)] pub enum DndEvent { - Test(T), + /// Dnd Offer event with the corresponding destination rectangle ID. + Offer(Option, OfferEvent), + /// Dnd Source event. + Source(SourceEvent), } impl Sender for calloop::channel::Sender> { @@ -63,21 +160,44 @@ impl Sender for calloop::channel::SyncSender> { } } -impl Clipboard { +impl Clipboard { /// Set up DnD operations for the Clipboard pub fn init_dnd( &self, tx: Box + Send>, ) -> Result<(), SendError>> { - self.request_sender.send(crate::worker::Command::InitDnD(tx)) + self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::InitDnD(tx))) } /// Start a DnD operation on the given surface with some data - pub fn start_dnd(&self, s: D) { - let s = DndSurface::new(s, &self.connection).unwrap(); - dbg!(&s.surface); + pub fn start_dnd( + &self, + source_surface: T, + icon_surface: Option, + content: D, + ) { + let source = DndSurface::new(source_surface, &self.connection).unwrap(); + let icon = icon_surface.map(|s| DndSurface::new(s, &self.connection).unwrap()); + _ = self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::StartDnd { + source, + icon, + content: Box::new(content), + })); } /// End the current DnD operation, if there is one pub fn end_dnd() {} + + /// Register a surface for receiving DnD offers + /// Rectangles should be provided in order of decreasing priority. + pub fn register_dnd_destination(&self, surface: T, rectangles: Vec) { + let s = DndSurface::new(surface, &self.connection).unwrap(); + + _ = self + .request_sender + .send(crate::worker::Command::DndRequest(DndRequest::Surface(s, rectangles))); + } + + /// Set the final action after presenting the user with a choice + pub fn set_action(&self, action: DndAction) {} } diff --git a/src/dnd/state.rs b/src/dnd/state.rs new file mode 100644 index 0000000..8d6d23b --- /dev/null +++ b/src/dnd/state.rs @@ -0,0 +1,315 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::io::{Error, ErrorKind, Read}; +use std::mem; +use std::os::unix::io::{AsRawFd, RawFd}; +use std::rc::Rc; + +use sctk::data_device_manager::data_offer::DragOffer; +use sctk::data_device_manager::data_source::DragSource; +use sctk::reexports::calloop::PostAction; +use sctk::reexports::client::protocol::wl_data_device::WlDataDevice; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Proxy; +use wayland_backend::client::ObjectId; + +use crate::mime::{AsMimeTypes, MimeType}; +use crate::state::{set_non_blocking, State, Target}; +use crate::text::Text; + +use super::{DndDestinationRectangle, DndEvent, DndRequest, DndSurface, OfferEvent}; + +pub(crate) struct DndState { + pub(crate) sender: Option>>, + destinations: HashMap, Vec)>, + dnd_sources: Option, + active_surface: Option<(DndSurface, Option)>, + source_actions: DndAction, + selected_action: DndAction, + selected_mime: Option, + pub(crate) source_content: Box, + pub(crate) source_mime_types: Rc>, +} + +impl Default for DndState { + fn default() -> Self { + Self { + sender: Default::default(), + destinations: Default::default(), + dnd_sources: Default::default(), + active_surface: None, + source_actions: DndAction::empty(), + selected_action: DndAction::empty(), + selected_mime: None, + source_content: Box::new(Text(String::new())), + source_mime_types: Rc::new(Cow::Owned(Vec::new())), + } + } +} + +impl DndState { + pub(crate) fn selected_action(&mut self, a: DndAction) { + self.selected_action = a; + } +} + +impl State +where + T: Clone + 'static, +{ + pub fn update_active_surface( + &mut self, + surface: &WlSurface, + x: f64, + y: f64, + dnd_state: Option<&DragOffer>, + ) { + let had_dest = self + .dnd_state + .active_surface + .as_ref() + .map(|(_, d)| d.as_ref().map(|d| d.id)) + .unwrap_or_default(); + self.dnd_state.active_surface = + self.dnd_state.destinations.get(&surface.id()).map(|(s, dests)| { + let Some((dest, mime, actions)) = dests.iter().find_map(|r| { + let actions = dnd_state.as_ref().map(|s| { + ( + s.source_actions.intersection(r.actions), + s.source_actions.intersection(r.preferred), + ) + }); + let mime = dnd_state.as_ref().and_then(|dnd_state| { + r.mime_types.iter().find(|m| { + dnd_state.with_mime_types(|mimes| mimes.iter().any(|a| &a == m)) + }) + }); + + dbg!(actions); + dbg!(mime); + + (r.rectangle.contains(x, y) + && (r.mime_types.is_empty() || mime.is_some()) + && (r.actions.is_all() + || dnd_state + .as_ref() + .map(|dnd_state| dnd_state.source_actions.intersects(r.actions)) + .unwrap_or(true))) + .then(|| (r.clone(), mime, actions)) + }) else { + if let Some(old_id) = had_dest { + if let Some(dnd_state) = dnd_state.as_ref() { + if let Some(tx) = self.dnd_state.sender.as_ref() { + _ = tx.send(DndEvent::Offer( + Some(old_id), + super::OfferEvent::LeaveDestination, + )); + } + dnd_state.set_actions(DndAction::empty(), DndAction::empty()); + dnd_state.accept_mime_type(dnd_state.serial, None); + self.dnd_state.selected_action = DndAction::empty(); + self.dnd_state.selected_mime = None; + } + } + return (s.clone(), None); + }; + if let (Some((action, preferred_action)), Some(mime_type), Some(dnd_state)) = + (actions, mime, dnd_state.as_ref()) + { + dnd_state.set_actions(action, preferred_action); + self.dnd_state.selected_mime = Some(mime_type.to_string()); + dnd_state.accept_mime_type(dnd_state.serial, Some(mime_type.to_string())) + } + (s.clone(), Some(dest)) + }); + } + + fn cur_id(&self) -> Option { + self.dnd_state.active_surface.as_ref().and_then(|(_, rect)| rect.as_ref().map(|r| r.id)) + } + + pub(crate) fn offer_drop(&mut self, wl_data_device: &WlDataDevice) { + let Some(tx) = self.dnd_state.sender.as_ref() else { + return; + }; + let id = self.cur_id(); + _ = tx.send(DndEvent::Offer(id, super::OfferEvent::Drop)); + + let Some(data_device) = self + .seats + .iter() + .find_map(|(_, s)| s.data_device.as_ref().filter(|dev| dev.inner() == wl_data_device)) + else { + return; + }; + let Some(dnd_state) = data_device.data().drag_offer() else { + return; + }; + + if self.dnd_state.selected_action == DndAction::Ask { + _ = tx.send(DndEvent::Offer( + id, + super::OfferEvent::SelectedAction(self.dnd_state.selected_action), + )); + return; + } else if self.dnd_state.selected_action.is_empty() { + return; + } + let Some(mime) = self.dnd_state.selected_mime.take() else { + dnd_state.accept_mime_type(dnd_state.serial, None); + return; + }; + + dnd_state.set_actions(self.dnd_state.selected_action, self.dnd_state.selected_action); + dnd_state.accept_mime_type(dnd_state.serial, Some(mime.clone())); + + _ = self.load_dnd(MimeType::Other(mime.into())); + } + + pub(crate) fn offer_enter( + &mut self, + x: f64, + y: f64, + surface: &WlSurface, + wl_data_device: &WlDataDevice, + ) { + if self.dnd_state.sender.is_none() { + return; + } + dbg!(&self.dnd_state.destinations); + let Some(data_device) = self + .seats + .iter() + .find_map(|(_, s)| s.data_device.as_ref().filter(|dev| dev.inner() == wl_data_device)) + else { + return; + }; + let dnd_state = data_device.data().drag_offer(); + self.update_active_surface(surface, x, y, dnd_state.as_ref()); + let Some((surface, id)) = self + .dnd_state + .active_surface + .as_ref() + .map(|(s, d)| (s.clone(), d.as_ref().map(|d| d.id))) + else { + return; + }; + let Some(tx) = self.dnd_state.sender.as_ref() else { + return; + }; + // TODO accept mime / action + _ = tx.send(DndEvent::Offer(id, super::OfferEvent::Enter { + x, + y, + surface: surface.s, + mime_types: Vec::new(), + })); + } + + pub(crate) fn offer_motion(&mut self, x: f64, y: f64, wl_data_device: &WlDataDevice) { + let Some((surface, dest)) = self + .dnd_state + .active_surface + .clone() + .map(|(s, dest)| (s, dest.filter(|d| d.rectangle.contains(x, y)))) + else { + return; + }; + let Some(data_device) = self + .seats + .iter() + .find_map(|(_, s)| s.data_device.as_ref().filter(|dev| dev.inner() == wl_data_device)) + else { + return; + }; + let dnd_state = data_device.data().drag_offer(); + if dest.is_none() { + self.update_active_surface(&surface.surface, x, y, dnd_state.as_ref()); + } + let id = self.cur_id(); + if let Some(tx) = self.dnd_state.sender.as_ref() { + _ = tx.send(DndEvent::Offer(id, super::OfferEvent::Motion { x, y })); + } + } + + pub(crate) fn offer_leave(&mut self) { + if let Some(tx) = self.dnd_state.sender.as_ref() { + self.dnd_state.active_surface = None; + self.dnd_state.selected_action = DndAction::empty(); + self.dnd_state.selected_mime = None; + _ = tx.send(DndEvent::Offer(None, super::OfferEvent::Leave)) + } + } + + pub(crate) fn handle_dnd_request(&mut self, r: DndRequest) { + match r { + DndRequest::InitDnD(sender) => self.dnd_state.sender = Some(sender), + DndRequest::Surface(s, dests) => { + self.dnd_state.destinations.insert(s.surface.id(), (s, dests)); + }, + DndRequest::StartDnd { source, icon, content } => {}, + DndRequest::SetAction(_) => { + todo!() + }, + }; + } + + /// Load data for the given target. + pub fn load_dnd(&mut self, mut mime_type: MimeType) -> std::io::Result<()> { + let cur_id = self.cur_id(); + let latest = self + .latest_seat + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::Other, "no events received on any seat"))?; + let seat = self + .seats + .get_mut(latest) + .ok_or_else(|| Error::new(ErrorKind::Other, "active seat lost"))?; + + if !seat.has_focus { + return Err(Error::new(ErrorKind::Other, "client doesn't have focus")); + } + let offer = seat + .data_device + .as_ref() + .and_then(|d| d.data().drag_offer()) + .ok_or_else(|| Error::new(ErrorKind::Other, "offer does not exist."))?; + + let read_pipe = { offer.receive(mime_type.to_string())? }; + + // Mark FD as non-blocking so we won't block ourselves. + unsafe { + set_non_blocking(read_pipe.as_raw_fd())?; + } + + let mut reader_buffer = [0; 4096]; + let mut content = Vec::new(); + let _ = self.loop_handle.insert_source(read_pipe, move |_, file, state| { + let file = unsafe { file.get_mut() }; + let Some(tx) = state.dnd_state.sender.as_ref() else { + return PostAction::Remove; + }; + loop { + match file.read(&mut reader_buffer) { + Ok(0) => { + offer.finish(); + let _ = tx.send(DndEvent::Offer(cur_id, OfferEvent::Data { + data: mem::take(&mut content), + mime_type: mem::take(&mut mime_type).to_string(), + })); + break PostAction::Remove; + }, + Ok(n) => content.extend_from_slice(&reader_buffer[..n]), + Err(err) if err.kind() == ErrorKind::WouldBlock => break PostAction::Continue, + Err(_) => { + // let _ = state.dnd_state.sender.unwrap().send(Err(err)); + break PostAction::Remove; + }, + }; + } + }); + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 0e0d0e1..1c0895f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ use std::sync::mpsc::{self, Receiver}; use sctk::reexports::calloop::channel::{self, Sender}; use sctk::reexports::client::backend::Backend; +use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::Connection; #[cfg(feature = "dnd")] @@ -24,7 +25,7 @@ use mime::{AllowedMimeTypes, AsMimeTypes, MimeType}; use state::SelectionTarget; use text::Text; -pub type SimpleClipboard = Clipboard<()>; +pub type SimpleClipboard = Clipboard; /// Access to a Wayland clipboard. pub struct Clipboard { @@ -34,7 +35,7 @@ pub struct Clipboard { connection: Connection, } -impl Clipboard { +impl Clipboard { /// Creates new clipboard which will be running on its own thread with its /// own event queue to handle clipboard requests. /// @@ -62,7 +63,7 @@ impl Clipboard { /// /// Load the requested type from a clipboard on the last observed seat. pub fn load(&self) -> Result { - self.load_inner(SelectionTarget::Clipboard, D::allowed()) + self.load_inner(Target::Clipboard, D::allowed()) } /// Load clipboard data. @@ -77,7 +78,7 @@ impl Clipboard { /// Load the requested type from a primary clipboard on the last observed /// seat. pub fn load_primary(&self) -> Result { - self.load_inner(SelectionTarget::Primary, D::allowed()) + self.load_inner(Target::Primary, D::allowed()) } /// Load primary clipboard data. @@ -94,7 +95,7 @@ impl Clipboard { &self, allowed: impl Into>, ) -> Result<(Vec, MimeType)> { - self.load_inner(SelectionTarget::Clipboard, allowed) + self.load_inner(Target::Clipboard, allowed) } /// Load raw primary clipboard data. @@ -104,14 +105,14 @@ impl Clipboard { &self, allowed: impl Into>, ) -> Result<(Vec, MimeType)> { - self.load_inner(SelectionTarget::Primary, allowed) + self.load_inner(Target::Primary, allowed) } /// Store custom data to a clipboard. /// /// Stores data of the provided type to a clipboard on a last observed seat. pub fn store(&self, data: D) { - self.store_inner(data, SelectionTarget::Clipboard); + self.store_inner(data, Target::Clipboard); } /// Store to a clipboard. @@ -126,7 +127,7 @@ impl Clipboard { /// Stores data of the provided type to a primary clipboard on a last /// observed seat. pub fn store_primary(&self, data: D) { - self.store_inner(data, SelectionTarget::Primary); + self.store_inner(data, Target::Primary); } /// Store to a primary clipboard. @@ -138,7 +139,7 @@ impl Clipboard { fn load_inner, MimeType)> + 'static>( &self, - target: SelectionTarget, + target: Target, allowed: impl Into>, ) -> Result { let _ = self.request_sender.send(worker::Command::Load(allowed.into(), target)); @@ -158,7 +159,7 @@ impl Clipboard { } } - fn store_inner(&self, data: D, target: SelectionTarget) { + fn store_inner(&self, data: D, target: Target) { let request = worker::Command::Store(Box::new(data), target); let _ = self.request_sender.send(request); } diff --git a/src/state.rs b/src/state.rs index debe84f..32812cb 100644 --- a/src/state.rs +++ b/src/state.rs @@ -9,11 +9,12 @@ use std::sync::mpsc::Sender; use sctk::data_device_manager::data_device::{DataDevice, DataDeviceHandler}; use sctk::data_device_manager::data_offer::{DataOfferError, DataOfferHandler, DragOffer}; -use sctk::data_device_manager::data_source::{CopyPasteSource, DataSourceHandler}; +use sctk::data_device_manager::data_source::{CopyPasteSource, DataSourceHandler, DragSource}; use sctk::data_device_manager::{DataDeviceManagerState, WritePipe}; use sctk::primary_selection::device::{PrimarySelectionDevice, PrimarySelectionDeviceHandler}; use sctk::primary_selection::selection::{PrimarySelectionSource, PrimarySelectionSourceHandler}; use sctk::primary_selection::PrimarySelectionManagerState; +use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::registry::{ProvidesRegistryState, RegistryState}; use sctk::seat::pointer::{PointerData, PointerEvent, PointerEventKind, PointerHandler}; use sctk::seat::{Capability, SeatHandler, SeatState}; @@ -37,6 +38,8 @@ use sctk::reexports::protocols::wp::primary_selection::zv1::client::{ }; use wayland_backend::client::ObjectId; +use crate::dnd::state::DndState; +use crate::dnd::{DndDestinationRectangle, DndSurface}; use crate::mime::{AsMimeTypes, MimeType}; use crate::text::Text; @@ -49,11 +52,11 @@ pub struct State { registry_state: RegistryState, seat_state: SeatState, - seats: HashMap, + pub(crate) seats: HashMap, /// The latest seat which got an event. - latest_seat: Option, + pub(crate) latest_seat: Option, - loop_handle: LoopHandle<'static, Self>, + pub(crate) loop_handle: LoopHandle<'static, Self>, queue_handle: QueueHandle, primary_sources: Vec, @@ -64,11 +67,11 @@ pub struct State { data_selection_content: Box, data_selection_mime_types: Rc>, #[cfg(feature = "dnd")] - pub(crate) sender: Option>>, + pub(crate) dnd_state: crate::dnd::state::DndState, _phantom: PhantomData, } -impl State { +impl State { #[must_use] pub fn new( globals: &GlobalList, @@ -110,7 +113,7 @@ impl State { primary_selection_mime_types: Rc::new(Default::default()), data_selection_mime_types: Rc::new(Default::default()), #[cfg(feature = "dnd")] - sender: None, + dnd_state: DndState::default(), _phantom: PhantomData, }) } @@ -118,11 +121,7 @@ impl State { /// Store selection for the given target. /// /// Selection source is only created when `Some(())` is returned. - pub fn store_selection( - &mut self, - ty: SelectionTarget, - contents: Box, - ) -> Option<()> { + pub fn store_selection(&mut self, ty: Target, contents: Box) -> Option<()> { let latest = self.latest_seat.as_ref()?; let seat = self.seats.get_mut(latest)?; @@ -131,7 +130,7 @@ impl State { } match ty { - SelectionTarget::Clipboard => { + Target::Clipboard => { let mgr = self.data_device_manager_state.as_ref()?; let mime_types = contents.available(); self.data_selection_content = contents; @@ -140,7 +139,7 @@ impl State { source.set_selection(seat.data_device.as_ref().unwrap(), seat.latest_serial); self.data_sources.push(source); }, - SelectionTarget::Primary => { + Target::Primary => { let mgr = self.primary_selection_manager_state.as_ref()?; let mime_types = contents.available(); self.primary_selection_content = contents; @@ -149,17 +148,17 @@ impl State { source.set_selection(seat.primary_device.as_ref().unwrap(), seat.latest_serial); self.primary_sources.push(source); }, + #[cfg(feature = "dnd")] + Target::DnD => { + unreachable!() + }, } Some(()) } - /// Load selection for the given target. - pub fn load_selection( - &mut self, - ty: SelectionTarget, - allowed_mime_types: &[MimeType], - ) -> Result<()> { + /// Load data for the given target. + pub fn load(&mut self, ty: Target, allowed_mime_types: &[MimeType]) -> Result<()> { let latest = self .latest_seat .as_ref() @@ -174,7 +173,7 @@ impl State { } let (read_pipe, mut mime_type) = match ty { - SelectionTarget::Clipboard => { + Target::Clipboard => { let selection = seat .data_device .as_ref() @@ -197,7 +196,7 @@ impl State { mime_type, ) }, - SelectionTarget::Primary => { + Target::Primary => { let selection = seat .primary_device .as_ref() @@ -212,6 +211,21 @@ impl State { (selection.receive(mime_type.to_string())?, mime_type) }, + #[cfg(feature = "dnd")] + Target::DnD => { + let offer = seat + .data_device + .as_ref() + .and_then(|d| d.data().drag_offer()) + .ok_or_else(|| Error::new(ErrorKind::Other, "offer does not exist."))?; + let Some(mime) = allowed_mime_types.get(0) else { + return Err(Error::new( + ErrorKind::NotFound, + "supported mime-type is not found", + )); + }; + (offer.receive(mime.to_string())?, mime.clone()) + }, }; // Mark FD as non-blocking so we won't block ourselves. @@ -244,10 +258,12 @@ impl State { Ok(()) } - fn send_request(&mut self, ty: SelectionTarget, write_pipe: WritePipe, mime: String) { + fn send_request(&mut self, ty: Target, write_pipe: WritePipe, mime: String) { let Some(mime_type) = MimeType::find_allowed(&[mime], match ty { - SelectionTarget::Clipboard => &self.data_selection_mime_types, - SelectionTarget::Primary => &self.primary_selection_mime_types, + Target::Clipboard => &self.data_selection_mime_types, + Target::Primary => &self.primary_selection_mime_types, + #[cfg(feature = "dnd")] + Target::DnD => &self.dnd_state.source_mime_types, }) else { return; }; @@ -262,8 +278,10 @@ impl State { // Don't access the content on the state directly, since it could change during // the send. let contents = match ty { - SelectionTarget::Clipboard => self.data_selection_content.as_bytes(&mime_type), - SelectionTarget::Primary => self.primary_selection_content.as_bytes(&mime_type), + Target::Clipboard => self.data_selection_content.as_bytes(&mime_type), + Target::Primary => self.primary_selection_content.as_bytes(&mime_type), + #[cfg(feature = "dnd")] + Target::DnD => self.dnd_state.source_content.as_bytes(&mime_type), }; let Some(contents) = contents else { @@ -288,7 +306,7 @@ impl State { } } -impl SeatHandler for State { +impl SeatHandler for State { fn seat_state(&mut self) -> &mut SeatState { &mut self.seat_state } @@ -371,7 +389,7 @@ impl SeatHandler for State { } } -impl PointerHandler for State { +impl PointerHandler for State { fn pointer_frame( &mut self, _: &Connection, @@ -405,20 +423,50 @@ impl PointerHandler for State { } } -impl DataDeviceHandler for State { - fn enter(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} +impl DataDeviceHandler for State +where + DndSurface: Clone, +{ + fn enter( + &mut self, + _: &Connection, + _: &QueueHandle, + wl_data_device: &WlDataDevice, + x: f64, + y: f64, + surface: &WlSurface, + ) { + #[cfg(feature = "dnd")] + self.offer_enter(x, y, surface, wl_data_device); + } - fn leave(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + fn leave(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) { + #[cfg(feature = "dnd")] + self.offer_leave(); + } - fn motion(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + wl_data_device: &WlDataDevice, + x: f64, + y: f64, + ) { + #[cfg(feature = "dnd")] + self.offer_motion(x, y, wl_data_device); + } - fn drop_performed(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + fn drop_performed(&mut self, _: &Connection, _: &QueueHandle, d: &WlDataDevice) { + #[cfg(feature = "dnd")] + self.offer_drop(d) + } // The selection is finished and ready to be used. fn selection(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} } -impl DataSourceHandler for State { +impl DataSourceHandler for State { fn send_request( &mut self, _: &Connection, @@ -427,7 +475,7 @@ impl DataSourceHandler for State { mime: String, write_pipe: WritePipe, ) { - self.send_request(SelectionTarget::Clipboard, write_pipe, mime) + self.send_request(Target::Clipboard, write_pipe, mime) } fn cancelled(&mut self, _: &Connection, _: &QueueHandle, deleted: &WlDataSource) { @@ -450,7 +498,7 @@ impl DataSourceHandler for State { fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) {} } -impl DataOfferHandler for State { +impl DataOfferHandler for State { fn source_actions( &mut self, _: &Connection, @@ -465,12 +513,14 @@ impl DataOfferHandler for State { _: &Connection, _: &QueueHandle, _: &mut DragOffer, - _: DndAction, + action: DndAction, ) { + #[cfg(feature = "dnd")] + self.dnd_state.selected_action(action); } } -impl ProvidesRegistryState for State { +impl ProvidesRegistryState for State { registry_handlers![SeatState]; fn registry(&mut self) -> &mut RegistryState { @@ -478,7 +528,7 @@ impl ProvidesRegistryState for State { } } -impl PrimarySelectionDeviceHandler for State { +impl PrimarySelectionDeviceHandler for State { fn selection( &mut self, _: &Connection, @@ -488,7 +538,7 @@ impl PrimarySelectionDeviceHandler for State { } } -impl PrimarySelectionSourceHandler for State { +impl PrimarySelectionSourceHandler for State { fn send_request( &mut self, _: &Connection, @@ -497,7 +547,7 @@ impl PrimarySelectionSourceHandler for State { mime: String, write_pipe: WritePipe, ) { - self.send_request(SelectionTarget::Primary, write_pipe, mime); + self.send_request(Target::Primary, write_pipe, mime); } fn cancelled( @@ -510,7 +560,7 @@ impl PrimarySelectionSourceHandler for State { } } -impl Dispatch> for State { +impl Dispatch> for State { fn event( state: &mut State, _: &WlKeyboard, @@ -543,27 +593,30 @@ impl Dispatch> for State { } } -delegate_seat!(@ State); -delegate_pointer!(@ State); -delegate_data_device!(@ State); -delegate_primary_selection!(@ State); -delegate_registry!(@ State); +delegate_seat!(@ State); +delegate_pointer!(@ State); +delegate_data_device!(@ State); +delegate_primary_selection!(@ State); +delegate_registry!(@ State); #[derive(Debug, Clone, Copy)] -pub enum SelectionTarget { +pub enum Target { /// The target is clipboard selection. Clipboard, /// The target is primary selection. Primary, + #[cfg(feature = "dnd")] + /// The targe is a DnD offer. + DnD, } #[derive(Debug, Default)] -struct ClipboardSeatState { +pub(crate) struct ClipboardSeatState { keyboard: Option, pointer: Option, - data_device: Option, + pub(crate) data_device: Option, primary_device: Option, - has_focus: bool, + pub(crate) has_focus: bool, /// The latest serial used to set the selection content. latest_serial: u32, @@ -585,7 +638,7 @@ impl Drop for ClipboardSeatState { } } -unsafe fn set_non_blocking(raw_fd: RawFd) -> std::io::Result<()> { +pub(crate) unsafe fn set_non_blocking(raw_fd: RawFd) -> std::io::Result<()> { let flags = libc::fcntl(raw_fd, libc::F_GETFL); if flags < 0 { diff --git a/src/worker.rs b/src/worker.rs index edc08a7..0a4f700 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -9,12 +9,13 @@ use sctk::reexports::calloop_wayland_source::WaylandSource; use sctk::reexports::client::globals::registry_queue_init; use sctk::reexports::client::Connection; +use crate::dnd::DndRequest; use crate::mime::{AsMimeTypes, MimeType}; -use crate::state::{SelectionTarget, State}; +use crate::state::{State, Target}; /// Spawn a clipboard worker, which dispatches its own `EventQueue` and handles /// clipboard requests. -pub fn spawn( +pub fn spawn( name: String, display: Connection, rx_chan: Channel>, @@ -31,12 +32,12 @@ pub fn spawn( /// Clipboard worker thread command. pub enum Command { /// Loads data for the first available mime type in the provided list. - Load(Cow<'static, [MimeType]>, SelectionTarget), + Load(Cow<'static, [MimeType]>, Target), /// Store Data with the given mime types. - Store(Box, SelectionTarget), + Store(Box, Target), #[cfg(feature = "dnd")] /// Init DnD - InitDnD(Box + Send>), + DndRequest(DndRequest), /// Shutdown the worker. Exit, /// Phantom data @@ -44,7 +45,7 @@ pub enum Command { } /// Handle clipboard requests. -fn worker_impl( +fn worker_impl( connection: Connection, rx_chan: Channel>, reply_tx: Sender, MimeType)>>, @@ -71,21 +72,17 @@ fn worker_impl( Command::Store(data, target) => { state.store_selection(target, data); }, - Command::Load(mime_types, SelectionTarget::Clipboard) + Command::Load(mime_types, Target::Clipboard) if state.data_device_manager_state.is_some() => { - if let Err(err) = - state.load_selection(SelectionTarget::Clipboard, &mime_types) - { + if let Err(err) = state.load(Target::Clipboard, &mime_types) { let _ = state.reply_tx.send(Err(err)); } }, - Command::Load(mime_types, SelectionTarget::Primary) + Command::Load(mime_types, Target::Primary) if state.primary_selection_manager_state.is_some() => { - if let Err(err) = - state.load_selection(SelectionTarget::Primary, &mime_types) - { + if let Err(err) = state.load(Target::Primary, &mime_types) { let _ = state.reply_tx.send(Err(err)); } }, @@ -96,8 +93,8 @@ fn worker_impl( ))); }, #[cfg(feature = "dnd")] - Command::InitDnD(sender) => { - state.sender = Some(sender); + Command::DndRequest(r) => { + state.handle_dnd_request(r); }, Command::Phantom(_) => unreachable!(), }