1use crate::{graphics::Graphics, renderer::GlowVertexAttribs};
2use glow::{Context, HasContext};
3#[cfg(not(target_arch = "wasm32"))]
4use glutin::{
5 dpi::LogicalSize,
6 event::{Event, WindowEvent},
7 event_loop::{ControlFlow, EventLoop},
8 platform::run_return::EventLoopExtRunReturn,
9 window::{Fullscreen, Window, WindowBuilder},
10 ContextBuilder, ContextWrapper, PossiblyCurrent,
11};
12#[cfg(target_arch = "wasm32")]
13use web_sys::{wasm_bindgen::JsCast, HtmlCanvasElement, WebGl2RenderingContext};
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>) {}
25
26 fn on_redraw(&mut self, graphics: &mut Graphics<V>) {}
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 #[cfg(not(target_arch = "wasm32"))]
132 width: u32,
133 #[cfg(not(target_arch = "wasm32"))]
134 height: u32,
135 refresh_on_event: bool,
136 event_loop: EventLoop<()>,
137 #[cfg(not(target_arch = "wasm32"))]
138 context_wrapper: ContextWrapper<PossiblyCurrent, Window>,
139 #[cfg(target_arch = "wasm32")]
140 window: Window,
141 graphics: Graphics<V>,
142}
143
144impl<V: GlowVertexAttribs> Default for App<V> {
145 fn default() -> Self {
146 Self::new(Default::default())
147 }
148}
149
150impl<V: GlowVertexAttribs> App<V> {
151 pub fn new(config: AppConfig) -> Self {
152 #[cfg(not(target_arch = "wasm32"))]
153 let AppConfig {
154 title,
155 width,
156 height,
157 fullscreen,
158 maximized,
159 vsync,
160 decorations,
161 transparent,
162 double_buffer,
163 hardware_acceleration,
164 refresh_on_event,
165 color,
166 } = config;
167 #[cfg(target_arch = "wasm32")]
168 let AppConfig {
169 title,
170 width,
171 height,
172 fullscreen,
173 maximized,
174 decorations,
175 transparent,
176 refresh_on_event,
177 color,
178 ..
179 } = config;
180 let fullscreen = if fullscreen {
181 Some(Fullscreen::Borderless(None))
182 } else {
183 None
184 };
185 let event_loop = EventLoop::new();
186 let window_builder = WindowBuilder::new()
187 .with_title(title.as_str())
188 .with_inner_size(LogicalSize::new(width, height))
189 .with_fullscreen(fullscreen)
190 .with_maximized(maximized)
191 .with_decorations(decorations)
192 .with_transparent(transparent);
193 #[cfg(not(target_arch = "wasm32"))]
194 let (context_wrapper, context) = {
195 let context_builder = ContextBuilder::new()
196 .with_vsync(vsync)
197 .with_double_buffer(double_buffer)
198 .with_hardware_acceleration(hardware_acceleration);
199 #[cfg(debug_assertions)]
200 crate::console_log!("* GL {:#?}", context_builder);
201 let context_wrapper = unsafe {
202 context_builder
203 .build_windowed(window_builder, &event_loop)
204 .expect("Could not build windowed context wrapper!")
205 .make_current()
206 .expect("Could not make windowed context wrapper a current one!")
207 };
208 let context = unsafe {
209 Context::from_loader_function(|name| {
210 context_wrapper.get_proc_address(name) as *const _
211 })
212 };
213 (context_wrapper, context)
214 };
215 #[cfg(target_arch = "wasm32")]
216 let (window, context) = {
217 use winit::platform::web::WindowBuilderExtWebSys;
218 let canvas = web_sys::window()
219 .unwrap()
220 .document()
221 .unwrap()
222 .get_element_by_id("screen")
223 .unwrap()
224 .dyn_into::<HtmlCanvasElement>()
225 .expect("DOM element is not HtmlCanvasElement");
226 let window = window_builder
227 .with_canvas(Some(canvas.clone()))
228 .build(&event_loop)
229 .expect("Could not build window!");
230 let context = Context::from_webgl2_context(
231 canvas
232 .get_context("webgl2")
233 .expect("Could not get WebGL 2 context!")
234 .expect("Could not get WebGL 2 context!")
235 .dyn_into::<WebGl2RenderingContext>()
236 .expect("DOM element is not WebGl2RenderingContext"),
237 );
238 (window, context)
239 };
240 let context_version = context.version();
241 #[cfg(debug_assertions)]
242 crate::console_log!("* GL Version: {:?}", context_version);
243 if context_version.major < 3 {
244 panic!("* Minimum GL version required is 3.0!");
245 }
246 let mut graphics = Graphics::<V>::new(context);
247 graphics.color = color;
248 Self {
249 #[cfg(not(target_arch = "wasm32"))]
250 width,
251 #[cfg(not(target_arch = "wasm32"))]
252 height,
253 refresh_on_event,
254 event_loop,
255 #[cfg(not(target_arch = "wasm32"))]
256 context_wrapper,
257 #[cfg(target_arch = "wasm32")]
258 window,
259 graphics,
260 }
261 }
262
263 pub fn run<S: AppState<V> + 'static>(self, mut state: S) {
264 #[cfg(not(target_arch = "wasm32"))]
265 let App {
266 mut width,
267 mut height,
268 refresh_on_event,
269 mut event_loop,
270 context_wrapper,
271 mut graphics,
272 } = self;
273 #[cfg(target_arch = "wasm32")]
274 let App {
275 refresh_on_event,
276 event_loop,
277 mut window,
278 mut graphics,
279 } = self;
280 #[cfg(not(target_arch = "wasm32"))]
281 let (context, mut window) = unsafe { context_wrapper.split() };
282 state.on_init(&mut graphics);
283 #[cfg(not(target_arch = "wasm32"))]
284 {
285 let mut running = true;
286 while running {
287 event_loop.run_return(|event, _, control_flow| {
288 *control_flow = if refresh_on_event {
289 ControlFlow::Wait
290 } else {
291 ControlFlow::Poll
292 };
293 match &event {
294 Event::MainEventsCleared => {
295 unsafe {
296 graphics
297 .context()
298 .unwrap()
299 .viewport(0, 0, width as _, height as _);
300 }
301 graphics.main_camera.screen_size.x = width as _;
302 graphics.main_camera.screen_size.y = height as _;
303 let _ = graphics.prepare_frame(true);
304 state.on_redraw(&mut graphics);
305 let _ = graphics.draw();
306 let _ = context.swap_buffers();
307 *control_flow = ControlFlow::Exit;
308 }
309 Event::WindowEvent { event, .. } => match event {
310 WindowEvent::Resized(physical_size) => {
311 context.resize(*physical_size);
312 width = physical_size.width;
313 height = physical_size.height;
314 }
315 WindowEvent::CloseRequested => {
316 running = false;
317 }
318 _ => {}
319 },
320 _ => {}
321 }
322 if !state.on_event(event, &mut window) {
323 running = false;
324 }
325 });
326 }
327 drop(graphics);
328 }
329 #[cfg(target_arch = "wasm32")]
330 {
331 event_loop.run(move |event, _, control_flow| {
332 *control_flow = if refresh_on_event {
333 ControlFlow::Wait
334 } else {
335 ControlFlow::Poll
336 };
337 match &event {
338 Event::MainEventsCleared => {
339 let dom_window = web_sys::window().unwrap();
340 let width = dom_window.inner_width().unwrap().as_f64().unwrap().max(1.0);
341 let height = dom_window
342 .inner_height()
343 .unwrap()
344 .as_f64()
345 .unwrap()
346 .max(1.0);
347 let scaled_width = width * window.scale_factor();
348 let scaled_height = height * window.scale_factor();
349 window.set_inner_size(LogicalSize::new(width, height));
350 graphics.main_camera.screen_size.x = scaled_width as _;
351 graphics.main_camera.screen_size.y = scaled_height as _;
352 let _ = graphics.prepare_frame(true);
353 state.on_redraw(&mut graphics);
354 let _ = graphics.draw();
355 window.request_redraw();
356 }
357 _ => {}
358 }
359 state.on_event(event, &mut window);
360 });
361 }
362 }
363}