use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use winit::application::ApplicationHandler;
use winit::event::{ElementState, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId};
use crate::Game;
use crate::context::{self, Context, GameEvent, Parts};
#[derive(Clone)]
pub struct AppConfig {
pub title: String,
pub width: u32,
pub height: u32,
pub vsync: bool,
pub fixed_timestep: Option<Duration>,
pub asset_root: std::path::PathBuf,
pub web_preload: Option<Vec<String>>,
pub canvas_id: Option<String>,
pub random_seed: u64,
pub depth_format: Option<wgpu::TextureFormat>,
pub msaa_samples: u32,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
title: "Game".into(),
width: 1280,
height: 720,
vsync: true,
fixed_timestep: None,
asset_root: std::path::PathBuf::from("assets"),
web_preload: None,
canvas_id: None,
random_seed: crate::Rng::DEFAULT_SEED,
depth_format: None,
msaa_samples: 1,
}
}
}
type InitDone = Box<(Arc<Window>, Result<Parts>)>;
pub fn run<G: Game>(config: AppConfig) -> Result<()> {
#[cfg(target_arch = "wasm32")]
crate::web::install_hooks();
let event_loop = EventLoop::<InitDone>::with_user_event().build()?;
event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut runner = AppRunner::<G> {
config,
state: None,
accumulator: Duration::ZERO,
#[cfg(target_arch = "wasm32")]
proxy: event_loop.create_proxy(),
#[cfg(target_arch = "wasm32")]
initializing: false,
};
#[cfg(not(target_arch = "wasm32"))]
event_loop.run_app(&mut runner)?;
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::EventLoopExtWebSys;
event_loop.spawn_app(runner);
}
Ok(())
}
struct AppRunner<G: Game> {
config: AppConfig,
state: Option<RunState<G>>,
accumulator: Duration,
#[cfg(target_arch = "wasm32")]
proxy: winit::event_loop::EventLoopProxy<InitDone>,
#[cfg(target_arch = "wasm32")]
initializing: bool,
}
struct RunState<G: Game> {
ctx: Context,
game: G,
}
impl<G: Game> ApplicationHandler<InitDone> for AppRunner<G> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_some() {
return;
}
#[cfg(target_arch = "wasm32")]
if self.initializing {
return;
}
let window = match context::create_window(event_loop, &self.config) {
Ok(w) => w,
Err(e) => {
log::error!("window creation failed: {e:?}");
event_loop.exit();
return;
}
};
#[cfg(not(target_arch = "wasm32"))]
{
let parts =
pollster::block_on(context::init_parts(window.clone(), self.config.clone()));
self.finish_init(event_loop, window, parts);
}
#[cfg(target_arch = "wasm32")]
{
self.initializing = true;
let proxy = self.proxy.clone();
let config = self.config.clone();
wasm_bindgen_futures::spawn_local(async move {
let parts = context::init_parts(window.clone(), config).await;
if proxy.send_event(Box::new((window, parts))).is_err() {
log::error!("event loop closed before init finished");
}
});
}
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: InitDone) {
let (window, parts) = *event;
#[cfg(target_arch = "wasm32")]
{
self.initializing = false;
}
self.finish_init(event_loop, window, parts);
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let Some(state) = self.state.as_mut() else {
return;
};
if is_user_gesture(&event) {
state.ctx.start_audio();
}
let consumed = state.game.raw_window_event(&mut state.ctx, &event);
match event {
WindowEvent::CloseRequested => {
state.game.event(&mut state.ctx, &GameEvent::CloseRequested);
event_loop.exit();
}
WindowEvent::Resized(size) => {
state.ctx.gfx.resize(size.width, size.height);
state.game.event(
&mut state.ctx,
&GameEvent::Resized {
width: size.width,
height: size.height,
},
);
}
WindowEvent::Focused(focused) => {
state
.ctx
.input
.handle_window_event(&WindowEvent::Focused(focused));
state
.game
.event(&mut state.ctx, &GameEvent::FocusChanged(focused));
}
WindowEvent::RedrawRequested => {
self.frame(event_loop);
}
ref other if !consumed => {
state.ctx.input.handle_window_event(other);
}
_ => {}
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(state) = self.state.as_ref() {
state.ctx.window.request_redraw();
}
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(state) = self.state.as_mut() {
state.game.shutdown(&mut state.ctx);
}
}
}
fn is_user_gesture(event: &WindowEvent) -> bool {
match event {
WindowEvent::KeyboardInput { event, .. } => event.state == ElementState::Pressed,
WindowEvent::MouseInput { state, .. } => *state == ElementState::Pressed,
WindowEvent::Touch(_) => true,
_ => false,
}
}
impl<G: Game> AppRunner<G> {
fn finish_init(
&mut self,
event_loop: &ActiveEventLoop,
window: Arc<Window>,
parts: Result<Parts>,
) {
let parts = match parts {
Ok(p) => p,
Err(e) => {
log::error!("context init failed: {e:?}");
event_loop.exit();
return;
}
};
let mut ctx = Context::from_parts(window, parts, &self.config);
let size = ctx.window.inner_size();
ctx.gfx.resize(size.width, size.height);
let game = match G::init(&mut ctx) {
Ok(g) => g,
Err(e) => {
log::error!("game init failed: {e:?}");
event_loop.exit();
return;
}
};
ctx.window.request_redraw();
self.state = Some(RunState { ctx, game });
}
fn frame(&mut self, event_loop: &ActiveEventLoop) {
let Some(state) = self.state.as_mut() else {
return;
};
state.ctx.time.tick();
state.ctx.input.poll_gamepads();
match self.config.fixed_timestep {
Some(step) => {
self.accumulator += state.ctx.time.delta;
while self.accumulator >= step {
state.game.update(&mut state.ctx, step.as_secs_f32());
self.accumulator -= step;
}
}
None => {
let dt = state.ctx.time.delta.as_secs_f32();
state.game.update(&mut state.ctx, dt);
}
}
if state.ctx.quit_requested {
event_loop.exit();
return;
}
state.ctx.input.end_frame();
match state.ctx.gfx.begin_frame() {
Ok(mut frame) => {
state.game.render(&mut state.ctx, &mut frame);
state.ctx.gfx.present(frame);
}
Err(e) => {
log::warn!("begin_frame failed: {e:?}");
let (w, h) = state.ctx.gfx.size();
state.ctx.gfx.resize(w, h);
}
}
}
}