spitfire_glow/
app.rs

1use crate::{graphics::Graphics, renderer::GlowVertexAttribs};
2use glow::{Context, HasContext};
3#[cfg(not(target_arch = "wasm32"))]
4use glutin::{
5    ContextBuilder, ContextWrapper, PossiblyCurrent,
6    dpi::{LogicalPosition, LogicalSize},
7    event::{Event, WindowEvent},
8    event_loop::{ControlFlow, EventLoop},
9    platform::run_return::EventLoopExtRunReturn,
10    window::{Fullscreen, Window, WindowBuilder},
11};
12#[cfg(target_arch = "wasm32")]
13use web_sys::{HtmlCanvasElement, WebGl2RenderingContext, wasm_bindgen::JsCast};
14#[cfg(target_arch = "wasm32")]
15use winit::{
16    dpi::LogicalSize,
17    event::Event,
18    event_loop::{ControlFlow, EventLoop},
19    window::{Fullscreen, Window, WindowBuilder},
20};
21
22#[allow(unused_variables)]
23pub trait AppState<V: GlowVertexAttribs> {
24    fn on_init(&mut self, graphics: &mut Graphics<V>, control: &mut AppControl) {}
25
26    fn on_redraw(&mut self, graphics: &mut Graphics<V>, control: &mut AppControl) {}
27
28    fn on_event(&mut self, event: Event<()>, window: &mut Window) -> bool {
29        true
30    }
31}
32
33#[derive(Debug, Clone)]
34pub struct AppConfig {
35    pub title: String,
36    pub width: u32,
37    pub height: u32,
38    pub fullscreen: bool,
39    pub maximized: bool,
40    pub vsync: bool,
41    pub decorations: bool,
42    pub transparent: bool,
43    pub double_buffer: Option<bool>,
44    pub hardware_acceleration: Option<bool>,
45    pub refresh_on_event: bool,
46    pub color: [f32; 4],
47}
48
49impl Default for AppConfig {
50    fn default() -> Self {
51        Self {
52            title: "Spitfire Application".to_owned(),
53            width: 1024,
54            height: 576,
55            fullscreen: false,
56            maximized: false,
57            vsync: false,
58            decorations: true,
59            transparent: false,
60            double_buffer: Some(true),
61            hardware_acceleration: Some(true),
62            refresh_on_event: false,
63            color: [1.0, 1.0, 1.0, 1.0],
64        }
65    }
66}
67
68impl AppConfig {
69    pub fn title(mut self, v: impl ToString) -> Self {
70        self.title = v.to_string();
71        self
72    }
73
74    pub fn width(mut self, v: u32) -> Self {
75        self.width = v;
76        self
77    }
78
79    pub fn height(mut self, v: u32) -> Self {
80        self.height = v;
81        self
82    }
83
84    pub fn fullscreen(mut self, v: bool) -> Self {
85        self.fullscreen = v;
86        self
87    }
88
89    pub fn maximized(mut self, v: bool) -> Self {
90        self.maximized = v;
91        self
92    }
93
94    pub fn vsync(mut self, v: bool) -> Self {
95        self.vsync = v;
96        self
97    }
98
99    pub fn decorations(mut self, v: bool) -> Self {
100        self.decorations = v;
101        self
102    }
103
104    pub fn transparent(mut self, v: bool) -> Self {
105        self.transparent = v;
106        self
107    }
108
109    pub fn double_buffer(mut self, v: Option<bool>) -> Self {
110        self.double_buffer = v;
111        self
112    }
113
114    pub fn hardware_acceleration(mut self, v: Option<bool>) -> Self {
115        self.hardware_acceleration = v;
116        self
117    }
118
119    pub fn refresh_on_event(mut self, v: bool) -> Self {
120        self.refresh_on_event = v;
121        self
122    }
123
124    pub fn color(mut self, v: impl Into<[f32; 4]>) -> Self {
125        self.color = v.into();
126        self
127    }
128}
129
130pub struct App<V: GlowVertexAttribs> {
131    refresh_on_event: bool,
132    event_loop: EventLoop<()>,
133    #[cfg(not(target_arch = "wasm32"))]
134    context_wrapper: ContextWrapper<PossiblyCurrent, Window>,
135    #[cfg(target_arch = "wasm32")]
136    window: Window,
137    graphics: Graphics<V>,
138    control: AppControl,
139}
140
141impl<V: GlowVertexAttribs> Default for App<V> {
142    fn default() -> Self {
143        Self::new(Default::default())
144    }
145}
146
147impl<V: GlowVertexAttribs> App<V> {
148    pub fn new(config: AppConfig) -> Self {
149        #[cfg(not(target_arch = "wasm32"))]
150        let AppConfig {
151            title,
152            width,
153            height,
154            fullscreen,
155            maximized,
156            vsync,
157            decorations,
158            transparent,
159            double_buffer,
160            hardware_acceleration,
161            refresh_on_event,
162            color,
163        } = config;
164        #[cfg(target_arch = "wasm32")]
165        let AppConfig {
166            title,
167            width,
168            height,
169            fullscreen,
170            maximized,
171            decorations,
172            transparent,
173            refresh_on_event,
174            color,
175            ..
176        } = config;
177        let fullscreen = if fullscreen {
178            Some(Fullscreen::Borderless(None))
179        } else {
180            None
181        };
182        let event_loop = EventLoop::new();
183        let window_builder = WindowBuilder::new()
184            .with_title(title.as_str())
185            .with_inner_size(LogicalSize::new(width, height))
186            .with_fullscreen(fullscreen)
187            .with_maximized(maximized)
188            .with_decorations(decorations)
189            .with_transparent(transparent);
190        #[cfg(not(target_arch = "wasm32"))]
191        let (context_wrapper, context) = {
192            let context_builder = ContextBuilder::new()
193                .with_vsync(vsync)
194                .with_double_buffer(double_buffer)
195                .with_hardware_acceleration(hardware_acceleration);
196            #[cfg(debug_assertions)]
197            crate::console_log!("* GL {:#?}", context_builder);
198            let context_wrapper = unsafe {
199                context_builder
200                    .build_windowed(window_builder, &event_loop)
201                    .expect("Could not build windowed context wrapper!")
202                    .make_current()
203                    .expect("Could not make windowed context wrapper a current one!")
204            };
205            let context = unsafe {
206                Context::from_loader_function(|name| {
207                    context_wrapper.get_proc_address(name) as *const _
208                })
209            };
210            (context_wrapper, context)
211        };
212        #[cfg(target_arch = "wasm32")]
213        let (window, context) = {
214            use winit::platform::web::WindowBuilderExtWebSys;
215            let canvas = web_sys::window()
216                .unwrap()
217                .document()
218                .unwrap()
219                .get_element_by_id("screen")
220                .unwrap()
221                .dyn_into::<HtmlCanvasElement>()
222                .expect("DOM element is not HtmlCanvasElement");
223            let window = window_builder
224                .with_canvas(Some(canvas.clone()))
225                .build(&event_loop)
226                .expect("Could not build window!");
227            let context = Context::from_webgl2_context(
228                canvas
229                    .get_context("webgl2")
230                    .expect("Could not get WebGL 2 context!")
231                    .expect("Could not get WebGL 2 context!")
232                    .dyn_into::<WebGl2RenderingContext>()
233                    .expect("DOM element is not WebGl2RenderingContext"),
234            );
235            (window, context)
236        };
237        let context_version = context.version();
238        #[cfg(debug_assertions)]
239        crate::console_log!("* GL Version: {:?}", context_version);
240        if context_version.major < 3 {
241            panic!("* Minimum GL version required is 3.0!");
242        }
243        let mut graphics = Graphics::<V>::new(context);
244        graphics.state.color = color;
245        Self {
246            refresh_on_event,
247            event_loop,
248            #[cfg(not(target_arch = "wasm32"))]
249            context_wrapper,
250            #[cfg(target_arch = "wasm32")]
251            window,
252            graphics,
253            control: AppControl {
254                x: 0,
255                y: 0,
256                dirty_pos: false,
257                width,
258                height,
259                dirty_size: false,
260                minimized: false,
261                dirty_minimized: false,
262                maximized,
263                dirty_maximized: false,
264                close_requested: false,
265            },
266        }
267    }
268
269    pub fn run<S: AppState<V> + 'static>(self, mut state: S) {
270        #[cfg(not(target_arch = "wasm32"))]
271        let App {
272            refresh_on_event,
273            mut event_loop,
274            context_wrapper,
275            mut graphics,
276            mut control,
277        } = self;
278        #[cfg(target_arch = "wasm32")]
279        let App {
280            refresh_on_event,
281            event_loop,
282            mut window,
283            mut graphics,
284            mut control,
285        } = self;
286        #[cfg(not(target_arch = "wasm32"))]
287        let (context, mut window) = unsafe { context_wrapper.split() };
288        if let Ok(pos) = window.outer_position() {
289            control.x = pos.x;
290            control.y = pos.y;
291        }
292        let size = window.inner_size();
293        control.width = size.width;
294        control.height = size.height;
295        control.minimized = control.width == 0 || control.height == 0;
296        control.maximized = window.is_maximized();
297        state.on_init(&mut graphics, &mut control);
298        #[cfg(not(target_arch = "wasm32"))]
299        {
300            let mut running = true;
301            while running {
302                if control.close_requested {
303                    break;
304                }
305                event_loop.run_return(|event, _, control_flow| {
306                    if control.dirty_pos {
307                        control.dirty_pos = false;
308                        window.set_outer_position(LogicalPosition::new(control.x, control.y));
309                    }
310                    if control.dirty_size {
311                        control.dirty_size = false;
312                        window.set_inner_size(LogicalSize::new(control.width, control.height));
313                    }
314                    if control.dirty_minimized {
315                        control.dirty_minimized = false;
316                        window.set_minimized(control.minimized);
317                    } else {
318                        control.minimized = control.width == 0 || control.height == 0;
319                    }
320                    if control.dirty_maximized {
321                        control.dirty_maximized = false;
322                        window.set_maximized(control.maximized);
323                    } else {
324                        control.maximized = window.is_maximized();
325                    }
326                    *control_flow = if refresh_on_event {
327                        ControlFlow::Wait
328                    } else {
329                        ControlFlow::Poll
330                    };
331                    match &event {
332                        Event::MainEventsCleared => {
333                            unsafe {
334                                graphics.context().unwrap().viewport(
335                                    0,
336                                    0,
337                                    control.width as _,
338                                    control.height as _,
339                                );
340                            }
341                            graphics.state.main_camera.screen_size.x = control.width as _;
342                            graphics.state.main_camera.screen_size.y = control.height as _;
343                            let _ = graphics.prepare_frame(true);
344                            state.on_redraw(&mut graphics, &mut control);
345                            let _ = graphics.draw();
346                            let _ = context.swap_buffers();
347                            *control_flow = ControlFlow::Exit;
348                        }
349                        Event::WindowEvent { event, .. } => match event {
350                            WindowEvent::Resized(physical_size) => {
351                                context.resize(*physical_size);
352                                control.width = physical_size.width;
353                                control.height = physical_size.height;
354                                control.minimized = control.width == 0 || control.height == 0;
355                            }
356                            WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
357                                context.resize(**new_inner_size);
358                                control.width = new_inner_size.width;
359                                control.height = new_inner_size.height;
360                                control.minimized = control.width == 0 || control.height == 0;
361                            }
362                            WindowEvent::CloseRequested => {
363                                running = false;
364                                control.close_requested = true;
365                            }
366                            WindowEvent::Moved(physical_position) => {
367                                control.x = physical_position.x;
368                                control.y = physical_position.y;
369                            }
370                            _ => {}
371                        },
372                        _ => {}
373                    }
374                    if !state.on_event(event, &mut window) {
375                        running = false;
376                    }
377                });
378            }
379            drop(graphics);
380        }
381        #[cfg(target_arch = "wasm32")]
382        {
383            event_loop.run(move |event, _, control_flow| {
384                *control_flow = if refresh_on_event {
385                    ControlFlow::Wait
386                } else {
387                    ControlFlow::Poll
388                };
389                match &event {
390                    Event::MainEventsCleared => {
391                        let dom_window = web_sys::window().unwrap();
392                        let width = dom_window.inner_width().unwrap().as_f64().unwrap().max(1.0);
393                        let height = dom_window
394                            .inner_height()
395                            .unwrap()
396                            .as_f64()
397                            .unwrap()
398                            .max(1.0);
399                        control.x = 0;
400                        control.y = 0;
401                        control.width = width as _;
402                        control.height = height as _;
403                        control.maximized = true;
404                        let scaled_width = width * window.scale_factor();
405                        let scaled_height = height * window.scale_factor();
406                        window.set_inner_size(LogicalSize::new(width, height));
407                        graphics.state.main_camera.screen_size.x = scaled_width as _;
408                        graphics.state.main_camera.screen_size.y = scaled_height as _;
409                        let _ = graphics.prepare_frame(true);
410                        state.on_redraw(&mut graphics, &mut control);
411                        let _ = graphics.draw();
412                        window.request_redraw();
413                    }
414                    _ => {}
415                }
416                state.on_event(event, &mut window);
417            });
418        }
419    }
420}
421
422#[derive(Debug)]
423pub struct AppControl {
424    x: i32,
425    y: i32,
426    dirty_pos: bool,
427    width: u32,
428    height: u32,
429    dirty_size: bool,
430    minimized: bool,
431    dirty_minimized: bool,
432    maximized: bool,
433    dirty_maximized: bool,
434    pub close_requested: bool,
435}
436
437impl AppControl {
438    pub fn position(&self) -> (i32, i32) {
439        (self.x, self.y)
440    }
441
442    pub fn set_position(&mut self, x: i32, y: i32) {
443        if self.x == x && self.y == y {
444            return;
445        }
446        self.x = x;
447        self.y = y;
448        self.dirty_pos = true;
449    }
450
451    pub fn size(&self) -> (u32, u32) {
452        (self.width, self.height)
453    }
454
455    pub fn set_size(&mut self, width: u32, height: u32) {
456        if self.width == width && self.height == height {
457            return;
458        }
459        self.width = width;
460        self.height = height;
461        self.dirty_size = true;
462    }
463
464    pub fn minimized(&self) -> bool {
465        self.minimized
466    }
467
468    pub fn set_minimized(&mut self, minimized: bool) {
469        if self.minimized == minimized {
470            return;
471        }
472        self.minimized = minimized;
473        self.dirty_minimized = true;
474    }
475
476    pub fn maximized(&self) -> bool {
477        self.maximized
478    }
479
480    pub fn set_maximized(&mut self, maximized: bool) {
481        if self.maximized == maximized {
482            return;
483        }
484        self.maximized = maximized;
485        self.dirty_maximized = true;
486    }
487}