use std::{
path::Path,
time::{Duration, Instant},
};
use image::ImageError;
use wgpu::TextureViewDescriptor;
use winit::{
application::ApplicationHandler,
event::{DeviceEvent, DeviceId, WindowEvent},
event_loop::{ActiveEventLoop, ControlFlow},
window::{Icon, WindowAttributes, WindowId},
};
use crate::{
EngineUpdates, Scene, UiLayoutSides, UiLayoutTopBottom, UiSettings,
system::{State, UserEvent, process_engine_updates},
};
const IDLE_WAKE_DT_MAX: Duration = Duration::from_nanos(16_666_667);
const CONTINUOUS_DT_MAX: Duration = Duration::from_millis(100);
fn frame_delta(elapsed: Duration, continuous_redraw: bool) -> Duration {
elapsed.min(if continuous_redraw {
CONTINUOUS_DT_MAX
} else {
IDLE_WAKE_DT_MAX
})
}
fn load_icon(path: &Path) -> Result<Icon, ImageError> {
let (icon_rgba, icon_width, icon_height) = {
let image = image::open(path)?.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
Ok(Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon"))
}
impl<T, FRender, FEventDev, FEventWin, FGui> State<T, FRender, FEventDev, FEventWin, FGui>
where
FRender: FnMut(&mut T, &mut Scene, f32) -> EngineUpdates + 'static,
FEventDev: FnMut(&mut T, DeviceEvent, &mut Scene, bool, f32) -> EngineUpdates + 'static,
FEventWin: FnMut(&mut T, WindowEvent, &mut Scene, f32) -> EngineUpdates + 'static,
FGui: FnMut(&mut T, &mut egui::Ui, &mut Scene) -> EngineUpdates + 'static,
{
fn redraw(&mut self) -> bool {
if self.paused || self.render.is_none() || self.graphics.is_none() {
return false;
}
self.next_repaint = None;
let sys = self.render.as_ref().unwrap();
let graphics = self.graphics.as_mut().unwrap();
let now = Instant::now();
self.dt = frame_delta(
now.saturating_duration_since(self.last_render_time),
self.continuous_redraw,
);
self.last_render_time = now;
let dt_secs = self.dt.as_secs() as f32 + self.dt.subsec_micros() as f32 / 1_000_000.;
let updates_render =
(self.render_handler)(&mut self.user_state, &mut graphics.scene, dt_secs);
let mut request_redraw = updates_render.request_redraw;
process_engine_updates(
&updates_render,
graphics,
&self.render.as_ref().unwrap().device,
&self.render.as_ref().unwrap().queue,
);
match sys.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(output_frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(output_frame) => {
let surface_texture = output_frame
.texture
.create_view(&TextureViewDescriptor::default());
let (resize_required, gui_request_redraw) = graphics.render(
self.gui.as_mut().unwrap(),
output_frame,
&surface_texture,
&sys.device,
&sys.queue,
self.dt,
sys.surface_cfg.width,
sys.surface_cfg.height,
&mut self.ui_settings,
&mut self.gui_handler,
&mut self.user_state,
);
request_redraw |= gui_request_redraw;
if resize_required {
self.resize(sys.size);
}
let pending = self.graphics.as_mut().unwrap().pending_msaa.take();
if let Some(new_msaa) = pending {
let device = &self.render.as_ref().unwrap().device;
let graphics = self.graphics.as_mut().unwrap();
graphics.msaa_samples = new_msaa;
graphics.apply_msaa_change(device);
self.graphics_settings.msaa_samples = new_msaa;
}
}
_ => (),
}
request_redraw
}
fn schedule_repaint(&mut self, repaint_at: Instant) {
if self
.next_repaint
.is_none_or(|scheduled| repaint_at < scheduled)
{
self.next_repaint = Some(repaint_at);
}
}
fn update_repaint_deadline(&mut self, event_loop: &ActiveEventLoop) {
if self.paused {
event_loop.set_control_flow(ControlFlow::Wait);
return;
}
let Some(repaint_at) = self.next_repaint else {
event_loop.set_control_flow(ControlFlow::Wait);
return;
};
if repaint_at <= Instant::now() {
self.next_repaint = None;
if let Some(graphics) = &self.graphics {
graphics.window.request_redraw();
}
event_loop.set_control_flow(ControlFlow::Wait);
} else {
event_loop.set_control_flow(ControlFlow::WaitUntil(repaint_at));
}
}
}
impl<T, FRender, FEventDev, FEventWin, FGui> ApplicationHandler<UserEvent>
for State<T, FRender, FEventDev, FEventWin, FGui>
where
FRender: FnMut(&mut T, &mut Scene, f32) -> EngineUpdates + 'static,
FEventDev: FnMut(&mut T, DeviceEvent, &mut Scene, bool, f32) -> EngineUpdates + 'static,
FEventWin: FnMut(&mut T, WindowEvent, &mut Scene, f32) -> EngineUpdates + 'static,
FGui: FnMut(&mut T, &mut egui::Ui, &mut Scene) -> EngineUpdates + 'static,
{
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let icon = match self.ui_settings.icon_path {
Some(ref p) => load_icon(Path::new(&p)).ok(),
None => None,
};
let requested_w = self.scene.window_size.0;
let requested_h = self.scene.window_size.1;
let fits_on_screen = event_loop
.primary_monitor()
.map(|m| {
let scale = m.scale_factor() as f32;
let logical_w = m.size().width as f32 / scale;
let logical_h = m.size().height as f32 / scale;
requested_w <= logical_w && requested_h <= logical_h
})
.unwrap_or(true);
let base_attributes = WindowAttributes::default()
.with_title(&self.scene.window_title)
.with_window_icon(icon);
let attributes = if fits_on_screen {
base_attributes.with_inner_size(winit::dpi::LogicalSize::new(requested_w, requested_h))
} else {
base_attributes.with_maximized(true)
};
let window = event_loop.create_window(attributes).unwrap();
self.init(window);
self.graphics.as_ref().unwrap().window.request_redraw();
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
if self.render.is_none() || self.graphics.is_none() {
return;
}
let redraw_for_event = !matches!(
event,
WindowEvent::RedrawRequested | WindowEvent::CloseRequested
);
let graphics = &mut self.graphics.as_mut().unwrap();
let gui = &mut self.gui.as_mut().unwrap();
if !gui.mouse_in_gui {
graphics.handle_input_window(&event, &self.scene.input_settings);
let dt_secs = self.dt.as_secs() as f32 + self.dt.subsec_micros() as f32 / 1_000_000.;
let updates_event = (self.event_win_handler)(
&mut self.user_state,
event.clone(),
&mut graphics.scene,
dt_secs,
);
let render = self.render.as_ref().unwrap();
process_engine_updates(&updates_event, graphics, &render.device, &render.queue);
}
let window = &graphics.window;
let _ = gui.egui_state.on_window_event(window, &event);
match event {
WindowEvent::RedrawRequested => {
let app_requested_redraw = self.redraw();
let engine_input_active = self
.graphics
.as_ref()
.is_some_and(|graphics| graphics.inputs_commanded.inputs_present());
self.continuous_redraw =
!self.paused && (app_requested_redraw || engine_input_active);
if self.continuous_redraw {
self.graphics.as_ref().unwrap().window.request_redraw();
}
}
WindowEvent::CursorMoved { position, .. } => {
let in_ui_horizontal = match self.ui_settings.layout_sides {
UiLayoutSides::Left => position.x < gui.size.0 as f64,
UiLayoutSides::Right => {
position.x > window.inner_size().width as f64 - gui.size.0 as f64
}
};
let in_ui_vertical = match self.ui_settings.layout_top_bottom {
UiLayoutTopBottom::Top => position.y < gui.size.1 as f64,
UiLayoutTopBottom::Bottom => {
position.y > window.inner_size().height as f64 - gui.size.1 as f64
}
};
let mouse_in_gui = in_ui_horizontal || in_ui_vertical;
if mouse_in_gui {
gui.mouse_in_gui = true;
self.graphics.as_mut().unwrap().inputs_commanded = Default::default();
} else {
gui.mouse_in_gui = false;
}
}
WindowEvent::CloseRequested => {
event_loop.exit();
}
WindowEvent::Resized(physical_size) => {
self.paused = physical_size.width == 0 || physical_size.height == 0;
if !self.paused {
self.resize(physical_size);
self.last_render_time = Instant::now();
self.dt = Default::default();
self.continuous_redraw = false;
}
self.graphics.as_mut().unwrap().inputs_commanded.free_look = false;
}
WindowEvent::ScaleFactorChanged {
scale_factor: _,
inner_size_writer: _,
..
} => {
println!("Scale factor changed");
}
WindowEvent::Moved(_) => {
gui.mouse_in_gui = true;
self.graphics.as_mut().unwrap().inputs_commanded.free_look = false;
}
WindowEvent::Occluded(occ) => {
self.paused = occ;
self.graphics.as_mut().unwrap().inputs_commanded.free_look = false;
if !self.paused {
self.last_render_time = Instant::now();
self.dt = Default::default();
self.continuous_redraw = false;
}
}
WindowEvent::Focused(focused) => {
self.paused = !focused;
self.graphics.as_mut().unwrap().inputs_commanded.free_look = false;
if focused {
self.last_render_time = Instant::now();
self.dt = Default::default();
self.continuous_redraw = false;
}
}
WindowEvent::CursorLeft { device_id: _ } => {
graphics.inputs_commanded.free_look = false;
graphics.inputs_commanded.cursor_out_of_window = true;
}
WindowEvent::CursorEntered { device_id: _ } => {
self.paused = false;
graphics.inputs_commanded.cursor_out_of_window = false;
}
WindowEvent::HoveredFile(_)
| WindowEvent::HoveredFileCancelled
| WindowEvent::DroppedFile(_) => {
self.graphics.as_ref().unwrap().window.request_redraw();
}
_ => {}
}
if redraw_for_event && !self.paused {
self.graphics.as_ref().unwrap().window.request_redraw();
}
}
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device_id: DeviceId,
event: DeviceEvent,
) {
if self.render.is_none() || self.graphics.is_none() {
return;
}
let render = &self.render.as_ref().unwrap();
let graphics = &mut self.graphics.as_mut().unwrap();
let gui = &mut self.gui.as_mut().unwrap();
if !gui.mouse_in_gui {
graphics.handle_input_device(&event, &self.scene.input_settings);
let inputs_present = graphics.inputs_commanded.inputs_present();
let dt_secs = self.dt.as_secs() as f32 + self.dt.subsec_micros() as f32 / 1_000_000.;
let updates_event = (self.event_dev_handler)(
&mut self.user_state,
event,
&mut graphics.scene,
inputs_present,
dt_secs,
);
process_engine_updates(&updates_event, graphics, &render.device, &render.queue);
}
if !self.paused {
self.graphics.as_ref().unwrap().window.request_redraw();
}
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::RequestRedraw(repaint_at) => self.schedule_repaint(repaint_at),
}
self.update_repaint_deadline(event_loop);
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
self.update_repaint_deadline(event_loop);
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {}
}
pub fn viewport_rect(
ui_size: (f32, f32), win_width: u32,
win_height: u32,
ui_settings: &UiSettings,
_pixels_per_pt: f32,
) -> (f32, f32, f32, f32) {
let mut x = 0.0;
let mut y = 0.0;
let mut eff_width = win_width as f32;
let mut eff_height = win_height as f32;
if ui_settings.layout_sides == UiLayoutSides::Left {
x = ui_size.0;
}
if ui_settings.layout_top_bottom == UiLayoutTopBottom::Top {
y = ui_size.1;
}
eff_width -= ui_size.0;
eff_height -= ui_size.1;
if eff_width < 1.0 {
eff_width = 1.0;
}
if eff_height < 1.0 {
eff_height = 1.0;
}
(x, y, eff_width, eff_height)
}