dotzuki_renderer/
window.rs1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use error_iter::ErrorIter as _;
5use log::error;
6use pixels::{PixelsBuilder, SurfaceTexture};
7use winit::dpi::LogicalSize;
8use winit::event::{ElementState, Event, WindowEvent};
9use winit::event_loop::EventLoop;
10use winit::keyboard::{KeyCode, PhysicalKey};
11use winit::window::Window;
12
13use crate::input::InputState;
14use crate::FbSurface;
15
16const FRAME_DURATION: Duration = Duration::from_nanos(16_742_706); pub struct GameWindowConfig {
20 pub title: String,
21 pub scale: u32,
22 pub resizable: bool,
23 pub width: u32,
26 pub height: u32,
29}
30
31pub trait GameLoop {
32 type Fb: FbSurface;
36
37 fn update(&mut self, input: &InputState);
38 fn draw(&mut self, frame_buffer: &mut Self::Fb);
39 fn should_exit(&self) -> bool {
40 false
41 }
42}
43
44#[derive(Debug, thiserror::Error)]
45pub enum WindowError {
46 #[error("Failed to create event loop: {0}")]
47 EventLoop(String),
48 #[error("Failed to create window: {0}")]
49 WindowCreation(String),
50 #[error("Failed to create pixel buffer: {0}")]
51 PixelBuffer(#[from] pixels::Error),
52}
53
54pub fn run<G: GameLoop + 'static>(
55 config: GameWindowConfig,
56 mut game: G,
57) -> Result<(), WindowError> {
58 let event_loop = EventLoop::new().map_err(|e| WindowError::EventLoop(e.to_string()))?;
59 let (fb_width, fb_height) = (config.width, config.height);
60 let window = {
61 let size = LogicalSize::new(
62 (fb_width * config.scale) as f64,
63 (fb_height * config.scale) as f64,
64 );
65 #[allow(deprecated)]
66 Arc::new(
67 event_loop
68 .create_window(
69 Window::default_attributes()
70 .with_title(&config.title)
71 .with_inner_size(size)
72 .with_min_inner_size(LogicalSize::new(fb_width as f64, fb_height as f64))
73 .with_resizable(config.resizable),
74 )
75 .map_err(|e| WindowError::WindowCreation(e.to_string()))?,
76 )
77 };
78
79 let mut pixels = {
80 let window_size = window.inner_size();
81 let surface_texture =
82 SurfaceTexture::new(window_size.width, window_size.height, Arc::clone(&window));
83 PixelsBuilder::new(fb_width, fb_height, surface_texture).build()?
84 };
85
86 let mut frame_buffer = G::Fb::new_screen(fb_width, fb_height);
87 let mut input = InputState::new();
88 let mut next_frame_time = Instant::now();
89
90 #[allow(deprecated)]
91 let res = event_loop.run(move |event, elwt| match event {
92 Event::WindowEvent { event, .. } => match event {
93 WindowEvent::CloseRequested => {
94 elwt.exit();
95 }
96 WindowEvent::RedrawRequested => {
97 game.draw(&mut frame_buffer);
98 frame_buffer.present_into(pixels.frame_mut());
99 if let Err(err) = pixels.render() {
100 log_error("pixels.render", err);
101 elwt.exit();
102 }
103 }
104 WindowEvent::Resized(size) => {
105 if size.width > 0 && size.height > 0 {
106 if let Err(err) = pixels.resize_surface(size.width, size.height) {
107 log_error("pixels.resize_surface", err);
108 elwt.exit();
109 }
110 }
111 }
112 WindowEvent::KeyboardInput {
113 event: key_event, ..
114 } => {
115 if let PhysicalKey::Code(keycode) = key_event.physical_key {
116 let pressed = key_event.state == ElementState::Pressed;
117 if pressed && keycode == KeyCode::Escape {
118 elwt.exit();
119 return;
120 }
121 input.set_from_keycode(keycode, pressed);
122 }
123 }
124 _ => {}
125 },
126 Event::AboutToWait => {
127 let now = Instant::now();
128 if now >= next_frame_time {
129 game.update(&input);
130 input.begin_frame();
131 if game.should_exit() {
132 elwt.exit();
133 return;
134 }
135 window.request_redraw();
136 next_frame_time += FRAME_DURATION;
137 if next_frame_time < now {
138 next_frame_time = now + FRAME_DURATION;
139 }
140 }
141 let sleep_duration = next_frame_time.saturating_duration_since(Instant::now());
142 if !sleep_duration.is_zero() {
143 std::thread::sleep(sleep_duration);
144 }
145 }
146 _ => {}
147 });
148
149 res.map_err(|e| WindowError::EventLoop(e.to_string()))
150}
151
152fn log_error<E: core::error::Error + 'static>(method_name: &str, err: E) {
153 error!("{method_name}() failed: {err}");
154 for source in err.sources().skip(1) {
155 error!(" Caused by: {source}");
156 }
157}