From b11264dc4b3d3e11724c4fa9cc130434081a23d2 Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 9 Jul 2026 20:16:33 -0700 Subject: [PATCH 1/4] build: zenity consistency --- flake.nix | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index 8f401aa..e684327 100644 --- a/flake.nix +++ b/flake.nix @@ -85,10 +85,12 @@ ] ++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper; - buildInputs = commonArgs.buildInputs ++ [ - pkgs.libgcc - pkgs.zenity - ]; + buildInputs = + commonArgs.buildInputs + ++ [ + pkgs.libgcc + ] + ++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.zenity; runtimeDependencies = dlDeps; From c3a5919e87cdc2b3fa87f5a7a21d6392b10b7205 Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 9 Jul 2026 20:16:56 -0700 Subject: [PATCH 2/4] feat: drag and drop (outside of wayland) --- src/main.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main.rs b/src/main.rs index d31b558..838eba8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,6 +111,16 @@ impl State { _ => {} }, + Message::Event(iced::Event::Window(window_event)) => match window_event { + window::Event::FileDropped(file) => { + self.error = self.load_image(file); + return self.allocate_image(); + } + + // ignore unused window events + _ => {} + }, + // ignore unused events Message::Event(_) => {} } From d577fe2e76095fbbc1bb29251261e56843140d4c Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 9 Jul 2026 20:48:34 -0700 Subject: [PATCH 3/4] refactor: move image logic to utils --- src/main.rs | 175 +++++++++------------------------------------------ src/utils.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 145 deletions(-) create mode 100644 src/utils.rs diff --git a/src/main.rs b/src/main.rs index 838eba8..5d1c684 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ -use std::{env, ffi::OsStr, fs, io::Write, path::Path}; +use std::{env, path::Path}; use iced::{ Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event, keyboard::{self, Key, key}, theme, widget, window, }; -use image::{DynamicImage, EncodableLayout, ImageDecoder, ImageReader, RgbaImage, imageops}; -use jpegxl_rs::image::ToDynamic; -use rfd::FileDialog; +use image::{EncodableLayout, RgbaImage, imageops}; + +mod utils; #[derive(Clone, Debug)] enum Message { @@ -27,7 +27,11 @@ impl State { fn new() -> (Self, Task) { let mut state = State::default(); - state.error = env::args().nth(1).and_then(|path| state.load_image(path)); + state.error = env::args().nth(1).and_then(|path| { + utils::load_image(path) + .map(|image| state.image = Some(image)) + .err() + }); let allocate_image = state.allocate_image(); (state, allocate_image) @@ -76,8 +80,7 @@ impl State { } Key::Character("o") => { - self.error = self.open_image(); - return self.allocate_image(); + return self.pick_and_load_image(); } Key::Character("r") => { @@ -102,7 +105,7 @@ impl State { } Key::Character("s") => { - self.error = self.save_image(); + self.save_image(); } Key::Character("q") => return window::latest().and_then(window::close), @@ -113,8 +116,7 @@ impl State { Message::Event(iced::Event::Window(window_event)) => match window_event { window::Event::FileDropped(file) => { - self.error = self.load_image(file); - return self.allocate_image(); + return self.load_image(file); } // ignore unused window events @@ -128,149 +130,32 @@ impl State { Task::none() } - fn load_image(&mut self, path: impl AsRef) -> Option { - let path = path.as_ref(); - - let decoded_image = match path.extension().map(OsStr::to_string_lossy).as_deref() { - Some("jxl") => { - let data = match std::fs::read(path) { - Ok(d) => d, - Err(e) => { - return Some(format!( - "failed to read data from '{}': {e}", - path.display() - )); - } - }; - - match jpegxl_rs::decoder_builder() - .build() - .unwrap() - .decode_to_image(&data) - { - Ok(Some(decoded_image)) => decoded_image, - Ok(None) => { - return Some(format!( - "failed to convert jxl '{}' to DynamicImage", - path.display() - )); - } - Err(e) => { - return Some(format!("failed to decode jxl '{}': {e}", path.display())); - } - } - } - - _ => { - let reader = match ImageReader::open(path) { - Ok(r) => r, - Err(e) => { - return Some(format!( - "failed to create reader for '{}': {e}", - path.display() - )); - } - }; - - let mut decoder = match reader.into_decoder() { - Ok(d) => d, - Err(e) => { - return Some(format!( - "failed to create decoder for '{}': {e}", - path.display() - )); - } - }; - - let oreintation = match decoder.orientation() { - Ok(o) => o, - Err(e) => { - return Some(format!( - "failed to get oreintation of '{}': {e}", - path.display() - )); - } - }; - - let mut decoded_image = match DynamicImage::from_decoder(decoder) { - Ok(i) => i, - Err(e) => { - return Some(format!("failed to decode image '{}': {e}", path.display())); - } - }; - - decoded_image.apply_orientation(oreintation); - - decoded_image + #[must_use] + fn load_image(&mut self, path: impl AsRef) -> Task { + let task; + (self.error, task) = match utils::load_image(path) { + Ok(image) => { + self.image = Some(image); + (None, self.allocate_image()) } + Err(e) => (Some(e), Task::none()), }; - self.image = Some(decoded_image.into_rgba8()); - - None + task } - fn open_image(&mut self) -> Option { - let Some(path) = FileDialog::new() - .add_filter( - "image", - &[ - "png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "avif", "jxl", "bmp", "exr", "ff", - "gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp", - ], - ) - .pick_file() - else { - return Some("no path to open provided".into()); + #[must_use] + fn pick_and_load_image(&mut self) -> Task { + let task; + (self.error, task) = match utils::pick_image() { + Ok(path) => (None, self.load_image(path)), + Err(e) => (Some(e), Task::none()), }; - self.load_image(path) + task } - fn save_image(&self) -> Option { - let Some(image) = self.image.as_ref() else { - return Some("no image to save".into()); - }; - - let Some(path) = FileDialog::new().save_file() else { - return Some("no path to save provided".into()); - }; - - let mut file = match fs::File::create(&path) { - Ok(f) => f, - Err(e) => return Some(format!("failed to create file '{}': {e}", path.display())), - }; - - match path.extension().map(OsStr::to_string_lossy).as_deref() { - Some("jxl") => { - let mut encoder = jpegxl_rs::encoder_builder() - .speed(jpegxl_rs::encode::EncoderSpeed::Glacier) - .lossless(true) - .build() - .unwrap(); - let rgb_image = DynamicImage::from(image.to_owned()).into_rgb8(); - let jxl = match encoder.encode::( - &rgb_image, - rgb_image.width(), - rgb_image.height(), - ) { - Ok(j) => j, - Err(e) => { - return Some(format!("failed to encode jxl '{}': {e}", path.display())); - } - }; - - if let Err(e) = file.write(&jxl.data) { - return Some(format!("failed to write jxl to '{}': {e}", path.display())); - }; - } - _ => { - if let Err(e) = image.save(&path) { - return Some(format!("failed to save '{}': {e}", path.display())); - }; - } - }; - - None + fn save_image(&mut self) { + self.error = utils::save_image(self.image.as_ref()); } fn allocate_image(&self) -> Task { diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..85a5bfc --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,151 @@ +use std::{ + ffi::OsStr, + fs, + io::Write, + path::{Path, PathBuf}, +}; + +use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage}; +use jpegxl_rs::image::ToDynamic; +use rfd::FileDialog; + +pub fn load_image(path: impl AsRef) -> Result { + let path = path.as_ref(); + + let decoded_image = match path.extension().map(OsStr::to_string_lossy).as_deref() { + Some("jxl") => { + let data = match std::fs::read(path) { + Ok(d) => d, + Err(e) => { + return Err(format!( + "failed to read data from '{}': {e}", + path.display() + )); + } + }; + + match jpegxl_rs::decoder_builder() + .build() + .unwrap() + .decode_to_image(&data) + { + Ok(Some(decoded_image)) => decoded_image, + Ok(None) => { + return Err(format!( + "failed to convert jxl '{}' to DynamicImage", + path.display() + )); + } + Err(e) => { + return Err(format!("failed to decode jxl '{}': {e}", path.display())); + } + } + } + + _ => { + let reader = match ImageReader::open(path) { + Ok(r) => r, + Err(e) => { + return Err(format!( + "failed to create reader for '{}': {e}", + path.display() + )); + } + }; + + let mut decoder = match reader.into_decoder() { + Ok(d) => d, + Err(e) => { + return Err(format!( + "failed to create decoder for '{}': {e}", + path.display() + )); + } + }; + + let oreintation = match decoder.orientation() { + Ok(o) => o, + Err(e) => { + return Err(format!( + "failed to get oreintation of '{}': {e}", + path.display() + )); + } + }; + + let mut decoded_image = match DynamicImage::from_decoder(decoder) { + Ok(i) => i, + Err(e) => { + return Err(format!("failed to decode image '{}': {e}", path.display())); + } + }; + + decoded_image.apply_orientation(oreintation); + + decoded_image + } + }; + + Ok(decoded_image.into_rgba8()) +} +pub fn pick_image() -> Result { + let Some(path) = FileDialog::new() + .add_filter( + "image", + &[ + "png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "avif", "jxl", "bmp", "exr", "ff", + "gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp", + ], + ) + .pick_file() + else { + return Err("no path to open provided".into()); + }; + + Ok(path) +} + +#[must_use] +pub fn save_image(image: Option<&RgbaImage>) -> Option { + let Some(image) = image else { + return Some("no image to save".into()); + }; + + let Some(path) = FileDialog::new().save_file() else { + return Some("no path to save provided".into()); + }; + + let mut file = match fs::File::create(&path) { + Ok(f) => f, + Err(e) => return Some(format!("failed to create file '{}': {e}", path.display())), + }; + + match path.extension().map(OsStr::to_string_lossy).as_deref() { + Some("jxl") => { + let mut encoder = jpegxl_rs::encoder_builder() + .speed(jpegxl_rs::encode::EncoderSpeed::Glacier) + .lossless(true) + .build() + .unwrap(); + let rgb_image = DynamicImage::from(image.to_owned()).into_rgb8(); + let jxl = + match encoder.encode::(&rgb_image, rgb_image.width(), rgb_image.height()) { + Ok(j) => j, + Err(e) => { + return Some(format!("failed to encode jxl '{}': {e}", path.display())); + } + }; + + if let Err(e) = file.write(&jxl.data) { + return Some(format!("failed to write jxl to '{}': {e}", path.display())); + }; + } + _ => { + if let Err(e) = image.save(&path) { + return Some(format!("failed to save '{}': {e}", path.display())); + }; + } + }; + + None +} From dab5f8e09fc1c292c0b57b8211c1131ac4c25045 Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 9 Jul 2026 20:51:06 -0700 Subject: [PATCH 4/4] refactor: annotate State::allocate_image with must_use --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 5d1c684..3aef49b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -158,6 +158,7 @@ impl State { self.error = utils::save_image(self.image.as_ref()); } + #[must_use] fn allocate_image(&self) -> Task { let Some(image) = self.image.as_ref() else { eprintln!("no image to allocate");