use {
crate::{
context::{Context, Limits},
r#loop::{Input, Keys, Loop, Mouse},
render::{Render, RenderResult},
size::Size,
time::Time,
},
std::num::NonZeroU32,
winit::{
event_loop::EventLoop,
window::{Window, WindowBuilder},
},
};
pub struct Canvas {
event_loop: EventLoop<CanvasEvent>,
window: Window,
}
impl Canvas {
#[cfg(not(target_arch = "wasm32"))]
pub fn run_blocking<M, L>(self, make_loop: M) -> !
where
M: FnOnce(&mut Context) -> L,
L: Loop + 'static,
{
pollster::block_on(self.run(make_loop))
}
pub async fn run<M, L>(self, make_loop: M) -> !
where
M: FnOnce(&mut Context) -> L,
L: Loop + 'static,
{
let Self { event_loop, window } = self;
let mut render = Render::new(&window).await;
render.resize({
let (width, height): (u32, u32) = window.inner_size().into();
Some(Size {
width: width.max(1).try_into().expect("non zero"),
height: height.max(1).try_into().expect("non zero"),
..Default::default()
})
});
let mut context = Context {
window,
proxy: event_loop.create_proxy(),
render,
limits: Limits::default(),
};
let mut lp = make_loop(&mut context);
let mut time = Time::new();
let mut cursor_position = None;
let mut mouse = Mouse::default();
let mut pressed_keys = vec![];
let mut released_keys = vec![];
event_loop.run(move |ev, _, flow| {
use {
wgpu::SurfaceError,
winit::{
dpi::PhysicalPosition,
event::{
DeviceEvent, ElementState, Event, KeyboardInput, MouseButton,
MouseScrollDelta, StartCause, WindowEvent,
},
event_loop::ControlFlow,
},
};
#[allow(clippy::cast_possible_truncation)]
match ev {
Event::WindowEvent { event, window_id } if window_id == context.window.id() => {
match event {
WindowEvent::Resized(size)
| WindowEvent::ScaleFactorChanged {
new_inner_size: &mut size,
..
} => context.render.resize({
let (width, height): (u32, u32) = size.into();
let size = context.render.size();
Some(Size {
width: NonZeroU32::new(width.max(1)).expect("non zero"),
height: NonZeroU32::new(height.max(1)).expect("non zero"),
..size
})
}),
WindowEvent::CloseRequested if lp.close_requested() => {
*flow = ControlFlow::Exit;
}
WindowEvent::KeyboardInput {
input:
KeyboardInput {
state,
virtual_keycode: Some(key),
..
},
..
} => match state {
ElementState::Pressed => pressed_keys.push(key),
ElementState::Released => released_keys.push(key),
},
WindowEvent::CursorMoved { position, .. } => {
cursor_position = Some(position.into());
}
WindowEvent::CursorLeft { .. } => {
cursor_position = None;
}
WindowEvent::MouseWheel { delta, .. } => match delta {
MouseScrollDelta::LineDelta(x, y) => {
mouse.wheel_delta.0 += x;
mouse.wheel_delta.1 += y;
}
MouseScrollDelta::PixelDelta(PhysicalPosition { .. }) => {
}
},
WindowEvent::MouseInput { state, button, .. } => match button {
MouseButton::Left => {
mouse.pressed_left = state == ElementState::Pressed;
}
MouseButton::Right => {
mouse.pressed_right = state == ElementState::Pressed;
}
MouseButton::Middle => {
mouse.pressed_middle = state == ElementState::Pressed;
}
MouseButton::Other(_) => {}
},
_ => {}
}
}
Event::RedrawRequested(window_id) if window_id == context.window.id() => {
let delta_time = time.delta();
if let Some(min_delta_time) = context.limits.min_frame_delta_time {
if delta_time < min_delta_time {
return;
}
}
let input = Input {
delta_time,
cursor_position,
mouse,
pressed_keys: Keys {
keys: &pressed_keys[..],
},
released_keys: Keys {
keys: &released_keys[..],
},
};
time.reset();
if let Err(err) = lp.update(&mut context, &input) {
lp.error_occurred(err);
}
mouse = Mouse::default();
pressed_keys.clear();
released_keys.clear();
match context.render.draw_frame(&lp) {
RenderResult::Ok => {}
RenderResult::SurfaceError(SurfaceError::Timeout) => {
log::error!("suface error: timeout");
}
RenderResult::SurfaceError(SurfaceError::Outdated) => {
log::error!("suface error: outdated");
}
RenderResult::SurfaceError(SurfaceError::Lost) => {
context.render.resize(None);
}
RenderResult::SurfaceError(SurfaceError::OutOfMemory) => {
log::error!("suface error: out of memory");
*flow = ControlFlow::Exit;
}
RenderResult::Error(err) => lp.error_occurred(err),
}
}
Event::DeviceEvent {
event: DeviceEvent::MouseMotion { delta: (x, y) },
..
} => {
mouse.motion_delta.0 += x as f32;
mouse.motion_delta.1 += y as f32;
}
Event::UserEvent(CanvasEvent::Close) => {
if lp.close_requested() {
*flow = ControlFlow::Exit;
}
}
Event::MainEventsCleared => context.window.request_redraw(),
Event::NewEvents(StartCause::Init) => {
_ = time.delta();
}
_ => {}
}
})
}
}
pub(crate) enum CanvasEvent {
Close,
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub fn make_window(state: InitialState) -> Canvas {
use winit::{dpi::PhysicalSize, event_loop::EventLoopBuilder, window::Fullscreen};
let builder = WindowBuilder::new().with_title(state.title);
let builder = match state.mode {
WindowMode::Fullscreen => builder.with_fullscreen(Some(Fullscreen::Borderless(None))),
WindowMode::Windowed { width, height } => {
builder.with_inner_size(PhysicalSize::new(width.max(1), height.max(1)))
}
};
let event_loop = EventLoopBuilder::with_user_event().build();
let window = builder.build(&event_loop).expect("build window");
window.set_cursor_visible(state.show_cursor);
Canvas { event_loop, window }
}
#[derive(Clone, Copy)]
pub struct InitialState<'a> {
pub title: &'a str,
pub mode: WindowMode,
pub show_cursor: bool,
}
impl Default for InitialState<'static> {
fn default() -> Self {
Self {
title: "Dunge",
mode: WindowMode::Fullscreen,
show_cursor: true,
}
}
}
#[derive(Clone, Copy)]
pub enum WindowMode {
Fullscreen,
Windowed { width: u32, height: u32 },
}
#[cfg(target_arch = "wasm32")]
#[must_use]
pub fn from_element(id: &str) -> Canvas {
use {
web_sys::Window,
winit::{dpi::PhysicalSize, event_loop::EventLoopBuilder, platform::web::WindowExtWebSys},
};
let event_loop = EventLoopBuilder::with_user_event().build();
let window = WindowBuilder::new()
.build(&event_loop)
.expect("build window");
let document = web_sys::window()
.as_ref()
.and_then(Window::document)
.expect("get document");
let Some(el) = document.get_element_by_id(id) else {
panic!("an element with id {id:?} not found");
};
window.set_inner_size({
let width = el.client_width().max(1) as u32;
let height = el.client_height().max(1) as u32;
PhysicalSize { width, height }
});
let canvas = window.canvas();
canvas.remove_attribute("style").expect("remove attribute");
el.append_child(&canvas).expect("append child");
Canvas { event_loop, window }
}