sable-platform 0.1.0

Platform abstraction layer for Sable Engine - windowing, input, and events
Documentation
//! Event loop and application events.
//!
//! Provides the main event loop abstraction and application-level events.

use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event::{ElementState, WindowEvent};
use winit::event_loop::ActiveEventLoop;
use winit::window::WindowId;

use crate::input::{InputEvent, KeyCode, MouseButton};
use crate::Result;

/// The main event loop for the application.
pub struct EventLoop {
    inner: winit::event_loop::EventLoop<()>,
}

impl EventLoop {
    /// Create a new event loop.
    ///
    /// # Errors
    ///
    /// Returns an error if the event loop could not be created.
    pub fn new() -> Result<Self> {
        let inner = winit::event_loop::EventLoop::new()?;
        Ok(Self { inner })
    }

    /// Get a reference to the inner winit event loop.
    #[must_use]
    pub fn inner(&self) -> &winit::event_loop::EventLoop<()> {
        &self.inner
    }

    /// Run the event loop with a callback handler.
    ///
    /// The callback receives [`AppEvent`]s and a [`ControlFlow`] to control the loop.
    ///
    /// # Errors
    ///
    /// Returns an error if the event loop fails to run.
    pub fn run<F>(self, mut callback: F) -> Result<()>
    where
        F: FnMut(AppEvent, &mut ControlFlow) + 'static,
    {
        let mut control_flow = ControlFlow::default();
        let mut handler = CallbackHandler {
            callback: &mut callback,
            control_flow: &mut control_flow,
        };

        self.inner.run_app(&mut handler)?;
        Ok(())
    }
}

/// Application-level events.
#[derive(Debug, Clone)]
pub enum AppEvent {
    /// The window was resized.
    Resized {
        /// The new width in physical pixels.
        width: u32,
        /// The new height in physical pixels.
        height: u32,
    },
    /// The window close button was pressed.
    CloseRequested,
    /// The window needs to be redrawn.
    RedrawRequested,
    /// The window gained or lost focus.
    Focused(bool),
    /// An input event occurred.
    Input(InputEvent),
    /// The application is about to exit.
    LoopExiting,
    /// A new frame is about to start (called at the start of each event batch).
    NewEvents,
    /// All events for this frame have been processed.
    AboutToWait,
}

/// Controls the behavior of the event loop.
#[derive(Debug, Default)]
pub struct ControlFlow {
    should_exit: bool,
}

impl ControlFlow {
    /// Request the event loop to exit after this event is processed.
    pub fn exit(&mut self) {
        self.should_exit = true;
    }

    /// Check if an exit has been requested.
    #[must_use]
    pub fn should_exit(&self) -> bool {
        self.should_exit
    }
}

/// Internal handler that bridges winit's `ApplicationHandler` to our callback.
struct CallbackHandler<'a, F>
where
    F: FnMut(AppEvent, &mut ControlFlow),
{
    callback: &'a mut F,
    control_flow: &'a mut ControlFlow,
}

impl<F> ApplicationHandler for CallbackHandler<'_, F>
where
    F: FnMut(AppEvent, &mut ControlFlow),
{
    fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
        // Window is ready, nothing specific to do here
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window_id: WindowId,
        event: WindowEvent,
    ) {
        let app_event = match event {
            WindowEvent::CloseRequested => Some(AppEvent::CloseRequested),
            WindowEvent::Resized(PhysicalSize { width, height }) => {
                Some(AppEvent::Resized { width, height })
            }
            WindowEvent::RedrawRequested => Some(AppEvent::RedrawRequested),
            WindowEvent::Focused(focused) => Some(AppEvent::Focused(focused)),
            WindowEvent::KeyboardInput { event, .. } => {
                if let Some(key_code) = convert_key_code(event.physical_key) {
                    let input_event = match event.state {
                        ElementState::Pressed => InputEvent::KeyPressed(key_code),
                        ElementState::Released => InputEvent::KeyReleased(key_code),
                    };
                    Some(AppEvent::Input(input_event))
                } else {
                    None
                }
            }
            WindowEvent::MouseInput { state, button, .. } => {
                let mouse_button = convert_mouse_button(button);
                let input_event = match state {
                    ElementState::Pressed => InputEvent::MousePressed(mouse_button),
                    ElementState::Released => InputEvent::MouseReleased(mouse_button),
                };
                Some(AppEvent::Input(input_event))
            }
            WindowEvent::CursorMoved { position, .. } => {
                Some(AppEvent::Input(InputEvent::MouseMoved {
                    x: position.x,
                    y: position.y,
                }))
            }
            WindowEvent::MouseWheel { delta, .. } => {
                let (x, y) = match delta {
                    winit::event::MouseScrollDelta::LineDelta(x, y) => (f64::from(x), f64::from(y)),
                    winit::event::MouseScrollDelta::PixelDelta(pos) => (pos.x, pos.y),
                };
                Some(AppEvent::Input(InputEvent::MouseWheel {
                    delta_x: x,
                    delta_y: y,
                }))
            }
            _ => None,
        };

        if let Some(event) = app_event {
            (self.callback)(event, self.control_flow);
        }

        if self.control_flow.should_exit() {
            event_loop.exit();
        }
    }

    fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: winit::event::StartCause) {
        (self.callback)(AppEvent::NewEvents, self.control_flow);
    }

    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        (self.callback)(AppEvent::AboutToWait, self.control_flow);
        if self.control_flow.should_exit() {
            event_loop.exit();
        }
    }

    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
        (self.callback)(AppEvent::LoopExiting, self.control_flow);
    }
}

/// Convert winit key code to our key code.
fn convert_key_code(key: winit::keyboard::PhysicalKey) -> Option<KeyCode> {
    use winit::keyboard::{KeyCode as WinitKey, PhysicalKey};

    match key {
        PhysicalKey::Code(code) => Some(match code {
            WinitKey::KeyA => KeyCode::A,
            WinitKey::KeyB => KeyCode::B,
            WinitKey::KeyC => KeyCode::C,
            WinitKey::KeyD => KeyCode::D,
            WinitKey::KeyE => KeyCode::E,
            WinitKey::KeyF => KeyCode::F,
            WinitKey::KeyG => KeyCode::G,
            WinitKey::KeyH => KeyCode::H,
            WinitKey::KeyI => KeyCode::I,
            WinitKey::KeyJ => KeyCode::J,
            WinitKey::KeyK => KeyCode::K,
            WinitKey::KeyL => KeyCode::L,
            WinitKey::KeyM => KeyCode::M,
            WinitKey::KeyN => KeyCode::N,
            WinitKey::KeyO => KeyCode::O,
            WinitKey::KeyP => KeyCode::P,
            WinitKey::KeyQ => KeyCode::Q,
            WinitKey::KeyR => KeyCode::R,
            WinitKey::KeyS => KeyCode::S,
            WinitKey::KeyT => KeyCode::T,
            WinitKey::KeyU => KeyCode::U,
            WinitKey::KeyV => KeyCode::V,
            WinitKey::KeyW => KeyCode::W,
            WinitKey::KeyX => KeyCode::X,
            WinitKey::KeyY => KeyCode::Y,
            WinitKey::KeyZ => KeyCode::Z,
            WinitKey::Digit0 => KeyCode::Num0,
            WinitKey::Digit1 => KeyCode::Num1,
            WinitKey::Digit2 => KeyCode::Num2,
            WinitKey::Digit3 => KeyCode::Num3,
            WinitKey::Digit4 => KeyCode::Num4,
            WinitKey::Digit5 => KeyCode::Num5,
            WinitKey::Digit6 => KeyCode::Num6,
            WinitKey::Digit7 => KeyCode::Num7,
            WinitKey::Digit8 => KeyCode::Num8,
            WinitKey::Digit9 => KeyCode::Num9,
            WinitKey::Escape => KeyCode::Escape,
            WinitKey::Enter => KeyCode::Enter,
            WinitKey::Space => KeyCode::Space,
            WinitKey::Tab => KeyCode::Tab,
            WinitKey::Backspace => KeyCode::Backspace,
            WinitKey::ShiftLeft | WinitKey::ShiftRight => KeyCode::Shift,
            WinitKey::ControlLeft | WinitKey::ControlRight => KeyCode::Control,
            WinitKey::AltLeft | WinitKey::AltRight => KeyCode::Alt,
            WinitKey::ArrowUp => KeyCode::Up,
            WinitKey::ArrowDown => KeyCode::Down,
            WinitKey::ArrowLeft => KeyCode::Left,
            WinitKey::ArrowRight => KeyCode::Right,
            WinitKey::F1 => KeyCode::F1,
            WinitKey::F2 => KeyCode::F2,
            WinitKey::F3 => KeyCode::F3,
            WinitKey::F4 => KeyCode::F4,
            WinitKey::F5 => KeyCode::F5,
            WinitKey::F6 => KeyCode::F6,
            WinitKey::F7 => KeyCode::F7,
            WinitKey::F8 => KeyCode::F8,
            WinitKey::F9 => KeyCode::F9,
            WinitKey::F10 => KeyCode::F10,
            WinitKey::F11 => KeyCode::F11,
            WinitKey::F12 => KeyCode::F12,
            _ => return None,
        }),
        PhysicalKey::Unidentified(_) => None,
    }
}

/// Convert winit mouse button to our mouse button.
fn convert_mouse_button(button: winit::event::MouseButton) -> MouseButton {
    match button {
        winit::event::MouseButton::Left => MouseButton::Left,
        winit::event::MouseButton::Right => MouseButton::Right,
        winit::event::MouseButton::Middle => MouseButton::Middle,
        winit::event::MouseButton::Back => MouseButton::Back,
        winit::event::MouseButton::Forward => MouseButton::Forward,
        winit::event::MouseButton::Other(id) => MouseButton::Other(id),
    }
}