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

@ -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),
}