From 5e7f2df05e0e0307e7b0fcebd24194a09f8d2567 Mon Sep 17 00:00:00 2001 From: Shawn Wallace Date: Thu, 6 Jun 2024 22:41:19 -0400 Subject: [PATCH] Sync clipboard between X11 and Wayland You would not believe how much work this was. Closes #23 --- Cargo.lock | 120 +++++++++- Cargo.toml | 6 +- src/clientside.rs | 39 ++-- src/data_device.rs | 128 ++++++++++ src/lib.rs | 17 +- src/server/dispatch.rs | 56 +++-- src/server/event.rs | 80 ++++--- src/server/mod.rs | 226 +++++++++++++----- src/server/tests.rs | 183 +++++++++++++-- src/{xstate.rs => xstate/mod.rs} | 69 ++++-- src/xstate/selection.rs | 387 +++++++++++++++++++++++++++++++ tests/integration.rs | 293 ++++++++++++++++++++++- testwl/Cargo.toml | 1 + testwl/src/lib.rs | 287 +++++++++++++++++++++-- 14 files changed, 1703 insertions(+), 189 deletions(-) create mode 100644 src/data_device.rs rename src/{xstate.rs => xstate/mod.rs} (95%) create mode 100644 src/xstate/selection.rs diff --git a/Cargo.lock b/Cargo.lock index d4dec8d..8eaa0b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,7 +78,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 1.0.109", "which", ] @@ -132,6 +132,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + [[package]] name = "dlib" version = "0.5.2" @@ -295,6 +301,15 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -456,6 +471,29 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smithay-client-toolkit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +dependencies = [ + "bitflags 2.5.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + [[package]] name = "syn" version = "1.0.109" @@ -467,6 +505,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -480,11 +529,32 @@ dependencies = [ name = "testwl" version = "0.1.0" dependencies = [ + "rustix", "wayland-protocols", "wayland-server", "wl_drm", ] +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -529,6 +599,28 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.5.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + [[package]] name = "wayland-protocols" version = "0.31.2" @@ -542,6 +634,19 @@ dependencies = [ "wayland-server", ] +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + [[package]] name = "wayland-scanner" version = "0.31.1" @@ -711,6 +816,18 @@ dependencies = [ "bindgen", ] +[[package]] +name = "xcursor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" + +[[package]] +name = "xkeysym" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" + [[package]] name = "xwayland-satellite" version = "0.2.0" @@ -723,6 +840,7 @@ dependencies = [ "pretty_env_logger", "rustix", "slotmap", + "smithay-client-toolkit", "testwl", "wayland-client", "wayland-protocols", diff --git a/Cargo.toml b/Cargo.toml index ba52e92..fb2bd7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ wayland-client = "0.31.2" wayland-protocols = "0.31.2" wayland-scanner = "0.31.1" wayland-server = "0.31.1" +rustix = "0.38.31" [package] name = "xwayland-satellite" @@ -18,7 +19,7 @@ crate-type = ["lib"] [dependencies] bitflags = "2.5.0" paste = "1.0.14" -rustix = { version = "0.38.31", features = ["event"] } +rustix = { workspace = true, features = ["event"] } wayland-client.workspace = true wayland-protocols = { workspace = true, features = ["client", "server", "staging", "unstable"] } wayland-scanner.workspace = true @@ -31,7 +32,8 @@ env_logger = "0.11.3" pretty_env_logger = "0.5.0" slotmap = "1.0.7" xcb-util-cursor = "0.3.2" +smithay-client-toolkit = { version = "0.18.1", default-features = false } [dev-dependencies] -rustix = { version = "0.38.31", features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } testwl = { path = "testwl" } diff --git a/src/clientside.rs b/src/clientside.rs index b5a7f43..2e42da7 100644 --- a/src/clientside.rs +++ b/src/clientside.rs @@ -8,7 +8,11 @@ use wayland_client::protocol::{ wl_registry::WlRegistry, wl_seat::WlSeat, wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_surface::WlSurface, wl_touch::WlTouch, }; -use wayland_client::{delegate_noop, Connection, Dispatch, EventQueue, Proxy, QueueHandle}; +use wayland_client::{ + delegate_noop, + globals::{registry_queue_init, Global, GlobalList, GlobalListContents}, + Connection, Dispatch, EventQueue, Proxy, QueueHandle, +}; use wayland_protocols::wp::relative_pointer::zv1::client::{ zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, zwp_relative_pointer_v1::ZwpRelativePointerV1, @@ -40,17 +44,16 @@ use wayland_protocols::{ use wayland_server::protocol as server; use wl_drm::client::wl_drm::WlDrm; -#[derive(Debug)] -pub struct GlobalData { - pub name: u32, - pub interface: String, - pub version: u32, -} - #[derive(Default)] pub struct Globals { pub(crate) events: Vec<(ObjectKey, ObjectEvent)>, - pub new_globals: Vec, + pub new_globals: Vec, + pub selection: Option, + pub selection_requests: Vec<( + String, + smithay_client_toolkit::data_device_manager::WritePipe, + )>, + pub cancelled: bool, } pub type ClientQueueHandle = QueueHandle; @@ -65,7 +68,7 @@ pub struct ClientState { pub queue: EventQueue, pub qh: ClientQueueHandle, pub globals: Globals, - pub registry: WlRegistry, + pub global_list: GlobalList, } impl ClientState { @@ -76,20 +79,16 @@ impl ClientState { Connection::connect_to_env() } .unwrap(); - let mut queue = connection.new_event_queue::(); + let (global_list, queue) = registry_queue_init::(&connection).unwrap(); + let globals = Globals::default(); let qh = queue.handle(); - let mut globals = Globals::default(); - - let registry = connection.display().get_registry(&qh, ()); - // Get initial globals - queue.roundtrip(&mut globals).unwrap(); Self { connection, queue, qh, globals, - registry, + global_list, } } } @@ -109,12 +108,12 @@ delegate_noop!(Globals: WpViewport); delegate_noop!(Globals: ZxdgOutputManagerV1); delegate_noop!(Globals: ZwpPointerConstraintsV1); -impl Dispatch for Globals { +impl Dispatch for Globals { fn event( state: &mut Self, _: &WlRegistry, event: ::Event, - _: &(), + _: &GlobalListContents, _: &wayland_client::Connection, _: &wayland_client::QueueHandle, ) { @@ -124,7 +123,7 @@ impl Dispatch for Globals { version, } = event { - state.new_globals.push(GlobalData { + state.new_globals.push(Global { name, interface, version, diff --git a/src/data_device.rs b/src/data_device.rs new file mode 100644 index 0000000..004724a --- /dev/null +++ b/src/data_device.rs @@ -0,0 +1,128 @@ +use crate::clientside::Globals; +use smithay_client_toolkit::{ + data_device_manager::{ + data_device::DataDeviceHandler, data_offer::DataOfferHandler, + data_source::DataSourceHandler, + }, + delegate_data_device, +}; +delegate_data_device!(Globals); + +impl DataDeviceHandler for Globals { + fn selection( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + data_device: &wayland_client::protocol::wl_data_device::WlDataDevice, + ) { + self.selection = Some(data_device.clone()); + } + + fn drop_performed( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_device::WlDataDevice, + ) { + } + + fn motion( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_device::WlDataDevice, + ) { + } + + fn leave( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_device::WlDataDevice, + ) { + } + + fn enter( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_device::WlDataDevice, + ) { + } +} + +impl DataSourceHandler for Globals { + fn send_request( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_source::WlDataSource, + mime: String, + fd: smithay_client_toolkit::data_device_manager::WritePipe, + ) { + self.selection_requests.push((mime, fd)); + } + + fn cancelled( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + self.cancelled = true; + } + + fn action( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_source::WlDataSource, + _: wayland_client::protocol::wl_data_device_manager::DndAction, + ) { + } + + fn dnd_finished( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + } + + fn dnd_dropped( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + } + + fn accept_mime( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &wayland_client::protocol::wl_data_source::WlDataSource, + _: Option, + ) { + } +} + +impl DataOfferHandler for Globals { + fn selected_action( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &mut smithay_client_toolkit::data_device_manager::data_offer::DragOffer, + _: wayland_client::protocol::wl_data_device_manager::DndAction, + ) { + } + + fn source_actions( + &mut self, + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + _: &mut smithay_client_toolkit::data_device_manager::data_offer::DragOffer, + _: wayland_client::protocol::wl_data_device_manager::DndAction, + ) { + } +} diff --git a/src/lib.rs b/src/lib.rs index 8ba83e7..45ddcdf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ mod clientside; +mod data_device; mod server; pub mod xstate; @@ -16,6 +17,7 @@ use xcb::x; pub trait XConnection: Sized + 'static { type ExtraData: FromServerState; + type MimeTypeData: MimeTypeData; fn root_window(&self) -> x::Window; fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState); @@ -28,6 +30,11 @@ pub trait FromServerState { fn create(state: &ServerState) -> Self; } +pub trait MimeTypeData { + fn name(&self) -> &str; + fn data(&self) -> &[u8]; +} + type RealServerState = ServerState>; pub trait RunData { @@ -152,12 +159,18 @@ pub fn main(data: impl RunData) -> Option<()> { server_state.atoms = Some(xstate.atoms.clone()); } - if let Some(state) = &mut xstate { - state.handle_events(&mut server_state); + if let Some(xstate) = &mut xstate { + xstate.handle_events(&mut server_state); } display.dispatch_clients(&mut server_state).unwrap(); server_state.run(); display.flush_clients().unwrap(); + + if let Some(xstate) = &mut xstate { + if let Some(sel) = server_state.new_selection() { + xstate.set_clipboard(sel); + } + } } } diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index ef0e0ea..2a52451 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -1,6 +1,7 @@ use super::*; use log::{debug, error, trace, warn}; use std::sync::{Arc, OnceLock}; +use wayland_client::globals::Global; use wayland_protocols::{ wp::{ linux_dmabuf::zv1::{client as c_dmabuf, server as s_dmabuf}, @@ -994,7 +995,7 @@ impl Default for ClientGlobalWrapper { macro_rules! global_dispatch_no_events { ($server:ty, $client:ty) => { - impl GlobalDispatch<$server, GlobalData> for ServerState + impl GlobalDispatch<$server, Global> for ServerState where ServerState: Dispatch<$server, ClientGlobalWrapper<$client>>, Globals: wayland_client::Dispatch<$client, ()>, @@ -1004,7 +1005,7 @@ macro_rules! global_dispatch_no_events { _: &DisplayHandle, _: &wayland_server::Client, resource: wayland_server::New<$server>, - data: &GlobalData, + data: &Global, data_init: &mut wayland_server::DataInit<'_, Self>, ) { let client = ClientGlobalWrapper::<$client>::default(); @@ -1014,8 +1015,9 @@ macro_rules! global_dispatch_no_events { .set( state .clientside - .registry - .bind(data.name, server.version(), &state.qh, ()), + .global_list + .registry() + .bind::<$client, _, _>(data.name, server.version(), &state.qh, ()), ) .unwrap(); } @@ -1025,7 +1027,7 @@ macro_rules! global_dispatch_no_events { macro_rules! global_dispatch_with_events { ($server:ty, $client:ty) => { - impl GlobalDispatch<$server, GlobalData> for ServerState + impl GlobalDispatch<$server, Global> for ServerState where $server: Resource, $client: Proxy, @@ -1038,17 +1040,16 @@ macro_rules! global_dispatch_with_events { _: &DisplayHandle, _: &wayland_server::Client, resource: wayland_server::New<$server>, - data: &GlobalData, + data: &Global, data_init: &mut wayland_server::DataInit<'_, Self>, ) { state.objects.insert_with_key(|key| { let server = data_init.init(resource, key); - let client = state.clientside.registry.bind::<$client, _, _>( - data.name, - server.version(), - &state.qh, - key, - ); + let client = state + .clientside + .global_list + .registry() + .bind::<$client, _, _>(data.name, server.version(), &state.qh, key); GenericObject { server, client }.into() }); } @@ -1070,7 +1071,36 @@ global_dispatch_no_events!( ); global_dispatch_no_events!(PointerConstraintsServer, PointerConstraintsClient); -global_dispatch_with_events!(WlSeat, client::wl_seat::WlSeat); +impl GlobalDispatch for ServerState +where + WlSeat: Resource, + client::wl_seat::WlSeat: Proxy, + ServerState: Dispatch, + Globals: wayland_client::Dispatch, + GenericObject: Into, +{ + fn bind( + state: &mut Self, + _: &DisplayHandle, + _: &wayland_server::Client, + resource: wayland_server::New, + data: &Global, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + state.objects.insert_with_key(|key| { + let server = data_init.init(resource, key); + let client = state + .clientside + .global_list + .registry() + .bind::(data.name, server.version(), &state.qh, key); + if let Some(c) = &mut state.clipboard_data { + c.device = Some(c.manager.get_data_device(&state.qh, &client)); + } + GenericObject { server, client }.into() + }); + } +} global_dispatch_with_events!(WlOutput, client::wl_output::WlOutput); global_dispatch_with_events!(WlDrmServer, WlDrmClient); diff --git a/src/server/event.rs b/src/server/event.rs index 7217539..28b2d43 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -493,45 +493,51 @@ impl HandleEvent for Keyboard { type Event = client::wl_keyboard::Event; fn handle_event(&mut self, event: Self::Event, state: &mut ServerState) { - simple_event_shunt! { - self.server, event: client::wl_keyboard::Event => [ - Keymap { - |format| convert_wenum(format), - |fd| fd.as_fd(), - size - }, - Enter { - serial, - |surface| state.get_server_surface_from_client(surface), - keys - }, - Leave { - serial, - |surface| { - if !surface.is_alive() { - return; + match event { + client::wl_keyboard::Event::Enter { + serial, + surface, + keys, + } => { + state.last_kb_serial = Some(serial); + self.server + .enter(serial, state.get_server_surface_from_client(surface), keys); + } + _ => simple_event_shunt! { + self.server, event: client::wl_keyboard::Event => [ + Keymap { + |format| convert_wenum(format), + |fd| fd.as_fd(), + size + }, + Leave { + serial, + |surface| { + if !surface.is_alive() { + return; + } + state.get_server_surface_from_client(surface) } - state.get_server_surface_from_client(surface) + }, + Key { + serial, + time, + key, + |state| convert_wenum(state) + }, + Modifiers { + serial, + mods_depressed, + mods_latched, + mods_locked, + group + }, + RepeatInfo { + rate, + delay } - }, - Key { - serial, - time, - key, - |state| convert_wenum(state) - }, - Modifiers { - serial, - mods_depressed, - mods_latched, - mods_locked, - group - }, - RepeatInfo { - rate, - delay - } - ] + ] + }, } } } diff --git a/src/server/mod.rs b/src/server/mod.rs index c1f2e72..ededef6 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -8,14 +8,21 @@ use self::event::*; use super::FromServerState; use crate::clientside::*; use crate::xstate::{Atoms, WindowDims, WmHints, WmName, WmNormalHints}; -use crate::XConnection; +use crate::{MimeTypeData, XConnection}; use log::{debug, warn}; use rustix::event::{poll, PollFd, PollFlags}; use slotmap::{new_key_type, HopSlotMap, SparseSecondaryMap}; +use smithay_client_toolkit::data_device_manager::{ + data_device::DataDevice, data_offer::SelectionOffer, data_source::CopyPasteSource, + DataDeviceManagerState, +}; use std::collections::HashMap; +use std::io::Read; +use std::io::Write; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::net::UnixStream; -use wayland_client::{protocol as client, Proxy}; +use std::rc::Rc; +use wayland_client::{globals::Global, protocol as client, Proxy}; use wayland_protocols::{ wp::{ linux_dmabuf::zv1::{client as c_dmabuf, server as s_dmabuf}, @@ -372,6 +379,39 @@ impl ObjectMapExt for ObjectMap { } } +fn handle_globals<'a, C: XConnection>( + dh: &DisplayHandle, + globals: impl IntoIterator, +) { + for global in globals { + macro_rules! server_global { + ($($global:ty),+) => { + match global.interface { + $( + ref x if x == <$global>::interface().name => { + dh.create_global::, $global, Global>(global.version, global.clone()); + } + )+ + _ => {} + } + } + } + + server_global![ + WlCompositor, + WlShm, + WlSeat, + WlOutput, + ZwpRelativePointerManagerV1, + WlDrmServer, + s_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + ZxdgOutputManagerV1, + s_vp::wp_viewporter::WpViewporter, + ZwpPointerConstraintsV1 + ]; + } +} + new_key_type! { pub struct ObjectKey; } @@ -383,42 +423,43 @@ pub struct ServerState { associated_windows: SparseSecondaryMap, windows: HashMap, - xdg_wm_base: XdgWmBase, qh: ClientQueueHandle, to_focus: Option, last_focused_toplevel: Option, connection: Option, -} -const XDG_WM_BASE_VERSION: u32 = 2; + xdg_wm_base: XdgWmBase, + clipboard_data: Option>, + last_kb_serial: Option, +} impl ServerState { pub fn new(dh: DisplayHandle, server_connection: Option) -> Self { - let mut clientside = ClientState::new(server_connection); + let clientside = ClientState::new(server_connection); let qh = clientside.qh.clone(); - let xdg_pos = clientside - .globals - .new_globals - .iter() - .position(|g| g.interface == XdgWmBase::interface().name) - .expect("Did not get an xdg_wm_base global"); - - let data = clientside.globals.new_globals.swap_remove(xdg_pos); - - assert!( - data.version >= XDG_WM_BASE_VERSION, - "xdg_wm_base older than version {XDG_WM_BASE_VERSION}" - ); - - let xdg_wm_base = - clientside - .registry - .bind::(data.name, XDG_WM_BASE_VERSION, &qh, ()); + let xdg_wm_base = clientside + .global_list + .bind::(&qh, 2..=6, ()) + .expect("Could not bind XdgWmBase"); + let manager = DataDeviceManagerState::bind(&clientside.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::>, + }); dh.create_global::(1, ()); + clientside + .global_list + .contents() + .with_list(|globals| handle_globals::(&dh, globals)); - let mut ret = Self { + Self { windows: HashMap::new(), clientside, atoms: None, @@ -430,9 +471,9 @@ impl ServerState { objects: Default::default(), associated_windows: Default::default(), xdg_wm_base, - }; - ret.handle_new_globals(); - ret + clipboard_data, + last_kb_serial: None, + } } pub fn clientside_fd(&self) -> BorrowedFd<'_> { @@ -451,33 +492,7 @@ impl ServerState { fn handle_new_globals(&mut self) { let globals = std::mem::take(&mut self.clientside.globals.new_globals); - for data in globals { - macro_rules! server_global { - ($($global:ty),+) => { - match data.interface { - $( - ref x if x == <$global>::interface().name => { - self.dh.create_global::(data.version, data); - } - )+ - _ => {} - } - } - } - - server_global![ - WlCompositor, - WlShm, - WlSeat, - WlOutput, - ZwpRelativePointerManagerV1, - WlDrmServer, - s_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, - ZxdgOutputManagerV1, - s_vp::wp_viewporter::WpViewporter, - ZwpPointerConstraintsV1 - ]; - } + handle_globals::(&self.dh, globals.iter()); } fn get_object_from_client_object(&self, proxy: &P) -> &T @@ -638,6 +653,24 @@ impl ServerState { let _ = self.windows.remove(&window); } + pub(crate) fn set_copy_paste_source(&mut self, mime_types: Rc>) { + if let Some(d) = &mut self.clipboard_data { + let src = d + .manager + .create_copy_paste_source(&self.qh, mime_types.iter().map(|m| m.name())); + let data = CopyPasteData::X11 { + inner: src, + data: mime_types, + }; + let CopyPasteData::X11 { inner, .. } = d.source.insert(data) else { + unreachable!(); + }; + if let Some(serial) = self.last_kb_serial.as_ref().copied() { + inner.set_selection(d.device.as_ref().unwrap(), serial); + } + } + } + pub fn run(&mut self) { if let Some(r) = self.clientside.queue.prepare_read() { let fd = r.connection_fd(); @@ -651,7 +684,6 @@ impl ServerState { .dispatch_pending(&mut self.clientside.globals) .unwrap(); self.handle_clientside_events(); - self.clientside.queue.flush().unwrap(); } pub fn handle_clientside_events(&mut self) { @@ -676,9 +708,52 @@ impl ServerState { } } + self.handle_clipboard_events(); self.clientside.queue.flush().unwrap(); } + 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) { + let globals = &mut self.clientside.globals; + + if let Some(clipboard) = self.clipboard_data.as_mut() { + for (mime_type, mut fd) in std::mem::take(&mut globals.selection_requests) { + let CopyPasteData::X11 { data, .. } = clipboard.source.as_ref().unwrap() else { + unreachable!() + }; + let pos = data.iter().position(|m| m.name() == mime_type).unwrap(); + if let Err(e) = fd.write_all(data[pos].data()) { + warn!("Failed to write selection data: {e:?}"); + } + } + + if clipboard.source.is_none() || globals.cancelled { + if globals.selection.take().is_some() { + let device = clipboard.device.as_ref().unwrap(); + let offer = device.data().selection_offer().unwrap(); + 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)); + } + globals.cancelled = false; + } + } + } + fn create_role_window(&mut self, window: x::Window, surface_key: ObjectKey) { let surface: &mut SurfaceData = self.objects[surface_key].as_mut(); surface.window = Some(window); @@ -837,3 +912,42 @@ 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.clientside.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: Rc>, + }, + Foreign(ForeignSelection), +} diff --git a/src/server/tests.rs b/src/server/tests.rs index dd2f164..c94db97 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -3,7 +3,7 @@ use crate::xstate::{SetState, WmName}; use paste::paste; use rustix::event::{poll, PollFd, PollFlags}; use std::collections::HashMap; -use std::os::fd::BorrowedFd; +use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::unix::net::UnixStream; use std::sync::{Arc, Mutex}; use wayland_client::{ @@ -12,8 +12,9 @@ use wayland_client::{ wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_display::WlDisplay, + wl_keyboard::WlKeyboard, wl_registry::WlRegistry, - wl_seat::WlSeat, + wl_seat::{self, WlSeat}, wl_shm::{Format, WlShm}, wl_shm_pool::WlShmPool, wl_surface::WlSurface, @@ -85,7 +86,8 @@ with_optional! { struct Compositor { compositor: TestObject, shm: TestObject, - shell: TestObject + shell: TestObject, + seat: TestObject } } @@ -140,7 +142,7 @@ impl FakeXConnection { fn window(&mut self, window: Window) -> &mut WindowData { self.windows .get_mut(&window) - .expect(&format!("Unknown window: {window:?}")) + .unwrap_or_else(|| panic!("Unknown window: {window:?}")) } } @@ -155,13 +157,22 @@ impl Default for FakeXConnection { } impl super::FromServerState for () { - fn create(_: &FakeServerState) -> Self { - () + fn create(_: &FakeServerState) -> Self {} +} + +impl crate::MimeTypeData for testwl::PasteData { + fn name(&self) -> &str { + &self.mime_type + } + + fn data(&self) -> &[u8] { + &self.data } } impl super::XConnection for FakeXConnection { type ExtraData = (); + type MimeTypeData = testwl::PasteData; fn root_window(&self) -> Window { self.root } @@ -270,12 +281,7 @@ impl TestFixture { self.run(); let events = std::mem::take(&mut *registry.data.events.lock().unwrap()); - assert!(events.len() > 0); - - let bind_req = |name, interface, version| Req::::Bind { - name, - id: (interface, version), - }; + assert!(!events.is_empty()); for event in events { if let Ev::::Global { @@ -284,23 +290,34 @@ impl TestFixture { version, } = event { + let bind_req = |interface| Req::::Bind { + name, + id: (interface, version), + }; + match interface { x if x == WlCompositor::interface().name => { ret.compositor = Some(TestObject::from_request( ®istry.obj, - bind_req(name, WlCompositor::interface(), version), + bind_req(WlCompositor::interface()), )); } x if x == WlShm::interface().name => { ret.shm = Some(TestObject::from_request( ®istry.obj, - bind_req(name, WlShm::interface(), version), + bind_req(WlShm::interface()), )); } x if x == XwaylandShellV1::interface().name => { ret.shell = Some(TestObject::from_request( ®istry.obj, - bind_req(name, XwaylandShellV1::interface(), version), + bind_req(XwaylandShellV1::interface()), + )); + } + x if x == WlSeat::interface().name => { + ret.seat = Some(TestObject::from_request( + ®istry.obj, + bind_req(WlSeat::interface()), )); } _ => {} @@ -500,7 +517,7 @@ impl TestFixture { }; let dims = data.dims; self.new_window(window, true, data, None); - self.map_window(&comp, window, &surface.obj, &buffer); + self.map_window(comp, window, &surface.obj, &buffer); self.run(); let popup_id = self.check_new_surface(); @@ -734,7 +751,7 @@ fn pass_through_globals() { TestObject::::from_request(&display, Req::::GetRegistry {}); f.run(); let events = std::mem::take(&mut *registry.data.events.lock().unwrap()); - assert!(events.len() > 0); + assert!(!events.is_empty()); for event in events { let Ev::::Global { interface, .. } = event else { unreachable!(); @@ -969,6 +986,138 @@ fn window_group_properties() { assert_eq!(data.toplevel().title, Some("window".into())); assert_eq!(data.toplevel().app_id, Some("class".into())); } + +#[test] +fn copy_from_x11() { + let (mut f, comp) = TestFixture::new_with_compositor(); + TestObject::::from_request(&comp.seat.obj, wl_seat::Request::GetKeyboard {}); + let win = unsafe { Window::new(1) }; + let (_surface, _id) = f.create_toplevel(&comp, win); + + let mimes = std::rc::Rc::new(vec![ + testwl::PasteData { + mime_type: "text".to_string(), + data: b"abc".to_vec(), + }, + testwl::PasteData { + mime_type: "data".to_string(), + data: vec![1, 2, 3, 4, 6, 10], + }, + ]); + + f.exwayland.set_copy_paste_source(mimes.clone()); + f.run(); + + let server_mimes = f.testwl.data_source_mimes(); + for mime in mimes.iter() { + assert!(server_mimes.contains(&mime.mime_type)); + } + + let data = f.testwl.paste_data(); + f.run(); + let data = data.resolve(); + assert_eq!(*mimes, data); +} + +#[test] +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) }; + let (_surface, _id) = f.create_toplevel(&comp, win); + + let mimes = vec![ + testwl::PasteData { + mime_type: "text".to_string(), + data: b"abc".to_vec(), + }, + testwl::PasteData { + mime_type: "data".to_string(), + data: vec![1, 2, 3, 4, 6, 10], + }, + ]; + f.testwl.create_data_offer(mimes.clone()); + f.run(); + + let selection = f.exwayland.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 + s.spawn(|| { + let pollfd = unsafe { BorrowedFd::borrow_raw(f.testwl.poll_fd().as_raw_fd()) }; + let mut pollfd = [PollFd::from_borrowed_fd(pollfd, PollFlags::IN)]; + if poll(&mut pollfd, 100).unwrap() == 0 { + panic!("Did not get events for testwl!"); + } + f.testwl.dispatch(); + while poll(&mut pollfd, 100).unwrap() > 0 { + f.testwl.dispatch(); + } + }); + selection.receive(mime.mime_type.clone(), &f.exwayland) + }); + f.run(); + assert_eq!(data, mime.data); + } +} + +#[test] +fn clipboard_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) }; + let (_surface, _id) = f.create_toplevel(&comp, win); + + let x11data = std::rc::Rc::new(vec![ + testwl::PasteData { + mime_type: "text".to_string(), + data: b"abc".to_vec(), + }, + testwl::PasteData { + mime_type: "data".to_string(), + data: vec![1, 2, 3, 4, 6, 10], + }, + ]); + + f.exwayland.set_copy_paste_source(x11data.clone()); + f.run(); + + let waylanddata = vec![ + testwl::PasteData { + mime_type: "asdf".to_string(), + data: b"fdaa".to_vec(), + }, + testwl::PasteData { + mime_type: "boing".to_string(), + data: vec![10, 20, 40, 50], + }, + ]; + f.testwl.create_data_offer(waylanddata.clone()); + f.run(); + f.run(); + + let selection = f.exwayland.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 + s.spawn(|| { + let pollfd = unsafe { BorrowedFd::borrow_raw(f.testwl.poll_fd().as_raw_fd()) }; + let mut pollfd = [PollFd::from_borrowed_fd(pollfd, PollFlags::IN)]; + if poll(&mut pollfd, 100).unwrap() == 0 { + panic!("Did not get events for testwl!"); + } + f.testwl.dispatch(); + while poll(&mut pollfd, 100).unwrap() > 0 { + f.testwl.dispatch(); + } + }); + selection.receive(mime.mime_type.clone(), &f.exwayland) + }); + f.run(); + assert_eq!(data, mime.data); + } +} + /// See Pointer::handle_event for an explanation. #[test] fn popup_pointer_motion_workaround() {} diff --git a/src/xstate.rs b/src/xstate/mod.rs similarity index 95% rename from src/xstate.rs rename to src/xstate/mod.rs index 5526f2e..376b4e0 100644 --- a/src/xstate.rs +++ b/src/xstate/mod.rs @@ -1,3 +1,6 @@ +mod selection; +use selection::{SelectionData, SelectionTarget}; + use crate::server::WindowAttributes; use bitflags::bitflags; use log::{debug, trace, warn}; @@ -7,12 +10,6 @@ use std::sync::Arc; use xcb::{x, Xid, XidNew}; use xcb_util_cursor::{Cursor, CursorContext}; -pub struct XState { - pub connection: Arc, - root: x::Window, - pub atoms: Atoms, -} - // Sometimes we'll get events on windows that have already been destroyed #[derive(Debug)] enum MaybeBadWindow { @@ -105,6 +102,14 @@ impl WmName { } } +pub struct XState { + pub connection: Arc, + pub atoms: Atoms, + root: x::Window, + wm_window: x::Window, + selection_data: SelectionData, +} + impl XState { pub fn new(fd: BorrowedFd) -> Self { let connection = Arc::new(xcb::Connection::connect_to_fd(fd.as_raw_fd(), None).unwrap()); @@ -144,10 +149,14 @@ impl XState { }) .unwrap(); + let wm_window = connection.generate_id(); + let mut r = Self { connection, + wm_window, root, atoms, + selection_data: Default::default(), }; r.create_ewmh_window(); r @@ -166,11 +175,10 @@ impl XState { } fn create_ewmh_window(&mut self) { - let window = self.connection.generate_id(); self.connection .send_and_check_request(&x::CreateWindow { depth: 0, - wid: window, + wid: self.wm_window, parent: self.root, x: 0, y: 0, @@ -183,29 +191,31 @@ impl XState { }) .unwrap(); - self.set_root_property(self.atoms.wm_check, x::ATOM_WINDOW, &[window]); + self.set_root_property(self.atoms.wm_check, x::ATOM_WINDOW, &[self.wm_window]); self.set_root_property(self.atoms.active_win, x::ATOM_WINDOW, &[x::Window::none()]); self.set_root_property(self.atoms.supported, x::ATOM_ATOM, &[self.atoms.active_win]); self.connection .send_and_check_request(&x::ChangeProperty { mode: x::PropMode::Replace, - window, + window: self.wm_window, property: self.atoms.wm_check, r#type: x::ATOM_WINDOW, - data: &[window], + data: &[self.wm_window], }) .unwrap(); self.connection .send_and_check_request(&x::ChangeProperty { mode: x::PropMode::Replace, - window, + window: self.wm_window, property: self.atoms.net_wm_name, r#type: x::ATOM_STRING, data: b"exwayland wm", }) .unwrap(); + + self.set_clipboard_owner(x::CURRENT_TIME); } pub fn handle_events(&mut self, server_state: &mut super::RealServerState) { @@ -225,6 +235,11 @@ impl XState { } while let Some(event) = self.connection.poll_for_event().unwrap() { trace!("x11 event: {event:?}"); + + if self.handle_selection_event(&event, server_state) { + continue; + } + match event { xcb::Event::X(x::Event::CreateNotify(e)) => { debug!("new window: {:?}", e); @@ -383,6 +398,14 @@ impl XState { } } + fn get_atom_name(&self, atom: x::Atom) -> String { + self.connection + .wait_for_reply(self.connection.send_request(&x::GetAtomName { atom })) + .unwrap() + .name() + .to_string() + } + fn get_window_attributes(&self, window: x::Window) -> XResult { let geometry = self.connection.send_request(&x::GetGeometry { drawable: x::Drawable::Window(window), @@ -564,7 +587,7 @@ impl XState { server_state: &mut super::RealServerState, ) { if event.state() != x::Property::NewValue { - println!("ignoring non newvalue for property {:?}", event.atom()); + debug!("ignoring non newvalue for property {:?}", event.atom()); return; } @@ -597,15 +620,11 @@ impl XState { } _ => { if log::log_enabled!(log::Level::Debug) { - let prop = self - .connection - .wait_for_reply( - self.connection - .send_request(&x::GetAtomName { atom: event.atom() }), - ) - .unwrap(); - - debug!("changed property {:?} for {:?}", prop.name(), window); + debug!( + "changed property {:?} for {:?}", + self.get_atom_name(event.atom()), + window + ); } } } @@ -629,6 +648,11 @@ xcb::atoms_struct! { pub client_list => b"_NET_CLIENT_LIST" only_if_exists = false, pub supported => b"_NET_SUPPORTED" only_if_exists = false, pub utf8_string => b"UTF8_STRING" only_if_exists = false, + pub clipboard => b"CLIPBOARD" only_if_exists = false, + pub targets => b"TARGETS" only_if_exists = false, + pub multiple => b"MULTIPLE" only_if_exists = false, + pub timestamp => b"TIMESTAMP" only_if_exists = false, + pub selection_reply => b"_selection_reply" only_if_exists = false, } } @@ -746,6 +770,7 @@ impl TryFrom for SetState { impl super::XConnection for Arc { type ExtraData = Atoms; + type MimeTypeData = SelectionTarget; fn root_window(&self) -> x::Window { self.get_setup().roots().next().unwrap().root() diff --git a/src/xstate/selection.rs b/src/xstate/selection.rs new file mode 100644 index 0000000..cff6704 --- /dev/null +++ b/src/xstate/selection.rs @@ -0,0 +1,387 @@ +use super::XState; +use crate::server::ForeignSelection; +use crate::{MimeTypeData, RealServerState}; +use log::{debug, warn}; +use std::rc::Rc; +use xcb::x; + +enum TargetValue { + U8(Vec), + U16(Vec), + U32(Vec), + Foreign, +} + +pub struct SelectionTarget { + name: String, + atom: x::Atom, + value: Option, +} + +impl MimeTypeData for SelectionTarget { + fn name(&self) -> &str { + &self.name + } + + fn data(&self) -> &[u8] { + match self.value.as_ref() { + Some(TargetValue::U8(v)) => v, + other => { + if let Some(other) = other { + warn!( + "Unexpectedly requesting data from mime type with data type {} - nothing will be copied", + std::any::type_name_of_val(other) + ); + } + &[] + } + } + } +} + +#[derive(Default)] +pub(crate) struct SelectionData { + clear_time: Option, + mime_types: Rc>, + /// List of property on self.wm_window and corresponding index in mime_types + mime_destinations: Vec<(x::Atom, usize)>, + foreign_data: Option, +} + +impl XState { + pub(crate) fn set_clipboard_owner(&mut self, time: u32) { + self.connection + .send_and_check_request(&x::SetSelectionOwner { + owner: self.wm_window, + selection: self.atoms.clipboard, + time, + }) + .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) { + let types = selection + .mime_types + .iter() + .map(|mime| { + let atom = self + .connection + .wait_for_reply(self.connection.send_request(&x::InternAtom { + only_if_exists: false, + name: mime.as_bytes(), + })) + .unwrap(); + + SelectionTarget { + name: mime.clone(), + atom: atom.atom(), + value: Some(TargetValue::Foreign), + } + }) + .collect(); + + self.selection_data.mime_types = Rc::new(types); + self.selection_data.foreign_data = Some(selection); + } + + pub(crate) fn handle_selection_event( + &mut self, + event: &xcb::Event, + server_state: &mut RealServerState, + ) -> bool { + match event { + xcb::Event::X(x::Event::SelectionClear(e)) => { + if e.selection() != self.atoms.clipboard { + warn!( + "Got SelectionClear for unexpected atom {}, ignoring", + self.get_atom_name(e.selection()) + ); + return true; + } + + // get the mime types + 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: e.time(), + }) + .unwrap(); + + self.selection_data.clear_time = Some(e.time()); + } + xcb::Event::X(x::Event::SelectionNotify(e)) => { + if e.property() == x::ATOM_NONE { + warn!("selection notify fail?"); + return true; + } + + match e.target() { + x if x == self.atoms.targets => self.handle_target_list(e.property()), + x if x == self.atoms.multiple => self.handle_new_clipboard_data(server_state), + atom => { + warn!( + "unexpected SelectionNotify type: {}", + self.get_atom_name(atom) + ) + } + } + } + xcb::Event::X(x::Event::SelectionRequest(e)) => { + let send_notify = |property| { + self.connection + .send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(e.requestor()), + event_mask: x::EventMask::empty(), + event: &x::SelectionNotifyEvent::new( + e.time(), + e.requestor(), + e.selection(), + e.target(), + property, + ), + }) + .unwrap(); + }; + let refuse = || send_notify(x::ATOM_NONE); + let success = || send_notify(e.property()); + + if log::log_enabled!(log::Level::Debug) { + let target = self.get_atom_name(e.target()); + debug!("Got selection request for target {target}"); + } + + if e.property() == x::ATOM_NONE { + debug!("refusing - property is set to none"); + refuse(); + return true; + } + + match e.target() { + x if x == self.atoms.targets => { + let atoms: Box<[x::Atom]> = self + .selection_data + .mime_types + .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) = self + .selection_data + .mime_types + .iter() + .find(|t| t.atom == other) + else { + debug!("refusing selection requst because given atom could not be found ({other:?})"); + refuse(); + return true; + }; + + macro_rules! set_property { + ($data:expr) => { + self.connection + .send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: e.requestor(), + property: e.property(), + r#type: target.atom, + data: $data, + }) + .unwrap() + }; + } + + match target.value.as_ref().unwrap() { + TargetValue::U8(v) => set_property!(v), + TargetValue::U16(v) => set_property!(v), + TargetValue::U32(v) => set_property!(v), + TargetValue::Foreign => { + let data = self + .selection_data + .foreign_data + .as_ref() + .unwrap() + .receive(target.name.clone(), server_state); + set_property!(&data); + } + } + + success(); + } + } + } + _ => return false, + } + + true + } + + fn handle_target_list(&mut self, dest_property: x::Atom) { + 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(); + let target_props: Box<[x::Atom]> = targets + .iter() + .copied() + .filter(|atom| ![self.atoms.targets, self.atoms.multiple].contains(atom)) + .enumerate() + .flat_map(|(idx, target)| { + let name = [b"dest", idx.to_string().as_bytes()].concat(); + let reply = self + .connection + .wait_for_reply(self.connection.send_request(&x::InternAtom { + name: &name, + only_if_exists: false, + })) + .unwrap(); + let dest = reply.atom(); + + [target, dest] + }) + .collect(); + + self.connection + .send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: self.wm_window, + property: self.atoms.selection_reply, + r#type: x::ATOM_ATOM, + data: &target_props, + }) + .unwrap(); + + self.connection + .send_and_check_request(&x::ConvertSelection { + requestor: self.wm_window, + selection: self.atoms.clipboard, + target: self.atoms.multiple, + property: self.atoms.selection_reply, + time: self.selection_data.clear_time.as_ref().copied().unwrap(), + }) + .unwrap(); + + let (types, dests) = target_props + .chunks_exact(2) + .enumerate() + .map(|(idx, atoms)| { + let [target, property] = atoms.try_into().unwrap(); + let name = self + .connection + .wait_for_reply( + self.connection + .send_request(&x::GetAtomName { atom: target }), + ) + .unwrap(); + let name = name.name().to_string(); + let target = SelectionTarget { + atom: target, + name, + value: None, + }; + let dest = (property, idx); + (target, dest) + }) + .unzip(); + + self.selection_data.mime_types = Rc::new(types); + self.selection_data.mime_destinations = dests; + } + + fn handle_new_clipboard_data(&mut self, server_state: &mut RealServerState) { + for (property, idx) in std::mem::take(&mut self.selection_data.mime_destinations) { + let types = Rc::get_mut(&mut self.selection_data.mime_types).unwrap(); + let target = &mut types[idx]; + let data = { + if target.atom == self.atoms.timestamp { + TargetValue::U32(vec![self + .selection_data + .clear_time + .as_ref() + .copied() + .unwrap()]) + } else { + let reply = self + .connection + .wait_for_reply(self.connection.send_request(&x::GetProperty { + delete: true, + window: self.wm_window, + property, + r#type: x::ATOM_ANY, + long_offset: 0, + long_length: u32::MAX, + })) + .unwrap(); + + match reply.format() { + 8 => TargetValue::U8(reply.value().to_vec()), + 16 => TargetValue::U16(reply.value().to_vec()), + 32 => TargetValue::U32(reply.value().to_vec()), + other => { + let atom = target.atom; + let target = self.get_atom_name(atom); + let ty = if reply.r#type() == x::ATOM_NONE { + "None".to_string() + } else { + self.get_atom_name(reply.r#type()) + }; + warn!("unexpected format: {other} (atom: {target}, type: {ty:?}, property: {property:?}) - copies as this type will fail!"); + continue; + } + } + } + }; + + target.value = Some(data); + } + + self.connection + .send_and_check_request(&x::DeleteProperty { + window: self.wm_window, + property: self.atoms.selection_reply, + }) + .unwrap(); + + self.set_clipboard_owner(self.selection_data.clear_time.unwrap()); + server_state.set_copy_paste_source(Rc::clone(&self.selection_data.mime_types)); + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 899d9a6..07c7c20 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -228,6 +228,12 @@ xcb::atoms_struct! { struct Atoms { wm_protocols => b"WM_PROTOCOLS", wm_delete_window => b"WM_DELETE_WINDOW", + clipboard => b"CLIPBOARD", + targets => b"TARGETS", + multiple => b"MULTIPLE", + wm_check => b"_NET_SUPPORTING_WM_CHECK", + mime1 => b"text/plain" only_if_exists = false, + mime2 => b"blah/blah" only_if_exists = false, } } @@ -307,6 +313,7 @@ impl Connection { .unwrap(); } + #[track_caller] fn set_property( &self, window: x::Window, @@ -331,6 +338,17 @@ impl Connection { "Did not get any X11 events" ); } + + #[track_caller] + fn get_reply( + &self, + req: &R, + ) -> ::Reply + where + R::Cookie: xcb::CookieWithReplyChecked, + { + self.wait_for_reply(self.send_request(req)).unwrap() + } } #[test] @@ -364,7 +382,6 @@ fn toplevel_flow() { x::ATOM_WM_NORMAL_HINTS, &[flags, 0, 0, 0, 0, 50, 100, 300, 400], ); - println!("set title: window"); connection.set_property( window, x::ATOM_STRING, @@ -579,3 +596,277 @@ fn quick_delete() { assert_eq!(f.testwl.get_surface_data(surf), None); } + +// aaaaaaaaaa +#[test] +fn copy_from_x11() { + let mut f = Fixture::new(); + let mut connection = Connection::new(&f.display); + + let window = connection.new_window(connection.root, 0, 0, 20, 20, false); + connection.map_window(window); + f.wait_and_dispatch(); + let surface = f + .testwl + .last_created_surface_id() + .expect("No surface created"); + f.configure_and_verify_new_toplevel(&mut connection, window, surface); + + // set data + connection + .send_and_check_request(&x::SetSelectionOwner { + owner: window, + selection: connection.atoms.clipboard, + time: x::CURRENT_TIME, + }) + .unwrap(); + let owner = connection + .wait_for_reply(connection.send_request(&x::GetSelectionOwner { + selection: connection.atoms.clipboard, + })) + .unwrap(); + assert_eq!(window, owner.owner()); + + // wait for request to come through + std::thread::sleep(std::time::Duration::from_millis(100)); + let request = match connection.poll_for_event().unwrap() { + Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r, + other => panic!("Didn't get selection request event, instead got {other:?}"), + }; + + assert_eq!(request.target(), connection.atoms.targets); + connection.set_property( + request.requestor(), + x::ATOM_ATOM, + request.property(), + &[connection.atoms.mime1, connection.atoms.mime2], + ); + connection + .send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(request.requestor()), + event_mask: x::EventMask::empty(), + event: &x::SelectionNotifyEvent::new( + request.time(), + request.requestor(), + request.selection(), + request.target(), + request.property(), + ), + }) + .unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(100)); + let request = match connection.poll_for_event().unwrap() { + Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r, + other => panic!("Didn't get selection request event, instead got {other:?}"), + }; + + assert_eq!(request.target(), connection.atoms.multiple); + let pairs = connection + .wait_for_reply(connection.send_request(&x::GetProperty { + delete: true, + window: request.requestor(), + property: request.property(), + r#type: x::ATOM_ATOM, + long_offset: 0, + long_length: 4, + })) + .unwrap(); + + let pairs: &[x::Atom] = pairs.value(); + assert_eq!(pairs.len(), 4); + assert!(pairs.contains(&connection.atoms.mime1)); + assert!(pairs.contains(&connection.atoms.mime2)); + + let mime1data = b"hello world"; + let mime2data = &[1u8, 2, 3, 4]; + for [target, property] in pairs + .chunks_exact(2) + .map(|pair| <[x::Atom; 2]>::try_from(pair).unwrap()) + { + match target { + x if x == connection.atoms.mime1 => { + connection.set_property(request.requestor(), x::ATOM_STRING, property, mime1data); + } + x if x == connection.atoms.mime2 => { + connection.set_property(request.requestor(), x::ATOM_INTEGER, property, mime2data); + } + _ => panic!("unexpected target: {target:?}"), + } + } + + connection + .send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(request.requestor()), + event_mask: x::EventMask::empty(), + event: &x::SelectionNotifyEvent::new( + request.time(), + request.requestor(), + request.selection(), + request.target(), + request.property(), + ), + }) + .unwrap(); + + f.wait_and_dispatch(); + + let owner = connection + .wait_for_reply(connection.send_request(&x::GetSelectionOwner { + selection: connection.atoms.clipboard, + })) + .unwrap(); + assert_ne!(window, owner.owner()); + + let mimes = f.testwl.data_source_mimes(); + assert!(mimes.contains(&"text/plain".into())); // mime1 + assert!(mimes.contains(&"blah/blah".into())); // mime2 + + let data = f.testwl.paste_data(); + f.testwl.dispatch(); + let data = data.resolve(); + for testwl::PasteData { mime_type, data } in data { + match mime_type { + x if x == "text/plain" => { + assert_eq!(&data, mime1data); + } + x if x == "blah/blah" => { + assert_eq!(&data, mime2data); + } + other => panic!("unexpected mime type: {other} ({data:?})"), + } + } +} + +#[test] +fn copy_from_wayland() { + let mut f = Fixture::new(); + let mut connection = Connection::new(&f.display); + + let window = connection.new_window(connection.root, 0, 0, 20, 20, false); + connection.map_window(window); + f.wait_and_dispatch(); + let surface = f + .testwl + .last_created_surface_id() + .expect("No surface created"); + f.configure_and_verify_new_toplevel(&mut connection, window, surface); + let offer = vec![ + testwl::PasteData { + mime_type: "text/plain".into(), + data: b"boingloings".to_vec(), + }, + testwl::PasteData { + mime_type: "yah/hah".into(), + data: vec![1, 2, 3, 2, 1], + }, + ]; + + f.testwl.create_data_offer(offer.clone()); + + let wm_window: x::Window = connection + .get_reply(&x::GetProperty { + delete: false, + window: connection.root, + property: connection.atoms.wm_check, + r#type: x::ATOM_WINDOW, + long_offset: 0, + long_length: 1, + }) + .value()[0]; + + let reply = connection.get_reply(&x::GetSelectionOwner { + selection: connection.atoms.clipboard, + }); + assert_eq!(reply.owner(), wm_window); + let dest1_atom = connection + .get_reply(&x::InternAtom { + name: b"dest1", + only_if_exists: false, + }) + .atom(); + + // I don't know why, but omitting this little sleep prevents the SelectionRequest notification + // from being sent, and I don't have the heart to determine why. + std::thread::sleep(std::time::Duration::from_millis(1)); + connection + .send_and_check_request(&x::ConvertSelection { + requestor: window, + selection: connection.atoms.clipboard, + target: connection.atoms.targets, + property: dest1_atom, + time: x::CURRENT_TIME, + }) + .unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(50)); + let request = match connection.poll_for_event().unwrap() { + Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r, + other => panic!("Didn't get selection notify event, instead got {other:?}"), + }; + + assert_eq!(request.requestor(), window); + assert_eq!(request.selection(), connection.atoms.clipboard); + assert_eq!(request.target(), connection.atoms.targets); + assert_eq!(request.property(), dest1_atom); + + let reply = connection.get_reply(&x::GetProperty { + delete: true, + window, + property: dest1_atom, + r#type: x::ATOM_ATOM, + long_offset: 0, + long_length: 10, + }); + let targets: &[x::Atom] = reply.value(); + assert_eq!(targets.len(), 2); + + for testwl::PasteData { mime_type, data } in offer { + let atom = connection + .get_reply(&x::InternAtom { + only_if_exists: true, + name: mime_type.as_bytes(), + }) + .atom(); + assert_ne!(atom, x::ATOM_NONE); + assert!(targets.contains(&atom)); + + std::thread::sleep(std::time::Duration::from_millis(50)); + connection + .send_and_check_request(&x::ConvertSelection { + requestor: window, + selection: connection.atoms.clipboard, + target: atom, + property: dest1_atom, + time: x::CURRENT_TIME, + }) + .unwrap(); + + f.wait_and_dispatch(); + let request = match connection.poll_for_event().unwrap() { + Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r, + other => panic!("Didn't get selection notify event, instead got {other:?}"), + }; + + assert_eq!(request.requestor(), window); + assert_eq!(request.selection(), connection.atoms.clipboard); + assert_eq!(request.target(), atom); + assert_eq!(request.property(), dest1_atom); + + let val: Vec = connection + .get_reply(&x::GetProperty { + delete: true, + window, + property: dest1_atom, + r#type: x::ATOM_ANY, + long_offset: 0, + long_length: 10, + }) + .value() + .to_vec(); + + assert_eq!(val, data); + } +} diff --git a/testwl/Cargo.toml b/testwl/Cargo.toml index eee00de..2ffa563 100644 --- a/testwl/Cargo.toml +++ b/testwl/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" wayland-protocols = { workspace = true, features = ["server", "unstable"] } wayland-server.workspace = true wl_drm = { path = "../wl_drm" } +rustix = { workspace = true, features = ["pipe"] } diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 92fb466..6601d66 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -1,6 +1,9 @@ use std::collections::{hash_map, HashMap, HashSet}; -use std::os::fd::BorrowedFd; +use std::io::Read; +use std::io::Write; +use std::os::fd::{AsFd, BorrowedFd, OwnedFd}; use std::os::unix::net::UnixStream; +use std::sync::Mutex; use std::time::Instant; use wayland_protocols::{ wp::{ @@ -27,6 +30,11 @@ use wayland_server::{ wl_buffer::WlBuffer, wl_callback::WlCallback, wl_compositor::WlCompositor, + wl_data_device::{self, WlDataDevice}, + wl_data_device_manager::{self, WlDataDeviceManager}, + wl_data_offer::{self, WlDataOffer}, + wl_data_source::{self, WlDataSource}, + wl_keyboard::{self, WlKeyboard}, wl_output::WlOutput, wl_pointer::{self, WlPointer}, wl_seat::{self, WlSeat}, @@ -52,6 +60,7 @@ pub struct SurfaceData { pub buffer: Option, pub last_damage: Option, pub role: Option, + pub last_enter_serial: Option, } impl SurfaceData { @@ -136,6 +145,11 @@ pub struct SurfaceId(u32); #[derive(Hash, Clone, Copy, Eq, PartialEq)] struct PositionerId(u32); +#[derive(Default)] +struct DataSourceData { + mimes: Vec, +} + struct State { surfaces: HashMap, positioners: HashMap, @@ -144,7 +158,30 @@ struct State { last_surface_id: Option, callbacks: Vec, pointer: Option, + keyboard: Option, configure_serial: u32, + selection: Option, + data_device_man: Option, + data_device: Option, +} + +impl Default for State { + fn default() -> Self { + Self { + surfaces: Default::default(), + buffers: Default::default(), + positioners: Default::default(), + begin: Instant::now(), + last_surface_id: None, + callbacks: Vec::new(), + pointer: None, + keyboard: None, + configure_serial: 0, + selection: None, + data_device_man: None, + data_device: None, + } + } } impl State { @@ -157,9 +194,18 @@ impl State { states: Vec, ) { let last_serial = self.configure_serial; + if states.contains(&xdg_toplevel::State::Activated) { + if let Some(kb) = &self.keyboard { + kb.enter( + last_serial, + &self.surfaces[&surface_id].surface, + Vec::default(), + ); + } + } let toplevel = self.get_toplevel(surface_id); toplevel.states = states.clone(); - let states = states + let states: Vec = states .into_iter() .map(|state| u32::from(state) as u8) .collect(); @@ -181,21 +227,6 @@ impl State { } } -impl Default for State { - fn default() -> Self { - Self { - surfaces: Default::default(), - buffers: Default::default(), - positioners: Default::default(), - begin: Instant::now(), - last_surface_id: None, - callbacks: Vec::new(), - pointer: None, - configure_serial: 0, - } - } -} - macro_rules! simple_global_dispatch { ($type:ty) => { impl GlobalDispatch<$type, ()> for State { @@ -251,6 +282,7 @@ impl Server { dh.create_global::(1, ()); dh.create_global::(6, ()); dh.create_global::(5, ()); + dh.create_global::(3, ()); global_noop!(WlOutput); global_noop!(ZwpLinuxDmabufV1); global_noop!(ZwpRelativePointerManagerV1); @@ -345,12 +377,210 @@ impl Server { pub fn pointer(&self) -> &WlPointer { self.state.pointer.as_ref().unwrap() } + + pub fn data_source_mimes(&self) -> Vec { + let Some(selection) = &self.state.selection else { + panic!("No selection set on data device"); + }; + + let data: &Mutex = selection.data().unwrap(); + let data = data.lock().unwrap(); + data.mimes.to_vec() + } + + pub fn paste_data(&mut self) -> PasteDataResolver { + let Some(selection) = &self.state.selection else { + panic!("No selection set on data device"); + }; + + let ret = PasteDataResolver::new(&selection); + self.display.flush_clients().unwrap(); + ret + } + + pub fn data_source_exists(&self) -> bool { + self.state.selection.is_none() + } + + pub fn create_data_offer(&mut self, data: Vec) { + let Some(dev) = &self.state.data_device else { + panic!("No data device created"); + }; + + if let Some(selection) = self.state.selection.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, 3, data) + .unwrap(); + dev.data_offer(&offer); + for mime in mimes { + offer.offer(mime); + } + dev.selection(Some(&offer)); + self.display.flush_clients().unwrap(); + } +} + +pub struct PasteDataResolver { + fds: Vec<(String, OwnedFd, OwnedFd)>, +} + +impl PasteDataResolver { + fn new(source: &WlDataSource) -> Self { + let data: &Mutex = source.data().unwrap(); + let data = data.lock().unwrap(); + let mimes = &data.mimes; + + let fds = mimes + .iter() + .map(|mime| { + let (rx, tx) = rustix::pipe::pipe().unwrap(); + source.send(mime.clone(), tx.as_fd()); + (mime.clone(), tx, rx) + }) + .collect(); + + PasteDataResolver { fds } + } + + pub fn resolve(self) -> Vec { + self.fds + .into_iter() + .map(|(mime, tx, rx)| { + drop(tx); + let mut data = Vec::new(); + let mut file = std::fs::File::from(rx); + file.read_to_end(&mut data).unwrap(); + PasteData { + mime_type: mime, + data, + } + }) + .collect() + } +} + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct PasteData { + pub mime_type: String, + pub data: Vec, } simple_global_dispatch!(WlShm); simple_global_dispatch!(WlCompositor); simple_global_dispatch!(XdgWmBase); +impl GlobalDispatch for State { + fn bind( + state: &mut Self, + _: &DisplayHandle, + _: &Client, + resource: wayland_server::New, + _: &(), + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + state.data_device_man = Some(data_init.init(resource, ())); + } +} + +impl Dispatch> for State { + fn request( + _: &mut Self, + _: &Client, + _: &WlDataOffer, + request: ::Request, + data: &Vec, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wl_data_offer::Request::Receive { mime_type, fd } => { + let pos = data + .iter() + .position(|data| data.mime_type == mime_type) + .expect("Invalid mime type: {mime_type}"); + + let mut stream = UnixStream::from(fd); + stream.write_all(&data[pos].data).unwrap(); + } + other => todo!("unhandled request: {other:?}"), + } + } +} + +impl Dispatch> for State { + fn request( + state: &mut Self, + _: &Client, + _: &WlDataSource, + request: ::Request, + data: &Mutex, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + let mut data = data.lock().unwrap(); + match request { + wl_data_source::Request::Offer { mime_type } => { + data.mimes.push(mime_type); + } + wl_data_source::Request::Destroy => { + state.selection = None; + } + other => todo!("unhandled request {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &WlDataDevice, + request: ::Request, + _: &WlSeat, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wl_data_device::Request::SetSelection { source, .. } => { + state.selection = source; + } + wl_data_device::Request::Release => { + state.data_device = None; + } + other => todo!("unhandled request {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &WlDataDeviceManager, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wl_data_device_manager::Request::CreateDataSource { id } => { + data_init.init(id, DataSourceData::default().into()); + } + wl_data_device_manager::Request::GetDataDevice { id, seat } => { + state.data_device = Some(data_init.init(id, seat)); + } + other => todo!("unhandled request: {other:?}"), + } + } +} + impl GlobalDispatch for State { fn bind( _: &mut Self, @@ -361,7 +591,7 @@ impl GlobalDispatch for State { data_init: &mut wayland_server::DataInit<'_, Self>, ) { let seat = data_init.init(resource, ()); - seat.capabilities(wl_seat::Capability::Pointer); + seat.capabilities(wl_seat::Capability::Pointer | wl_seat::Capability::Keyboard); } } @@ -379,6 +609,9 @@ impl Dispatch for State { wl_seat::Request::GetPointer { id } => { state.pointer = Some(data_init.init(id, ())); } + wl_seat::Request::GetKeyboard { id } => { + state.keyboard = Some(data_init.init(id, ())); + } wl_seat::Request::Release => {} other => todo!("unhandled request {other:?}"), } @@ -417,6 +650,23 @@ impl Dispatch for State { } } +impl Dispatch for State { + fn request( + _: &mut Self, + _: &Client, + _: &WlKeyboard, + request: ::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wl_keyboard::Request::Release => {} + other => todo!("unhandled request {other:?}"), + } + } +} + impl Dispatch for State { fn request( _: &mut Self, @@ -787,6 +1037,7 @@ impl Dispatch for State { buffer: None, last_damage: None, role: None, + last_enter_serial: None, }, ); state.last_surface_id = Some(SurfaceId(id));