Added filters, scroll thumbnails

This commit is contained in:
Dawid Pietrykowski 2025-04-12 23:19:02 +02:00
parent b41554c608
commit b00bbbadc0
4 changed files with 122 additions and 56 deletions

View File

@ -1,11 +1,11 @@
use crate::egui_tools::EguiRenderer;
use egui::load::{ImageLoadResult, ImageLoader};
use egui::{Align2, Color32, ColorImage, Event, ImageSource, Key, PointerButton};
use egui::{Align2, Color32, ColorImage, Event, Image, ImageSource, Key, PointerButton, Sense};
use egui_wgpu::wgpu::SurfaceError;
use egui_wgpu::{ScreenDescriptor, wgpu};
use image::metadata::Orientation;
use imflow::image::swap_wh;
use imflow::store::ImageStore;
use imflow::image::{ImageFormat, swap_wh};
use imflow::store::{FileFilters, ImageStore};
use std::cmp::{max, min};
use std::collections::HashMap;
use std::path::PathBuf;
@ -223,6 +223,7 @@ pub struct AppState {
pub render_pipeline: wgpu::RenderPipeline,
pub transform_buffer: wgpu::Buffer,
pub transform_data: TransformData,
pub filters: FileFilters,
}
impl AppState {
@ -315,6 +316,7 @@ impl AppState {
render_pipeline,
transform_buffer,
transform_data,
filters: FileFilters::default(),
}
}
@ -629,18 +631,23 @@ impl App {
render_pass.draw_indexed(0..6, 0, 0..1);
}
let mut rating_filter = [false; 6];
// let mut file_filters;
let rating;
let path;
let current_id;
let image_count;
let filename;
let window;
let filtered_images;
{
let store = state.store.read().unwrap();
rating = store.get_current_rating();
path = store.current_image_path.clone();
current_id = store.current_image_id;
image_count = store.available_images.len();
filtered_images = store.get_filtered_images(&state.filters);
filename = path.path.file_name().unwrap();
window = self.window.as_ref().unwrap();
}
@ -665,59 +672,45 @@ impl App {
);
});
});
egui::Window::new("Id")
.collapsible(false)
.resizable(false)
.default_width(5.0)
.anchor(Align2::RIGHT_TOP, [-5.0, 5.0])
.pivot(Align2::RIGHT_TOP)
.show(state.egui_renderer.context(), |ui| {
ui.vertical_centered(|ui| {
ui.label(
egui::RichText::new(format!("{}/{}", current_id, image_count))
.size(22.0)
.strong(),
);
});
});
egui::Window::new("Images")
.collapsible(false)
.resizable(false)
.default_width(500.0)
.default_height(300.0)
.anchor(Align2::CENTER_BOTTOM, [0.0, 10.0])
.pivot(Align2::CENTER_BOTTOM)
.show(state.egui_renderer.context(), |ui| {
ui.horizontal(|ui| {
// ui.label(
// egui::RichText::new(format!("{}/{}", current_id, image_count))
// .size(22.0)
// .strong(),
// );
const NUM: i32 = 5;
for i in max((current_id as i32) - NUM, 0)
..min((current_id as i32) + NUM + 1, image_count as i32)
{
egui::TopBottomPanel::bottom("Thumbnails")
.exact_height(120.0)
.show(state.egui_renderer.context(), |panel_ui| {
egui::ScrollArea::horizontal().show(panel_ui, |ui| {
ui.horizontal_centered(|horizontal| {
for image in filtered_images {
let source = ImageSource::Bytes {
uri: std::borrow::Cow::Owned(i.to_string()),
uri: std::borrow::Cow::Owned(image.get_hash_str()),
bytes: egui::load::Bytes::Static(&[]),
};
ui.add(
let image_widget = horizontal.add(
egui::Image::new(source)
// .sca
// .load_for_size(ctx, available_size)
// .fit_to_fraction(Vec2::new(10.0, 10.0))
// .max_width(200.0)
.fit_to_original_size(1.0)
.corner_radius(10),
.fit_to_original_size(0.8)
.corner_radius(10)
.sense(Sense::click()),
);
if image_widget.clicked() {
image_widget.scroll_to_me(None);
// ui.scroll_to_rect(image_widget.rect, None);
println!("{}", image.get_hash_str());
}
}
// ui.image(source);
});
});
});
egui::SidePanel::right("Filters").show(state.egui_renderer.context(), |ui| {
for (i, mut rating) in state.filters.rating.iter_mut().enumerate().rev() {
ui.checkbox(&mut rating, format!("{} stars", i));
}
ui.text_edit_singleline(&mut state.filters.name);
for (format, mut value) in state.filters.file_format.iter_mut() {
ui.checkbox(&mut value, format!("{}", format));
}
});
state.egui_renderer.end_frame_and_draw(
&state.device,
@ -845,7 +838,7 @@ impl ApplicationHandler for App {
pub struct ImflowEguiLoader {
store: Arc<RwLock<ImageStore>>,
// stored: Option<ImageLoadResult>,
cache: egui::mutex::Mutex<HashMap<usize, ImageLoadResult>>,
cache: egui::mutex::Mutex<HashMap<String, ImageLoadResult>>,
}
impl ImflowEguiLoader {
@ -870,13 +863,14 @@ impl ImageLoader for ImflowEguiLoader {
) -> egui::load::ImageLoadResult {
let mut cache = self.cache.lock();
let id = uri.parse::<usize>().unwrap();
// let id = uri.parse::<usize>().unwrap();
let id = uri.to_string();
if let Some(handle) = cache.get(&id) {
handle.clone()
} else {
let imbuf = {
let binding = self.store.read().unwrap();
binding.get_thumbnail_id(id).clone()
binding.get_thumbnail_hash(id.clone()).clone()
};
let mut image = ColorImage::new([imbuf.width, imbuf.height], Color32::BLACK);
let image_buffer = image.as_raw_mut();

View File

@ -1,4 +1,4 @@
use egui::Context;
use egui::{Color32, Context, Visuals};
use egui_wgpu::wgpu::{CommandEncoder, Device, Queue, StoreOp, TextureFormat, TextureView};
use egui_wgpu::{Renderer, ScreenDescriptor, wgpu};
use egui_winit::State;
@ -24,6 +24,14 @@ impl EguiRenderer {
window: &Window,
) -> EguiRenderer {
let egui_context = Context::default();
egui_context.options_mut(|o| o.line_scroll_speed = 200.0);
egui_context.set_visuals_of(
egui::Theme::Dark,
Visuals {
panel_fill: Color32::BLACK,
..Default::default()
},
);
let egui_state = egui_winit::State::new(
egui_context,

View File

@ -18,6 +18,8 @@ use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace;
use zune_image::codecs::qoi::zune_core::options::DecoderOptions;
use std::env;
use std::fmt::Display;
// use std::fmt::Write;
use std::fs;
use std::fs::File;
use std::fs::read;
@ -37,6 +39,16 @@ pub enum ImageFormat {
Heif,
}
impl Display for ImageFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageFormat::Jpg => f.write_str("JPG"),
ImageFormat::Jxl => f.write_str("JXL"),
ImageFormat::Heif => f.write_str("HEIF"),
}
}
}
#[derive(Clone)]
pub struct ImageData {
pub path: PathBuf,
@ -44,6 +56,7 @@ pub struct ImageData {
pub embedded_thumbnail: bool,
pub orientation: Orientation,
pub hash: GenericArray<u8, U32>,
pub rating: i32,
}
impl ImageData {
@ -56,6 +69,10 @@ impl ImageData {
let hash_hex = format!("{:x}", self.hash);
return cache_dir.join(hash_hex).to_path_buf();
}
pub fn get_hash_str(&self) -> String {
format!("{:x}", self.hash)
}
}
impl Hash for ImageData {
@ -253,12 +270,14 @@ pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
let orientation = Orientation::from_exif(meta.get_orientation() as u8)
.unwrap_or(Orientation::NoTransforms);
let hash = get_file_hash(&path);
let rating = meta.get_tag_numeric("Xmp.xmp.Rating");
Some(ImageData {
path,
format,
embedded_thumbnail,
orientation,
hash,
rating,
})
} else {
None

View File

@ -1,4 +1,4 @@
use crate::image::{ImageData, load_thumbnail};
use crate::image::{ImageData, ImageFormat, load_thumbnail};
use crate::image::{ImflowImageBuffer, load_available_images, load_image};
use crossbeam_channel::{Receiver, Sender, unbounded};
use rayon::prelude::*;
@ -11,6 +11,26 @@ use threadpool::ThreadPool;
const PRELOAD_NEXT_IMAGE_N: usize = 16;
pub struct FileFilters {
pub rating: [bool; 6],
pub name: String,
pub file_format: HashMap<ImageFormat, bool>,
}
impl Default for FileFilters {
fn default() -> Self {
let mut formats = HashMap::new();
formats.insert(ImageFormat::Jpg, true);
formats.insert(ImageFormat::Jxl, true);
formats.insert(ImageFormat::Heif, true);
FileFilters {
rating: [true; 6],
name: "".to_string(),
file_format: formats,
}
}
}
pub struct ImageStore {
pub current_image_id: usize,
pub(crate) loaded_images: HashMap<ImageData, ImflowImageBuffer>,
@ -176,6 +196,14 @@ impl ImageStore {
self.loaded_images_thumbnails.get(path).unwrap()
}
pub fn get_thumbnail_hash(&self, hash: String) -> &ImflowImageBuffer {
self.loaded_images_thumbnails
.iter()
.find(|f| f.0.get_hash_str() == hash)
.unwrap()
.1
}
pub fn get_thumbnail(&mut self) -> &ImflowImageBuffer {
if self
.loaded_images_thumbnails
@ -196,4 +224,21 @@ impl ImageStore {
.get(&self.current_image_path)
.unwrap();
}
pub fn get_filtered_images(&self, filter: &FileFilters) -> Vec<ImageData> {
self.available_images
.iter()
.filter(|f| filter.rating[f.rating.clamp(0, 5) as usize])
.filter(|f| filter.file_format[&f.format])
.filter(|f| {
f.path
.file_name()
.unwrap()
.to_str()
.unwrap()
.contains(&filter.name)
})
.map(|f| f.clone())
.collect::<Vec<ImageData>>()
}
}