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

120
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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<GlobalData>,
pub new_globals: Vec<Global>,
pub selection: Option<wayland_client::protocol::wl_data_device::WlDataDevice>,
pub selection_requests: Vec<(
String,
smithay_client_toolkit::data_device_manager::WritePipe,
)>,
pub cancelled: bool,
}
pub type ClientQueueHandle = QueueHandle<Globals>;
@ -65,7 +68,7 @@ pub struct ClientState {
pub queue: EventQueue<Globals>,
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::<Globals>();
let (global_list, queue) = registry_queue_init::<Globals>(&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<WlRegistry, ()> for Globals {
impl Dispatch<WlRegistry, GlobalListContents> for Globals {
fn event(
state: &mut Self,
_: &WlRegistry,
event: <WlRegistry as Proxy>::Event,
_: &(),
_: &GlobalListContents,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
) {
@ -124,7 +123,7 @@ impl Dispatch<WlRegistry, ()> for Globals {
version,
} = event
{
state.new_globals.push(GlobalData {
state.new_globals.push(Global {
name,
interface,
version,

128
src/data_device.rs Normal file
View file

@ -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<Self>,
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<Self>,
_: &wayland_client::protocol::wl_data_device::WlDataDevice,
) {
}
fn motion(
&mut self,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
_: &wayland_client::protocol::wl_data_device::WlDataDevice,
) {
}
fn leave(
&mut self,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
_: &wayland_client::protocol::wl_data_device::WlDataDevice,
) {
}
fn enter(
&mut self,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
_: &wayland_client::protocol::wl_data_device::WlDataDevice,
) {
}
}
impl DataSourceHandler for Globals {
fn send_request(
&mut self,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
_: &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<Self>,
_: &wayland_client::protocol::wl_data_source::WlDataSource,
) {
self.cancelled = true;
}
fn action(
&mut self,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
_: &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<Self>,
_: &wayland_client::protocol::wl_data_source::WlDataSource,
) {
}
fn dnd_dropped(
&mut self,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
_: &wayland_client::protocol::wl_data_source::WlDataSource,
) {
}
fn accept_mime(
&mut self,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
_: &wayland_client::protocol::wl_data_source::WlDataSource,
_: Option<String>,
) {
}
}
impl DataOfferHandler for Globals {
fn selected_action(
&mut self,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
_: &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<Self>,
_: &mut smithay_client_toolkit::data_device_manager::data_offer::DragOffer,
_: wayland_client::protocol::wl_data_device_manager::DndAction,
) {
}
}

View file

@ -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<Self>;
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<C: XConnection> {
fn create(state: &ServerState<C>) -> Self;
}
pub trait MimeTypeData {
fn name(&self) -> &str;
fn data(&self) -> &[u8];
}
type RealServerState = ServerState<Arc<xcb::Connection>>;
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);
}
}
}
}

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,18 +493,23 @@ 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! {
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
},
Enter {
serial,
|surface| state.get_server_surface_from_client(surface),
keys
},
Leave {
serial,
|surface| {
@ -532,6 +537,7 @@ impl HandleEvent for Keyboard {
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,92 +379,17 @@ impl ObjectMapExt for ObjectMap {
}
}
new_key_type! {
pub struct ObjectKey;
}
pub struct ServerState<C: XConnection> {
pub atoms: Option<Atoms>,
dh: DisplayHandle,
clientside: ClientState,
objects: ObjectMap,
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;
impl<C: XConnection> ServerState<C> {
pub fn new(dh: DisplayHandle, server_connection: Option<UnixStream>) -> Self {
let mut 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, ());
dh.create_global::<Self, XwaylandShellV1, _>(1, ());
let mut ret = Self {
windows: HashMap::new(),
clientside,
atoms: None,
qh,
dh,
to_focus: None,
last_focused_toplevel: None,
connection: None,
objects: Default::default(),
associated_windows: Default::default(),
xdg_wm_base,
};
ret.handle_new_globals();
ret
}
pub fn clientside_fd(&self) -> BorrowedFd<'_> {
self.clientside.queue.as_fd()
}
pub fn connect(&mut self, connection: UnixStream) {
self.dh
.insert_client(connection, std::sync::Arc::new(()))
.unwrap();
}
pub fn set_x_connection(&mut self, connection: C) {
self.connection = Some(connection);
}
fn handle_new_globals(&mut self) {
let globals = std::mem::take(&mut self.clientside.globals.new_globals);
for data in globals {
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 data.interface {
match global.interface {
$(
ref x if x == <$global>::interface().name => {
self.dh.create_global::<Self, $global, GlobalData>(data.version, data);
dh.create_global::<ServerState<C>, $global, Global>(global.version, global.clone());
}
)+
_ => {}
@ -480,6 +412,89 @@ impl<C: XConnection> ServerState<C> {
}
}
new_key_type! {
pub struct ObjectKey;
}
pub struct ServerState<C: XConnection> {
pub atoms: Option<Atoms>,
dh: DisplayHandle,
clientside: ClientState,
objects: ObjectMap,
associated_windows: SparseSecondaryMap<ObjectKey, x::Window>,
windows: HashMap<x::Window, WindowData>,
qh: ClientQueueHandle,
to_focus: Option<x::Window>,
last_focused_toplevel: Option<x::Window>,
connection: Option<C>,
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 clientside = ClientState::new(server_connection);
let qh = clientside.qh.clone();
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));
Self {
windows: HashMap::new(),
clientside,
atoms: None,
qh,
dh,
to_focus: None,
last_focused_toplevel: None,
connection: None,
objects: Default::default(),
associated_windows: Default::default(),
xdg_wm_base,
clipboard_data,
last_kb_serial: None,
}
}
pub fn clientside_fd(&self) -> BorrowedFd<'_> {
self.clientside.queue.as_fd()
}
pub fn connect(&mut self, connection: UnixStream) {
self.dh
.insert_client(connection, std::sync::Arc::new(()))
.unwrap();
}
pub fn set_x_connection(&mut self, connection: C) {
self.connection = Some(connection);
}
fn handle_new_globals(&mut self) {
let globals = std::mem::take(&mut self.clientside.globals.new_globals);
handle_globals::<C>(&self.dh, globals.iter());
}
fn get_object_from_client_object<T, P: Proxy>(&self, proxy: &P) -> &T
where
for<'a> &'a T: TryFrom<&'a Object, Error = String>,
@ -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() {}

View file

@ -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<xcb::Connection>,
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<xcb::Connection>,
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<WindowAttributes> {
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<u32> for SetState {
impl super::XConnection for Arc<xcb::Connection> {
type ExtraData = Atoms;
type MimeTypeData = SelectionTarget;
fn root_window(&self) -> x::Window {
self.get_setup().roots().next().unwrap().root()

387
src/xstate/selection.rs Normal file
View file

@ -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<u8>),
U16(Vec<u16>),
U32(Vec<u32>),
Foreign,
}
pub struct SelectionTarget {
name: String,
atom: x::Atom,
value: Option<TargetValue>,
}
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<u32>,
mime_types: Rc<Vec<SelectionTarget>>,
/// List of property on self.wm_window and corresponding index in mime_types
mime_destinations: Vec<(x::Atom, usize)>,
foreign_data: Option<ForeignSelection>,
}
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));
}
}

View file

@ -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<P: x::PropEl>(
&self,
window: x::Window,
@ -331,6 +338,17 @@ impl Connection {
"Did not get any X11 events"
);
}
#[track_caller]
fn get_reply<R: xcb::Request>(
&self,
req: &R,
) -> <R::Cookie as xcb::CookieWithReplyChecked>::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<u8> = 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);
}
}

View file

@ -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"] }

View file

@ -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<WlBuffer>,
pub last_damage: Option<BufferDamage>,
pub role: Option<SurfaceRole>,
pub last_enter_serial: Option<u32>,
}
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<String>,
}
struct State {
surfaces: HashMap<SurfaceId, SurfaceData>,
positioners: HashMap<PositionerId, PositionerState>,
@ -144,7 +158,30 @@ struct State {
last_surface_id: Option<SurfaceId>,
callbacks: Vec<WlCallback>,
pointer: Option<WlPointer>,
keyboard: Option<WlKeyboard>,
configure_serial: u32,
selection: Option<WlDataSource>,
data_device_man: Option<WlDataDeviceManager>,
data_device: Option<WlDataDevice>,
}
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<xdg_toplevel::State>,
) {
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<u8> = 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::<State, WlShm, _>(1, ());
dh.create_global::<State, XdgWmBase, _>(6, ());
dh.create_global::<State, WlSeat, _>(5, ());
dh.create_global::<State, WlDataDeviceManager, _>(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<String> {
let Some(selection) = &self.state.selection else {
panic!("No selection set on data device");
};
let data: &Mutex<DataSourceData> = 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<PasteData>) {
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<DataSourceData> = 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<PasteData> {
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<u8>,
}
simple_global_dispatch!(WlShm);
simple_global_dispatch!(WlCompositor);
simple_global_dispatch!(XdgWmBase);
impl GlobalDispatch<WlDataDeviceManager, ()> for State {
fn bind(
state: &mut Self,
_: &DisplayHandle,
_: &Client,
resource: wayland_server::New<WlDataDeviceManager>,
_: &(),
data_init: &mut wayland_server::DataInit<'_, Self>,
) {
state.data_device_man = Some(data_init.init(resource, ()));
}
}
impl Dispatch<WlDataOffer, Vec<PasteData>> for State {
fn request(
_: &mut Self,
_: &Client,
_: &WlDataOffer,
request: <WlDataOffer as Resource>::Request,
data: &Vec<PasteData>,
_: &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<WlDataSource, Mutex<DataSourceData>> for State {
fn request(
state: &mut Self,
_: &Client,
_: &WlDataSource,
request: <WlDataSource as Resource>::Request,
data: &Mutex<DataSourceData>,
_: &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<WlDataDevice, WlSeat> for State {
fn request(
state: &mut Self,
_: &Client,
_: &WlDataDevice,
request: <WlDataDevice as Resource>::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<WlDataDeviceManager, ()> for State {
fn request(
state: &mut Self,
_: &Client,
_: &WlDataDeviceManager,
request: <WlDataDeviceManager as Resource>::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<WlSeat, ()> for State {
fn bind(
_: &mut Self,
@ -361,7 +591,7 @@ impl GlobalDispatch<WlSeat, ()> 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<WlSeat, ()> 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<WlPointer, ()> for State {
}
}
impl Dispatch<WlKeyboard, ()> for State {
fn request(
_: &mut Self,
_: &Client,
_: &WlKeyboard,
request: <WlKeyboard as Resource>::Request,
_: &(),
_: &DisplayHandle,
_: &mut wayland_server::DataInit<'_, Self>,
) {
match request {
wl_keyboard::Request::Release => {}
other => todo!("unhandled request {other:?}"),
}
}
}
impl Dispatch<XdgPopup, SurfaceId> for State {
fn request(
_: &mut Self,
@ -787,6 +1037,7 @@ impl Dispatch<WlCompositor, ()> for State {
buffer: None,
last_damage: None,
role: None,
last_enter_serial: None,
},
);
state.last_surface_id = Some(SurfaceId(id));