From ed66f17139d4e5b45f6d494a406cdd07df591401 Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Sun, 6 Apr 2025 14:07:27 +0200 Subject: [PATCH] zoom/pan, path option, heic WIP --- src/app.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++---- src/image.rs | 16 ++++--- src/main.rs | 9 ++-- src/shader.wgsl | 8 +++- src/store.rs | 2 +- 5 files changed, 125 insertions(+), 21 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4b41144..3b0754f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,6 +5,7 @@ use egui_wgpu::wgpu::SurfaceError; use egui_wgpu::{ScreenDescriptor, wgpu}; use imflow::store::ImageStore; use std::any::Any; +use std::path::PathBuf; use std::process::exit; use std::sync::Arc; use std::time; @@ -17,12 +18,39 @@ use winit::event_loop::ActiveEventLoop; use winit::platform::x11::WindowAttributesExtX11; use winit::window::{Window, WindowId}; +// Uniforms for transformations +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct Transforms { + transform: [f32; 16], // 4x4 matrix +} + +struct TransformData { + pan_x: f32, + pan_y: f32, + zoom: f32, +} + +fn create_transform_matrix(data: &TransformData) -> [f32; 16] { + const ZOOM_MULTIPLIER: f32 = 3.0; + let zoom = data.zoom.powf(ZOOM_MULTIPLIER); + [ + zoom, 0.0, 0.0, 0.0, 0.0, zoom, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, data.pan_x, data.pan_y, 0.0, + 1.0, + ] +} + fn setup_texture( device: &wgpu::Device, surface_config: SurfaceConfiguration, width: u32, height: u32, -) -> (wgpu::Texture, wgpu::BindGroup, wgpu::RenderPipeline) { +) -> ( + wgpu::Texture, + wgpu::BindGroup, + wgpu::RenderPipeline, + wgpu::Buffer, +) { // Create your texture (one-time setup) let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Image texture"), @@ -71,9 +99,26 @@ fn setup_texture( ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ], }); + let transform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Transform Uniform Buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + // Create bind group with your texture let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Texture Bind Group"), @@ -87,6 +132,10 @@ fn setup_texture( binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: transform_buffer.as_entire_binding(), + }, ], }); // Define vertex buffer layout @@ -151,7 +200,7 @@ fn setup_texture( cache: None, }); - (texture, bind_group, render_pipeline) + (texture, bind_group, render_pipeline, transform_buffer) } pub struct AppState { @@ -165,6 +214,8 @@ pub struct AppState { pub image_texture: wgpu::Texture, pub bind_group: wgpu::BindGroup, pub render_pipeline: wgpu::RenderPipeline, + pub transform_buffer: wgpu::Buffer, + pub transform_data: TransformData, } impl AppState { @@ -174,6 +225,7 @@ impl AppState { window: &Window, width: u32, height: u32, + path: PathBuf ) -> Self { let power_pref = wgpu::PowerPreference::default(); let adapter = instance @@ -224,11 +276,17 @@ impl AppState { let scale_factor = 1.0; - let store = ImageStore::new("./test_images".into()); + let store = ImageStore::new(path); - let (image_texture, bind_group, render_pipeline) = + let (image_texture, bind_group, render_pipeline, transform_buffer) = setup_texture(&device, surface_config.clone(), 6000, 4000); + let transform_data = TransformData { + pan_x: 0.0, + pan_y: 0.0, + zoom: 1.0, + }; + Self { device, queue, @@ -240,6 +298,8 @@ impl AppState { image_texture, bind_group, render_pipeline, + transform_buffer, + transform_data, } } @@ -254,15 +314,17 @@ pub struct App { instance: wgpu::Instance, state: Option, window: Option>, + path: PathBuf } impl App { - pub fn new() -> Self { + pub fn new(path: PathBuf) -> Self { let instance = egui_wgpu::wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); Self { instance, state: None, window: None, + path } } @@ -284,12 +346,14 @@ impl App { &window, initial_width, initial_width, + self.path.clone() ) .await; self.window.get_or_insert(window); self.state.get_or_insert(state); + self.pan_zoom(0.0, 0.0, 0.0); self.update_texture(); } @@ -340,6 +404,19 @@ impl App { ); } + pub fn pan_zoom(&mut self, zoom_delta: f32, pan_x: f32, pan_y: f32) { + let state = self.state.as_mut().unwrap(); + state.transform_data.zoom = (state.transform_data.zoom + zoom_delta).clamp(1.0, 20.0); + state.transform_data.pan_x += pan_x; + state.transform_data.pan_y += pan_y; + let transform = create_transform_matrix(&state.transform_data); + state.queue.write_buffer( + &state.transform_buffer, + 0, + bytemuck::cast_slice(&[Transforms { transform }]), + ); + } + fn handle_redraw(&mut self) { // Attempt to handle minimizing window if let Some(window) = self.window.as_ref() { @@ -489,6 +566,8 @@ impl App { } let rating = state.store.get_current_rating(); + let path = state.store.current_image_path.clone(); + let filename = path.file_name().unwrap(); let window = self.window.as_ref().unwrap(); { state.egui_renderer.begin_frame(window); @@ -504,6 +583,11 @@ impl App { .size(42.0) .strong(), ); + ui.label( + egui::RichText::new(format!("{}", filename.to_str().unwrap())) + .size(10.0) + .strong(), + ); // ui.add_space(10.0); }); }); @@ -550,15 +634,13 @@ impl ApplicationHandler for App { self.handle_redraw(); // println!("Updated in: {}ms", start.elapsed().as_millis()); // Extract the events by cloning them from the input context - let events = self + let (events, keys_down) = self .state .as_ref() .unwrap() .egui_renderer .context() - .input(|i| { - i.events.clone() // Clone the events to own them outside the closure - }); + .input(|i| (i.events.clone(), i.keys_down.clone())); // Now use the extracted events outside the closure events.iter().for_each(|e| { @@ -595,6 +677,17 @@ impl ApplicationHandler for App { Key::Escape => exit(0), _ => {} } + } else if let Event::MouseWheel { + unit, + delta, + modifiers, + } = e + { + self.pan_zoom(delta.y * 0.2, 0.0, 0.0); + } else if let Event::PointerMoved(pos) = e { + if keys_down.contains(&Key::Tab) { + self.pan_zoom(0.0, pos.x * 0.00001, pos.y * 0.00001); + } } }); diff --git a/src/image.rs b/src/image.rs index d3bccd9..c174e35 100644 --- a/src/image.rs +++ b/src/image.rs @@ -18,7 +18,7 @@ pub struct ImflowImageBuffer { pub width: usize, pub height: usize, pub argb_buffer: Vec, - pub rating: i32 + pub rating: i32, } pub fn create_iced_handle(width: u32, height: u32, rgba: Vec) -> Handle { @@ -81,7 +81,7 @@ pub fn load_image(path: PathBuf) -> ImflowImageBuffer { width, height, argb_buffer: buffer_u32, - rating + rating, } } @@ -103,7 +103,7 @@ pub fn load_available_images(dir: PathBuf) -> Vec { let mut files: Vec = fs::read_dir(dir) .unwrap() .map(|f| f.unwrap().path()) - .filter(|f| f.extension().unwrap().to_ascii_lowercase() == "jpg") + .filter(|f| ["jpg", "heic"].contains(&f.extension().unwrap().to_ascii_lowercase().to_str().unwrap())) .collect(); files.sort(); files @@ -113,8 +113,10 @@ pub fn get_embedded_thumbnail(path: PathBuf) -> Option> { let meta = rexiv2::Metadata::new_from_path(path); match meta { Ok(meta) => { - for preview in meta.get_preview_images().unwrap() { - return Some(preview.get_data().unwrap()); + if let Some(previews) = meta.get_preview_images() { + for preview in previews { + return Some(preview.get_data().unwrap()); + } } None } @@ -155,7 +157,7 @@ pub fn load_thumbnail_exif(path: &PathBuf) -> Option { width, height, argb_buffer: buffer, - rating + rating, }) } _ => None, @@ -180,6 +182,6 @@ pub fn load_thumbnail_full(path: &PathBuf) -> ImflowImageBuffer { width, height, argb_buffer: buffer, - rating + rating, } } diff --git a/src/main.rs b/src/main.rs index e885709..859a3e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -323,18 +323,21 @@ mod egui_tools; use winit::event_loop::{ControlFlow, EventLoop}; fn main() { + + let args = Args::parse(); + let path = args.path.unwrap_or("./test_images".into()); #[cfg(not(target_arch = "wasm32"))] { - pollster::block_on(run()); + pollster::block_on(run(path)); } } -async fn run() { +async fn run(path: PathBuf) { let event_loop = EventLoop::new().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); - let mut app = app::App::new(); + let mut app = app::App::new(path); event_loop.run_app(&mut app).expect("Failed to run app"); diff --git a/src/shader.wgsl b/src/shader.wgsl index fb80642..e557902 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -1,3 +1,8 @@ +struct Transforms { + transform: mat4x4, +}; +@group(0) @binding(2) var transforms: Transforms; + struct VertexInput { @location(0) position: vec3, @location(1) uv: vec2, @@ -11,7 +16,8 @@ struct VertexOutput { @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; - out.position = vec4(in.position, 1.0); + // Apply zoom/transformation matrix + out.position = transforms.transform * vec4(in.position, 1.0); out.uv = in.uv; return out; } diff --git a/src/store.rs b/src/store.rs index 15c691e..802e187 100644 --- a/src/store.rs +++ b/src/store.rs @@ -15,7 +15,7 @@ pub struct ImageStore { pub(crate) loaded_images: HashMap, pub(crate) loaded_images_thumbnails: HashMap, pub(crate) available_images: Vec, - pub(crate) current_image_path: PathBuf, + pub current_image_path: PathBuf, pub(crate) pool: ThreadPool, pub(crate) loader_rx: mpsc::Receiver<(PathBuf, ImflowImageBuffer)>, pub(crate) loader_tx: mpsc::Sender<(PathBuf, ImflowImageBuffer)>,