use {
crate::{
context::Context,
r#loop::{Input, Keys, Loop, Mouse},
render::{RenderContext, RenderResult},
time::Time,
},
winit::{
event_loop::{EventLoop, EventLoopBuilder},
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, config: CanvasConfig, make_loop: M) -> Error
where
M: FnOnce(&mut Context) -> L,
L: Loop + 'static,
{
pollster::block_on(self.run(config, make_loop))
}
pub async fn run<M, L>(self, config: CanvasConfig, make_loop: M) -> Error
where
M: FnOnce(&mut Context) -> L,
L: Loop + 'static,
{
let Self { event_loop, window } = self;
let mut context = {
let render_context = match RenderContext::new(config, &window).await {
Ok(render) => render,
Err(err) => return err,
};
Box::new(Context::new(
window,
event_loop.create_proxy(),
render_context,
))
};
let mut lp = make_loop(&mut context);
let mut active = false;
let mut time = Time::new();
let mut cursor_position = None;
let mut last_touch = None;
let mut mouse = Mouse::default();
let mut pressed_keys = vec![];
let mut released_keys = vec![];
event_loop.run(move |ev, _, flow| {
use {
std::time::Duration,
wgpu::SurfaceError,
winit::{
dpi::PhysicalPosition,
event::{
DeviceEvent, ElementState, Event, KeyboardInput, MouseButton,
MouseScrollDelta, StartCause, Touch, TouchPhase, WindowEvent,
},
},
};
const WAIT_TIME: f32 = 0.1;
match ev {
Event::NewEvents(cause) => match cause {
StartCause::ResumeTimeReached { .. } => {
log::info!("resume time reached");
context.window.request_redraw();
}
StartCause::WaitCancelled {
requested_resume, ..
} => {
log::info!("wait cancelled");
if let Some(resume) = requested_resume {
flow.set_wait_until(resume);
}
}
StartCause::Poll => {
log::info!("poll");
flow.set_wait_timeout(Duration::from_secs_f32(WAIT_TIME));
}
StartCause::Init => log::info!("init"),
},
Event::WindowEvent { event, window_id } if window_id == context.window.id() => {
log::info!("window event: {event:?}");
match event {
WindowEvent::Resized(size)
| WindowEvent::ScaleFactorChanged {
new_inner_size: &mut size,
..
} => context.render.resize(size.into()),
WindowEvent::CloseRequested if lp.close_requested() => flow.set_exit(),
WindowEvent::Focused(true) => context.window.request_redraw(),
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(_) => {}
},
WindowEvent::Touch(Touch {
phase,
location: PhysicalPosition { x, y },
..
}) => match phase {
TouchPhase::Started => {}
TouchPhase::Moved => {
let (nx, ny) = (x as f32, y as f32);
if let Some((lx, ly)) = last_touch {
mouse.motion_delta.0 = lx - nx;
mouse.motion_delta.1 = ly - ny;
}
last_touch = Some((nx, ny));
}
TouchPhase::Ended | TouchPhase::Cancelled => last_touch = None,
},
_ => {}
}
}
Event::RedrawRequested(window_id) if window_id == context.window.id() => {
log::info!(
"redraw requested {active}",
active = if active { "(active)" } else { "" },
);
if !active {
flow.set_wait_timeout(Duration::from_secs_f32(WAIT_TIME));
return;
}
let delta_time = time.delta();
if let Some(min_delta_time) = context.min_frame_delta_time() {
if delta_time < min_delta_time {
let wait = min_delta_time - delta_time;
flow.set_wait_timeout(Duration::from_secs_f32(wait));
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, &context.resources) {
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.set_screen(None);
}
RenderResult::SurfaceError(SurfaceError::OutOfMemory) => {
log::error!("suface error: out of memory");
flow.set_exit();
}
RenderResult::Error(err) => lp.error_occurred(err),
}
}
Event::DeviceEvent {
event: DeviceEvent::MouseMotion { delta: (x, y) },
..
} => {
log::info!("device event: mouse motion");
mouse.motion_delta.0 += x as f32;
mouse.motion_delta.1 += y as f32;
}
Event::UserEvent(CanvasEvent::Close) if lp.close_requested() => {
log::info!("user event: close");
flow.set_exit();
}
Event::Suspended => {
log::info!("suspended");
context.render.drop_surface();
active = false;
}
Event::Resumed => {
log::info!("resumed");
context.render.recreate_surface(&context.window);
context.render.resize(context.window.inner_size().into());
active = true;
context.window.request_redraw();
time.reset();
}
_ => {}
}
})
}
}
#[derive(Clone, Copy, Debug)]
#[must_use]
pub enum Error {
BackendSelection,
RequestDevice,
}
impl Error {
pub fn into_panic(self) {
panic!("{self:?}");
}
}
pub(crate) enum CanvasEvent {
Close,
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod window {
use super::*;
#[must_use]
pub fn make_window(state: InitialState) -> Canvas {
use winit::{dpi::PhysicalSize, window::Fullscreen};
let mut builder = EventLoopBuilder::with_user_event();
#[cfg(target_os = "linux")]
{
use {std::env, winit::platform::x11::EventLoopBuilderExtX11};
builder.with_x11();
env::remove_var("WAYLAND_DISPLAY"); }
let event_loop = builder.build();
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 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, 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 }
}
#[cfg(target_os = "android")]
pub(crate) mod android {
use super::*;
use winit::platform::android::activity::AndroidApp;
pub fn from_app(app: AndroidApp) -> Canvas {
use winit::platform::android::EventLoopBuilderExtAndroid;
let event_loop = EventLoopBuilder::with_user_event()
.with_android_app(app)
.build();
let window = WindowBuilder::new()
.build(&event_loop)
.expect("build window");
Canvas { event_loop, window }
}
}
#[derive(Default)]
pub struct CanvasConfig {
pub backend: Backend,
pub selector: Selector,
}
#[derive(Default)]
pub enum Selector {
#[default]
Auto,
#[cfg(not(target_arch = "wasm32"))]
Callback(Box<dyn FnMut(Vec<SelectorEntry>) -> Option<usize>>),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Backend {
#[cfg_attr(not(target_arch = "wasm32"), default)]
Vulkan,
#[cfg_attr(target_arch = "wasm32", default)]
Gl,
Dx12,
Dx11,
Metal,
WebGpu,
}
#[derive(Debug)]
pub struct SelectorEntry {
pub name: String,
pub device: Device,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Device {
IntegratedGpu,
DiscreteGpu,
VirtualGpu,
Cpu,
}