diff --git a/flake.nix b/flake.nix index 5a57299..d7fa343 100644 --- a/flake.nix +++ b/flake.nix @@ -19,25 +19,22 @@ cargoToml = fromTOML (builtins.readFile ./Cargo.toml); name = cargoToml.package.name; - dlDeps = - with pkgs; - [ - # needed for both x11 and wayland - libxkbcommon - libGL + dlDeps = with pkgs; [ + # libdbus, for rfd + dbus.lib - libx11 - libxcursor - libxi - libxcb - ] - ++ lib.optionals stdenv.hostPlatform.isLinux [ - # libdbus, for rfd - dbus.lib + # needed for both x11 and wayland + libxkbcommon + libGL + vulkan-loader - vulkan-loader - wayland - ]; + wayland + + libx11 + libxcursor + libxi + libxcb + ]; commonArgs = { # all that's needed for artifacts and checks diff --git a/src/main.rs b/src/main.rs index b478294..5ad2bf3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,4 @@ -use std::{ - borrow::Cow, - env, - ffi::OsStr, - path::{Path, PathBuf}, -}; +use std::{borrow::Cow, env, ffi::OsStr, path::Path}; use iced::{ Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event, @@ -17,9 +12,7 @@ mod utils; #[derive(Clone, Debug)] enum Message { - ImagePicked(Result), ImageDisplayReady(Result), - SavePathPicked(Result), Event(iced::Event), } @@ -36,7 +29,7 @@ struct State { } impl State { fn new() -> (Self, Task) { - let mut state = Self::default(); + let mut state = State::default(); if let Some(path) = env::args().nth(1) { match state.load_image(path) { @@ -50,25 +43,22 @@ impl State { fn view(&self) -> Element<'_, Message, Theme, Renderer> { let mut main = Vec::new(); - main.push(self.image_display.as_ref().map_or_else( - || { - widget::container(widget::text(include_str!("usage.txt"))) - .height(Length::Fill) - .width(Length::Fill) - .align_x(Alignment::Center) - .align_y(Alignment::Center) - .into() - }, - |allocation| { - widget::image::viewer(allocation.handle().clone()) - .filter_method(self.image_filter) - .max_scale(50.) - .min_scale(1.) - .width(Length::Fill) - .height(Length::Fill) - .into() - }, - )); + main.push(if let Some(allocation) = self.image_display.as_ref() { + widget::image::viewer(allocation.handle().clone()) + .filter_method(self.image_filter) + .max_scale(50.) + .min_scale(1.) + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + widget::container(widget::text(include_str!("usage.txt"))) + .height(Length::Fill) + .width(Length::Fill) + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .into() + }); if let Some(error) = self.error.as_ref() { main.push(widget::text(error).style(widget::text::danger).into()); @@ -81,26 +71,9 @@ impl State { } fn update(&mut self, message: Message) -> Task { 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) => { 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 { key, @@ -111,30 +84,39 @@ impl State { Key::Named(key::Named::Tab) => { if modifiers.shift() { return widget::operation::focus_previous(); + } else { + return widget::operation::focus_next(); } - return widget::operation::focus_next(); } - Key::Character("o") => { - return Task::perform(utils::pick_image(), Message::ImagePicked); - } + Key::Character("o") => match self.pick_and_load_image() { + Ok(task) => { + self.error = None; + return task; + } + Err(e) => self.error = Some(e), + }, - Key::Character("r") => match self.image.as_ref() { - None => self.error = Some("no image to rotate".into()), - Some(image) => { - self.error = None; - self.image = Some(imageops::rotate90(image)); - return self.allocate_image(); - } - }, - Key::Character("i") => match self.image.as_mut() { - None => self.error = Some("no image to invert".into()), - Some(image) => { - self.error = None; - imageops::invert(image); - return self.allocate_image(); - } - }, + Key::Character("r") => { + match self.image.as_ref() { + None => self.error = Some("no image to rotate".into()), + Some(image) => { + self.error = None; + self.image = Some(imageops::rotate90(image)); + return self.allocate_image(); + } + }; + } + Key::Character("i") => { + match self.image.as_mut() { + None => self.error = Some("no image to invert".into()), + Some(image) => { + self.error = None; + imageops::invert(image); + return self.allocate_image(); + } + }; + } Key::Character("f") => { self.image_filter = match self.image_filter { widget::image::FilterMethod::Linear => widget::image::FilterMethod::Nearest, @@ -146,12 +128,7 @@ impl State { } Key::Character("s") => { - 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()) - } + self.save_image(); } Key::Character("q") => return window::latest().and_then(window::close), @@ -183,7 +160,8 @@ impl State { "{} {}x{}", path.as_ref() .file_name() - .map_or(Cow::Borrowed("[no file]"), OsStr::to_string_lossy), + .map(OsStr::to_string_lossy) + .unwrap_or(Cow::Borrowed("[no file]")), image.width(), image.height(), )); @@ -194,6 +172,13 @@ impl State { self.allocate_image() }) } + fn pick_and_load_image(&mut self) -> Result, 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 { let Some(image) = self.image.as_ref() else { @@ -225,15 +210,13 @@ impl State { event::listen().map(Message::Event) } fn title(&self) -> String { - self.info.as_ref().map_or_else( - || "imagey".into(), - |info| { - format!( - "imagey {info} {}", - utils::string_from_filter_type(self.image_filter), - ) - }, - ) + match self.info.as_ref() { + Some(info) => format!( + "imagey {info} {}", + utils::string_from_filter_type(self.image_filter), + ), + None => "imagey".into(), + } } } diff --git a/src/utils.rs b/src/utils.rs index fc1841d..dafd1f2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,12 +2,12 @@ use std::path::{Path, PathBuf}; use iced::widget; use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; -use rfd::AsyncFileDialog; +use rfd::FileDialog; pub fn load_image(path: impl AsRef) -> Result { - load_image_impl(path).map_err(|e| e.to_string()) + _load_image(path).map_err(|e| e.to_string()) } -fn load_image_impl(path: impl AsRef) -> ImageResult { +fn _load_image(path: impl AsRef) -> ImageResult { let mut decoder = ImageReader::open(path)? .with_guessed_format()? .into_decoder()?; @@ -20,8 +20,8 @@ fn load_image_impl(path: impl AsRef) -> ImageResult { Ok(decoded_image.into_rgba8()) } -pub async fn pick_image() -> Result { - let Some(filehandle) = AsyncFileDialog::new() +pub fn pick_image() -> Result { + let Some(path) = FileDialog::new() .add_filter( "image", &[ @@ -30,23 +30,30 @@ pub async fn pick_image() -> Result { ], ) .pick_file() - .await else { return Err("no path to open provided".into()); }; - Ok(filehandle.path().to_owned()) + Ok(path) } -pub async fn pick_save_path() -> Result { - let Some(filehandle) = AsyncFileDialog::new().save_file().await else { +pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> { + let Some(image) = image else { + return Err("no image to save".into()); + }; + + let Some(path) = FileDialog::new().save_file() else { return Err("no path to save provided".into()); }; - Ok(filehandle.path().to_owned()) + if let Err(e) = image.save(&path) { + return Err(e.to_string()); + }; + + Ok(()) } -pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str { +pub fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str { match f { widget::image::FilterMethod::Linear => "bilinear", widget::image::FilterMethod::Nearest => "nearest neighbor",