use crate::prelude::*;
use crate::ecs::world::World;
#[cfg(target_arch = "wasm32")]
use crate::render::wgpu;
use winit::{
event::{DeviceEvent, WindowEvent},
event_loop::{ControlFlow, EventLoop},
};
#[cfg(all(not(target_arch = "wasm32"), feature = "tracing"))]
pub use crate::plugins::log::log_file_name;
pub use crate::plugins::log::{LogConfig, LogRotation};
pub use crate::state::{NextStateBuilder, RenderResources, State};
#[cfg(feature = "egui")]
mod egui_support;
mod file_drop;
mod input_translation;
mod pacing;
pub mod offscreen;
pub use offscreen::*;
#[cfg(not(target_arch = "wasm32"))]
pub mod pump;
use file_drop::process_dropped_files;
use input_translation::{forward_window_events, input_event_system};
use pacing::{
refresh_macos_frame_rate_limit, sync_macos_auto_frame_rate_limit, throttle_to_frame_rate_limit,
};
#[cfg(target_arch = "wasm32")]
use file_drop::setup_wasm_file_drop;
#[cfg(not(target_arch = "wasm32"))]
use input_translation::recenter_locked_cursor;
#[cfg(target_arch = "wasm32")]
use pacing::wasm_capped_dpr;
pub fn launch(state: impl State + 'static) -> Result<(), Box<dyn std::error::Error>> {
launch_windowed(state)
}
pub fn launch_windowed(state: impl State + 'static) -> Result<(), Box<dyn std::error::Error>> {
launch_with_world(state, World::default())
}
pub fn launch_with_world(
state: impl State + 'static,
world: World,
) -> Result<(), Box<dyn std::error::Error>> {
let state = Box::new(state);
let event_loop = EventLoop::builder().build()?;
event_loop.set_control_flow(ControlFlow::Poll);
#[cfg(not(target_arch = "wasm32"))]
{
let mut context = WindowContext {
state,
world,
renderer: None,
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
feature = "core"
))]
accesskit: None,
initialized: false,
};
event_loop.run_app(&mut context)?;
}
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::EventLoopExtWebSys;
let context = WindowContext {
state,
world,
renderer: None,
renderer_receiver: None,
initialized: false,
};
event_loop.spawn_app(context);
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn sync_min_window_size(world: &mut World) {
let desired = world
.res::<crate::render::config::RendererState>()
.min_window_size;
if world
.res::<crate::ecs::window::resources::Window>()
.applied_min_window_size
== desired
{
return;
}
let Some(handle) = world
.res::<crate::ecs::window::resources::Window>()
.handle
.as_ref()
else {
return;
};
match desired {
Some((min_width, min_height)) => {
handle.set_min_inner_size(Some(winit::dpi::LogicalSize::new(
min_width as f64,
min_height as f64,
)));
let scale = handle.scale_factor();
let physical_min_width = (min_width as f64 * scale).round() as u32;
let physical_min_height = (min_height as f64 * scale).round() as u32;
let current = handle.inner_size();
if current.width < physical_min_width || current.height < physical_min_height {
let _ = handle.request_inner_size(winit::dpi::PhysicalSize::new(
current.width.max(physical_min_width),
current.height.max(physical_min_height),
));
}
}
None => {
handle.set_min_inner_size(None::<winit::dpi::LogicalSize<f64>>);
}
}
world
.res_mut::<crate::ecs::window::resources::Window>()
.applied_min_window_size = desired;
}
#[cfg(target_arch = "wasm32")]
fn sync_min_window_size(_world: &mut World) {}
#[cfg(not(target_arch = "wasm32"))]
fn sync_window_title(world: &mut World) {
let (title, handle) = {
let window = world.res::<crate::ecs::window::resources::Window>();
if window.applied_title.as_deref() == Some(&window.title) {
return;
}
(window.title.clone(), window.handle.clone())
};
let Some(handle) = handle else {
return;
};
handle.set_title(&title);
world
.res_mut::<crate::ecs::window::resources::Window>()
.applied_title = Some(title);
}
#[cfg(target_arch = "wasm32")]
fn sync_window_title(_world: &mut World) {}
#[cfg(all(not(target_arch = "wasm32"), feature = "assets"))]
fn sync_window_icon(world: &mut World) {
let current = world
.res::<crate::ecs::window::resources::Window>()
.icon_bytes;
let applied = world
.res::<crate::ecs::window::resources::Window>()
.applied_icon_bytes;
let unchanged = match (current, applied) {
(None, None) => true,
(Some(left), Some(right)) => std::ptr::eq(left.as_ptr(), right.as_ptr()),
_ => false,
};
if unchanged {
return;
}
let Some(handle) = world
.res::<crate::ecs::window::resources::Window>()
.handle
.as_ref()
else {
return;
};
let icon = current.and_then(|bytes| {
let image = image::load_from_memory(bytes).ok()?;
let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions();
winit::window::Icon::from_rgba(rgba.into_raw(), width, height).ok()
});
handle.set_window_icon(icon);
world
.res_mut::<crate::ecs::window::resources::Window>()
.applied_icon_bytes = current;
}
#[cfg(any(target_arch = "wasm32", not(feature = "assets")))]
fn sync_window_icon(_world: &mut World) {}
pub(crate) fn step(
world: &mut World,
state: &mut Box<dyn State + 'static>,
renderer: &mut Option<crate::render::wgpu::WgpuRenderer>,
event: &WindowEvent,
) -> Option<Box<dyn State>> {
let delta_ms = (world.res::<crate::ecs::time::Time>().delta_time * 1000.0) as u32;
let _span = tracing::info_span!("frame", delta_ms).entered();
sync_min_window_size(world);
sync_window_title(world);
sync_window_icon(world);
if matches!(event, WindowEvent::RedrawRequested) {
sync_macos_auto_frame_rate_limit(world);
throttle_to_frame_rate_limit(world);
}
handle_event_systems(world, event);
match event {
WindowEvent::RedrawRequested => match renderer.as_mut() {
Some(renderer) => run_frame_body(world, state.as_mut(), renderer),
None if !world
.res::<crate::ecs::window::resources::Window>()
.create_renderer =>
{
run_frame_logic(world, state.as_mut())
}
None => None,
},
event => {
forward_window_events(world, event);
None
}
}
}
pub fn run_frame_body(
world: &mut World,
state: &mut dyn State,
renderer: &mut crate::render::wgpu::WgpuRenderer,
) -> Option<Box<dyn State>> {
if let Some(next_state) = run_frame_logic(world, state) {
return Some(next_state);
}
let (viewport_width, viewport_height) = if let Some(window_handle) = world
.res::<crate::ecs::window::resources::Window>()
.handle
.as_ref()
{
let size = window_handle.inner_size();
(size.width, size.height)
} else {
world
.res::<crate::ecs::window::resources::Window>()
.cached_viewport_size
.unwrap_or((0, 0))
};
let should_render = viewport_width > 0 && viewport_height > 0;
if should_render {
if let Err(error) = crate::render_driver::update_with_state(renderer, state, world) {
tracing::error!("Failed to update render graph: {error}");
}
state.pre_render(renderer, world);
if let Err(error) = crate::render_driver::render_frame(renderer, world) {
tracing::error!("Failed to draw frame: {error}");
}
}
None
}
pub(crate) fn run_frame_logic(world: &mut World, state: &mut dyn State) -> Option<Box<dyn State>> {
world.ecs.worlds[CORE].increment_tick();
process_dropped_files(world);
#[cfg(target_arch = "wasm32")]
crate::ecs::world::commands::sync_cursor_lock_state(world);
#[cfg(not(target_arch = "wasm32"))]
recenter_locked_cursor(world);
#[cfg(feature = "egui")]
egui_support::begin_frame(world);
state.run_systems(world);
world
.res_mut::<crate::ecs::input::resources::Input>()
.events
.clear();
#[cfg(feature = "egui")]
egui_support::finish_frame(world);
if let Some(builder) = world.res_mut::<crate::state::PendingState>().0.take() {
return Some(builder(world));
}
let window_handle = world
.res::<crate::ecs::window::resources::Window>()
.handle
.clone();
if world
.res::<crate::ecs::ui::resources::RetainedUiState>()
.visible
&& let Some(window_handle) = window_handle
{
let requested = world
.res::<crate::ecs::ui::resources::RetainedUiState>()
.interaction
.requested_cursor;
let applied = world
.res::<crate::ecs::ui::resources::RetainedUiState>()
.interaction
.applied_cursor;
if requested.is_some() && requested != applied {
if let Some(cursor) = requested {
window_handle.set_cursor(cursor);
}
world
.res_mut::<crate::ecs::ui::resources::RetainedUiState>()
.interaction
.applied_cursor = requested;
} else if requested.is_none()
&& applied.is_some()
&& applied != Some(winit::window::CursorIcon::Default)
{
window_handle.set_cursor(winit::window::CursorIcon::Default);
world
.res_mut::<crate::ecs::ui::resources::RetainedUiState>()
.interaction
.applied_cursor = Some(winit::window::CursorIcon::Default);
}
}
None
}
fn handle_event_systems(world: &mut World, event: &WindowEvent) {
let _span = tracing::info_span!("event_handling").entered();
#[cfg(feature = "egui")]
egui_support::handle_window_event(world, event);
input_event_system(world, event);
timing_system(world, event);
}
fn timing_system(world: &mut World, event: &WindowEvent) {
let _span = tracing::info_span!("timing").entered();
if let WindowEvent::RedrawRequested = event {
advance_timing(world);
}
}
pub fn advance_timing(world: &mut World) {
use web_time::Instant;
let now = Instant::now();
let timing = world.res_mut::<crate::ecs::time::Time>();
if timing.initial_frame_start_instant.is_none() {
timing.initial_frame_start_instant = Some(now);
}
timing.raw_delta_time = timing
.last_frame_start_instant
.map_or(0.0, |last_frame| (now - last_frame).as_secs_f32());
timing.delta_time = if timing.paused {
0.0
} else {
timing.raw_delta_time * timing.time_speed
};
timing.last_frame_start_instant = Some(now);
if timing.current_frame_start_instant.is_none() {
timing.current_frame_start_instant = Some(now);
}
if let Some(app_start) = timing.initial_frame_start_instant.as_ref() {
timing.uptime_milliseconds = (now - *app_start).as_millis() as u64;
}
timing.frame_counter += 1;
if let Some(start) = timing.current_frame_start_instant.as_ref() {
if (now - *start).as_secs_f32() >= 1.0 {
timing.frames_per_second = timing.frame_counter as f32;
timing.frame_counter = 0;
timing.current_frame_start_instant = Some(now);
}
} else {
timing.current_frame_start_instant = Some(now);
}
}
pub(crate) fn apply_surface_resize(
world: &mut World,
renderer: &mut Option<crate::render::wgpu::WgpuRenderer>,
width: u32,
height: u32,
) {
world
.res_mut::<crate::ecs::window::resources::Window>()
.cached_viewport_size = Some((width, height));
if let Some(renderer) = renderer.as_mut()
&& let Err(error) = renderer.resize_surface(width, height)
{
tracing::error!("Failed to resize surface: {error}");
}
}
pub struct WindowContext {
pub world: World,
pub state: Box<dyn State>,
pub renderer: Option<crate::render::wgpu::WgpuRenderer>,
#[cfg(target_arch = "wasm32")]
pub renderer_receiver:
Option<futures::channel::oneshot::Receiver<crate::render::wgpu::WgpuRenderer>>,
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
feature = "core"
))]
pub accesskit: Option<crate::ecs::ui::accessibility::AccessKitHost>,
pub initialized: bool,
}
impl winit::application::ApplicationHandler for WindowContext {
fn suspended(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
crate::ecs::events::emit_app_event(
&mut self.world,
crate::ecs::input::events::AppEvent::Suspended,
);
self.renderer = None;
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.handle = None;
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
feature = "core"
))]
{
self.accesskit = None;
}
}
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
if self
.world
.res::<crate::ecs::window::resources::Window>()
.handle
.is_some()
{
return;
}
let mut attributes = winit::window::Window::default_attributes();
#[cfg(not(target_arch = "wasm32"))]
{
attributes = attributes
.with_title(
&self
.world
.res::<crate::ecs::window::resources::Window>()
.title,
)
.with_resizable(true)
.with_decorations(true)
.with_visible(false)
.with_min_inner_size(winit::dpi::LogicalSize::new(400.0, 300.0));
if let Some((width, height)) = self
.world
.res::<crate::ecs::window::resources::Window>()
.initial_size
{
attributes =
attributes.with_inner_size(winit::dpi::PhysicalSize::new(width, height));
}
#[cfg(feature = "assets")]
if let Some(icon_bytes) = self
.world
.res::<crate::ecs::window::resources::Window>()
.icon_bytes
&& let Ok(icon_image) = image::load_from_memory(icon_bytes)
{
let icon_rgba = icon_image.to_rgba8();
let (width, height) = icon_rgba.dimensions();
if let Ok(icon) =
winit::window::Icon::from_rgba(icon_rgba.into_raw(), width, height)
{
attributes = attributes.with_window_icon(Some(icon));
}
}
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.applied_title = Some(
self.world
.res::<crate::ecs::window::resources::Window>()
.title
.clone(),
);
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.applied_icon_bytes = self
.world
.res_mut::<crate::ecs::window::resources::Window>()
.icon_bytes;
}
#[cfg(target_arch = "wasm32")]
let (canvas_width, canvas_height) = {
use wasm_bindgen::JsCast;
use winit::platform::web::WindowAttributesExtWebSys;
let web_window = wgpu::web_sys::window().unwrap();
let canvas = web_window
.document()
.unwrap()
.get_element_by_id("canvas")
.unwrap()
.dyn_into::<wgpu::web_sys::HtmlCanvasElement>()
.unwrap();
let raw_dpr = web_window.device_pixel_ratio().max(1.0);
let css_width = web_window
.inner_width()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(800.0);
let css_height = web_window
.inner_height()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(600.0);
let dpr = wasm_capped_dpr(css_width, css_height, raw_dpr);
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.pointer_viewport_scale = (dpr / raw_dpr) as f32;
let physical_width = (css_width * dpr).round() as u32;
let physical_height = (css_height * dpr).round() as u32;
canvas.set_width(physical_width);
canvas.set_height(physical_height);
attributes = attributes.with_canvas(Some(canvas));
(physical_width, physical_height)
};
#[cfg(target_arch = "wasm32")]
setup_wasm_file_drop();
let Ok(window) = event_loop.create_window(attributes) else {
return;
};
let window_handle = std::sync::Arc::new(window);
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.cached_scale_factor = window_handle.scale_factor() as f32;
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.handle = Some(window_handle.clone());
#[cfg(feature = "egui")]
egui_support::initialize(&mut self.world);
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
feature = "core"
))]
{
self.accesskit = Some(crate::ecs::ui::accessibility::AccessKitHost::new(
event_loop,
&window_handle,
));
}
crate::ecs::window::resources::refresh_monitors_from_event_loop(
self.world
.res_mut::<crate::ecs::window::resources::Monitors>(),
event_loop,
);
sync_macos_auto_frame_rate_limit(&mut self.world);
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::WindowExtWebSys;
if let Some(canvas) = window_handle.canvas() {
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.cached_viewport_size = Some((canvas.width(), canvas.height()));
}
}
#[cfg(not(target_arch = "wasm32"))]
let dimension = window_handle.inner_size();
#[cfg(not(target_arch = "wasm32"))]
if self
.world
.res::<crate::ecs::window::resources::Window>()
.create_renderer
{
match pollster::block_on(crate::render::core::create_renderer(
window_handle.clone(),
dimension.width,
dimension.height,
)) {
Ok(mut renderer) => {
if let Err(error) = crate::render_driver::configure_with_state(
&mut renderer,
self.state.as_mut(),
) {
tracing::error!("Failed to configure renderer with state: {error}");
}
self.renderer = Some(renderer);
}
Err(error) => {
tracing::error!("Failed to create renderer: {error}");
}
}
}
#[cfg(target_arch = "wasm32")]
if self
.world
.res::<crate::ecs::window::resources::Window>()
.create_renderer
{
let (sender, receiver) = futures::channel::oneshot::channel();
self.renderer_receiver = Some(receiver);
let window_handle_clone = std::sync::Arc::clone(&window_handle);
wasm_bindgen_futures::spawn_local(async move {
match crate::render::core::create_renderer(
window_handle_clone,
canvas_width,
canvas_height,
)
.await
{
Ok(renderer) => {
if sender.send(renderer).is_err() {
tracing::error!("Failed to send renderer");
}
}
Err(error) => {
tracing::error!("Failed to create renderer: {error}");
}
}
});
}
#[cfg(target_arch = "wasm32")]
if !self
.world
.res::<crate::ecs::window::resources::Window>()
.create_renderer
&& !self.initialized
{
self.state.initialize(&mut self.world);
sync_min_window_size(&mut self.world);
crate::ecs::camera::systems::sync_wasm_canvas_size(&mut self.world);
self.initialized = true;
}
#[cfg(not(target_arch = "wasm32"))]
{
if !self.initialized {
self.state.initialize(&mut self.world);
sync_min_window_size(&mut self.world);
#[cfg(target_arch = "wasm32")]
crate::ecs::camera::systems::sync_wasm_canvas_size(&mut self.world);
self.initialized = true;
}
crate::ecs::events::emit_app_event(
&mut self.world,
crate::ecs::input::events::AppEvent::Resumed,
);
let window_handle = self
.world
.res::<crate::ecs::window::resources::Window>()
.handle
.clone();
if self
.world
.res::<crate::render::config::RendererState>()
.use_fullscreen
&& let Some(window_handle) = &window_handle
{
window_handle.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
}
if let Some(window_handle) = &window_handle {
let show_cursor = self
.world
.res::<crate::render::config::RendererState>()
.show_cursor;
window_handle.set_cursor_visible(show_cursor);
if !self
.world
.res::<crate::ecs::window::resources::Window>()
.start_hidden
{
window_handle.set_visible(true);
}
}
}
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
_window_id: winit::window::WindowId,
event: winit::event::WindowEvent,
) {
#[cfg(target_arch = "wasm32")]
{
let mut renderer_received = false;
if let Some(receiver) = self.renderer_receiver.as_mut()
&& let Ok(Some(mut renderer)) = receiver.try_recv()
{
if let Err(error) =
crate::render_driver::configure_with_state(&mut renderer, self.state.as_mut())
{
tracing::error!("Failed to configure renderer with state: {error}");
}
self.renderer = Some(renderer);
self.state.initialize(&mut self.world);
sync_min_window_size(&mut self.world);
#[cfg(target_arch = "wasm32")]
crate::ecs::camera::systems::sync_wasm_canvas_size(&mut self.world);
let show_cursor = self
.world
.res::<crate::render::config::RendererState>()
.show_cursor;
if let Some(window_handle) = &self
.world
.res::<crate::ecs::window::resources::Window>()
.handle
{
window_handle.set_cursor_visible(show_cursor);
}
renderer_received = true;
}
if renderer_received {
self.renderer_receiver = None;
}
}
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
feature = "core"
))]
if let Some(host) = self.accesskit.as_mut()
&& let Some(window) = self
.world
.res::<crate::ecs::window::resources::Window>()
.handle
.as_ref()
{
host.process_event(window, &event);
}
if self
.world
.res::<crate::ecs::window::resources::Window>()
.should_exit
|| matches!(event, winit::event::WindowEvent::CloseRequested)
{
event_loop.exit();
return;
}
match event {
winit::event::WindowEvent::Focused(focused) => {
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.is_focused = focused;
if !focused {
self.world
.res_mut::<crate::ecs::input::resources::Input>()
.mouse
.position_initialized = false;
}
}
winit::event::WindowEvent::Moved(_) => {
if let Some(handle) = self
.world
.res::<crate::ecs::window::resources::Window>()
.handle
.clone()
{
refresh_macos_frame_rate_limit(&mut self.world, &handle);
}
}
winit::event::WindowEvent::ScaleFactorChanged { .. } => {
if let Some(handle) = self
.world
.res::<crate::ecs::window::resources::Window>()
.handle
.as_ref()
{
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.cached_scale_factor = handle.scale_factor() as f32;
}
crate::ecs::window::resources::refresh_monitors_from_event_loop(
self.world
.res_mut::<crate::ecs::window::resources::Monitors>(),
event_loop,
);
if let Some(handle) = self
.world
.res::<crate::ecs::window::resources::Window>()
.handle
.clone()
{
refresh_macos_frame_rate_limit(&mut self.world, &handle);
}
}
winit::event::WindowEvent::Resized(winit::dpi::PhysicalSize { width, height })
if width > 0 && height > 0 =>
{
if let Some(handle) = self
.world
.res::<crate::ecs::window::resources::Window>()
.handle
.as_ref()
{
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.cached_scale_factor = handle.scale_factor() as f32;
}
crate::ecs::window::resources::refresh_monitors_from_event_loop(
self.world
.res_mut::<crate::ecs::window::resources::Monitors>(),
event_loop,
);
#[cfg(target_arch = "wasm32")]
let (surface_width, surface_height) = {
let web_window = wgpu::web_sys::window().unwrap();
let raw_dpr = web_window.device_pixel_ratio().max(1.0);
let css_width = web_window
.inner_width()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(width as f64);
let css_height = web_window
.inner_height()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(height as f64);
let dpr = wasm_capped_dpr(css_width, css_height, raw_dpr);
self.world
.res_mut::<crate::ecs::window::resources::Window>()
.pointer_viewport_scale = (dpr / raw_dpr) as f32;
let physical_width = (css_width * dpr).round() as u32;
let physical_height = (css_height * dpr).round() as u32;
use winit::platform::web::WindowExtWebSys;
if let Some(window) = self
.world
.res::<crate::ecs::window::resources::Window>()
.handle
.as_ref()
&& let Some(canvas) = window.canvas()
{
canvas.set_width(physical_width);
canvas.set_height(physical_height);
}
(physical_width, physical_height)
};
#[cfg(not(target_arch = "wasm32"))]
let (surface_width, surface_height) = (width, height);
apply_surface_resize(
&mut self.world,
&mut self.renderer,
surface_width,
surface_height,
);
}
_ => {}
}
if let Some(new_state) =
crate::run::step(&mut self.world, &mut self.state, &mut self.renderer, &event)
{
self.state = new_state;
}
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
feature = "core"
))]
if let Some(host) = self.accesskit.as_mut() {
host.update(&mut self.world);
}
request_redraw_system(&mut self.world);
}
fn device_event(
&mut self,
_event_loop: &winit::event_loop::ActiveEventLoop,
_device_id: winit::event::DeviceId,
event: DeviceEvent,
) {
if let DeviceEvent::MouseMotion { delta } = event {
let mouse = &mut self
.world
.res_mut::<crate::ecs::input::resources::Input>()
.mouse;
mouse.raw_mouse_delta.x += delta.0 as f32;
mouse.raw_mouse_delta.y += delta.1 as f32;
}
}
fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
request_redraw_system(&mut self.world);
}
fn exiting(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {}
}
pub(crate) fn request_redraw_system(world: &mut World) {
let Some(window_handle) = world
.res_mut::<crate::ecs::window::resources::Window>()
.handle
.as_mut()
else {
return;
};
let winit::dpi::PhysicalSize { width, height } = window_handle.inner_size();
if width == 0 || height == 0 {
return;
}
window_handle.request_redraw();
}