use super::App;
use crate::Pt;
use crate::platform;
use crate::scenes::take_quit_request;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::window::{Window, WindowId};
pub(crate) struct PlatformData {
pub(crate) window: Option<Window>,
pub(crate) window_id: Option<WindowId>,
#[cfg(all(target_os = "ios", feature = "sensors"))]
pub(crate) sensor_state: Option<super::ios::IosSensorState>,
}
impl PlatformData {
pub(crate) fn new() -> Self {
Self {
window: None,
window_id: None,
#[cfg(all(target_os = "ios", feature = "sensors"))]
sensor_state: None,
}
}
}
impl App {
fn sync_window_metrics(&mut self, width: u32, height: u32) {
self.ctx.set_scale_factor(self.scale_factor);
self.ctx.set_window_logical_size(
Pt::from_physical_px(width as f64, self.scale_factor),
Pt::from_physical_px(height as f64, self.scale_factor),
);
}
fn request_redraw(&self) {
if let Some(window) = self.platform.window.as_ref() {
window.request_redraw();
}
}
fn create_window_if_needed(&mut self, event_loop: &ActiveEventLoop) {
if self.platform.window.is_some() {
return;
}
let width = self.window_config.width.0.max(1.0) as f64;
let height = self.window_config.height.0.max(1.0) as f64;
let attributes = Window::default_attributes()
.with_title(self.window_config.title.clone())
.with_resizable(self.window_config.resizable)
.with_transparent(true);
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
let attributes = {
use wasm_bindgen::JsCast;
use winit::platform::web::WindowAttributesExtWebSys;
let canvas = self.platform.canvas_id.as_deref().and_then(|id| {
web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id(id))
.and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok())
});
attributes
.with_inner_size(winit::dpi::LogicalSize::new(width, height))
.with_canvas(canvas)
};
#[cfg(not(any(target_os = "ios", target_os = "android", target_arch = "wasm32")))]
let attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(width, height));
let window = event_loop
.create_window(attributes)
.expect("failed to create window");
window.set_ime_allowed(true);
self.scale_factor = window.scale_factor();
let size = window.inner_size();
self.sync_window_metrics(size.width, size.height);
eprintln!(
"[spot][init] Window created: {}x{} (dpr: {})",
size.width, size.height, self.scale_factor
);
self.platform.window_id = Some(window.id());
self.platform.window = Some(window);
}
fn ensure_audio_initialized(&mut self) {
if self.ctx.runtime.audio.is_none() {
match crate::audio::AudioSystem::new() {
Ok(audio) => self.ctx.runtime.audio = Some(audio),
Err(e) => eprintln!("[spot][audio] initialization failed: {:?}", e),
}
}
}
fn ensure_surface(&mut self) {
if self.surface.is_some() {
return;
}
let Some(window) = self.platform.window.as_ref() else {
return;
};
let size = window.inner_size();
match self.instance.create_surface(window) {
Ok(surface) => {
let surface = unsafe {
std::mem::transmute::<wgpu::Surface<'_>, wgpu::Surface<'static>>(surface)
};
self.surface = Some(surface);
if let platform::GraphicsInitState::Ready(_) = self.init_state
&& let Some(surface) = self.surface.as_ref()
&& let Some(g) = self.ctx.runtime.graphics.as_mut()
{
g.resize(surface, size.width, size.height);
}
}
Err(e) => eprintln!("[spot][surface] create failed: {:?}", e),
}
}
fn recreate_surface(&mut self) {
let Some(window) = self.platform.window.as_ref() else {
return;
};
let size = window.inner_size();
match self.instance.create_surface(window) {
Ok(surface) => {
let surface = unsafe {
std::mem::transmute::<wgpu::Surface<'_>, wgpu::Surface<'static>>(surface)
};
self.surface = Some(surface);
if let Some(surface) = self.surface.as_ref()
&& let Some(g) = self.ctx.runtime.graphics.as_mut()
{
g.resize(surface, size.width, size.height);
}
eprintln!("[spot][surface] Surface recreated successfully.");
}
Err(e) => {
eprintln!("[spot][surface] recreate after error failed: {:?}", e);
self.surface.take();
}
}
}
fn begin_graphics_init_if_needed(&mut self) {
if !matches!(self.init_state, platform::GraphicsInitState::NotStarted) {
return;
}
let Some(surface) = self.surface.as_ref() else {
return;
};
let Some(window) = self.platform.window.as_ref() else {
return;
};
let size = window.inner_size();
#[cfg(not(target_arch = "wasm32"))]
{
platform::begin_graphics_init(
&mut self.init_state,
&self.instance,
surface,
size.width,
size.height,
self.window_config.transparent,
);
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
let instance = self.instance.clone();
let surface_ptr: *const wgpu::Surface<'static> = surface;
let app_ptr: *mut App = self;
platform::begin_graphics_init(
&mut self.init_state,
instance,
surface_ptr,
size.width,
size.height,
self.window_config.transparent,
Box::new(move |graphics_r| unsafe {
super::wasm::handle_wasm_graphics_init_result(app_ptr, graphics_r)
}),
);
}
}
fn ensure_scene_ready(&mut self) {
if self.scene.has_active_scene() {
return;
}
if let Some(graphics) = platform::finalize_graphics(&mut self.init_state) {
self.ctx.runtime.graphics = Some(graphics);
self.scene.initialize_if_missing(&mut self.ctx);
}
}
fn handle_surface_error(&mut self, event_loop: &ActiveEventLoop, error: wgpu::SurfaceError) {
match error {
wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated => {
eprintln!(
"[spot][surface] Surface lost or outdated: {:?}. Recreating...",
error
);
self.recreate_surface();
self.request_redraw();
}
wgpu::SurfaceError::OutOfMemory => event_loop.exit(),
wgpu::SurfaceError::Timeout | wgpu::SurfaceError::Other => {
eprintln!("[spot][surface] draw error: {:?}", error);
self.request_redraw();
}
}
}
fn draw_frame(&mut self, event_loop: &ActiveEventLoop) {
self.ensure_scene_ready();
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if self.scene.has_active_scene() {
self.sync_canvas_resize();
}
let Some(surface) = self.surface.as_ref() else {
return;
};
self.ctx.begin_frame();
if let Some(spot) = self.scene.spot_mut() {
spot.draw(&mut self.ctx);
}
self.scene.apply_pending_switch(&mut self.ctx);
let mut graphics = self.ctx.runtime.graphics.take();
let draw_result = graphics
.as_mut()
.map(|g| g.draw_context(surface, &mut self.ctx));
self.ctx.runtime.graphics = graphics;
if let Some(Err(error)) = draw_result {
self.handle_surface_error(event_loop, error);
}
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
web_sys::console::log_1(&"[spot][wasm] resumed() called".into());
event_loop.set_control_flow(ControlFlow::Poll);
self.timing.reset();
self.create_window_if_needed(event_loop);
self.ensure_audio_initialized();
self.ensure_surface();
self.begin_graphics_init_if_needed();
if let Some(spot) = self.scene.spot_mut() {
spot.resumed(&mut self.ctx);
}
#[cfg(all(target_os = "ios", feature = "sensors"))]
{
if self.platform.sensor_state.is_none() {
self.platform.sensor_state = Some(super::ios::IosSensorState::new());
}
if let Some(state) = self.platform.sensor_state.as_ref() {
state.enable();
}
}
self.request_redraw();
}
fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(spot) = self.scene.spot_mut() {
spot.suspended(&mut self.ctx);
}
#[cfg(all(target_os = "ios", feature = "sensors"))]
if let Some(state) = self.platform.sensor_state.as_ref() {
state.disable();
}
self.surface.take();
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Focused(focused) => self.ctx.input_mut().handle_focus(focused),
WindowEvent::Ime(ime) => self.ctx.input_mut().handle_ime(ime),
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.scale_factor = scale_factor;
if let Some(window) = self.platform.window.as_ref() {
let size = window.inner_size();
self.sync_window_metrics(size.width, size.height);
}
}
WindowEvent::Resized(new_size) => {
if let Some(surface) = self.surface.as_ref()
&& let Some(g) = self.ctx.runtime.graphics.as_mut()
{
g.resize(surface, new_size.width, new_size.height);
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
self.platform.last_physical_size =
Some((new_size.width.max(1), new_size.height.max(1)));
}
self.sync_window_metrics(new_size.width, new_size.height);
}
WindowEvent::CursorMoved { position, .. } => {
let x = Pt::from_physical_px(position.x, self.scale_factor);
let y = Pt::from_physical_px(position.y, self.scale_factor);
self.ctx.input_mut().handle_cursor_moved(x, y);
}
WindowEvent::Touch(touch) => {
self.ctx.input_mut().handle_touch(touch, self.scale_factor);
}
WindowEvent::MouseInput { state, button, .. } => {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
self.init_audio_on_gesture();
platform::try_resume_audio(&mut self.ctx);
}
self.ctx.input_mut().handle_mouse_input(state, button);
}
WindowEvent::MouseWheel { delta, .. } => {
self.ctx.input_mut().handle_mouse_wheel(delta);
}
WindowEvent::KeyboardInput { event, .. } => {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
self.init_audio_on_gesture();
platform::try_resume_audio(&mut self.ctx);
}
self.ctx
.input_mut()
.handle_keyboard_input(event.state, event.physical_key);
if matches!(event.state, winit::event::ElementState::Pressed)
&& let Some(text) = event.text.as_deref()
{
for ch in text.chars() {
self.ctx.input_mut().handle_received_character(ch);
}
}
}
WindowEvent::RedrawRequested => self.draw_frame(event_loop),
_ => {}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if take_quit_request() {
event_loop.exit();
return;
}
self.timing.run_updates(8, |dt| {
#[cfg(all(target_os = "ios", feature = "sensors"))]
if let Some(state) = self.platform.sensor_state.as_ref() {
state.poll(&mut self.ctx.input_mut());
}
if let Some(spot) = self.scene.spot_mut() {
spot.update(&mut self.ctx, dt);
}
self.ctx.input_mut().end_frame();
});
self.request_redraw();
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
self.surface.take();
self.platform.window.take();
}
}