rpack/crates/rpack_egui/src/app.rs

987 lines
44 KiB
Rust

use crossbeam::queue::SegQueue;
use egui::containers::menu::MenuButton;
use egui::Grid;
use egui::{
util::undoer::Undoer, Button, Color32, FontFamily, FontId, Frame, Image, Label, Layout,
RichText, Sense, Slider, Ui,
};
use egui_extras::{Column, TableBuilder};
use once_cell::sync::Lazy;
use rpack_cli::TilemapGenerationConfig;
use rpack_cli::{
packer::SkylinePacker, ImageFile, Spritesheet, SpritesheetBuildConfig, SpritesheetError,
};
use texture_packer::{Rect, TexturePackerConfig};
use crate::helpers::DroppedFileHelper;
static INPUT_QUEUE: Lazy<SegQueue<AppImageAction>> = Lazy::new(|| SegQueue::new());
pub const MY_ACCENT_COLOR32: Color32 = Color32::from_rgb(230, 102, 1);
pub const GIT_HASH: &str = env!("GIT_HASH");
pub const VERSION: &str = concat!(" v ", env!("CARGO_PKG_VERSION"), " ");
pub const ICON_DATA: &[u8] = include_bytes!("../static/base_icon.png");
#[derive(Clone)]
#[allow(dead_code)]
pub enum AppImageAction {
Add(AppImageData),
Replace(Vec<AppImageData>),
Remove(usize),
UpdateSpriteSheet(Result<Spritesheet, SpritesheetError>),
Clear,
RebuildAtlas,
#[cfg(not(target_arch = "wasm32"))]
ReadFromConfig(TilemapGenerationConfig, PathBuf),
}
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
use std::{future::Future, ops::RangeInclusive};
#[cfg(not(target_arch = "wasm32"))]
fn execute<F: Future<Output = ()> + Send + 'static>(f: F) {
// this is stupid... use any executor of your choice instead
std::thread::spawn(move || futures::executor::block_on(f));
}
#[cfg(target_arch = "wasm32")]
fn execute<F: Future<Output = ()> + 'static>(f: F) {
wasm_bindgen_futures::spawn_local(f);
}
pub enum SpriteSheetState {
Empty,
Building,
Ok(Spritesheet),
}
impl SpriteSheetState {
pub fn is_ok(&self) -> bool {
matches!(self, SpriteSheetState::Ok(_))
}
pub fn is_none(&self) -> bool {
matches!(self, SpriteSheetState::Empty)
}
pub fn is_building(&self) -> bool {
matches!(self, SpriteSheetState::Building)
}
}
pub struct Application {
data: ApplicationData,
output: SpriteSheetState,
last_error: Option<SpritesheetError>,
undoer: Undoer<ApplicationData>,
last_editor_paths: Vec<String>,
}
#[derive(serde::Deserialize, serde::Serialize, Default, Clone, PartialEq)]
pub struct ApplicationData {
#[serde(skip, default)]
image_data: Vec<AppImageData>,
#[serde(skip, default)]
settings: TilemapGenerationConfig,
#[serde(skip, default)]
min_size: u32,
}
#[derive(Clone, PartialEq)]
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()
}
pub fn update_id(&mut self, prefix: &str) {
self.data.id = self
.path
.strip_prefix(prefix)
.unwrap_or(&self.path)
.to_owned();
}
}
impl Default for Application {
fn default() -> Self {
Self {
data: Default::default(),
undoer: Default::default(),
output: SpriteSheetState::Empty,
last_error: None,
last_editor_paths: Vec::new(),
}
}
}
impl Application {
#[cfg(not(target_arch = "wasm32"))]
pub fn read_config(&mut self, config: rpack_cli::TilemapGenerationConfig) {
self.data.settings = config;
let (file_paths, prefix) = self.data.settings.get_file_paths_and_prefix();
INPUT_QUEUE.push(AppImageAction::Replace(
file_paths
.iter()
.flat_map(|f| f.create_image(&prefix))
.collect(),
));
}
pub fn get_common_prefix(&self) -> String {
let file_paths: Vec<String> = self
.data
.image_data
.iter()
.map(|image| image.path.clone())
.collect();
rpack_cli::get_common_prefix(&file_paths)
}
pub fn rebuild_image_data(&mut self) {
let prefix = self.get_common_prefix();
self.data
.image_data
.iter_mut()
.for_each(|f| f.update_id(prefix.as_str()));
self.update_min_size();
}
pub fn update_min_size(&mut self) {
self.data.min_size = self
.data
.image_data
.iter()
.max_by(|a, b| a.width.cmp(&b.width))
.map_or(32, |s| s.width)
.max(
self.data
.image_data
.iter()
.max_by(|a, b| a.height.cmp(&b.height))
.map_or(32, |s| s.height),
);
let config: TexturePackerConfig = (&self.data.settings).into();
for nr in [32, 64, 128, 256, 512, 1024, 2048, 4096] {
if nr < self.data.min_size {
continue;
}
let mut packer = SkylinePacker::new(TexturePackerConfig {
max_width: nr,
max_height: nr,
..config
});
let mut success = true;
for image in &self.data.image_data {
let data = Rect {
x: 0,
y: 0,
w: image.width,
h: image.height,
};
if !packer.can_pack(&data) || packer.pack(&data).is_none() {
success = false;
break;
}
}
if success && nr >= self.data.min_size {
self.data.min_size = nr;
break;
}
}
}
/// Called once before the first frame.
#[allow(dead_code, unused_variables, unused_mut)]
pub fn new(cc: &eframe::CreationContext<'_>, config_file: Option<String>) -> Self {
crate::fonts::setup_custom_fonts(&cc.egui_ctx);
// This is also where you can customize the look and feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
egui_extras::install_image_loaders(&cc.egui_ctx);
let mut last_editor_paths: Vec<String> = if let Some(storage) = cc.storage {
eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default()
} else {
Default::default()
};
let mut app = Self {
last_editor_paths,
..Default::default()
};
cc.egui_ctx.include_bytes("bytes://image.png", ICON_DATA);
#[cfg(not(target_arch = "wasm32"))]
if let Some(config_file) = config_file {
if let Ok(config) = rpack_cli::TilemapGenerationConfig::read_from_file(&config_file) {
app.read_config(config);
}
}
app
}
fn build_atlas(&mut self, ctx: &egui::Context) {
self.last_error = None;
ctx.forget_image("bytes://output.png");
if self.data.image_data.is_empty() {
self.output = SpriteSheetState::Empty;
return;
}
self.output = SpriteSheetState::Building;
let mut packer_config: TexturePackerConfig = (&self.data.settings).into();
if packer_config.max_height < self.data.min_size {
packer_config.max_height = self.data.min_size;
}
if packer_config.max_width < self.data.min_size {
packer_config.max_width = self.data.min_size;
}
let images: Vec<ImageFile> = self
.data
.image_data
.iter()
.map(|file| file.data.clone())
.collect();
let config = SpritesheetBuildConfig {
packer_config,
skip_metadata_serialization: self
.data
.settings
.skip_serializing_metadata
.unwrap_or_default(),
};
let path = format!("{}.png", &self.data.settings.output_path);
execute(async move {
let result = Spritesheet::build(config, &images, &path);
INPUT_QUEUE.push(AppImageAction::UpdateSpriteSheet(result));
});
}
fn save_json(&self) -> Result<(), String> {
let SpriteSheetState::Ok(spritesheet) = &self.output else {
return Err("Data is incorrect".to_owned());
};
let data = spritesheet.atlas_asset_json.to_string();
let filename = format!("{}.rpack.json", self.data.settings.output_path);
execute(async move {
let file_handle = rfd::AsyncFileDialog::new()
.set_directory(".")
.add_filter(".rpack.json", &["rpack.json"])
.set_file_name(filename)
.save_file()
.await;
if let Some(file_handle) = file_handle {
let _ = file_handle.write(data.as_bytes()).await;
}
});
Ok(())
}
fn read_files(&self) {
let common_prefix = self.get_common_prefix();
let working_dir = std::path::absolute(common_prefix)
.map_or(String::from("."), |p| p.to_string_lossy().to_string());
execute(async move {
#[cfg(target_arch = "wasm32")]
let title = "Open Images";
#[cfg(target_arch = "wasm32")]
let files = ["png", "jpg", "jpeg", "dds"];
#[cfg(not(target_arch = "wasm32"))]
let title = "Open Images or config";
#[cfg(not(target_arch = "wasm32"))]
let files = ["png", "jpg", "jpeg", "dds", "json"];
let file_handles = rfd::AsyncFileDialog::new()
.set_directory(&working_dir)
.set_title(title)
.add_filter("Files", &files)
.pick_files()
.await;
if let Some(file_handles) = file_handles {
#[cfg(not(target_arch = "wasm32"))]
if let Some(file) = file_handles
.iter()
.find(|s| s.file_name().ends_with("rpack_gen.json"))
{
if let Ok(config) =
rpack_cli::TilemapGenerationConfig::read_from_file(file.path())
{
INPUT_QUEUE.push(AppImageAction::ReadFromConfig(
config,
file.path().to_path_buf(),
));
}
return;
}
for file in file_handles {
let content = file.read().await;
#[cfg(target_arch = "wasm32")]
let name = file.file_name();
#[cfg(not(target_arch = "wasm32"))]
let name = file.path().to_string_lossy().to_string();
if let Some(image) = (content, name).create_image("") {
INPUT_QUEUE.push(AppImageAction::Add(image));
}
}
}
});
}
fn save_atlas(&self) -> Result<(), String> {
let SpriteSheetState::Ok(spritesheet) = &self.output else {
return Err("Data is incorrect".to_owned());
};
let filename = format!("{}.png", self.data.settings.output_path);
let mut data = vec![];
let Ok(()) = spritesheet.image_data.write_to(
&mut std::io::Cursor::new(&mut data),
image::ImageFormat::Png,
) else {
return Err("Failed to copy data".to_owned());
};
execute(async move {
let Some(file) = rfd::AsyncFileDialog::new()
.set_directory(".")
.set_file_name(filename)
.save_file()
.await
else {
return;
};
file.write(&data).await.unwrap();
});
Ok(())
}
}
impl eframe::App for Application {
/// Called by the framework to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, &self.last_editor_paths);
}
/// Called each time the UI needs repainting, which may be many times per second.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
{
#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))]
puffin::profile_scope!("handle_undo");
self.undoer
.feed_state(ctx.input(|input| input.time), &self.data);
}
if !INPUT_QUEUE.is_empty() {
let mut rebuild = false;
#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))]
puffin::profile_scope!("loading_images");
#[allow(dead_code)]
while let Some(cmd) = INPUT_QUEUE.pop() {
match cmd {
AppImageAction::Add(image) => {
rebuild = true;
let mut out_vec = vec![];
image
.data
.image
.thumbnail(64, 64)
.write_to(
&mut std::io::Cursor::new(&mut out_vec),
image::ImageFormat::Png,
)
.unwrap();
ctx.include_bytes(format!("bytes://{}", image.path), out_vec);
self.data.image_data.push(image);
}
AppImageAction::Remove(i) => {
rebuild = true;
self.data.image_data.remove(i);
}
AppImageAction::Clear => {
rebuild = true;
self.data.image_data.clear();
}
AppImageAction::RebuildAtlas => {
rebuild = true;
// Will be called after this loop
}
#[cfg(not(target_arch = "wasm32"))]
AppImageAction::ReadFromConfig(config, path) => {
let path_str = path.to_string_lossy().to_string();
if self.last_editor_paths.iter().all(|p| path_str != *p) {
self.last_editor_paths.insert(0, path_str);
if self.last_editor_paths.len() > 3 {
self.last_editor_paths.pop();
}
}
self.read_config(config);
}
AppImageAction::Replace(new_images) => {
rebuild = true;
self.data.image_data.clear();
self.data.image_data.extend(new_images);
for image in &self.data.image_data {
let mut out_vec = vec![];
image
.data
.image
.thumbnail(64, 64)
.write_to(
&mut std::io::Cursor::new(&mut out_vec),
image::ImageFormat::Png,
)
.unwrap();
ctx.include_bytes(format!("bytes://{}", image.path), out_vec);
}
}
AppImageAction::UpdateSpriteSheet(result) => match result {
Ok(spritesheet) => {
let mut out_vec = vec![];
spritesheet
.image_data
.write_to(
&mut std::io::Cursor::new(&mut out_vec),
image::ImageFormat::Png,
)
.unwrap();
ctx.include_bytes("bytes://output.png", out_vec);
self.output = SpriteSheetState::Ok(spritesheet);
}
Err(e) => {
self.last_error = Some(e);
}
},
}
}
if rebuild {
self.rebuild_image_data();
self.build_atlas(ctx);
}
}
egui::TopBottomPanel::top("topPanel")
.frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| {
#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))]
puffin::profile_scope!("top_panel");
// ui.add_space(TOP_SIDE_MARGIN);
#[cfg(not(target_arch = "wasm32"))]
let title_font = FontId::new(26.0, FontFamily::Name("semibold".into()));
#[cfg(target_arch = "wasm32")]
let title_font = FontId::new(26.0, FontFamily::Proportional);
ui.with_layout(Layout::left_to_right(egui::Align::Center), |ui| {
ui.add_space(10.0);
ui.add(
Label::new(
egui::RichText::new("rPack")
.font(title_font)
.color(MY_ACCENT_COLOR32)
.strong(),
)
.selectable(false),
);
ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(10.0);
if self.output.is_none() {
if ui.add(egui::Button::new("Open")).clicked() {
self.read_files();
}
}
if self.output.is_ok() {
if ui.add(egui::Button::new("Save atlas image")).clicked() {
if let Err(error) = self.save_atlas() {
eprintln!("ERROR: {}", error);
}
}
if ui.add(egui::Button::new("Save atlas json")).clicked() {
if let Err(error) = self.save_json() {
eprintln!("ERROR: {}", error);
}
}
}
if ui.available_width() > 15.0 {
ui.add_space(ui.available_width() - 10.0);
}
});
});
// ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
// let text = ;
// ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, HEADER_HEIGHT));
// ui.add(egui::Label::new(text).selectable(false));
// });
});
ctx.input(|i| {
#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))]
puffin::profile_scope!("dropped_files");
if i.raw.dropped_files.is_empty() {
return;
}
for file in i.raw.dropped_files.iter() {
#[cfg(not(target_arch = "wasm32"))]
if let Some(path) = &file.path {
if path.is_dir() {
let Ok(dir) = path.read_dir() else {
continue;
};
for entry in dir {
if let Ok(entry) = entry {
if let Some(dyn_image) =
entry.metadata().ok().and_then(|metadata| {
if metadata.is_file() {
entry.create_image("")
} else {
None
}
})
{
INPUT_QUEUE.push(AppImageAction::Add(dyn_image));
}
}
}
} else {
let Some(path) = &file.path else {
continue;
};
if path.to_string_lossy().ends_with(".rpack_gen.json") {
if let Ok(config) =
rpack_cli::TilemapGenerationConfig::read_from_file(&path)
{
INPUT_QUEUE
.push(AppImageAction::ReadFromConfig(config, path.clone()));
break;
}
}
}
}
if let Some(dyn_image) = file.create_image("") {
INPUT_QUEUE.push(AppImageAction::Add(dyn_image));
}
}
});
egui::TopBottomPanel::bottom("bottom_panel")
.frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| {
#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))]
puffin::profile_scope!("bottom_panel");
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.add_space(5.0);
ui.add_enabled_ui(self.undoer.has_undo(&self.data), |ui| {
if ui.button("").on_hover_text("Go back").clicked() {
if let Some(action) = self.undoer.undo(&self.data) {
self.data = action.clone();
self.rebuild_image_data();
self.build_atlas(ui.ctx());
}
}
});
ui.add_enabled_ui(self.undoer.has_redo(&self.data), |ui| {
if ui.button("").on_hover_text("Redo").clicked() {
if let Some(action) = self.undoer.redo(&self.data) {
self.data = action.clone();
self.rebuild_image_data();
self.build_atlas(ui.ctx());
}
}
});
ui.add_space(5.0);
powered_by_egui_and_eframe(ui);
});
ui.add_space(5.0);
});
egui::SidePanel::right("right")
.min_width(200.0)
.max_width(400.0)
.frame(egui::Frame::canvas(&ctx.style()).inner_margin(10))
.show_animated(ctx, !self.data.image_data.is_empty(), |ui| {
egui::ScrollArea::vertical()
.id_salt("rightPanel_scroll")
.show(ui, |ui| {
ui.add_enabled_ui(!self.output.is_building(), |ui| {
#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))]
puffin::profile_scope!("right_panel");
let mut changed = false;
Grid::new("settings_grid")
.num_columns(2)
.spacing((10.0, 10.0))
.striped(true)
.show(ui, |ui| {
ui.style_mut().visuals.faint_bg_color =
Color32::from_white_alpha(15);
ui.style_mut().interaction.selectable_labels = false;
let id = ui.label("File Name").id;
changed |= ui
.text_edit_singleline(&mut self.data.settings.output_path)
.labelled_by(id)
.changed();
ui.end_row();
ui.label("Output size");
let selected_size = match self.data.settings.size {
Some(size) => {
format!("{}x{}", size, size)
}
None => String::from("2048x2048"),
};
changed |= MenuButton::from_button(
Button::new(selected_size).frame_when_inactive(false),
)
.ui(ui, |ui| {
for size in [32, 64, 128, 256, 512, 1024, 2048, 4096] {
let same_size =
self.data.settings.size.is_some_and(|s| s == size);
if ui
.add_enabled(
size >= self.data.min_size,
Button::new(format!("{}x{}", size, size))
.selected(same_size),
)
.clicked()
{
self.data.settings.size =
if same_size { None } else { Some(size) };
return true;
}
}
false
})
.1
.map_or(false, |s| s.inner);
ui.end_row();
changed |= slider_field(
ui,
"Border Padding",
&mut self.data.settings.border_padding,
0,
0..=10,
);
ui.end_row();
changed |= slider_field(
ui,
"Texture Padding",
&mut self.data.settings.texture_padding,
2,
0..=10,
);
ui.end_row();
let mut serialize_metadata = self
.data
.settings
.skip_serializing_metadata
.unwrap_or_default();
ui.label("Skip Metadata Serialization");
if ui.checkbox(&mut serialize_metadata, "").changed() {
self.data.settings.skip_serializing_metadata =
Some(serialize_metadata);
}
});
ui.vertical_centered_justified(|ui| {
ui.add_space(10.0);
// changed |= ui.checkbox(&mut
// self.data.settings., "Force Max Dimensions").changed();
// ui.checkbox(&mut self.config.allow_rotation, "Allow Rotation")
// .on_hover_text("True to allow rotation of the input images. Default value is `true`. Images rotated will be rotated 90 degrees clockwise.");
// ui.checkbox(&mut self.data.config.texture_outlines, "Texture Outlines")
// .on_hover_text("Draw the red line on the edge of the each frames. Useful for debugging.");
ui.add_space(10.0);
});
if changed {
INPUT_QUEUE.push(AppImageAction::RebuildAtlas);
}
ui.separator();
{
ui.style_mut().interaction.selectable_labels = false;
#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))]
puffin::profile_scope!("image_list");
ui.horizontal(|ui| {
ui.heading("Images");
ui.with_layout(
Layout::right_to_left(egui::Align::Center),
|ui| {
if !self.data.image_data.is_empty()
&& ui
.add(egui::Button::new("🗙").frame(false))
.on_hover_text("Remove all images")
.clicked()
{
INPUT_QUEUE.push(AppImageAction::Clear);
}
ui.add_space(10.0);
if ui
.add(egui::Button::new("").frame(false))
.on_hover_text("Add more images")
.clicked()
{
self.read_files();
}
if ui.available_width() > 15.0 {
ui.add_space(ui.available_width() - 10.0);
}
},
)
});
let length = self.data.image_data.len();
let text_height =
egui::TextStyle::Body.resolve(ui.style()).size * 1.5;
let table = TableBuilder::new(ui)
.striped(true)
.vscroll(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::remainder().at_least(50.0))
.column(Column::exact(70.0))
.column(Column::auto().at_most(150.0).at_least(30.0))
.column(Column::auto())
.sense(egui::Sense::click());
table
.header(text_height, |mut row| {
row.col(|col| {
col.vertical_centered_justified(|ui| {
ui.add(
egui::Label::new("Name")
.wrap_mode(egui::TextWrapMode::Truncate)
.selectable(false), // .sense(Sense::click()),
)
});
});
row.col(|col| {
col.vertical_centered_justified(|ui| {
ui.add(
egui::Label::new("Preview")
.wrap_mode(egui::TextWrapMode::Truncate)
.selectable(false),
)
});
});
row.col(|col| {
col.vertical_centered_justified(|ui| {
ui.add(
egui::Label::new("Dimensions")
.wrap_mode(egui::TextWrapMode::Truncate)
.selectable(false)
.sense(Sense::click()),
)
});
});
row.col(|col| {
col.vertical_centered_justified(|ui| {
ui.add(
egui::Label::new("")
.wrap_mode(egui::TextWrapMode::Truncate)
.selectable(false),
)
});
});
})
.body(|body| {
body.rows(64.0, length, |mut row| {
let index = row.index();
let file = &self.data.image_data[index];
row.col(|ui| {
ui.add(
Label::new(file.id())
.selectable(false)
.wrap_mode(egui::TextWrapMode::Truncate),
);
});
row.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.add_sized(
(64.0, 64.0),
Image::from_uri(format!(
"bytes://{}",
file.path.as_str()
))
.corner_radius(5u8),
);
});
});
row.col(|ui| {
ui.add(
Label::new(format!(
"{}x{}",
file.width, file.height
))
.selectable(false),
);
});
row.col(|ui| {
if ui
.add(Button::new("🗙").frame(false))
.on_hover_text("Remove")
.clicked()
{
INPUT_QUEUE.push(AppImageAction::Remove(index));
}
});
});
});
}
});
});
});
egui::CentralPanel::default()
.frame(Frame::central_panel(&ctx.style()).inner_margin(16i8))
.show(ctx, |ui| {
#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))]
puffin::profile_scope!("central_panel");
if let Some(error) = &self.last_error {
let text = egui::RichText::new(format!("Error: {}", &error))
.font(FontId::new(20.0, FontFamily::Name("semibold".into())))
.color(Color32::RED)
.strong();
ui.add(egui::Label::new(text));
}
egui::ScrollArea::vertical()
.id_salt("vertical_scroll")
.show(ui, |ui| {
ui.vertical_centered_justified(|ui| {
if self.data.image_data.is_empty() {
ui.add_space(25.0);
Grid::new("Header").num_columns(2).show(ui, |ui|{
ui.add_sized(
(256.0,256.0),Image::new("bytes://image.png"));
ui.vertical_centered_justified(|ui|{
ui.add_space(15.0);
ui.add(
Label::new(
RichText::new("Create spritesheets in seconds!")
.heading()
// .color(MY_ACCENT_COLOR32),
)
.selectable(false),
);
ui.separator();
ui.add_space(10.0);
ui.add(
Label::new(
RichText::new("Drag and drop images here to start creating new spritesheets.")
)
.selectable(false),
);
});
});
ui.add_space(10.0);
// if ui.button("Open files").clicked() {
// self.read_files();
// }
#[cfg(not(target_arch = "wasm32"))]
{
if !self.last_editor_paths.is_empty() {
ui.separator();
ui.add(
Label::new(RichText::new("Recent projects").heading())
.selectable(false),
);
}
ui.add_space(10.0);
for p in &self.last_editor_paths {
if ui.add(Button::new(p).frame(false)).clicked() {
if let Ok(config) =
rpack_cli::TilemapGenerationConfig::read_from_file(
p,
)
{
use std::str::FromStr;
INPUT_QUEUE.push(AppImageAction::ReadFromConfig(
config,
PathBuf::from_str(p).unwrap_or_default(),
));
}
}
ui.add_space(10.0);
}
}
}
if self.output.is_building(){
ui.heading(
egui::RichText::new("Building atlas...").color(MY_ACCENT_COLOR32),
);
ui.add_space(10.0);
ui.spinner();
}
let SpriteSheetState::Ok(data) = &self.output else {
return;
};
// ui.add_space(10.0);
// ui.heading(
// egui::RichText::new("Created atlas").color(MY_ACCENT_COLOR32),
// );
ui.add_space(10.0);
ui.horizontal(|ui|{
ui.label(format!(
"{} sprites\nsize: {}x{}",
data.atlas_asset.frames.len(),
data.atlas_asset.size[0],
data.atlas_asset.size[1]
));
ui.separator();
ui.vertical_centered_justified(|ui|{
ui.add_space(10.0);
if ui
.add(egui::Button::new("Copy JSON to Clipboard"))
.clicked()
{
ui.ctx()
.copy_text(data.atlas_asset_json.to_string());
};
ui.add_space(10.0);
egui_json_tree::JsonTree::new(
"simple-tree",
&data.atlas_asset_json,
)
.show(ui);
});
});
ui.add_space(10.0);
ui.separator();
ui.add(Image::from_uri("bytes://output.png").bg_fill(Color32::from_black_alpha(200)));
ui.separator();
ui.add_space(20.0);
});
});
});
}
}
fn powered_by_egui_and_eframe(ui: &mut egui::Ui) {
ui.with_layout(Layout::right_to_left(egui::Align::Min), |ui| {
ui.add_space(10.0);
if ui
.add(egui::Button::new(VERSION).frame(false))
.on_hover_text(GIT_HASH)
.clicked()
{
ui.ctx()
.open_url(egui::OpenUrl::new_tab(env!("CARGO_PKG_HOMEPAGE")));
}
ui.separator();
egui::widgets::global_theme_preference_switch(ui);
ui.separator();
ui.spacing_mut().item_spacing.x = 0.0;
ui.hyperlink_to("Mev Lyshkin", "https://www.mevlyshkin.com/");
ui.add_space(10.0);
ui.label("Made by");
ui.add_space((ui.available_width() - 10.0).max(15.0));
});
}
fn slider_field(
ui: &mut Ui,
label: &str,
field: &mut Option<u32>,
default_value: u32,
range: RangeInclusive<u32>,
) -> bool {
ui.label(label);
let mut value = field.unwrap_or(default_value);
if ui.add(Slider::new(&mut value, range)).changed() {
*field = Some(value);
true
} else {
false
}
}