feat: make pickers async

This commit is contained in:
electria 2026-07-31 15:11:41 -07:00
commit 5d5756121a
Signed by: electria
SSH key fingerprint: SHA256:8LlB3ucPbBHqozqkhsNbaV5oG3SlzzqUj8FZDL6IPQs
2 changed files with 42 additions and 31 deletions

View file

@ -1,4 +1,9 @@
use std::{borrow::Cow, env, ffi::OsStr, path::Path}; use std::{
borrow::Cow,
env,
ffi::OsStr,
path::{Path, PathBuf},
};
use iced::{ use iced::{
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event, Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event,
@ -12,7 +17,9 @@ mod utils;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum Message { enum Message {
ImagePicked(Result<PathBuf, String>),
ImageDisplayReady(Result<widget::image::Allocation, widget::image::Error>), ImageDisplayReady(Result<widget::image::Allocation, widget::image::Error>),
SavePathPicked(Result<PathBuf, String>),
Event(iced::Event), Event(iced::Event),
} }
@ -74,9 +81,26 @@ impl State {
} }
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::ImagePicked(result) => match result.and_then(|path| self.load_image(path)) {
Err(e) => self.error = Some(e),
Ok(task) => {
self.error = None;
return task;
}
},
Message::ImageDisplayReady(result) => { Message::ImageDisplayReady(result) => {
self.image_display = Some(result.unwrap()); self.image_display = Some(result.unwrap());
} }
Message::SavePathPicked(result) => {
self.error = result
.and_then(|path| {
self.image.as_ref().map_or_else(
|| Err("no image to save".into()),
|image| image.save(path).map_err(|e| e.to_string()),
)
})
.err();
}
Message::Event(iced::Event::Keyboard(keyboard::Event::KeyPressed { Message::Event(iced::Event::Keyboard(keyboard::Event::KeyPressed {
key, key,
@ -91,13 +115,9 @@ impl State {
return widget::operation::focus_next(); return widget::operation::focus_next();
} }
Key::Character("o") => match self.pick_and_load_image() { Key::Character("o") => {
Ok(task) => { return Task::perform(utils::pick_image(), Message::ImagePicked);
self.error = None;
return task;
} }
Err(e) => self.error = Some(e),
},
Key::Character("r") => match self.image.as_ref() { Key::Character("r") => match self.image.as_ref() {
None => self.error = Some("no image to rotate".into()), None => self.error = Some("no image to rotate".into()),
@ -126,7 +146,12 @@ impl State {
} }
Key::Character("s") => { Key::Character("s") => {
self.save_image(); if self.image.is_some() {
self.error = None;
return Task::perform(utils::pick_save_path(), Message::SavePathPicked);
} else {
self.error = Some("no image to save".into())
}
} }
Key::Character("q") => return window::latest().and_then(window::close), Key::Character("q") => return window::latest().and_then(window::close),
@ -169,13 +194,6 @@ impl State {
self.allocate_image() self.allocate_image()
}) })
} }
fn pick_and_load_image(&mut self) -> Result<Task<Message>, String> {
utils::pick_image().and_then(|path| self.load_image(path))
}
fn save_image(&mut self) {
self.error = utils::save_image(self.image.as_ref()).err();
}
fn allocate_image(&self) -> Task<Message> { fn allocate_image(&self) -> Task<Message> {
let Some(image) = self.image.as_ref() else { let Some(image) = self.image.as_ref() else {

View file

@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use iced::widget; use iced::widget;
use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage};
use rfd::FileDialog; 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> {
load_image_impl(path).map_err(|e| e.to_string()) load_image_impl(path).map_err(|e| e.to_string())
@ -20,8 +20,8 @@ fn load_image_impl(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
Ok(decoded_image.into_rgba8()) Ok(decoded_image.into_rgba8())
} }
pub fn pick_image() -> Result<PathBuf, String> { pub async fn pick_image() -> Result<PathBuf, String> {
let Some(path) = FileDialog::new() let Some(filehandle) = AsyncFileDialog::new()
.add_filter( .add_filter(
"image", "image",
&[ &[
@ -30,27 +30,20 @@ pub fn pick_image() -> Result<PathBuf, String> {
], ],
) )
.pick_file() .pick_file()
.await
else { else {
return Err("no path to open provided".into()); return Err("no path to open provided".into());
}; };
Ok(path) Ok(filehandle.path().to_owned())
} }
pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> { pub async fn pick_save_path() -> Result<PathBuf, String> {
let Some(image) = image else { let Some(filehandle) = AsyncFileDialog::new().save_file().await else {
return Err("no image to save".into());
};
let Some(path) = FileDialog::new().save_file() else {
return Err("no path to save provided".into()); return Err("no path to save provided".into());
}; };
if let Err(e) = image.save(&path) { Ok(filehandle.path().to_owned())
return Err(e.to_string());
}
Ok(())
} }
pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str { pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str {