From 63b0f17f297574343afbc2f95bfa0ddf5c44fc69 Mon Sep 17 00:00:00 2001 From: Ian Elsbree Date: Sat, 6 Jan 2024 00:38:13 -0800 Subject: [PATCH] Images can now be viewed. There's a BIG crash when non-image files are drag-dropped. --- src/image.rs | 78 ++++++++++++++++++++++ src/main.rs | 182 +++++++++++++++++++++++++++++---------------------- 2 files changed, 182 insertions(+), 78 deletions(-) create mode 100644 src/image.rs diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 0000000..6fab0f4 --- /dev/null +++ b/src/image.rs @@ -0,0 +1,78 @@ +use crate::path_buf_to_filename_string; +use eframe::egui; +use eframe::egui::{ColorImage, TextureHandle, TextureOptions}; +use image as image_lib; +use image_lib::io::Reader as ImageReader; +use image_lib::DynamicImage; +use std::fmt::{Debug, Formatter}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// An image as defined by this tool, containing image data and other metadata +#[derive(Default, Clone, PartialEq)] +pub struct Image { + id: usize, + name: String, + path: PathBuf, + data: DynamicImage, + texture: Option, +} + +impl Debug for Image { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let has_texture = &self.texture.is_some(); + f.debug_struct("Image") + .field("id", &self.id) + .field("path", &self.path) + .field("data", &self.data) + .field("texture", has_texture) + .field("name", &self.name) + .finish() + } +} + +#[allow(dead_code)] +impl Image { + pub fn new(filename: PathBuf) -> Self { + static ID_COUNTER: AtomicUsize = AtomicUsize::new(1); + let id = ID_COUNTER.fetch_add(1, Ordering::Relaxed); + let data = ImageReader::open(&filename) + .expect("could not open image") + .decode() + .expect("could not decode image"); + let name = path_buf_to_filename_string(&filename); + + Self { + id, + name, + path: filename, + data, + texture: None, + } + } + + pub fn load_texture(&mut self, ctx: &egui::Context) { + let image_buffer = self.data.to_rgba8(); + let size = (self.data.width() as usize, self.data.height() as usize); + let pixels = image_buffer.into_vec(); + assert_eq!(size.0 * size.1 * 4, pixels.len()); + let pixels = ColorImage::from_rgba_unmultiplied([size.0, size.1], &pixels); + self.texture = Some(ctx.load_texture(&self.name, pixels, TextureOptions::default())); + } + + pub fn id(&self) -> usize { + self.id + } + pub fn name(&self) -> &str { + &self.name + } + pub fn path(&self) -> &PathBuf { + &self.path + } + pub fn data(&self) -> &DynamicImage { + &self.data + } + pub fn texture(&self) -> &Option { + &self.texture + } +} diff --git a/src/main.rs b/src/main.rs index 81ef7bb..cc7a1d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,10 @@ use std::path::PathBuf; use eframe::egui; use eframe::egui::{menu, CollapsingHeader, ScrollArea}; +use image::Image; + +mod image; + fn main() -> Result<(), eframe::Error> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). let options = eframe::NativeOptions { @@ -24,29 +28,28 @@ fn main() -> Result<(), eframe::Error> { ) } -#[derive(Default, Debug, Clone, PartialEq)] -struct Image { - path: PathBuf, - data: Vec, -} - -impl Image { - pub fn new(filename: PathBuf) -> Self { - Self { - path: filename, - data: Vec::new(), - } - } - - fn import_from_path(&mut self) {} -} - #[derive(Default)] struct MyApp { image_sequence: Vec, selected_image: Option, } +impl MyApp { + fn import_images(&mut self) { + if let Some(paths) = rfd::FileDialog::new() + .add_filter("PNG", &["png"]) + .pick_files() + { + for path in paths { + self.image_sequence.push(Image::new(path)); + } + } + } + fn no_images_loaded(&self) -> bool { + self.image_sequence.len() == 0 + } +} + /*impl Default for MyApp { fn default() -> Self { Self { @@ -59,21 +62,12 @@ struct MyApp { impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // system menu egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { menu::bar(ui, |ui| { ui.menu_button("File", |ui| { if ui.button("Import Images").clicked() { - if let Some(paths) = rfd::FileDialog::new() - .add_filter("PNG", &["png"]) - .pick_files() - { - for path in paths { - self.image_sequence.push(Image::new(path)); - } - for image in &mut self.image_sequence { - image.import_from_path(); - } - } + self.import_images(); ui.close_menu(); } if ui.button("Unload All Images").clicked() { @@ -92,64 +86,96 @@ impl eframe::App for MyApp { }) }); }); + + // main window egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("Speedy Spritesheets"); - - ui.label(format!( - "Number of images imported: {}", - self.image_sequence.len() - )); - - // if self.image_sequence.len() > 0 { - ui.heading("Imported Images"); - ui.horizontal(|ui| { - ui.label("Show data for image:"); - egui::ComboBox::from_label("") - .selected_text(if let Some(selected_image) = &self.selected_image { - path_buf_to_string(&selected_image.path) - } else if self.image_sequence.len() == 0 { - "No imported images".to_string() - } else { - "Select image".to_string() - }) - .show_ui(ui, |ui| { - ui.style_mut().wrap = Some(false); - for image in &self.image_sequence { - ui.selectable_value( - &mut self.selected_image, - Some(image.clone()), - path_buf_to_string(&image.path), - ); + ScrollArea::vertical().auto_shrink(false).show(ui, |ui| { + ui.heading("Speedy Spritesheets"); + + ui.label(format!( + "Number of images imported: {}", + self.image_sequence.len() + )); + + ui.heading("Frames"); + if self.no_images_loaded() { + ui.horizontal(|ui| { + ui.label("No images loaded"); + if ui.button("Import images").clicked() { + self.import_images(); } - ui.style_mut().wrap = Some(true); }); - }); - ScrollArea::vertical().auto_shrink(true).show(ui, |ui| { - // ui.monospace(format!("{:?}", self.selected_image)); - if let Some(image) = &self.selected_image { - ui.add(egui::Image::from_bytes("bytes://image", image.data.clone())); } - }); - CollapsingHeader::new("Imported Image Paths").show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Show data for image:"); + egui::ComboBox::from_label("") + .selected_text(if let Some(selected_image) = &self.selected_image { + &selected_image.name() + } else if self.no_images_loaded() { + "No imported images" + } else { + "Select image" + }) + .show_ui(ui, |ui| { + ui.style_mut().wrap = Some(false); + for image in &self.image_sequence { + ui.selectable_value( + &mut self.selected_image, + Some(image.clone()), + image.name(), + ); + } + ui.style_mut().wrap = Some(true); + }); + }); ScrollArea::vertical().auto_shrink(true).show(ui, |ui| { - for image in &self.image_sequence { - ui.monospace(format!("{}", image.path.display().to_string())); + // ui.monospace(format!("{:?}", self.selected_image)); + if let Some(image) = &mut self.selected_image { + // ui.add(egui::Image::from_bytes("bytes://image", image.data.clone())); + if image.texture().is_none() { + image.load_texture(&ctx); + } + if let Some(texture) = &image.texture() { + ui.add(egui::Image::new(texture).max_size([100.0, 100.0].into())); + } else { + ui.label("Image is loaded but does not have a texture."); + } } }); - }); - // } - - // file drag-and-drop - preview_files_being_dropped(ctx); - ctx.input(|i| { - if !i.raw.dropped_files.is_empty() { - let dropped_files = i.raw.dropped_files.clone(); - for file in dropped_files { - if let Some(path) = file.path { - self.image_sequence.push(Image::new(path)); + CollapsingHeader::new("Loaded Images").show(ui, |ui| { + ScrollArea::vertical().auto_shrink(true).show(ui, |ui| { + for image in &mut self.image_sequence { + if image.texture().is_none() { + image.load_texture(&ctx); + } + if let Some(texture) = &image.texture() { + ui.add(egui::Image::new(texture).max_size([100.0, 100.0].into())); + } else { + ui.label("Image is loaded but does not have a texture."); + } + } + }); + }); + CollapsingHeader::new("Imported Image Paths").show(ui, |ui| { + ScrollArea::vertical().auto_shrink(true).show(ui, |ui| { + for image in &self.image_sequence { + ui.monospace(format!("{}", image.path().display().to_string())); + } + }); + }); + + // file drag-and-drop + preview_files_being_dropped(ctx); + ctx.input(|i| { + if !i.raw.dropped_files.is_empty() { + let dropped_files = i.raw.dropped_files.clone(); + for file in dropped_files { + if let Some(path) = file.path { + self.image_sequence.push(Image::new(path)); + } } } - } + }); }); }); } @@ -189,7 +215,7 @@ fn preview_files_being_dropped(ctx: &egui::Context) { } } -fn path_buf_to_string(path_buf: &PathBuf) -> String { +fn path_buf_to_filename_string(path_buf: &PathBuf) -> String { path_buf .file_name() .expect("selected file terminated in `..` or was empty")