Sync clipboard between X11 and Wayland

You would not believe how much work this was.
Closes #23
This commit is contained in:
Shawn Wallace 2024-06-06 22:41:19 -04:00
parent 601223d3ae
commit 5e7f2df05e
14 changed files with 1703 additions and 189 deletions

View file

@ -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<T: Proxy> Default for ClientGlobalWrapper<T> {
macro_rules! global_dispatch_no_events {
($server:ty, $client:ty) => {
impl<C: XConnection> GlobalDispatch<$server, GlobalData> for ServerState<C>
impl<C: XConnection> GlobalDispatch<$server, Global> for ServerState<C>
where
ServerState<C>: 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<C: XConnection> GlobalDispatch<$server, GlobalData> for ServerState<C>
impl<C: XConnection> GlobalDispatch<$server, Global> for ServerState<C>
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<C: XConnection> GlobalDispatch<WlSeat, Global> for ServerState<C>
where
WlSeat: Resource,
client::wl_seat::WlSeat: Proxy,
ServerState<C>: Dispatch<WlSeat, ObjectKey>,
Globals: wayland_client::Dispatch<client::wl_seat::WlSeat, ObjectKey>,
GenericObject<WlSeat, client::wl_seat::WlSeat>: Into<Object>,
{
fn bind(
state: &mut Self,
_: &DisplayHandle,
_: &wayland_server::Client,
resource: wayland_server::New<WlSeat>,
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::<client::wl_seat::WlSeat, _, _>(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);

View file

@ -493,45 +493,51 @@ impl HandleEvent for Keyboard {
type Event = client::wl_keyboard::Event;
fn handle_event<C: XConnection>(&mut self, event: Self::Event, state: &mut ServerState<C>) {
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
}
]
]
},
}
}
}

View file

@ -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<Item = &'a Global>,
) {
for global in globals {
macro_rules! server_global {
($($global:ty),+) => {
match global.interface {
$(
ref x if x == <$global>::interface().name => {
dh.create_global::<ServerState<C>, $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<C: XConnection> {
associated_windows: SparseSecondaryMap<ObjectKey, x::Window>,
windows: HashMap<x::Window, WindowData>,
xdg_wm_base: XdgWmBase,
qh: ClientQueueHandle,
to_focus: Option<x::Window>,
last_focused_toplevel: Option<x::Window>,
connection: Option<C>,
}
const XDG_WM_BASE_VERSION: u32 = 2;
xdg_wm_base: XdgWmBase,
clipboard_data: Option<ClipboardData<C::MimeTypeData>>,
last_kb_serial: Option<u32>,
}
impl<C: XConnection> ServerState<C> {
pub fn new(dh: DisplayHandle, server_connection: Option<UnixStream>) -> 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::<XdgWmBase, _, _>(data.name, XDG_WM_BASE_VERSION, &qh, ());
let xdg_wm_base = clientside
.global_list
.bind::<XdgWmBase, _, _>(&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::<CopyPasteData<C::MimeTypeData>>,
});
dh.create_global::<Self, XwaylandShellV1, _>(1, ());
clientside
.global_list
.contents()
.with_list(|globals| handle_globals::<C>(&dh, globals));
let mut ret = Self {
Self {
windows: HashMap::new(),
clientside,
atoms: None,
@ -430,9 +471,9 @@ impl<C: XConnection> ServerState<C> {
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<C: XConnection> ServerState<C> {
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::<Self, $global, GlobalData>(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::<C>(&self.dh, globals.iter());
}
fn get_object_from_client_object<T, P: Proxy>(&self, proxy: &P) -> &T
@ -638,6 +653,24 @@ impl<C: XConnection> ServerState<C> {
let _ = self.windows.remove(&window);
}
pub(crate) fn set_copy_paste_source(&mut self, mime_types: Rc<Vec<C::MimeTypeData>>) {
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<C: XConnection> ServerState<C> {
.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<C: XConnection> ServerState<C> {
}
}
self.handle_clipboard_events();
self.clientside.queue.flush().unwrap();
}
pub fn new_selection(&mut self) -> Option<ForeignSelection> {
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<M: MimeTypeData> {
manager: DataDeviceManagerState,
device: Option<DataDevice>,
source: Option<CopyPasteData<M>>,
}
pub struct ForeignSelection {
pub mime_types: Box<[String]>,
inner: SelectionOffer,
}
impl ForeignSelection {
pub(crate) fn receive(
&self,
mime_type: String,
state: &ServerState<impl XConnection>,
) -> Vec<u8> {
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<M: MimeTypeData> {
X11 {
inner: CopyPasteSource,
data: Rc<Vec<M>>,
},
Foreign(ForeignSelection),
}

View file

@ -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<WlCompositor>,
shm: TestObject<WlShm>,
shell: TestObject<XwaylandShellV1>
shell: TestObject<XwaylandShellV1>,
seat: TestObject<WlSeat>
}
}
@ -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<FakeXConnection> 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::<WlRegistry>::Bind {
name,
id: (interface, version),
};
assert!(!events.is_empty());
for event in events {
if let Ev::<WlRegistry>::Global {
@ -284,23 +290,34 @@ impl TestFixture {
version,
} = event
{
let bind_req = |interface| Req::<WlRegistry>::Bind {
name,
id: (interface, version),
};
match interface {
x if x == WlCompositor::interface().name => {
ret.compositor = Some(TestObject::from_request(
&registry.obj,
bind_req(name, WlCompositor::interface(), version),
bind_req(WlCompositor::interface()),
));
}
x if x == WlShm::interface().name => {
ret.shm = Some(TestObject::from_request(
&registry.obj,
bind_req(name, WlShm::interface(), version),
bind_req(WlShm::interface()),
));
}
x if x == XwaylandShellV1::interface().name => {
ret.shell = Some(TestObject::from_request(
&registry.obj,
bind_req(name, XwaylandShellV1::interface(), version),
bind_req(XwaylandShellV1::interface()),
));
}
x if x == WlSeat::interface().name => {
ret.seat = Some(TestObject::from_request(
&registry.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::<WlRegistry>::from_request(&display, Req::<WlDisplay>::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::<WlRegistry>::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::<WlKeyboard>::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::<WlKeyboard>::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::<WlKeyboard>::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() {}