use std::{borrow::Cow, cell::RefCell, io::Cursor};
use gpui::{ClipboardEntry, ClipboardItem, Image, ImageFormat};
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Kind {
Clipboard,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
Primary,
}
#[derive(Default)]
pub(crate) struct Clipboard {
inner: RefCell<Option<arboard::Clipboard>>,
metadata: RefCell<Option<(Kind, String, String)>>,
}
impl Clipboard {
fn with<T>(
&self,
op: &str,
f: impl FnOnce(&mut arboard::Clipboard) -> Result<T, arboard::Error>,
) -> Option<T> {
let mut slot = self.inner.borrow_mut();
if slot.is_none() {
match arboard::Clipboard::new() {
Ok(cb) => *slot = Some(cb),
Err(e) => {
log::warn!("tauri-plugin-gpui: cannot open clipboard: {e}");
return None;
}
}
}
match f(slot.as_mut()?) {
Ok(v) => Some(v),
Err(arboard::Error::ContentNotAvailable) => None,
Err(e) => {
log::warn!("tauri-plugin-gpui: {op} failed: {e}");
None
}
}
}
pub(crate) fn read(&self, kind: Kind) -> Option<ClipboardItem> {
if let Some(text) = self.with("clipboard read", |cb| get(cb, kind).text()) {
let metadata = self.metadata.borrow();
return Some(match &*metadata {
Some((k, t, m)) if *k == kind && *t == text => {
ClipboardItem::new_string_with_metadata(text, m.clone())
}
_ => ClipboardItem::new_string(text),
});
}
let image = self.with("clipboard image read", |cb| get(cb, kind).image())?;
encode_png(image)
.map(|png| ClipboardItem::new_image(&Image::from_bytes(ImageFormat::Png, png)))
}
pub(crate) fn write(&self, kind: Kind, item: ClipboardItem) {
let mut metadata = self.metadata.borrow_mut();
if metadata.as_ref().is_some_and(|(k, ..)| *k == kind) {
*metadata = None;
}
if let Some(text) = item.text() {
if let Some(m) = item.metadata() {
*metadata = Some((kind, text.clone(), m.clone()));
}
self.with("clipboard write", |cb| set(cb, kind).text(text));
return;
}
let image = item.entries().iter().find_map(|e| match e {
ClipboardEntry::Image(image) => Some(image),
_ => None,
});
if let Some(image) = image.and_then(decode_rgba) {
self.with("clipboard image write", |cb| set(cb, kind).image(image));
}
}
}
fn get(cb: &mut arboard::Clipboard, kind: Kind) -> arboard::Get<'_> {
match kind {
Kind::Clipboard => cb.get(),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
Kind::Primary => {
use arboard::{GetExtLinux, LinuxClipboardKind};
cb.get().clipboard(LinuxClipboardKind::Primary)
}
}
}
fn set(cb: &mut arboard::Clipboard, kind: Kind) -> arboard::Set<'_> {
match kind {
Kind::Clipboard => cb.set(),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
Kind::Primary => {
use arboard::{LinuxClipboardKind, SetExtLinux};
cb.set().clipboard(LinuxClipboardKind::Primary)
}
}
}
fn decode_rgba(image: &Image) -> Option<arboard::ImageData<'static>> {
let decoded = image::ImageFormat::from_mime_type(image.format.mime_type())
.ok_or_else(|| format!("unsupported format {:?}", image.format))
.and_then(|format| {
image::load_from_memory_with_format(&image.bytes, format).map_err(|e| e.to_string())
});
match decoded {
Ok(decoded) => {
let rgba = decoded.into_rgba8();
Some(arboard::ImageData {
width: rgba.width() as usize,
height: rgba.height() as usize,
bytes: Cow::Owned(rgba.into_raw()),
})
}
Err(e) => {
log::warn!("tauri-plugin-gpui: cannot copy image to clipboard: {e}");
None
}
}
}
fn encode_png(image: arboard::ImageData) -> Option<Vec<u8>> {
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
let rgba = image::RgbaImage::from_raw(
image.width as u32,
image.height as u32,
image.bytes.into_owned(),
)?;
let mut png = Vec::new();
let encoder = PngEncoder::new_with_quality(
Cursor::new(&mut png),
CompressionType::Fast,
FilterType::Sub,
);
match rgba.write_with_encoder(encoder) {
Ok(()) => Some(png),
Err(e) => {
log::warn!("tauri-plugin-gpui: cannot read clipboard image: {e}");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_round_trips_through_png() {
let rgba = arboard::ImageData {
width: 2,
height: 1,
bytes: Cow::Owned(vec![255, 0, 0, 255, 0, 0, 255, 128]),
};
let png = encode_png(rgba.clone()).unwrap();
let back = decode_rgba(&Image::from_bytes(ImageFormat::Png, png)).unwrap();
assert_eq!((back.width, back.height), (2, 1));
assert_eq!(back.bytes, rgba.bytes);
}
}
#[cfg(test)]
mod perf {
use super::*;
#[test]
#[ignore]
fn bench_encode_png() {
let (width, height) = (3840usize, 2160usize);
let pixel = |f: &dyn Fn(usize, usize, usize) -> u8| -> Vec<u8> {
(0..width * height * 4)
.map(|i| f((i / 4) % width, (i / 4) / width, i % 4))
.collect()
};
let screenshot = pixel(&|x, y, c| {
if y % 40 < 3 {
((x * 7 + y * 13 + c * 31) % 251) as u8
} else {
((x / 200) * 37 + (y / 300) * 11 + c * 5) as u8
}
});
let photo = pixel(&|x, y, c| {
let noise = (x.wrapping_mul(2654435761) ^ y.wrapping_mul(40503) ^ c) % 17;
((x * 255 / width + y * 255 / height) / 2 + noise) as u8
});
for (name, bytes) in [("screenshot", screenshot), ("photo", photo)] {
let mut times = Vec::new();
let mut size = 0;
for _ in 0..11 {
let image = arboard::ImageData {
width,
height,
bytes: Cow::Owned(bytes.clone()),
};
let start = std::time::Instant::now();
size = encode_png(image).unwrap().len();
times.push(start.elapsed());
}
times.sort();
println!(
"bench: encode 4K {name}: median {:.1} ms, min {:.1} ms, {:.2} MB",
times[5].as_secs_f64() * 1e3,
times[0].as_secs_f64() * 1e3,
size as f64 / 1e6
);
}
}
}