wayland: Refactor clipboard implementation (#12405)
Fixes https://github.com/zed-industries/zed/issues/12054 Replaces the `copypasta`/`smithay-clipboard` implementation with a new, custom one TODO list: - [x] Cleanup code - [x] Remove `smithay-clipboard` - [x] Add more mime types to the supported list Release Notes: - Fixed drag and drop on Gnome - Fixed clipboard paste on Hyprland
This commit is contained in:
parent
b55961b57a
commit
f6fa6600bc
10 changed files with 491 additions and 175 deletions
|
@ -10,8 +10,6 @@ use calloop::timer::{TimeoutAction, Timer};
|
|||
use calloop::{EventLoop, LoopHandle};
|
||||
use calloop_wayland_source::WaylandSource;
|
||||
use collections::HashMap;
|
||||
use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary};
|
||||
use copypasta::ClipboardProvider;
|
||||
use filedescriptor::Pipe;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
@ -22,9 +20,10 @@ use wayland_client::event_created_child;
|
|||
use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContents};
|
||||
use wayland_client::protocol::wl_callback::{self, WlCallback};
|
||||
use wayland_client::protocol::wl_data_device_manager::DndAction;
|
||||
use wayland_client::protocol::wl_data_offer::WlDataOffer;
|
||||
use wayland_client::protocol::wl_pointer::AxisSource;
|
||||
use wayland_client::protocol::{
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_output, wl_region,
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,
|
||||
};
|
||||
use wayland_client::{
|
||||
delegate_noop,
|
||||
|
@ -40,6 +39,13 @@ use wayland_protocols::wp::cursor_shape::v1::client::{
|
|||
use wayland_protocols::wp::fractional_scale::v1::client::{
|
||||
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
|
||||
};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
|
||||
self, ZwpPrimarySelectionOfferV1,
|
||||
};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::{
|
||||
zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1,
|
||||
zwp_primary_selection_source_v1,
|
||||
};
|
||||
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{
|
||||
ContentHint, ContentPurpose,
|
||||
};
|
||||
|
@ -60,6 +66,9 @@ use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
|
|||
use super::display::WaylandDisplay;
|
||||
use super::window::{ImeInput, WaylandWindowStatePtr};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::wayland::clipboard::{
|
||||
Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPE,
|
||||
};
|
||||
use crate::platform::linux::wayland::cursor::Cursor;
|
||||
use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
|
||||
use crate::platform::linux::wayland::window::WaylandWindow;
|
||||
|
@ -88,6 +97,8 @@ pub struct Globals {
|
|||
pub compositor: wl_compositor::WlCompositor,
|
||||
pub cursor_shape_manager: Option<wp_cursor_shape_manager_v1::WpCursorShapeManagerV1>,
|
||||
pub data_device_manager: Option<wl_data_device_manager::WlDataDeviceManager>,
|
||||
pub primary_selection_manager:
|
||||
Option<zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1>,
|
||||
pub wm_base: xdg_wm_base::XdgWmBase,
|
||||
pub shm: wl_shm::WlShm,
|
||||
pub seat: wl_seat::WlSeat,
|
||||
|
@ -125,6 +136,7 @@ impl Globals {
|
|||
(),
|
||||
)
|
||||
.ok(),
|
||||
primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
shm: globals.bind(&qh, 1..=1, ()).unwrap(),
|
||||
seat,
|
||||
wm_base: globals.bind(&qh, 1..=1, ()).unwrap(),
|
||||
|
@ -177,6 +189,7 @@ pub(crate) struct WaylandClientState {
|
|||
wl_keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
|
||||
data_device: Option<wl_data_device::WlDataDevice>,
|
||||
primary_selection: Option<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1>,
|
||||
text_input: Option<zwp_text_input_v3::ZwpTextInputV3>,
|
||||
pre_edit_text: Option<String>,
|
||||
composing: bool,
|
||||
|
@ -204,13 +217,13 @@ pub(crate) struct WaylandClientState {
|
|||
keyboard_focused_window: Option<WaylandWindowStatePtr>,
|
||||
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
|
||||
cursor_style: Option<CursorStyle>,
|
||||
clipboard: Clipboard,
|
||||
data_offers: Vec<DataOffer<WlDataOffer>>,
|
||||
primary_data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
|
||||
cursor: Cursor,
|
||||
clipboard: Option<Clipboard>,
|
||||
primary: Option<Primary>,
|
||||
pending_open_uri: Option<String>,
|
||||
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
|
||||
common: LinuxCommon,
|
||||
|
||||
pending_open_uri: Option<String>,
|
||||
}
|
||||
|
||||
pub struct DragState {
|
||||
|
@ -311,9 +324,6 @@ impl Drop for WaylandClient {
|
|||
let mut state = self.0.borrow_mut();
|
||||
state.windows.clear();
|
||||
|
||||
// Drop the clipboard to prevent a seg fault after we've closed all Wayland connections.
|
||||
state.primary = None;
|
||||
state.clipboard = None;
|
||||
if let Some(wl_pointer) = &state.wl_pointer {
|
||||
wl_pointer.release();
|
||||
}
|
||||
|
@ -395,8 +405,6 @@ impl WaylandClient {
|
|||
}
|
||||
});
|
||||
|
||||
let display = conn.backend().display_ptr() as *mut std::ffi::c_void;
|
||||
|
||||
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
@ -428,7 +436,10 @@ impl WaylandClient {
|
|||
.as_ref()
|
||||
.map(|data_device_manager| data_device_manager.get_data_device(&seat, &qh, ()));
|
||||
|
||||
let (primary, clipboard) = unsafe { create_clipboards_from_external(display) };
|
||||
let primary_selection = globals
|
||||
.primary_selection_manager
|
||||
.as_ref()
|
||||
.map(|primary_selection_manager| primary_selection_manager.get_device(&seat, &qh, ()));
|
||||
|
||||
let mut cursor = Cursor::new(&conn, &globals, 24);
|
||||
|
||||
|
@ -470,6 +481,7 @@ impl WaylandClient {
|
|||
wl_keyboard: None,
|
||||
cursor_shape_device: None,
|
||||
data_device,
|
||||
primary_selection,
|
||||
text_input: None,
|
||||
pre_edit_text: None,
|
||||
composing: false,
|
||||
|
@ -515,12 +527,12 @@ impl WaylandClient {
|
|||
loop_handle: handle.clone(),
|
||||
enter_token: None,
|
||||
cursor_style: None,
|
||||
clipboard: Clipboard::new(conn.clone(), handle.clone()),
|
||||
data_offers: Vec::new(),
|
||||
primary_data_offer: None,
|
||||
cursor,
|
||||
clipboard: Some(clipboard),
|
||||
primary: Some(primary),
|
||||
event_loop: Some(event_loop),
|
||||
|
||||
pending_open_uri: None,
|
||||
event_loop: Some(event_loop),
|
||||
}));
|
||||
|
||||
WaylandSource::new(conn, event_queue)
|
||||
|
@ -651,33 +663,46 @@ impl LinuxClient for WaylandClient {
|
|||
}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.primary
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_contents(item.text)
|
||||
.ok();
|
||||
let mut state = self.0.borrow_mut();
|
||||
let (Some(primary_selection_manager), Some(primary_selection)) = (
|
||||
state.globals.primary_selection_manager.clone(),
|
||||
state.primary_selection.clone(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() {
|
||||
let serial = state.serial_tracker.get(SerialKind::KeyEnter);
|
||||
let data_source = primary_selection_manager.create_source(&state.globals.qh, ());
|
||||
data_source.offer(state.clipboard.self_mime());
|
||||
data_source.offer(TEXT_MIME_TYPE.to_string());
|
||||
primary_selection.set_selection(Some(&data_source), serial);
|
||||
state.clipboard.set_primary(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_contents(item.text)
|
||||
.ok();
|
||||
let mut state = self.0.borrow_mut();
|
||||
let (Some(data_device_manager), Some(data_device)) = (
|
||||
state.globals.data_device_manager.clone(),
|
||||
state.data_device.clone(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() {
|
||||
let serial = state.serial_tracker.get(SerialKind::KeyEnter);
|
||||
let data_source = data_device_manager.create_data_source(&state.globals.qh, ());
|
||||
data_source.offer(state.clipboard.self_mime());
|
||||
data_source.offer(TEXT_MIME_TYPE.to_string());
|
||||
data_device.set_selection(Some(&data_source), serial);
|
||||
state.clipboard.set(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.primary
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.get_contents()
|
||||
.ok()
|
||||
.clipboard
|
||||
.read_primary()
|
||||
.map(|s| crate::ClipboardItem {
|
||||
text: s,
|
||||
metadata: None,
|
||||
|
@ -688,10 +713,7 @@ impl LinuxClient for WaylandClient {
|
|||
self.0
|
||||
.borrow_mut()
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.get_contents()
|
||||
.ok()
|
||||
.read()
|
||||
.map(|s| crate::ClipboardItem {
|
||||
text: s,
|
||||
metadata: None,
|
||||
|
@ -771,6 +793,7 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor);
|
|||
delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDeviceManager);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer);
|
||||
|
@ -1061,7 +1084,10 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
|||
xkb::compose::STATE_NO_FLAGS,
|
||||
));
|
||||
}
|
||||
wl_keyboard::Event::Enter { surface, .. } => {
|
||||
wl_keyboard::Event::Enter {
|
||||
serial, surface, ..
|
||||
} => {
|
||||
state.serial_tracker.update(SerialKind::KeyEnter, serial);
|
||||
state.keyboard_focused_window = get_window(&mut state, &surface.id());
|
||||
state.enter_token = Some(());
|
||||
|
||||
|
@ -1074,6 +1100,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
|||
let keyboard_focused_window = get_window(&mut state, &surface.id());
|
||||
state.keyboard_focused_window = None;
|
||||
state.enter_token.take();
|
||||
state.clipboard.set_offer(None);
|
||||
|
||||
if let Some(window) = keyboard_focused_window {
|
||||
if let Some(ref mut compose) = state.compose_state {
|
||||
|
@ -1649,8 +1676,6 @@ impl Dispatch<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, ObjectId>
|
|||
}
|
||||
}
|
||||
|
||||
const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
|
||||
|
||||
impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
|
@ -1664,6 +1689,28 @@ impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
|
|||
let mut state = client.borrow_mut();
|
||||
|
||||
match event {
|
||||
// Clipboard
|
||||
wl_data_device::Event::DataOffer { id: data_offer } => {
|
||||
state.data_offers.push(DataOffer::new(data_offer));
|
||||
if state.data_offers.len() > 2 {
|
||||
// At most we store a clipboard offer and a drag and drop offer.
|
||||
state.data_offers.remove(0).inner.destroy();
|
||||
}
|
||||
}
|
||||
wl_data_device::Event::Selection { id: data_offer } => {
|
||||
if let Some(offer) = data_offer {
|
||||
let offer = state
|
||||
.data_offers
|
||||
.iter()
|
||||
.find(|wrapper| wrapper.inner.id() == offer.id());
|
||||
let offer = offer.cloned();
|
||||
state.clipboard.set_offer(offer);
|
||||
} else {
|
||||
state.clipboard.set_offer(None);
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and drop
|
||||
wl_data_device::Event::Enter {
|
||||
serial,
|
||||
surface,
|
||||
|
@ -1800,10 +1847,134 @@ impl Dispatch<wl_data_offer::WlDataOffer, ()> for WaylandClientStatePtr {
|
|||
|
||||
match event {
|
||||
wl_data_offer::Event::Offer { mime_type } => {
|
||||
// Drag and drop
|
||||
if mime_type == FILE_LIST_MIME_TYPE {
|
||||
let serial = state.serial_tracker.get(SerialKind::DataDevice);
|
||||
let mime_type = mime_type.clone();
|
||||
data_offer.accept(serial, Some(mime_type));
|
||||
}
|
||||
|
||||
// Clipboard
|
||||
if let Some(offer) = state
|
||||
.data_offers
|
||||
.iter_mut()
|
||||
.find(|wrapper| wrapper.inner.id() == data_offer.id())
|
||||
{
|
||||
offer.add_mime_type(mime_type);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_data_source::WlDataSource, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
data_source: &wl_data_source::WlDataSource,
|
||||
event: wl_data_source::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
match event {
|
||||
wl_data_source::Event::Send { mime_type, fd } => {
|
||||
state.clipboard.send(mime_type, fd);
|
||||
}
|
||||
wl_data_source::Event::Cancelled => {
|
||||
data_source.destroy();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, ()>
|
||||
for WaylandClientStatePtr
|
||||
{
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
_: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1,
|
||||
event: zwp_primary_selection_device_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
match event {
|
||||
zwp_primary_selection_device_v1::Event::DataOffer { offer } => {
|
||||
let old_offer = state.primary_data_offer.replace(DataOffer::new(offer));
|
||||
if let Some(old_offer) = old_offer {
|
||||
old_offer.inner.destroy();
|
||||
}
|
||||
}
|
||||
zwp_primary_selection_device_v1::Event::Selection { id: data_offer } => {
|
||||
if data_offer.is_some() {
|
||||
let offer = state.primary_data_offer.clone();
|
||||
state.clipboard.set_primary_offer(offer);
|
||||
} else {
|
||||
state.clipboard.set_primary_offer(None);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
event_created_child!(WaylandClientStatePtr, zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, [
|
||||
zwp_primary_selection_device_v1::EVT_DATA_OFFER_OPCODE => (zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()),
|
||||
]);
|
||||
}
|
||||
|
||||
impl Dispatch<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()>
|
||||
for WaylandClientStatePtr
|
||||
{
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
_data_offer: &zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1,
|
||||
event: zwp_primary_selection_offer_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
match event {
|
||||
zwp_primary_selection_offer_v1::Event::Offer { mime_type } => {
|
||||
if let Some(offer) = state.primary_data_offer.as_mut() {
|
||||
offer.add_mime_type(mime_type);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
|
||||
for WaylandClientStatePtr
|
||||
{
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
selection_source: &zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1,
|
||||
event: zwp_primary_selection_source_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
match event {
|
||||
zwp_primary_selection_source_v1::Event::Send { mime_type, fd } => {
|
||||
state.clipboard.send_primary(mime_type, fd);
|
||||
}
|
||||
zwp_primary_selection_source_v1::Event::Cancelled => {
|
||||
selection_source.destroy();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue