zoom/pan, path option, heic WIP

This commit is contained in:
Dawid Pietrykowski 2025-04-06 14:07:27 +02:00
parent f55424b98f
commit ed66f17139
5 changed files with 125 additions and 21 deletions

View File

@ -5,6 +5,7 @@ use egui_wgpu::wgpu::SurfaceError;
use egui_wgpu::{ScreenDescriptor, wgpu}; use egui_wgpu::{ScreenDescriptor, wgpu};
use imflow::store::ImageStore; use imflow::store::ImageStore;
use std::any::Any; use std::any::Any;
use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::sync::Arc; use std::sync::Arc;
use std::time; use std::time;
@ -17,12 +18,39 @@ use winit::event_loop::ActiveEventLoop;
use winit::platform::x11::WindowAttributesExtX11; use winit::platform::x11::WindowAttributesExtX11;
use winit::window::{Window, WindowId}; 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( fn setup_texture(
device: &wgpu::Device, device: &wgpu::Device,
surface_config: SurfaceConfiguration, surface_config: SurfaceConfiguration,
width: u32, width: u32,
height: u32, height: u32,
) -> (wgpu::Texture, wgpu::BindGroup, wgpu::RenderPipeline) { ) -> (
wgpu::Texture,
wgpu::BindGroup,
wgpu::RenderPipeline,
wgpu::Buffer,
) {
// Create your texture (one-time setup) // Create your texture (one-time setup)
let texture = device.create_texture(&wgpu::TextureDescriptor { let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Image texture"), label: Some("Image texture"),
@ -71,9 +99,26 @@ fn setup_texture(
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None, 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::<Transforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Create bind group with your texture // Create bind group with your texture
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Texture Bind Group"), label: Some("Texture Bind Group"),
@ -87,6 +132,10 @@ fn setup_texture(
binding: 1, binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler), resource: wgpu::BindingResource::Sampler(&sampler),
}, },
wgpu::BindGroupEntry {
binding: 2,
resource: transform_buffer.as_entire_binding(),
},
], ],
}); });
// Define vertex buffer layout // Define vertex buffer layout
@ -151,7 +200,7 @@ fn setup_texture(
cache: None, cache: None,
}); });
(texture, bind_group, render_pipeline) (texture, bind_group, render_pipeline, transform_buffer)
} }
pub struct AppState { pub struct AppState {
@ -165,6 +214,8 @@ pub struct AppState {
pub image_texture: wgpu::Texture, pub image_texture: wgpu::Texture,
pub bind_group: wgpu::BindGroup, pub bind_group: wgpu::BindGroup,
pub render_pipeline: wgpu::RenderPipeline, pub render_pipeline: wgpu::RenderPipeline,
pub transform_buffer: wgpu::Buffer,
pub transform_data: TransformData,
} }
impl AppState { impl AppState {
@ -174,6 +225,7 @@ impl AppState {
window: &Window, window: &Window,
width: u32, width: u32,
height: u32, height: u32,
path: PathBuf
) -> Self { ) -> Self {
let power_pref = wgpu::PowerPreference::default(); let power_pref = wgpu::PowerPreference::default();
let adapter = instance let adapter = instance
@ -224,11 +276,17 @@ impl AppState {
let scale_factor = 1.0; 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); setup_texture(&device, surface_config.clone(), 6000, 4000);
let transform_data = TransformData {
pan_x: 0.0,
pan_y: 0.0,
zoom: 1.0,
};
Self { Self {
device, device,
queue, queue,
@ -240,6 +298,8 @@ impl AppState {
image_texture, image_texture,
bind_group, bind_group,
render_pipeline, render_pipeline,
transform_buffer,
transform_data,
} }
} }
@ -254,15 +314,17 @@ pub struct App {
instance: wgpu::Instance, instance: wgpu::Instance,
state: Option<AppState>, state: Option<AppState>,
window: Option<Arc<Window>>, window: Option<Arc<Window>>,
path: PathBuf
} }
impl App { impl App {
pub fn new() -> Self { pub fn new(path: PathBuf) -> Self {
let instance = egui_wgpu::wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); let instance = egui_wgpu::wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
Self { Self {
instance, instance,
state: None, state: None,
window: None, window: None,
path
} }
} }
@ -284,12 +346,14 @@ impl App {
&window, &window,
initial_width, initial_width,
initial_width, initial_width,
self.path.clone()
) )
.await; .await;
self.window.get_or_insert(window); self.window.get_or_insert(window);
self.state.get_or_insert(state); self.state.get_or_insert(state);
self.pan_zoom(0.0, 0.0, 0.0);
self.update_texture(); 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) { fn handle_redraw(&mut self) {
// Attempt to handle minimizing window // Attempt to handle minimizing window
if let Some(window) = self.window.as_ref() { if let Some(window) = self.window.as_ref() {
@ -489,6 +566,8 @@ impl App {
} }
let rating = state.store.get_current_rating(); 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(); let window = self.window.as_ref().unwrap();
{ {
state.egui_renderer.begin_frame(window); state.egui_renderer.begin_frame(window);
@ -504,6 +583,11 @@ impl App {
.size(42.0) .size(42.0)
.strong(), .strong(),
); );
ui.label(
egui::RichText::new(format!("{}", filename.to_str().unwrap()))
.size(10.0)
.strong(),
);
// ui.add_space(10.0); // ui.add_space(10.0);
}); });
}); });
@ -550,15 +634,13 @@ impl ApplicationHandler for App {
self.handle_redraw(); self.handle_redraw();
// println!("Updated in: {}ms", start.elapsed().as_millis()); // println!("Updated in: {}ms", start.elapsed().as_millis());
// Extract the events by cloning them from the input context // Extract the events by cloning them from the input context
let events = self let (events, keys_down) = self
.state .state
.as_ref() .as_ref()
.unwrap() .unwrap()
.egui_renderer .egui_renderer
.context() .context()
.input(|i| { .input(|i| (i.events.clone(), i.keys_down.clone()));
i.events.clone() // Clone the events to own them outside the closure
});
// Now use the extracted events outside the closure // Now use the extracted events outside the closure
events.iter().for_each(|e| { events.iter().for_each(|e| {
@ -595,6 +677,17 @@ impl ApplicationHandler for App {
Key::Escape => exit(0), 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);
}
} }
}); });

View File

@ -18,7 +18,7 @@ pub struct ImflowImageBuffer {
pub width: usize, pub width: usize,
pub height: usize, pub height: usize,
pub argb_buffer: Vec<u32>, pub argb_buffer: Vec<u32>,
pub rating: i32 pub rating: i32,
} }
pub fn create_iced_handle(width: u32, height: u32, rgba: Vec<u8>) -> Handle { pub fn create_iced_handle(width: u32, height: u32, rgba: Vec<u8>) -> Handle {
@ -81,7 +81,7 @@ pub fn load_image(path: PathBuf) -> ImflowImageBuffer {
width, width,
height, height,
argb_buffer: buffer_u32, argb_buffer: buffer_u32,
rating rating,
} }
} }
@ -103,7 +103,7 @@ pub fn load_available_images(dir: PathBuf) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = fs::read_dir(dir) let mut files: Vec<PathBuf> = fs::read_dir(dir)
.unwrap() .unwrap()
.map(|f| f.unwrap().path()) .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(); .collect();
files.sort(); files.sort();
files files
@ -113,8 +113,10 @@ pub fn get_embedded_thumbnail(path: PathBuf) -> Option<Vec<u8>> {
let meta = rexiv2::Metadata::new_from_path(path); let meta = rexiv2::Metadata::new_from_path(path);
match meta { match meta {
Ok(meta) => { Ok(meta) => {
for preview in meta.get_preview_images().unwrap() { if let Some(previews) = meta.get_preview_images() {
return Some(preview.get_data().unwrap()); for preview in previews {
return Some(preview.get_data().unwrap());
}
} }
None None
} }
@ -155,7 +157,7 @@ pub fn load_thumbnail_exif(path: &PathBuf) -> Option<ImflowImageBuffer> {
width, width,
height, height,
argb_buffer: buffer, argb_buffer: buffer,
rating rating,
}) })
} }
_ => None, _ => None,
@ -180,6 +182,6 @@ pub fn load_thumbnail_full(path: &PathBuf) -> ImflowImageBuffer {
width, width,
height, height,
argb_buffer: buffer, argb_buffer: buffer,
rating rating,
} }
} }

View File

@ -323,18 +323,21 @@ mod egui_tools;
use winit::event_loop::{ControlFlow, EventLoop}; use winit::event_loop::{ControlFlow, EventLoop};
fn main() { fn main() {
let args = Args::parse();
let path = args.path.unwrap_or("./test_images".into());
#[cfg(not(target_arch = "wasm32"))] #[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(); let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll); 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"); event_loop.run_app(&mut app).expect("Failed to run app");

View File

@ -1,3 +1,8 @@
struct Transforms {
transform: mat4x4<f32>,
};
@group(0) @binding(2) var<uniform> transforms: Transforms;
struct VertexInput { struct VertexInput {
@location(0) position: vec3<f32>, @location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>, @location(1) uv: vec2<f32>,
@ -11,7 +16,8 @@ struct VertexOutput {
@vertex @vertex
fn vs_main(in: VertexInput) -> VertexOutput { fn vs_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
out.position = vec4<f32>(in.position, 1.0); // Apply zoom/transformation matrix
out.position = transforms.transform * vec4<f32>(in.position, 1.0);
out.uv = in.uv; out.uv = in.uv;
return out; return out;
} }

View File

@ -15,7 +15,7 @@ pub struct ImageStore {
pub(crate) loaded_images: HashMap<PathBuf, ImflowImageBuffer>, pub(crate) loaded_images: HashMap<PathBuf, ImflowImageBuffer>,
pub(crate) loaded_images_thumbnails: HashMap<PathBuf, ImflowImageBuffer>, pub(crate) loaded_images_thumbnails: HashMap<PathBuf, ImflowImageBuffer>,
pub(crate) available_images: Vec<PathBuf>, pub(crate) available_images: Vec<PathBuf>,
pub(crate) current_image_path: PathBuf, pub current_image_path: PathBuf,
pub(crate) pool: ThreadPool, pub(crate) pool: ThreadPool,
pub(crate) loader_rx: mpsc::Receiver<(PathBuf, ImflowImageBuffer)>, pub(crate) loader_rx: mpsc::Receiver<(PathBuf, ImflowImageBuffer)>,
pub(crate) loader_tx: mpsc::Sender<(PathBuf, ImflowImageBuffer)>, pub(crate) loader_tx: mpsc::Sender<(PathBuf, ImflowImageBuffer)>,