refactor: use iced 0.15 rich clipboard

it does use arboard internally,
and yet somehow it's able to copy huge (25MB) images.

either way, it's nicer to use stuff built into iced
instead of dealing with the Clipboard object ourselves.
This commit is contained in:
electria 2026-08-28 12:28:45 -07:00
commit 6b4af8cd7d
Signed by: electria
SSH key fingerprint: SHA256:8LlB3ucPbBHqozqkhsNbaV5oG3SlzzqUj8FZDL6IPQs
4 changed files with 228 additions and 486 deletions

View file

@ -6,9 +6,8 @@ use std::{
path::{Path, PathBuf},
};
use arboard::Clipboard;
use iced::{
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event,
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, clipboard, event,
keyboard::{self, Key, key},
theme, widget, window,
};
@ -29,6 +28,8 @@ enum Message {
Yank,
Put,
ImageYanked(Result<(), clipboard::Error>),
ImagePut(Result<clipboard::Image, clipboard::Error>),
FocusNext,
FocusPrevious,
@ -49,8 +50,6 @@ struct State {
error: Option<Cow<'static, str>>,
info: Option<String>,
infobar_shown: bool,
clipboard: Option<Clipboard>,
}
impl State {
fn new() -> (Self, Task<Message>) {
@ -177,44 +176,27 @@ impl State {
Message::Yank => {
if let Some(image) = self.image.as_ref() {
match match self.clipboard.take() {
Some(c) => Ok(c),
None => Clipboard::new(),
} {
Ok(mut clipboard) => {
self.error = clipboard
.set_image(utils::arboard_from_rgbaimage(image))
.map_err(|e| e.to_string().into())
.err();
self.clipboard = Some(clipboard);
}
Err(e) => self.error = Some(e.to_string().into()),
}
return clipboard::write(utils::clipboard_image_from_rgbaimage(&image))
.map(Message::ImageYanked);
} else {
self.error = Some("no image to yank".into());
}
}
Message::Put => {
match match self.clipboard.take() {
Some(c) => Ok(c),
None => Clipboard::new(),
} {
Ok(mut clipboard) => {
let result = clipboard.get_image().map(utils::rgbaimage_from_arboard);
Message::Put => return clipboard::read_image().map(Message::ImagePut),
self.error = result.as_ref().map_err(|e| e.to_string().into()).err();
self.clipboard = Some(clipboard);
Message::ImageYanked(result) => {
self.error = result.err().map(|e| format!("{e:?}").into());
}
Message::ImagePut(result) => {
self.error = result.as_ref().err().map(|e| format!("{e:?}").into());
if let Ok(image) = result {
self.info = Some(format!("{}x{}", image.width(), image.height(),));
if image.dimensions() < (100, 100) {
self.image_filter = widget::image::FilterMethod::Nearest;
}
self.image = Some(image);
return self.allocate_image();
}
if let Ok(image) = result {
self.info = Some(format!("{}x{}", image.size.width, image.size.height,));
if (image.size.width, image.size.height) < (100, 100) {
self.image_filter = widget::image::FilterMethod::Nearest;
}
Err(e) => self.error = Some(e.to_string().into()),
self.image = Some(utils::rgbaimage_from_clipboard_image(image));
return self.allocate_image();
}
}
@ -333,10 +315,10 @@ fn main() -> Result<(), iced::Error> {
.title(State::title)
.theme(Theme::custom(
"custom",
theme::Palette {
theme::palette::Seed {
background: Color::BLACK,
text: Color::WHITE,
..theme::Palette::DARK
..theme::palette::Seed::DARK
},
))
.run()

View file

@ -4,8 +4,8 @@ use std::{
path::{Path, PathBuf},
};
use iced::widget;
use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage};
use iced::{clipboard, widget};
use image::{DynamicImage, EncodableLayout, ImageDecoder, ImageReader, ImageResult, RgbaImage};
use rfd::AsyncFileDialog;
pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> {
@ -67,18 +67,18 @@ pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static
}
}
pub fn arboard_from_rgbaimage<'a>(image: &'a RgbaImage) -> arboard::ImageData<'a> {
arboard::ImageData {
width: image.width() as usize,
height: image.height() as usize,
bytes: Cow::Borrowed(image.as_ref()),
pub fn clipboard_image_from_rgbaimage(image: &RgbaImage) -> clipboard::Image {
clipboard::Image {
size: (image.width(), image.height()).into(),
// SAFETY: this is also probably a race condition,
// where if the user switches the image before it's done,
// then something will die.
//
// however, especially with larger images,
// copying them is just too expensive.
rgba: unsafe { std::mem::transmute::<&[u8], &'static [u8]>(image.as_bytes()) }.into(),
}
}
pub fn rgbaimage_from_arboard(image: arboard::ImageData) -> RgbaImage {
RgbaImage::from_raw(
image.width as u32,
image.height as u32,
image.bytes.into_owned(),
)
.unwrap()
pub fn rgbaimage_from_clipboard_image(image: clipboard::Image) -> RgbaImage {
RgbaImage::from_raw(image.size.width, image.size.height, image.rgba.into()).unwrap()
}