linux(x11): Add support for pasting images from clipboard (#29387)

Closes:
https://github.com/zed-industries/zed/pull/29177#issuecomment-2823359242

Removes dependency on
[quininer/x11-clipboard](https://github.com/quininer/x11-clipboard) as
it is in [maintenance
mode](https://github.com/quininer/x11-clipboard/issues/19).

X11 clipboard functionality is now built-in to GPUI which was
accomplished by stripping the non-x11-related code/abstractions from
[1Password/arboard](https://github.com/1Password/arboard) and extending
it to support all image formats already supported by GPUI on wayland and
macos.

A benefit of switching over to the `arboard` implementation, is that we
now make an attempt to have an X11 "clipboard manager" (if available -
something the user has to setup themselves) save the contents of
clipboard (if the last copy operation was within Zed) so that the copied
contents can still be pasted once Zed has completely stopped.

Release Notes:

- Linux(X11): Add support for pasting images from clipboard
This commit is contained in:
Ben Kunkle 2025-04-28 13:55:26 -04:00 committed by GitHub
parent cd86905ebe
commit ef33666701
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1240 additions and 49 deletions

View file

@ -1,4 +1,3 @@
use crate::platform::scap_screen_capture::scap_screen_sources;
use core::str;
use std::{
cell::RefCell,
@ -41,8 +40,9 @@ use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSIO
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask, STATE_LAYOUT_EFFECTIVE};
use super::{
ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail, get_valuator_axis_index,
modifiers_from_state, pressed_button_from_mask,
ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail,
clipboard::{self, Clipboard},
get_valuator_axis_index, modifiers_from_state, pressed_button_from_mask,
};
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
use super::{XimCallbackEvent, XimHandler};
@ -56,6 +56,7 @@ use crate::platform::{
reveal_path_internal,
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
},
scap_screen_capture::scap_screen_sources,
};
use crate::{
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
@ -201,7 +202,7 @@ pub struct X11ClientState {
pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
pub(crate) common: LinuxCommon,
pub(crate) clipboard: x11_clipboard::Clipboard,
pub(crate) clipboard: Clipboard,
pub(crate) clipboard_item: Option<ClipboardItem>,
pub(crate) xdnd_state: Xdnd,
}
@ -388,7 +389,7 @@ impl X11Client {
.reply()
.unwrap();
let clipboard = x11_clipboard::Clipboard::new().unwrap();
let clipboard = Clipboard::new().unwrap();
let xcb_connection = Rc::new(xcb_connection);
@ -1496,39 +1497,36 @@ impl LinuxClient for X11Client {
let state = self.0.borrow_mut();
state
.clipboard
.store(
state.clipboard.setter.atoms.primary,
state.clipboard.setter.atoms.utf8_string,
item.text().unwrap_or_default().as_bytes(),
.set_text(
std::borrow::Cow::Owned(item.text().unwrap_or_default()),
clipboard::ClipboardKind::Primary,
clipboard::WaitConfig::None,
)
.ok();
.context("Failed to write to clipboard (primary)")
.log_with_level(log::Level::Debug);
}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
let mut state = self.0.borrow_mut();
state
.clipboard
.store(
state.clipboard.setter.atoms.clipboard,
state.clipboard.setter.atoms.utf8_string,
item.text().unwrap_or_default().as_bytes(),
.set_text(
std::borrow::Cow::Owned(item.text().unwrap_or_default()),
clipboard::ClipboardKind::Clipboard,
clipboard::WaitConfig::None,
)
.ok();
.context("Failed to write to clipboard (clipboard)")
.log_with_level(log::Level::Debug);
state.clipboard_item.replace(item);
}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
let state = self.0.borrow_mut();
state
return state
.clipboard
.load(
state.clipboard.getter.atoms.primary,
state.clipboard.getter.atoms.utf8_string,
state.clipboard.getter.atoms.property,
Duration::from_secs(3),
)
.map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap()))
.ok()
.get_any(clipboard::ClipboardKind::Primary)
.context("Failed to read from clipboard (primary)")
.log_with_level(log::Level::Debug);
}
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
@ -1537,26 +1535,15 @@ impl LinuxClient for X11Client {
// which has metadata attached.
if state
.clipboard
.setter
.connection
.get_selection_owner(state.clipboard.setter.atoms.clipboard)
.ok()
.and_then(|r| r.reply().ok())
.map(|reply| reply.owner == state.clipboard.setter.window)
.unwrap_or(false)
.is_owner(clipboard::ClipboardKind::Clipboard)
{
return state.clipboard_item.clone();
}
state
return state
.clipboard
.load(
state.clipboard.getter.atoms.clipboard,
state.clipboard.getter.atoms.utf8_string,
state.clipboard.getter.atoms.property,
Duration::from_secs(3),
)
.map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap()))
.ok()
.get_any(clipboard::ClipboardKind::Clipboard)
.context("Failed to read from clipboard (clipboard)")
.log_with_level(log::Level::Debug);
}
fn run(&self) {