From 2b39656463655377ea920e3bac471306263d16ea Mon Sep 17 00:00:00 2001 From: Darksome Date: Sat, 6 May 2023 19:35:57 +0400 Subject: [PATCH] Implement wlr_data_control protocol --- src/config/mod.rs | 2 + src/shell/focus/mod.rs | 22 +- src/state.rs | 29 ++ src/wayland/handlers/data_control.rs | 109 +++++++ src/wayland/handlers/data_device.rs | 86 ++++-- src/wayland/handlers/mod.rs | 1 + src/wayland/handlers/primary_selection.rs | 95 ++++-- src/wayland/protocols/data_control/device.rs | 133 +++++++++ src/wayland/protocols/data_control/manager.rs | 95 ++++++ src/wayland/protocols/data_control/mod.rs | 274 ++++++++++++++++++ src/wayland/protocols/data_control/offer.rs | 76 +++++ src/wayland/protocols/data_control/source.rs | 140 +++++++++ src/wayland/protocols/mod.rs | 1 + src/xwayland.rs | 103 +++++-- 14 files changed, 1090 insertions(+), 76 deletions(-) create mode 100644 src/wayland/handlers/data_control.rs create mode 100644 src/wayland/protocols/data_control/device.rs create mode 100644 src/wayland/protocols/data_control/manager.rs create mode 100644 src/wayland/protocols/data_control/mod.rs create mode 100644 src/wayland/protocols/data_control/offer.rs create mode 100644 src/wayland/protocols/data_control/source.rs diff --git a/src/config/mod.rs b/src/config/mod.rs index 9e34d709..4c939a8c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -41,6 +41,7 @@ pub struct StaticConfig { pub active_hint: u8, #[serde(default = "default_gaps")] pub gaps: (u8, u8), + pub data_control_enabled: bool, } #[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)] @@ -235,6 +236,7 @@ impl Config { tiling_enabled: false, active_hint: default_active_hint(), gaps: default_gaps(), + data_control_enabled: false, } } diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs index bd53c4fa..79bb7b14 100644 --- a/src/shell/focus/mod.rs +++ b/src/shell/focus/mod.rs @@ -1,6 +1,6 @@ use crate::{ shell::{element::CosmicMapped, Shell, Workspace}, - state::Common, + state::{Common, SelectionSources}, utils::prelude::*, wayland::handlers::xdg_shell::PopupGrabData, xwayland::XWaylandState, @@ -11,7 +11,7 @@ use smithay::{ input::Seat, utils::{IsAlive, Serial, SERIAL_COUNTER}, }; -use std::cell::RefCell; +use std::cell::{RefCell, RefMut}; use tracing::{debug, trace}; use self::target::{KeyboardFocusTarget, WindowGroup}; @@ -165,6 +165,24 @@ impl Shell { } impl Common { + fn selection_sources_inner(seat: &Seat) -> &RefCell { + seat.user_data() + .insert_if_missing(|| RefCell::new(SelectionSources::default())); + seat.user_data().get::>().unwrap() + } + + pub fn selection_sources(seat: &Seat) -> SelectionSources { + *Self::selection_sources_inner(seat).borrow() + } + + pub fn update_selection_sources( + seat: &Seat, + f: impl FnOnce(RefMut<'_, SelectionSources>), + ) { + let sources = Self::selection_sources_inner(seat).borrow_mut(); + f(sources) + } + pub fn set_focus( state: &mut State, target: Option<&KeyboardFocusTarget>, diff --git a/src/state.rs b/src/state.rs index 3597e341..6ffe03e5 100644 --- a/src/state.rs +++ b/src/state.rs @@ -10,6 +10,7 @@ use crate::{ shell::{layout::floating::SeatMoveGrabState, Shell}, utils::prelude::*, wayland::protocols::{ + data_control, drm::WlDrmState, output_configuration::OutputConfigurationState, screencopy::{BufferParams, ScreencopyState, Session as ScreencopySession}, @@ -62,6 +63,7 @@ use smithay::{ shm::ShmState, viewporter::ViewporterState, }, + xwayland::xwm::XwmId, }; use tracing::error; @@ -117,6 +119,7 @@ pub struct Common { pub output_configuration_state: OutputConfigurationState, pub presentation_state: PresentationState, pub primary_selection_state: PrimarySelectionState, + pub data_control_state: Option>, pub screencopy_state: ScreencopyState, pub seat_state: SeatState, pub shm_state: ShmState, @@ -129,6 +132,26 @@ pub struct Common { pub xwayland_state: Option, } +#[derive(Clone, Copy, Default)] +pub enum SelectionSource { + #[default] + Native, + DataControl, + XWayland(XwmId), +} + +#[derive(Clone, Copy, Default)] +pub struct SelectionSources { + pub clipboard: SelectionSource, + pub primary_selection: SelectionSource, +} + +impl SelectionSources { + pub fn tuple(self) -> (SelectionSource, SelectionSource) { + (self.clipboard, self.primary_selection) + } +} + pub enum BackendData { X11(X11State), Winit(WinitState), @@ -261,6 +284,11 @@ impl State { let kde_decoration_state = KdeDecorationState::new::(&dh, Mode::Client); let xdg_decoration_state = XdgDecorationState::new::(&dh); + let data_control_state = config + .static_conf + .data_control_enabled + .then(|| data_control::State::::new(dh)); + let shell = Shell::new(&config, dh); State { @@ -299,6 +327,7 @@ impl State { output_configuration_state, presentation_state, primary_selection_state, + data_control_state, viewporter_state, wl_drm_state, kde_decoration_state, diff --git a/src/wayland/handlers/data_control.rs b/src/wayland/handlers/data_control.rs new file mode 100644 index 00000000..963753b0 --- /dev/null +++ b/src/wayland/handlers/data_control.rs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::{Common, SelectionSource, State}; +use crate::wayland::protocols::data_control::{self, SelectionType}; +use smithay::wayland::data_device::{ + clear_data_device_selection, request_data_device_client_selection, set_data_device_selection, +}; +use smithay::wayland::primary_selection::{ + clear_primary_selection, request_primary_client_selection, set_primary_selection, +}; +use smithay::xwayland::xwm; +use tracing::{error, warn}; + +use std::os::unix::io::OwnedFd; + +impl data_control::Handler for State { + fn state(&mut self) -> &mut data_control::State { + self.common.data_control_state.as_mut().unwrap() + } + + fn new_selection(&mut self, ty: SelectionType, mime_types: Option>) { + if let Some(state) = self.common.xwayland_state.as_mut() { + if let Some(xwm) = state.xwm.as_mut() { + if let Err(err) = xwm.new_selection(ty.into(), mime_types.clone()) { + warn!(?err, "Failed to set Xwayland DataControl selection"); + } + } + } + + let dh = &self.common.display_handle; + let seat = self.common.last_active_seat(); + match (ty, mime_types) { + (SelectionType::Clipboard, Some(mt)) => set_data_device_selection(dh, seat, mt, ()), + (SelectionType::Clipboard, None) => clear_data_device_selection(dh, seat), + (SelectionType::Primary, Some(mt)) => set_primary_selection(dh, seat, mt, ()), + (SelectionType::Primary, None) => clear_primary_selection(dh, seat), + } + + Common::update_selection_sources(&seat, |mut sources| match ty { + SelectionType::Clipboard => sources.clipboard = SelectionSource::DataControl, + SelectionType::Primary => sources.primary_selection = SelectionSource::DataControl, + }); + } + + fn send_selection(&mut self, ty: SelectionType, mime_type: String, fd: OwnedFd) { + let seat = self.common.last_active_seat(); + match (ty, Common::selection_sources(seat).tuple()) { + (SelectionType::Clipboard, (SelectionSource::Native, _)) => { + if let Err(err) = request_data_device_client_selection(seat, mime_type, fd) { + error!( + ?err, + "Failed to request current wayland clipboard for DataControl.", + ); + } + } + (SelectionType::Primary, (_, SelectionSource::Native)) => { + if let Err(err) = request_primary_client_selection(seat, mime_type, fd) { + error!( + ?err, + "Failed to request current wayland primary selection for DataControl.", + ); + } + } + (SelectionType::Clipboard, (SelectionSource::XWayland(_), _)) + | (SelectionType::Primary, (_, SelectionSource::XWayland(_))) => { + if let Some(xwm) = self + .common + .xwayland_state + .as_mut() + .and_then(|xstate| xstate.xwm.as_mut()) + { + if let Err(err) = xwm.send_selection( + ty.into(), + mime_type, + fd, + self.common.event_loop_handle.clone(), + ) { + warn!( + ?err, + "Failed to send primary selection (X11 -> DataControl)." + ); + } + } + } + (SelectionType::Clipboard, (SelectionSource::DataControl, _)) + | (SelectionType::Primary, (_, SelectionSource::DataControl)) => {} + } + } +} + +impl From for xwm::SelectionType { + fn from(ty: SelectionType) -> Self { + match ty { + SelectionType::Clipboard => xwm::SelectionType::Clipboard, + SelectionType::Primary => xwm::SelectionType::Clipboard, + } + } +} + +impl From for SelectionType { + fn from(ty: xwm::SelectionType) -> Self { + match ty { + xwm::SelectionType::Clipboard => SelectionType::Clipboard, + xwm::SelectionType::Primary => SelectionType::Clipboard, + } + } +} + +data_control::delegate!(State); diff --git a/src/wayland/handlers/data_device.rs b/src/wayland/handlers/data_device.rs index 91519f02..5d5d59a8 100644 --- a/src/wayland/handlers/data_device.rs +++ b/src/wayland/handlers/data_device.rs @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only -use crate::state::State; +use crate::{ + state::{Common, SelectionSource, State}, + wayland::protocols::data_control, +}; use smithay::{ delegate_data_device, input::Seat, @@ -10,7 +13,7 @@ use smithay::{ with_source_metadata, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler, }, - xwayland::xwm::{SelectionType, XwmId}, + xwayland::xwm::SelectionType, }; use std::{cell::RefCell, os::unix::io::OwnedFd}; use tracing::warn; @@ -51,22 +54,23 @@ impl ClientDndGrabHandler for State { } impl ServerDndGrabHandler for State {} impl DataDeviceHandler for State { - type SelectionUserData = XwmId; + type SelectionUserData = (); fn data_device_state(&self) -> &DataDeviceState { &self.common.data_device_state } fn new_selection(&mut self, source: Option) { + let Ok(mime_types) = source + .map(|s| with_source_metadata(&s, |metadata| metadata.mime_types.clone())) + .transpose() else { return }; + if let Some(state) = self.common.xwayland_state.as_mut() { if let Some(xwm) = state.xwm.as_mut() { - if let Some(source) = &source { - if let Ok(Err(err)) = with_source_metadata(source, |metadata| { - xwm.new_selection( - SelectionType::Clipboard, - Some(metadata.mime_types.clone()), - ) - }) { + if let Some(mime_types) = &mime_types { + if let Err(err) = + xwm.new_selection(SelectionType::Clipboard, Some(mime_types.clone())) + { warn!(?err, "Failed to set Xwayland clipboard selection."); } } else if let Err(err) = xwm.new_selection(SelectionType::Clipboard, None) { @@ -74,6 +78,22 @@ impl DataDeviceHandler for State { } } } + + let seat = self.common.last_active_seat().clone(); + if self.common.data_control_state.is_some() { + let dh = self.common.display_handle.clone(); + data_control::set_selection( + self, + &seat, + &dh, + data_control::SelectionType::Clipboard, + mime_types, + ); + } + + Common::update_selection_sources(&seat, |mut sources| { + sources.clipboard = SelectionSource::Native + }); } fn send_selection( @@ -82,20 +102,40 @@ impl DataDeviceHandler for State { fd: OwnedFd, _user_data: &Self::SelectionUserData, ) { - if let Some(xwm) = self - .common - .xwayland_state - .as_mut() - .and_then(|xstate| xstate.xwm.as_mut()) - { - if let Err(err) = xwm.send_selection( - SelectionType::Clipboard, - mime_type, - fd, - self.common.event_loop_handle.clone(), - ) { - warn!(?err, "Failed to send clipboard (X11 -> Wayland)."); + let seat = self.common.last_active_seat(); + match Common::selection_sources(seat).clipboard { + SelectionSource::XWayland(_) => { + if let Some(xwm) = self + .common + .xwayland_state + .as_mut() + .and_then(|xstate| xstate.xwm.as_mut()) + { + if let Err(err) = xwm.send_selection( + SelectionType::Clipboard, + mime_type, + fd, + self.common.event_loop_handle.clone(), + ) { + warn!(?err, "Failed to send clipboard (X11 -> Wayland)."); + } + } + } + SelectionSource::DataControl => { + if self.common.data_control_state.is_some() { + let seat = seat.clone(); + if let Err(err) = data_control::request_selection( + self, + &seat, + data_control::SelectionType::Clipboard, + mime_type, + fd, + ) { + warn!(?err, "Failed to send clipboard (X11 -> DataControl)."); + }; + } } + SelectionSource::Native => {} } } } diff --git a/src/wayland/handlers/mod.rs b/src/wayland/handlers/mod.rs index 56e4a9be..2055faf1 100644 --- a/src/wayland/handlers/mod.rs +++ b/src/wayland/handlers/mod.rs @@ -2,6 +2,7 @@ pub mod buffer; pub mod compositor; +pub mod data_control; pub mod data_device; pub mod decoration; pub mod dmabuf; diff --git a/src/wayland/handlers/primary_selection.rs b/src/wayland/handlers/primary_selection.rs index 51a7d23e..26771cd6 100644 --- a/src/wayland/handlers/primary_selection.rs +++ b/src/wayland/handlers/primary_selection.rs @@ -1,35 +1,63 @@ // SPDX-License-Identifier: GPL-3.0-only -use crate::state::State; +use crate::{ + state::{Common, SelectionSource, State}, + wayland::protocols::data_control, +}; use smithay::{ delegate_primary_selection, - wayland::primary_selection::{PrimarySelectionHandler, PrimarySelectionState, with_source_metadata}, xwayland::xwm::{XwmId, SelectionType}, reexports::wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, + wayland::primary_selection::{ + with_source_metadata, PrimarySelectionHandler, PrimarySelectionState, + }, + xwayland::xwm::SelectionType, }; +use smithay:: + reexports::wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1; use tracing::warn; use std::os::unix::io::OwnedFd; impl PrimarySelectionHandler for State { - type SelectionUserData = XwmId; + type SelectionUserData = (); fn primary_selection_state(&self) -> &PrimarySelectionState { &self.common.primary_selection_state } fn new_selection(&mut self, source: Option) { + let Ok(mime_types) = source + .map(|s| with_source_metadata(&s, |metadata| metadata.mime_types.clone())) + .transpose() else { return }; + if let Some(state) = self.common.xwayland_state.as_mut() { if let Some(xwm) = state.xwm.as_mut() { - if let Some(source) = &source { - if let Ok(Err(err)) = with_source_metadata(source, |metadata| { - xwm.new_selection(SelectionType::Primary, Some(metadata.mime_types.clone())) - }) { - warn!(?err, "Failed to set Xwayland primary selection"); + if let Some(mime_types) = &mime_types { + if let Err(err) = + xwm.new_selection(SelectionType::Primary, Some(mime_types.clone())) + { + warn!(?err, "Failed to set Xwayland primary selection."); } } else if let Err(err) = xwm.new_selection(SelectionType::Primary, None) { - warn!(?err, "Failed to clear Xwayland primary selection"); + warn!(?err, "Failed to clear Xwayland primary selection."); } } } + + let seat = self.common.last_active_seat().clone(); + if self.common.data_control_state.is_some() { + let dh = self.common.display_handle.clone(); + data_control::set_selection( + self, + &seat, + &dh, + data_control::SelectionType::Primary, + mime_types, + ); + } + + Common::update_selection_sources(&seat, |mut sources| { + sources.primary_selection = SelectionSource::Native + }); } fn send_selection( @@ -38,20 +66,43 @@ impl PrimarySelectionHandler for State { fd: OwnedFd, _user_data: &Self::SelectionUserData, ) { - if let Some(xwm) = self - .common - .xwayland_state - .as_mut() - .and_then(|xstate| xstate.xwm.as_mut()) - { - if let Err(err) = xwm.send_selection( - SelectionType::Primary, - mime_type, - fd, - self.common.event_loop_handle.clone(), - ) { - warn!(?err, "Failed to send primary selection (X11 -> Wayland)."); + let seat = self.common.last_active_seat(); + match Common::selection_sources(seat).primary_selection { + SelectionSource::XWayland(_) => { + if let Some(xwm) = self + .common + .xwayland_state + .as_mut() + .and_then(|xstate| xstate.xwm.as_mut()) + { + if let Err(err) = xwm.send_selection( + SelectionType::Primary, + mime_type, + fd, + self.common.event_loop_handle.clone(), + ) { + warn!(?err, "Failed to send primary selection (X11 -> Wayland)."); + } + } + } + SelectionSource::DataControl => { + if self.common.data_control_state.is_some() { + let seat = seat.clone(); + if let Err(err) = data_control::request_selection( + self, + &seat, + data_control::SelectionType::Primary, + mime_type, + fd, + ) { + warn!( + ?err, + "Failed to send primary selection (DataControl -> PrimarySelection)." + ); + }; + } } + SelectionSource::Native => {} } } } diff --git a/src/wayland/protocols/data_control/device.rs b/src/wayland/protocols/data_control/device.rs new file mode 100644 index 00000000..bd9dc838 --- /dev/null +++ b/src/wayland/protocols/data_control/device.rs @@ -0,0 +1,133 @@ +pub use super::server::zwlr_data_control_device_v1::{ + Error, Request, ZwlrDataControlDeviceV1 as Addr, +}; + +use super::{offer, source, ClientSelection, Handler, Selection, SelectionType, State}; +use smithay::{ + input::{Seat, SeatHandler}, + reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, Resource}, +}; +use tracing::{debug, error}; +use wayland_backend::{server::ClientId, server::ObjectId}; + +pub struct Device { + addr: Addr, + seat: Seat, +} + +impl Device { + pub fn new(addr: Addr, seat: Seat) -> Self { + Self { addr, seat } + } + + pub fn addr(&self) -> &Addr { + &self.addr + } + + pub fn seat(&self) -> &Seat { + &self.seat + } +} + +pub fn data_offer(addr: &Addr, offer_addr: &offer::Addr) { + debug!(id = %addr.id(), offer_id = %offer_addr.id(), "data_offer"); + addr.data_offer(offer_addr); +} + +pub fn selection(addr: &Addr, offer_addr: Option<&offer::Addr>) { + debug!(offer_id = ?offer_addr.map(|addr| addr.id()), "selection"); + addr.selection(offer_addr); +} + +fn finished(addr: &Addr) { + debug!(id = %addr.id(), "finished"); + addr.finished(); +} + +pub fn primary_selection(addr: &Addr, offer_addr: Option<&offer::Addr>) { + debug!(offer_id = ?offer_addr.map(|addr| addr.id()), "primary_selection"); + addr.primary_selection(offer_addr); +} + +fn error_used_source(addr: &Addr) { + const MSG: &str = + "Source given to set_selection or set_primary_selection was already used before"; + + error!(id = %addr.id(), MSG, "user_source"); + addr.post_error(Error::UsedSource, MSG); +} + +impl Dispatch for State +where + D: SeatHandler + Handler + Dispatch + 'static, +{ + fn request( + handler: &mut D, + _client: &Client, + addr: &Addr, + request: Request, + _data: &(), + dh: &DisplayHandle, + _di: &mut DataInit<'_, D>, + ) { + match request { + Request::SetSelection { source } => { + debug!(source_id = ?source.as_ref().map(|addr| addr.id()), "set_selection"); + set_selection((handler, dh), addr, source, SelectionType::Clipboard); + } + Request::SetPrimarySelection { source } => { + debug!(source_id = ?source.as_ref().map(|addr| addr.id()), "set_primary_selection"); + set_selection((handler, dh), addr, source, SelectionType::Primary); + } + Request::Destroy => debug!(id = %addr.id(), "destroy"), + + _ => unreachable!(), + } + } + + fn destroyed(handler: &mut D, _: ClientId, id: ObjectId, _: &()) { + let Some(device) = handler.state().devices.remove(&id) else { return }; + finished(&device.addr); + } +} + +fn set_selection( + (handler, dh): (&mut D, &DisplayHandle), + device_addr: &Addr, + source_addr: Option, + ty: SelectionType, +) where + D: Handler + Dispatch + 'static, +{ + let (mime_types, selection) = if let Some(source_addr) = source_addr { + let source = handler.state().sources.get_mut(&source_addr.id()); + let Some(mime_types) = source.and_then(|s| s.consume(device_addr, ty, dh)) else { + return error_used_source(device_addr); + }; + + let selection = ClientSelection { + mime_types, + source_addr, + }; + + (Some(selection.mime_types.clone()), Some(selection)) + } else { + (None, None) + }; + + handler.new_selection(ty, mime_types); + + let state = handler.state(); + let Some(seat) = state.devices.get(&device_addr.id()).map(Device::seat) else { + return error!(id = %device_addr.id(), "Missing device!") + }; + + super::set_selection_and_broadcast( + (seat, dh), + &state.devices, + &mut state.offers, + &mut state.seat_selections, + Selection::Client(selection), + ty, + ) +} diff --git a/src/wayland/protocols/data_control/manager.rs b/src/wayland/protocols/data_control/manager.rs new file mode 100644 index 00000000..d678cf61 --- /dev/null +++ b/src/wayland/protocols/data_control/manager.rs @@ -0,0 +1,95 @@ +pub use super::server::zwlr_data_control_manager_v1::{Request, ZwlrDataControlManagerV1 as Addr}; + +use smithay::{ + input::{Seat, SeatHandler}, + reexports::wayland_server::{self, Dispatch, DisplayHandle, GlobalDispatch, Resource}, +}; + +use tracing::{debug, error}; + +use wayland_backend::server::GlobalId; + +use super::{device, offer, source, Device, Handler, SelectionType, Source, State}; + +pub struct Manager { + _id: GlobalId, +} + +pub fn new(dh: &DisplayHandle) -> Manager +where + D: GlobalDispatch + 'static, +{ + Manager { + _id: dh.create_global::(1, ()), + } +} + +impl GlobalDispatch for State +where + D: SeatHandler + Dispatch, +{ + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + addr: wayland_server::New, + _global_data: &(), + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init(addr, ()); + } +} + +impl Dispatch for State +where + D: SeatHandler + + Handler + + Dispatch + + Dispatch + + Dispatch + + 'static, +{ + fn request( + handler: &mut D, + client: &wayland_server::Client, + _addr: &Addr, + request: Request, + _data: &(), + dh: &DisplayHandle, + di: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + Request::CreateDataSource { id } => { + debug!("create_data_source"); + + let source = Source::new(di.init(id, ())); + let _ = handler.state().sources.insert(source.addr().id(), source); + } + Request::GetDataDevice { id, seat } => { + debug!(?seat, "get_data_device"); + + let Some(seat) = Seat::::from_resource(&seat) else { + error!(?seat, "Failed to retrieve `Seat` from `WlSeat`"); + return; + }; + + let state = handler.state(); + + let device = Device::new(di.init(id, ()), seat); + let device = state.devices.entry(device.addr().id()).or_insert(device); + + for ty in [SelectionType::Clipboard, SelectionType::Primary] { + super::offer_selection::( + (device.seat(), dh, client), + &mut state.offers, + &mut state.seat_selections, + device.addr(), + ty, + ) + } + } + Request::Destroy => debug!("destroy"), + _ => unreachable!(), + } + } +} diff --git a/src/wayland/protocols/data_control/mod.rs b/src/wayland/protocols/data_control/mod.rs new file mode 100644 index 00000000..e85b48f3 --- /dev/null +++ b/src/wayland/protocols/data_control/mod.rs @@ -0,0 +1,274 @@ +use std::cell::{RefCell, RefMut}; +use std::collections::HashMap; +use std::os::unix::io::OwnedFd; + +use smithay::input::{Seat, SeatHandler}; +use smithay::reexports::wayland_protocols_wlr::data_control::v1::server; +use smithay::reexports::wayland_server::{ + backend::ObjectId, Client, Dispatch, DisplayHandle, GlobalDispatch, Resource, +}; +use tracing::error; + +use self::{device::Device, manager::Manager, offer::Offer, source::Source}; + +mod device; +mod manager; +mod offer; +mod source; + +/// Events that are generated by interactions of the clients with the data control protocol. +pub trait Handler: SeatHandler + Sized { + fn state(&mut self) -> &mut State; + + /// A client has set the clipboard selection. + fn new_selection(&mut self, _ty: SelectionType, _mime_types: Option>) {} + + /// A client requested to read the server-set clipboard selection. + /// + /// * `mime_type` - the requested mime type + /// * `fd` - the fd to write into + fn send_selection(&mut self, _ty: SelectionType, _mime_type: String, _fd: OwnedFd) {} +} + +pub struct State { + _manager: Manager, + + devices: HashMap>, + sources: HashMap, + offers: HashMap>, + + seat_selections: SeatSelections, +} + +struct SeatSelections; + +impl SeatSelections { + fn get_selections_mut<'a, 'b: 'a, D: SeatHandler + 'static>( + &'a mut self, + seat: &'b Seat, + ) -> RefMut<'a, Selections> { + seat.user_data().insert_if_missing(|| { + RefCell::new(self::Selections { + clipboard: Selection::Client(None), + primary_selection: Selection::Compositor(None), + }) + }); + + seat.user_data() + .get::>() + .unwrap() + .borrow_mut() + } +} + +pub struct Selections { + clipboard: Selection, + primary_selection: Selection, +} + +impl Selections { + fn get_mut(&mut self, ty: SelectionType) -> &mut Selection { + match ty { + SelectionType::Clipboard => &mut self.clipboard, + SelectionType::Primary => &mut self.primary_selection, + } + } +} + +impl State { + pub fn new(dh: &DisplayHandle) -> Self + where + D: GlobalDispatch + 'static, + { + Self { + _manager: manager::new::(dh), + devices: HashMap::new(), + sources: HashMap::new(), + offers: HashMap::new(), + seat_selections: SeatSelections, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum Selection { + Client(Option), + Compositor(Option), +} + +impl Drop for Selection { + fn drop(&mut self) { + if let Self::Client(Some(s)) = self { + source::cancelled(&s.source_addr); + } + } +} + +impl Selection { + fn mime_types(&self) -> Option> { + match self { + Selection::Client(Some(s)) => Some(s.mime_types.clone()), + Selection::Compositor(Some(s)) => Some(s.mime_types.clone()), + _ => None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ClientSelection { + mime_types: Vec, + source_addr: source::Addr, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct CompositorSelection { + mime_types: Vec, +} + +#[derive(Clone, Copy, Debug)] +pub enum SelectionType { + Clipboard, + Primary, +} + +fn set_selection_and_broadcast( + (seat, dh): (&Seat, &DisplayHandle), + devices: &HashMap>, + offers: &mut HashMap>, + seat_selections: &mut SeatSelections, + new_selection: Selection, + ty: SelectionType, +) where + D: Handler + Dispatch + 'static, +{ + match seat_selections.get_selections_mut(seat).get_mut(ty) { + s if s == &new_selection => return, + s => *s = new_selection, + } + + for dev in devices.values() { + if dev.seat() != seat { + continue; + } + + if let Ok(client) = dh.get_client(dev.addr().id()) { + offer_selection::((seat, dh, &client), offers, seat_selections, dev.addr(), ty); + }; + } +} + +fn offer_selection( + (seat, dh, client): (&Seat, &DisplayHandle, &Client), + offers: &mut HashMap>, + seat_selections: &mut SeatSelections, + dev_addr: &device::Addr, + ty: SelectionType, +) where + D: Handler + Dispatch + 'static, +{ + let mut sels = seat_selections.get_selections_mut(seat); + let offer_addr = sels.get_mut(ty).mime_types().and_then(|mime_types| { + let offer = client + .create_resource::<_, _, D>(dh, dev_addr.version(), ()) + .map(|addr| Offer::new(addr, ty, seat.clone())) + .map(|offer| offers.entry(offer.addr().id()).or_insert(offer)) + .map_err(|e| error!("Failed to create Offer: {e:?}")) + .ok()?; + + device::data_offer(dev_addr, offer.addr()); + for mime_type in mime_types { + offer::offer(offer.addr(), mime_type); + } + + Some(offer.addr()) + }); + + match ty { + SelectionType::Clipboard => device::selection(dev_addr, offer_addr), + SelectionType::Primary => device::primary_selection(dev_addr, offer_addr), + } +} + +/// Set a compositor-provided primary selection +/// +/// You need to provide the available mime types for this selection. +/// +/// Whenever a client requests to read the selection, your callback will +/// receive a [`Handler::send_selection`] event. +pub fn set_selection( + handler: &mut D, + seat: &Seat, + dh: &DisplayHandle, + ty: SelectionType, + mime_types: Option>, +) where + D: Handler + Dispatch + 'static, +{ + let state = handler.state(); + set_selection_and_broadcast( + (seat, dh), + &mut state.devices, + &mut state.offers, + &mut state.seat_selections, + Selection::Compositor(mime_types.map(|mt| CompositorSelection { mime_types: mt })), + ty, + ) +} + +/// Request the current data_control selection of the given type +/// to be written to the provided file descriptor in the given mime type. +pub fn request_selection( + handler: &mut D, + seat: &Seat, + ty: SelectionType, + mime_type: String, + fd: OwnedFd, +) -> Result<(), SelectionRequestError> { + let state = handler.state(); + match state.seat_selections.get_selections_mut(seat).get_mut(ty) { + Selection::Client(Some(s)) if s.mime_types.contains(&mime_type) => { + Ok(source::send(&s.source_addr, mime_type, fd)) + } + Selection::Client(Some(_)) => Err(SelectionRequestError::InvalidMimetype), + Selection::Client(None) => Err(SelectionRequestError::NoSelection), + Selection::Compositor(_) => Err(SelectionRequestError::ServerSideSelection), + } +} + +/// Errors happening when requesting selection contents +#[derive(Debug, thiserror::Error)] +pub enum SelectionRequestError { + /// Requested mime type is not available + #[error("Requested mime type is not available")] + InvalidMimetype, + /// Requesting server side selection contents is not supported + #[error("Current selection is server-side")] + ServerSideSelection, + /// There is no active selection + #[error("No active selection to query")] + NoSelection, +} + +#[allow(missing_docs)] +#[macro_export] +macro_rules! delegate { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1: () + ] => $crate::wayland::protocols::data_control::State); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1: () + ] => $crate::wayland::protocols::data_control::State); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: () + ] => $crate::wayland::protocols::data_control::State); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: () + ] => $crate::wayland::protocols::data_control::State); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: () + ] => $crate::wayland::protocols::data_control::State); + }; +} +pub(crate) use delegate; diff --git a/src/wayland/protocols/data_control/offer.rs b/src/wayland/protocols/data_control/offer.rs new file mode 100644 index 00000000..69a289a5 --- /dev/null +++ b/src/wayland/protocols/data_control/offer.rs @@ -0,0 +1,76 @@ +pub use super::server::zwlr_data_control_offer_v1::{Request, ZwlrDataControlOfferV1 as Addr}; + +use super::{source, Handler, Selection, SelectionType, State}; +use smithay::{ + input::{Seat, SeatHandler}, + reexports::wayland_server::{self, Dispatch, DisplayHandle, Resource}, +}; +use tracing::{debug, error}; +use wayland_backend::server::{ClientId, ObjectId}; + +pub struct Offer { + addr: Addr, + selection_type: SelectionType, + seat: Seat, +} + +impl Offer { + pub fn new(addr: Addr, selection_type: SelectionType, seat: Seat) -> Self { + Self { + addr, + selection_type, + seat, + } + } + + pub fn addr(&self) -> &Addr { + &self.addr + } +} + +pub fn offer(addr: &Addr, mime_type: String) { + debug!(id = %addr.id(), mime_type, "offer"); + addr.offer(mime_type); +} + +impl Dispatch for State { + fn request( + handler: &mut D, + _client: &wayland_server::Client, + addr: &Addr, + request: Request, + _data: &(), + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + Request::Receive { mime_type, fd } => { + debug!(id = %addr.id(), mime_type, ?fd, "receive"); + + let selection_type = { + let state = handler.state(); + + let Some(offer) = state.offers.get(&addr.id()) else { + return error!(id = %addr.id(), "Missing Offer!"); + }; + + let mut selections = state.seat_selections.get_selections_mut(&offer.seat); + if let Selection::Client(Some(s)) = selections.get_mut(offer.selection_type) { + return source::send(&s.source_addr, mime_type, fd); + }; + + offer.selection_type + }; + + handler.send_selection(selection_type, mime_type, fd); + } + Request::Destroy => debug!(id = %addr.id(), "destroy"), + _ => unreachable!(), + } + } + + fn destroyed(handler: &mut D, _client: ClientId, id: ObjectId, _data: &()) { + debug!(%id, "destroyed"); + let _ = handler.state().offers.remove(&id); + } +} diff --git a/src/wayland/protocols/data_control/source.rs b/src/wayland/protocols/data_control/source.rs new file mode 100644 index 00000000..c51feae1 --- /dev/null +++ b/src/wayland/protocols/data_control/source.rs @@ -0,0 +1,140 @@ +pub use super::server::zwlr_data_control_source_v1::{ + Error, Request, ZwlrDataControlSourceV1 as Addr, +}; + +use smithay::reexports::wayland_server::{ + self, + backend::{ClientId, ObjectId}, + Dispatch, DisplayHandle, Resource, +}; +use std::os::fd::AsRawFd; +use tracing::{debug, error}; +use wayland_backend::io_lifetimes::OwnedFd; + +use super::{ + device::{self, Device}, + offer, Handler, Selection, SelectionType, +}; + +pub struct Source { + addr: Addr, + state: State, +} + +impl Source { + pub fn new(addr: Addr) -> Self { + Self { + addr, + state: State::Offering { + mime_types: Vec::new(), + }, + } + } + + pub fn addr(&self) -> &Addr { + &self.addr + } + + pub fn consume( + &mut self, + by: &device::Addr, + selection_type: SelectionType, + dh: &DisplayHandle, + ) -> Option> { + match &mut self.state { + State::Offering { mime_types } => { + let mime_types = std::mem::take(mime_types); + self.state = State::Consumed { + by: by.clone(), + selection_type, + display_handle: dh.clone(), + }; + Some(mime_types) + } + State::Consumed { .. } => None, + } + } +} + +enum State { + Offering { + mime_types: Vec, + }, + Consumed { + by: device::Addr, + selection_type: SelectionType, + display_handle: DisplayHandle, + }, +} + +pub fn send(addr: &Addr, mime_type: String, fd: OwnedFd) { + debug!(id = %addr.id(), mime_type, ?fd, "send"); + addr.send(mime_type, fd.as_raw_fd()); +} + +pub fn cancelled(addr: &Addr) { + debug!(id = %addr.id(), "cancelled"); + addr.cancelled(); +} + +fn error_invalid_offer(addr: &Addr) { + error!(id = %addr.id(), "invalid_offer"); + addr.post_error( + Error::InvalidOffer, + "offer sent after wlr_data_control_device.set_selection", + ); +} + +impl Dispatch for super::State +where + D: Handler + Dispatch + 'static, +{ + fn request( + handler: &mut D, + _client: &wayland_server::Client, + addr: &Addr, + request: Request, + _data: &(), + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + Request::Offer { mime_type } => { + debug!(id = %addr.id(), mime_type, "offer"); + + match handler.state().sources.get_mut(&addr.id()) { + Some(Source { + state: State::Offering { mime_types }, + .. + }) => { + mime_types.push(mime_type); + } + Some(_) => error_invalid_offer(addr), + None => error!(id = %addr.id(), "Missing source"), + }; + } + Request::Destroy => debug!(id = %addr.id(), "destroy"), + _ => unreachable!(), + } + } + + fn destroyed(handler: &mut D, _client: ClientId, id: ObjectId, _: &()) { + // Set empty selection if this Source is currently being used. + + let Some(source) = handler.state().sources.remove(&id) else { return }; + let State::Consumed { by: device_addr, selection_type, display_handle: dh } = source.state else { return }; + let Some(seat) = handler.state().devices.get(&device_addr.id()).map(Device::seat).cloned() else { return }; + + match handler + .state() + .seat_selections + .get_selections_mut(&seat) + .get_mut(selection_type) + { + Selection::Client(Some(s)) if s.source_addr == source.addr => {} + _ => return, + }; + + super::set_selection(handler, &seat, &dh, selection_type, None); + } +} diff --git a/src/wayland/protocols/mod.rs b/src/wayland/protocols/mod.rs index e84f5ad1..398d3838 100644 --- a/src/wayland/protocols/mod.rs +++ b/src/wayland/protocols/mod.rs @@ -2,6 +2,7 @@ pub mod drm; //pub mod export_dmabuf; +pub mod data_control; pub mod output_configuration; pub mod screencopy; pub mod toplevel_info; diff --git a/src/xwayland.rs b/src/xwayland.rs index 5627ba4f..962db9e9 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -3,9 +3,12 @@ use std::{ffi::OsString, os::unix::io::OwnedFd}; use crate::{ backend::render::cursor::Cursor, shell::{focus::target::KeyboardFocusTarget, CosmicSurface, Shell}, - state::{Data, State}, + state::{Data, SelectionSource, State}, utils::prelude::*, - wayland::{handlers::screencopy::PendingScreencopyBuffers, protocols::screencopy::SessionType}, + wayland::{ + handlers::screencopy::PendingScreencopyBuffers, + protocols::{data_control, screencopy::SessionType}, + }, }; use smithay::{ backend::drm::DrmNode, @@ -14,12 +17,11 @@ use smithay::{ utils::{Logical, Point, Rectangle, Size}, wayland::{ data_device::{ - clear_data_device_selection, current_data_device_selection_userdata, - request_data_device_client_selection, set_data_device_selection, + clear_data_device_selection, request_data_device_client_selection, + set_data_device_selection, }, primary_selection::{ - clear_primary_selection, current_primary_selection_userdata, - request_primary_client_selection, set_primary_selection, + clear_primary_selection, request_primary_client_selection, set_primary_selection, }, }, xwayland::{ @@ -457,8 +459,8 @@ impl XwmHandler for Data { fd: OwnedFd, ) { let seat = self.state.common.last_active_seat(); - match selection { - SelectionType::Clipboard => { + match (selection, Common::selection_sources(seat).tuple()) { + (SelectionType::Clipboard, (SelectionSource::Native, _)) => { if let Err(err) = request_data_device_client_selection(seat, mime_type, fd) { error!( ?err, @@ -466,7 +468,7 @@ impl XwmHandler for Data { ); } } - SelectionType::Primary => { + (SelectionType::Primary, (_, SelectionSource::Native)) => { if let Err(err) = request_primary_client_selection(seat, mime_type, fd) { error!( ?err, @@ -474,6 +476,27 @@ impl XwmHandler for Data { ); } } + (SelectionType::Clipboard, (SelectionSource::DataControl, _)) + | (SelectionType::Primary, (_, SelectionSource::DataControl)) => { + if self.state.common.data_control_state.is_some() { + let seat = seat.clone(); + if let Err(err) = data_control::request_selection( + &mut self.state, + &seat, + selection.into(), + mime_type, + fd, + ) { + error!( + ?err, + ?selection, + "Failed to request current data control selection for Xwayland.", + ); + } + } + } + (SelectionType::Clipboard, (SelectionSource::XWayland(_), _)) + | (SelectionType::Primary, (_, SelectionSource::XWayland(_))) => {} } } @@ -485,34 +508,56 @@ impl XwmHandler for Data { trace!(?selection, ?mime_types, "Got Selection from Xwayland",); if self.state.common.is_x_focused(xwm) { - let seat = self.state.common.last_active_seat(); - match selection { - SelectionType::Clipboard => set_data_device_selection( - &self.state.common.display_handle, + let seat = self.state.common.last_active_seat().clone(); + let dh = self.state.common.display_handle.clone(); + + if self.state.common.data_control_state.is_some() { + data_control::set_selection( + &mut self.state, &seat, - mime_types, - xwm, - ), + &dh, + selection.into(), + Some(mime_types.clone()), + ); + } + + match selection { + SelectionType::Clipboard => set_data_device_selection(&dh, &seat, mime_types, ()), + SelectionType::Primary => set_primary_selection(&dh, &seat, mime_types, ()), + } + + Common::update_selection_sources(&seat, |mut sources| match selection { + SelectionType::Clipboard => sources.clipboard = SelectionSource::XWayland(xwm), SelectionType::Primary => { - set_primary_selection(&self.state.common.display_handle, &seat, mime_types, xwm) + sources.primary_selection = SelectionSource::XWayland(xwm) } - } + }); } } fn cleared_selection(&mut self, xwm: XwmId, selection: SelectionType) { - for seat in self.state.common.seats() { + let dh = self.state.common.display_handle.clone(); + let seats = self.state.common.seats(); + let seats = seats + .filter(|seat| { + let source = match selection { + SelectionType::Clipboard => Common::selection_sources(&seat).clipboard, + SelectionType::Primary => Common::selection_sources(&seat).primary_selection, + }; + + matches!(source,SelectionSource::XWayland(id) if id == xwm) + }) + .cloned() + .collect::>(); + + for seat in seats { match selection { - SelectionType::Clipboard => { - if current_data_device_selection_userdata(seat).as_deref() == Some(&xwm) { - clear_data_device_selection(&self.state.common.display_handle, seat) - } - } - SelectionType::Primary => { - if current_primary_selection_userdata(seat).as_deref() == Some(&xwm) { - clear_primary_selection(&self.state.common.display_handle, seat) - } - } + SelectionType::Clipboard => clear_data_device_selection(&dh, &seat), + SelectionType::Primary => clear_primary_selection(&dh, &seat), + }; + + if self.state.common.data_control_state.is_some() { + data_control::set_selection(&mut self.state, &seat, &dh, selection.into(), None); } } }