gapp-winit 0.12.0

Abstract event loop library for winit-based applications with OpenGL and wgpu backends, integrating gapp traits for clean separation of input, update, render, and present
Documentation
#![deny(missing_docs)]

/*!
An abstract event loop library for winit-based applications with support for OpenGL (glutin) and wgpu backends.
It cleanly separates input handling, updates, rendering, and presenting via traits and manages a fixed-timestep FPS loop.

# Features

- `opengl`: Enables OpenGL support.
- `wgpu`: Enables wgpu support (recommended).

# Usage

Implement `WindowInput<I, R>`, `Present<R>`, `Render<R>` (from gapp), and `Update` (from gapp) for your app.
Create a `Windows<R>` collection with your backend-specific `WindowData`, then call `run()`.

See [the template](https://gitlab.com/porky11/gapp-template) for reference. It can be used as a starting point.
*/

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::WindowEvent,
    event_loop::ActiveEventLoop,
    window::{Window, WindowId},
};

/// Trait for custom input handling.
///
/// Called for **every** `WindowEvent`, **after** possible internal handling.
/// Handle events like `CloseRequested`, `KeyboardInput`, etc. here.
/// Call `event_loop.exit()` to quit the app.
/// The `windows` parameter provides mutable access to all managed windows and their renderers.
pub trait WindowInput<C, R> {
    /// Processes a `WindowEvent`.
    ///
    /// # Arguments
    ///
    /// - `window_id`: The ID of the window that received the event.
    /// - `event`: The incoming `WindowEvent`.
    /// - `event_loop`: Reference to the active event loop for control (e.g., `exit()`).
    /// - `context`: Custom context to represent input state.
    /// - `windows`: Mutable access to all managed windows and their renderers.
    fn input(
        &mut self,
        window_id: WindowId,
        event: &WindowEvent,
        event_loop: &ActiveEventLoop,
        context: &mut C,
        windows: &mut Windows<R>,
    );
}

/// Platform- and backend-specific window data for OpenGL.
#[cfg(feature = "opengl")]
pub struct WindowData {
    /// The winit window.
    pub window: Window,
    /// The GL context (glutin).
    #[cfg(not(target_arch = "wasm32"))]
    pub context: PossiblyCurrentContext,
    /// The GL surface (glutin).
    #[cfg(not(target_arch = "wasm32"))]
    pub surface: Surface<WindowSurface>,
}

/// Platform- and backend-specific window data for wgpu.
#[cfg(feature = "wgpu")]
pub struct WindowData {
    /// The wgpu device.
    pub device: wgpu::Device,
    /// The wgpu queue.
    pub queue: wgpu::Queue,
    /// Current surface configuration (updated on resize).
    pub surface_config: wgpu::SurfaceConfiguration,
    /// The wgpu surface (with `'static` lifetime for compatibility).
    pub surface: wgpu::Surface<'static>,
    /// Arc-wrapped winit window (thread-safe).
    pub window: Arc<Window>,
}

/// Collection of windows with their associated renderers.
pub struct Windows<R> {
    map: HashMap<WindowId, (WindowData, R)>,
}

impl<R> Windows<R> {
    /// Creates an empty window collection.
    pub fn new() -> Self {
        Self {
            map: HashMap::new(),
        }
    }

    /// Inserts a window and its renderer. Returns the `WindowId`.
    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
    }

    /// Removes a window by ID, returning its data and renderer if present.
    pub fn remove(&mut self, id: &WindowId) -> Option<(WindowData, R)> {
        self.map.remove(id)
    }

    /// Returns a reference to the window data and renderer for the given ID.
    pub fn get(&self, id: &WindowId) -> Option<&(WindowData, R)> {
        self.map.get(id)
    }

    /// Returns a mutable reference to the window data and renderer for the given ID.
    pub fn get_mut(&mut self, id: &WindowId) -> Option<&mut (WindowData, R)> {
        self.map.get_mut(id)
    }

    /// Returns `true` if there are no windows.
    pub fn is_empty(&self) -> bool {
        self.map.is_empty()
    }

    /// Returns the number of windows.
    pub fn len(&self) -> usize {
        self.map.len()
    }

    /// Iterates over all windows.
    pub fn iter(&self) -> impl Iterator<Item = (&WindowId, &(WindowData, R))> {
        self.map.iter()
    }

    /// Iterates mutably over all windows.
    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()
    }
}

/// Trait for presenting the rendered frame.
///
/// Called after `application.render()` in `RedrawRequested` events.
pub trait Present<R> {
    /// Presents the frame to the surface.
    ///
    /// # Arguments
    ///
    /// - `renderer`: Mutable renderer (e.g., for swapchain submit).
    /// - `window_data`: Backend-specific data (surface/context).
    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,
}

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;
        };

        #[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);
                }
            }
        }

        if let WindowEvent::RedrawRequested = &event {
            self.application.input(
                window_id,
                &event,
                event_loop,
                &mut self.input_context,
                windows,
            );
            if let Some((window_data, renderer)) = windows.get_mut(&window_id) {
                self.application.render(renderer);
                self.application.present(renderer, window_data);
            }
        } else {
            self.application.input(
                window_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) {}
}

/// Starts the main event loop with a fixed timestep.
///
/// Manages winit events and calls app traits in the correct order:
/// - `input()` after every `WindowEvent` (after internal resize/redraw handling).
/// - `update(timestep)` with fixed timestep in `about_to_wait`.
/// - `render()` + `present()` on `RedrawRequested` (per window).
///
/// **Important notes:**
/// - App (`E`) and states must be `'static` (moved into closure).
/// - Automatic resize handling (clamped to min 1px size).
/// - No automatic close handling: implement in `input()`.
/// - Redraw is requested for all windows on each frame.
/// - The `create_windows` closure is called once in `resumed()`.
///
/// # Type Parameters
///
/// - `I`: Input context type (for `WindowInput`).
/// - `R`: Renderer type.
/// - `E`: App type (must implement `WindowInput<I,R> + Present<R> + Render<R> + Update + 'static`).
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(),
    };

    let _ = event_loop.run_app(&mut state);
}