chinchilib/
lib.rs

1use std::usize;
2
3pub use pixels;
4use pixels::{Pixels, SurfaceTexture};
5pub use rgb;
6pub use winit;
7use winit::window::{Window, WindowId};
8
9/// Mapping for the keys that are recognized. They are centered an AZERTY keyboard's essential keys
10/// needed for games.
11/// TODO: for keys that correspondond to a character, use a unique enum variant that contains a
12/// SmolStr.
13#[derive(Eq, Hash, PartialEq)]
14pub enum Key {
15    KeyA,
16    KeyZ,
17    KeyE,
18    KeyQ,
19    KeyS,
20    KeyD,
21    KeyW,
22    KeyX,
23    KeyC,
24    Up,
25    Down,
26    Left,
27    Right,
28}
29
30impl std::convert::TryFrom<&winit::keyboard::Key> for Key {
31    type Error = ();
32    fn try_from(value: &winit::keyboard::Key) -> Result<Self, ()> {
33        use winit::keyboard::{Key as WKey, NamedKey as WNamedKey};
34        match value {
35            WKey::Named(WNamedKey::ArrowLeft) => Some(Key::Left),
36            WKey::Named(WNamedKey::ArrowRight) => Some(Key::Right),
37            WKey::Named(WNamedKey::ArrowUp) => Some(Key::Up),
38            WKey::Named(WNamedKey::ArrowDown) => Some(Key::Down),
39            WKey::Character(name) if name == "q" => Some(Key::KeyQ),
40            WKey::Character(name) if name == "d" => Some(Key::KeyD),
41            WKey::Character(name) if name == "z" => Some(Key::KeyZ),
42            WKey::Character(name) if name == "s" => Some(Key::KeyS),
43            WKey::Character(name) if name == "a" => Some(Key::KeyA),
44            WKey::Character(name) if name == "e" => Some(Key::KeyE),
45            WKey::Character(name) if name == "w" => Some(Key::KeyW),
46            WKey::Character(name) if name == "x" => Some(Key::KeyX),
47            WKey::Character(name) if name == "c" => Some(Key::KeyC),
48            _ => None,
49        }
50        .ok_or(())
51    }
52}
53
54/// Everyting about the window. Pixels and Window are options because they
55/// are constructed on "resume" and cannot be construted earlier
56pub struct WinitHandler {
57    winfbx: Option<WinFbx>,
58    width: usize,
59    height: usize,
60    last_frame: std::time::Instant,
61    tick: std::time::Duration,
62    /// Set to true if your app has something special to do at every tick even if there are no user
63    /// events. This can be used if you have physics or an animation to run. Defaults to false to
64    /// preserve performance.
65    always_tick: bool,
66    app: Option<Box<dyn GfxApp>>,
67    cursor_pos: (f64, f64),
68}
69
70fn hz_to_nanosec_period(hz: u16) -> u64 {
71    let nano_period = 1.0 / hz as f64 * 1_000_000_000.0;
72    nano_period as u64
73}
74
75#[cfg(test)]
76mod test {
77    #[test]
78    fn hz_to_nanosec_period() {
79        assert_eq!(super::hz_to_nanosec_period(60), 16_666_666);
80        assert_eq!(super::hz_to_nanosec_period(1), 1_000_000_000);
81    }
82}
83
84impl WinitHandler {
85    /// Create a new handler with an app, a window size and a desired tick rate. Run app with
86    /// `.run()`
87    pub fn new(app: Box<dyn GfxApp>, size: (usize, usize), tick_per_second: u16) -> Self {
88        let nsec_period = hz_to_nanosec_period(tick_per_second);
89        Self {
90            winfbx: None,
91            width: size.0,
92            height: size.1,
93            last_frame: std::time::Instant::now(),
94            tick: std::time::Duration::from_nanos(nsec_period),
95            app: Some(app),
96            cursor_pos: (0.0, 0.0),
97            always_tick: false,
98        }
99    }
100
101    pub fn run(&mut self) -> Result<(), winit::error::EventLoopError> {
102        let event_loop = winit::event_loop::EventLoop::new()?;
103
104        // event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
105        event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
106
107        event_loop.run_app(self)?;
108        Ok(())
109    }
110
111    /// Set to true if your app has something special to do at every tick even if there are no user
112    /// events. This can be used if you have physics or an animation to run. Defaults to false to
113    /// preserve performance.
114    pub fn set_always_tick(&mut self, val: bool) {
115        self.always_tick = val;
116    }
117}
118
119impl winit::application::ApplicationHandler for WinitHandler {
120    /// Resume gets called when window gets loaded for the first time
121    fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
122        log::info!(".resumed() called, creating window");
123        if let Some(app) = self.app.take() {
124            self.winfbx = Some(WinFbx::new(event_loop, self.width, self.height, app));
125        }
126    }
127
128    /// Instead of redrawing for every event, or every keyprss, we only try to
129    /// render after all evens have been processed.
130    fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
131        let app = self
132            .winfbx
133            .as_mut()
134            .expect("about_to_wait not to be called if window doesn't exist.");
135
136        if app.done() {
137            event_loop.exit();
138            return;
139        }
140        let now = std::time::Instant::now();
141        let duration_from_last_tick = now.duration_since(self.last_frame);
142        // If time since last tick is greater or equal than tickrate, we want to prompt that app
143        // for a redraw.
144        // Otherwise if they are key pressed we wait for next tick. If none are pressed we wait
145        // until we get an event.
146        // TODO: condition this behaviour to a flag
147        if duration_from_last_tick >= self.tick {
148            self.last_frame = now;
149            app.on_tick();
150            app.window.request_redraw();
151        } else {
152            if self.always_tick || !app.pressed_keys.is_empty() {
153                let duration_to_next_tick = self.tick - duration_from_last_tick;
154                event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
155                    now + duration_to_next_tick,
156                ));
157            } else {
158                event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
159            }
160        }
161    }
162
163    fn window_event(
164        &mut self,
165        event_loop: &winit::event_loop::ActiveEventLoop,
166        _: WindowId,
167        event: winit::event::WindowEvent,
168    ) {
169        let app = self
170            .winfbx
171            .as_mut()
172            .expect("window_event not to be called if window doesn't exist.");
173        use winit::event::WindowEvent;
174        match event {
175            WindowEvent::CloseRequested => {
176                log::info!("The close button was pressed; stopping");
177                event_loop.exit();
178            }
179            WindowEvent::Resized(size) => app.process_resize(size),
180            WindowEvent::KeyboardInput {
181                device_id: _,
182                event,
183                is_synthetic: _,
184            } if event.repeat == false => app.process_kbd_input(event, event_loop),
185            WindowEvent::RedrawRequested => app.on_redraw(),
186            WindowEvent::CursorMoved {
187                device_id: _,
188                position,
189            } => {
190                self.cursor_pos = (position.x, position.y);
191            }
192            WindowEvent::MouseInput {
193                device_id: _,
194                state,
195                button: _,
196            } if state.is_pressed() => {
197                log::info!(
198                    "clicked at x: {}, y: {}",
199                    self.cursor_pos.0,
200                    self.cursor_pos.1
201                )
202            }
203            _ => {}
204        }
205    }
206}
207
208pub fn put_pixel(frame: &mut [u8], width: usize, x: usize, y: usize, color: rgb::RGBA8) {
209    use rgb::*;
210    let idx = width * y + x;
211    frame.as_rgba_mut()[idx] = color;
212}
213
214/// Manages the actual winit::Window, the Pixels, handles resizes, records pressed keys into a
215/// custom structure and call the given app tick and draw methods.
216struct WinFbx {
217    window: Window,
218    pixels: Pixels,
219    pause: bool,
220    height: usize,
221    width: usize,
222    pressed_keys: std::collections::HashSet<Key>,
223    released_keys: std::collections::HashSet<Key>,
224    needs_render: bool,
225    app: Box<dyn GfxApp>,
226}
227
228impl WinFbx {
229    fn new(
230        event_loop: &winit::event_loop::ActiveEventLoop,
231        width: usize,
232        height: usize,
233        app: Box<dyn GfxApp>,
234    ) -> Self {
235        let mut attr = Window::default_attributes();
236        let size = winit::dpi::PhysicalSize::new(width as u16, height as u16);
237        attr = attr.with_inner_size(size).with_title("Box");
238        let win = event_loop.create_window(attr).unwrap();
239
240        let mut pixels = {
241            let surface_texture = SurfaceTexture::new(width as u32, height as u32, &win);
242            Pixels::new(width as u32, height as u32, surface_texture).unwrap()
243        };
244        pixels.clear_color(pixels::wgpu::Color {
245            r: 0.0,
246            g: 0.0,
247            b: 255.0,
248            a: 255.0,
249        });
250        Self {
251            window: win,
252            pixels,
253            height,
254            width,
255            pause: false,
256            pressed_keys: std::collections::HashSet::new(),
257            released_keys: std::collections::HashSet::new(),
258            needs_render: true,
259            app,
260        }
261    }
262
263    fn on_redraw(&mut self) {
264        if self.needs_render {
265            self.app.draw(&mut self.pixels, self.width);
266
267            if let Err(err) = self.pixels.render() {
268                log::error!("failed to render with error {}", err);
269                return;
270            }
271        }
272        self.needs_render = false;
273    }
274
275    fn done(&self) -> bool {
276        self.app.done() == DoneStatus::Exit
277    }
278
279    fn on_tick(&mut self) {
280        if self.app.done() == DoneStatus::NotDone {
281            self.needs_render = self.app.on_tick(&self.pressed_keys);
282        }
283        self.pressed_keys
284            .retain(|candidate| !self.released_keys.contains(candidate));
285        self.released_keys.clear();
286    }
287
288    fn process_kbd_input(
289        &mut self,
290        event: winit::event::KeyEvent,
291        event_loop: &winit::event_loop::ActiveEventLoop,
292    ) {
293        use winit::keyboard::{Key, NamedKey};
294        if let Ok(my_key) = (&event.logical_key).try_into() {
295            if event.state == winit::event::ElementState::Pressed {
296                self.pressed_keys.insert(my_key);
297            } else if event.state == winit::event::ElementState::Released {
298                self.released_keys.insert(my_key);
299            }
300        };
301        if event.state == winit::event::ElementState::Pressed {
302            match event.logical_key {
303                Key::Named(NamedKey::Escape) => event_loop.exit(),
304                Key::Named(NamedKey::Space) => {
305                    self.pause = !self.pause;
306                }
307                _ => {}
308            }
309        }
310    }
311
312    fn process_resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
313        self.width = size.width as usize;
314        self.height = size.height as usize;
315        self.pixels.resize_surface(size.width, size.height).unwrap();
316        self.pixels.resize_buffer(size.width, size.height).unwrap();
317        self.window.request_redraw();
318        self.needs_render = true;
319    }
320}
321
322#[derive(Eq, PartialEq)]
323pub enum DoneStatus {
324    /// The program should quit, the app has nothing left to do.
325    Exit,
326    /// The program should remain open, but the app is done. Useful when you want the result of the
327    /// app to stay on the screen. On `Remain` the `draw` and `on_tick` methodes will not be called
328    /// anymore.
329    Remain,
330    /// The program should continue, the app is not done.
331    NotDone,
332}
333
334pub trait GfxApp {
335    /// Every tick, this method gets called with currently pressed keys. Released keys during the tick are considered still pressed. But will be removed after this call.
336    fn on_tick(&mut self, pressed_keys: &std::collections::HashSet<Key>) -> bool;
337
338    /// You get the pixel array, so you can draw on it before the render.
339    fn draw(&mut self, pixels: &mut Pixels, width: usize);
340
341    /// Indicate if the app logic is done and if the program should remain or exit. For oneshot
342    /// drawing, return `DoneStatus::Remain` so that the result stays on screen.
343    fn done(&self) -> DoneStatus;
344}