1#![deny(missing_docs)]
2
3use std::collections::HashMap;
21#[cfg(feature = "wgpu")]
22use std::sync::Arc;
23#[cfg(not(target_arch = "wasm32"))]
24use std::time::{Duration, Instant};
25#[cfg(target_arch = "wasm32")]
26use web_time::{Duration, Instant};
27
28pub use gapp::{Render, Update};
29
30#[cfg(feature = "opengl")]
31#[cfg(not(target_arch = "wasm32"))]
32use glutin::{
33 context::PossiblyCurrentContext,
34 prelude::*,
35 surface::{Surface, WindowSurface},
36};
37#[cfg(not(target_arch = "wasm32"))]
38use winit::dpi::PhysicalSize;
39use winit::{
40 event::{DeviceEvent, DeviceId, WindowEvent},
41 event_loop::ActiveEventLoop,
42 window::{Window, WindowId},
43};
44
45pub trait WindowInput<C, R> {
55 fn input(
65 &mut self,
66 window_id: WindowId,
67 event: &WindowEvent,
68 event_loop: &ActiveEventLoop,
69 context: &mut C,
70 windows: &mut Windows<R>,
71 );
72
73 fn device_input(
86 &mut self,
87 device_id: DeviceId,
88 event: &DeviceEvent,
89 event_loop: &ActiveEventLoop,
90 context: &mut C,
91 windows: &mut Windows<R>,
92 ) {
93 let _ = (device_id, event, event_loop, context, windows);
94 }
95}
96
97#[cfg(feature = "opengl")]
99pub struct WindowData {
100 pub window: Window,
102 #[cfg(not(target_arch = "wasm32"))]
104 pub context: PossiblyCurrentContext,
105 #[cfg(not(target_arch = "wasm32"))]
107 pub surface: Surface<WindowSurface>,
108}
109
110#[cfg(feature = "opengl")]
111#[cfg(not(target_arch = "wasm32"))]
112impl WindowData {
113 #[allow(clippy::expect_used)]
120 pub fn new_opengl(
121 event_loop: &ActiveEventLoop,
122 attributes: winit::window::WindowAttributes,
123 ) -> Result<Self, Box<dyn std::error::Error>> {
124 use glutin::{
125 config::ConfigTemplateBuilder,
126 context::{ContextApi, ContextAttributesBuilder},
127 display::GetGlDisplay,
128 surface::SurfaceAttributesBuilder,
129 };
130 use glutin_winit::DisplayBuilder;
131 use raw_window_handle::HasWindowHandle;
132 use std::num::NonZeroU32;
133
134 let template = ConfigTemplateBuilder::new().with_alpha_size(8);
135 let display_builder = DisplayBuilder::new().with_window_attributes(Some(attributes));
136
137 let (window, gl_config) = display_builder.build(event_loop, template, |configs| {
138 configs
139 .reduce(|accum, config| {
140 let prefer_config = config.supports_transparency().unwrap_or(false)
141 & !accum.supports_transparency().unwrap_or(false);
142 if prefer_config || config.num_samples() > accum.num_samples() {
143 config
144 } else {
145 accum
146 }
147 })
148 .expect("no OpenGL configurations available")
149 })?;
150
151 let window = window.ok_or("display builder returned no window")?;
152 let raw_handle = window.window_handle()?.as_raw();
153 let gl_display = gl_config.display();
154
155 let context_attributes = ContextAttributesBuilder::new().build(Some(raw_handle));
156 let fallback_attributes = ContextAttributesBuilder::new()
157 .with_context_api(ContextApi::Gles(None))
158 .build(Some(raw_handle));
159
160 let context = unsafe {
161 gl_display
162 .create_context(&gl_config, &context_attributes)
163 .or_else(|_| gl_display.create_context(&gl_config, &fallback_attributes))?
164 };
165
166 let size = window.inner_size();
167 let width = NonZeroU32::new(size.width.max(1)).ok_or("zero window width")?;
168 let height = NonZeroU32::new(size.height.max(1)).ok_or("zero window height")?;
169 let surface_attributes =
170 SurfaceAttributesBuilder::<WindowSurface>::new().build(raw_handle, width, height);
171 let surface = unsafe { gl_display.create_window_surface(&gl_config, &surface_attributes)? };
172
173 let context = context.make_current(&surface)?;
174
175 Ok(Self {
176 window,
177 context,
178 surface,
179 })
180 }
181}
182
183#[cfg(feature = "wgpu")]
185pub struct WindowData {
186 pub device: wgpu::Device,
188 pub queue: wgpu::Queue,
190 pub surface_config: wgpu::SurfaceConfiguration,
192 pub surface: wgpu::Surface<'static>,
194 pub window: Arc<Window>,
196}
197
198#[cfg(feature = "wgpu")]
199impl WindowData {
200 pub async fn new_wgpu(
214 event_loop: &ActiveEventLoop,
215 attributes: winit::window::WindowAttributes,
216 ) -> Result<Self, Box<dyn std::error::Error>> {
217 let window = Arc::new(event_loop.create_window(attributes)?);
218
219 let instance = wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor {
220 backends: wgpu::Backends::from_env().unwrap_or_default(),
221 flags: wgpu::InstanceFlags::from_build_config().with_env(),
222 ..wgpu::InstanceDescriptor::new_without_display_handle()
223 })
224 .await;
225
226 let surface = instance.create_surface(window.clone())?;
227
228 let adapter =
229 wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface)).await?;
230
231 let (device, queue) = adapter
232 .request_device(&wgpu::DeviceDescriptor {
233 required_limits: wgpu::Limits::downlevel_webgl2_defaults()
234 .using_resolution(adapter.limits()),
235 ..Default::default()
236 })
237 .await?;
238
239 let size = window.inner_size();
240 let mut surface_config = surface
241 .get_default_config(&adapter, size.width.max(1), size.height.max(1))
242 .ok_or("surface is not supported by the adapter")?;
243
244 let capabilities = surface.get_capabilities(&adapter);
245 surface_config.format = capabilities
246 .formats
247 .iter()
248 .find(|format| !format.is_srgb())
249 .copied()
250 .unwrap_or(capabilities.formats[0]);
251 surface.configure(&device, &surface_config);
252
253 Ok(Self {
254 device,
255 queue,
256 surface_config,
257 surface,
258 window,
259 })
260 }
261}
262
263pub struct Windows<R> {
265 map: HashMap<WindowId, (WindowData, R)>,
266}
267
268impl<R> Windows<R> {
269 pub fn new() -> Self {
271 Self {
272 map: HashMap::new(),
273 }
274 }
275
276 pub fn insert(&mut self, window_data: WindowData, renderer: R) -> WindowId {
278 let id = window_data.window.id();
279 self.map.insert(id, (window_data, renderer));
280 id
281 }
282
283 pub fn remove(&mut self, id: &WindowId) -> Option<(WindowData, R)> {
285 self.map.remove(id)
286 }
287
288 pub fn get(&self, id: &WindowId) -> Option<&(WindowData, R)> {
290 self.map.get(id)
291 }
292
293 pub fn get_mut(&mut self, id: &WindowId) -> Option<&mut (WindowData, R)> {
295 self.map.get_mut(id)
296 }
297
298 pub fn is_empty(&self) -> bool {
300 self.map.is_empty()
301 }
302
303 pub fn len(&self) -> usize {
305 self.map.len()
306 }
307
308 pub fn iter(&self) -> impl Iterator<Item = (&WindowId, &(WindowData, R))> {
310 self.map.iter()
311 }
312
313 pub fn iter_mut(&mut self) -> impl Iterator<Item = (&WindowId, &mut (WindowData, R))> {
315 self.map.iter_mut()
316 }
317}
318
319impl<R> Default for Windows<R> {
320 fn default() -> Self {
321 Self::new()
322 }
323}
324
325pub trait Present<R> {
329 fn present(&self, renderer: &mut R, window_data: &WindowData);
336}
337
338type WindowFactory<R> = Box<dyn FnOnce(&ActiveEventLoop) -> Windows<R>>;
339
340struct AppState<I, R, E> {
341 application: E,
342 timestep: Duration,
343 create_windows: Option<WindowFactory<R>>,
344 windows: Option<Windows<R>>,
345 input_context: I,
346 prev_time: Instant,
347 focus_grace_until: Option<Instant>,
348}
349
350const FOCUS_GRACE: Duration = Duration::from_millis(50);
353
354impl<I: 'static, R: 'static, E> winit::application::ApplicationHandler for AppState<I, R, E>
355where
356 E: WindowInput<I, R> + Present<R> + Render<R> + Update,
357{
358 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
359 if self.windows.is_some() {
360 return;
361 }
362 if let Some(create_windows) = self.create_windows.take() {
363 self.windows = Some(create_windows(event_loop));
364 self.prev_time = Instant::now();
365 }
366 }
367
368 fn window_event(
369 &mut self,
370 event_loop: &ActiveEventLoop,
371 window_id: WindowId,
372 event: WindowEvent,
373 ) {
374 let Some(windows) = &mut self.windows else {
375 return;
376 };
377
378 if let WindowEvent::Focused(focused) = &event {
379 self.focus_grace_until = focused.then(|| Instant::now() + FOCUS_GRACE);
380 }
381 if matches!(event, WindowEvent::KeyboardInput { .. })
382 && self
383 .focus_grace_until
384 .is_some_and(|until| Instant::now() < until)
385 {
386 return;
387 }
388
389 #[cfg(not(target_arch = "wasm32"))]
390 if let WindowEvent::Resized(PhysicalSize { width, height }) = &event {
391 let width = (*width).max(1);
392 let height = (*height).max(1);
393 if let Some((window_data, _)) = windows.get_mut(&window_id) {
394 #[cfg(feature = "opengl")]
395 if let (Ok(non_zero_width), Ok(non_zero_height)) =
396 (width.try_into(), height.try_into())
397 {
398 window_data.surface.resize(
399 &window_data.context,
400 non_zero_width,
401 non_zero_height,
402 );
403 }
404 #[cfg(feature = "wgpu")]
405 {
406 window_data.surface_config.width = width;
407 window_data.surface_config.height = height;
408 window_data
409 .surface
410 .configure(&window_data.device, &window_data.surface_config);
411 }
412 }
413 }
414
415 self.application.input(
416 window_id,
417 &event,
418 event_loop,
419 &mut self.input_context,
420 windows,
421 );
422
423 if matches!(event, WindowEvent::RedrawRequested)
424 && let Some((window_data, renderer)) = windows.get_mut(&window_id)
425 {
426 self.application.render(renderer);
427 self.application.present(renderer, window_data);
428 }
429 }
430
431 fn device_event(
432 &mut self,
433 event_loop: &ActiveEventLoop,
434 device_id: DeviceId,
435 event: DeviceEvent,
436 ) {
437 let Some(windows) = &mut self.windows else {
438 return;
439 };
440
441 self.application.device_input(
442 device_id,
443 &event,
444 event_loop,
445 &mut self.input_context,
446 windows,
447 );
448 }
449
450 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
451 let Some(windows) = &self.windows else {
452 return;
453 };
454
455 self.application.update(self.timestep.as_secs_f32());
456
457 #[cfg(not(target_arch = "wasm32"))]
458 {
459 let frame_duration = self.prev_time.elapsed();
460 if frame_duration < self.timestep {
461 std::thread::sleep(self.timestep - frame_duration);
462 }
463 }
464
465 for (_, (window_data, _)) in windows.iter() {
466 window_data.window.request_redraw();
467 }
468
469 self.prev_time = Instant::now();
470 }
471
472 fn exiting(&mut self, _event_loop: &ActiveEventLoop) {}
473}
474
475pub fn run<
495 I: 'static,
496 R: 'static,
497 E: WindowInput<I, R> + Present<R> + Render<R> + Update + 'static,
498>(
499 application: E,
500 event_loop: winit::event_loop::EventLoop<()>,
501 fps: u64,
502 create_windows: impl FnOnce(&ActiveEventLoop) -> Windows<R> + 'static,
503 input_context: I,
504) {
505 let timestep = Duration::from_nanos(1_000_000_000 / fps);
506
507 let mut state = AppState {
508 application,
509 timestep,
510 create_windows: Some(Box::new(create_windows)),
511 windows: None,
512 input_context,
513 prev_time: Instant::now(),
514 focus_grace_until: None,
515 };
516
517 let _ = event_loop.run_app(&mut state);
518}