1use cfg_if::cfg_if;
2use pyxel_platform::Event;
3
4use crate::image::{Color, Image, SharedImage};
5use crate::keys::{
6 Key, GAMEPAD1_BUTTON_A, GAMEPAD1_BUTTON_B, GAMEPAD1_BUTTON_DPAD_DOWN,
7 GAMEPAD1_BUTTON_DPAD_LEFT, GAMEPAD1_BUTTON_DPAD_RIGHT, GAMEPAD1_BUTTON_DPAD_UP,
8 GAMEPAD1_BUTTON_X, GAMEPAD1_BUTTON_Y, KEY_0, KEY_1, KEY_2, KEY_3, KEY_8, KEY_9, KEY_ALT,
9 KEY_RETURN, KEY_SHIFT,
10};
11use crate::profiler::Profiler;
12use crate::pyxel::Pyxel;
13use crate::settings::{MAX_ELAPSED_MS, NUM_MEASURE_FRAMES, NUM_SCREEN_TYPES};
14use crate::utils;
15use crate::watch_info::WatchInfo;
16
17pub trait PyxelCallback {
18 fn update(&mut self, pyxel: &mut Pyxel);
19 fn draw(&mut self, pyxel: &mut Pyxel);
20}
21
22pub struct System {
23 one_frame_ms: f64,
24 next_update_ms: f64,
25 quit_key: Key,
26 paused: bool,
27 fps_profiler: Profiler,
28 update_profiler: Profiler,
29 draw_profiler: Profiler,
30 perf_monitor_enabled: bool,
31 integer_scale_enabled: bool,
32 watch_info: WatchInfo,
33 pub screen_x: i32,
34 pub screen_y: i32,
35 pub screen_scale: f64,
36 pub screen_mode: u32,
37}
38
39impl System {
40 pub fn new(fps: u32, quit_key: Key) -> Self {
41 Self {
42 one_frame_ms: 1000.0 / fps as f64,
43 next_update_ms: 0.0,
44 quit_key,
45 paused: false,
46 fps_profiler: Profiler::new(NUM_MEASURE_FRAMES),
47 update_profiler: Profiler::new(NUM_MEASURE_FRAMES),
48 draw_profiler: Profiler::new(NUM_MEASURE_FRAMES),
49 perf_monitor_enabled: false,
50 integer_scale_enabled: false,
51 watch_info: WatchInfo::new(),
52 screen_x: 0,
53 screen_y: 0,
54 screen_scale: 0.0,
55 screen_mode: 0,
56 }
57 }
58}
59
60impl Pyxel {
61 pub fn run<T: PyxelCallback>(&mut self, mut callback: T) {
62 pyxel_platform::run(move || {
63 self.process_frame(&mut callback);
64 });
65 }
66
67 pub fn show(&mut self) {
68 struct App {
69 image: SharedImage,
70 }
71
72 impl PyxelCallback for App {
73 fn update(&mut self, _pyxel: &mut Pyxel) {}
74 fn draw(&mut self, pyxel: &mut Pyxel) {
75 pyxel.screen.lock().blt(
76 0.0,
77 0.0,
78 self.image.clone(),
79 0.0,
80 0.0,
81 pyxel.width as f64,
82 pyxel.height as f64,
83 None,
84 None,
85 None,
86 );
87 }
88 }
89
90 let image = Image::new(self.width, self.height);
91 image.lock().blt(
92 0.0,
93 0.0,
94 self.screen.clone(),
95 0.0,
96 0.0,
97 self.width as f64,
98 self.height as f64,
99 None,
100 None,
101 None,
102 );
103
104 self.run(App { image });
105 }
106
107 pub fn flip(&mut self) {
108 cfg_if! {
109 if #[cfg(target_os = "emscripten")] {
110 panic!("flip is not supported for Web");
111 } else {
112 self.process_frame_for_flip();
113 }
114 }
115 }
116
117 pub fn quit(&self) {
118 pyxel_platform::quit();
119 }
120
121 pub fn title(&self, title: &str) {
122 pyxel_platform::set_window_title(title);
123 }
124
125 pub fn icon(&self, data_str: &[&str], scale: u32, transparent: Option<Color>) {
126 let colors = self.colors.lock();
127 let width = utils::simplify_string(data_str[0]).len() as u32;
128 let height = data_str.len() as u32;
129 let image = Image::new(width, height);
130 let mut image = image.lock();
131 image.set(0, 0, data_str);
132 let image_data = &image.canvas.data;
133 let scaled_width = width * scale;
134 let scaled_height = height * scale;
135 let mut rgba_data: Vec<u8> =
136 Vec::with_capacity((scaled_width * scaled_height * 4) as usize);
137
138 for y in 0..height {
139 for _sy in 0..scale {
140 for x in 0..width {
141 let color = image_data[(width * y + x) as usize];
142 let rgb = colors[color as usize];
143 let r = (rgb >> 16) as u8;
144 let g = (rgb >> 8) as u8;
145 let b = rgb as u8;
146 let a = if Some(color) == transparent {
147 0x00
148 } else {
149 0xff
150 };
151 for _sx in 0..scale {
152 rgba_data.push(r);
153 rgba_data.push(g);
154 rgba_data.push(b);
155 rgba_data.push(a);
156 }
157 }
158 }
159 }
160
161 pyxel_platform::set_window_icon(scaled_width, scaled_height, &rgba_data);
162 }
163
164 pub fn perf_monitor(&mut self, enabled: bool) {
165 self.system.perf_monitor_enabled = enabled;
166 }
167
168 pub fn integer_scale(&mut self, enabled: bool) {
169 self.system.integer_scale_enabled = enabled;
170 }
171
172 pub fn screen_mode(&mut self, screen_mode: u32) {
173 self.system.screen_mode = screen_mode;
174 }
175
176 pub fn fullscreen(&self, enabled: bool) {
177 pyxel_platform::set_fullscreen(enabled);
178 }
179
180 fn process_events(&mut self) {
181 self.start_input_frame();
182 let events = pyxel_platform::poll_events();
183
184 for event in events {
185 match event {
186 Event::WindowShown => {
187 self.system.paused = false;
188 pyxel_platform::set_audio_enabled(true);
189 }
190 Event::WindowHidden => {
191 self.system.paused = true;
192 pyxel_platform::set_audio_enabled(false);
193 }
194 Event::KeyPressed { key } => {
195 self.press_key(key);
196 }
197 Event::KeyReleased { key } => {
198 self.release_key(key);
199 }
200 Event::KeyValueChanged { key, value } => {
201 self.change_key_value(key, value);
202 }
203 Event::TextInput { text } => {
204 self.add_input_text(&text);
205 }
206 Event::FileDropped { filename } => {
207 self.add_dropped_file(&filename);
208 }
209 Event::Quit => {
210 pyxel_platform::quit();
211 }
212 }
213 }
214 }
215
216 fn check_special_input(&mut self) {
217 if self.btnp(self.system.quit_key, None, None) {
218 self.reset_key(self.system.quit_key);
219 self.quit();
220 } else if self.btn(KEY_ALT) {
221 if self.btn(KEY_SHIFT) {
222 if self.btnp(KEY_0, None, None) {
223 self.reset_key(KEY_0);
224 self.dump_palette();
225 } else {
226 for i in 0..=8 {
227 if self.btnp(KEY_1 + i, None, None) {
228 self.reset_key(KEY_1 + i);
229 self.dump_image_bank(i);
230 }
231 }
232 }
233 } else if self.btnp(KEY_1, None, None) {
234 self.reset_key(KEY_1);
235 self.screenshot(None);
236 } else if self.btnp(KEY_2, None, None) {
237 self.reset_key(KEY_2);
238 self.reset_screencast();
239 } else if self.btnp(KEY_3, None, None) {
240 self.reset_key(KEY_3);
241 self.screencast(None);
242 } else if self.btnp(KEY_8, None, None) {
243 self.reset_key(KEY_8);
244 self.integer_scale(!self.system.integer_scale_enabled);
245 } else if self.btnp(KEY_9, None, None) {
246 self.reset_key(KEY_9);
247 self.screen_mode((self.system.screen_mode + 1) % NUM_SCREEN_TYPES);
248 } else if self.btnp(KEY_0, None, None) {
249 self.reset_key(KEY_0);
250 self.perf_monitor(!self.system.perf_monitor_enabled);
251 } else if self.btnp(KEY_RETURN, None, None) {
252 self.reset_key(KEY_RETURN);
253 self.fullscreen(!pyxel_platform::is_fullscreen());
254 }
255 } else if self.btn(GAMEPAD1_BUTTON_A)
256 && self.btn(GAMEPAD1_BUTTON_B)
257 && self.btn(GAMEPAD1_BUTTON_X)
258 && self.btn(GAMEPAD1_BUTTON_Y)
259 {
260 if self.btnp(GAMEPAD1_BUTTON_DPAD_LEFT, None, None) {
261 self.reset_key(GAMEPAD1_BUTTON_DPAD_UP);
262 self.integer_scale(!self.system.integer_scale_enabled);
263 } else if self.btnp(GAMEPAD1_BUTTON_DPAD_RIGHT, None, None) {
264 self.reset_key(GAMEPAD1_BUTTON_DPAD_DOWN);
265 self.screen_mode((self.system.screen_mode + 1) % NUM_SCREEN_TYPES);
266 } else if self.btnp(GAMEPAD1_BUTTON_DPAD_UP, None, None) {
267 self.reset_key(GAMEPAD1_BUTTON_DPAD_LEFT);
268 self.perf_monitor(!self.system.perf_monitor_enabled);
269 } else if self.btnp(GAMEPAD1_BUTTON_DPAD_DOWN, None, None) {
270 self.reset_key(GAMEPAD1_BUTTON_DPAD_RIGHT);
271 self.fullscreen(!pyxel_platform::is_fullscreen());
272 }
273 }
274 }
275
276 fn update_screen_params(&mut self) {
277 let (window_width, window_height) = pyxel_platform::window_size();
278
279 if self.system.integer_scale_enabled {
280 self.system.screen_scale = f64::max(
281 f64::min(
282 (window_width as f64 / self.width as f64) as i32 as f64,
283 (window_height as f64 / self.height as f64) as i32 as f64,
284 ),
285 1.0,
286 );
287 } else {
288 self.system.screen_scale = f64::max(
289 f64::min(
290 window_width as f64 / self.width as f64,
291 window_height as f64 / self.height as f64,
292 ),
293 1.0,
294 );
295 }
296
297 self.system.screen_x =
298 (window_width as i32 - (self.width as f64 * self.system.screen_scale) as i32) / 2;
299 self.system.screen_y =
300 (window_height as i32 - (self.height as f64 * self.system.screen_scale) as i32) / 2;
301 }
302
303 fn update_frame(&mut self, callback: Option<&mut dyn PyxelCallback>) {
304 self.system
305 .update_profiler
306 .start(pyxel_platform::elapsed_time());
307
308 self.process_events();
309
310 if self.system.paused {
311 return;
312 }
313
314 self.check_special_input();
315
316 if let Some(callback) = callback {
317 callback.update(self);
318 self.system
319 .update_profiler
320 .end(pyxel_platform::elapsed_time());
321 }
322 }
323
324 fn draw_perf_monitor(&self) {
325 if !self.system.perf_monitor_enabled {
326 return;
327 }
328
329 let mut screen = self.screen.lock();
330 let clip_rect = screen.canvas.clip_rect;
331 let camera_x = screen.canvas.camera_x;
332 let camera_y = screen.canvas.camera_y;
333 let palette1 = screen.palette[1];
334 let palette2 = screen.palette[2];
335 let alpha = screen.canvas.alpha;
336
337 screen.clip0();
338 screen.camera0();
339 screen.pal(1, 1);
340 screen.pal(2, 9);
341 screen.dither(1.0);
342
343 let fps = format!("{:.*}", 2, self.system.fps_profiler.average_fps());
344 screen.text(1.0, 0.0, &fps, 1, None);
345 screen.text(0.0, 0.0, &fps, 2, None);
346
347 let update_time = format!("{:.*}", 2, self.system.update_profiler.average_time());
348 screen.text(1.0, 6.0, &update_time, 1, None);
349 screen.text(0.0, 6.0, &update_time, 2, None);
350
351 let draw_time = format!("{:.*}", 2, self.system.draw_profiler.average_time());
352 screen.text(1.0, 12.0, &draw_time, 1, None);
353 screen.text(0.0, 12.0, &draw_time, 2, None);
354
355 screen.canvas.clip_rect = clip_rect;
356 screen.canvas.camera_x = camera_x;
357 screen.canvas.camera_y = camera_y;
358 screen.pal(1, palette1);
359 screen.pal(2, palette2);
360 screen.dither(alpha);
361 }
362
363 fn draw_cursor(&self) {
364 let x = self.mouse_x;
365 let y = self.mouse_y;
366
367 pyxel_platform::set_mouse_visible(
368 x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32,
369 );
370
371 if !self.is_mouse_visible() {
372 return;
373 }
374
375 let width = self.cursor.lock().width() as i32;
376 let height = self.cursor.lock().height() as i32;
377
378 if x <= -width || x >= self.width as i32 || y <= -height || y >= self.height as i32 {
379 return;
380 }
381
382 let mut screen = self.screen.lock();
383 let clip_rect = screen.canvas.clip_rect;
384 let camera_x = screen.canvas.camera_x;
385 let camera_y = screen.canvas.camera_y;
386 let palette = screen.palette;
387
388 screen.clip0();
389 screen.camera0();
390 screen.blt(
391 x as f64,
392 y as f64,
393 self.cursor.clone(),
394 0.0,
395 0.0,
396 width as f64,
397 height as f64,
398 Some(0),
399 None,
400 None,
401 );
402
403 screen.canvas.clip_rect = clip_rect;
404 screen.canvas.camera_x = camera_x;
405 screen.canvas.camera_y = camera_y;
406 screen.palette = palette;
407 }
408
409 fn draw_frame(&mut self, callback: Option<&mut dyn PyxelCallback>) {
410 if self.system.paused {
411 return;
412 }
413
414 self.system
415 .draw_profiler
416 .start(pyxel_platform::elapsed_time());
417
418 if let Some(callback) = callback {
419 callback.draw(self);
420 }
421
422 self.system.watch_info.update();
423 self.draw_perf_monitor();
424 self.draw_cursor();
425 self.render_screen();
426 self.capture_screen();
427
428 self.system
429 .draw_profiler
430 .end(pyxel_platform::elapsed_time());
431 }
432
433 fn process_frame(&mut self, callback: &mut dyn PyxelCallback) {
434 let tick_count = pyxel_platform::elapsed_time();
435 let elapsed_ms = tick_count as f64 - self.system.next_update_ms;
436
437 if elapsed_ms < 0.0 {
438 return;
439 }
440
441 if self.frame_count == 0 {
442 self.system.next_update_ms = tick_count as f64 + self.system.one_frame_ms;
443 } else {
444 self.system.fps_profiler.end(tick_count);
445 self.system.fps_profiler.start(tick_count);
446
447 let update_count: u32;
448
449 if elapsed_ms > MAX_ELAPSED_MS as f64 {
450 update_count = 1;
451 self.system.next_update_ms =
452 pyxel_platform::elapsed_time() as f64 + self.system.one_frame_ms;
453 } else {
454 update_count = (elapsed_ms / self.system.one_frame_ms) as u32 + 1;
455 self.system.next_update_ms += self.system.one_frame_ms * update_count as f64;
456 }
457
458 for _ in 1..update_count {
459 self.update_frame(Some(callback));
460 self.frame_count += 1;
461 }
462 }
463
464 self.update_screen_params();
465 self.update_frame(Some(callback));
466 self.draw_frame(Some(callback));
467 self.frame_count += 1;
468 }
469
470 #[cfg(not(target_os = "emscripten"))]
471 fn process_frame_for_flip(&mut self) {
472 self.system
473 .update_profiler
474 .end(pyxel_platform::elapsed_time());
475
476 self.update_screen_params();
477 self.draw_frame(None);
478 self.frame_count += 1;
479
480 let mut tick_count;
481 let mut elapsed_ms;
482
483 loop {
484 tick_count = pyxel_platform::elapsed_time();
485 elapsed_ms = tick_count as f64 - self.system.next_update_ms;
486
487 let wait_ms = self.system.next_update_ms - pyxel_platform::elapsed_time() as f64;
488
489 if wait_ms > 0.0 {
490 pyxel_platform::sleep((wait_ms / 2.0) as u32);
491 } else {
492 break;
493 }
494 }
495
496 self.system.fps_profiler.end(tick_count);
497 self.system.fps_profiler.start(tick_count);
498
499 if elapsed_ms > MAX_ELAPSED_MS as f64 {
500 self.system.next_update_ms =
501 pyxel_platform::elapsed_time() as f64 + self.system.one_frame_ms;
502 } else {
503 self.system.next_update_ms += self.system.one_frame_ms;
504 }
505
506 self.update_frame(None);
507 }
508}