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

619
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ size = "0.5.0"
zip = "8.6.0" zip = "8.6.0"
[dependencies.iced] [dependencies.iced]
version = "0.14.0" version = "0.15.0-dev"
features = [ "image" ] features = [ "image" ]
[dependencies.image] [dependencies.image]
@ -25,6 +25,5 @@ version = "0.12.6"
# I really wish the image crate was named better # I really wish the image crate was named better
features = [ "image" ] features = [ "image" ]
[dependencies.arboard] [patch.crates-io]
version = "3.6.1" iced.git = "https://github.com/iced-rs/iced"
features = [ "wayland-data-control" ]

View file

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

View file

@ -4,8 +4,8 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use iced::widget; use iced::{clipboard, widget};
use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; use image::{DynamicImage, EncodableLayout, ImageDecoder, ImageReader, ImageResult, RgbaImage};
use rfd::AsyncFileDialog; use rfd::AsyncFileDialog;
pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> { 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> { pub fn clipboard_image_from_rgbaimage(image: &RgbaImage) -> clipboard::Image {
arboard::ImageData { clipboard::Image {
width: image.width() as usize, size: (image.width(), image.height()).into(),
height: image.height() as usize, // SAFETY: this is also probably a race condition,
bytes: Cow::Borrowed(image.as_ref()), // 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 { pub fn rgbaimage_from_clipboard_image(image: clipboard::Image) -> RgbaImage {
RgbaImage::from_raw( RgbaImage::from_raw(image.size.width, image.size.height, image.rgba.into()).unwrap()
image.width as u32,
image.height as u32,
image.bytes.into_owned(),
)
.unwrap()
} }