diff --git a/src/lib.rs b/src/lib.rs index 82bd0ab..2c6ba60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ use crate::server::{NoConnection, PendingSurfaceState, ServerState}; use crate::xstate::{RealConnection, XState}; use log::{error, info}; use rustix::event::{poll, PollFd, PollFlags}; +use server::selection::{Clipboard, Primary}; use smithay_client_toolkit::data_device_manager::WritePipe; use std::io::{BufRead, BufReader, Read, Write}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; @@ -212,10 +213,14 @@ pub fn main(mut data: impl RunData) -> Option<()> { server_state.run(); display.flush_clients().unwrap(); - if let Some(sel) = server_state.new_selection() { + if let Some(sel) = server_state.new_selection::() { xstate.set_clipboard(sel); } + if let Some(sel) = server_state.new_selection::() { + xstate.set_primary_selection(sel); + } + if let Some(scale) = server_state.new_global_scale() { xstate.update_global_scale(scale); } diff --git a/src/server/clientside.rs b/src/server/clientside.rs index 0f910d0..f6aecc6 100644 --- a/src/server/clientside.rs +++ b/src/server/clientside.rs @@ -7,7 +7,12 @@ use smithay_client_toolkit::{ data_offer::{DataOfferHandler, SelectionOffer}, data_source::DataSourceHandler, }, - delegate_activation, delegate_data_device, + delegate_activation, delegate_data_device, delegate_primary_selection, + primary_selection::{ + device::{PrimarySelectionDeviceData, PrimarySelectionDeviceHandler}, + offer::PrimarySelectionOffer, + selection::PrimarySelectionSourceHandler, + }, }; use std::sync::{mpsc, Mutex, OnceLock}; use wayland_client::protocol::{ @@ -41,6 +46,11 @@ use wayland_protocols::{ zwp_locked_pointer_v1::ZwpLockedPointerV1, zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, }, + primary_selection::zv1::client::{ + zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1, + zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, + zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, + }, tablet::zv2::client::{ zwp_tablet_manager_v2::ZwpTabletManagerV2, zwp_tablet_pad_group_v2::{ZwpTabletPadGroupV2, EVT_RING_OPCODE, EVT_STRIP_OPCODE}, @@ -73,18 +83,33 @@ use wayland_server::protocol as server; use wl_drm::client::wl_drm::WlDrm; use xcb::x; +pub(super) struct SelectionEvents { + pub offer: Option, + pub requests: Vec<( + String, + smithay_client_toolkit::data_device_manager::WritePipe, + )>, + pub cancelled: bool, +} + +impl Default for SelectionEvents { + fn default() -> Self { + Self { + offer: None, + requests: Default::default(), + cancelled: false, + } + } +} + pub(super) struct MyWorld { pub world: World, pub global_list: GlobalList, pub new_globals: Vec, events: Vec<(Entity, ObjectEvent)>, queued_events: Vec>, - pub selection_offer: Option, - pub selection_requests: Vec<( - String, - smithay_client_toolkit::data_device_manager::WritePipe, - )>, - pub selection_cancelled: bool, + pub clipboard: SelectionEvents, + pub primary: SelectionEvents, pub pending_activations: Vec<(xcb::x::Window, String)>, } @@ -96,9 +121,8 @@ impl MyWorld { new_globals: Vec::new(), events: Vec::new(), queued_events: Vec::new(), - selection_offer: None, - selection_requests: Vec::new(), - selection_cancelled: false, + clipboard: Default::default(), + primary: Default::default(), pending_activations: Vec::new(), } } @@ -156,6 +180,7 @@ delegate_noop!(MyWorld: XdgActivationV1); delegate_noop!(MyWorld: ZxdgDecorationManagerV1); delegate_noop!(MyWorld: WpFractionalScaleManagerV1); delegate_noop!(MyWorld: ignore ZxdgToplevelDecorationV1); +delegate_noop!(MyWorld: ZwpPrimarySelectionDeviceManagerV1); impl Dispatch for MyWorld { fn event( @@ -387,7 +412,7 @@ impl DataDeviceHandler for MyWorld { data_device: &wayland_client::protocol::wl_data_device::WlDataDevice, ) { let data: &DataDeviceData = data_device.data().unwrap(); - self.selection_offer = data.selection_offer(); + self.clipboard.offer = data.selection_offer(); } fn drop_performed( @@ -437,7 +462,7 @@ impl DataSourceHandler for MyWorld { mime: String, fd: smithay_client_toolkit::data_device_manager::WritePipe, ) { - self.selection_requests.push((mime, fd)); + self.clipboard.requests.push((mime, fd)); } fn cancelled( @@ -446,7 +471,7 @@ impl DataSourceHandler for MyWorld { _: &wayland_client::QueueHandle, _: &wayland_client::protocol::wl_data_source::WlDataSource, ) { - self.selection_cancelled = true; + self.clipboard.cancelled = true; } fn action( @@ -538,3 +563,42 @@ impl ActivationHandler for MyWorld { self.pending_activations.push((data.window, token)); } } + +delegate_primary_selection!(MyWorld); + +impl PrimarySelectionDeviceHandler for MyWorld { + fn selection( + &mut self, + _: &Connection, + _: &QueueHandle, + primary_selection_device: &ZwpPrimarySelectionDeviceV1, + ) { + let Some(data) = primary_selection_device.data::() else { + return; + }; + + self.primary.offer = data.selection_offer(); + } +} + +impl PrimarySelectionSourceHandler for MyWorld { + fn send_request( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &ZwpPrimarySelectionSourceV1, + mime: String, + write_pipe: smithay_client_toolkit::data_device_manager::WritePipe, + ) { + self.primary.requests.push((mime, write_pipe)); + } + + fn cancelled( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &ZwpPrimarySelectionSourceV1, + ) { + self.primary.cancelled = true; + } +} diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index 31d66a8..f540259 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -1341,9 +1341,8 @@ impl GlobalDispatch for InnerServerState { .global_list .registry() .bind::(data.name, server.version(), &state.qh, entity); - if let Some(c) = &mut state.clipboard_data { - c.device = Some(c.manager.get_data_device(&state.qh, &client)); - } + + state.selection_states.seat_created(&state.qh, &client); state.world.spawn_at(entity, (server, client)); } } diff --git a/src/server/event.rs b/src/server/event.rs index 520d0b8..8ebd4be 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -32,6 +32,7 @@ use wayland_protocols::{ zwp_tablet_v2::ZwpTabletV2 as TabletServer, }, }, + viewporter::client::wp_viewport::WpViewport, }, xdg::{ shell::client::{xdg_popup, xdg_surface, xdg_toplevel}, diff --git a/src/server/mod.rs b/src/server/mod.rs index 6a0bf00..d818562 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,6 +1,7 @@ mod clientside; mod dispatch; mod event; +pub(crate) mod selection; #[cfg(test)] mod tests; @@ -12,16 +13,10 @@ use hecs::{Entity, World}; use log::{debug, warn}; use rustix::event::{poll, PollFd, PollFlags}; use smithay_client_toolkit::activation::ActivationState; -use smithay_client_toolkit::data_device_manager::{ - data_device::DataDevice, data_offer::SelectionOffer, data_source::CopyPasteSource, - DataDeviceManagerState, -}; use std::collections::{HashMap, HashSet}; -use std::io::Read; use std::ops::{Deref, DerefMut}; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::net::UnixStream; -use std::rc::{Rc, Weak}; use wayland_client::{ globals::{registry_queue_init, Global}, protocol as client, Connection, EventQueue, Proxy, QueueHandle, @@ -47,7 +42,7 @@ use wayland_protocols::{ zwp_tablet_pad_v2, zwp_tablet_seat_v2, zwp_tablet_tool_v2, zwp_tablet_v2, }, tablet::zv2::server::zwp_tablet_manager_v2::ZwpTabletManagerV2, - viewporter::client::{wp_viewport::WpViewport, wp_viewporter::WpViewporter}, + viewporter::client::wp_viewporter::WpViewporter, }, xdg::{ shell::client::{ @@ -454,7 +449,7 @@ pub struct InnerServerState { viewporter: WpViewporter, fractional_scale: Option, decoration_manager: Option, - clipboard_data: Option>, + selection_states: selection::SelectionStates, last_kb_serial: Option<(client::wl_seat::WlSeat, u32)>, activation_state: Option, global_output_offset: GlobalOutputOffset, @@ -494,17 +489,6 @@ impl ServerState> { .inspect_err(|e| warn!("Couldn't bind fractional scale manager: {e}. Fractional scaling will not work.")) .ok(); - let manager = DataDeviceManagerState::bind(&global_list, &qh) - .inspect_err(|e| { - warn!("Could not bind data device manager ({e:?}). Clipboard will not work.") - }) - .ok(); - let clipboard_data = manager.map(|manager| ClipboardData { - manager, - device: None, - source: None::>, - }); - let activation_state = ActivationState::bind(&global_list, &qh) .inspect_err(|e| { warn!("Could not bind xdg activation ({e:?}). Windows might not receive focus depending on compositor focus stealing policy.") @@ -518,7 +502,10 @@ impl ServerState> { }) .ok(); + let selection_states = selection::SelectionStates::new(&global_list, &qh); + dh.create_global::, XwaylandShellV1, _>(1, ()); + global_list .contents() .with_list(|globals| handle_globals::(&dh, globals)); @@ -539,7 +526,7 @@ impl ServerState> { xdg_wm_base, viewporter, fractional_scale, - clipboard_data, + selection_states, last_kb_serial: None, activation_state, global_output_offset: GlobalOutputOffset { @@ -691,7 +678,7 @@ impl ServerState { self.unfocus = false; } - self.handle_clipboard_events(); + self.handle_selection_events(); self.handle_activations(); self.queue .flush() @@ -1113,78 +1100,10 @@ impl InnerServerState { } } - pub(crate) fn set_copy_paste_source(&mut self, selection: &Rc) { - if let Some(d) = &mut self.clipboard_data { - let src = d - .manager - .create_copy_paste_source(&self.qh, selection.mime_types()); - let data = CopyPasteData::X11 { - inner: src, - data: Rc::downgrade(selection), - }; - let CopyPasteData::X11 { inner, .. } = d.source.insert(data) else { - unreachable!(); - }; - if let Some(serial) = self - .last_kb_serial - .as_ref() - .map(|(_seat, serial)| serial) - .copied() - { - inner.set_selection(d.device.as_ref().unwrap(), serial); - } - } - } - pub fn new_global_scale(&mut self) -> Option { self.new_scale.take() } - pub fn new_selection(&mut self) -> Option { - self.clipboard_data.as_mut().and_then(|c| { - c.source.take().and_then(|s| match s { - CopyPasteData::Foreign(f) => Some(f), - CopyPasteData::X11 { .. } => { - c.source = Some(s); - None - } - }) - }) - } - - fn handle_clipboard_events(&mut self) { - if let Some(clipboard) = self.clipboard_data.as_mut() { - for (mime_type, fd) in std::mem::take(&mut self.world.selection_requests) { - let CopyPasteData::X11 { data, .. } = clipboard.source.as_ref().unwrap() else { - unreachable!("Got selection request without having set the selection?") - }; - if let Some(data) = data.upgrade() { - data.write_to(&mime_type, fd); - } - } - - if self.world.selection_cancelled { - clipboard.source = None; - self.world.selection_cancelled = false; - } - - if clipboard.source.is_none() { - if let Some(offer) = self.world.selection_offer.take() { - if offer.inner().is_alive() { - let mime_types: Box<[String]> = offer.with_mime_types(|mimes| mimes.into()); - let foreign = ForeignSelection { - mime_types, - inner: offer, - }; - clipboard.source = Some(CopyPasteData::Foreign(foreign)); - } else { - clipboard.source = None; - } - } - } - } - } - fn handle_activations(&mut self) { let Some(activation_state) = self.activation_state.as_ref() else { return; @@ -1444,42 +1363,3 @@ pub struct PendingSurfaceState { pub width: i32, pub height: i32, } - -struct ClipboardData { - manager: DataDeviceManagerState, - device: Option, - source: Option>, -} - -pub struct ForeignSelection { - pub mime_types: Box<[String]>, - inner: SelectionOffer, -} - -impl ForeignSelection { - pub(crate) fn receive( - &self, - mime_type: String, - state: &ServerState, - ) -> Vec { - let mut pipe = self.inner.receive(mime_type).unwrap(); - state.queue.flush().unwrap(); - let mut data = Vec::new(); - pipe.read_to_end(&mut data).unwrap(); - data - } -} - -impl Drop for ForeignSelection { - fn drop(&mut self) { - self.inner.destroy(); - } -} - -enum CopyPasteData { - X11 { - inner: CopyPasteSource, - data: Weak, - }, - Foreign(ForeignSelection), -} diff --git a/src/server/selection.rs b/src/server/selection.rs new file mode 100644 index 0000000..ef2b30f --- /dev/null +++ b/src/server/selection.rs @@ -0,0 +1,287 @@ +use super::clientside::SelectionEvents; +use super::{InnerServerState, MyWorld, ServerState}; +use crate::{X11Selection, XConnection}; +use log::{info, warn}; +use smithay_client_toolkit::data_device_manager::ReadPipe; +use wayland_client::globals::GlobalList; +use wayland_client::protocol::wl_seat::WlSeat; +use wayland_client::{Proxy, QueueHandle}; + +use smithay_client_toolkit::data_device_manager::{ + data_device::DataDevice, data_offer::SelectionOffer as WlSelectionOffer, + data_source::CopyPasteSource, DataDeviceManagerState, +}; +use smithay_client_toolkit::primary_selection::device::PrimarySelectionDevice; +use smithay_client_toolkit::primary_selection::offer::PrimarySelectionOffer; +use smithay_client_toolkit::primary_selection::selection::PrimarySelectionSource; +use smithay_client_toolkit::primary_selection::PrimarySelectionManagerState; +use std::io::Read; +use std::rc::{Rc, Weak}; + +pub(super) struct SelectionStates { + clipboard: Option>, + primary: Option>, +} + +impl SelectionStates { + pub fn new(global_list: &GlobalList, qh: &QueueHandle) -> Self { + Self { + clipboard: DataDeviceManagerState::bind(global_list, qh) + .inspect_err(|e| { + warn!("Could not bind data device manager ({e:?}). Clipboard will not work.") + }) + .ok() + .map(SelectionState::new), + primary: PrimarySelectionManagerState::bind(global_list, qh) + .inspect_err(|_| info!("Primary selection unsupported.")) + .ok() + .map(SelectionState::new), + } + } + + pub fn seat_created(&mut self, qh: &QueueHandle, seat: &WlSeat) { + if let Some(c) = &mut self.clipboard { + c.device = Some(c.manager.get_data_device(qh, seat)); + } + + if let Some(d) = &mut self.primary { + d.device = Some(d.manager.get_selection_device(qh, seat)); + } + } +} + +enum SelectionData { + X11 { inner: T::Source, data: Weak }, + Foreign(ForeignSelection), +} + +struct SelectionState { + manager: T::Manager, + device: Option, + source: Option>, +} + +impl SelectionState { + fn new(manager: T::Manager) -> Self { + Self { + manager, + device: None, + source: None, + } + } +} + +impl InnerServerState { + pub(super) fn handle_selection_events(&mut self) { + self.handle_impl::(); + self.handle_impl::(); + } + + fn handle_impl(&mut self) { + let Some(state) = T::selection_state(&mut self.selection_states) else { + return; + }; + + let events = T::get_events(&mut self.world); + + for (mime_type, fd) in std::mem::take(&mut events.requests) { + let SelectionData::X11 { data, .. } = state.source.as_ref().unwrap() else { + unreachable!("Got selection request without having set the selection?") + }; + if let Some(data) = data.upgrade() { + data.write_to(&mime_type, fd); + } + } + + if events.cancelled { + state.source = None; + events.cancelled = false; + } + + if state.source.is_none() { + if let Some(offer) = T::take_offer(&mut events.offer) { + let mime_types = T::get_mimes(&offer); + let foreign = ForeignSelection { + mime_types, + inner: offer, + }; + state.source = Some(SelectionData::Foreign(foreign)); + } + } + } + + pub(crate) fn set_selection_source(&mut self, selection: &Rc) { + if let Some(state) = T::selection_state(&mut self.selection_states) { + let src = T::create_source(&state.manager, &self.qh, selection.mime_types()); + let data = SelectionData::X11 { + inner: src, + data: Rc::downgrade(selection), + }; + let SelectionData::X11 { inner, .. } = state.source.insert(data) else { + unreachable!(); + }; + if let Some(serial) = self + .last_kb_serial + .as_ref() + .map(|(_seat, serial)| serial) + .copied() + { + T::set_selection(inner, state.device.as_ref().unwrap(), serial); + } + } + } + + pub(crate) fn new_selection(&mut self) -> Option> { + T::selection_state(&mut self.selection_states) + .as_mut() + .and_then(|state| { + state.source.take().and_then(|s| match s { + SelectionData::Foreign(f) => Some(f), + SelectionData::X11 { .. } => { + state.source = Some(s); + None + } + }) + }) + } +} + +pub struct ForeignSelection { + pub mime_types: Box<[String]>, + inner: T::Offer, +} + +#[allow(private_bounds)] +impl ForeignSelection { + pub(crate) fn receive( + &self, + mime_type: String, + state: &ServerState, + ) -> Vec { + let mut pipe = T::receive_offer(&self.inner, mime_type).unwrap(); + state.queue.flush().unwrap(); + let mut data = Vec::new(); + pipe.read_to_end(&mut data).unwrap(); + data + } +} + +#[allow(private_bounds, private_interfaces)] +pub trait SelectionType: Sized { + type Source; + type Offer; + type Manager; + type DataDevice; + + // The methods in this trait shouldn't be used outside of this file. + + fn selection_state( + state: &mut SelectionStates, + ) -> &mut Option>; + + fn create_source( + manager: &Self::Manager, + qh: &QueueHandle, + mime_types: Vec<&str>, + ) -> Self::Source; + + fn set_selection(source: &Self::Source, device: &Self::DataDevice, serial: u32); + + fn get_events(world: &mut MyWorld) -> &mut SelectionEvents; + + fn receive_offer(offer: &Self::Offer, mime_type: String) -> std::io::Result; + + fn take_offer(offer: &mut Option) -> Option { + offer.take() + } + + fn get_mimes(offer: &Self::Offer) -> Box<[String]>; +} + +pub enum Clipboard {} +pub enum Primary {} + +#[allow(private_bounds, private_interfaces)] +impl SelectionType for Clipboard { + type Source = CopyPasteSource; + type Offer = WlSelectionOffer; + type Manager = DataDeviceManagerState; + type DataDevice = DataDevice; + + fn selection_state( + state: &mut SelectionStates, + ) -> &mut Option> { + &mut state.clipboard + } + + fn create_source( + manager: &Self::Manager, + qh: &QueueHandle, + mime_types: Vec<&str>, + ) -> Self::Source { + manager.create_copy_paste_source(qh, mime_types) + } + + fn set_selection(source: &Self::Source, device: &Self::DataDevice, serial: u32) { + source.set_selection(device, serial); + } + + fn get_events(world: &mut MyWorld) -> &mut SelectionEvents { + &mut world.clipboard + } + + fn take_offer(offer: &mut Option) -> Option { + offer.take().filter(|offer| offer.inner().is_alive()) + } + + fn get_mimes(offer: &Self::Offer) -> Box<[String]> { + offer.with_mime_types(|mimes| mimes.into()) + } + + fn receive_offer(offer: &Self::Offer, mime_type: String) -> std::io::Result { + offer.receive(mime_type).map_err(|e| { + match e { + smithay_client_toolkit::data_device_manager::data_offer::DataOfferError::InvalidReceive => std::io::Error::from(std::io::ErrorKind::Other), + smithay_client_toolkit::data_device_manager::data_offer::DataOfferError::Io(e) => e + } + }) + } +} + +#[allow(private_bounds, private_interfaces)] +impl SelectionType for Primary { + type Source = PrimarySelectionSource; + type Offer = PrimarySelectionOffer; + type Manager = PrimarySelectionManagerState; + type DataDevice = PrimarySelectionDevice; + + fn selection_state( + state: &mut SelectionStates, + ) -> &mut Option> { + &mut state.primary + } + + fn create_source( + manager: &Self::Manager, + qh: &QueueHandle, + mime_types: Vec<&str>, + ) -> Self::Source { + manager.create_selection_source(qh, mime_types) + } + + fn set_selection(source: &Self::Source, device: &Self::DataDevice, serial: u32) { + source.set_selection(device, serial); + } + + fn get_events(world: &mut MyWorld) -> &mut SelectionEvents { + &mut world.primary + } + + fn receive_offer(offer: &Self::Offer, mime_type: String) -> std::io::Result { + offer.receive(mime_type) + } + + fn get_mimes(offer: &Self::Offer) -> Box<[String]> { + offer.with_mime_types(|mimes| mimes.into()) + } +} diff --git a/src/server/tests.rs b/src/server/tests.rs index badde7a..44a2654 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -1,4 +1,5 @@ -use super::{InnerServerState, NoConnection, ServerState, WindowDims}; +use super::{selection::Clipboard, InnerServerState, NoConnection, ServerState, WindowDims}; +use crate::server::selection::{Primary, SelectionType}; use crate::xstate::{SetState, WinSize, WmName}; use crate::XConnection; use rustix::event::{poll, PollFd, PollFlags}; @@ -7,6 +8,7 @@ use std::io::Write; use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::unix::net::UnixStream; use std::sync::{Arc, Mutex}; +use testwl::SendDataForMimeFn; use wayland_client::{ backend::{protocol::Message, Backend, ObjectData, ObjectId, WaylandError}, protocol::{ @@ -1267,8 +1269,70 @@ fn window_group_properties() { assert_eq!(data.toplevel().app_id, Some("class".into())); } -#[test] -fn copy_from_x11() { +trait SelectionTest { + type SelectionType: SelectionType; + fn mimes(testwl: &mut testwl::Server) -> Vec; + fn paste_data( + testwl: &mut testwl::Server, + send_data: impl SendDataForMimeFn, + ) -> Vec; + fn create_offer(testwl: &mut testwl::Server, data: Vec); +} + +macro_rules! selection_tests { + ($name:ident, $selection_type:ty, $get_mime_fn:ident, $get_paste_data_fn:ident, $create_offer_fn:ident) => { + impl SelectionTest for $selection_type { + type SelectionType = $selection_type; + fn mimes(testwl: &mut testwl::Server) -> Vec { + testwl.$get_mime_fn() + } + fn paste_data( + testwl: &mut testwl::Server, + send_data: impl SendDataForMimeFn, + ) -> Vec { + testwl.$get_paste_data_fn(send_data) + } + fn create_offer(testwl: &mut testwl::Server, data: Vec) { + testwl.$create_offer_fn(data); + } + } + + mod $name { + use super::*; + #[test] + fn copy_from_x11() { + super::copy_from_x11::<$selection_type>(); + } + + #[test] + fn copy_from_wayland() { + super::copy_from_wayland::<$selection_type>(); + } + + #[test] + fn x11_then_wayland() { + super::selection_x11_then_wayland::<$selection_type>(); + } + } + }; +} + +selection_tests!( + clipboard, + Clipboard, + data_source_mimes, + clipboard_paste_data, + create_data_offer +); +selection_tests!( + primary, + Primary, + primary_source_mimes, + primary_paste_data, + create_primary_offer +); + +fn copy_from_x11() { let (mut f, comp) = TestFixture::new_with_compositor(); let win = unsafe { Window::new(1) }; let (_surface, _id) = f.create_toplevel(&comp, win); @@ -1284,23 +1348,22 @@ fn copy_from_x11() { }, ]); - f.satellite.set_copy_paste_source(&mimes); + f.satellite.set_selection_source::(&mimes); f.run(); - let server_mimes = f.testwl.data_source_mimes(); + let server_mimes = T::mimes(&mut f.testwl); for mime in mimes.iter() { assert!(server_mimes.contains(&mime.mime_type)); } - let data = f.testwl.paste_data(|_, _| { + let data = T::paste_data(&mut f.testwl, |_, _| { f.satellite.run(); true }); assert_eq!(*mimes, data); } -#[test] -fn copy_from_wayland() { +fn copy_from_wayland() { let (mut f, comp) = TestFixture::new_with_compositor(); TestObject::::from_request(&comp.seat.obj, wl_seat::Request::GetKeyboard {}); let win = unsafe { Window::new(1) }; @@ -1316,10 +1379,13 @@ fn copy_from_wayland() { data: vec![1, 2, 3, 4, 6, 10], }, ]; - f.testwl.create_data_offer(mimes.clone()); + T::create_offer(&mut f.testwl, mimes.clone()); f.run(); - let selection = f.satellite.new_selection().expect("No new selection"); + let selection = f + .satellite + .new_selection::() + .expect("No new selection"); for mime in &mimes { let data = std::thread::scope(|s| { // receive requires a queue flush - dispatch testwl from another thread @@ -1341,8 +1407,7 @@ fn copy_from_wayland() { } } -#[test] -fn clipboard_x11_then_wayland() { +fn selection_x11_then_wayland() { let (mut f, comp) = TestFixture::new_with_compositor(); TestObject::::from_request(&comp.seat.obj, wl_seat::Request::GetKeyboard {}); let win = unsafe { Window::new(1) }; @@ -1359,7 +1424,8 @@ fn clipboard_x11_then_wayland() { }, ]); - f.satellite.set_copy_paste_source(&x11data); + f.satellite + .set_selection_source::(&x11data); f.run(); let waylanddata = vec![ @@ -1372,11 +1438,14 @@ fn clipboard_x11_then_wayland() { data: vec![10, 20, 40, 50], }, ]; - f.testwl.create_data_offer(waylanddata.clone()); + T::create_offer(&mut f.testwl, waylanddata.clone()); f.run(); f.run(); - let selection = f.satellite.new_selection().expect("No new selection"); + let selection = f + .satellite + .new_selection::() + .expect("No new selection"); for mime in &waylanddata { let data = std::thread::scope(|s| { // receive requires a queue flush - dispatch testwl from another thread @@ -2429,7 +2498,7 @@ fn quick_empty_data_offer() { f.testwl.empty_data_offer(); f.run(); - let selection = f.satellite.new_selection(); + let selection = f.satellite.new_selection::(); assert!(selection.is_none()); } diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index 0e81611..799b687 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -1,7 +1,7 @@ mod settings; use settings::Settings; mod selection; -use selection::{Selection, SelectionData}; +use selection::{Selection, SelectionState}; use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1; use crate::XConnection; @@ -117,7 +117,7 @@ pub struct XState { window_atoms: WindowTypes, root: x::Window, wm_window: x::Window, - selection_data: SelectionData, + selection_state: SelectionState, settings: Settings, } @@ -201,6 +201,15 @@ impl XState { | SelectionEventMask::SELECTION_CLIENT_CLOSE, }) .unwrap(); + connection + .send_and_check_request(&xcb::xfixes::SelectSelectionInput { + window: root, + selection: atoms.primary, + event_mask: SelectionEventMask::SET_SELECTION_OWNER + | SelectionEventMask::SELECTION_WINDOW_DESTROY + | SelectionEventMask::SELECTION_CLIENT_CLOSE, + }) + .unwrap(); { // Setup default cursor theme let ctx = CursorContext::new(&connection, screen).unwrap(); @@ -214,7 +223,7 @@ impl XState { } let wm_window = connection.generate_id(); - let selection_data = SelectionData::new(&connection, root); + let selection_state = SelectionState::new(&connection, root, &atoms); let window_atoms = WindowTypes::intern_all(&connection).unwrap(); let settings = Settings::new(&connection, &atoms, root); @@ -224,7 +233,7 @@ impl XState { root, atoms, window_atoms, - selection_data, + selection_state, settings, }; r.create_ewmh_window(); @@ -917,6 +926,7 @@ xcb::atoms_struct! { incr => b"INCR" only_if_exists = false, xsettings => b"_XSETTINGS_S0" only_if_exists = false, xsettings_settings => b"_XSETTINGS_SETTINGS" only_if_exists = false, + primary => b"PRIMARY" only_if_exists = false, } } diff --git a/src/xstate/selection.rs b/src/xstate/selection.rs index 50a2a82..884f091 100644 --- a/src/xstate/selection.rs +++ b/src/xstate/selection.rs @@ -1,5 +1,5 @@ use super::{get_atom_name, XState}; -use crate::server::ForeignSelection; +use crate::server::selection::{Clipboard, ForeignSelection, Primary, SelectionType}; use crate::{RealServerState, X11Selection}; use log::{debug, error, warn}; use smithay_client_toolkit::data_device_manager::WritePipe; @@ -26,7 +26,7 @@ pub struct Selection { connection: Rc, window: x::Window, pending: RefCell>, - clipboard: x::Atom, + selection: x::Atom, selection_time: u32, incr: x::Atom, } @@ -46,13 +46,13 @@ impl X11Selection for Selection { .connection .send_and_check_request(&x::ConvertSelection { requestor: self.window, - selection: self.clipboard, + selection: self.selection, target: target.atom, property: target.atom, time: self.selection_time, }) { - error!("Failed to request clipboard data (mime type: {mime}, error: {e})"); + error!("Failed to request selection data (mime type: {mime}, error: {e})"); return; } @@ -162,21 +162,275 @@ impl Selection { } } -enum CurrentSelection { +enum CurrentSelection { X11(Rc), Wayland { mimes: Vec, - inner: ForeignSelection, + inner: ForeignSelection, }, } -pub(crate) struct SelectionData { + +struct SelectionData { last_selection_timestamp: u32, - target_window: x::Window, - current_selection: Option, + atom: x::Atom, + current_selection: Option>, } -impl SelectionData { - pub fn new(connection: &xcb::Connection, root: x::Window) -> Self { +// This is a trait so that we can use &dyn +trait SelectionDataImpl { + fn set_owner(&self, connection: &xcb::Connection, wm_window: x::Window); + fn handle_new_owner( + &mut self, + connection: &xcb::Connection, + wm_window: x::Window, + atoms: &super::Atoms, + owner: x::Window, + timestamp: u32, + ); + fn handle_target_list( + &mut self, + connection: &Rc, + wm_window: x::Window, + atoms: &super::Atoms, + target_window: x::Window, + dest_property: x::Atom, + server_state: &mut RealServerState, + ); + fn x11_selection(&self) -> Option<&Selection>; + fn handle_selection_request( + &self, + connection: &xcb::Connection, + atoms: &super::Atoms, + request: &x::SelectionRequestEvent, + success: &dyn Fn(), + refuse: &dyn Fn(), + server_state: &mut RealServerState, + ); + fn atom(&self) -> x::Atom; +} + +impl SelectionData { + fn new(atom: x::Atom) -> Self { + Self { + last_selection_timestamp: x::CURRENT_TIME, + atom, + current_selection: None, + } + } +} + +impl SelectionDataImpl for SelectionData { + fn atom(&self) -> x::Atom { + self.atom + } + fn set_owner(&self, connection: &xcb::Connection, wm_window: x::Window) { + connection + .send_and_check_request(&x::SetSelectionOwner { + owner: wm_window, + selection: self.atom, + time: self.last_selection_timestamp, + }) + .unwrap(); + + let reply = connection + .wait_for_reply(connection.send_request(&x::GetSelectionOwner { + selection: self.atom, + })) + .unwrap(); + + if reply.owner() != wm_window { + warn!( + "Could not get {} selection (owned by {:?})", + get_atom_name(connection, self.atom), + reply.owner() + ); + } + } + + fn handle_new_owner( + &mut self, + connection: &xcb::Connection, + wm_window: x::Window, + atoms: &super::Atoms, + owner: x::Window, + timestamp: u32, + ) { + debug!( + "new {} owner: {owner:?}", + get_atom_name(connection, self.atom) + ); + self.last_selection_timestamp = timestamp; + // Grab targets + connection + .send_and_check_request(&x::ConvertSelection { + requestor: wm_window, + selection: self.atom, + target: atoms.targets, + property: atoms.selection_reply, + time: timestamp, + }) + .unwrap(); + } + + fn handle_target_list( + &mut self, + connection: &Rc, + wm_window: x::Window, + atoms: &super::Atoms, + target_window: x::Window, + dest_property: x::Atom, + server_state: &mut RealServerState, + ) { + let reply = connection + .wait_for_reply(connection.send_request(&x::GetProperty { + delete: true, + window: wm_window, + property: dest_property, + r#type: x::ATOM_ATOM, + long_offset: 0, + long_length: 20, + })) + .unwrap(); + + let targets: &[x::Atom] = reply.value(); + if targets.is_empty() { + warn!("Got empty selection target list, trying again..."); + match connection.wait_for_reply(connection.send_request(&x::GetSelectionOwner { + selection: self.atom, + })) { + Ok(reply) => { + if reply.owner() == wm_window { + warn!("We are unexpectedly the selection owner? Clipboard may be broken!"); + } else { + self.handle_new_owner( + connection, + wm_window, + atoms, + reply.owner(), + self.last_selection_timestamp, + ); + } + } + Err(e) => { + error!("Couldn't grab selection owner: {e:?}. Clipboard is stale!"); + } + } + return; + } + if log::log_enabled!(log::Level::Debug) { + let targets_str: Vec = targets + .iter() + .map(|t| get_atom_name(connection, *t)) + .collect(); + debug!("got targets: {targets_str:?}"); + } + + let mimes = targets + .iter() + .copied() + .filter(|atom| ![atoms.targets, atoms.multiple, atoms.save_targets].contains(atom)) + .map(|target_atom| SelectionTargetId { + name: get_atom_name(connection, target_atom), + atom: target_atom, + source: None, + }) + .collect(); + + let selection = Rc::new(Selection { + mimes, + connection: connection.clone(), + window: target_window, + pending: RefCell::default(), + selection: self.atom, + selection_time: self.last_selection_timestamp, + incr: atoms.incr, + }); + + server_state.set_selection_source::(&selection); + self.current_selection = Some(CurrentSelection::X11(selection)); + debug!("{} set from X11", get_atom_name(connection, self.atom)); + } + + fn x11_selection(&self) -> Option<&Selection> { + match &self.current_selection { + Some(CurrentSelection::X11(selection)) => Some(selection), + _ => None, + } + } + + fn handle_selection_request( + &self, + connection: &xcb::Connection, + atoms: &super::Atoms, + request: &x::SelectionRequestEvent, + success: &dyn Fn(), + refuse: &dyn Fn(), + server_state: &mut RealServerState, + ) { + let Some(CurrentSelection::Wayland { mimes, inner }) = &self.current_selection else { + warn!("Got selection request, but we don't seem to be the selection owner"); + refuse(); + return; + }; + + match request.target() { + x if x == atoms.targets => { + let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect(); + + connection + .send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: request.requestor(), + property: request.property(), + r#type: x::ATOM_ATOM, + data: &atoms, + }) + .unwrap(); + + success(); + } + other => { + let Some(target) = mimes.iter().find(|t| t.atom == other) else { + if log::log_enabled!(log::Level::Debug) { + let name = get_atom_name(connection, other); + debug!("refusing selection request because given atom could not be found ({name})"); + } + refuse(); + return; + }; + + let mime_name = target + .source + .as_ref() + .cloned() + .unwrap_or_else(|| target.name.clone()); + let data = inner.receive(mime_name, server_state); + match connection.send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: request.requestor(), + property: request.property(), + r#type: target.atom, + data: &data, + }) { + Ok(_) => success(), + Err(e) => { + warn!("Failed setting selection property: {e:?}"); + refuse(); + } + } + } + } + } +} + +pub(super) struct SelectionState { + clipboard: SelectionData, + primary: SelectionData, + target_window: x::Window, +} + +impl SelectionState { + pub fn new(connection: &xcb::Connection, root: x::Window, atoms: &super::Atoms) -> Self { let target_window = connection.generate_id(); connection .send_and_check_request(&x::CreateWindow { @@ -195,39 +449,15 @@ impl SelectionData { }) .expect("Couldn't create window for selections"); Self { - last_selection_timestamp: x::CURRENT_TIME, target_window, - current_selection: None, + clipboard: SelectionData::new(atoms.clipboard), + primary: SelectionData::new(atoms.primary), } } } impl XState { - fn set_clipboard_owner(&mut self) { - self.connection - .send_and_check_request(&x::SetSelectionOwner { - owner: self.wm_window, - selection: self.atoms.clipboard, - time: self.selection_data.last_selection_timestamp, - }) - .unwrap(); - - let reply = self - .connection - .wait_for_reply(self.connection.send_request(&x::GetSelectionOwner { - selection: self.atoms.clipboard, - })) - .unwrap(); - - if reply.owner() != self.wm_window { - warn!( - "Could not get CLIPBOARD selection (owned by {:?})", - reply.owner() - ); - } - } - - pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) { + pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) { let mut utf8_xwl = false; let mut utf8_wl = false; let mut mimes: Vec = selection @@ -273,51 +503,133 @@ impl XState { }); } - self.selection_data.current_selection = Some(CurrentSelection::Wayland { + self.selection_state.clipboard.current_selection = Some(CurrentSelection::Wayland { mimes, inner: selection, }); - self.set_clipboard_owner(); + self.selection_state + .clipboard + .set_owner(&self.connection, self.wm_window); debug!("Clipboard set from Wayland"); } + pub(crate) fn set_primary_selection(&mut self, selection: ForeignSelection) { + let mut utf8_xwl = false; + let mut utf8_wl = false; + let mut mimes: Vec = selection + .mime_types + .iter() + .map(|mime| { + match mime.as_str() { + "UTF8_STRING" => utf8_xwl = true, + "text/plain;charset=utf-8" => utf8_wl = true, + _ => {} + } + + let atom = self + .connection + .wait_for_reply(self.connection.send_request(&x::InternAtom { + only_if_exists: false, + name: mime.as_bytes(), + })) + .unwrap(); + + SelectionTargetId { + name: mime.clone(), + atom: atom.atom(), + source: None, + } + }) + .collect(); + + if utf8_wl && !utf8_xwl { + let name = "UTF8_STRING".to_string(); + let atom = self + .connection + .wait_for_reply(self.connection.send_request(&x::InternAtom { + only_if_exists: false, + name: name.as_bytes(), + })) + .unwrap() + .atom(); + mimes.push(SelectionTargetId { + name, + atom, + source: Some("text/plain;charset=utf-8".to_string()), + }); + } + + self.selection_state.primary.current_selection = Some(CurrentSelection::Wayland { + mimes, + inner: selection, + }); + self.selection_state + .primary + .set_owner(&self.connection, self.wm_window); + debug!("Primaryset from Wayland"); + } + pub(super) fn handle_selection_event( &mut self, event: &xcb::Event, server_state: &mut RealServerState, ) -> bool { + macro_rules! get_selection_data { + ($selection:expr) => { + match $selection { + x if x == self.atoms.clipboard => { + &mut self.selection_state.clipboard as &mut dyn SelectionDataImpl + } + x if x == self.atoms.primary => &mut self.selection_state.primary as _, + _ => return true, + } + }; + } match event { xcb::Event::X(x::Event::SelectionClear(e)) => { - if e.selection() == self.atoms.clipboard { - self.handle_new_selection_owner(e.owner(), e.time()); - } + let data = get_selection_data!(e.selection()); + data.handle_new_owner( + &self.connection, + self.wm_window, + &self.atoms, + e.owner(), + e.time(), + ); } xcb::Event::X(x::Event::SelectionNotify(e)) => { if e.property() == x::ATOM_NONE { - warn!("selection notify fail?"); + warn!( + "selection notify fail? {}", + get_atom_name(&self.connection, e.selection()) + ); return true; } + let data = get_selection_data!(e.selection()); debug!( - "selection notify requestor: {:?} target: {}", + "selection notify requestor: {:?} target: {} selection: {}", e.requestor(), - get_atom_name(&self.connection, e.target()) + get_atom_name(&self.connection, e.target()), + get_atom_name(&self.connection, e.selection()), ); if e.requestor() == self.wm_window { match e.target() { - x if x == self.atoms.targets => { - self.handle_target_list(e.property(), server_state) - } + x if x == self.atoms.targets => data.handle_target_list( + &self.connection, + self.wm_window, + &self.atoms, + self.selection_state.target_window, + e.property(), + server_state, + ), other => warn!( "got unexpected selection notify for target {}", get_atom_name(&self.connection, other) ), } - } else if e.requestor() == self.selection_data.target_window { - if let Some(CurrentSelection::X11(selection)) = - &self.selection_data.current_selection - { + } else if e.requestor() == self.selection_state.target_window { + if let Some(selection) = data.x11_selection() { selection.handle_notify(e.target()); } } else { @@ -328,6 +640,7 @@ impl XState { } } xcb::Event::X(x::Event::SelectionRequest(e)) => { + let data = get_selection_data!(e.selection()); let send_notify = |property| { self.connection .send_and_check_request(&x::SendEvent { @@ -349,7 +662,8 @@ impl XState { if log::log_enabled!(log::Level::Debug) { let target = get_atom_name(&self.connection, e.target()); - debug!("Got selection request for target {target}"); + let selection = get_atom_name(&self.connection, data.atom()); + debug!("Got selection request for target {target} (selection: {selection})"); } if e.property() == x::ATOM_NONE { @@ -358,76 +672,37 @@ impl XState { return true; } - let Some(CurrentSelection::Wayland { mimes, inner }) = - &self.selection_data.current_selection - else { - warn!("Got selection request, but we don't seem to be the selection owner"); - refuse(); - return true; - }; - - match e.target() { - x if x == self.atoms.targets => { - let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect(); - - self.connection - .send_and_check_request(&x::ChangeProperty { - mode: x::PropMode::Replace, - window: e.requestor(), - property: e.property(), - r#type: x::ATOM_ATOM, - data: &atoms, - }) - .unwrap(); - - success(); - } - other => { - let Some(target) = mimes.iter().find(|t| t.atom == other) else { - if log::log_enabled!(log::Level::Debug) { - let name = get_atom_name(&self.connection, other); - debug!("refusing selection request because given atom could not be found ({name})"); - } - refuse(); - return true; - }; - - let mime_name = target - .source - .as_ref() - .cloned() - .unwrap_or_else(|| target.name.clone()); - let data = inner.receive(mime_name, server_state); - match self.connection.send_and_check_request(&x::ChangeProperty { - mode: x::PropMode::Replace, - window: e.requestor(), - property: e.property(), - r#type: target.atom, - data: &data, - }) { - Ok(_) => success(), - Err(e) => { - warn!("Failed setting selection property: {e:?}"); - refuse(); - } - } - } - } + data.handle_selection_request( + &self.connection, + &self.atoms, + e, + &success, + &refuse, + server_state, + ); } xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => match e.selection() { - x if x == self.atoms.clipboard => match e.subtype() { + x if x == self.atoms.clipboard || x == self.atoms.primary => match e.subtype() { xcb::xfixes::SelectionEvent::SetSelectionOwner => { if e.owner() == self.wm_window { return true; } - self.handle_new_selection_owner(e.owner(), e.selection_timestamp()); + let data = get_selection_data!(x); + + data.handle_new_owner( + &self.connection, + self.wm_window, + &self.atoms, + e.owner(), + e.timestamp(), + ); } xcb::xfixes::SelectionEvent::SelectionClientClose | xcb::xfixes::SelectionEvent::SelectionWindowDestroy => { debug!("Selection owner destroyed, selection will be unset"); - self.selection_data.current_selection = None; + self.selection_state.clipboard.current_selection = None; } }, x if x == self.atoms.xsettings => match e.subtype() { @@ -446,105 +721,17 @@ impl XState { true } - fn handle_new_selection_owner(&mut self, owner: x::Window, timestamp: u32) { - debug!("new selection owner: {owner:?}"); - self.selection_data.last_selection_timestamp = timestamp; - // Grab targets - self.connection - .send_and_check_request(&x::ConvertSelection { - requestor: self.wm_window, - selection: self.atoms.clipboard, - target: self.atoms.targets, - property: self.atoms.selection_reply, - time: timestamp, - }) - .unwrap(); - } - - fn handle_target_list(&mut self, dest_property: x::Atom, server_state: &mut RealServerState) { - let reply = self - .connection - .wait_for_reply(self.connection.send_request(&x::GetProperty { - delete: true, - window: self.wm_window, - property: dest_property, - r#type: x::ATOM_ATOM, - long_offset: 0, - long_length: 20, - })) - .unwrap(); - - let targets: &[x::Atom] = reply.value(); - if targets.is_empty() { - warn!("Got empty selection target list, trying again..."); - match self.connection.wait_for_reply(self.connection.send_request( - &x::GetSelectionOwner { - selection: self.atoms.clipboard, - }, - )) { - Ok(reply) => { - if reply.owner() == self.wm_window { - warn!("We are unexpectedly the selection owner? Clipboard may be broken!"); - } else { - self.handle_new_selection_owner( - reply.owner(), - self.selection_data.last_selection_timestamp, - ); - } - } - Err(e) => { - error!("Couldn't grab selection owner: {e:?}. Clipboard is stale!"); - } - } - return; - } - if log::log_enabled!(log::Level::Debug) { - let targets_str: Vec = targets - .iter() - .map(|t| get_atom_name(&self.connection, *t)) - .collect(); - debug!("got targets: {targets_str:?}"); - } - - let mimes = targets - .iter() - .copied() - .filter(|atom| { - ![ - self.atoms.targets, - self.atoms.multiple, - self.atoms.save_targets, - ] - .contains(atom) - }) - .map(|target_atom| SelectionTargetId { - name: get_atom_name(&self.connection, target_atom), - atom: target_atom, - source: None, - }) - .collect(); - - let selection = Rc::new(Selection { - mimes, - connection: self.connection.clone(), - window: self.selection_data.target_window, - pending: RefCell::default(), - clipboard: self.atoms.clipboard, - selection_time: self.selection_data.last_selection_timestamp, - incr: self.atoms.incr, - }); - - server_state.set_copy_paste_source(&selection); - self.selection_data.current_selection = Some(CurrentSelection::X11(selection)); - debug!("Clipboard set from X11"); - } - pub(super) fn handle_selection_property_change( &mut self, event: &x::PropertyNotifyEvent, ) -> bool { - if let Some(CurrentSelection::X11(selection)) = &self.selection_data.current_selection { - return selection.check_for_incr(event); + for data in [ + &self.selection_state.primary as &dyn SelectionDataImpl, + &self.selection_state.clipboard as _, + ] { + if let Some(selection) = &data.x11_selection() { + return selection.check_for_incr(event); + } } false } diff --git a/tests/integration.rs b/tests/integration.rs index b8c61f5..ca613c6 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1004,7 +1004,7 @@ fn copy_from_x11() { ); } - let data = f.testwl.paste_data(|mime, _| { + let data = f.testwl.clipboard_paste_data(|mime, _| { let request = connection.await_selection_request(); let data = mimes_truth .iter() @@ -1201,7 +1201,7 @@ fn bad_clipboard_data() { connection.send_selection_notify(&request); f.wait_and_dispatch(); - let mut data = f.testwl.paste_data(|_, _| { + let mut data = f.testwl.clipboard_paste_data(|_, _| { let request = connection.await_selection_request(); assert_eq!(request.target(), connection.atoms.mime2); // Don't actually set any data as requested - just report success @@ -1388,7 +1388,7 @@ fn incr_copy_from_x11() { .take(3000) .collect(); let mut it = data.chunks(500).enumerate(); - let mut paste_data = f.testwl.paste_data(|_, testwl| { + let mut paste_data = f.testwl.clipboard_paste_data(|_, testwl| { if let Some(begin) = begin_incr.take() { destination_property = begin(&mut connection); testwl.dispatch(); diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 01c1eea..1d4e2c1 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -5,6 +5,10 @@ use std::os::fd::{AsFd, BorrowedFd, OwnedFd}; use std::os::unix::net::UnixStream; use std::sync::{Arc, Mutex, OnceLock}; use std::time::Instant; +use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1; +use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1; +use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1; +use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1; use wayland_protocols::{ wp::{ fractional_scale::v1::server::{ @@ -253,9 +257,12 @@ struct State { tablet: Option, tablet_tool: Option, configure_serial: u32, - selection: Option, + clipboard: Option, + primary: Option, data_device_man: Option, data_device: Option, + primary_man: Option, + primary_device: Option, xdg_activation: Option, valid_tokens: HashSet, token_counter: u32, @@ -279,7 +286,10 @@ impl Default for State { tablet: None, tablet_tool: None, configure_serial: 0, - selection: None, + clipboard: None, + primary: None, + primary_man: None, + primary_device: None, data_device_man: None, data_device: None, xdg_activation: None, @@ -404,6 +414,9 @@ pub struct Server { client: Option, } +pub trait SendDataForMimeFn: FnMut(&str, &mut Server) -> bool {} +impl SendDataForMimeFn for T where T: FnMut(&str, &mut Server) -> bool {} + impl Server { pub fn new(noops: bool) -> Self { let display = Display::new().unwrap(); @@ -436,6 +449,7 @@ impl Server { dh.create_global::(6, ()); dh.create_global::(5, ()); dh.create_global::(3, ()); + dh.create_global::(1, ()); dh.create_global::(1, ()); dh.create_global::(1, ()); dh.create_global::(1, ()); @@ -610,7 +624,7 @@ impl Server { #[track_caller] pub fn data_source_mimes(&self) -> Vec { - let Some(selection) = &self.state.selection else { + let Some(selection) = &self.state.clipboard else { panic!("No selection set on data device"); }; @@ -620,20 +634,28 @@ impl Server { } #[track_caller] - pub fn paste_data( + pub fn primary_source_mimes(&self) -> Vec { + let Some(selection) = &self.state.primary else { + panic!("No selection set on primary device"); + }; + + let data: &Mutex = selection.data().unwrap(); + let data = data.lock().unwrap(); + data.mimes.to_vec() + } + + fn paste_impl( &mut self, - mut send_data_for_mime: impl FnMut(&str, &mut Self) -> bool, + data: &Mutex, + mut send_data_for_mime: impl SendDataForMimeFn, + mut send_selection: impl FnMut(String, std::os::unix::io::BorrowedFd), ) -> Vec { struct PendingData { rx: std::fs::File, data: Vec, } - let Some(selection) = self.state.selection.take() else { - panic!("No selection set on data device"); - }; type PendingRet = Vec<(String, Option)>; let mut pending_ret: PendingRet = { - let data: &Mutex = selection.data().unwrap(); data.lock() .unwrap() .mimes @@ -673,7 +695,7 @@ impl Server { Some(pending) => try_transfer(&mut pending_ret, mime, pending), None => { let (rx, tx) = rustix::pipe::pipe().unwrap(); - selection.send(mime.clone(), tx.as_fd()); + send_selection(mime.clone(), tx.as_fd()); drop(tx); let rx = std::fs::File::from(rx); @@ -689,13 +711,47 @@ impl Server { } } - self.state.selection = Some(selection); + ret + } + #[track_caller] + pub fn clipboard_paste_data( + &mut self, + send_data_for_mime: impl SendDataForMimeFn, + ) -> Vec { + let Some(selection) = self.state.clipboard.take() else { + panic!("No selection set on data device"); + }; + + let ret = self.paste_impl( + selection.data().unwrap(), + send_data_for_mime, + |mime_type, fd| selection.send(mime_type, fd), + ); + self.state.clipboard = Some(selection); + ret + } + + #[track_caller] + pub fn primary_paste_data( + &mut self, + send_data_for_mime: impl SendDataForMimeFn, + ) -> Vec { + let Some(selection) = self.state.primary.take() else { + panic!("No selection set on primary data device"); + }; + + let ret = self.paste_impl( + selection.data().unwrap(), + send_data_for_mime, + |mime_type, fd| selection.send(mime_type, fd), + ); + self.state.primary = Some(selection); ret } pub fn data_source_exists(&self) -> bool { - self.state.selection.is_none() + self.state.clipboard.is_none() } #[track_caller] @@ -704,7 +760,7 @@ impl Server { panic!("No data device created"); }; - if let Some(selection) = self.state.selection.take() { + if let Some(selection) = self.state.clipboard.take() { selection.cancelled(); } @@ -723,13 +779,38 @@ impl Server { self.display.flush_clients().unwrap(); } + #[track_caller] + pub fn create_primary_offer(&mut self, data: Vec) { + let Some(dev) = &self.state.primary_device else { + panic!("No primary device created"); + }; + + if let Some(selection) = self.state.primary.take() { + selection.cancelled(); + } + + let mimes: Vec<_> = data.iter().map(|m| m.mime_type.clone()).collect(); + let offer = self + .client + .as_ref() + .unwrap() + .create_resource::<_, _, State>(&self.dh, 1, data) + .unwrap(); + dev.data_offer(&offer); + for mime in mimes { + offer.offer(mime); + } + dev.selection(Some(&offer)); + self.display.flush_clients().unwrap(); + } + #[track_caller] pub fn empty_data_offer(&mut self) { let Some(dev) = &self.state.data_device else { panic!("No data device created"); }; - if let Some(selection) = self.state.selection.take() { + if let Some(selection) = self.state.clipboard.take() { selection.cancelled(); } @@ -996,6 +1077,118 @@ impl Dispatch for State { } } +impl GlobalDispatch for State { + fn bind( + state: &mut Self, + _: &DisplayHandle, + _: &Client, + resource: wayland_server::New, + _: &(), + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + state.primary_man = Some(data_init.init(resource, ())); + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &ZwpPrimarySelectionDeviceManagerV1, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_manager_v1::Request; + match request { + Request::CreateSource { id } => { + data_init.init(id, DataSourceData::default().into()); + } + Request::GetDevice { id, seat } => { + state.primary_device = Some(data_init.init(id, seat)); + } + Request::Destroy => { + state.primary_man = None; + } + _ => todo!("{request:?}"), + } + } +} + +impl Dispatch> for State { + fn request( + _: &mut Self, + _: &Client, + _: &ZwpPrimarySelectionOfferV1, + request: ::Request, + data: &Vec, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_offer_v1::Request; + match request { + Request::Receive { mime_type, fd } => { + let pos = data + .iter() + .position(|data| data.mime_type == mime_type) + .unwrap_or_else(|| panic!("Invalid mime type: {mime_type}")); + + let mut stream = UnixStream::from(fd); + stream.write_all(&data[pos].data).unwrap(); + } + Request::Destroy => {} + other => todo!("{other:?}"), + } + } +} + +impl Dispatch> for State { + fn request( + state: &mut Self, + _: &Client, + _: &ZwpPrimarySelectionSourceV1, + request: ::Request, + data: &Mutex, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::Request; + match request { + Request::Offer { mime_type } => { + data.lock().unwrap().mimes.push(mime_type); + } + Request::Destroy => { + state.primary = None; + } + _ => todo!("{request:?}"), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &ZwpPrimarySelectionDeviceV1, + request: ::Request, + _: &WlSeat, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_v1::Request; + match request { + Request::SetSelection { source, .. } => { + state.primary = source; + } + Request::Destroy => { + state.primary_device = None; + } + other => todo!("unhandled request {other:?}"), + } + } +} + impl GlobalDispatch for State { fn bind( state: &mut Self, @@ -1051,7 +1244,7 @@ impl Dispatch> for State { data.mimes.push(mime_type); } wl_data_source::Request::Destroy => { - state.selection = None; + state.clipboard = None; } other => todo!("unhandled request {other:?}"), } @@ -1070,7 +1263,7 @@ impl Dispatch for State { ) { match request { wl_data_device::Request::SetSelection { source, .. } => { - state.selection = source; + state.clipboard = source; } wl_data_device::Request::Release => { state.data_device = None;