md_mage/
lib.rs

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//
110// Errors
111//
112
113#[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
127//
128// Rogue building
129//
130
131pub 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            //
244            // Windowed Events
245            //
246            Event::WindowEvent { event, window_id } if window.id() == window_id => {
247                match event {
248                    //
249                    // Closing the window
250                    //
251                    WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
252
253                    //
254                    // Keyboard Events
255                    //
256                    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                        //
269                        // Check for system keys
270                        //
271                        match key_state {
272                            KeyState {
273                                pressed: true,
274                                vkey: Some(VirtualKeyCode::Escape),
275                                ..
276                            } => {
277                                //
278                                // Exit
279                                //
280                                *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                                //
290                                // Toggle fullscreen
291                                //
292                                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                    //
312                    // Modifier keys
313                    //
314                    WindowEvent::ModifiersChanged(mods) => {
315                        key_state.alt = mods.alt();
316                        key_state.ctrl = mods.ctrl();
317                        key_state.shift = mods.shift();
318                    }
319                    //
320                    // Resizing
321                    //
322                    WindowEvent::Resized(new_size) => render.resize(new_size),
323                    WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
324                        render.resize(*new_inner_size)
325                    }
326
327                    _ => {} // No more windowed events
328                }
329            }
330            //
331            // Idle
332            //
333            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            //
340            // Redraw
341            //
342            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            _ => {} // No more events
353        }
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}