This commit is contained in:
Piotr Siuszko 2023-12-07 18:24:34 +01:00
commit 7e62334e54
24 changed files with 4353 additions and 0 deletions

6
.cargo/config.toml Normal file
View File

@ -0,0 +1,6 @@
# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility
# we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93
[target.wasm32-unknown-unknown]
rustflags = ["--cfg=web_sys_unstable_apis"]

46
.github/workflows/pages.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Github Pages
# By default, runs if you push to master. keeps your deployed app in sync with master branch.
on:
push:
branches:
- main
- master
# to only run when you do a new github release, comment out above part and uncomment the below trigger.
# on:
# release:
# types:
# - published
permissions:
contents: write # for committing to gh-pages branch.
jobs:
build-github-pages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2 # repo checkout
- uses: actions-rs/toolchain@v1 # get rust toolchain for wasm
with:
profile: minimal
toolchain: stable
target: wasm32-unknown-unknown
override: true
- name: Rust Cache # cache the rust build artefacts
uses: Swatinem/rust-cache@v1
- name: Download and install Trunk binary
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
- name: Build # build
# "${GITHUB_REPOSITORY#*/}" evaluates into the name of the repository
# using --public-url something will allow trunk to modify all the href paths like from favicon.ico to repo_name/favicon.ico .
# this is necessary for github pages where the site is deployed to username.github.io/repo_name and all files must be requested
# relatively as eframe_template/favicon.ico. if we skip public-url option, the href paths will instead request username.github.io/favicon.ico which
# will obviously return error 404 not found.
run: ./trunk build --release
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: dist
# this option will not maintain any history of your previous pages deployment
# set to false if you want all page build to be committed to your gh-pages branch history
single-commit: true

105
.github/workflows/rust.yml vendored Normal file
View File

@ -0,0 +1,105 @@
on: [push, pull_request]
name: CI
env:
# This is required to enable the web_sys clipboard API which egui_web uses
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
RUSTFLAGS: --cfg=web_sys_unstable_apis
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features
check_wasm:
name: Check wasm32
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
target: wasm32-unknown-unknown
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features --lib --target wasm32-unknown-unknown
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
- uses: actions-rs/cargo@v1
with:
command: test
args: --lib
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
trunk:
name: trunk
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.72.0
target: wasm32-unknown-unknown
override: true
- name: Download and install Trunk binary
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
- name: Build
run: ./trunk build

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
/dist
skyline-packer-output.png
result.png

3394
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

55
Cargo.toml Normal file
View File

@ -0,0 +1,55 @@
[package]
name = "rpack"
version = "0.1.0"
authors = ["Piotr Siuszko <siuszko@zoho.com>"]
edition = "2021"
rust-version = "1.72"
repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack"
[dependencies]
egui = "0.24.1"
eframe = { version = "0.24.1", 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"
# You only need serde if you want app persistence:
serde = { version = "1", features = ["derive"] }
texture_packer = {version="0.25.0", features = ["common"]}
image = { version = "0.24", features = ["jpeg", "png"] }
egui_extras = { version = "*", features = ["all_loaders"] }
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
env_logger = "0.10"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"
wasm-bindgen = "0.2"
web-sys = {version = "0.3", features=["Url","HtmlAnchorElement","Blob", "BlobPropertyBag"]}
js-sys = "0.3"
[profile.release]
opt-level = 2 # fast and small wasm
# Optimize all dependencies even in debug builds:
[profile.dev.package."*"]
opt-level = 2
[patch.crates-io]
# If you want to use the bleeding edge version of egui and eframe:
# egui = { git = "https://github.com/emilk/egui", branch = "master" }
# eframe = { git = "https://github.com/emilk/egui", branch = "master" }
# If you fork https://github.com/emilk/egui you can test with:
# egui = { path = "../egui/crates/egui" }
# eframe = { path = "../egui/crates/eframe" }

43
README.md Normal file
View File

@ -0,0 +1,43 @@
# rpack
[![Build Status](https://github.com/Leinnan/rpack/workflows/CI/badge.svg)](https://github.com/Leinnan/rpack/actions?workflow=CI)
Attempt to build texture atlas packer GUI.
### Testing locally
Make sure you are using the latest version of stable rust by running `rustup update`.
`cargo run --release`
On Linux you need to first run:
`sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
On Fedora Rawhide you need to run:
`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel gtk3-devel atk fontconfig-devel`
### Web Locally
You can compile your app to [WASM](https://en.wikipedia.org/wiki/WebAssembly) and publish it as a web page.
We use [Trunk](https://trunkrs.dev/) to build for web target.
1. Install the required target with `rustup target add wasm32-unknown-unknown`.
2. Install Trunk with `cargo install --locked trunk`.
3. Run `trunk serve` to build and serve on `http://127.0.0.1:8080`. Trunk will rebuild automatically if you edit the project.
4. Open `http://127.0.0.1:8080/index.html#dev` in a browser. See the warning below.
> `assets/sw.js` script will try to cache our app, and loads the cached version when it cannot connect to server allowing your app to work offline (like PWA).
> appending `#dev` to `index.html` will skip this caching, allowing us to load the latest builds during development.
### Web Deploy
1. Just run `trunk build --release`.
2. It will generate a `dist` directory as a "static html" website
3. Upload the `dist` directory to any of the numerous free hosting websites including [GitHub Pages](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site).
4. we already provide a workflow that auto-deploys our app to GitHub pages if you enable it.
> To enable Github Pages, you need to go to Repository -> Settings -> Pages -> Source -> set to `gh-pages` branch and `/` (root).
>
> If `gh-pages` is not available in `Source`, just create and push a branch called `gh-pages` and it should be available.
You can test the template app at <https://emilk.github.io/eframe_template/>.

1
Trunk.toml Normal file
View File

@ -0,0 +1 @@
[build]

BIN
assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/icon-1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

BIN
assets/icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

28
assets/manifest.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "egui Template PWA",
"short_name": "egui-template-pwa",
"icons": [
{
"src": "./icon-256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "./maskable_icon_x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "./icon-1024.png",
"sizes": "1024x1024",
"type": "image/png"
}
],
"lang": "en-US",
"id": "/index.html",
"start_url": "./index.html",
"display": "standalone",
"background_color": "white",
"theme_color": "white"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

25
assets/sw.js Normal file
View File

@ -0,0 +1,25 @@
var cacheName = 'egui-template-pwa';
var filesToCache = [
'./',
'./index.html',
'./rpack.js',
'./rpack_bg.wasm',
];
/* Start the service worker and cache all of the app's content */
self.addEventListener('install', function (e) {
e.waitUntil(
caches.open(cacheName).then(function (cache) {
return cache.addAll(filesToCache);
})
);
});
/* Serve cached content when offline */
self.addEventListener('fetch', function (e) {
e.respondWith(
caches.match(e.request).then(function (response) {
return response || fetch(e.request);
})
);
});

21
build.rs Normal file
View File

@ -0,0 +1,21 @@
use std::process::Command;
fn main() {
// note: add error checking yourself.
{
let output = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.unwrap();
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", &git_hash[..7]);
}
{
let output = Command::new("git")
.args(["log", "-1", "--date=format:%Y/%m/%d %T", "--format=%ad"])
.output()
.unwrap();
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_DATE={}", git_hash);
}
}

11
check.sh Normal file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
# This scripts runs various CI-like checks in a convenient way.
set -eux
cargo check --workspace --all-targets
cargo check --workspace --all-features --lib --target wasm32-unknown-unknown
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::all
cargo test --workspace --all-targets --all-features
cargo test --workspace --doc
trunk build

140
index.html Normal file
View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head>
<!-- change this to your project name -->
<title>rPack</title>
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
<link data-trunk rel="rust" data-wasm-opt="2" />
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
<base data-trunk-public-url />
<link data-trunk rel="icon" href="assets/favicon.ico">
<link data-trunk rel="copy-file" href="assets/sw.js" />
<link data-trunk rel="copy-file" href="assets/manifest.json" />
<link data-trunk rel="copy-file" href="assets/icon-1024.png" />
<link data-trunk rel="copy-file" href="assets/icon-256.png" />
<link data-trunk rel="copy-file" href="assets/icon_ios_touch_192.png" />
<link data-trunk rel="copy-file" href="assets/maskable_icon_x512.png" />
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="icon_ios_touch_192.png">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (prefers-color-scheme: dark) {
body {
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}
/* Position canvas in center-top: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0%;
left: 50%;
transform: translate(-50%, 0%);
}
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<!-- The WASM code will resize the canvas dynamically -->
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
<canvas id="the_canvas_id"></canvas>
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
<script>
// We disable caching during development so that we always view the latest version.
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
window.addEventListener('load', function () {
navigator.serviceWorker.register('sw.js');
});
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->

10
rust-toolchain Normal file
View File

@ -0,0 +1,10 @@
# 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.72.0"
components = [ "rustfmt", "clippy" ]
targets = [ "wasm32-unknown-unknown" ]

420
src/app.rs Normal file
View File

@ -0,0 +1,420 @@
use std::{collections::HashMap, io::Cursor};
use egui::{CollapsingHeader, Color32, DroppedFile, FontFamily, FontId, Image, RichText};
use image::DynamicImage;
use texture_packer::{
importer::ImageImporter, texture::Texture, TexturePacker, TexturePackerConfig,
};
pub const MY_ACCENT_COLOR32: Color32 = Color32::from_rgb(230, 102, 1);
pub const TOP_SIDE_MARGIN: f32 = 10.0;
pub const HEADER_HEIGHT: f32 = 45.0;
pub const TOP_BUTTON_WIDTH: f32 = 150.0;
pub const GIT_HASH: &str = env!("GIT_HASH");
#[derive(Clone)]
pub struct Spritesheet {
pub data: Vec<u8>,
pub frames: HashMap<String, texture_packer::Frame<String>>,
pub size: (u32, u32),
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] // if we add new fields, give them default values when deserializing old state
pub struct TemplateApp {
// Example stuff:
label: String,
#[serde(skip)] // This how you opt-out of serialization of a field
value: f32,
#[serde(skip)]
dropped_files: Vec<DroppedFile>,
#[serde(skip)]
config: TexturePackerConfig,
#[serde(skip)]
image: Option<Image<'static>>,
#[serde(skip)]
counter: i32,
#[serde(skip)]
data: Option<Spritesheet>,
#[serde(skip)]
error: Option<String>,
}
impl Default for TemplateApp {
fn default() -> Self {
Self {
// Example stuff:
label: "Hello World!".to_owned(),
value: 2.7,
dropped_files: vec![],
config: TexturePackerConfig {
max_width: 2048,
max_height: 2048,
allow_rotation: false,
border_padding: 2,
trim: false,
..Default::default()
},
counter: 0,
image: None,
data: None,
error: None,
}
}
}
impl TemplateApp {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
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);
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
if let Some(storage) = cc.storage {
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
}
Default::default()
}
fn build_atlas(&mut self, ctx: &egui::Context) {
self.error = None;
let mut packer = TexturePacker::new_skyline(self.config);
for file in &self.dropped_files {
let id = id_for_file(file);
let texture = dynamic_image_from_file(file);
let can_pack = packer.can_pack(&texture);
if can_pack {
packer.pack_own(id, texture).unwrap();
} else {
self.error = Some(format!(
"Consider making atlas bigger. Could not make atlas, failed on: {}",
id
));
return;
}
}
for (name, frame) in packer.get_frames() {
println!(" {:7} : {:?}", name, frame.frame);
}
let mut out_vec = vec![];
let exporter = texture_packer::exporter::ImageExporter::export(&packer).unwrap();
exporter
.write_to(&mut Cursor::new(&mut out_vec), image::ImageFormat::Png)
.unwrap();
self.data = Some(Spritesheet {
data: out_vec.clone(),
frames: packer.get_frames().clone(),
size: (packer.width(), packer.height()),
});
let id = format!("bytes://output_{}.png", self.counter);
self.image = None;
ctx.forget_image(&id);
self.counter += 1;
let id = format!("bytes://output_{}.png", self.counter);
ctx.include_bytes(id.clone(), out_vec.clone());
println!("LENGTH OF {}: {}", id.clone(), out_vec.len());
self.image = Some(Image::from_uri(id.clone()));
ctx.request_repaint();
}
fn save_atlas(&mut self) {
if self.data.is_none() {
return;
}
let data = self.data.clone().unwrap().data;
#[cfg(not(target_arch = "wasm32"))]
{
use std::io::Write;
let mut file = std::fs::File::create("result.png").unwrap();
let write_result = file.write_all(&data);
if write_result.is_err() {
self.error = Some(format!(
"Could not make atlas, error: {:?}",
write_result.unwrap_err()
));
} else {
println!("Output texture stored in {:?}", file);
}
}
#[cfg(target_arch = "wasm32")]
save_blob_on_wasm(&data, "result.png");
}
}
#[cfg(target_arch = "wasm32")]
fn save_blob_on_wasm(buf: &[u8], id: &str) {
use wasm_bindgen::*;
use web_sys::*;
let window = web_sys::window().unwrap();
let doc = window.document().unwrap();
let arr: js_sys::Array = buf
.iter()
.copied()
.flat_map(|n| n.to_be_bytes().into_iter().map(JsValue::from))
.collect();
let blob = Blob::new_with_u8_array_sequence_and_options(
&arr,
web_sys::BlobPropertyBag::new().type_("data:image/png;base64"),
)
.unwrap();
let blob_url = web_sys::Url::create_object_url_with_blob(&blob).unwrap();
let download_link = doc.create_element("a").unwrap();
let download_link: HtmlAnchorElement = download_link.unchecked_into();
download_link.set_href(&blob_url);
download_link.set_download(id);
doc.body().unwrap().append_child(&download_link).unwrap();
download_link.click();
}
fn setup_custom_fonts(ctx: &egui::Context) {
// Start with the default fonts (we will be adding to them rather than replacing them).
let mut fonts = egui::FontDefinitions::default();
// Install my own font (maybe supporting non-latin characters).
// .ttf and .otf files supported.
fonts.font_data.insert(
"regular".to_owned(),
egui::FontData::from_static(include_bytes!("../static/JetBrainsMonoNL-Regular.ttf")),
);
fonts.font_data.insert(
"semibold".to_owned(),
egui::FontData::from_static(include_bytes!("../static/JetBrainsMono-SemiBold.ttf")),
);
// Put my font first (highest priority) for proportional text:
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "regular".to_owned());
fonts
.families
.entry(egui::FontFamily::Name("semibold".into()))
.or_default()
.insert(0, "semibold".to_owned());
// Put my font as last fallback for monospace:
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.push("regular".to_owned());
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
impl eframe::App for TemplateApp {
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
/// 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) {
egui::TopBottomPanel::top("topPanel")
.frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| {
ui.with_layout(
egui::Layout::left_to_right(egui::Align::Center)
.with_cross_align(eframe::emath::Align::Center),
|ui| {
let text = egui::RichText::new("rPack")
.font(FontId::new(26.0, FontFamily::Name("semibold".into())))
.color(MY_ACCENT_COLOR32)
.strong();
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, HEADER_HEIGHT));
ui.add(egui::Label::new(text));
let available_width =
ui.available_width() - ((TOP_BUTTON_WIDTH - TOP_SIDE_MARGIN) * 3.0);
ui.allocate_space(egui::vec2(available_width, HEADER_HEIGHT));
ui.add_enabled_ui(self.data.is_some(), |ui| {
if ui
.add_sized([TOP_BUTTON_WIDTH, 30.0], egui::Button::new("Save"))
.clicked()
{
self.save_atlas();
}
});
ui.add_enabled_ui(!self.dropped_files.is_empty(), |ui| {
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, 10.0));
if ui
.add_sized(
[TOP_BUTTON_WIDTH, 30.0],
egui::Button::new("Build atlas"),
)
.clicked()
{
self.build_atlas(ctx);
}
});
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, 10.0));
},
);
});
ctx.input(|i| {
if !i.raw.dropped_files.is_empty() {
self.dropped_files = i.raw.dropped_files.clone();
}
});
egui::CentralPanel::default().show(ctx, |ui| {
if let Some(error) = &self.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));
}
if !self.dropped_files.is_empty() {
CollapsingHeader::new("Settings")
.default_open(false)
.show(ui, |ui| {
ui.add(
egui::Slider::new(&mut self.config.max_width, 64..=4096).text("Max width"),
);
ui.add(
egui::Slider::new(&mut self.config.max_height, 64..=4096).text("Max height"),
);
ui.add(
egui::Slider::new(&mut self.config.border_padding, 0..=10).text("border padding"),
);
ui.add(
egui::Slider::new(&mut self.config.texture_padding, 0..=10).text("texture padding"),
);
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.config.texture_outlines, "Texture outlines")
.on_hover_text("True to draw the red line on the edge of the each frames. Useful for debugging.");
ui.checkbox(&mut self.config.trim, "Trim").on_hover_text("True to trim the empty pixels of the input images.");
});
ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui|{
if let Some(image) = &self.image {
ui.horizontal_top(|ui|{
let data = &self.data.clone().unwrap();
ui.label(format!("{} frames, size: {}x{}",data.frames.len(),data.size.0,data.size.1));
});
CollapsingHeader::new("Preview")
.default_open(true)
.show(ui, |ui| {
ui.add(image.clone());
});
}
ui.separator();
egui::ScrollArea::vertical().auto_shrink(false).show(ui, |ui| {
let mut index_to_remove : Option<usize> = None;
for (i, file) in self.dropped_files.iter().enumerate() {
let mut info = if let Some(path) = &file.path {
path.display().to_string()
} else if !file.name.is_empty() {
file.name.clone()
} else {
"???".to_owned()
};
if let Some(bytes) = &file.bytes {
info += &format!(" ({} bytes)", bytes.len());
}
ui.horizontal_top(|ui|{
if ui.button("x").clicked(){
index_to_remove = Some(i);
}
ui.add_space(10.0);
ui.label(info);
});
}
if let Some(index) = index_to_remove{
self.dropped_files.remove(index);
}
});
if ui.button("clear list").clicked() {
self.dropped_files.clear();
}
});
} else {
ui.vertical_centered_justified(|ui|{
ui.add_space(50.0);
ui.label(
RichText::new("Drop files here")
.heading()
.color(MY_ACCENT_COLOR32),
);
});
}
});
egui::TopBottomPanel::bottom("bottom_panel")
.frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| {
powered_by_egui_and_eframe(ui);
});
}
}
fn id_for_file(file: &DroppedFile) -> String {
let id;
#[cfg(not(target_arch = "wasm32"))]
{
let path = file.path.as_ref().unwrap().clone();
id = path
.file_name()
.unwrap()
.to_os_string()
.into_string()
.unwrap();
}
#[cfg(target_arch = "wasm32")]
{
id = file.name.clone();
}
id.replace(".png", "")
}
fn dynamic_image_from_file(file: &DroppedFile) -> DynamicImage {
#[cfg(target_arch = "wasm32")]
{
let bytes = file.bytes.as_ref().clone();
ImageImporter::import_from_memory(&bytes.unwrap())
.expect("Unable to import file. Run this example with --features=\"png\"")
}
#[cfg(not(target_arch = "wasm32"))]
{
let path = file.path.as_ref().unwrap().clone();
ImageImporter::import_from_file(&path)
.expect("Unable to import file. Run this example with --features=\"png\"")
}
}
fn powered_by_egui_and_eframe(ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.hyperlink_to(format!("Build: {}", GIT_HASH), env!("CARGO_PKG_HOMEPAGE"));
egui::warn_if_debug_build(ui);
ui.separator();
egui::widgets::global_dark_light_mode_buttons(ui);
ui.separator();
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("Made by ");
ui.hyperlink_to("Mev Lyshkin", "https://www.mevlyshkin.com/");
ui.label(". ");
ui.label("Powered by ");
ui.hyperlink_to("egui", "https://github.com/emilk/egui");
ui.label(" and ");
ui.hyperlink_to(
"eframe",
"https://github.com/emilk/egui/tree/master/crates/eframe",
);
ui.label(".");
});
}

4
src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
#![warn(clippy::all, rust_2018_idioms)]
mod app;
pub use app::TemplateApp;

40
src/main.rs Normal file
View File

@ -0,0 +1,40 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0])
.with_min_inner_size([300.0, 220.0]),
..Default::default()
};
eframe::run_native(
"rPack",
native_options,
Box::new(|cc| Box::new(rpack::TemplateApp::new(cc))),
)
}
// When compiling to web using trunk:
#[cfg(target_arch = "wasm32")]
fn main() {
// Redirect `log` message to `console.log` and friends:
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
let web_options = eframe::WebOptions::default();
wasm_bindgen_futures::spawn_local(async {
eframe::WebRunner::new()
.start(
"the_canvas_id", // hardcode it
web_options,
Box::new(|cc| Box::new(rpack::TemplateApp::new(cc))),
)
.await
.expect("failed to start eframe");
});
}

Binary file not shown.

Binary file not shown.