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

View File

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

View File

@ -1,24 +1,23 @@
[package]
name = "rpack_egui"
version = "0.2.0"
version = "0.3.0"
description = "GUI application for generating rpack atlases"
authors = ["Piotr Siuszko <siuszko@zoho.com>"]
edition = "2021"
rust-version = "1.81"
edition = "2024"
repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack"
license = "MIT OR Apache-2.0"
[dependencies]
egui = "0.30"
eframe = { version = "0.30", default-features = false, features = [
egui = "0.32"
eframe = { version = "0.32", default-features = false, features = [
"accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies.
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
] }
log = "0.4"
egui_json_tree = "0.10"
egui_json_tree = "0.13"
rpack_cli = { default-features = false, path = "../rpack_cli", version = "0.2" }
# You only need serde if you want app persistence:
@ -26,7 +25,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
texture_packer = { 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 = [] }
wasm-bindgen-futures = "0.4"
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 egui::{CollapsingHeader, Color32, DroppedFile, FontFamily, FontId, Image, RichText};
use image::GenericImageView;
use egui::{
CollapsingHeader, Color32, DroppedFile, FontFamily, FontId, Grid, Image, Label, RichText,
};
use rpack_cli::{ImageFile, Spritesheet, SpritesheetError};
use texture_packer::TexturePackerConfig;
@ -33,8 +34,22 @@ pub struct Application {
#[serde(skip)]
max_size: u32,
#[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 {
fn default() -> Self {
Self {
@ -71,7 +86,7 @@ impl Application {
self.image_data = self
.dropped_files
.iter()
.flat_map(|f| f.create_image(&prefix))
.flat_map(|f| f.create_image(&prefix).map(|i| (i.id().to_string(), i)))
.collect();
self.update_min_size();
}
@ -79,18 +94,18 @@ impl Application {
if let Some(file) = self
.image_data
.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 {
self.min_size[0] = 32;
}
if let Some(file) = self
.image_data
.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 {
self.min_size[1] = 32;
}
@ -114,7 +129,11 @@ impl Application {
fn build_atlas(&mut self, ctx: &egui::Context) {
self.data = 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] {
if size < self.min_size[0] || size < self.min_size[1] {
@ -328,25 +347,41 @@ impl eframe::App for Application {
CollapsingHeader::new("Image list")
.default_open(true)
.show(ui, |ui| {
ui.horizontal(|ui|{
if !self.image_data.is_empty() && ui.button("clear list").clicked() {
self.image_data.clear();
self.dropped_files.clear();
self.data = None;
self.update_min_size();
}
let mut to_remove: Option<String> = None;
for (id, file) in self.image_data.iter() {
ui.horizontal_top(|ui| {
ui.add_space(10.0);
#[cfg(not(target_arch = "wasm32"))]
if ui.button("Add").clicked() {
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();
}
}
});
let mut to_remove: Option<String> = None;
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.add_space(10.0);
let (x, y) = file.image.dimensions();
ui.label(&file.id)
.on_hover_text(format!("Dimensions: {}x{}", x, y));
});
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(i) = self
.dropped_files
@ -377,10 +412,13 @@ impl eframe::App for Application {
if self.dropped_files.is_empty() {
ui.vertical_centered_justified(|ui| {
ui.add_space(50.0);
ui.label(
ui.add(
Label::new(
RichText::new("Drop images here first")
.heading()
.color(MY_ACCENT_COLOR32),
)
.selectable(false),
);
});
}
@ -444,10 +482,8 @@ impl eframe::App for Application {
.add(egui::Button::new("Copy JSON to Clipboard"))
.clicked()
{
ui.output_mut(|o| {
o.copied_text =
data.atlas_asset_json.to_string();
});
ui.ctx()
.copy_text(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>)> {
let font_path = std::path::Path::new("/System/Library/Fonts");
let regular = fs::read(font_path.join("SFNSRounded.ttf"))?;
let semibold = fs::read(font_path.join("SFCompact.ttf"))?;
let regular = std::fs::read(font_path.join("SFNSRounded.ttf"))?;
let semibold = std::fs::read(font_path.join("SFCompact.ttf"))?;
Ok((regular, semibold))
}

View File

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