pyxel/
system.rs

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}