gapp-winit 0.11.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
#![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;
use std::time::Duration;

pub use gapp::{Render, Update};

#[cfg(feature = "opengl")]
#[cfg(not(target_arch = "wasm32"))]
use glutin::{
    context::PossiblyCurrentContext,
    prelude::*,
    surface::{Surface, WindowSurface},
};
use instant::Instant;
#[cfg(not(target_arch = "wasm32"))]
use winit::dpi::PhysicalSize;
use winit::{
    event::{Event, WindowEvent},
    event_loop::{EventLoop, EventLoopWindowTarget},
    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 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<T>(
        &mut self,
        window_id: WindowId,
        event: &WindowEvent,
        event_loop: &EventLoopWindowTarget<T>,
        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);
}

/// 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 `AboutToWait`.
/// - `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.
///
/// # Type Parameters
///
/// - `I`: Input context type (for `WindowInput`).
/// - `R`: Renderer type.
/// - `T`: `EventLoop` user data type.
/// - `E`: App type (must implement `WindowInput<I,R> + Present<R> + Render<R> + Update + 'static`).
pub fn run<
    I: 'static,
    R: 'static,
    T,
    E: WindowInput<I, R> + Present<R> + Render<R> + Update + 'static,
>(
    mut application: E,
    event_loop: EventLoop<T>,
    fps: u64,
    windows: Windows<R>,
    mut input_context: I,
) {
    let mut windows = windows;

    let timestep = Duration::from_nanos(1000000000 / fps);

    let start = Instant::now();
    let mut prev_time = start;

    let _ = event_loop.run(move |event, event_loop| match event {
        Event::WindowEvent {
            ref event,
            window_id,
        } => {
            if let Some((window_data, renderer)) = windows.get_mut(&window_id) {
                match event {
                    #[cfg(not(target_arch = "wasm32"))]
                    &WindowEvent::Resized(PhysicalSize { width, height }) => {
                        let width = width.max(1);
                        let height = height.max(1);
                        #[cfg(feature = "opengl")]
                        window_data.surface.resize(
                            &window_data.context,
                            width.try_into().unwrap(),
                            height.try_into().unwrap(),
                        );
                        #[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);
                        }
                    }
                    WindowEvent::RedrawRequested => {
                        application.render(renderer);
                        application.present(renderer, window_data);
                    }
                    _ => (),
                }
            }

            application.input(
                window_id,
                event,
                event_loop,
                &mut input_context,
                &mut windows,
            );
        }
        Event::AboutToWait => {
            #[allow(unused_variables)]
            let frame_duration = prev_time.elapsed();

            application.update(timestep.as_secs_f32());

            #[cfg(not(target_arch = "wasm32"))]
            if frame_duration < timestep {
                std::thread::sleep(timestep - frame_duration);
            }

            for (_, (window_data, _)) in windows.iter() {
                window_data.window.request_redraw();
            }

            prev_time = Instant::now();
        }
        _ => (),
    });
}