use raw_window_handle::RawDisplayHandle;
pub struct Clipboard {
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
arboard: Option<arboard::Clipboard>,
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
smithay: Option<smithay_clipboard::Clipboard>,
clipboard: String,
}
impl Clipboard {
#[expect(unsafe_code)]
pub unsafe fn new(_raw_display_handle: Option<RawDisplayHandle>) -> Self {
Self {
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
arboard: init_arboard(),
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
smithay: unsafe { init_smithay_clipboard(_raw_display_handle) },
clipboard: Default::default(),
}
}
#[cfg_attr(
not(feature = "clipboard"),
expect(clippy::needless_pass_by_ref_mut, clippy::unnecessary_wraps)
)]
pub fn get(&mut self) -> Option<String> {
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
if let Some(clipboard) = &mut self.smithay {
match clipboard.load() {
Ok(text) => return Some(text),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => {
log::debug!("smithay paste error: {err}");
}
}
}
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
return match clipboard.get_text() {
Ok(text) => Some(text),
Err(err) => {
if !is_expected_content_absence(&err) {
log::error!("arboard paste error: {err}");
}
None
}
};
}
Some(self.clipboard.clone())
}
pub fn set_text(&mut self, text: String) {
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
if let Some(clipboard) = &mut self.smithay {
clipboard.store(text);
return;
}
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_text(text) {
log::error!("arboard copy/cut error: {err}");
}
return;
}
self.clipboard = text;
}
#[cfg_attr(
not(feature = "clipboard"),
expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)
)]
pub fn get_image(&mut self) -> Option<egui::ColorImage> {
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
return match clipboard.get_image() {
Ok(image) => Some(color_image_from_arboard(&image)),
Err(err) => {
if !is_expected_content_absence(&err) {
log::error!("arboard paste-image error: {err}");
}
None
}
};
}
None
}
#[cfg_attr(
not(feature = "clipboard"),
expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)
)]
pub fn set_image(&mut self, image: &egui::ColorImage) {
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_image(arboard_from_color_image(image)) {
log::error!("arboard copy/cut error: {err}");
}
log::debug!("Copied image to clipboard");
return;
}
log::error!(
"Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it."
);
_ = image;
}
}
cfg_select! {
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
) => {
#[cfg_attr(
not(any(feature = "arboard", feature = "smithay-clipboard")),
expect(
clippy::unused_self,
clippy::needless_pass_by_ref_mut,
clippy::needless_pass_by_value,
reason = "no backend to use"
)
)]
impl Clipboard {
pub fn get_primary_text(&mut self) -> Option<String> {
#[cfg(feature = "smithay-clipboard")]
if let Some(clipboard) = &mut self.smithay {
return clipboard
.load_primary()
.inspect_err(|err| log::debug!("smithay primary paste error: {err}"))
.ok();
}
#[cfg(feature = "arboard")]
if let Some(clipboard) = &mut self.arboard {
use arboard::GetExtLinux as _;
return clipboard
.get()
.clipboard(arboard::LinuxClipboardKind::Primary)
.text()
.inspect_err(|err| log::debug!("arboard primary paste error: {err}"))
.ok();
}
None
}
pub fn set_primary_text(&mut self, text: String) {
#[cfg(feature = "smithay-clipboard")]
if let Some(clipboard) = &mut self.smithay {
clipboard.store_primary(text);
return;
}
#[cfg(feature = "arboard")]
if let Some(clipboard) = &mut self.arboard {
use arboard::SetExtLinux as _;
if let Err(err) = clipboard
.set()
.clipboard(arboard::LinuxClipboardKind::Primary)
.text(text)
{
log::error!("arboard primary copy error: {err}");
}
return;
}
_ = text;
}
}
}
_ => {
#[expect(
clippy::unused_self,
clippy::needless_pass_by_ref_mut,
reason = "there is no PRIMARY selection on this platform"
)]
impl Clipboard {
pub fn get_primary_text(&mut self) -> Option<String> {
None
}
pub fn set_primary_text(&mut self, _text: String) {}
}
}
}
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
fn is_expected_content_absence(err: &arboard::Error) -> bool {
matches!(err, arboard::Error::ContentNotAvailable)
}
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
fn color_image_from_arboard(image: &arboard::ImageData<'_>) -> egui::ColorImage {
egui::ColorImage::from_rgba_unmultiplied([image.width, image.height], &image.bytes)
}
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
fn arboard_from_color_image(image: &egui::ColorImage) -> arboard::ImageData<'static> {
let bytes: Vec<u8> = image
.pixels
.iter()
.flat_map(|color| color.to_srgba_unmultiplied())
.collect();
arboard::ImageData {
width: image.width(),
height: image.height(),
bytes: bytes.into(),
}
}
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
fn init_arboard() -> Option<arboard::Clipboard> {
profiling::function_scope!();
log::trace!("Initializing arboard clipboard…");
match arboard::Clipboard::new() {
Ok(clipboard) => Some(clipboard),
Err(err) => {
log::warn!("Failed to initialize arboard clipboard: {err}");
None
}
}
}
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
#[expect(unsafe_code)]
unsafe fn init_smithay_clipboard(
raw_display_handle: Option<RawDisplayHandle>,
) -> Option<smithay_clipboard::Clipboard> {
profiling::function_scope!();
if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
log::trace!("Initializing smithay clipboard…");
Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) })
} else {
#[cfg(feature = "wayland")]
log::debug!("Cannot init smithay clipboard without a Wayland display handle");
#[cfg(not(feature = "wayland"))]
log::debug!(
"Cannot init smithay clipboard: the 'wayland' feature of 'egui-winit' is not enabled"
);
None
}
}
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
#[cfg(test)]
mod tests {
use super::{arboard_from_color_image, color_image_from_arboard, is_expected_content_absence};
#[test]
fn only_content_not_available_is_treated_as_expected() {
assert!(is_expected_content_absence(
&arboard::Error::ContentNotAvailable
));
assert!(!is_expected_content_absence(
&arboard::Error::ClipboardNotSupported
));
assert!(!is_expected_content_absence(
&arboard::Error::ClipboardOccupied
));
assert!(!is_expected_content_absence(
&arboard::Error::ConversionFailure
));
assert!(!is_expected_content_absence(&arboard::Error::Unknown {
description: "anything".to_owned(),
}));
}
#[test]
fn color_image_from_arboard_converts_straight_to_premultiplied_alpha() {
let image = arboard::ImageData {
width: 2,
height: 1,
bytes: std::borrow::Cow::Borrowed(&[255, 0, 0, 255, 255, 255, 255, 128]),
};
let color_image = color_image_from_arboard(&image);
assert_eq!(color_image.size, [2, 1]);
assert_eq!(
color_image.pixels[0],
egui::Color32::from_rgba_unmultiplied(255, 0, 0, 255)
);
assert_eq!(
color_image.pixels[1],
egui::Color32::from_rgba_unmultiplied(255, 255, 255, 128)
);
}
#[test]
fn arboard_from_color_image_converts_premultiplied_to_straight_alpha() {
let color_image = egui::ColorImage::new(
[3, 1],
vec![
egui::Color32::from_rgba_unmultiplied(255, 0, 0, 255),
egui::Color32::from_rgba_unmultiplied(255, 255, 255, 128),
egui::Color32::TRANSPARENT,
],
);
let image = arboard_from_color_image(&color_image);
assert_eq!([image.width, image.height], [3, 1]);
assert_eq!(
&*image.bytes,
&[255, 0, 0, 255, 255, 255, 255, 128, 0, 0, 0, 0]
);
}
}