#![deny(missing_docs)]
use std::collections::HashMap;
#[cfg(feature = "wgpu")]
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use std::time::{Duration, Instant};
#[cfg(target_arch = "wasm32")]
use web_time::{Duration, Instant};
pub use gapp::{Render, Update};
#[cfg(feature = "opengl")]
#[cfg(not(target_arch = "wasm32"))]
use glutin::{
context::PossiblyCurrentContext,
prelude::*,
surface::{Surface, WindowSurface},
};
#[cfg(not(target_arch = "wasm32"))]
use winit::dpi::PhysicalSize;
use winit::{
event::{DeviceEvent, DeviceId, WindowEvent},
event_loop::ActiveEventLoop,
window::{Window, WindowId},
};
pub trait WindowInput<C, R> {
fn input(
&mut self,
window_id: WindowId,
event: &WindowEvent,
event_loop: &ActiveEventLoop,
context: &mut C,
windows: &mut Windows<R>,
);
fn device_input(
&mut self,
device_id: DeviceId,
event: &DeviceEvent,
event_loop: &ActiveEventLoop,
context: &mut C,
windows: &mut Windows<R>,
) {
let _ = (device_id, event, event_loop, context, windows);
}
}
#[cfg(feature = "opengl")]
pub struct WindowData {
pub window: Window,
#[cfg(not(target_arch = "wasm32"))]
pub context: PossiblyCurrentContext,
#[cfg(not(target_arch = "wasm32"))]
pub surface: Surface<WindowSurface>,
}
#[cfg(feature = "opengl")]
#[cfg(not(target_arch = "wasm32"))]
impl WindowData {
#[allow(clippy::expect_used)]
pub fn new_opengl(
event_loop: &ActiveEventLoop,
attributes: winit::window::WindowAttributes,
) -> Result<Self, Box<dyn std::error::Error>> {
use glutin::{
config::ConfigTemplateBuilder,
context::{ContextApi, ContextAttributesBuilder},
display::GetGlDisplay,
surface::SurfaceAttributesBuilder,
};
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasWindowHandle;
use std::num::NonZeroU32;
let template = ConfigTemplateBuilder::new().with_alpha_size(8);
let display_builder = DisplayBuilder::new().with_window_attributes(Some(attributes));
let (window, gl_config) = display_builder.build(event_loop, template, |configs| {
configs
.reduce(|accum, config| {
let prefer_config = config.supports_transparency().unwrap_or(false)
& !accum.supports_transparency().unwrap_or(false);
if prefer_config || config.num_samples() > accum.num_samples() {
config
} else {
accum
}
})
.expect("no OpenGL configurations available")
})?;
let window = window.ok_or("display builder returned no window")?;
let raw_handle = window.window_handle()?.as_raw();
let gl_display = gl_config.display();
let context_attributes = ContextAttributesBuilder::new().build(Some(raw_handle));
let fallback_attributes = ContextAttributesBuilder::new()
.with_context_api(ContextApi::Gles(None))
.build(Some(raw_handle));
let context = unsafe {
gl_display
.create_context(&gl_config, &context_attributes)
.or_else(|_| gl_display.create_context(&gl_config, &fallback_attributes))?
};
let size = window.inner_size();
let width = NonZeroU32::new(size.width.max(1)).ok_or("zero window width")?;
let height = NonZeroU32::new(size.height.max(1)).ok_or("zero window height")?;
let surface_attributes =
SurfaceAttributesBuilder::<WindowSurface>::new().build(raw_handle, width, height);
let surface = unsafe { gl_display.create_window_surface(&gl_config, &surface_attributes)? };
let context = context.make_current(&surface)?;
Ok(Self {
window,
context,
surface,
})
}
}
#[cfg(feature = "wgpu")]
pub struct WindowData {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub surface_config: wgpu::SurfaceConfiguration,
pub surface: wgpu::Surface<'static>,
pub window: Arc<Window>,
}
#[cfg(feature = "wgpu")]
impl WindowData {
pub async fn new_wgpu(
event_loop: &ActiveEventLoop,
attributes: winit::window::WindowAttributes,
) -> Result<Self, Box<dyn std::error::Error>> {
let window = Arc::new(event_loop.create_window(attributes)?);
let instance = wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor {
backends: wgpu::Backends::from_env().unwrap_or_default(),
flags: wgpu::InstanceFlags::from_build_config().with_env(),
..wgpu::InstanceDescriptor::new_without_display_handle()
})
.await;
let surface = instance.create_surface(window.clone())?;
let adapter =
wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface)).await?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_limits: wgpu::Limits::downlevel_webgl2_defaults()
.using_resolution(adapter.limits()),
..Default::default()
})
.await?;
let size = window.inner_size();
let mut surface_config = surface
.get_default_config(&adapter, size.width.max(1), size.height.max(1))
.ok_or("surface is not supported by the adapter")?;
let capabilities = surface.get_capabilities(&adapter);
surface_config.format = capabilities
.formats
.iter()
.find(|format| !format.is_srgb())
.copied()
.unwrap_or(capabilities.formats[0]);
surface.configure(&device, &surface_config);
Ok(Self {
device,
queue,
surface_config,
surface,
window,
})
}
}
pub struct Windows<R> {
map: HashMap<WindowId, (WindowData, R)>,
}
impl<R> Windows<R> {
pub fn new() -> Self {
Self {
map: HashMap::new(),
}
}
pub fn insert(&mut self, window_data: WindowData, renderer: R) -> WindowId {
let id = window_data.window.id();
self.map.insert(id, (window_data, renderer));
id
}
pub fn remove(&mut self, id: &WindowId) -> Option<(WindowData, R)> {
self.map.remove(id)
}
pub fn get(&self, id: &WindowId) -> Option<&(WindowData, R)> {
self.map.get(id)
}
pub fn get_mut(&mut self, id: &WindowId) -> Option<&mut (WindowData, R)> {
self.map.get_mut(id)
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn iter(&self) -> impl Iterator<Item = (&WindowId, &(WindowData, R))> {
self.map.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&WindowId, &mut (WindowData, R))> {
self.map.iter_mut()
}
}
impl<R> Default for Windows<R> {
fn default() -> Self {
Self::new()
}
}
pub trait Present<R> {
fn present(&self, renderer: &mut R, window_data: &WindowData);
}
type WindowFactory<R> = Box<dyn FnOnce(&ActiveEventLoop) -> Windows<R>>;
struct AppState<I, R, E> {
application: E,
timestep: Duration,
create_windows: Option<WindowFactory<R>>,
windows: Option<Windows<R>>,
input_context: I,
prev_time: Instant,
focus_grace_until: Option<Instant>,
}
const FOCUS_GRACE: Duration = Duration::from_millis(50);
impl<I: 'static, R: 'static, E> winit::application::ApplicationHandler for AppState<I, R, E>
where
E: WindowInput<I, R> + Present<R> + Render<R> + Update,
{
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.windows.is_some() {
return;
}
if let Some(create_windows) = self.create_windows.take() {
self.windows = Some(create_windows(event_loop));
self.prev_time = Instant::now();
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
let Some(windows) = &mut self.windows else {
return;
};
if let WindowEvent::Focused(focused) = &event {
self.focus_grace_until = focused.then(|| Instant::now() + FOCUS_GRACE);
}
if matches!(event, WindowEvent::KeyboardInput { .. })
&& self
.focus_grace_until
.is_some_and(|until| Instant::now() < until)
{
return;
}
#[cfg(not(target_arch = "wasm32"))]
if let WindowEvent::Resized(PhysicalSize { width, height }) = &event {
let width = (*width).max(1);
let height = (*height).max(1);
if let Some((window_data, _)) = windows.get_mut(&window_id) {
#[cfg(feature = "opengl")]
if let (Ok(non_zero_width), Ok(non_zero_height)) =
(width.try_into(), height.try_into())
{
window_data.surface.resize(
&window_data.context,
non_zero_width,
non_zero_height,
);
}
#[cfg(feature = "wgpu")]
{
window_data.surface_config.width = width;
window_data.surface_config.height = height;
window_data
.surface
.configure(&window_data.device, &window_data.surface_config);
}
}
}
self.application.input(
window_id,
&event,
event_loop,
&mut self.input_context,
windows,
);
if matches!(event, WindowEvent::RedrawRequested)
&& let Some((window_data, renderer)) = windows.get_mut(&window_id)
{
self.application.render(renderer);
self.application.present(renderer, window_data);
}
}
fn device_event(
&mut self,
event_loop: &ActiveEventLoop,
device_id: DeviceId,
event: DeviceEvent,
) {
let Some(windows) = &mut self.windows else {
return;
};
self.application.device_input(
device_id,
&event,
event_loop,
&mut self.input_context,
windows,
);
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
let Some(windows) = &self.windows else {
return;
};
self.application.update(self.timestep.as_secs_f32());
#[cfg(not(target_arch = "wasm32"))]
{
let frame_duration = self.prev_time.elapsed();
if frame_duration < self.timestep {
std::thread::sleep(self.timestep - frame_duration);
}
}
for (_, (window_data, _)) in windows.iter() {
window_data.window.request_redraw();
}
self.prev_time = Instant::now();
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {}
}
pub fn run<
I: 'static,
R: 'static,
E: WindowInput<I, R> + Present<R> + Render<R> + Update + 'static,
>(
application: E,
event_loop: winit::event_loop::EventLoop<()>,
fps: u64,
create_windows: impl FnOnce(&ActiveEventLoop) -> Windows<R> + 'static,
input_context: I,
) {
let timestep = Duration::from_nanos(1_000_000_000 / fps);
let mut state = AppState {
application,
timestep,
create_windows: Some(Box::new(create_windows)),
windows: None,
input_context,
prev_time: Instant::now(),
focus_grace_until: None,
};
let _ = event_loop.run_app(&mut state);
}