three_d/window/
winit_window.rs

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///
22/// Error associated with a window.
23///
24#[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///
41/// Error associated with a window.
42///
43#[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
65///
66/// Default window, context and event handling which uses [winit](https://crates.io/crates/winit).
67///
68/// To get full control over the creation of the [winit](https://crates.io/crates/winit) window, use [Window::from_winit_window].
69/// To take control over everything, including the context creation and [winit](https://crates.io/crates/winit) event loop,
70/// use [WindowedContext::from_winit_window] and [FrameInputGenerator].
71///
72pub 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    ///
84    /// Constructs a new Window with the given [settings].
85    ///
86    ///
87    /// [settings]: WindowSettings
88    pub fn new(window_settings: WindowSettings) -> Result<Self, WindowError> {
89        Self::from_event_loop(window_settings, EventLoop::new())
90    }
91
92    /// Exactly the same as [`Window::new()`] except with the ability to supply
93    /// an existing [`EventLoop`].
94    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    ///
178    /// Creates a new window from a [winit](https://crates.io/crates/winit) window and event loop with the given surface settings, giving the user full
179    /// control over the creation of the window.
180    /// This method takes ownership of the winit window and event loop, if this is not desired, use a [WindowedContext] or [HeadlessContext](crate::HeadlessContext) instead.
181    ///
182    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    ///
220    /// Start the main render loop which calls the `callback` closure each frame.
221    ///
222    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    ///
296    /// Return the current logical size of the window.
297    ///
298    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    ///
306    /// Returns the current viewport of the window in physical pixels (the size of the screen returned from [FrameInput::screen]).
307    ///
308    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    ///
314    /// Returns the device pixel ratio for this window.
315    ///
316    pub fn device_pixel_ratio(&self) -> f32 {
317        self.window.scale_factor() as f32
318    }
319
320    ///
321    /// Returns the graphics context for this window.
322    ///
323    pub fn gl(&self) -> Context {
324        (*self.gl).clone()
325    }
326}