#![allow(unsafe_code)]
use crate::core::{Context, CoreError, Viewport};
use winit::event::{Event, WindowEvent};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowBuilder;
use winit::*;
mod settings;
pub use settings::*;
mod frame_io;
pub use frame_io::*;
mod frame_input_generator;
pub use frame_input_generator::*;
mod windowed_context;
pub use windowed_context::*;
use thiserror::Error;
#[cfg(not(target_arch = "wasm32"))]
#[derive(Error, Debug)]
#[allow(missing_docs)]
pub enum WindowError {
#[error("glutin error")]
GlutinError(#[from] glutin::error::Error),
#[error("winit error")]
WinitError(#[from] winit::error::OsError),
#[error("error in three-d")]
ThreeDError(#[from] CoreError),
#[error("the number of MSAA samples must be a power of two")]
InvalidNumberOfMSAASamples,
#[error("it's not possible to create a graphics context/surface with the given settings")]
SurfaceCreationError,
}
#[cfg(target_arch = "wasm32")]
#[derive(Error, Debug)]
#[allow(missing_docs)]
pub enum WindowError {
#[error("failed to create a new winit window")]
WinitError(#[from] winit::error::OsError),
#[error("failed creating a new window")]
WindowCreation,
#[error("unable to get document from canvas")]
DocumentMissing,
#[error("unable to convert canvas to html canvas: {0}")]
CanvasConvertFailed(String),
#[error("unable to get webgl2 context for the given canvas, maybe the browser doesn't support WebGL2{0}")]
WebGL2NotSupported(String),
#[error("unable to get EXT_color_buffer_float extension for the given canvas, maybe the browser doesn't support EXT_color_buffer_float: {0}")]
ColorBufferFloatNotSupported(String),
#[error("unable to get OES_texture_float extension for the given canvas, maybe the browser doesn't support OES_texture_float: {0}")]
OESTextureFloatNotSupported(String),
#[error("error in three-d")]
ThreeDError(#[from] CoreError),
}
pub struct Window {
window: winit::window::Window,
event_loop: EventLoop<()>,
#[cfg(target_arch = "wasm32")]
closure: wasm_bindgen::closure::Closure<dyn FnMut(web_sys::Event)>,
gl: WindowedContext,
#[allow(dead_code)]
maximized: bool,
}
impl Window {
pub fn new(window_settings: WindowSettings) -> Result<Self, WindowError> {
Self::from_event_loop(window_settings, EventLoop::new())
}
pub fn from_event_loop(
window_settings: WindowSettings,
event_loop: EventLoop<()>,
) -> Result<Self, WindowError> {
#[cfg(not(target_arch = "wasm32"))]
let window_builder = {
let window_builder = WindowBuilder::new()
.with_title(&window_settings.title)
.with_min_inner_size(dpi::LogicalSize::new(
window_settings.min_size.0,
window_settings.min_size.1,
))
.with_decorations(!window_settings.borderless);
if let Some((width, height)) = window_settings.max_size {
window_builder
.with_inner_size(dpi::LogicalSize::new(width as f64, height as f64))
.with_max_inner_size(dpi::LogicalSize::new(width as f64, height as f64))
} else {
window_builder.with_maximized(true)
}
};
#[cfg(target_arch = "wasm32")]
let window_builder = {
use wasm_bindgen::JsCast;
use winit::{dpi::LogicalSize, platform::web::WindowBuilderExtWebSys};
let canvas = if let Some(canvas) = window_settings.canvas {
canvas
} else {
web_sys::window()
.ok_or(WindowError::WindowCreation)?
.document()
.ok_or(WindowError::DocumentMissing)?
.get_elements_by_tag_name("canvas")
.item(0)
.expect(
"settings doesn't contain canvas and DOM doesn't have a canvas element either",
)
.dyn_into::<web_sys::HtmlCanvasElement>()
.map_err(|e| WindowError::CanvasConvertFailed(format!("{:?}", e)))?
};
let inner_size = window_settings
.max_size
.map(|(width, height)| LogicalSize::new(width as f64, height as f64))
.unwrap_or_else(|| {
let browser_window = canvas
.owner_document()
.and_then(|doc| doc.default_view())
.or_else(web_sys::window)
.unwrap();
LogicalSize::new(
browser_window.inner_width().unwrap().as_f64().unwrap(),
browser_window.inner_height().unwrap().as_f64().unwrap(),
)
});
WindowBuilder::new()
.with_title(window_settings.title)
.with_canvas(Some(canvas))
.with_inner_size(inner_size)
.with_prevent_default(true)
};
let winit_window = window_builder.build(&event_loop)?;
Self::from_winit_window(
winit_window,
event_loop,
window_settings.surface_settings,
window_settings.max_size.is_none(),
)
}
pub fn from_winit_window(
winit_window: window::Window,
event_loop: EventLoop<()>,
mut surface_settings: SurfaceSettings,
maximized: bool,
) -> Result<Self, WindowError> {
let mut gl = WindowedContext::from_winit_window(&winit_window, surface_settings);
if gl.is_err() {
surface_settings.multisamples = 0;
gl = WindowedContext::from_winit_window(&winit_window, surface_settings);
}
#[cfg(target_arch = "wasm32")]
let closure = {
use wasm_bindgen::JsCast;
use winit::platform::web::WindowExtWebSys;
let closure =
wasm_bindgen::closure::Closure::wrap(Box::new(move |event: web_sys::Event| {
event.prevent_default();
}) as Box<dyn FnMut(_)>);
winit_window
.canvas()
.add_event_listener_with_callback("contextmenu", closure.as_ref().unchecked_ref())
.expect("failed to listen to canvas context menu");
closure
};
Ok(Self {
window: winit_window,
event_loop,
gl: gl?,
#[cfg(target_arch = "wasm32")]
closure,
maximized,
})
}
pub fn render_loop<F: 'static + FnMut(FrameInput) -> FrameOutput>(self, mut callback: F) {
let mut frame_input_generator = FrameInputGenerator::from_winit_window(&self.window);
self.event_loop
.run(move |event, _, control_flow| match event {
Event::LoopDestroyed => {
#[cfg(target_arch = "wasm32")]
{
use wasm_bindgen::JsCast;
use winit::platform::web::WindowExtWebSys;
self.window
.canvas()
.remove_event_listener_with_callback(
"contextmenu",
self.closure.as_ref().unchecked_ref(),
)
.unwrap();
}
}
Event::MainEventsCleared => {
self.window.request_redraw();
}
Event::RedrawRequested(_) => {
#[cfg(target_arch = "wasm32")]
if self.maximized || option_env!("THREE_D_SCREENSHOT").is_some() {
use winit::platform::web::WindowExtWebSys;
let html_canvas = self.window.canvas();
let browser_window = html_canvas
.owner_document()
.and_then(|doc| doc.default_view())
.or_else(web_sys::window)
.unwrap();
self.window.set_inner_size(dpi::LogicalSize {
width: browser_window.inner_width().unwrap().as_f64().unwrap(),
height: browser_window.inner_height().unwrap().as_f64().unwrap(),
});
}
let frame_input = frame_input_generator.generate(&self.gl);
let frame_output = callback(frame_input);
if frame_output.exit {
*control_flow = ControlFlow::Exit;
} else {
if frame_output.swap_buffers && option_env!("THREE_D_SCREENSHOT").is_none()
{
self.gl.swap_buffers().unwrap();
}
if frame_output.wait_next_event {
*control_flow = ControlFlow::Wait;
} else {
*control_flow = ControlFlow::Poll;
self.window.request_redraw();
}
}
}
Event::WindowEvent { ref event, .. } => {
frame_input_generator.handle_winit_window_event(event);
match event {
WindowEvent::Resized(physical_size) => {
self.gl.resize(*physical_size);
}
WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
self.gl.resize(**new_inner_size);
}
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
_ => (),
}
}
_ => (),
});
}
pub fn size(&self) -> (u32, u32) {
self.window
.inner_size()
.to_logical::<f64>(self.window.scale_factor())
.into()
}
pub fn viewport(&self) -> Viewport {
let (w, h): (u32, u32) = self.window.inner_size().into();
Viewport::new_at_origo(w, h)
}
pub fn device_pixel_ratio(&self) -> f32 {
self.window.scale_factor() as f32
}
pub fn gl(&self) -> Context {
(*self.gl).clone()
}
}