diff --git a/flake.nix b/flake.nix index 0589766..e4b3c3c 100644 --- a/flake.nix +++ b/flake.nix @@ -32,8 +32,6 @@ libxcb ] ++ lib.optionals stdenv.hostPlatform.isLinux [ - dbus.lib - vulkan-loader wayland ]; diff --git a/src/app/maybe_image.rs b/src/app/maybe_image.rs deleted file mode 100644 index c8fff05..0000000 --- a/src/app/maybe_image.rs +++ /dev/null @@ -1,28 +0,0 @@ -use iced::{task, widget}; -use image::DynamicImage; - -#[derive(Clone, Debug, Default)] -pub enum MaybeImage { - #[default] - Unloaded, - - #[allow(dead_code)] - /// should be abort_on_drop, - /// so that it will abort, for instance, - /// when it's set back to `Self::Unloaded` - Loading(task::Handle), - - Loaded(widget::image::Handle), -} -impl MaybeImage { - pub fn handle(&self) -> Option<&widget::image::Handle> { - match self { - Self::Loaded(h) => Some(&h), - _ => None, - } - } -} - -pub fn handle_from_image(image: DynamicImage) -> widget::image::Handle { - widget::image::Handle::from_rgba(image.width(), image.height(), image.into_rgba8().into_raw()) -} diff --git a/src/app/mod.rs b/src/app/mod.rs index 8ef9843..4d51ac0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,46 +1,27 @@ -use std::{ - borrow::Cow, - collections::BTreeMap, - env, - path::{Path, PathBuf}, - process, - sync::Arc, -}; +use std::{borrow::Cow, collections::HashMap, env, path::PathBuf, process}; use iced::{ Color, Element, Event, Length, Renderer, Subscription, Task, Theme, application, event, keyboard, theme, widget, window, }; use ignore::{WalkBuilder, types::TypesBuilder}; -use image::ImageError; use rayon::iter::{ParallelBridge, ParallelIterator}; -use rfd::AsyncFileDialog; -use crate::{ - SUPPORTED_TYPES, - app::maybe_image::{MaybeImage, handle_from_image}, - utils, -}; - -mod maybe_image; +use crate::SUPPORTED_TYPES; #[derive(Clone, Debug)] enum Message { LoadImage(PathBuf), - ImageLoaded(Result<(PathBuf, widget::image::Handle), Arc>), - UnloadImage(PathBuf), OpenImage(PathBuf), - OpenPath(Option), - KeyPressed(keyboard::Key, keyboard::Modifiers), } #[derive(Default)] struct State { - images: BTreeMap, + images: HashMap>, columns: usize, @@ -48,58 +29,47 @@ struct State { } impl State { - fn new() -> (Self, Task) { - ( - Self { - columns: 3, - error: None, - images: BTreeMap::new(), - }, - Task::done(Message::OpenPath(Some( + fn new() -> Self { + let mut builder = TypesBuilder::new(); + builder.add_defaults(); + for ext in SUPPORTED_TYPES { + builder.add("supportedImages", ext).unwrap(); + } + builder.select("supportedImages"); + + Self { + columns: 3, + error: None, + images: WalkBuilder::new( env::args() .nth(1) - .unwrap_or_else(|| String::from("./")) - .into(), - ))), - ) + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed("./")) + .as_ref(), + ) + .types(builder.build().unwrap()) + .max_depth(Some(1)) + .build() + .par_bridge() + .filter_map(|r| { + r.inspect_err(|e| eprintln!("{e}")) + .ok() + .map(|entry| entry.into_path()) + }) + .filter(|path| path.is_file()) + .map(|path| (path, None)) + .collect(), + } } fn update(&mut self, message: Message) -> Task { match message { Message::LoadImage(path) => { - let entry = self.images.entry(path.clone()).or_default(); - - if matches!(entry, MaybeImage::Unloaded) { - let size = 1000 / self.columns as u32; - let (task, handle) = Task::perform( - async move { - utils::load_thumbnail(&path, size) - .map(|img| (path, handle_from_image(img))) - .map_err(Arc::new) - }, - Message::ImageLoaded, - ) - .abortable(); - - *entry = MaybeImage::Loading(handle.abort_on_drop()); - return task; - } + self.images + .entry(path.clone()) + .and_modify(|e| *e = Some(widget::image::Handle::from_path(path))); } - Message::ImageLoaded(result) => { - self.error = result.as_ref().err().map(|e| e.to_string()); - - if let Ok((path, handle)) = result { - let entry = self.images.entry(path).or_default(); - - if matches!(entry, MaybeImage::Loaded(_)) { - self.error = Some("tried to load already-loaded image!".into()) - } else { - *entry = MaybeImage::Loaded(handle) - } - } - } - Message::UnloadImage(path) => { - self.images.entry(path).insert_entry(MaybeImage::Unloaded); + self.images.entry(path).and_modify(|e| *e = None); } Message::OpenImage(path) => { @@ -110,17 +80,6 @@ impl State { .err() } - Message::OpenPath(opt_path) => { - self.error = match opt_path { - Some(_) => None, - None => Some("no path to open given".into()), - }; - - if let Some(path) = opt_path { - self.open_path(path); - } - } - Message::KeyPressed(key, modifiers) => match key.as_ref() { // input field cycling keyboard::Key::Named(keyboard::key::Named::Tab) => { @@ -131,13 +90,6 @@ impl State { }; } - keyboard::Key::Character("o") => { - return Task::perform( - async { AsyncFileDialog::new().pick_folder().await.map(Into::into) }, - Message::OpenPath, - ); - } - keyboard::Key::Character("=" | "+") => { self.columns = (self.columns + 1).clamp(1, 10) } @@ -157,28 +109,18 @@ impl State { fn view(&self) -> Element<'_, Message, Theme, Renderer> { widget::column([ widget::scrollable( - widget::grid(self.images.iter().map(|(path, maybe_image)| { - widget::button(widget::column([ + widget::grid(self.images.iter().map(|(path, opt_handle)| { + widget::button( widget::container( - widget::sensor(maybe_image.handle().map_or_else( + widget::sensor(opt_handle.as_ref().map_or_else( || Element::from(widget::space()), |handle| widget::image(handle).into(), )) - .anticipate(2000) .on_show(|_size| Message::LoadImage(path.clone())) .on_hide(Message::UnloadImage(path.clone())), ) - .center(Length::Fill) - .into(), - widget::text( - path.file_name() - .map_or(Cow::Borrowed("[no file]"), |extension| { - extension.to_string_lossy() - }), - ) - .center() - .into(), - ])) + .center(Length::Fill), + ) .style(widget::button::subtle) .on_press(Message::OpenImage(path.clone())) .into() @@ -187,10 +129,13 @@ impl State { ) .height(Length::Fill) .into(), - self.error.as_ref().map_or_else( - || Element::from(widget::space()), - |error| widget::text(error).style(widget::text::danger).into(), - ), + self.error + .as_ref() + .map_or_else( + || Element::from(widget::space()), + |error| widget::text(error).style(widget::text::danger).into(), + ) + .into(), ]) .into() } @@ -203,29 +148,6 @@ impl State { _ => None, }) } - - fn open_path(&mut self, path: impl AsRef) { - let mut builder = TypesBuilder::new(); - builder.add_defaults(); - for ext in SUPPORTED_TYPES { - builder.add("supportedImages", ext).unwrap(); - } - builder.select("supportedImages"); - - self.images = WalkBuilder::new(path) - .types(builder.build().unwrap()) - .max_depth(Some(1)) - .build() - .par_bridge() - .filter_map(|r| { - r.inspect_err(|e| eprintln!("{e}")) - .ok() - .map(|entry| entry.into_path()) - }) - .filter(|path| path.is_file()) - .map(|path| (path, MaybeImage::Unloaded)) - .collect(); - } } pub fn run() -> Result<(), impl std::error::Error> { diff --git a/src/main.rs b/src/main.rs index 96bfe83..8a836ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ mod app; -mod utils; const SUPPORTED_TYPES: &[&str; 20] = &[ "*.png", "*.PNG", "*.jpg", "*.JPG", "*.jpeg", "*.JPEG", "*.avif", "*.jxl", "*.bmp", "*.exr", diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 3eb1795..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::{ffi::OsStr, path::Path}; - -use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult}; - -pub fn load_thumbnail(path: impl AsRef, max_size: u32) -> ImageResult { - let mut decoder = ImageReader::open(&path)? - .with_guessed_format()? - .into_decoder()?; - - let oreintation = decoder.orientation()?; - - let mut decoded_image = DynamicImage::from_decoder(decoder)?.thumbnail(max_size, max_size); - - // the condition is a workaround to not rotate JXL images twice; - // since they are already rotated by the decoder - // (while jpegs for instance aren't) - if !path - .as_ref() - .extension() - .map(OsStr::to_string_lossy) - .is_some_and(|s| s == "jxl") - { - decoded_image.apply_orientation(oreintation); - } - - Ok(decoded_image) -}