1use std::path::PathBuf;
2
3use fastpack_core::types::{
4 atlas::AtlasFrame,
5 config::{PackerConfig, Project, SourceSpec},
6};
7use rust_i18n::t;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum LogLevel {
12 Info,
14 Warn,
16 Error,
18}
19
20pub struct LogEntry {
22 pub level: LogLevel,
24 pub message: String,
26 pub time: String,
28}
29
30fn format_time() -> String {
31 use std::time::{SystemTime, UNIX_EPOCH};
32 let secs = SystemTime::now()
33 .duration_since(UNIX_EPOCH)
34 .unwrap_or_default()
35 .as_secs();
36 let h = (secs % 86400) / 3600;
37 let m = (secs % 3600) / 60;
38 let s = secs % 60;
39 format!("{h:02}:{m:02}:{s:02}")
40}
41
42impl LogEntry {
43 pub fn info(msg: impl Into<String>) -> Self {
45 Self {
46 level: LogLevel::Info,
47 message: msg.into(),
48 time: format_time(),
49 }
50 }
51 pub fn warn(msg: impl Into<String>) -> Self {
53 Self {
54 level: LogLevel::Warn,
55 message: msg.into(),
56 time: format_time(),
57 }
58 }
59 pub fn error(msg: impl Into<String>) -> Self {
61 Self {
62 level: LogLevel::Error,
63 message: msg.into(),
64 time: format_time(),
65 }
66 }
67}
68
69#[derive(Clone)]
71pub struct FrameInfo {
72 pub id: String,
74 pub sheet_idx: usize,
76 pub x: u32,
78 pub y: u32,
80 pub w: u32,
82 pub h: u32,
84 pub alias_of: Option<String>,
86}
87
88pub struct SheetData {
90 pub rgba: Vec<u8>,
92 pub width: u32,
94 pub height: u32,
96 pub frames: Vec<FrameInfo>,
98 pub atlas_frames: Vec<AtlasFrame>,
100}
101
102#[derive(Default)]
104pub struct PendingActions {
105 pub pack: bool,
107 pub export: bool,
109 pub new_project: bool,
111 pub open_project: bool,
113 pub save_project: bool,
115 pub save_project_as: bool,
117 pub add_source: bool,
119 pub open_prefs: bool,
121 pub rebuild_watcher: bool,
123}
124
125pub struct AnimPreviewState {
127 pub open: bool,
129 pub playing: bool,
131 pub fps: f32,
133 pub looping: bool,
135 pub current_frame: usize,
137 pub elapsed_secs: f64,
139 pub zoom: f32,
141 pub pan: [f32; 2],
143}
144
145impl Default for AnimPreviewState {
146 fn default() -> Self {
147 Self {
148 open: false,
149 playing: false,
150 fps: 24.0,
151 looping: true,
152 current_frame: 0,
153 elapsed_secs: 0.0,
154 zoom: 1.0,
155 pan: [0.0, 0.0],
156 }
157 }
158}
159
160pub struct AppState {
162 pub project: Project,
164 pub project_path: Option<PathBuf>,
166 pub dirty: bool,
168
169 pub log: Vec<LogEntry>,
171
172 pub sheets: Vec<SheetData>,
174 pub frames: Vec<FrameInfo>,
176 pub sprite_count: usize,
178 pub alias_count: usize,
180 pub overflow_count: usize,
182
183 pub packing: bool,
185
186 pub selected_frames: Vec<usize>,
188 pub anchor_frame: Option<usize>,
190 pub anim_preview: AnimPreviewState,
192
193 pub atlas_pan: [f32; 2],
195 pub atlas_zoom: f32,
197
198 pub dark_mode: bool,
200
201 pub pending: PendingActions,
203}
204
205impl Default for AppState {
206 fn default() -> Self {
207 Self {
208 project: Project::default(),
209 project_path: None,
210 dirty: false,
211 log: Vec::new(),
212 sheets: Vec::new(),
213 frames: Vec::new(),
214 sprite_count: 0,
215 alias_count: 0,
216 overflow_count: 0,
217 packing: false,
218 selected_frames: Vec::new(),
219 anchor_frame: None,
220 anim_preview: AnimPreviewState::default(),
221 atlas_pan: [0.0, 0.0],
222 atlas_zoom: 1.0,
223 dark_mode: true,
224 pending: PendingActions::default(),
225 }
226 }
227}
228
229impl AppState {
230 pub fn log_info(&mut self, msg: impl Into<String>) {
232 self.log.push(LogEntry::info(msg));
233 }
234 pub fn log_warn(&mut self, msg: impl Into<String>) {
236 self.log.push(LogEntry::warn(msg));
237 }
238 pub fn log_error(&mut self, msg: impl Into<String>) {
240 self.log.push(LogEntry::error(msg));
241 }
242
243 pub fn window_title(&self) -> String {
245 let name = self
246 .project_path
247 .as_ref()
248 .and_then(|p| p.file_stem())
249 .map(|s| s.to_string_lossy().into_owned())
250 .unwrap_or_else(|| t!("state.untitled").to_string());
251 if self.dirty {
252 format!("FastPack — {}*", name)
253 } else {
254 format!("FastPack — {}", name)
255 }
256 }
257
258 pub fn new_project(&mut self, default_config: PackerConfig) {
260 let dark_mode = self.dark_mode;
261 *self = AppState::default();
262 self.dark_mode = dark_mode;
263 self.project.config = default_config;
264 self.log.push(LogEntry::info(t!("state.new_project")));
265 }
266
267 pub fn add_source_path(&mut self, path: PathBuf) {
269 let path = std::fs::canonicalize(&path).unwrap_or(path);
270 if self.project.sources.iter().any(|s| {
272 let stored = std::fs::canonicalize(&s.path).unwrap_or_else(|_| s.path.clone());
273 path.starts_with(&stored)
274 }) {
275 return;
276 }
277 let display = path.display().to_string();
278 self.project.sources.push(SourceSpec {
279 path,
280 filter: "**/*.png".to_string(),
281 });
282 self.dirty = true;
283 self.pending.pack = true;
284 self.pending.rebuild_watcher = true;
285 self.log_info(t!("state.added_source", path = display));
286 }
287
288 pub fn remove_source(&mut self, index: usize) {
290 if index < self.project.sources.len() {
291 let removed = self.project.sources.remove(index);
292 self.dirty = true;
293 self.pending.pack = true;
294 self.pending.rebuild_watcher = true;
295 self.log_info(t!(
296 "state.removed_source",
297 path = removed.path.display().to_string()
298 ));
299 }
300 }
301}