zoom/pan, path option, heic WIP
This commit is contained in:
parent
f55424b98f
commit
ed66f17139
111
src/app.rs
111
src/app.rs
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
16
src/image.rs
16
src/image.rs
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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)>,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user