1#![allow(unsafe_code)]
2use crate::core::{Context, CoreError, Viewport};
3use winit::event::{Event, WindowEvent};
4use winit::event_loop::{ControlFlow, EventLoop};
5use winit::window::WindowBuilder;
6use winit::*;
7
8mod settings;
9pub use settings::*;
10
11mod frame_io;
12pub use frame_io::*;
13
14mod frame_input_generator;
15pub use frame_input_generator::*;
16
17mod windowed_context;
18pub use windowed_context::*;
19
20use thiserror::Error;
21#[cfg(not(target_arch = "wasm32"))]
25#[derive(Error, Debug)]
26#[allow(missing_docs)]
27pub enum WindowError {
28 #[error("glutin error")]
29 GlutinError(#[from] glutin::error::Error),
30 #[error("winit error")]
31 WinitError(#[from] winit::error::OsError),
32 #[error("error in three-d")]
33 ThreeDError(#[from] CoreError),
34 #[error("the number of MSAA samples must be a power of two")]
35 InvalidNumberOfMSAASamples,
36 #[error("it's not possible to create a graphics context/surface with the given settings")]
37 SurfaceCreationError,
38}
39
40#[cfg(target_arch = "wasm32")]
44#[derive(Error, Debug)]
45#[allow(missing_docs)]
46pub enum WindowError {
47 #[error("failed to create a new winit window")]
48 WinitError(#[from] winit::error::OsError),
49 #[error("failed creating a new window")]
50 WindowCreation,
51 #[error("unable to get document from canvas")]
52 DocumentMissing,
53 #[error("unable to convert canvas to html canvas: {0}")]
54 CanvasConvertFailed(String),
55 #[error("unable to get webgl2 context for the given canvas, maybe the browser doesn't support WebGL2{0}")]
56 WebGL2NotSupported(String),
57 #[error("unable to get EXT_color_buffer_float extension for the given canvas, maybe the browser doesn't support EXT_color_buffer_float: {0}")]
58 ColorBufferFloatNotSupported(String),
59 #[error("unable to get OES_texture_float extension for the given canvas, maybe the browser doesn't support OES_texture_float: {0}")]
60 OESTextureFloatNotSupported(String),
61 #[error("error in three-d")]
62 ThreeDError(#[from] CoreError),
63}
64
65pub struct Window {
73 window: winit::window::Window,
74 event_loop: EventLoop<()>,
75 #[cfg(target_arch = "wasm32")]
76 closure: wasm_bindgen::closure::Closure<dyn FnMut(web_sys::Event)>,
77 gl: WindowedContext,
78 #[allow(dead_code)]
79 maximized: bool,
80}
81
82impl Window {
83 pub fn new(window_settings: WindowSettings) -> Result<Self, WindowError> {
89 Self::from_event_loop(window_settings, EventLoop::new())
90 }
91
92 pub fn from_event_loop(
95 window_settings: WindowSettings,
96 event_loop: EventLoop<()>,
97 ) -> Result<Self, WindowError> {
98 #[cfg(not(target_arch = "wasm32"))]
99 let window_builder = {
100 let window_builder = WindowBuilder::new()
101 .with_title(&window_settings.title)
102 .with_min_inner_size(dpi::LogicalSize::new(
103 window_settings.min_size.0,
104 window_settings.min_size.1,
105 ))
106 .with_decorations(!window_settings.borderless);
107
108 match (window_settings.initial_size, window_settings.max_size) {
109 (Some((width, height)), Some((max_width, max_height))) => window_builder
110 .with_inner_size(dpi::LogicalSize::new(width as f64, height as f64))
111 .with_max_inner_size(dpi::LogicalSize::new(
112 max_width as f64,
113 max_height as f64,
114 )),
115 (Some((width, height)), None) => window_builder
116 .with_inner_size(dpi::LogicalSize::new(width as f64, height as f64)),
117 (None, Some((width, height))) => window_builder
118 .with_inner_size(dpi::LogicalSize::new(width as f64, height as f64))
119 .with_max_inner_size(dpi::LogicalSize::new(width as f64, height as f64)),
120 (None, None) => window_builder.with_maximized(true),
121 }
122 };
123 #[cfg(target_arch = "wasm32")]
124 let window_builder = {
125 use wasm_bindgen::JsCast;
126 use winit::{dpi::LogicalSize, platform::web::WindowBuilderExtWebSys};
127
128 let canvas = if let Some(canvas) = window_settings.canvas {
129 canvas
130 } else {
131 web_sys::window()
132 .ok_or(WindowError::WindowCreation)?
133 .document()
134 .ok_or(WindowError::DocumentMissing)?
135 .get_elements_by_tag_name("canvas")
136 .item(0)
137 .expect(
138 "settings doesn't contain canvas and DOM doesn't have a canvas element either",
139 )
140 .dyn_into::<web_sys::HtmlCanvasElement>()
141 .map_err(|e| WindowError::CanvasConvertFailed(format!("{:?}", e)))?
142 };
143
144 let inner_size = window_settings
145 .initial_size
146 .or(window_settings.max_size)
147 .map(|(width, height)| LogicalSize::new(width as f64, height as f64))
148 .unwrap_or_else(|| {
149 let browser_window = canvas
150 .owner_document()
151 .and_then(|doc| doc.default_view())
152 .or_else(web_sys::window)
153 .unwrap();
154 LogicalSize::new(
155 browser_window.inner_width().unwrap().as_f64().unwrap(),
156 browser_window.inner_height().unwrap().as_f64().unwrap(),
157 )
158 });
159
160 WindowBuilder::new()
161 .with_title(window_settings.title)
162 .with_canvas(Some(canvas))
163 .with_inner_size(inner_size)
164 .with_prevent_default(true)
165 };
166
167 let winit_window = window_builder.build(&event_loop)?;
168 winit_window.focus_window();
169 Self::from_winit_window(
170 winit_window,
171 event_loop,
172 window_settings.surface_settings,
173 window_settings.max_size.is_none() && window_settings.initial_size.is_none(),
174 )
175 }
176
177 pub fn from_winit_window(
183 winit_window: window::Window,
184 event_loop: EventLoop<()>,
185 mut surface_settings: SurfaceSettings,
186 maximized: bool,
187 ) -> Result<Self, WindowError> {
188 let mut gl = WindowedContext::from_winit_window(&winit_window, surface_settings);
189 if gl.is_err() {
190 surface_settings.multisamples = 0;
191 gl = WindowedContext::from_winit_window(&winit_window, surface_settings);
192 }
193
194 #[cfg(target_arch = "wasm32")]
195 let closure = {
196 use wasm_bindgen::JsCast;
197 use winit::platform::web::WindowExtWebSys;
198 let closure =
199 wasm_bindgen::closure::Closure::wrap(Box::new(move |event: web_sys::Event| {
200 event.prevent_default();
201 }) as Box<dyn FnMut(_)>);
202 winit_window
203 .canvas()
204 .add_event_listener_with_callback("contextmenu", closure.as_ref().unchecked_ref())
205 .expect("failed to listen to canvas context menu");
206 closure
207 };
208
209 Ok(Self {
210 window: winit_window,
211 event_loop,
212 gl: gl?,
213 #[cfg(target_arch = "wasm32")]
214 closure,
215 maximized,
216 })
217 }
218
219 pub fn render_loop<F: 'static + FnMut(FrameInput) -> FrameOutput>(self, mut callback: F) {
223 let mut frame_input_generator = FrameInputGenerator::from_winit_window(&self.window);
224 self.event_loop
225 .run(move |event, _, control_flow| match event {
226 Event::LoopDestroyed => {
227 #[cfg(target_arch = "wasm32")]
228 {
229 use wasm_bindgen::JsCast;
230 use winit::platform::web::WindowExtWebSys;
231 self.window
232 .canvas()
233 .remove_event_listener_with_callback(
234 "contextmenu",
235 self.closure.as_ref().unchecked_ref(),
236 )
237 .unwrap();
238 }
239 }
240 Event::MainEventsCleared => {
241 self.window.request_redraw();
242 }
243 Event::RedrawRequested(_) => {
244 #[cfg(target_arch = "wasm32")]
245 if self.maximized || option_env!("THREE_D_SCREENSHOT").is_some() {
246 use winit::platform::web::WindowExtWebSys;
247
248 let html_canvas = self.window.canvas();
249 let browser_window = html_canvas
250 .owner_document()
251 .and_then(|doc| doc.default_view())
252 .or_else(web_sys::window)
253 .unwrap();
254
255 self.window.set_inner_size(dpi::LogicalSize {
256 width: browser_window.inner_width().unwrap().as_f64().unwrap(),
257 height: browser_window.inner_height().unwrap().as_f64().unwrap(),
258 });
259 }
260
261 let frame_input = frame_input_generator.generate(&self.gl);
262 let frame_output = callback(frame_input);
263 if frame_output.exit {
264 *control_flow = ControlFlow::Exit;
265 } else {
266 if frame_output.swap_buffers && option_env!("THREE_D_SCREENSHOT").is_none()
267 {
268 self.gl.swap_buffers().unwrap();
269 }
270 if frame_output.wait_next_event {
271 *control_flow = ControlFlow::Wait;
272 } else {
273 *control_flow = ControlFlow::Poll;
274 self.window.request_redraw();
275 }
276 }
277 }
278 Event::WindowEvent { ref event, .. } => {
279 frame_input_generator.handle_winit_window_event(event);
280 match event {
281 WindowEvent::Resized(physical_size) => {
282 self.gl.resize(*physical_size);
283 }
284 WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
285 self.gl.resize(**new_inner_size);
286 }
287 WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
288 _ => (),
289 }
290 }
291 _ => (),
292 });
293 }
294
295 pub fn size(&self) -> (u32, u32) {
299 self.window
300 .inner_size()
301 .to_logical::<f64>(self.window.scale_factor())
302 .into()
303 }
304
305 pub fn viewport(&self) -> Viewport {
309 let (w, h): (u32, u32) = self.window.inner_size().into();
310 Viewport::new_at_origo(w, h)
311 }
312
313 pub fn device_pixel_ratio(&self) -> f32 {
317 self.window.scale_factor() as f32
318 }
319
320 pub fn gl(&self) -> Context {
324 (*self.gl).clone()
325 }
326}