Support primary selection

This was more tedious than expected.
Fixes #103
This commit is contained in:
Shawn Wallace 2025-08-14 01:30:04 -04:00
parent 13469566b0
commit 5a184d4359
11 changed files with 1086 additions and 391 deletions

View file

@ -5,6 +5,7 @@ use crate::server::{NoConnection, PendingSurfaceState, ServerState};
use crate::xstate::{RealConnection, XState};
use log::{error, info};
use rustix::event::{poll, PollFd, PollFlags};
use server::selection::{Clipboard, Primary};
use smithay_client_toolkit::data_device_manager::WritePipe;
use std::io::{BufRead, BufReader, Read, Write};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
@ -212,10 +213,14 @@ pub fn main(mut data: impl RunData) -> Option<()> {
server_state.run();
display.flush_clients().unwrap();
if let Some(sel) = server_state.new_selection() {
if let Some(sel) = server_state.new_selection::<Clipboard>() {
xstate.set_clipboard(sel);
}
if let Some(sel) = server_state.new_selection::<Primary>() {
xstate.set_primary_selection(sel);
}
if let Some(scale) = server_state.new_global_scale() {
xstate.update_global_scale(scale);
}

View file

@ -7,7 +7,12 @@ use smithay_client_toolkit::{
data_offer::{DataOfferHandler, SelectionOffer},
data_source::DataSourceHandler,
},
delegate_activation, delegate_data_device,
delegate_activation, delegate_data_device, delegate_primary_selection,
primary_selection::{
device::{PrimarySelectionDeviceData, PrimarySelectionDeviceHandler},
offer::PrimarySelectionOffer,
selection::PrimarySelectionSourceHandler,
},
};
use std::sync::{mpsc, Mutex, OnceLock};
use wayland_client::protocol::{
@ -41,6 +46,11 @@ use wayland_protocols::{
zwp_locked_pointer_v1::ZwpLockedPointerV1,
zwp_pointer_constraints_v1::ZwpPointerConstraintsV1,
},
primary_selection::zv1::client::{
zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1,
zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1,
zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1,
},
tablet::zv2::client::{
zwp_tablet_manager_v2::ZwpTabletManagerV2,
zwp_tablet_pad_group_v2::{ZwpTabletPadGroupV2, EVT_RING_OPCODE, EVT_STRIP_OPCODE},
@ -73,18 +83,33 @@ use wayland_server::protocol as server;
use wl_drm::client::wl_drm::WlDrm;
use xcb::x;
pub(super) struct SelectionEvents<T> {
pub offer: Option<T>,
pub requests: Vec<(
String,
smithay_client_toolkit::data_device_manager::WritePipe,
)>,
pub cancelled: bool,
}
impl<T> Default for SelectionEvents<T> {
fn default() -> Self {
Self {
offer: None,
requests: Default::default(),
cancelled: false,
}
}
}
pub(super) struct MyWorld {
pub world: World,
pub global_list: GlobalList,
pub new_globals: Vec<Global>,
events: Vec<(Entity, ObjectEvent)>,
queued_events: Vec<mpsc::Receiver<(Entity, ObjectEvent)>>,
pub selection_offer: Option<SelectionOffer>,
pub selection_requests: Vec<(
String,
smithay_client_toolkit::data_device_manager::WritePipe,
)>,
pub selection_cancelled: bool,
pub clipboard: SelectionEvents<SelectionOffer>,
pub primary: SelectionEvents<PrimarySelectionOffer>,
pub pending_activations: Vec<(xcb::x::Window, String)>,
}
@ -96,9 +121,8 @@ impl MyWorld {
new_globals: Vec::new(),
events: Vec::new(),
queued_events: Vec::new(),
selection_offer: None,
selection_requests: Vec::new(),
selection_cancelled: false,
clipboard: Default::default(),
primary: Default::default(),
pending_activations: Vec::new(),
}
}
@ -156,6 +180,7 @@ delegate_noop!(MyWorld: XdgActivationV1);
delegate_noop!(MyWorld: ZxdgDecorationManagerV1);
delegate_noop!(MyWorld: WpFractionalScaleManagerV1);
delegate_noop!(MyWorld: ignore ZxdgToplevelDecorationV1);
delegate_noop!(MyWorld: ZwpPrimarySelectionDeviceManagerV1);
impl Dispatch<WlRegistry, GlobalListContents> for MyWorld {
fn event(
@ -387,7 +412,7 @@ impl DataDeviceHandler for MyWorld {
data_device: &wayland_client::protocol::wl_data_device::WlDataDevice,
) {
let data: &DataDeviceData = data_device.data().unwrap();
self.selection_offer = data.selection_offer();
self.clipboard.offer = data.selection_offer();
}
fn drop_performed(
@ -437,7 +462,7 @@ impl DataSourceHandler for MyWorld {
mime: String,
fd: smithay_client_toolkit::data_device_manager::WritePipe,
) {
self.selection_requests.push((mime, fd));
self.clipboard.requests.push((mime, fd));
}
fn cancelled(
@ -446,7 +471,7 @@ impl DataSourceHandler for MyWorld {
_: &wayland_client::QueueHandle<Self>,
_: &wayland_client::protocol::wl_data_source::WlDataSource,
) {
self.selection_cancelled = true;
self.clipboard.cancelled = true;
}
fn action(
@ -538,3 +563,42 @@ impl ActivationHandler for MyWorld {
self.pending_activations.push((data.window, token));
}
}
delegate_primary_selection!(MyWorld);
impl PrimarySelectionDeviceHandler for MyWorld {
fn selection(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
primary_selection_device: &ZwpPrimarySelectionDeviceV1,
) {
let Some(data) = primary_selection_device.data::<PrimarySelectionDeviceData>() else {
return;
};
self.primary.offer = data.selection_offer();
}
}
impl PrimarySelectionSourceHandler for MyWorld {
fn send_request(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &ZwpPrimarySelectionSourceV1,
mime: String,
write_pipe: smithay_client_toolkit::data_device_manager::WritePipe,
) {
self.primary.requests.push((mime, write_pipe));
}
fn cancelled(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &ZwpPrimarySelectionSourceV1,
) {
self.primary.cancelled = true;
}
}

View file

@ -1341,9 +1341,8 @@ impl<S: X11Selection> GlobalDispatch<WlSeat, Global> for InnerServerState<S> {
.global_list
.registry()
.bind::<client::wl_seat::WlSeat, _, _>(data.name, server.version(), &state.qh, entity);
if let Some(c) = &mut state.clipboard_data {
c.device = Some(c.manager.get_data_device(&state.qh, &client));
}
state.selection_states.seat_created(&state.qh, &client);
state.world.spawn_at(entity, (server, client));
}
}

View file

@ -32,6 +32,7 @@ use wayland_protocols::{
zwp_tablet_v2::ZwpTabletV2 as TabletServer,
},
},
viewporter::client::wp_viewport::WpViewport,
},
xdg::{
shell::client::{xdg_popup, xdg_surface, xdg_toplevel},

View file

@ -1,6 +1,7 @@
mod clientside;
mod dispatch;
mod event;
pub(crate) mod selection;
#[cfg(test)]
mod tests;
@ -12,16 +13,10 @@ use hecs::{Entity, World};
use log::{debug, warn};
use rustix::event::{poll, PollFd, PollFlags};
use smithay_client_toolkit::activation::ActivationState;
use smithay_client_toolkit::data_device_manager::{
data_device::DataDevice, data_offer::SelectionOffer, data_source::CopyPasteSource,
DataDeviceManagerState,
};
use std::collections::{HashMap, HashSet};
use std::io::Read;
use std::ops::{Deref, DerefMut};
use std::os::fd::{AsFd, BorrowedFd};
use std::os::unix::net::UnixStream;
use std::rc::{Rc, Weak};
use wayland_client::{
globals::{registry_queue_init, Global},
protocol as client, Connection, EventQueue, Proxy, QueueHandle,
@ -47,7 +42,7 @@ use wayland_protocols::{
zwp_tablet_pad_v2, zwp_tablet_seat_v2, zwp_tablet_tool_v2, zwp_tablet_v2,
},
tablet::zv2::server::zwp_tablet_manager_v2::ZwpTabletManagerV2,
viewporter::client::{wp_viewport::WpViewport, wp_viewporter::WpViewporter},
viewporter::client::wp_viewporter::WpViewporter,
},
xdg::{
shell::client::{
@ -454,7 +449,7 @@ pub struct InnerServerState<S: X11Selection> {
viewporter: WpViewporter,
fractional_scale: Option<WpFractionalScaleManagerV1>,
decoration_manager: Option<ZxdgDecorationManagerV1>,
clipboard_data: Option<ClipboardData<S>>,
selection_states: selection::SelectionStates<S>,
last_kb_serial: Option<(client::wl_seat::WlSeat, u32)>,
activation_state: Option<ActivationState>,
global_output_offset: GlobalOutputOffset,
@ -494,17 +489,6 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
.inspect_err(|e| warn!("Couldn't bind fractional scale manager: {e}. Fractional scaling will not work."))
.ok();
let manager = DataDeviceManagerState::bind(&global_list, &qh)
.inspect_err(|e| {
warn!("Could not bind data device manager ({e:?}). Clipboard will not work.")
})
.ok();
let clipboard_data = manager.map(|manager| ClipboardData {
manager,
device: None,
source: None::<CopyPasteData<S>>,
});
let activation_state = ActivationState::bind(&global_list, &qh)
.inspect_err(|e| {
warn!("Could not bind xdg activation ({e:?}). Windows might not receive focus depending on compositor focus stealing policy.")
@ -518,7 +502,10 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
})
.ok();
let selection_states = selection::SelectionStates::new(&global_list, &qh);
dh.create_global::<InnerServerState<S>, XwaylandShellV1, _>(1, ());
global_list
.contents()
.with_list(|globals| handle_globals::<S>(&dh, globals));
@ -539,7 +526,7 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
xdg_wm_base,
viewporter,
fractional_scale,
clipboard_data,
selection_states,
last_kb_serial: None,
activation_state,
global_output_offset: GlobalOutputOffset {
@ -691,7 +678,7 @@ impl<C: XConnection> ServerState<C> {
self.unfocus = false;
}
self.handle_clipboard_events();
self.handle_selection_events();
self.handle_activations();
self.queue
.flush()
@ -1113,78 +1100,10 @@ impl<S: X11Selection + 'static> InnerServerState<S> {
}
}
pub(crate) fn set_copy_paste_source(&mut self, selection: &Rc<S>) {
if let Some(d) = &mut self.clipboard_data {
let src = d
.manager
.create_copy_paste_source(&self.qh, selection.mime_types());
let data = CopyPasteData::X11 {
inner: src,
data: Rc::downgrade(selection),
};
let CopyPasteData::X11 { inner, .. } = d.source.insert(data) else {
unreachable!();
};
if let Some(serial) = self
.last_kb_serial
.as_ref()
.map(|(_seat, serial)| serial)
.copied()
{
inner.set_selection(d.device.as_ref().unwrap(), serial);
}
}
}
pub fn new_global_scale(&mut self) -> Option<f64> {
self.new_scale.take()
}
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) {
if let Some(clipboard) = self.clipboard_data.as_mut() {
for (mime_type, fd) in std::mem::take(&mut self.world.selection_requests) {
let CopyPasteData::X11 { data, .. } = clipboard.source.as_ref().unwrap() else {
unreachable!("Got selection request without having set the selection?")
};
if let Some(data) = data.upgrade() {
data.write_to(&mime_type, fd);
}
}
if self.world.selection_cancelled {
clipboard.source = None;
self.world.selection_cancelled = false;
}
if clipboard.source.is_none() {
if let Some(offer) = self.world.selection_offer.take() {
if offer.inner().is_alive() {
let mime_types: Box<[String]> = offer.with_mime_types(|mimes| mimes.into());
let foreign = ForeignSelection {
mime_types,
inner: offer,
};
clipboard.source = Some(CopyPasteData::Foreign(foreign));
} else {
clipboard.source = None;
}
}
}
}
}
fn handle_activations(&mut self) {
let Some(activation_state) = self.activation_state.as_ref() else {
return;
@ -1444,42 +1363,3 @@ pub struct PendingSurfaceState {
pub width: i32,
pub height: i32,
}
struct ClipboardData<X: X11Selection> {
manager: DataDeviceManagerState,
device: Option<DataDevice>,
source: Option<CopyPasteData<X>>,
}
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.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<X: X11Selection> {
X11 {
inner: CopyPasteSource,
data: Weak<X>,
},
Foreign(ForeignSelection),
}

287
src/server/selection.rs Normal file
View file

@ -0,0 +1,287 @@
use super::clientside::SelectionEvents;
use super::{InnerServerState, MyWorld, ServerState};
use crate::{X11Selection, XConnection};
use log::{info, warn};
use smithay_client_toolkit::data_device_manager::ReadPipe;
use wayland_client::globals::GlobalList;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Proxy, QueueHandle};
use smithay_client_toolkit::data_device_manager::{
data_device::DataDevice, data_offer::SelectionOffer as WlSelectionOffer,
data_source::CopyPasteSource, DataDeviceManagerState,
};
use smithay_client_toolkit::primary_selection::device::PrimarySelectionDevice;
use smithay_client_toolkit::primary_selection::offer::PrimarySelectionOffer;
use smithay_client_toolkit::primary_selection::selection::PrimarySelectionSource;
use smithay_client_toolkit::primary_selection::PrimarySelectionManagerState;
use std::io::Read;
use std::rc::{Rc, Weak};
pub(super) struct SelectionStates<S: X11Selection> {
clipboard: Option<SelectionState<S, Clipboard>>,
primary: Option<SelectionState<S, Primary>>,
}
impl<S: X11Selection> SelectionStates<S> {
pub fn new(global_list: &GlobalList, qh: &QueueHandle<MyWorld>) -> Self {
Self {
clipboard: DataDeviceManagerState::bind(global_list, qh)
.inspect_err(|e| {
warn!("Could not bind data device manager ({e:?}). Clipboard will not work.")
})
.ok()
.map(SelectionState::new),
primary: PrimarySelectionManagerState::bind(global_list, qh)
.inspect_err(|_| info!("Primary selection unsupported."))
.ok()
.map(SelectionState::new),
}
}
pub fn seat_created(&mut self, qh: &QueueHandle<MyWorld>, seat: &WlSeat) {
if let Some(c) = &mut self.clipboard {
c.device = Some(c.manager.get_data_device(qh, seat));
}
if let Some(d) = &mut self.primary {
d.device = Some(d.manager.get_selection_device(qh, seat));
}
}
}
enum SelectionData<S: X11Selection, T: SelectionType> {
X11 { inner: T::Source, data: Weak<S> },
Foreign(ForeignSelection<T>),
}
struct SelectionState<S: X11Selection, T: SelectionType> {
manager: T::Manager,
device: Option<T::DataDevice>,
source: Option<SelectionData<S, T>>,
}
impl<S: X11Selection, T: SelectionType> SelectionState<S, T> {
fn new(manager: T::Manager) -> Self {
Self {
manager,
device: None,
source: None,
}
}
}
impl<S: X11Selection> InnerServerState<S> {
pub(super) fn handle_selection_events(&mut self) {
self.handle_impl::<Clipboard>();
self.handle_impl::<Primary>();
}
fn handle_impl<T: SelectionType>(&mut self) {
let Some(state) = T::selection_state(&mut self.selection_states) else {
return;
};
let events = T::get_events(&mut self.world);
for (mime_type, fd) in std::mem::take(&mut events.requests) {
let SelectionData::X11 { data, .. } = state.source.as_ref().unwrap() else {
unreachable!("Got selection request without having set the selection?")
};
if let Some(data) = data.upgrade() {
data.write_to(&mime_type, fd);
}
}
if events.cancelled {
state.source = None;
events.cancelled = false;
}
if state.source.is_none() {
if let Some(offer) = T::take_offer(&mut events.offer) {
let mime_types = T::get_mimes(&offer);
let foreign = ForeignSelection {
mime_types,
inner: offer,
};
state.source = Some(SelectionData::Foreign(foreign));
}
}
}
pub(crate) fn set_selection_source<T: SelectionType>(&mut self, selection: &Rc<S>) {
if let Some(state) = T::selection_state(&mut self.selection_states) {
let src = T::create_source(&state.manager, &self.qh, selection.mime_types());
let data = SelectionData::X11 {
inner: src,
data: Rc::downgrade(selection),
};
let SelectionData::X11 { inner, .. } = state.source.insert(data) else {
unreachable!();
};
if let Some(serial) = self
.last_kb_serial
.as_ref()
.map(|(_seat, serial)| serial)
.copied()
{
T::set_selection(inner, state.device.as_ref().unwrap(), serial);
}
}
}
pub(crate) fn new_selection<T: SelectionType>(&mut self) -> Option<ForeignSelection<T>> {
T::selection_state(&mut self.selection_states)
.as_mut()
.and_then(|state| {
state.source.take().and_then(|s| match s {
SelectionData::Foreign(f) => Some(f),
SelectionData::X11 { .. } => {
state.source = Some(s);
None
}
})
})
}
}
pub struct ForeignSelection<T: SelectionType> {
pub mime_types: Box<[String]>,
inner: T::Offer,
}
#[allow(private_bounds)]
impl<T: SelectionType> ForeignSelection<T> {
pub(crate) fn receive(
&self,
mime_type: String,
state: &ServerState<impl XConnection>,
) -> Vec<u8> {
let mut pipe = T::receive_offer(&self.inner, mime_type).unwrap();
state.queue.flush().unwrap();
let mut data = Vec::new();
pipe.read_to_end(&mut data).unwrap();
data
}
}
#[allow(private_bounds, private_interfaces)]
pub trait SelectionType: Sized {
type Source;
type Offer;
type Manager;
type DataDevice;
// The methods in this trait shouldn't be used outside of this file.
fn selection_state<S: X11Selection>(
state: &mut SelectionStates<S>,
) -> &mut Option<SelectionState<S, Self>>;
fn create_source(
manager: &Self::Manager,
qh: &QueueHandle<MyWorld>,
mime_types: Vec<&str>,
) -> Self::Source;
fn set_selection(source: &Self::Source, device: &Self::DataDevice, serial: u32);
fn get_events(world: &mut MyWorld) -> &mut SelectionEvents<Self::Offer>;
fn receive_offer(offer: &Self::Offer, mime_type: String) -> std::io::Result<ReadPipe>;
fn take_offer(offer: &mut Option<Self::Offer>) -> Option<Self::Offer> {
offer.take()
}
fn get_mimes(offer: &Self::Offer) -> Box<[String]>;
}
pub enum Clipboard {}
pub enum Primary {}
#[allow(private_bounds, private_interfaces)]
impl SelectionType for Clipboard {
type Source = CopyPasteSource;
type Offer = WlSelectionOffer;
type Manager = DataDeviceManagerState;
type DataDevice = DataDevice;
fn selection_state<S: X11Selection>(
state: &mut SelectionStates<S>,
) -> &mut Option<SelectionState<S, Self>> {
&mut state.clipboard
}
fn create_source(
manager: &Self::Manager,
qh: &QueueHandle<MyWorld>,
mime_types: Vec<&str>,
) -> Self::Source {
manager.create_copy_paste_source(qh, mime_types)
}
fn set_selection(source: &Self::Source, device: &Self::DataDevice, serial: u32) {
source.set_selection(device, serial);
}
fn get_events(world: &mut MyWorld) -> &mut SelectionEvents<Self::Offer> {
&mut world.clipboard
}
fn take_offer(offer: &mut Option<Self::Offer>) -> Option<Self::Offer> {
offer.take().filter(|offer| offer.inner().is_alive())
}
fn get_mimes(offer: &Self::Offer) -> Box<[String]> {
offer.with_mime_types(|mimes| mimes.into())
}
fn receive_offer(offer: &Self::Offer, mime_type: String) -> std::io::Result<ReadPipe> {
offer.receive(mime_type).map_err(|e| {
match e {
smithay_client_toolkit::data_device_manager::data_offer::DataOfferError::InvalidReceive => std::io::Error::from(std::io::ErrorKind::Other),
smithay_client_toolkit::data_device_manager::data_offer::DataOfferError::Io(e) => e
}
})
}
}
#[allow(private_bounds, private_interfaces)]
impl SelectionType for Primary {
type Source = PrimarySelectionSource;
type Offer = PrimarySelectionOffer;
type Manager = PrimarySelectionManagerState;
type DataDevice = PrimarySelectionDevice;
fn selection_state<S: X11Selection>(
state: &mut SelectionStates<S>,
) -> &mut Option<SelectionState<S, Self>> {
&mut state.primary
}
fn create_source(
manager: &Self::Manager,
qh: &QueueHandle<MyWorld>,
mime_types: Vec<&str>,
) -> Self::Source {
manager.create_selection_source(qh, mime_types)
}
fn set_selection(source: &Self::Source, device: &Self::DataDevice, serial: u32) {
source.set_selection(device, serial);
}
fn get_events(world: &mut MyWorld) -> &mut SelectionEvents<Self::Offer> {
&mut world.primary
}
fn receive_offer(offer: &Self::Offer, mime_type: String) -> std::io::Result<ReadPipe> {
offer.receive(mime_type)
}
fn get_mimes(offer: &Self::Offer) -> Box<[String]> {
offer.with_mime_types(|mimes| mimes.into())
}
}

View file

@ -1,4 +1,5 @@
use super::{InnerServerState, NoConnection, ServerState, WindowDims};
use super::{selection::Clipboard, InnerServerState, NoConnection, ServerState, WindowDims};
use crate::server::selection::{Primary, SelectionType};
use crate::xstate::{SetState, WinSize, WmName};
use crate::XConnection;
use rustix::event::{poll, PollFd, PollFlags};
@ -7,6 +8,7 @@ use std::io::Write;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::os::unix::net::UnixStream;
use std::sync::{Arc, Mutex};
use testwl::SendDataForMimeFn;
use wayland_client::{
backend::{protocol::Message, Backend, ObjectData, ObjectId, WaylandError},
protocol::{
@ -1267,8 +1269,70 @@ fn window_group_properties() {
assert_eq!(data.toplevel().app_id, Some("class".into()));
}
#[test]
fn copy_from_x11() {
trait SelectionTest {
type SelectionType: SelectionType;
fn mimes(testwl: &mut testwl::Server) -> Vec<String>;
fn paste_data(
testwl: &mut testwl::Server,
send_data: impl SendDataForMimeFn,
) -> Vec<testwl::PasteData>;
fn create_offer(testwl: &mut testwl::Server, data: Vec<testwl::PasteData>);
}
macro_rules! selection_tests {
($name:ident, $selection_type:ty, $get_mime_fn:ident, $get_paste_data_fn:ident, $create_offer_fn:ident) => {
impl SelectionTest for $selection_type {
type SelectionType = $selection_type;
fn mimes(testwl: &mut testwl::Server) -> Vec<String> {
testwl.$get_mime_fn()
}
fn paste_data(
testwl: &mut testwl::Server,
send_data: impl SendDataForMimeFn,
) -> Vec<testwl::PasteData> {
testwl.$get_paste_data_fn(send_data)
}
fn create_offer(testwl: &mut testwl::Server, data: Vec<testwl::PasteData>) {
testwl.$create_offer_fn(data);
}
}
mod $name {
use super::*;
#[test]
fn copy_from_x11() {
super::copy_from_x11::<$selection_type>();
}
#[test]
fn copy_from_wayland() {
super::copy_from_wayland::<$selection_type>();
}
#[test]
fn x11_then_wayland() {
super::selection_x11_then_wayland::<$selection_type>();
}
}
};
}
selection_tests!(
clipboard,
Clipboard,
data_source_mimes,
clipboard_paste_data,
create_data_offer
);
selection_tests!(
primary,
Primary,
primary_source_mimes,
primary_paste_data,
create_primary_offer
);
fn copy_from_x11<T: SelectionTest>() {
let (mut f, comp) = TestFixture::new_with_compositor();
let win = unsafe { Window::new(1) };
let (_surface, _id) = f.create_toplevel(&comp, win);
@ -1284,23 +1348,22 @@ fn copy_from_x11() {
},
]);
f.satellite.set_copy_paste_source(&mimes);
f.satellite.set_selection_source::<T::SelectionType>(&mimes);
f.run();
let server_mimes = f.testwl.data_source_mimes();
let server_mimes = T::mimes(&mut f.testwl);
for mime in mimes.iter() {
assert!(server_mimes.contains(&mime.mime_type));
}
let data = f.testwl.paste_data(|_, _| {
let data = T::paste_data(&mut f.testwl, |_, _| {
f.satellite.run();
true
});
assert_eq!(*mimes, data);
}
#[test]
fn copy_from_wayland() {
fn copy_from_wayland<T: SelectionTest>() {
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) };
@ -1316,10 +1379,13 @@ fn copy_from_wayland() {
data: vec![1, 2, 3, 4, 6, 10],
},
];
f.testwl.create_data_offer(mimes.clone());
T::create_offer(&mut f.testwl, mimes.clone());
f.run();
let selection = f.satellite.new_selection().expect("No new selection");
let selection = f
.satellite
.new_selection::<T::SelectionType>()
.expect("No new selection");
for mime in &mimes {
let data = std::thread::scope(|s| {
// receive requires a queue flush - dispatch testwl from another thread
@ -1341,8 +1407,7 @@ fn copy_from_wayland() {
}
}
#[test]
fn clipboard_x11_then_wayland() {
fn selection_x11_then_wayland<T: SelectionTest>() {
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) };
@ -1359,7 +1424,8 @@ fn clipboard_x11_then_wayland() {
},
]);
f.satellite.set_copy_paste_source(&x11data);
f.satellite
.set_selection_source::<T::SelectionType>(&x11data);
f.run();
let waylanddata = vec![
@ -1372,11 +1438,14 @@ fn clipboard_x11_then_wayland() {
data: vec![10, 20, 40, 50],
},
];
f.testwl.create_data_offer(waylanddata.clone());
T::create_offer(&mut f.testwl, waylanddata.clone());
f.run();
f.run();
let selection = f.satellite.new_selection().expect("No new selection");
let selection = f
.satellite
.new_selection::<T::SelectionType>()
.expect("No new selection");
for mime in &waylanddata {
let data = std::thread::scope(|s| {
// receive requires a queue flush - dispatch testwl from another thread
@ -2429,7 +2498,7 @@ fn quick_empty_data_offer() {
f.testwl.empty_data_offer();
f.run();
let selection = f.satellite.new_selection();
let selection = f.satellite.new_selection::<Clipboard>();
assert!(selection.is_none());
}

View file

@ -1,7 +1,7 @@
mod settings;
use settings::Settings;
mod selection;
use selection::{Selection, SelectionData};
use selection::{Selection, SelectionState};
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
use crate::XConnection;
@ -117,7 +117,7 @@ pub struct XState {
window_atoms: WindowTypes,
root: x::Window,
wm_window: x::Window,
selection_data: SelectionData,
selection_state: SelectionState,
settings: Settings,
}
@ -201,6 +201,15 @@ impl XState {
| SelectionEventMask::SELECTION_CLIENT_CLOSE,
})
.unwrap();
connection
.send_and_check_request(&xcb::xfixes::SelectSelectionInput {
window: root,
selection: atoms.primary,
event_mask: SelectionEventMask::SET_SELECTION_OWNER
| SelectionEventMask::SELECTION_WINDOW_DESTROY
| SelectionEventMask::SELECTION_CLIENT_CLOSE,
})
.unwrap();
{
// Setup default cursor theme
let ctx = CursorContext::new(&connection, screen).unwrap();
@ -214,7 +223,7 @@ impl XState {
}
let wm_window = connection.generate_id();
let selection_data = SelectionData::new(&connection, root);
let selection_state = SelectionState::new(&connection, root, &atoms);
let window_atoms = WindowTypes::intern_all(&connection).unwrap();
let settings = Settings::new(&connection, &atoms, root);
@ -224,7 +233,7 @@ impl XState {
root,
atoms,
window_atoms,
selection_data,
selection_state,
settings,
};
r.create_ewmh_window();
@ -917,6 +926,7 @@ xcb::atoms_struct! {
incr => b"INCR" only_if_exists = false,
xsettings => b"_XSETTINGS_S0" only_if_exists = false,
xsettings_settings => b"_XSETTINGS_SETTINGS" only_if_exists = false,
primary => b"PRIMARY" only_if_exists = false,
}
}

View file

@ -1,5 +1,5 @@
use super::{get_atom_name, XState};
use crate::server::ForeignSelection;
use crate::server::selection::{Clipboard, ForeignSelection, Primary, SelectionType};
use crate::{RealServerState, X11Selection};
use log::{debug, error, warn};
use smithay_client_toolkit::data_device_manager::WritePipe;
@ -26,7 +26,7 @@ pub struct Selection {
connection: Rc<xcb::Connection>,
window: x::Window,
pending: RefCell<Vec<PendingSelectionData>>,
clipboard: x::Atom,
selection: x::Atom,
selection_time: u32,
incr: x::Atom,
}
@ -46,13 +46,13 @@ impl X11Selection for Selection {
.connection
.send_and_check_request(&x::ConvertSelection {
requestor: self.window,
selection: self.clipboard,
selection: self.selection,
target: target.atom,
property: target.atom,
time: self.selection_time,
})
{
error!("Failed to request clipboard data (mime type: {mime}, error: {e})");
error!("Failed to request selection data (mime type: {mime}, error: {e})");
return;
}
@ -162,21 +162,275 @@ impl Selection {
}
}
enum CurrentSelection {
enum CurrentSelection<T: SelectionType> {
X11(Rc<Selection>),
Wayland {
mimes: Vec<SelectionTargetId>,
inner: ForeignSelection,
inner: ForeignSelection<T>,
},
}
pub(crate) struct SelectionData {
struct SelectionData<T: SelectionType> {
last_selection_timestamp: u32,
target_window: x::Window,
current_selection: Option<CurrentSelection>,
atom: x::Atom,
current_selection: Option<CurrentSelection<T>>,
}
impl SelectionData {
pub fn new(connection: &xcb::Connection, root: x::Window) -> Self {
// This is a trait so that we can use &dyn
trait SelectionDataImpl {
fn set_owner(&self, connection: &xcb::Connection, wm_window: x::Window);
fn handle_new_owner(
&mut self,
connection: &xcb::Connection,
wm_window: x::Window,
atoms: &super::Atoms,
owner: x::Window,
timestamp: u32,
);
fn handle_target_list(
&mut self,
connection: &Rc<xcb::Connection>,
wm_window: x::Window,
atoms: &super::Atoms,
target_window: x::Window,
dest_property: x::Atom,
server_state: &mut RealServerState,
);
fn x11_selection(&self) -> Option<&Selection>;
fn handle_selection_request(
&self,
connection: &xcb::Connection,
atoms: &super::Atoms,
request: &x::SelectionRequestEvent,
success: &dyn Fn(),
refuse: &dyn Fn(),
server_state: &mut RealServerState,
);
fn atom(&self) -> x::Atom;
}
impl<T: SelectionType> SelectionData<T> {
fn new(atom: x::Atom) -> Self {
Self {
last_selection_timestamp: x::CURRENT_TIME,
atom,
current_selection: None,
}
}
}
impl<T: SelectionType> SelectionDataImpl for SelectionData<T> {
fn atom(&self) -> x::Atom {
self.atom
}
fn set_owner(&self, connection: &xcb::Connection, wm_window: x::Window) {
connection
.send_and_check_request(&x::SetSelectionOwner {
owner: wm_window,
selection: self.atom,
time: self.last_selection_timestamp,
})
.unwrap();
let reply = connection
.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
selection: self.atom,
}))
.unwrap();
if reply.owner() != wm_window {
warn!(
"Could not get {} selection (owned by {:?})",
get_atom_name(connection, self.atom),
reply.owner()
);
}
}
fn handle_new_owner(
&mut self,
connection: &xcb::Connection,
wm_window: x::Window,
atoms: &super::Atoms,
owner: x::Window,
timestamp: u32,
) {
debug!(
"new {} owner: {owner:?}",
get_atom_name(connection, self.atom)
);
self.last_selection_timestamp = timestamp;
// Grab targets
connection
.send_and_check_request(&x::ConvertSelection {
requestor: wm_window,
selection: self.atom,
target: atoms.targets,
property: atoms.selection_reply,
time: timestamp,
})
.unwrap();
}
fn handle_target_list(
&mut self,
connection: &Rc<xcb::Connection>,
wm_window: x::Window,
atoms: &super::Atoms,
target_window: x::Window,
dest_property: x::Atom,
server_state: &mut RealServerState,
) {
let reply = connection
.wait_for_reply(connection.send_request(&x::GetProperty {
delete: true,
window: wm_window,
property: dest_property,
r#type: x::ATOM_ATOM,
long_offset: 0,
long_length: 20,
}))
.unwrap();
let targets: &[x::Atom] = reply.value();
if targets.is_empty() {
warn!("Got empty selection target list, trying again...");
match connection.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
selection: self.atom,
})) {
Ok(reply) => {
if reply.owner() == wm_window {
warn!("We are unexpectedly the selection owner? Clipboard may be broken!");
} else {
self.handle_new_owner(
connection,
wm_window,
atoms,
reply.owner(),
self.last_selection_timestamp,
);
}
}
Err(e) => {
error!("Couldn't grab selection owner: {e:?}. Clipboard is stale!");
}
}
return;
}
if log::log_enabled!(log::Level::Debug) {
let targets_str: Vec<String> = targets
.iter()
.map(|t| get_atom_name(connection, *t))
.collect();
debug!("got targets: {targets_str:?}");
}
let mimes = targets
.iter()
.copied()
.filter(|atom| ![atoms.targets, atoms.multiple, atoms.save_targets].contains(atom))
.map(|target_atom| SelectionTargetId {
name: get_atom_name(connection, target_atom),
atom: target_atom,
source: None,
})
.collect();
let selection = Rc::new(Selection {
mimes,
connection: connection.clone(),
window: target_window,
pending: RefCell::default(),
selection: self.atom,
selection_time: self.last_selection_timestamp,
incr: atoms.incr,
});
server_state.set_selection_source::<T>(&selection);
self.current_selection = Some(CurrentSelection::X11(selection));
debug!("{} set from X11", get_atom_name(connection, self.atom));
}
fn x11_selection(&self) -> Option<&Selection> {
match &self.current_selection {
Some(CurrentSelection::X11(selection)) => Some(selection),
_ => None,
}
}
fn handle_selection_request(
&self,
connection: &xcb::Connection,
atoms: &super::Atoms,
request: &x::SelectionRequestEvent,
success: &dyn Fn(),
refuse: &dyn Fn(),
server_state: &mut RealServerState,
) {
let Some(CurrentSelection::Wayland { mimes, inner }) = &self.current_selection else {
warn!("Got selection request, but we don't seem to be the selection owner");
refuse();
return;
};
match request.target() {
x if x == atoms.targets => {
let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect();
connection
.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: request.requestor(),
property: request.property(),
r#type: x::ATOM_ATOM,
data: &atoms,
})
.unwrap();
success();
}
other => {
let Some(target) = mimes.iter().find(|t| t.atom == other) else {
if log::log_enabled!(log::Level::Debug) {
let name = get_atom_name(connection, other);
debug!("refusing selection request because given atom could not be found ({name})");
}
refuse();
return;
};
let mime_name = target
.source
.as_ref()
.cloned()
.unwrap_or_else(|| target.name.clone());
let data = inner.receive(mime_name, server_state);
match connection.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: request.requestor(),
property: request.property(),
r#type: target.atom,
data: &data,
}) {
Ok(_) => success(),
Err(e) => {
warn!("Failed setting selection property: {e:?}");
refuse();
}
}
}
}
}
}
pub(super) struct SelectionState {
clipboard: SelectionData<Clipboard>,
primary: SelectionData<Primary>,
target_window: x::Window,
}
impl SelectionState {
pub fn new(connection: &xcb::Connection, root: x::Window, atoms: &super::Atoms) -> Self {
let target_window = connection.generate_id();
connection
.send_and_check_request(&x::CreateWindow {
@ -195,39 +449,15 @@ impl SelectionData {
})
.expect("Couldn't create window for selections");
Self {
last_selection_timestamp: x::CURRENT_TIME,
target_window,
current_selection: None,
clipboard: SelectionData::new(atoms.clipboard),
primary: SelectionData::new(atoms.primary),
}
}
}
impl XState {
fn set_clipboard_owner(&mut self) {
self.connection
.send_and_check_request(&x::SetSelectionOwner {
owner: self.wm_window,
selection: self.atoms.clipboard,
time: self.selection_data.last_selection_timestamp,
})
.unwrap();
let reply = self
.connection
.wait_for_reply(self.connection.send_request(&x::GetSelectionOwner {
selection: self.atoms.clipboard,
}))
.unwrap();
if reply.owner() != self.wm_window {
warn!(
"Could not get CLIPBOARD selection (owned by {:?})",
reply.owner()
);
}
}
pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) {
pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection<Clipboard>) {
let mut utf8_xwl = false;
let mut utf8_wl = false;
let mut mimes: Vec<SelectionTargetId> = selection
@ -273,51 +503,133 @@ impl XState {
});
}
self.selection_data.current_selection = Some(CurrentSelection::Wayland {
self.selection_state.clipboard.current_selection = Some(CurrentSelection::Wayland {
mimes,
inner: selection,
});
self.set_clipboard_owner();
self.selection_state
.clipboard
.set_owner(&self.connection, self.wm_window);
debug!("Clipboard set from Wayland");
}
pub(crate) fn set_primary_selection(&mut self, selection: ForeignSelection<Primary>) {
let mut utf8_xwl = false;
let mut utf8_wl = false;
let mut mimes: Vec<SelectionTargetId> = selection
.mime_types
.iter()
.map(|mime| {
match mime.as_str() {
"UTF8_STRING" => utf8_xwl = true,
"text/plain;charset=utf-8" => utf8_wl = true,
_ => {}
}
let atom = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: mime.as_bytes(),
}))
.unwrap();
SelectionTargetId {
name: mime.clone(),
atom: atom.atom(),
source: None,
}
})
.collect();
if utf8_wl && !utf8_xwl {
let name = "UTF8_STRING".to_string();
let atom = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: name.as_bytes(),
}))
.unwrap()
.atom();
mimes.push(SelectionTargetId {
name,
atom,
source: Some("text/plain;charset=utf-8".to_string()),
});
}
self.selection_state.primary.current_selection = Some(CurrentSelection::Wayland {
mimes,
inner: selection,
});
self.selection_state
.primary
.set_owner(&self.connection, self.wm_window);
debug!("Primaryset from Wayland");
}
pub(super) fn handle_selection_event(
&mut self,
event: &xcb::Event,
server_state: &mut RealServerState,
) -> bool {
macro_rules! get_selection_data {
($selection:expr) => {
match $selection {
x if x == self.atoms.clipboard => {
&mut self.selection_state.clipboard as &mut dyn SelectionDataImpl
}
x if x == self.atoms.primary => &mut self.selection_state.primary as _,
_ => return true,
}
};
}
match event {
xcb::Event::X(x::Event::SelectionClear(e)) => {
if e.selection() == self.atoms.clipboard {
self.handle_new_selection_owner(e.owner(), e.time());
}
let data = get_selection_data!(e.selection());
data.handle_new_owner(
&self.connection,
self.wm_window,
&self.atoms,
e.owner(),
e.time(),
);
}
xcb::Event::X(x::Event::SelectionNotify(e)) => {
if e.property() == x::ATOM_NONE {
warn!("selection notify fail?");
warn!(
"selection notify fail? {}",
get_atom_name(&self.connection, e.selection())
);
return true;
}
let data = get_selection_data!(e.selection());
debug!(
"selection notify requestor: {:?} target: {}",
"selection notify requestor: {:?} target: {} selection: {}",
e.requestor(),
get_atom_name(&self.connection, e.target())
get_atom_name(&self.connection, e.target()),
get_atom_name(&self.connection, e.selection()),
);
if e.requestor() == self.wm_window {
match e.target() {
x if x == self.atoms.targets => {
self.handle_target_list(e.property(), server_state)
}
x if x == self.atoms.targets => data.handle_target_list(
&self.connection,
self.wm_window,
&self.atoms,
self.selection_state.target_window,
e.property(),
server_state,
),
other => warn!(
"got unexpected selection notify for target {}",
get_atom_name(&self.connection, other)
),
}
} else if e.requestor() == self.selection_data.target_window {
if let Some(CurrentSelection::X11(selection)) =
&self.selection_data.current_selection
{
} else if e.requestor() == self.selection_state.target_window {
if let Some(selection) = data.x11_selection() {
selection.handle_notify(e.target());
}
} else {
@ -328,6 +640,7 @@ impl XState {
}
}
xcb::Event::X(x::Event::SelectionRequest(e)) => {
let data = get_selection_data!(e.selection());
let send_notify = |property| {
self.connection
.send_and_check_request(&x::SendEvent {
@ -349,7 +662,8 @@ impl XState {
if log::log_enabled!(log::Level::Debug) {
let target = get_atom_name(&self.connection, e.target());
debug!("Got selection request for target {target}");
let selection = get_atom_name(&self.connection, data.atom());
debug!("Got selection request for target {target} (selection: {selection})");
}
if e.property() == x::ATOM_NONE {
@ -358,76 +672,37 @@ impl XState {
return true;
}
let Some(CurrentSelection::Wayland { mimes, inner }) =
&self.selection_data.current_selection
else {
warn!("Got selection request, but we don't seem to be the selection owner");
refuse();
return true;
};
match e.target() {
x if x == self.atoms.targets => {
let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect();
self.connection
.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: e.requestor(),
property: e.property(),
r#type: x::ATOM_ATOM,
data: &atoms,
})
.unwrap();
success();
}
other => {
let Some(target) = mimes.iter().find(|t| t.atom == other) else {
if log::log_enabled!(log::Level::Debug) {
let name = get_atom_name(&self.connection, other);
debug!("refusing selection request because given atom could not be found ({name})");
}
refuse();
return true;
};
let mime_name = target
.source
.as_ref()
.cloned()
.unwrap_or_else(|| target.name.clone());
let data = inner.receive(mime_name, server_state);
match self.connection.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: e.requestor(),
property: e.property(),
r#type: target.atom,
data: &data,
}) {
Ok(_) => success(),
Err(e) => {
warn!("Failed setting selection property: {e:?}");
refuse();
}
}
}
}
data.handle_selection_request(
&self.connection,
&self.atoms,
e,
&success,
&refuse,
server_state,
);
}
xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => match e.selection() {
x if x == self.atoms.clipboard => match e.subtype() {
x if x == self.atoms.clipboard || x == self.atoms.primary => match e.subtype() {
xcb::xfixes::SelectionEvent::SetSelectionOwner => {
if e.owner() == self.wm_window {
return true;
}
self.handle_new_selection_owner(e.owner(), e.selection_timestamp());
let data = get_selection_data!(x);
data.handle_new_owner(
&self.connection,
self.wm_window,
&self.atoms,
e.owner(),
e.timestamp(),
);
}
xcb::xfixes::SelectionEvent::SelectionClientClose
| xcb::xfixes::SelectionEvent::SelectionWindowDestroy => {
debug!("Selection owner destroyed, selection will be unset");
self.selection_data.current_selection = None;
self.selection_state.clipboard.current_selection = None;
}
},
x if x == self.atoms.xsettings => match e.subtype() {
@ -446,106 +721,18 @@ impl XState {
true
}
fn handle_new_selection_owner(&mut self, owner: x::Window, timestamp: u32) {
debug!("new selection owner: {owner:?}");
self.selection_data.last_selection_timestamp = timestamp;
// Grab targets
self.connection
.send_and_check_request(&x::ConvertSelection {
requestor: self.wm_window,
selection: self.atoms.clipboard,
target: self.atoms.targets,
property: self.atoms.selection_reply,
time: timestamp,
})
.unwrap();
}
fn handle_target_list(&mut self, dest_property: x::Atom, server_state: &mut RealServerState) {
let reply = self
.connection
.wait_for_reply(self.connection.send_request(&x::GetProperty {
delete: true,
window: self.wm_window,
property: dest_property,
r#type: x::ATOM_ATOM,
long_offset: 0,
long_length: 20,
}))
.unwrap();
let targets: &[x::Atom] = reply.value();
if targets.is_empty() {
warn!("Got empty selection target list, trying again...");
match self.connection.wait_for_reply(self.connection.send_request(
&x::GetSelectionOwner {
selection: self.atoms.clipboard,
},
)) {
Ok(reply) => {
if reply.owner() == self.wm_window {
warn!("We are unexpectedly the selection owner? Clipboard may be broken!");
} else {
self.handle_new_selection_owner(
reply.owner(),
self.selection_data.last_selection_timestamp,
);
}
}
Err(e) => {
error!("Couldn't grab selection owner: {e:?}. Clipboard is stale!");
}
}
return;
}
if log::log_enabled!(log::Level::Debug) {
let targets_str: Vec<String> = targets
.iter()
.map(|t| get_atom_name(&self.connection, *t))
.collect();
debug!("got targets: {targets_str:?}");
}
let mimes = targets
.iter()
.copied()
.filter(|atom| {
![
self.atoms.targets,
self.atoms.multiple,
self.atoms.save_targets,
]
.contains(atom)
})
.map(|target_atom| SelectionTargetId {
name: get_atom_name(&self.connection, target_atom),
atom: target_atom,
source: None,
})
.collect();
let selection = Rc::new(Selection {
mimes,
connection: self.connection.clone(),
window: self.selection_data.target_window,
pending: RefCell::default(),
clipboard: self.atoms.clipboard,
selection_time: self.selection_data.last_selection_timestamp,
incr: self.atoms.incr,
});
server_state.set_copy_paste_source(&selection);
self.selection_data.current_selection = Some(CurrentSelection::X11(selection));
debug!("Clipboard set from X11");
}
pub(super) fn handle_selection_property_change(
&mut self,
event: &x::PropertyNotifyEvent,
) -> bool {
if let Some(CurrentSelection::X11(selection)) = &self.selection_data.current_selection {
for data in [
&self.selection_state.primary as &dyn SelectionDataImpl,
&self.selection_state.clipboard as _,
] {
if let Some(selection) = &data.x11_selection() {
return selection.check_for_incr(event);
}
}
false
}
}

View file

@ -1004,7 +1004,7 @@ fn copy_from_x11() {
);
}
let data = f.testwl.paste_data(|mime, _| {
let data = f.testwl.clipboard_paste_data(|mime, _| {
let request = connection.await_selection_request();
let data = mimes_truth
.iter()
@ -1201,7 +1201,7 @@ fn bad_clipboard_data() {
connection.send_selection_notify(&request);
f.wait_and_dispatch();
let mut data = f.testwl.paste_data(|_, _| {
let mut data = f.testwl.clipboard_paste_data(|_, _| {
let request = connection.await_selection_request();
assert_eq!(request.target(), connection.atoms.mime2);
// Don't actually set any data as requested - just report success
@ -1388,7 +1388,7 @@ fn incr_copy_from_x11() {
.take(3000)
.collect();
let mut it = data.chunks(500).enumerate();
let mut paste_data = f.testwl.paste_data(|_, testwl| {
let mut paste_data = f.testwl.clipboard_paste_data(|_, testwl| {
if let Some(begin) = begin_incr.take() {
destination_property = begin(&mut connection);
testwl.dispatch();

View file

@ -5,6 +5,10 @@ use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use std::os::unix::net::UnixStream;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;
use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1;
use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1;
use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1;
use wayland_protocols::{
wp::{
fractional_scale::v1::server::{
@ -253,9 +257,12 @@ struct State {
tablet: Option<ZwpTabletV2>,
tablet_tool: Option<ZwpTabletToolV2>,
configure_serial: u32,
selection: Option<WlDataSource>,
clipboard: Option<WlDataSource>,
primary: Option<ZwpPrimarySelectionSourceV1>,
data_device_man: Option<WlDataDeviceManager>,
data_device: Option<WlDataDevice>,
primary_man: Option<ZwpPrimarySelectionDeviceManagerV1>,
primary_device: Option<ZwpPrimarySelectionDeviceV1>,
xdg_activation: Option<XdgActivationV1>,
valid_tokens: HashSet<String>,
token_counter: u32,
@ -279,7 +286,10 @@ impl Default for State {
tablet: None,
tablet_tool: None,
configure_serial: 0,
selection: None,
clipboard: None,
primary: None,
primary_man: None,
primary_device: None,
data_device_man: None,
data_device: None,
xdg_activation: None,
@ -404,6 +414,9 @@ pub struct Server {
client: Option<Client>,
}
pub trait SendDataForMimeFn: FnMut(&str, &mut Server) -> bool {}
impl<T> SendDataForMimeFn for T where T: FnMut(&str, &mut Server) -> bool {}
impl Server {
pub fn new(noops: bool) -> Self {
let display = Display::new().unwrap();
@ -436,6 +449,7 @@ impl Server {
dh.create_global::<State, XdgWmBase, _>(6, ());
dh.create_global::<State, WlSeat, _>(5, ());
dh.create_global::<State, WlDataDeviceManager, _>(3, ());
dh.create_global::<State, ZwpPrimarySelectionDeviceManagerV1, _>(1, ());
dh.create_global::<State, ZwpTabletManagerV2, _>(1, ());
dh.create_global::<State, XdgActivationV1, _>(1, ());
dh.create_global::<State, ZxdgDecorationManagerV1, _>(1, ());
@ -610,7 +624,7 @@ impl Server {
#[track_caller]
pub fn data_source_mimes(&self) -> Vec<String> {
let Some(selection) = &self.state.selection else {
let Some(selection) = &self.state.clipboard else {
panic!("No selection set on data device");
};
@ -620,20 +634,28 @@ impl Server {
}
#[track_caller]
pub fn paste_data(
pub fn primary_source_mimes(&self) -> Vec<String> {
let Some(selection) = &self.state.primary else {
panic!("No selection set on primary device");
};
let data: &Mutex<DataSourceData> = selection.data().unwrap();
let data = data.lock().unwrap();
data.mimes.to_vec()
}
fn paste_impl(
&mut self,
mut send_data_for_mime: impl FnMut(&str, &mut Self) -> bool,
data: &Mutex<DataSourceData>,
mut send_data_for_mime: impl SendDataForMimeFn,
mut send_selection: impl FnMut(String, std::os::unix::io::BorrowedFd),
) -> Vec<PasteData> {
struct PendingData {
rx: std::fs::File,
data: Vec<u8>,
}
let Some(selection) = self.state.selection.take() else {
panic!("No selection set on data device");
};
type PendingRet = Vec<(String, Option<PendingData>)>;
let mut pending_ret: PendingRet = {
let data: &Mutex<DataSourceData> = selection.data().unwrap();
data.lock()
.unwrap()
.mimes
@ -673,7 +695,7 @@ impl Server {
Some(pending) => try_transfer(&mut pending_ret, mime, pending),
None => {
let (rx, tx) = rustix::pipe::pipe().unwrap();
selection.send(mime.clone(), tx.as_fd());
send_selection(mime.clone(), tx.as_fd());
drop(tx);
let rx = std::fs::File::from(rx);
@ -689,13 +711,47 @@ impl Server {
}
}
self.state.selection = Some(selection);
ret
}
#[track_caller]
pub fn clipboard_paste_data(
&mut self,
send_data_for_mime: impl SendDataForMimeFn,
) -> Vec<PasteData> {
let Some(selection) = self.state.clipboard.take() else {
panic!("No selection set on data device");
};
let ret = self.paste_impl(
selection.data().unwrap(),
send_data_for_mime,
|mime_type, fd| selection.send(mime_type, fd),
);
self.state.clipboard = Some(selection);
ret
}
#[track_caller]
pub fn primary_paste_data(
&mut self,
send_data_for_mime: impl SendDataForMimeFn,
) -> Vec<PasteData> {
let Some(selection) = self.state.primary.take() else {
panic!("No selection set on primary data device");
};
let ret = self.paste_impl(
selection.data().unwrap(),
send_data_for_mime,
|mime_type, fd| selection.send(mime_type, fd),
);
self.state.primary = Some(selection);
ret
}
pub fn data_source_exists(&self) -> bool {
self.state.selection.is_none()
self.state.clipboard.is_none()
}
#[track_caller]
@ -704,7 +760,7 @@ impl Server {
panic!("No data device created");
};
if let Some(selection) = self.state.selection.take() {
if let Some(selection) = self.state.clipboard.take() {
selection.cancelled();
}
@ -723,13 +779,38 @@ impl Server {
self.display.flush_clients().unwrap();
}
#[track_caller]
pub fn create_primary_offer(&mut self, data: Vec<PasteData>) {
let Some(dev) = &self.state.primary_device else {
panic!("No primary device created");
};
if let Some(selection) = self.state.primary.take() {
selection.cancelled();
}
let mimes: Vec<_> = data.iter().map(|m| m.mime_type.clone()).collect();
let offer = self
.client
.as_ref()
.unwrap()
.create_resource::<_, _, State>(&self.dh, 1, data)
.unwrap();
dev.data_offer(&offer);
for mime in mimes {
offer.offer(mime);
}
dev.selection(Some(&offer));
self.display.flush_clients().unwrap();
}
#[track_caller]
pub fn empty_data_offer(&mut self) {
let Some(dev) = &self.state.data_device else {
panic!("No data device created");
};
if let Some(selection) = self.state.selection.take() {
if let Some(selection) = self.state.clipboard.take() {
selection.cancelled();
}
@ -996,6 +1077,118 @@ impl Dispatch<WlOutput, ()> for State {
}
}
impl GlobalDispatch<ZwpPrimarySelectionDeviceManagerV1, ()> for State {
fn bind(
state: &mut Self,
_: &DisplayHandle,
_: &Client,
resource: wayland_server::New<ZwpPrimarySelectionDeviceManagerV1>,
_: &(),
data_init: &mut wayland_server::DataInit<'_, Self>,
) {
state.primary_man = Some(data_init.init(resource, ()));
}
}
impl Dispatch<ZwpPrimarySelectionDeviceManagerV1, ()> for State {
fn request(
state: &mut Self,
_: &Client,
_: &ZwpPrimarySelectionDeviceManagerV1,
request: <ZwpPrimarySelectionDeviceManagerV1 as Resource>::Request,
_: &(),
_: &DisplayHandle,
data_init: &mut wayland_server::DataInit<'_, Self>,
) {
use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_manager_v1::Request;
match request {
Request::CreateSource { id } => {
data_init.init(id, DataSourceData::default().into());
}
Request::GetDevice { id, seat } => {
state.primary_device = Some(data_init.init(id, seat));
}
Request::Destroy => {
state.primary_man = None;
}
_ => todo!("{request:?}"),
}
}
}
impl Dispatch<ZwpPrimarySelectionOfferV1, Vec<PasteData>> for State {
fn request(
_: &mut Self,
_: &Client,
_: &ZwpPrimarySelectionOfferV1,
request: <ZwpPrimarySelectionOfferV1 as Resource>::Request,
data: &Vec<PasteData>,
_: &DisplayHandle,
_: &mut wayland_server::DataInit<'_, Self>,
) {
use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_offer_v1::Request;
match request {
Request::Receive { mime_type, fd } => {
let pos = data
.iter()
.position(|data| data.mime_type == mime_type)
.unwrap_or_else(|| panic!("Invalid mime type: {mime_type}"));
let mut stream = UnixStream::from(fd);
stream.write_all(&data[pos].data).unwrap();
}
Request::Destroy => {}
other => todo!("{other:?}"),
}
}
}
impl Dispatch<ZwpPrimarySelectionSourceV1, Mutex<DataSourceData>> for State {
fn request(
state: &mut Self,
_: &Client,
_: &ZwpPrimarySelectionSourceV1,
request: <ZwpPrimarySelectionSourceV1 as Resource>::Request,
data: &Mutex<DataSourceData>,
_: &DisplayHandle,
_: &mut wayland_server::DataInit<'_, Self>,
) {
use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::Request;
match request {
Request::Offer { mime_type } => {
data.lock().unwrap().mimes.push(mime_type);
}
Request::Destroy => {
state.primary = None;
}
_ => todo!("{request:?}"),
}
}
}
impl Dispatch<ZwpPrimarySelectionDeviceV1, WlSeat> for State {
fn request(
state: &mut Self,
_: &Client,
_: &ZwpPrimarySelectionDeviceV1,
request: <ZwpPrimarySelectionDeviceV1 as Resource>::Request,
_: &WlSeat,
_: &DisplayHandle,
_: &mut wayland_server::DataInit<'_, Self>,
) {
use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_v1::Request;
match request {
Request::SetSelection { source, .. } => {
state.primary = source;
}
Request::Destroy => {
state.primary_device = None;
}
other => todo!("unhandled request {other:?}"),
}
}
}
impl GlobalDispatch<WlDataDeviceManager, ()> for State {
fn bind(
state: &mut Self,
@ -1051,7 +1244,7 @@ impl Dispatch<WlDataSource, Mutex<DataSourceData>> for State {
data.mimes.push(mime_type);
}
wl_data_source::Request::Destroy => {
state.selection = None;
state.clipboard = None;
}
other => todo!("unhandled request {other:?}"),
}
@ -1070,7 +1263,7 @@ impl Dispatch<WlDataDevice, WlSeat> for State {
) {
match request {
wl_data_device::Request::SetSelection { source, .. } => {
state.selection = source;
state.clipboard = source;
}
wl_data_device::Request::Release => {
state.data_device = None;