diff --git a/window/src/os/wayland/copy_and_paste.rs b/window/src/os/wayland/copy_and_paste.rs index 81b00855c..ad83e462b 100644 --- a/window/src/os/wayland/copy_and_paste.rs +++ b/window/src/os/wayland/copy_and_paste.rs @@ -1,7 +1,15 @@ -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, Error, bail}; use filedescriptor::{FileDescriptor, Pipe}; use smithay_client_toolkit as toolkit; -use std::os::fd::AsRawFd; +use toolkit::globals::GlobalData; +use wayland_client::{Dispatch, event_created_child}; +use wayland_client::globals::{GlobalList, BindError}; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_v1::{ZwpPrimarySelectionDeviceV1, self, Event as PrimarySelectionDeviceEvent}; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{ZwpPrimarySelectionOfferV1, Event as PrimarySelectionOfferEvent}; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_source_v1::{ZwpPrimarySelectionSourceV1, Event as PrimarySelectionSourceEvent}; +use std::io::Write; +use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; use std::sync::{Arc, Mutex}; use toolkit::reexports::client::protocol::wl_data_offer::WlDataOffer; @@ -30,32 +38,71 @@ impl CopyAndPaste { pub(super) fn get_clipboard_data( &mut self, - _clipboard: Clipboard, + clipboard: Clipboard, ) -> anyhow::Result { - // TODO; primary selection - let offer = self - .data_offer - .as_ref() - .ok_or_else(|| anyhow!("no data offer"))?; - let pipe = Pipe::new().map_err(Error::msg)?; - offer.receive(TEXT_MIME_TYPE.to_string(), pipe.write.as_raw_fd()); - Ok(pipe.read) + let conn = crate::Connection::get().unwrap().wayland(); + let wayland_state = conn.wayland_state.borrow(); + let primary_selection = if let Clipboard::PrimarySelection = clipboard { + wayland_state.primary_selection_manager.as_ref() + } else { + None + }; + + match primary_selection { + Some(primary_selection) => { + let inner = primary_selection.inner.lock().unwrap(); + let offer = inner + .offer + .as_ref() + .ok_or_else(|| anyhow!("no primary selection offer"))?; + let pipe = Pipe::new().map_err(Error::msg)?; + offer.receive(TEXT_MIME_TYPE.to_string(), pipe.write.as_raw_fd()); + Ok(pipe.read) + } + None => { + let offer = self + .data_offer + .as_ref() + .ok_or_else(|| anyhow!("no data offer"))?; + let pipe = Pipe::new().map_err(Error::msg)?; + offer.receive(TEXT_MIME_TYPE.to_string(), pipe.write.as_raw_fd()); + Ok(pipe.read) + } + } } - pub(super) fn set_clipboard_data(&mut self, _clipboard: Clipboard, data: String) { - // TODO: primary selection - + pub(super) fn set_clipboard_data(&mut self, clipboard: Clipboard, data: String) { let conn = crate::Connection::get().unwrap().wayland(); let qh = conn.event_queue.borrow().handle(); let mut wayland_state = conn.wayland_state.borrow_mut(); let last_serial = *wayland_state.last_serial.borrow(); - let data_device = &wayland_state.data_device; - let source = wayland_state - .data_device_manager_state - .create_copy_paste_source(&qh, vec![TEXT_MIME_TYPE]); - source.set_selection(data_device.as_ref().unwrap(), last_serial); - wayland_state.copy_paste_source.replace((source, data)); + let primary_selection = if let Clipboard::PrimarySelection = clipboard { + wayland_state.primary_selection_manager.as_ref() + } else { + None + }; + + match primary_selection { + Some(primary_selection) => { + let manager = &primary_selection.manager; + let selection_device = wayland_state.primary_select_device.as_ref().unwrap(); + let source = manager.create_source(&qh, PrimarySelectionManagerData::default()); + source.offer(TEXT_MIME_TYPE.to_string()); + selection_device.set_selection(Some(&source), last_serial); + wayland_state + .primary_selection_source + .replace((source, data)); + } + None => { + let data_device = &wayland_state.data_device; + let source = wayland_state + .data_device_manager_state + .create_copy_paste_source(&qh, vec![TEXT_MIME_TYPE]); + source.set_selection(data_device.as_ref().unwrap(), last_serial); + wayland_state.copy_paste_source.replace((source, data)); + } + } } pub(super) fn confirm_selection(&mut self, offer: WlDataOffer) { @@ -74,3 +121,186 @@ impl WaylandState { } } } + +pub(super) fn write_selection_to_pipe(fd: FileDescriptor, text: &str) { + if let Err(e) = write_pipe_with_timeout(fd, text.as_bytes()) { + log::error!("while sending primary selection to pipe: {}", e); + } +} + +fn write_pipe_with_timeout(mut file: FileDescriptor, data: &[u8]) -> anyhow::Result<()> { + file.set_non_blocking(true)?; + let mut pfd = libc::pollfd { + fd: file.as_raw_fd(), + events: libc::POLLOUT, + revents: 0, + }; + + let mut buf = data; + + while !buf.is_empty() { + if unsafe { libc::poll(&mut pfd, 1, 3000) == 1 } { + match file.write(buf) { + Ok(size) if size == 0 => { + bail!("zero byte write"); + } + Ok(size) => { + buf = &buf[size..]; + } + Err(e) => bail!("error writing to pipe: {}", e), + } + } else { + bail!("timed out writing to pipe"); + } + } + + Ok(()) +} + +// Smithay has their own primary selection handler in 0.18 +// Some code borrowed from https://github.com/Smithay/client-toolkit/commit/4a5c4f59f640bc588a55277261bbed1bd2abea98 +pub(super) struct PrimarySelectionManagerState { + pub(super) manager: ZwpPrimarySelectionDeviceManagerV1, + inner: Mutex, +} + +#[derive(Default, Debug)] +struct PrimaryInner { + pending_offer: Option, + offer: Option, + valid_mime: bool, +} + +#[derive(Default)] +pub(super) struct PrimarySelectionManagerData {} + +impl PrimarySelectionManagerState { + pub(super) fn bind( + globals: &GlobalList, + queue_handle: &wayland_client::QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { + manager, + inner: Mutex::new(PrimaryInner::default()), + }) + } +} + +impl Dispatch + for PrimarySelectionManagerState +{ + fn event( + _state: &mut WaylandState, + _proxy: &ZwpPrimarySelectionDeviceManagerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &wayland_client::Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + unreachable!("primary selection manager has no events"); + } +} + +impl Dispatch + for PrimarySelectionManagerState +{ + fn event( + state: &mut WaylandState, + source: &ZwpPrimarySelectionSourceV1, + event: ::Event, + _data: &PrimarySelectionManagerData, + _conn: &wayland_client::Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + match event { + PrimarySelectionSourceEvent::Send { mime_type, fd } => { + if mime_type != TEXT_MIME_TYPE { + return; + }; + + if let Some((ps_source, data)) = &state.primary_selection_source { + if ps_source != source { + return; + } + let fd = unsafe { FileDescriptor::from_raw_fd(fd.into_raw_fd()) }; + write_selection_to_pipe(fd, data); + } + } + PrimarySelectionSourceEvent::Cancelled => { + state.primary_selection_source.take(); + source.destroy(); + } + _ => unreachable!(), + } + } +} + +impl Dispatch + for PrimarySelectionManagerState +{ + fn event( + state: &mut WaylandState, + _proxy: &ZwpPrimarySelectionOfferV1, + event: ::Event, + _data: &PrimarySelectionManagerData, + _conn: &wayland_client::Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + match event { + PrimarySelectionOfferEvent::Offer { mime_type } => { + if mime_type == TEXT_MIME_TYPE { + let mgr = state.primary_selection_manager.as_ref().unwrap(); + let mut inner = mgr.inner.lock().unwrap(); + inner.valid_mime = true; + } + } + _ => unreachable!(), + } + } +} + +impl Dispatch + for PrimarySelectionManagerState +{ + event_created_child!(WaylandState, ZwpPrimarySelectionDeviceV1, [ + zwp_primary_selection_device_v1::EVT_DATA_OFFER_OPCODE => (ZwpPrimarySelectionOfferV1, PrimarySelectionManagerData::default()) + ]); + + fn event( + state: &mut WaylandState, + _primary_selection_device: &ZwpPrimarySelectionDeviceV1, + event: ::Event, + _data: &PrimarySelectionManagerData, + _conn: &wayland_client::Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + let psm = state.primary_selection_manager.as_ref().unwrap(); + let mut inner = psm.inner.lock().unwrap(); + match event { + PrimarySelectionDeviceEvent::DataOffer { offer } => { + inner.pending_offer = Some(offer); + } + PrimarySelectionDeviceEvent::Selection { id } => { + if !inner.valid_mime { + return; + } + + if let Some(offer) = inner.offer.take() { + offer.destroy(); + } + if id == inner.pending_offer { + inner.offer = inner.pending_offer.take(); + } else { + // Remove the pending offer, assign the new delivered one. + if let Some(offer) = inner.pending_offer.take() { + offer.destroy() + } + + inner.offer = id; + } + } + _ => unreachable!(), + } + } +} diff --git a/window/src/os/wayland/data_device.rs b/window/src/os/wayland/data_device.rs index 719b69e8f..d14857df1 100644 --- a/window/src/os/wayland/data_device.rs +++ b/window/src/os/wayland/data_device.rs @@ -1,7 +1,5 @@ -use std::io::Write; -use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; +use std::os::fd::{FromRawFd, IntoRawFd}; -use anyhow::bail; use filedescriptor::FileDescriptor; use smithay_client_toolkit::data_device_manager::data_device::{ DataDevice, DataDeviceDataExt, DataDeviceHandler, @@ -16,6 +14,7 @@ use crate::wayland::drag_and_drop::SurfaceAndOffer; use crate::wayland::pointer::PointerUserData; use crate::wayland::SurfaceUserData; +use super::copy_and_paste::write_selection_to_pipe; use super::drag_and_drop::{DragAndDrop, SurfaceAndPipe}; use super::state::WaylandState; @@ -239,38 +238,3 @@ impl DataSourceHandler for WaylandState { ) { } } - -fn write_selection_to_pipe(fd: FileDescriptor, text: &str) { - if let Err(e) = write_pipe_with_timeout(fd, text.as_bytes()) { - log::error!("while sending primary selection to pipe: {}", e); - } -} - -fn write_pipe_with_timeout(mut file: FileDescriptor, data: &[u8]) -> anyhow::Result<()> { - file.set_non_blocking(true)?; - let mut pfd = libc::pollfd { - fd: file.as_raw_fd(), - events: libc::POLLOUT, - revents: 0, - }; - - let mut buf = data; - - while !buf.is_empty() { - if unsafe { libc::poll(&mut pfd, 1, 3000) == 1 } { - match file.write(buf) { - Ok(size) if size == 0 => { - bail!("zero byte write"); - } - Ok(size) => { - buf = &buf[size..]; - } - Err(e) => bail!("error writing to pipe: {}", e), - } - } else { - bail!("timed out writing to pipe"); - } - } - - Ok(()) -} diff --git a/window/src/os/wayland/seat.rs b/window/src/os/wayland/seat.rs index 198a61600..d9e6b1bba 100644 --- a/window/src/os/wayland/seat.rs +++ b/window/src/os/wayland/seat.rs @@ -3,6 +3,7 @@ use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState}; use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::{Connection, QueueHandle}; +use crate::wayland::copy_and_paste::PrimarySelectionManagerData; use crate::wayland::keyboard::KeyboardData; use crate::wayland::pointer::PointerUserData; @@ -48,6 +49,12 @@ impl SeatHandler for WaylandState { let data_device_manager = &self.data_device_manager_state; let data_device = data_device_manager.get_data_device(qh, &seat); self.data_device.replace(data_device); + + let primary_select_device = self.primary_selection_manager.as_ref().map(|m| { + m.manager + .get_device(&seat, qh, PrimarySelectionManagerData::default()) + }); + self.primary_select_device = primary_select_device; } } diff --git a/window/src/os/wayland/state.rs b/window/src/os/wayland/state.rs index ae46eb5e5..44b85ed36 100644 --- a/window/src/os/wayland/state.rs +++ b/window/src/os/wayland/state.rs @@ -30,11 +30,16 @@ use wayland_client::protocol::wl_output::WlOutput; use wayland_client::protocol::wl_pointer::WlPointer; use wayland_client::protocol::wl_surface::WlSurface; use wayland_client::{delegate_dispatch, Connection, QueueHandle}; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1; use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; use crate::x11::KeyboardWithFallback; +use super::copy_and_paste::{PrimarySelectionManagerData, PrimarySelectionManagerState}; use super::inputhandler::{TextInputData, TextInputState}; use super::pointer::{PendingMouse, PointerUserData}; use super::{OutputManagerData, OutputManagerState, SurfaceUserData, WaylandWindowInner}; @@ -65,6 +70,9 @@ pub(super) struct WaylandState { pub(super) data_device_manager_state: DataDeviceManagerState, pub(super) data_device: Option, pub(super) copy_paste_source: Option<(CopyPasteSource, String)>, + pub(super) primary_selection_manager: Option, + pub(super) primary_select_device: Option, + pub(super) primary_selection_source: Option<(ZwpPrimarySelectionSourceV1, String)>, pub(super) shm: Shm, pub(super) mem_pool: RefCell, } @@ -98,6 +106,9 @@ impl WaylandState { data_device_manager_state: DataDeviceManagerState::bind(globals, qh)?, data_device: None, copy_paste_source: None, + primary_selection_manager: PrimarySelectionManagerState::bind(globals, qh).ok(), + primary_select_device: None, + primary_selection_source: None, shm, mem_pool: RefCell::new(mem_pool), }; @@ -169,3 +180,8 @@ delegate_dispatch!(WaylandState: [ZwpTextInputV3: TextInputData] => TextInputSta delegate_dispatch!(WaylandState: [ZwlrOutputManagerV1: GlobalData] => OutputManagerState); delegate_dispatch!(WaylandState: [ZwlrOutputHeadV1: OutputManagerData] => OutputManagerState); delegate_dispatch!(WaylandState: [ZwlrOutputModeV1: OutputManagerData] => OutputManagerState); + +delegate_dispatch!(WaylandState: [ZwpPrimarySelectionDeviceManagerV1: GlobalData] => PrimarySelectionManagerState); +delegate_dispatch!(WaylandState: [ZwpPrimarySelectionDeviceV1: PrimarySelectionManagerData] => PrimarySelectionManagerState); +delegate_dispatch!(WaylandState: [ZwpPrimarySelectionSourceV1: PrimarySelectionManagerData] => PrimarySelectionManagerState); +delegate_dispatch!(WaylandState: [ZwpPrimarySelectionOfferV1: PrimarySelectionManagerData] => PrimarySelectionManagerState);