Compare commits

..
5 changed files with 48 additions and 184 deletions

View file

@ -32,8 +32,6 @@
libxcb libxcb
] ]
++ lib.optionals stdenv.hostPlatform.isLinux [ ++ lib.optionals stdenv.hostPlatform.isLinux [
dbus.lib
vulkan-loader vulkan-loader
wayland wayland
]; ];

View file

@ -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())
}

View file

@ -1,46 +1,27 @@
use std::{ use std::{borrow::Cow, collections::HashMap, env, path::PathBuf, process};
borrow::Cow,
collections::BTreeMap,
env,
path::{Path, PathBuf},
process,
sync::Arc,
};
use iced::{ use iced::{
Color, Element, Event, Length, Renderer, Subscription, Task, Theme, application, event, Color, Element, Event, Length, Renderer, Subscription, Task, Theme, application, event,
keyboard, theme, widget, window, keyboard, theme, widget, window,
}; };
use ignore::{WalkBuilder, types::TypesBuilder}; use ignore::{WalkBuilder, types::TypesBuilder};
use image::ImageError;
use rayon::iter::{ParallelBridge, ParallelIterator}; use rayon::iter::{ParallelBridge, ParallelIterator};
use rfd::AsyncFileDialog;
use crate::{ use crate::SUPPORTED_TYPES;
SUPPORTED_TYPES,
app::maybe_image::{MaybeImage, handle_from_image},
utils,
};
mod maybe_image;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum Message { enum Message {
LoadImage(PathBuf), LoadImage(PathBuf),
ImageLoaded(Result<(PathBuf, widget::image::Handle), Arc<ImageError>>),
UnloadImage(PathBuf), UnloadImage(PathBuf),
OpenImage(PathBuf), OpenImage(PathBuf),
OpenPath(Option<PathBuf>),
KeyPressed(keyboard::Key, keyboard::Modifiers), KeyPressed(keyboard::Key, keyboard::Modifiers),
} }
#[derive(Default)] #[derive(Default)]
struct State { struct State {
images: BTreeMap<PathBuf, MaybeImage>, images: HashMap<PathBuf, Option<widget::image::Handle>>,
columns: usize, columns: usize,
@ -48,58 +29,47 @@ struct State {
} }
impl State { impl State {
fn new() -> (Self, Task<Message>) { 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 { Self {
columns: 3, columns: 3,
error: None, error: None,
images: BTreeMap::new(), images: WalkBuilder::new(
},
Task::done(Message::OpenPath(Some(
env::args() env::args()
.nth(1) .nth(1)
.unwrap_or_else(|| String::from("./")) .map(Cow::Owned)
.into(), .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<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::LoadImage(path) => { Message::LoadImage(path) => {
let entry = self.images.entry(path.clone()).or_default(); self.images
.entry(path.clone())
if matches!(entry, MaybeImage::Unloaded) { .and_modify(|e| *e = Some(widget::image::Handle::from_path(path)));
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;
} }
}
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) => { Message::UnloadImage(path) => {
self.images.entry(path).insert_entry(MaybeImage::Unloaded); self.images.entry(path).and_modify(|e| *e = None);
} }
Message::OpenImage(path) => { Message::OpenImage(path) => {
@ -110,17 +80,6 @@ impl State {
.err() .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() { Message::KeyPressed(key, modifiers) => match key.as_ref() {
// input field cycling // input field cycling
keyboard::Key::Named(keyboard::key::Named::Tab) => { 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("=" | "+") => { keyboard::Key::Character("=" | "+") => {
self.columns = (self.columns + 1).clamp(1, 10) self.columns = (self.columns + 1).clamp(1, 10)
} }
@ -157,28 +109,18 @@ impl State {
fn view(&self) -> Element<'_, Message, Theme, Renderer> { fn view(&self) -> Element<'_, Message, Theme, Renderer> {
widget::column([ widget::column([
widget::scrollable( widget::scrollable(
widget::grid(self.images.iter().map(|(path, maybe_image)| { widget::grid(self.images.iter().map(|(path, opt_handle)| {
widget::button(widget::column([ widget::button(
widget::container( widget::container(
widget::sensor(maybe_image.handle().map_or_else( widget::sensor(opt_handle.as_ref().map_or_else(
|| Element::from(widget::space()), || Element::from(widget::space()),
|handle| widget::image(handle).into(), |handle| widget::image(handle).into(),
)) ))
.anticipate(2000)
.on_show(|_size| Message::LoadImage(path.clone())) .on_show(|_size| Message::LoadImage(path.clone()))
.on_hide(Message::UnloadImage(path.clone())), .on_hide(Message::UnloadImage(path.clone())),
) )
.center(Length::Fill) .center(Length::Fill),
.into(),
widget::text(
path.file_name()
.map_or(Cow::Borrowed("[no file]"), |extension| {
extension.to_string_lossy()
}),
) )
.center()
.into(),
]))
.style(widget::button::subtle) .style(widget::button::subtle)
.on_press(Message::OpenImage(path.clone())) .on_press(Message::OpenImage(path.clone()))
.into() .into()
@ -187,10 +129,13 @@ impl State {
) )
.height(Length::Fill) .height(Length::Fill)
.into(), .into(),
self.error.as_ref().map_or_else( self.error
.as_ref()
.map_or_else(
|| Element::from(widget::space()), || Element::from(widget::space()),
|error| widget::text(error).style(widget::text::danger).into(), |error| widget::text(error).style(widget::text::danger).into(),
), )
.into(),
]) ])
.into() .into()
} }
@ -203,29 +148,6 @@ impl State {
_ => None, _ => None,
}) })
} }
fn open_path(&mut self, path: impl AsRef<Path>) {
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> { pub fn run() -> Result<(), impl std::error::Error> {

View file

@ -1,5 +1,4 @@
mod app; mod app;
mod utils;
const SUPPORTED_TYPES: &[&str; 20] = &[ const SUPPORTED_TYPES: &[&str; 20] = &[
"*.png", "*.PNG", "*.jpg", "*.JPG", "*.jpeg", "*.JPEG", "*.avif", "*.jxl", "*.bmp", "*.exr", "*.png", "*.PNG", "*.jpg", "*.JPG", "*.jpeg", "*.JPEG", "*.avif", "*.jxl", "*.bmp", "*.exr",

View file

@ -1,27 +0,0 @@
use std::{ffi::OsStr, path::Path};
use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult};
pub fn load_thumbnail(path: impl AsRef<Path>, max_size: u32) -> ImageResult<DynamicImage> {
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)
}