use std::path::PathBuf;
use fastpack_core::types::{
atlas::AtlasFrame,
config::{PackerConfig, Project, SourceSpec},
};
use rust_i18n::t;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Info,
Warn,
Error,
}
pub struct LogEntry {
pub level: LogLevel,
pub message: String,
pub time: String,
}
fn format_time() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let h = (secs % 86400) / 3600;
let m = (secs % 3600) / 60;
let s = secs % 60;
format!("{h:02}:{m:02}:{s:02}")
}
impl LogEntry {
pub fn info(msg: impl Into<String>) -> Self {
Self {
level: LogLevel::Info,
message: msg.into(),
time: format_time(),
}
}
pub fn warn(msg: impl Into<String>) -> Self {
Self {
level: LogLevel::Warn,
message: msg.into(),
time: format_time(),
}
}
pub fn error(msg: impl Into<String>) -> Self {
Self {
level: LogLevel::Error,
message: msg.into(),
time: format_time(),
}
}
}
#[derive(Clone)]
pub struct FrameInfo {
pub id: String,
pub sheet_idx: usize,
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
pub alias_of: Option<String>,
}
pub struct SheetData {
pub rgba: Vec<u8>,
pub width: u32,
pub height: u32,
pub frames: Vec<FrameInfo>,
pub atlas_frames: Vec<AtlasFrame>,
}
#[derive(Default)]
pub struct PendingActions {
pub pack: bool,
pub export: bool,
pub new_project: bool,
pub open_project: bool,
pub save_project: bool,
pub save_project_as: bool,
pub add_source: bool,
pub open_prefs: bool,
}
pub struct AnimPreviewState {
pub open: bool,
pub playing: bool,
pub fps: f32,
pub looping: bool,
pub current_frame: usize,
pub elapsed_secs: f64,
pub zoom: f32,
pub pan: [f32; 2],
}
impl Default for AnimPreviewState {
fn default() -> Self {
Self {
open: false,
playing: false,
fps: 24.0,
looping: true,
current_frame: 0,
elapsed_secs: 0.0,
zoom: 1.0,
pan: [0.0, 0.0],
}
}
}
pub struct AppState {
pub project: Project,
pub project_path: Option<PathBuf>,
pub dirty: bool,
pub log: Vec<LogEntry>,
pub sheets: Vec<SheetData>,
pub frames: Vec<FrameInfo>,
pub sprite_count: usize,
pub alias_count: usize,
pub overflow_count: usize,
pub packing: bool,
pub selected_frames: Vec<usize>,
pub anchor_frame: Option<usize>,
pub anim_preview: AnimPreviewState,
pub atlas_pan: [f32; 2],
pub atlas_zoom: f32,
pub dark_mode: bool,
pub pending: PendingActions,
}
impl Default for AppState {
fn default() -> Self {
Self {
project: Project::default(),
project_path: None,
dirty: false,
log: Vec::new(),
sheets: Vec::new(),
frames: Vec::new(),
sprite_count: 0,
alias_count: 0,
overflow_count: 0,
packing: false,
selected_frames: Vec::new(),
anchor_frame: None,
anim_preview: AnimPreviewState::default(),
atlas_pan: [0.0, 0.0],
atlas_zoom: 1.0,
dark_mode: true,
pending: PendingActions::default(),
}
}
}
impl AppState {
pub fn log_info(&mut self, msg: impl Into<String>) {
self.log.push(LogEntry::info(msg));
}
pub fn log_warn(&mut self, msg: impl Into<String>) {
self.log.push(LogEntry::warn(msg));
}
pub fn log_error(&mut self, msg: impl Into<String>) {
self.log.push(LogEntry::error(msg));
}
pub fn window_title(&self) -> String {
let name = self
.project_path
.as_ref()
.and_then(|p| p.file_stem())
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| t!("state.untitled").to_string());
if self.dirty {
format!("FastPack — {}*", name)
} else {
format!("FastPack — {}", name)
}
}
pub fn new_project(&mut self, default_config: PackerConfig) {
let dark_mode = self.dark_mode;
*self = AppState::default();
self.dark_mode = dark_mode;
self.project.config = default_config;
self.log.push(LogEntry::info(t!("state.new_project")));
}
pub fn add_source_path(&mut self, path: PathBuf) {
let display = path.display().to_string();
self.project.sources.push(SourceSpec {
path,
filter: "**/*.png".to_string(),
});
self.dirty = true;
self.pending.pack = true;
self.log_info(t!("state.added_source", path = display));
}
pub fn remove_source(&mut self, index: usize) {
if index < self.project.sources.len() {
let removed = self.project.sources.remove(index);
self.dirty = true;
self.log_info(t!(
"state.removed_source",
path = removed.path.display().to_string()
));
}
}
}