Update EGUI

This commit is contained in:
Piotr Siuszko 2025-09-15 11:41:19 +02:00
parent 9bd2a2bd5b
commit 8db4d5b2bc
7 changed files with 104 additions and 71 deletions

View File

@ -2,7 +2,7 @@
name = "bevy_rpack" name = "bevy_rpack"
description = "Bevy plugin with rpack atlas support" description = "Bevy plugin with rpack atlas support"
version = "0.2.0" version = "0.2.0"
edition = "2021" edition = "2024"
repository = "https://github.com/Leinnan/rpack.git" repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack" homepage = "https://github.com/Leinnan/rpack"
authors = ["Piotr Siuszko <siuszko@zoho.com>"] authors = ["Piotr Siuszko <siuszko@zoho.com>"]
@ -21,7 +21,7 @@ bevy = { version = "0.16", optional = true, default-features = false, features =
"bevy_image", "bevy_image",
"bevy_ui" "bevy_ui"
] } ] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
thiserror = "2" thiserror = "2"

View File

@ -6,7 +6,7 @@ repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack" homepage = "https://github.com/Leinnan/rpack"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
version = "0.2.0" version = "0.2.0"
edition = "2021" edition = "2024"
[features] [features]
default = ["cli", "dds"] default = ["cli", "dds"]

View File

@ -1,24 +1,23 @@
[package] [package]
name = "rpack_egui" name = "rpack_egui"
version = "0.2.0" version = "0.3.0"
description = "GUI application for generating rpack atlases" description = "GUI application for generating rpack atlases"
authors = ["Piotr Siuszko <siuszko@zoho.com>"] authors = ["Piotr Siuszko <siuszko@zoho.com>"]
edition = "2021" edition = "2024"
rust-version = "1.81"
repository = "https://github.com/Leinnan/rpack.git" repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack" homepage = "https://github.com/Leinnan/rpack"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
[dependencies] [dependencies]
egui = "0.30" egui = "0.32"
eframe = { version = "0.30", default-features = false, features = [ eframe = { version = "0.32", default-features = false, features = [
"accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies. "accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies.
"default_fonts", # Embed the default egui fonts. "default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu". "glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app. "persistence", # Enable restoring app state when restarting the app.
] } ] }
log = "0.4" log = "0.4"
egui_json_tree = "0.10" egui_json_tree = "0.13"
rpack_cli = { default-features = false, path = "../rpack_cli", version = "0.2" } rpack_cli = { default-features = false, path = "../rpack_cli", version = "0.2" }
# You only need serde if you want app persistence: # You only need serde if you want app persistence:
@ -26,7 +25,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
texture_packer = { workspace = true } texture_packer = { workspace = true }
image = { workspace = true } image = { workspace = true }
egui_extras = { version = "0.30", features = ["all_loaders"] } egui_extras = { version = "0.32", features = ["all_loaders"] }
rfd = { version = "0.15", features = [] } rfd = { version = "0.15", features = [] }
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
anyhow = "1" anyhow = "1"

View File

@ -1,10 +0,0 @@
# If you see this, run "rustup self update" to get rustup 1.23 or newer.
# NOTE: above comment is for older `rustup` (before TOML support was added),
# which will treat the first line as the toolchain name, and therefore show it
# to the user in the error, instead of "error: invalid channel name '[toolchain]'".
[toolchain]
channel = "1.81"
components = [ "rustfmt", "clippy" ]
targets = [ "wasm32-unknown-unknown" ]

View File

@ -1,7 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use egui::{CollapsingHeader, Color32, DroppedFile, FontFamily, FontId, Image, RichText}; use egui::{
use image::GenericImageView; CollapsingHeader, Color32, DroppedFile, FontFamily, FontId, Grid, Image, Label, RichText,
};
use rpack_cli::{ImageFile, Spritesheet, SpritesheetError}; use rpack_cli::{ImageFile, Spritesheet, SpritesheetError};
use texture_packer::TexturePackerConfig; use texture_packer::TexturePackerConfig;
@ -33,8 +34,22 @@ pub struct Application {
#[serde(skip)] #[serde(skip)]
max_size: u32, max_size: u32,
#[serde(skip)] #[serde(skip)]
image_data: HashMap<String, ImageFile>, image_data: HashMap<String, AppImageData>,
} }
pub struct AppImageData {
pub width: u32,
pub height: u32,
pub data: ImageFile,
pub path: String,
}
impl AppImageData {
pub fn id(&self) -> &str {
self.data.id.as_str()
}
}
impl Default for Application { impl Default for Application {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -71,7 +86,7 @@ impl Application {
self.image_data = self self.image_data = self
.dropped_files .dropped_files
.iter() .iter()
.flat_map(|f| f.create_image(&prefix)) .flat_map(|f| f.create_image(&prefix).map(|i| (i.id().to_string(), i)))
.collect(); .collect();
self.update_min_size(); self.update_min_size();
} }
@ -79,18 +94,18 @@ impl Application {
if let Some(file) = self if let Some(file) = self
.image_data .image_data
.values() .values()
.max_by(|a, b| a.image.width().cmp(&b.image.width())) .max_by(|a, b| a.width.cmp(&b.width))
{ {
self.min_size[0] = file.image.width(); self.min_size[0] = file.width;
} else { } else {
self.min_size[0] = 32; self.min_size[0] = 32;
} }
if let Some(file) = self if let Some(file) = self
.image_data .image_data
.values() .values()
.max_by(|a, b| a.image.height().cmp(&b.image.height())) .max_by(|a, b| a.height.cmp(&b.height))
{ {
self.min_size[1] = file.image.height(); self.min_size[1] = file.height;
} else { } else {
self.min_size[1] = 32; self.min_size[1] = 32;
} }
@ -114,7 +129,11 @@ impl Application {
fn build_atlas(&mut self, ctx: &egui::Context) { fn build_atlas(&mut self, ctx: &egui::Context) {
self.data = None; self.data = None;
self.image = None; self.image = None;
let images: Vec<ImageFile> = self.image_data.values().cloned().collect(); let images: Vec<ImageFile> = self
.image_data
.values()
.map(|file| file.data.clone())
.collect();
for size in [32, 64, 128, 256, 512, 1024, 2048, 4096] { for size in [32, 64, 128, 256, 512, 1024, 2048, 4096] {
if size < self.min_size[0] || size < self.min_size[1] { if size < self.min_size[0] || size < self.min_size[1] {
@ -328,25 +347,41 @@ impl eframe::App for Application {
CollapsingHeader::new("Image list") CollapsingHeader::new("Image list")
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
if !self.image_data.is_empty() && ui.button("clear list").clicked() { ui.horizontal(|ui|{
self.image_data.clear();
self.dropped_files.clear(); if !self.image_data.is_empty() && ui.button("clear list").clicked() {
self.data = None; self.image_data.clear();
self.update_min_size(); self.dropped_files.clear();
} self.data = None;
let mut to_remove: Option<String> = None; self.update_min_size();
for (id, file) in self.image_data.iter() { }
ui.horizontal_top(|ui| { ui.add_space(10.0);
ui.add_space(10.0); #[cfg(not(target_arch = "wasm32"))]
if ui.button("x").clicked() { if ui.button("Add").clicked() {
to_remove = Some(id.clone()); if let Some(files) = rfd::FileDialog::new().set_title("Add images").add_filter("Images", &["png", "jpg", "jpeg","dds"]).pick_files(){
for file in files.iter() {
let Ok(image) = texture_packer::importer::ImageImporter::import_from_file(file) else { continue };
let id = crate::helpers::id_from_path(&file.to_string_lossy());
self.image_data.insert(file.to_string_lossy().to_string(), AppImageData { width: image.width(), height: image.height(), data: ImageFile { id: id, image }, path: file.to_string_lossy().to_string() });
}
self.update_min_size();
} }
ui.add_space(10.0); }
let (x, y) = file.image.dimensions(); });
ui.label(&file.id) let mut to_remove: Option<String> = None;
.on_hover_text(format!("Dimensions: {}x{}", x, y)); Grid::new("Image List").num_columns(4).striped(true).spacing((10.0,10.0)).show(ui, |ui|{
});
} for (id, file) in self.image_data.iter() {
if ui.button("x").clicked() {
to_remove = Some(id.clone());
}
ui.image(format!("file://{}", file.path));
ui.add(Label::new(file.id()).selectable(false));
ui.add(Label::new(format!("{}x{}", file.width, file.height)).selectable(false));
ui.end_row();
}
});
if let Some(index) = to_remove { if let Some(index) = to_remove {
if let Some(i) = self if let Some(i) = self
.dropped_files .dropped_files
@ -377,10 +412,13 @@ impl eframe::App for Application {
if self.dropped_files.is_empty() { if self.dropped_files.is_empty() {
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
ui.add_space(50.0); ui.add_space(50.0);
ui.label( ui.add(
RichText::new("Drop images here first") Label::new(
.heading() RichText::new("Drop images here first")
.color(MY_ACCENT_COLOR32), .heading()
.color(MY_ACCENT_COLOR32),
)
.selectable(false),
); );
}); });
} }
@ -444,10 +482,8 @@ impl eframe::App for Application {
.add(egui::Button::new("Copy JSON to Clipboard")) .add(egui::Button::new("Copy JSON to Clipboard"))
.clicked() .clicked()
{ {
ui.output_mut(|o| { ui.ctx()
o.copied_text = .copy_text(data.atlas_asset_json.to_string());
data.atlas_asset_json.to_string();
});
}; };
} }
}); });

View File

@ -55,8 +55,8 @@ fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> { fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
let font_path = std::path::Path::new("/System/Library/Fonts"); let font_path = std::path::Path::new("/System/Library/Fonts");
let regular = fs::read(font_path.join("SFNSRounded.ttf"))?; let regular = std::fs::read(font_path.join("SFNSRounded.ttf"))?;
let semibold = fs::read(font_path.join("SFCompact.ttf"))?; let semibold = std::fs::read(font_path.join("SFCompact.ttf"))?;
Ok((regular, semibold)) Ok((regular, semibold))
} }

View File

@ -3,34 +3,37 @@ use image::DynamicImage;
use rpack_cli::ImageFile; use rpack_cli::ImageFile;
use texture_packer::importer::ImageImporter; use texture_packer::importer::ImageImporter;
use crate::app::AppImageData;
pub trait DroppedFileHelper { pub trait DroppedFileHelper {
fn file_path(&self) -> String; fn file_path(&self) -> String;
fn create_image<P>(&self, prefix: P) -> Option<(String, ImageFile)> fn create_image<P>(&self, prefix: P) -> Option<AppImageData>
where where
P: AsRef<str>; P: AsRef<str>;
fn dynamic_image(&self) -> Option<DynamicImage>; fn dynamic_image(&self) -> Option<DynamicImage>;
} }
pub fn id_from_path(path: &str) -> String {
match path.rfind('.') {
Some(index) => path[..index].to_string(),
None => path.to_string(),
}
.replace("\\", "/")
}
impl DroppedFileHelper for DroppedFile { impl DroppedFileHelper for DroppedFile {
fn file_path(&self) -> String { fn file_path(&self) -> String {
let id; match self.path.as_ref() {
#[cfg(not(target_arch = "wasm32"))] Some(path) => path.to_string_lossy().to_string(),
{ None => self.name.clone(),
let path = self.path.as_ref().unwrap().clone();
id = path.to_str().unwrap().to_owned();
} }
#[cfg(target_arch = "wasm32")]
{
id = self.name.clone();
}
id.replace(".png", "").replace("\\", "/")
} }
fn create_image<P>(&self, prefix: P) -> Option<(String, ImageFile)> fn create_image<P>(&self, prefix: P) -> Option<AppImageData>
where where
P: AsRef<str>, P: AsRef<str>,
{ {
let path = self.file_path(); let path = self.file_path();
let base_id = path.replace(".png", ""); let base_id = id_from_path(&path);
let id = base_id let id = base_id
.strip_prefix(prefix.as_ref()) .strip_prefix(prefix.as_ref())
@ -38,7 +41,12 @@ impl DroppedFileHelper for DroppedFile {
.to_owned(); .to_owned();
let image: DynamicImage = self.dynamic_image()?; let image: DynamicImage = self.dynamic_image()?;
Some((path, ImageFile { id, image })) Some(AppImageData {
width: image.width(),
height: image.height(),
data: ImageFile { id: id, image },
path,
})
} }
fn dynamic_image(&self) -> Option<DynamicImage> { fn dynamic_image(&self) -> Option<DynamicImage> {