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(
73 fb_width as f64,
74 fb_height as f64,
75 ))
76 .with_resizable(config.resizable),
77 )
78 .map_err(|e| WindowError::WindowCreation(e.to_string()))?,
79 )
80 };
81
82 let mut pixels = {
83 let window_size = window.inner_size();
84 let surface_texture =
85 SurfaceTexture::new(window_size.width, window_size.height, Arc::clone(&window));
86 PixelsBuilder::new(fb_width, fb_height, surface_texture).build()?
87 };
88
89 let mut frame_buffer = G::Fb::new_screen(fb_width, fb_height);
90 let mut input = InputState::new();
91 let mut next_frame_time = Instant::now();
92
93 #[allow(deprecated)]
94 let res = event_loop.run(move |event, elwt| match event {
95 Event::WindowEvent { event, .. } => match event {
96 WindowEvent::CloseRequested => {
97 elwt.exit();
98 }
99 WindowEvent::RedrawRequested => {
100 game.draw(&mut frame_buffer);
101 frame_buffer.present_into(pixels.frame_mut());
102 if let Err(err) = pixels.render() {
103 log_error("pixels.render", err);
104 elwt.exit();
105 }
106 }
107 WindowEvent::Resized(size) => {
108 if size.width > 0 && size.height > 0 {
109 if let Err(err) = pixels.resize_surface(size.width, size.height) {
110 log_error("pixels.resize_surface", err);
111 elwt.exit();
112 }
113 }
114 }
115 WindowEvent::KeyboardInput {
116 event: key_event, ..
117 } => {
118 if let PhysicalKey::Code(keycode) = key_event.physical_key {
119 let pressed = key_event.state == ElementState::Pressed;
120 if pressed && keycode == KeyCode::Escape {
121 elwt.exit();
122 return;
123 }
124 input.set_from_keycode(keycode, pressed);
125 }
126 }
127 _ => {}
128 },
129 Event::AboutToWait => {
130 let now = Instant::now();
131 if now >= next_frame_time {
132 game.update(&input);
133 input.begin_frame();
134 if game.should_exit() {
135 elwt.exit();
136 return;
137 }
138 window.request_redraw();
139 next_frame_time += FRAME_DURATION;
140 if next_frame_time < now {
141 next_frame_time = now + FRAME_DURATION;
142 }
143 }
144 let sleep_duration = next_frame_time.saturating_duration_since(Instant::now());
145 if !sleep_duration.is_zero() {
146 std::thread::sleep(sleep_duration);
147 }
148 }
149 _ => {}
150 });
151
152 res.map_err(|e| WindowError::EventLoop(e.to_string()))
153}
154
155fn log_error<E: std::error::Error + 'static>(method_name: &str, err: E) {
156 error!("{method_name}() failed: {err}");
157 for source in err.sources().skip(1) {
158 error!(" Caused by: {source}");
159 }
160}