diff --git a/src/lib.rs b/src/lib.rs index 93abab3..baf42cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ use crate::server::{PendingSurfaceState, ServerState}; use crate::xstate::{RealConnection, XState}; use log::{error, info}; use rustix::event::{poll, PollFd, PollFlags}; +use smithay_client_toolkit::data_device_manager::WritePipe; use std::io::{BufRead, BufReader, Read, Write}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; use std::os::unix::net::UnixStream; @@ -16,7 +17,7 @@ use xcb::x; pub trait XConnection: Sized + 'static { type ExtraData: FromServerState; - type MimeTypeData: MimeTypeData; + type X11Selection: X11Selection; fn root_window(&self) -> x::Window; fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState); @@ -35,9 +36,9 @@ pub trait FromServerState { fn create(state: &ServerState) -> Self; } -pub trait MimeTypeData { - fn name(&self) -> &str; - fn data(&self) -> &[u8]; +pub trait X11Selection { + fn mime_types(&self) -> Vec<&str>; + fn write_to(&self, mime: &str, pipe: WritePipe); } type RealServerState = ServerState; diff --git a/src/server/mod.rs b/src/server/mod.rs index af3698a..b1d8027 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -8,7 +8,7 @@ use self::event::*; use super::FromServerState; use crate::clientside::*; use crate::xstate::{Atoms, WindowDims, WmHints, WmName, WmNormalHints}; -use crate::{MimeTypeData, XConnection}; +use crate::{X11Selection, XConnection}; use log::{debug, warn}; use rustix::event::{poll, PollFd, PollFlags}; use slotmap::{new_key_type, HopSlotMap, SparseSecondaryMap}; @@ -18,10 +18,9 @@ use smithay_client_toolkit::data_device_manager::{ }; 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 std::rc::Rc; +use std::rc::{Rc, Weak}; use wayland_client::{globals::Global, protocol as client, Proxy}; use wayland_protocols::{ wp::{ @@ -492,7 +491,7 @@ pub struct ServerState { pub connection: Option, xdg_wm_base: XdgWmBase, - clipboard_data: Option>, + clipboard_data: Option>, last_kb_serial: Option, } @@ -518,7 +517,7 @@ impl ServerState { let clipboard_data = manager.map(|manager| ClipboardData { manager, device: None, - source: None::>, + source: None::>, }); dh.create_global::(1, ()); @@ -680,11 +679,7 @@ impl ServerState { return true; }; - if win.mapped && !win.attrs.override_redirect { - false - } else { - true - } + !(win.mapped && !win.attrs.override_redirect) } pub fn reconfigure_window(&mut self, event: x::ConfigureNotifyEvent) { @@ -805,14 +800,14 @@ impl ServerState { let _ = self.windows.remove(&window); } - pub(crate) fn set_copy_paste_source(&mut self, mime_types: Rc>) { + 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, mime_types.iter().map(|m| m.name())); + .create_copy_paste_source(&self.qh, selection.mime_types()); let data = CopyPasteData::X11 { inner: src, - data: mime_types, + data: Rc::downgrade(selection), }; let CopyPasteData::X11 { inner, .. } = d.source.insert(data) else { unreachable!(); @@ -891,13 +886,12 @@ impl ServerState { 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) { + for (mime_type, fd) in std::mem::take(&mut globals.selection_requests) { let CopyPasteData::X11 { data, .. } = clipboard.source.as_ref().unwrap() else { - unreachable!() + unreachable!("Got selection request without having set the selection?") }; - 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 let Some(data) = data.upgrade() { + data.write_to(&mime_type, fd); } } @@ -1090,10 +1084,10 @@ pub struct PendingSurfaceState { pub height: i32, } -struct ClipboardData { +struct ClipboardData { manager: DataDeviceManagerState, device: Option, - source: Option>, + source: Option>, } pub struct ForeignSelection { @@ -1121,10 +1115,10 @@ impl Drop for ForeignSelection { } } -enum CopyPasteData { +enum CopyPasteData { X11 { inner: CopyPasteSource, - data: Rc>, + data: Weak, }, Foreign(ForeignSelection), } diff --git a/src/server/tests.rs b/src/server/tests.rs index f8d02a7..86e9d6c 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -3,6 +3,7 @@ use crate::xstate::{SetState, WmName}; use paste::paste; use rustix::event::{poll, PollFd, PollFlags}; use std::collections::HashMap; +use std::io::Write; use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::unix::net::UnixStream; use std::sync::{Arc, Mutex}; @@ -182,19 +183,30 @@ impl super::FromServerState for () { fn create(_: &FakeServerState) -> Self {} } -impl crate::MimeTypeData for testwl::PasteData { - fn name(&self) -> &str { - &self.mime_type +impl crate::X11Selection for Vec { + fn mime_types(&self) -> Vec<&str> { + self.iter().map(|data| data.mime_type.as_str()).collect() } - fn data(&self) -> &[u8] { - &self.data + fn write_to( + &self, + mime: &str, + mut pipe: smithay_client_toolkit::data_device_manager::WritePipe, + ) { + println!("writing"); + let data = self + .iter() + .find(|data| data.mime_type == mime) + .unwrap_or_else(|| panic!("Couldn't find mime type {mime}")); + pipe.write_all(&data.data) + .expect("Couldn't write paste data"); + println!("goodbye pipe {mime}"); } } impl super::XConnection for FakeXConnection { type ExtraData = (); - type MimeTypeData = testwl::PasteData; + type X11Selection = Vec; fn root_window(&self) -> Window { self.root } @@ -1162,7 +1174,7 @@ fn copy_from_x11() { }, ]); - f.satellite.set_copy_paste_source(mimes.clone()); + f.satellite.set_copy_paste_source(&mimes); f.run(); let server_mimes = f.testwl.data_source_mimes(); @@ -1170,9 +1182,10 @@ fn copy_from_x11() { assert!(server_mimes.contains(&mime.mime_type)); } - let data = f.testwl.paste_data(); - f.run(); - let data = data.resolve(); + let data = f.testwl.paste_data(|_, _| { + f.satellite.run(); + true + }); assert_eq!(*mimes, data); } @@ -1236,7 +1249,7 @@ fn clipboard_x11_then_wayland() { }, ]); - f.satellite.set_copy_paste_source(x11data.clone()); + f.satellite.set_copy_paste_source(&x11data); f.run(); let waylanddata = vec![ diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index 219927b..e4e39c4 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -1,5 +1,5 @@ mod selection; -use selection::{SelectionData, SelectionTarget}; +use selection::{Selection, SelectionData}; use crate::{server::WindowAttributes, XConnection}; use bitflags::bitflags; @@ -117,7 +117,11 @@ impl XState { xcb::Connection::connect_to_fd_with_extensions( fd.as_raw_fd(), None, - &[xcb::Extension::Composite, xcb::Extension::RandR], + &[ + xcb::Extension::Composite, + xcb::Extension::RandR, + xcb::Extension::XFixes, + ], &[], ) .unwrap(), @@ -156,6 +160,28 @@ impl XState { }) .unwrap(); + // negotiate xfixes version + let reply = connection + .wait_for_reply(connection.send_request(&xcb::xfixes::QueryVersion { + client_major_version: 1, + client_minor_version: 0, + })) + .unwrap(); + log::info!( + "xfixes version: {}.{}", + reply.major_version(), + reply.minor_version() + ); + use xcb::xfixes::SelectionEventMask; + connection + .send_and_check_request(&xcb::xfixes::SelectSelectionInput { + window: root, + selection: atoms.clipboard, + 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(); @@ -169,13 +195,14 @@ impl XState { } let wm_window = connection.generate_id(); + let selection_data = SelectionData::new(&connection, root); let mut r = Self { connection, wm_window, root, atoms, - selection_data: Default::default(), + selection_data, }; r.create_ewmh_window(); r @@ -240,8 +267,6 @@ impl XState { data: b"xwayland-satellite", }) .unwrap(); - - self.set_clipboard_owner(x::CURRENT_TIME); } pub fn handle_events(&mut self, server_state: &mut super::RealServerState) { @@ -666,7 +691,7 @@ impl XState { server_state.set_win_class(window, class); } _ => { - if !self.handle_selection_property_change(&event, server_state) + if !self.handle_selection_property_change(&event) && log::log_enabled!(log::Level::Debug) { debug!( @@ -872,7 +897,7 @@ impl RealConnection { impl XConnection for RealConnection { type ExtraData = Atoms; - type MimeTypeData = SelectionTarget; + type X11Selection = Selection; fn root_window(&self) -> x::Window { self.connection.get_setup().roots().next().unwrap().root() @@ -898,14 +923,16 @@ impl XConnection for RealConnection { &[] }; - if let Err(e) = self.connection + if let Err(e) = self + .connection .send_and_check_request(&x::ChangeProperty:: { mode: x::PropMode::Replace, window, property: atoms.net_wm_state, r#type: x::ATOM_ATOM, data, - }) { + }) + { warn!("Failed to set fullscreen state on {window:?} ({e})"); } } diff --git a/src/xstate/selection.rs b/src/xstate/selection.rs index 03723f7..7b39a94 100644 --- a/src/xstate/selection.rs +++ b/src/xstate/selection.rs @@ -1,91 +1,213 @@ use super::{get_atom_name, XState}; use crate::server::ForeignSelection; -use crate::{MimeTypeData, RealServerState}; -use log::{debug, trace, warn}; +use crate::{RealServerState, X11Selection}; +use log::{debug, error, warn}; +use smithay_client_toolkit::data_device_manager::WritePipe; +use std::cell::RefCell; +use std::io::Write; use std::rc::Rc; use xcb::x; -#[derive(Debug)] -enum TargetValue { - U8(Vec), - U16(Vec), - U32(Vec), - Foreign, -} - #[derive(Debug)] struct SelectionTargetId { name: String, atom: x::Atom, } -pub struct SelectionTarget { - id: SelectionTargetId, - value: TargetValue, +struct PendingSelectionData { + target: x::Atom, + pipe: WritePipe, + incr: bool, } -impl MimeTypeData for SelectionTarget { - fn name(&self) -> &str { - &self.id.name +pub struct Selection { + mimes: Vec, + connection: Rc, + window: x::Window, + pending: RefCell>, + clipboard: x::Atom, + selection_time: u32, + incr: x::Atom, +} + +impl X11Selection for Selection { + fn mime_types(&self) -> Vec<&str> { + self.mimes + .iter() + .map(|target| target.name.as_str()) + .collect() } - fn data(&self) -> &[u8] { - match &self.value { - TargetValue::U8(v) => v, - TargetValue::U32(v) => unsafe { v.align_to().1 }, - other => { - warn!( - "Unexpectedly requesting data from mime type with data type {} - nothing will be copied", - std::any::type_name_of_val(other) - ); - &[] + fn write_to(&self, mime: &str, pipe: WritePipe) { + if let Some(target) = self.mimes.iter().find(|target| target.name == mime) { + // We use the target as the property to write to + if let Err(e) = self + .connection + .send_and_check_request(&x::ConvertSelection { + requestor: self.window, + selection: self.clipboard, + target: target.atom, + property: target.atom, + time: self.selection_time, + }) + { + error!("Failed to request clipboard data (mime type: {mime}, error: {e})"); + return; } + + self.pending.borrow_mut().push(PendingSelectionData { + target: target.atom, + pipe, + incr: false, + }) + } else { + warn!("Could not find mime type {mime}"); } } } -enum PendingMimeDataType { - Standard, - Incremental(TargetValue), -} +impl Selection { + fn handle_notify(&self, target: x::Atom) { + let mut pending = self.pending.borrow_mut(); + let Some(idx) = pending.iter().position(|t| t.target == target) else { + warn!( + "Got selection notify for unknown target {}", + get_atom_name(&self.connection, target), + ); + return; + }; -struct PendingMimeData { - ty: PendingMimeDataType, - id: SelectionTargetId, - dest_property: x::Atom, -} + let PendingSelectionData { + mut pipe, + incr, + target, + } = pending.swap_remove(idx); + let reply = match get_property_any(&self.connection, self.window, target) { + Ok(reply) => reply, + Err(e) => { + warn!( + "Couldn't get mime type for {}: {e:?}", + get_atom_name(&self.connection, target) + ); + return; + } + }; -enum MimeTypes { - Temporary { - /// Temporary mime data, being built - data: Vec, - /// Mime types we still need to receive feedback on - to_grab: Vec, - }, - /// Done grabbing mime data - Complete(Rc>), -} + debug!( + "got type {} for mime type {}", + get_atom_name(&self.connection, reply.r#type()), + get_atom_name(&self.connection, target) + ); -impl Default for MimeTypes { - fn default() -> Self { - Self::Complete(Default::default()) + if reply.r#type() == self.incr { + debug!( + "beginning incr for {}", + get_atom_name(&self.connection, target) + ); + pending.push(PendingSelectionData { + target, + pipe, + incr: true, + }); + return; + } + + let data = match reply.format() { + 8 => reply.value::(), + 32 => unsafe { reply.value::().align_to().1 }, + other => { + warn!("Unexpected format {other} in selection reply"); + return; + } + }; + + if !incr || !data.is_empty() { + if let Err(e) = pipe.write_all(data) { + warn!("Failed to write selection data: {e:?}"); + } else if incr { + debug!( + "recieved some incr data for {}", + get_atom_name(&self.connection, target) + ); + pending.push(PendingSelectionData { + target, + pipe, + incr: true, + }) + } + } else if incr { + // data is empty + debug!( + "completed incr for mime {}", + get_atom_name(&self.connection, target) + ); + } + } + + fn check_for_incr(&self, event: &x::PropertyNotifyEvent) -> bool { + if event.window() != self.window || event.state() != x::Property::NewValue { + return false; + } + + let target = self.pending.borrow().iter().find_map(|pending| { + (pending.target == event.atom() && pending.incr).then_some(pending.target) + }); + if let Some(target) = target { + self.handle_notify(target); + true + } else { + false + } } } -#[derive(Default)] +enum CurrentSelection { + X11(Rc), + Wayland { + mimes: Vec, + inner: ForeignSelection, + }, +} pub(crate) struct SelectionData { - clear_time: Option, - mime_types: MimeTypes, - foreign_data: Option, + last_selection_timestamp: u32, + target_window: x::Window, + current_selection: Option, +} + +impl SelectionData { + pub fn new(connection: &xcb::Connection, root: x::Window) -> Self { + let target_window = connection.generate_id(); + connection + .send_and_check_request(&x::CreateWindow { + wid: target_window, + width: 1, + height: 1, + depth: 0, + parent: root, + x: 0, + y: 0, + border_width: 0, + class: x::WindowClass::InputOnly, + visual: x::COPY_FROM_PARENT, + // Watch for INCR property changes. + value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], + }) + .expect("Couldn't create window for selections"); + Self { + last_selection_timestamp: x::CURRENT_TIME, + target_window, + current_selection: None, + } + } } impl XState { - pub(crate) fn set_clipboard_owner(&mut self, time: u32) { + fn set_clipboard_owner(&mut self) { self.connection .send_and_check_request(&x::SetSelectionOwner { owner: self.wm_window, selection: self.atoms.clipboard, - time, + time: self.selection_data.last_selection_timestamp, }) .unwrap(); @@ -105,7 +227,7 @@ impl XState { } pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) { - let types = selection + let mimes = selection .mime_types .iter() .map(|mime| { @@ -117,18 +239,18 @@ impl XState { })) .unwrap(); - SelectionTarget { - id: SelectionTargetId { - name: mime.clone(), - atom: atom.atom(), - }, - value: TargetValue::Foreign, + SelectionTargetId { + name: mime.clone(), + atom: atom.atom(), } }) .collect(); - self.selection_data.mime_types = MimeTypes::Complete(Rc::new(types)); - self.selection_data.foreign_data = Some(selection); + self.selection_data.current_selection = Some(CurrentSelection::Wayland { + mimes, + inner: selection, + }); + self.set_clipboard_owner(); debug!("Clipboard set from Wayland"); } @@ -138,29 +260,9 @@ impl XState { server_state: &mut RealServerState, ) -> bool { match event { - // Someone else is the clipboard owner - get the data from them and then reestablish - // ourselves as the owner + // Someone else took the clipboard owner xcb::Event::X(x::Event::SelectionClear(e)) => { - if e.selection() != self.atoms.clipboard { - warn!( - "Got SelectionClear for unexpected atom {}, ignoring", - get_atom_name(&self.connection, 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()); + self.handle_new_selection_owner(e.owner(), e.time()); } xcb::Event::X(x::Event::SelectionNotify(e)) => { if e.property() == x::ATOM_NONE { @@ -168,24 +270,33 @@ impl XState { return true; } - trace!( - "selection notify target: {}", + debug!( + "selection notify requestor: {:?} target: {}", + e.requestor(), get_atom_name(&self.connection, e.target()) ); - match e.target() { - x if x == self.atoms.targets => self.handle_target_list(e.property()), - atom => self.handle_clipboard_data(atom), - } - if let MimeTypes::Temporary { to_grab, .. } = &self.selection_data.mime_types { - if to_grab.is_empty() { - let MimeTypes::Temporary { data, .. } = - std::mem::take(&mut self.selection_data.mime_types) - else { - unreachable!() - }; - self.finish_mime_data(server_state, data); + if e.requestor() == self.wm_window { + match e.target() { + x if x == self.atoms.targets => { + self.handle_target_list(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 + { + selection.handle_notify(e.target()); + } + } else { + warn!( + "Got selection notify from unexpected requestor: {:?}", + e.requestor() + ); } } xcb::Event::X(x::Event::SelectionRequest(e)) => { @@ -219,15 +330,17 @@ impl XState { return true; } - let MimeTypes::Complete(mime_data) = &self.selection_data.mime_types else { - warn!("Got selection request, but mime data is incomplete"); + 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]> = mime_data.iter().map(|t| t.id.atom).collect(); + let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect(); self.connection .send_and_check_request(&x::ChangeProperty { @@ -242,7 +355,7 @@ impl XState { success(); } other => { - let Some(target) = mime_data.iter().find(|t| t.id.atom == other) else { + 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); @@ -251,48 +364,63 @@ impl XState { return true; }; - macro_rules! set_property { - ($data:expr) => { - match self.connection.send_and_check_request(&x::ChangeProperty { - mode: x::PropMode::Replace, - window: e.requestor(), - property: e.property(), - r#type: target.id.atom, - data: $data, - }) { - Ok(_) => success(), - Err(e) => { - warn!("Failed setting selection property: {e:?}"); - refuse(); - } - } - }; - } - - match &target.value { - 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.id.name.clone(), server_state); - set_property!(&data); + let data = inner.receive(target.name.clone(), 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(); } } } } } + + xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => { + assert_eq!(e.selection(), self.atoms.clipboard); + 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()); + } + xcb::xfixes::SelectionEvent::SelectionClientClose + | xcb::xfixes::SelectionEvent::SelectionWindowDestroy => { + debug!("Selection owner destroyed, selection will be unset"); + self.selection_data.current_selection = None; + } + } + } _ => return false, } true } - fn handle_target_list(&mut self, dest_property: x::Atom) { + 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 { @@ -306,6 +434,29 @@ impl XState { .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() @@ -314,7 +465,7 @@ impl XState { debug!("got targets: {targets_str:?}"); } - let to_grab = targets + let mimes = targets .iter() .copied() .filter(|atom| { @@ -325,218 +476,35 @@ impl XState { ] .contains(atom) }) - .enumerate() - .map(|(idx, target_atom)| { - let dest_name = [b"dest", idx.to_string().as_bytes()].concat(); - let reply = self - .connection - .wait_for_reply(self.connection.send_request(&x::InternAtom { - name: &dest_name, - only_if_exists: false, - })) - .unwrap(); - let dest_property = reply.atom(); - - self.connection - .send_and_check_request(&x::ConvertSelection { - requestor: self.wm_window, - selection: self.atoms.clipboard, - target: target_atom, - property: dest_property, - time: self.selection_data.clear_time.as_ref().copied().unwrap(), - }) - .unwrap(); - - let target_name = get_atom_name(&self.connection, target_atom); - PendingMimeData { - ty: PendingMimeDataType::Standard, - id: SelectionTargetId { - name: target_name, - atom: target_atom, - }, - dest_property, - } + .map(|target_atom| SelectionTargetId { + name: get_atom_name(&self.connection, target_atom), + atom: target_atom, }) .collect(); - self.selection_data.mime_types = MimeTypes::Temporary { - to_grab, - data: Vec::new(), - }; - } + 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, + }); - fn handle_clipboard_data(&mut self, atom: x::Atom) { - let MimeTypes::Temporary { data, to_grab } = &mut self.selection_data.mime_types else { - warn!("Got selection notify, but not awaiting selection data..."); - return; - }; - - let Some(idx) = to_grab - .iter() - .position(|PendingMimeData { id, .. }| id.atom == atom) - else { - warn!( - "unexpected SelectionNotify type: {}", - get_atom_name(&self.connection, atom) - ); - return; - }; - - let PendingMimeData { - ty, - id, - dest_property, - } = to_grab.swap_remove(idx); - - let value = match atom { - x if x == self.atoms.timestamp => TargetValue::U32(vec![self - .selection_data - .clear_time - .as_ref() - .copied() - .unwrap()]), - _ => { - let reply = get_property_any(&self.connection, self.wm_window, dest_property); - - trace!( - "got type {} for mime type {}", - get_atom_name(&self.connection, reply.r#type()), - get_atom_name(&self.connection, atom) - ); - - match reply.r#type() { - x if x == self.atoms.incr => { - assert!(matches!(ty, PendingMimeDataType::Standard)); - debug!( - "beginning incr process for {}", - get_atom_name(&self.connection, atom) - ); - if let Some(data) = - begin_incr(&self.connection, self.wm_window, reply, id, dest_property) - { - to_grab.push(data); - } - return; - } - _ => 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 => { - if log::log_enabled!(log::Level::Debug) { - let target_name = &id.name; - let ty = if reply.r#type() == x::ATOM_NONE { - "None".to_string() - } else { - get_atom_name(&self.connection, reply.r#type()) - }; - let dest = get_atom_name(&self.connection, dest_property); - let value = reply.value::().to_vec(); - debug!("unexpected format: {other} (atom: {target_name}, type: {ty:?}, property: {dest}, value: {value:?})"); - } - return; - } - }, - } - } - }; - - trace!("Selection data: {id:?} {value:?}"); - data.push(SelectionTarget { id, value }); + 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, - server_state: &mut RealServerState, ) -> bool { - if event.window() != self.wm_window { - return false; + if let Some(CurrentSelection::X11(selection)) = &self.selection_data.current_selection { + return selection.check_for_incr(event); } - - let MimeTypes::Temporary { data, to_grab } = &mut self.selection_data.mime_types else { - debug!("Got potential selection property change, but not awaiting mime data"); - return false; - }; - - let Some(idx) = to_grab.iter().position(|p| { - matches!(p.ty, PendingMimeDataType::Incremental(_)) && p.dest_property == event.atom() - }) else { - debug!( - "Changed non selection property: {}", - get_atom_name(&self.connection, event.atom()) - ); - return false; - }; - - let pending = &mut to_grab[idx]; - let reply = get_property_any(&self.connection, self.wm_window, pending.dest_property); - - if reply.r#type() != pending.id.atom { - warn!( - "wrong getproperty type: {}", - get_atom_name(&self.connection, reply.r#type()) - ); - return false; - } - - match reply.format() { - 8 => { - let value: &[u8] = reply.value(); - trace!("got incr data ({} bytes)", value.len()); - if value.is_empty() { - let pending = to_grab.swap_remove(idx); - let PendingMimeDataType::Incremental(value) = pending.ty else { - unreachable!() - }; - let atom = pending.id.atom; - data.push(SelectionTarget { - id: pending.id, - value, - }); - trace!( - "finalized incr for {}", - get_atom_name(&self.connection, atom) - ); - } else { - let PendingMimeDataType::Incremental(TargetValue::U8(data)) = &mut pending.ty - else { - unreachable!() - }; - data.extend_from_slice(value); - trace!("new incr len: {}", data.len()); - } - } - other => { - warn!("Got unexpected format {other} for INCR data - copy/pasting with mime type {} will fail!", get_atom_name(&self.connection, reply.r#type())); - to_grab.swap_remove(idx); - } - } - - if to_grab.is_empty() { - let MimeTypes::Temporary { data, .. } = - std::mem::take(&mut self.selection_data.mime_types) - else { - unreachable!() - }; - self.finish_mime_data(server_state, data); - } - - true - } - - fn finish_mime_data(&mut self, server_state: &mut RealServerState, data: Vec) { - self.connection - .send_and_check_request(&x::ChangeWindowAttributes { - window: self.wm_window, - value_list: &[x::Cw::EventMask(x::EventMask::empty())], - }) - .unwrap(); - let data = Rc::new(data); - self.selection_data.mime_types = MimeTypes::Complete(data.clone()); - self.set_clipboard_owner(self.selection_data.clear_time.unwrap()); - server_state.set_copy_paste_source(data); - debug!("Clipboard set from X11"); + false } } @@ -544,48 +512,13 @@ fn get_property_any( connection: &xcb::Connection, window: x::Window, property: x::Atom, -) -> x::GetPropertyReply { - connection - .wait_for_reply(connection.send_request(&x::GetProperty { - delete: true, - window, - property, - r#type: x::ATOM_ANY, - long_offset: 0, - long_length: u32::MAX, - })) - .unwrap() -} -fn begin_incr( - connection: &xcb::Connection, - window: x::Window, - reply: x::GetPropertyReply, - id: SelectionTargetId, - dest_property: x::Atom, -) -> Option { - let size = match reply.format() { - 8 => reply.value::()[0] as usize, - 16 => reply.value::()[0] as usize, - 32 => reply.value::()[0] as usize, - other => { - warn!("unexpected incr format: {other}"); - return None; - } - }; - - connection - .send_and_check_request(&x::ChangeWindowAttributes { - window, - value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], - }) - .unwrap(); - - // XXX: storing INCR property data in memory could significantly bloat memory depending on how - // much data is going to be stuck into the clipboard, but we'll cross that bridge when we get - // to it. - Some(PendingMimeData { - ty: PendingMimeDataType::Incremental(TargetValue::U8(Vec::with_capacity(size))), - id, - dest_property, - }) +) -> xcb::Result { + connection.wait_for_reply(connection.send_request(&x::GetProperty { + delete: true, + window, + property, + r#type: x::ATOM_ANY, + long_offset: 0, + long_length: u32::MAX, + })) } diff --git a/tests/integration.rs b/tests/integration.rs index 886b0f4..2d2f348 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -185,11 +185,9 @@ impl Fixture { .configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]); self.testwl.focus_toplevel(surface); self.wait_and_dispatch(); - let geometry = connection - .wait_for_reply(connection.send_request(&x::GetGeometry { - drawable: x::Drawable::Window(window), - })) - .unwrap(); + let geometry = connection.get_reply(&x::GetGeometry { + drawable: x::Drawable::Window(window), + }); assert_eq!(geometry.x(), 0); assert_eq!(geometry.y(), 0); @@ -278,6 +276,7 @@ struct Connection { atoms: Atoms, root: x::Window, visual: u32, + wm_window: x::Window, } impl std::ops::Deref for Connection { @@ -289,7 +288,18 @@ impl std::ops::Deref for Connection { impl Connection { fn new(display: &str) -> Self { - let (inner, _) = xcb::Connection::connect(Some(display)).unwrap(); + let (inner, _) = + xcb::Connection::connect_with_extensions(Some(display), &[xcb::Extension::XFixes], &[]) + .unwrap(); + // xfixes init + let reply = inner + .wait_for_reply(inner.send_request(&xcb::xfixes::QueryVersion { + client_major_version: 1, + client_minor_version: 0, + })) + .unwrap(); + assert_eq!(reply.major_version(), 1); + let fd = unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) }; let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN); let atoms = Atoms::intern_all(&inner).unwrap(); @@ -297,12 +307,25 @@ impl Connection { let root = screen.root(); let visual = screen.root_visual(); + let wm_window: x::Window = inner + .wait_for_reply(inner.send_request(&x::GetProperty { + delete: false, + window: root, + property: atoms.wm_check, + r#type: x::ATOM_WINDOW, + long_offset: 0, + long_length: 1, + })) + .expect("Couldn't get WM window") + .value()[0]; + Self { inner, pollfd, atoms, root, visual, + wm_window, } } @@ -368,6 +391,7 @@ impl Connection { #[track_caller] fn await_event(&mut self) { + self.pollfd.clear_revents(); assert!( poll(&mut [self.pollfd.clone()], 100).expect("poll failed") > 0, "Did not get any X11 events" @@ -393,12 +417,29 @@ impl Connection { time: x::CURRENT_TIME, }) .unwrap(); - let owner = self - .wait_for_reply(self.send_request(&x::GetSelectionOwner { - selection: self.atoms.clipboard, - })) - .unwrap(); - assert_eq!(window, owner.owner()); + let owner = self.get_reply(&x::GetSelectionOwner { + selection: self.atoms.clipboard, + }); + + assert_eq!(window, owner.owner(), "Unexpected selection owner"); + } + + #[track_caller] + fn await_selection_request(&mut self) -> x::SelectionRequestEvent { + self.await_event(); + match self.poll_for_event().unwrap() { + Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r, + other => panic!("Didn't get selection request event, instead got {other:?}"), + } + } + + #[track_caller] + fn await_selection_notify(&mut self) -> x::SelectionNotifyEvent { + self.await_event(); + match self.poll_for_event().unwrap() { + Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r, + other => panic!("Didn't get selection notify event, instead got {other:?}"), + } } #[track_caller] @@ -419,11 +460,35 @@ impl Connection { } #[track_caller] - fn atom_name(&self, atom: x::Atom) -> String { - self.get_reply(&x::GetAtomName { atom }) - .name() - .as_ascii() - .to_string() + fn verify_clipboard_owner(&self, window: x::Window) { + let owner = self.get_reply(&x::GetSelectionOwner { + selection: self.atoms.clipboard, + }); + assert_eq!(owner.owner(), window, "Clipboard owner does not match"); + } + + #[track_caller] + fn await_selection_owner_change(&mut self) -> xcb::xfixes::SelectionNotifyEvent { + self.await_event(); + match self.poll_for_event().unwrap() { + Some(xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e))) => e, + other => panic!("Expected XFixes SelectionNotify, got {other:?}"), + } + } + + #[track_caller] + fn get_selection_owner_change_events(&self, enable: bool, window: x::Window) { + let event_mask = if enable { + xcb::xfixes::SelectionEventMask::SET_SELECTION_OWNER + } else { + xcb::xfixes::SelectionEventMask::empty() + }; + self.send_and_check_request(&xcb::xfixes::SelectSelectionInput { + window, + selection: self.atoms.clipboard, + event_mask, + }) + .unwrap(); } } @@ -607,22 +672,17 @@ fn input_focus() { let conn = std::cell::RefCell::new(&mut connection); let check_focus = |win: x::Window| { let connection = conn.borrow(); - let focus = connection - .wait_for_reply(connection.send_request(&x::GetInputFocus {})) - .unwrap() - .focus(); + let focus = connection.get_reply(&x::GetInputFocus {}).focus(); assert_eq!(win, focus); - let reply = connection - .wait_for_reply(connection.send_request(&x::GetProperty { - delete: false, - window: connection.root, - property: connection.atoms.net_active_window, - r#type: x::ATOM_WINDOW, - long_offset: 0, - long_length: 1, - })) - .unwrap(); + let reply = connection.get_reply(&x::GetProperty { + delete: false, + window: connection.root, + property: connection.atoms.net_active_window, + r#type: x::ATOM_WINDOW, + long_offset: 0, + long_length: 1, + }); assert_eq!(&[win], reply.value::()); }; @@ -717,23 +777,10 @@ fn copy_from_x11() { 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); - + f.map_as_toplevel(&mut connection, window); connection.set_selection_owner(window); - // wait for requests 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:?}"), - }; - + let request = connection.await_selection_request(); assert_eq!(request.target(), connection.atoms.targets); connection.set_property( request.requestor(), @@ -742,75 +789,61 @@ fn copy_from_x11() { &[connection.atoms.mime1, connection.atoms.mime2], ); connection.send_selection_notify(&request); - - connection.await_event(); - let mut mime_data = vec![ - ( - connection.atoms.mime1, - x::ATOM_STRING, - b"hello world".as_slice(), - ), - (connection.atoms.mime2, x::ATOM_INTEGER, &[1u8, 2, 3, 4]), - ]; - - while let Some(request) = connection.poll_for_event().unwrap() { - let xcb::Event::X(x::Event::SelectionRequest(request)) = request else { - continue; - }; - - let target = request.target(); - let Some(idx) = mime_data.iter().position(|(atom, _, _)| *atom == target) else { - panic!("Expected atom in {mime_data:?}, got {target:?}"); - }; - - let (_, ty, data) = mime_data.swap_remove(idx); - connection.set_property(request.requestor(), ty, request.property(), data); - - 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(50)); - } - - assert!( - mime_data.is_empty(), - "Didn't get all mime types: {mime_data:?}" - ); 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()); + struct MimeData { + mime: x::Atom, + data: testwl::PasteData, + } + let mimes_truth = [ + MimeData { + mime: connection.atoms.mime1, + data: testwl::PasteData { + mime_type: "text/plain".to_string(), + data: b"hello world".to_vec(), + }, + }, + MimeData { + mime: connection.atoms.mime2, + data: testwl::PasteData { + mime_type: "blah/blah".to_string(), + data: vec![1, 2, 3, 4], + }, + }, + ]; - let mimes = f.testwl.data_source_mimes(); - assert!( - mimes.contains(&"text/plain".into()), - "text/plain not in mimes: {mimes:?}" - ); // mime1 - assert!( - mimes.contains(&"blah/blah".into()), - "blah/blah not in mimes: {mimes:?}" - ); // mime2 + let advertised_mimes = f.testwl.data_source_mimes(); + assert_eq!( + advertised_mimes.len(), + mimes_truth.len(), + "Wrong number of advertised mimes: {advertised_mimes:?}" + ); + for MimeData { data, .. } in &mimes_truth { + assert!( + advertised_mimes.contains(&data.mime_type), + "Missing mime type {}", + data.mime_type + ); + } - let data = f.testwl.paste_data(); - f.testwl.dispatch(); - let data = data.resolve(); + let data = f.testwl.paste_data(|mime, _| { + let request = connection.await_selection_request(); + let data = mimes_truth + .iter() + .find(|data| data.data.mime_type == mime) + .unwrap_or_else(|| panic!("Asked for unknown mime: {mime}")); + connection.set_property( + request.requestor(), + data.mime, + request.property(), + &data.data.data, + ); + connection.send_selection_notify(&request); + true + }); + let mut found_mimes = Vec::new(); for testwl::PasteData { mime_type, data } in data { - match mime_type { + match &mime_type { x if x == "text/plain" => { assert_eq!(&data, b"hello world"); } @@ -819,7 +852,17 @@ fn copy_from_x11() { } other => panic!("unexpected mime type: {other} ({data:?})"), } + found_mimes.push(mime_type); } + + assert!( + found_mimes.contains(&"text/plain".to_string()), + "Didn't get mime data for text/plain" + ); + assert!( + found_mimes.contains(&"blah/blah".to_string()), + "Didn't get mime data for blah/blah" + ); } #[test] @@ -828,13 +871,8 @@ fn copy_from_wayland() { 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); + connection.get_selection_owner_change_events(true, window); + f.map_as_toplevel(&mut connection, window); let offer = vec![ testwl::PasteData { mime_type: "text/plain".into(), @@ -847,22 +885,10 @@ fn copy_from_wayland() { ]; f.testwl.create_data_offer(offer.clone()); + connection.await_selection_owner_change(); + connection.verify_clipboard_owner(connection.wm_window); + connection.get_selection_owner_change_events(false, window); - 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", @@ -870,9 +896,6 @@ fn copy_from_wayland() { }) .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, @@ -883,12 +906,7 @@ fn copy_from_wayland() { }) .unwrap(); - connection.await_event(); - 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:?}"), - }; - + let request = connection.await_selection_notify(); assert_eq!(request.requestor(), window); assert_eq!(request.selection(), connection.atoms.clipboard); assert_eq!(request.target(), connection.atoms.targets); @@ -927,11 +945,7 @@ fn copy_from_wayland() { .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:?}"), - }; - + let request = connection.await_selection_notify(); assert_eq!(request.requestor(), window); assert_eq!(request.selection(), connection.atoms.clipboard); assert_eq!(request.target(), atom); @@ -974,7 +988,7 @@ fn different_output_position() { f.testwl.move_pointer_to(surface, 10.0, 10.0); f.wait_and_dispatch(); let reply = connection.get_reply(&x::QueryPointer { window }); - assert_eq!(reply.same_screen(), true); + assert!(reply.same_screen()); assert_eq!(reply.win_x(), 10); assert_eq!(reply.win_y(), 10); @@ -985,7 +999,7 @@ fn different_output_position() { f.wait_and_dispatch(); let reply = connection.get_reply(&x::QueryPointer { window }); println!("reply: {reply:?}"); - assert_eq!(reply.same_screen(), true); + assert!(reply.same_screen()); assert_eq!(reply.win_x(), 150); assert_eq!(reply.win_y(), 12); } @@ -1002,20 +1016,9 @@ fn bad_clipboard_data() { .last_created_surface_id() .expect("No surface created"); f.configure_and_verify_new_toplevel(&mut connection, window, surface); + connection.set_selection_owner(window); - connection - .send_and_check_request(&x::SetSelectionOwner { - owner: window, - selection: connection.atoms.clipboard, - time: x::CURRENT_TIME, - }) - .unwrap(); - - connection.await_event(); - 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:?}"), - }; + let request = connection.await_selection_request(); assert_eq!(request.target(), connection.atoms.targets); connection.set_property( request.requestor(), @@ -1023,73 +1026,22 @@ fn bad_clipboard_data() { request.property(), &[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(); + connection.send_selection_notify(&request); - connection.await_event(); - 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.mime2); + f.wait_and_dispatch(); + let mut data = f.testwl.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 + connection.send_selection_notify(&request); + true + }); - // Don't actually set any data as requested - just report success - - 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(50)); - let owner = connection - .wait_for_reply(connection.send_request(&x::GetSelectionOwner { - selection: connection.atoms.clipboard, - })) - .unwrap(); - assert_ne!(window, owner.owner()); - - connection - .send_and_check_request(&x::ConvertSelection { - requestor: window, - selection: connection.atoms.clipboard, - target: connection.atoms.mime2, - property: connection.atoms.mime1, - time: x::CURRENT_TIME, - }) - .unwrap(); - - connection.await_event(); - let mut e = None; - while let Some(event) = connection.poll_for_event().unwrap() { - if let xcb::Event::X(x::Event::SelectionNotify(event)) = event { - e = Some(event); - break; - } - } - let e = e.expect("No selection notify event"); - assert_eq!(e.property(), x::ATOM_NONE); + connection.verify_clipboard_owner(window); + assert_eq!(data.len(), 1, "Unexpected data: {data:?}"); + let data = data.pop().unwrap(); + assert_eq!(data.mime_type, "blah/blah"); + assert!(data.data.is_empty(), "Unexpected data: {:?}", data.data); } // issue #42 @@ -1161,7 +1113,7 @@ fn primary_output() { let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root }); let config_timestamp = reply.config_timestamp(); - let mut it = reply.outputs().into_iter().copied().map(|output| { + let mut it = reply.outputs().iter().copied().map(|output| { let reply = conn.get_reply(&xcb::randr::GetOutputInfo { output, config_timestamp, @@ -1214,21 +1166,15 @@ fn primary_output() { assert_eq!(reply.output(), output3); } -// TODO: these sleeps are horrible. #[test] fn incr_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); f.map_as_toplevel(&mut connection, window); connection.set_selection_owner(window); - std::thread::sleep(std::time::Duration::from_millis(10)); - 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:?}"), - }; + let request = connection.await_selection_request(); assert_eq!(request.target(), connection.atoms.targets); connection.set_property( request.requestor(), @@ -1237,92 +1183,126 @@ fn incr_copy_from_x11() { &[connection.atoms.targets, connection.atoms.mime1], ); connection.send_selection_notify(&request); - connection.await_event(); + f.wait_and_dispatch(); - 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.mime1); - let destination_property = request.property(); - - connection - .send_and_check_request(&x::ChangeWindowAttributes { - window: request.requestor(), - value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], - }) - .unwrap(); - connection.set_property( - request.requestor(), - connection.atoms.incr, - destination_property, - &[3000u32], - ); - connection.send_selection_notify(&request); - // skip NewValue - let notify = match connection.poll_for_event().unwrap() { - Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p, - other => panic!("Didn't get property notify event, instead got {other:?}"), - }; - assert_eq!(notify.atom(), request.property()); - assert_eq!(notify.state(), x::Property::NewValue); - - let data: Vec = std::iter::successors(Some(1u8), |n| Some(n.wrapping_add(1))) - .take(3000) - .collect(); - for (idx, chunk) in data.chunks(500).enumerate() { - std::thread::sleep(std::time::Duration::from_millis(10)); - let notify = match connection.poll_for_event().unwrap() { - Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p, - other => panic!("Didn't get property notify event, instead got {other:?}"), - }; - assert_eq!(notify.atom(), destination_property, "chunk {idx}"); - assert_eq!(notify.state(), x::Property::Delete, "chunk {idx}"); + let mut destination_property = x::Atom::none(); + let mut begin_incr = Some(|connection: &mut Connection| { + let request = connection.await_selection_request(); + assert_eq!(request.target(), connection.atoms.mime1); + connection + .send_and_check_request(&x::ChangeWindowAttributes { + window: request.requestor(), + value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], + }) + .unwrap(); connection.set_property( request.requestor(), - connection.atoms.mime1, - destination_property, - chunk, + connection.atoms.incr, + request.property(), + &[3000u32], ); + connection.send_selection_notify(&request); // skip NewValue let notify = match connection.poll_for_event().unwrap() { Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p, other => panic!("Didn't get property notify event, instead got {other:?}"), }; - assert_eq!(notify.atom(), destination_property, "chunk {idx}"); - assert_eq!(notify.state(), x::Property::NewValue, "chunk {idx}"); - } + assert_eq!(notify.atom(), request.property()); + assert_eq!(notify.state(), x::Property::NewValue); + request.property() + }); - std::thread::sleep(std::time::Duration::from_millis(10)); - let notify = match connection.poll_for_event().unwrap() { - Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p, - other => panic!("Didn't get property notify event, instead got {other:?}"), - }; - assert_eq!(notify.atom(), destination_property); - assert_eq!(notify.state(), x::Property::Delete); - connection.set_property::( - request.requestor(), - connection.atoms.mime1, - destination_property, - &[], - ); + let data: Vec = std::iter::successors(Some(1u8), |n| Some(n.wrapping_add(1))) + .take(3000) + .collect(); + let mut it = data.chunks(500).enumerate(); + let mut paste_data = f.testwl.paste_data(|_, testwl| { + if let Some(begin) = begin_incr.take() { + destination_property = begin(&mut connection); + testwl.dispatch(); + return false; + } + assert_ne!(destination_property, x::Atom::none()); - std::thread::sleep(std::time::Duration::from_millis(100)); - let owner = connection - .wait_for_reply(connection.send_request(&x::GetSelectionOwner { - selection: connection.atoms.clipboard, - })) - .unwrap(); - assert_ne!(window, owner.owner()); - f.wait_and_dispatch(); + connection.await_event(); + let notify = match connection.poll_for_event().unwrap() { + Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p, + other => panic!("Didn't get property notify event, instead got {other:?}"), + }; + + match it.next() { + Some((idx, chunk)) => { + assert_eq!(notify.atom(), destination_property, "chunk {idx}"); + assert_eq!(notify.state(), x::Property::Delete, "chunk {idx}"); + connection.set_property( + notify.window(), + connection.atoms.mime1, + destination_property, + chunk, + ); + testwl.dispatch(); + // skip NewValue + let notify = match connection.poll_for_event().unwrap() { + Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p, + other => panic!("Didn't get property notify event, instead got {other:?}"), + }; + assert_eq!(notify.atom(), destination_property, "chunk {idx}"); + assert_eq!(notify.state(), x::Property::NewValue, "chunk {idx}"); + false + } + None => { + // INCR completed! + assert_eq!(notify.atom(), destination_property); + assert_eq!(notify.state(), x::Property::Delete); + connection.set_property::( + notify.window(), + connection.atoms.mime1, + destination_property, + &[], + ); + true + } + } + }); assert_eq!(f.testwl.data_source_mimes(), vec!["text/plain"]); - let wl_data = f.testwl.paste_data(); - f.testwl.dispatch(); - let mut wl_data = wl_data.resolve(); - assert_eq!(wl_data.len(), 1); - let wl_data = wl_data.swap_remove(0); - assert_eq!(wl_data.mime_type, "text/plain"); - assert_eq!(&wl_data.data, &data); + assert_eq!(paste_data.len(), 1); + let paste_data = paste_data.swap_remove(0); + assert_eq!(paste_data.mime_type, "text/plain"); + assert_eq!(&paste_data.data, &data); +} + +#[test] +fn wayland_then_x11_clipboard_owner() { + 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.get_selection_owner_change_events(true, window); + + f.map_as_toplevel(&mut connection, window); + 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()); + + connection.await_selection_owner_change(); + connection.verify_clipboard_owner(connection.wm_window); + connection.get_selection_owner_change_events(false, window); + + connection.set_selection_owner(window); + f.testwl.dispatch(); + connection.verify_clipboard_owner(window); + + connection.await_event(); + let request = connection.await_selection_request(); + assert_eq!(request.selection(), connection.atoms.clipboard); + assert_eq!(request.target(), connection.atoms.targets); } diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 9422707..1e47274 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -503,6 +503,7 @@ impl Server { self.state.pointer.as_ref().unwrap() } + #[track_caller] pub fn data_source_mimes(&self) -> Vec { let Some(selection) = &self.state.selection else { panic!("No selection set on data device"); @@ -513,13 +514,79 @@ impl Server { data.mimes.to_vec() } - pub fn paste_data(&mut self) -> PasteDataResolver { - let Some(selection) = &self.state.selection else { + #[track_caller] + pub fn paste_data( + &mut self, + mut send_data_for_mime: impl FnMut(&str, &mut Self) -> bool, + ) -> 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 + .iter() + .rev() + .map(|mime| (mime.clone(), None)) + .collect() + }; + + let mut ret = Vec::new(); + let mut try_transfer = + |pending_ret: &mut PendingRet, mime: String, mut pending: PendingData| { + self.display.flush_clients().unwrap(); + let transfer_complete = send_data_for_mime(&mime, self); + if transfer_complete { + pending.rx.read_to_end(&mut pending.data).unwrap(); + ret.push(PasteData { + mime_type: mime, + data: pending.data, + }); + } else { + loop { + match pending.rx.read(&mut pending.data) { + Ok(0) => break, + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break, + Err(e) => panic!("Failed reading data for mime {mime}: {e:?}"), + } + } + pending_ret.push((mime, Some(pending))); + self.dispatch(); + } + }; + + while !pending_ret.is_empty() { + let (mime, pending) = pending_ret.pop().unwrap(); + match pending { + Some(pending) => try_transfer(&mut pending_ret, mime, pending), + None => { + let (rx, tx) = rustix::pipe::pipe().unwrap(); + selection.send(mime.clone(), tx.as_fd()); + drop(tx); + + let rx = std::fs::File::from(rx); + try_transfer( + &mut pending_ret, + mime, + PendingData { + rx, + data: Vec::new(), + }, + ); + } + } + } + + self.state.selection = Some(selection); - let ret = PasteDataResolver::new(&selection); - self.display.flush_clients().unwrap(); ret } @@ -527,6 +594,7 @@ impl Server { self.state.selection.is_none() } + #[track_caller] pub fn create_data_offer(&mut self, data: Vec) { let Some(dev) = &self.state.data_device else { panic!("No data device created"); @@ -611,45 +679,6 @@ impl Server { } } -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, @@ -857,6 +886,7 @@ impl Dispatch> for State { let mut stream = UnixStream::from(fd); stream.write_all(&data[pos].data).unwrap(); } + wl_data_offer::Request::Destroy => {} other => todo!("unhandled request: {other:?}"), } }