1mod render;
2
3use bytemuck::cast_slice;
4use image::{EncodableLayout, GenericImageView, ImageFormat};
5use render::*;
6use std::{cmp::max, mem::replace, time::Duration};
7use thiserror::Error;
8use wgpu::SwapChainError;
9use winit::{
10 dpi::PhysicalSize,
11 event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent},
12 event_loop::{ControlFlow, EventLoop},
13 window::{Fullscreen, WindowBuilder},
14};
15
16pub trait Game {
17 fn start(&mut self);
18 fn tick(&mut self, sim_input: SimInput) -> TickResult;
19 fn present(&self, present_input: PresentInput);
20}
21
22pub enum TickResult {
23 Continue,
24 Stop,
25}
26
27pub struct KeyState {
28 pub pressed: bool,
29 pub shift: bool,
30 pub ctrl: bool,
31 pub alt: bool,
32 pub vkey: Option<VirtualKeyCode>,
33}
34
35impl KeyState {
36 pub fn alt_pressed(&self) -> bool {
37 self.alt && !self.ctrl && !self.shift
38 }
39 pub fn ctrl_pressed(&self) -> bool {
40 !self.alt && self.ctrl && !self.shift
41 }
42 pub fn shift_pressed(&self) -> bool {
43 !self.alt && !self.ctrl && self.shift
44 }
45 pub fn key_pressed(&self, key: VirtualKeyCode) -> bool {
46 if let Some(vkey) = self.vkey {
47 if key == vkey {
48 return true;
49 }
50 }
51 false
52 }
53}
54
55pub struct MouseState {
56 pub on_screen: bool,
57 pub left_pressed: bool,
58 pub right_pressed: bool,
59 pub x: i32,
60 pub y: i32,
61}
62
63pub struct SimInput<'a> {
64 pub dt: Duration,
65 pub width: u32,
66 pub height: u32,
67 pub key: &'a KeyState,
68 pub mouse: Option<MouseState>,
69}
70
71pub struct PresentInput<'a> {
72 pub width: u32,
73 pub height: u32,
74 pub fore_image: &'a mut Vec<u32>,
75 pub back_image: &'a mut Vec<u32>,
76 pub text_image: &'a mut Vec<u32>,
77}
78
79pub fn new_colour(r: u8, g: u8, b: u8) -> u32 {
80 0xff000000u32 + ((b as u32) << 16) + ((g as u32) << 8) + (r as u32)
81}
82
83pub enum Colour {
84 Black,
85 Red,
86 Green,
87 Yellow,
88 Blue,
89 Magenta,
90 Cyan,
91 White,
92}
93
94impl From<Colour> for u32 {
95 fn from(c: Colour) -> Self {
96 match c {
97 Colour::Black => new_colour(0, 0, 0),
98 Colour::Red => new_colour(255, 0, 0),
99 Colour::Green => new_colour(0, 255, 0),
100 Colour::Yellow => new_colour(255, 255, 0),
101 Colour::Blue => new_colour(0, 0, 255),
102 Colour::Magenta => new_colour(255, 0, 255),
103 Colour::Cyan => new_colour(0, 255, 255),
104 Colour::White => new_colour(255, 255, 255),
105 }
106 }
107}
108
109#[derive(Error, Debug)]
114pub enum RogueError {
115 #[error(transparent)]
116 OSError(#[from] winit::error::OsError),
117
118 #[error(transparent)]
119 RenderError(#[from] render::RenderError),
120
121 #[error("Unable to read font data")]
122 BadFont,
123}
124
125pub type RogueResult<T> = Result<T, RogueError>;
126
127pub struct RogueBuilder {
132 inner_size: (usize, usize),
133 title: String,
134 font: RogueFont,
135}
136
137pub struct RogueFontData {
138 data: Vec<u32>,
139 width: u32,
140 height: u32,
141}
142
143enum RogueFont {
144 Default,
145 Custom(RogueFontData),
146}
147
148impl RogueBuilder {
149 pub fn new() -> Self {
150 RogueBuilder {
151 inner_size: (100, 100),
152 title: "md-rogue window".to_string(),
153 font: RogueFont::Default,
154 }
155 }
156
157 pub fn with_inner_size(&mut self, width: usize, height: usize) -> &mut Self {
158 self.inner_size = (width, height);
159 self
160 }
161
162 pub fn with_title(&mut self, title: &str) -> &mut Self {
163 self.title = String::from(title);
164 self
165 }
166
167 pub fn with_font(&mut self, font: RogueFontData) -> &mut Self {
168 self.font = RogueFont::Custom(font);
169 self
170 }
171
172 pub fn build(&mut self) -> Self {
173 RogueBuilder {
174 inner_size: self.inner_size,
175 title: self.title.clone(),
176 font: replace(&mut self.font, RogueFont::Default),
177 }
178 }
179}
180
181impl Default for RogueBuilder {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187pub fn load_font_image(data: &[u8], format: ImageFormat) -> RogueResult<RogueFontData> {
188 let font_image =
189 image::load_from_memory_with_format(data, format).map_err(|_| RogueError::BadFont)?;
190 let dimensions = font_image.dimensions();
191 let font_rgba = font_image.to_rgba8();
192 let font_data = font_rgba.as_bytes();
193 let data_u32: &[u32] = cast_slice(font_data);
194 let char_width = dimensions.0 / 16;
195 let char_height = dimensions.1 / 16;
196 if char_width == 0 || char_height == 0 {
197 return Err(RogueError::BadFont);
198 }
199
200 Ok(RogueFontData {
201 width: char_width,
202 height: char_height,
203 data: Vec::from(data_u32),
204 })
205}
206
207pub async fn run(rogue: RogueBuilder, mut game: Box<dyn Game>) -> RogueResult<()> {
208 let font_data = match rogue.font {
209 RogueFont::Default => load_font_image(include_bytes!("font1.png"), ImageFormat::Png)?,
210 RogueFont::Custom(font) => font,
211 };
212
213 let width = max(20, rogue.inner_size.0 as u32) / font_data.width * font_data.width;
214 let height = max(20, rogue.inner_size.1 as u32) / font_data.height * font_data.height;
215
216 let event_loop = EventLoop::new();
217 let window = WindowBuilder::new()
218 .with_inner_size(PhysicalSize::new(width, height))
219 .with_title(rogue.title)
220 .with_min_inner_size(PhysicalSize::new(
221 20 * font_data.width,
222 20 * font_data.height,
223 ))
224 .build(&event_loop)?;
225 let mut render = RenderState::new(&window, &font_data).await?;
226
227 let mut key_state = KeyState {
228 vkey: None,
229 pressed: false,
230 alt: false,
231 ctrl: false,
232 shift: false,
233 };
234
235 game.start();
236
237 event_loop.run(move |event, _, control_flow| {
238 *control_flow = ControlFlow::Poll;
239 key_state.pressed = false;
240 key_state.vkey = None;
241
242 match event {
243 Event::WindowEvent { event, window_id } if window.id() == window_id => {
247 match event {
248 WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
252
253 WindowEvent::KeyboardInput {
257 input:
258 KeyboardInput {
259 state,
260 virtual_keycode,
261 ..
262 },
263 ..
264 } => {
265 key_state.pressed = state == ElementState::Pressed;
266 key_state.vkey = virtual_keycode;
267
268 match key_state {
272 KeyState {
273 pressed: true,
274 vkey: Some(VirtualKeyCode::Escape),
275 ..
276 } => {
277 *control_flow = ControlFlow::Exit;
281 }
282 KeyState {
283 pressed: true,
284 shift: false,
285 ctrl: false,
286 alt: true,
287 vkey: Some(VirtualKeyCode::Return),
288 } => {
289 if window.fullscreen().is_some() {
293 window.set_fullscreen(None);
294 } else if let Some(monitor) = window.current_monitor() {
295 if let Some(video_mode) = monitor.video_modes().next() {
296 if cfg!(any(target_os = "macos", unix)) {
297 window.set_fullscreen(Some(Fullscreen::Borderless(
298 Some(monitor),
299 )));
300 } else {
301 window.set_fullscreen(Some(Fullscreen::Exclusive(
302 video_mode,
303 )));
304 }
305 };
306 };
307 }
308 _ => {}
309 }
310 }
311 WindowEvent::ModifiersChanged(mods) => {
315 key_state.alt = mods.alt();
316 key_state.ctrl = mods.ctrl();
317 key_state.shift = mods.shift();
318 }
319 WindowEvent::Resized(new_size) => render.resize(new_size),
323 WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
324 render.resize(*new_inner_size)
325 }
326
327 _ => {} }
329 }
330 Event::MainEventsCleared => {
334 if let TickResult::Stop = simulate(game.as_mut(), &render, &key_state) {
335 *control_flow = ControlFlow::Exit;
336 }
337 window.request_redraw();
338 }
339 Event::RedrawRequested(_) => {
343 present(game.as_ref(), &mut render);
344 match render.render() {
345 Ok(_) => {}
346 Err(SwapChainError::Lost) => render.resize(window.inner_size()),
347 Err(wgpu::SwapChainError::OutOfMemory) => *control_flow = ControlFlow::Exit,
348 Err(e) => eprintln!("{:?}", e),
349 };
350 }
351
352 _ => {} }
354 });
355}
356
357fn simulate(game: &mut dyn Game, render: &RenderState, key_state: &KeyState) -> TickResult {
358 let (width, height) = render.chars_size();
359 let sim_input = SimInput {
360 dt: Duration::ZERO,
361 width,
362 height,
363 key: key_state,
364 mouse: None,
365 };
366
367 game.tick(sim_input)
368}
369
370fn present(game: &dyn Game, render: &mut RenderState) {
371 let (width, height) = render.chars_size();
372 let (fore_image, back_image, text_image) = render.images();
373
374 let present_input = PresentInput {
375 width,
376 height,
377 fore_image,
378 back_image,
379 text_image,
380 };
381
382 game.present(present_input);
383}