pkecs_window 9.2.0

Provides windowing capabilities for `pkecs`.
Documentation
//! Provides a graphical application over pkecs.

pub mod component;
pub mod config;
pub mod event;
mod metrics;

use log::debug;
use winit::{
    application::ApplicationHandler,
    dpi::PhysicalSize,
    error::EventLoopError,
    event::{
        DeviceEvent,
        DeviceId,
        ElementState,
        KeyEvent,
        MouseButton,
        WindowEvent,
    },
    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
    window::WindowId,
};
use pkecs::{
    app::Plugin,
    ecs::{
        event::{handler::into::IntoEventHandler, Events},
        world::UnsafeWorldCell,
        system::{
            handler::into::IntoSystemHandler,
            schedule::{Startup, Update, FixedUpdate, Shutdown},
            Systems,
        },
    },
};
use component::{lifecycle::ApplicationLifecycle, time::GameTime, window::WindowContext};
use config::GraphicalApplicationOptions;
use event::{resize::Resized, key::KeyPress, mouse::MouseMotion};
use metrics::Metrics;

/// An application with a graphical interface.
#[derive(Default)]
pub struct GraphicalApplication {
    events: Events,
    systems: Systems,
    world: UnsafeWorldCell,

    metrics: Metrics,
    options: GraphicalApplicationOptions,
}

impl GraphicalApplication {
    /// Initializes [`self`] from [`GraphicalApplicationOptions`].
    pub fn from_config(options: impl Into<GraphicalApplicationOptions>) -> Self {
        let events = Events::default();
        let systems = Systems::default();
        let world = UnsafeWorldCell::default();

        let metrics = Metrics::default();
        let options = options.into();

        Self { events, systems, world, metrics, options }
    }

    /// Runs the application.
    ///
    /// # Panics
    /// Will panics if the event loop fails.
    pub fn run(&mut self) {
        debug!("Running application...");

        self.run_inner()
            .expect("Application failed unexpectedly!");
    }

    /// Runs a winit event loop.
    ///
    /// Configures the event loop to poll as this is ideal for real-time graphics.
    fn run_inner(&mut self) -> Result<(), EventLoopError> {
        let event_loop = EventLoop::new()?;
        event_loop.set_control_flow(ControlFlow::Poll);
        event_loop.run_app(self)?;

        Ok(())
    }

    /// Performs actions while a [`Window`] closes.
    ///
    /// - [`Shutdown`] systems are executed.
    fn close(&mut self, event_loop: &ActiveEventLoop) {
        event_loop.exit();

        // Run after the event loop closes for the window to shut properly.
        self.systems.run(Shutdown, &self.world);
    }

    /// Performs actions during a redraw.
    ///
    /// - [`Update`] and [`FixedUpdate`] systems are ran.
    /// - [`GameTime`] is measured.
    /// - [`Window`] is redrawn.
    fn redraw(&mut self) {
        self.systems.run(Update, &self.world);

        self.metrics.measure();
        if self.metrics.elapsed() > self.options.tick_rate {
            self.systems.run(FixedUpdate, &self.world);
            self.metrics.reset_elapsed();
        }

        self.time_measure();
        self.window_redraw();
    }

    /// Measures the applications temporals.
    fn time_measure(&mut self) {
        let world = self.world.get_mut();
        let components = world.components_mut();

        let mut time_query = components.query_mut::<GameTime>();
        let time = time_query
            .first_mut()
            .expect("Failed to retrieve a temporal device while redrawing.");

        time.delta = self.metrics.frame_time() as f32;
    }

    /// Performs a redraw of the [`Window`].
    fn window_redraw(&self) {
        let world = self.world.get();
        let components = world.components();

        let context_query = components.query::<WindowContext>();
        let context = context_query
            .first()
            .expect("Failed to retrieve a window while redrawing.");

        context.redraw();
    }

    /// Executed when the window resizes.
    fn resize(&mut self, width: u32, height: u32) {
        self.options.with_size(width, height);

        let world = self.world.get_mut();
        let events = world.events_mut();

        events.queue(Resized::from_size(width, height));
    }

    /// Handles keyboard input.
    fn keyboard_input(&mut self, event: &KeyEvent) {
        let Ok(key) = KeyPress::try_from(event) else {
            return;
        };

        let world = self.world.get_mut();
        let events = world.events_mut();

        events.queue(key);
    }

    /// Handles mouse input.
    fn mouse_input(&mut self, state: &ElementState, button: &MouseButton) {
        if !state.is_pressed() {
            return;
        }

        let world = self.world.get_mut();
        let events = world.events_mut();

        events.queue(Into::<MouseButton>::into(*button));
    }

    /// Handles mouse motion.
    fn mouse_motion(&mut self, delta_x: f64, delta_y: f64) {
        let world = self.world.get_mut();
        let events = world.events_mut();

        events.queue(MouseMotion::from_delta(delta_x, delta_y));
    }

    pub fn add_system<S, F, P>(&mut self, schedule: S, system: F) -> &mut Self
        where
            S: 'static,
            F: IntoSystemHandler<P> + 'static,
            P: 'static,
    {
        self.systems.add(schedule, system);

        self
    }

    pub fn handle_event<E, H, P>(&mut self, handler: H) -> &mut Self
        where
            E: 'static,
            H: IntoEventHandler<E, P> + 'static,
            P: 'static,
    {
        self.events.register(handler);

        self
    }

    pub fn add_plugin<TPlugin>(&mut self, plugin: TPlugin) -> &mut Self
        where
            TPlugin: Plugin,
    {
        plugin.attach(&mut self.events, &mut self.systems);

        self
    }
}

impl ApplicationHandler for GraphicalApplication {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        debug!("Window resumed...");

        let Ok(window) = event_loop.create_window(self.options.to_attr()) else {
            panic!("Failed to initialize the window!");
        };

        let world = self.world.get_mut();
        let components = world.components_mut();

        components.spawn(WindowContext::from_window(window));
        components.spawn(ApplicationLifecycle::default());
        components.spawn(GameTime::default());

        self.systems.run(Startup, &self.world);

        let events = world.events_mut();

        // Trigger to ensure the surface is ready for presentation.
        events.queue(self.options.to_resized());
        self.events.consume(&self.world);
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
        match event {
            WindowEvent::CloseRequested => self.close(event_loop),
            WindowEvent::RedrawRequested => self.redraw(),
            WindowEvent::Resized(PhysicalSize { width, height }) => self.resize(width, height),
            WindowEvent::KeyboardInput { event, .. } => self.keyboard_input(&event),
            WindowEvent::MouseInput { state, button, .. } => self.mouse_input(&state, &button),
            _ => { },
        };

        let world = self.world.get();
        let components = world.components();

        let lifecycle = components.query::<ApplicationLifecycle>();
        if lifecycle.iter().any(|l| l.is_exiting()) {
            self.close(event_loop);
        }

        self.events.consume(&self.world);
    }

    fn device_event(&mut self, event_loop: &ActiveEventLoop, _id: DeviceId, event: DeviceEvent) {
        match event {
            DeviceEvent::MouseMotion { delta: (delta_x, delta_y) } => self.mouse_motion(delta_x, delta_y),
            _ => { }
        };

        let world = self.world.get();
        let components = world.components();

        let lifecycle = components.query::<ApplicationLifecycle>();
        if lifecycle.iter().any(|l| l.is_exiting()) {
            self.close(event_loop);
        }

        self.events.consume(&self.world);
    }
}