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}