sable-platform 0.1.0

Platform abstraction layer for Sable Engine - windowing, input, and events
Documentation
//! Window management for the Sable engine.
//!
//! Provides a cross-platform window abstraction built on winit.

use std::sync::Arc;

use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use winit::dpi::{LogicalSize, PhysicalSize};
use winit::window::WindowAttributes;

use crate::Result;

/// Configuration for creating a window.
#[derive(Debug, Clone)]
pub struct WindowConfig {
    /// Window title.
    pub title: String,
    /// Initial width in logical pixels.
    pub width: u32,
    /// Initial height in logical pixels.
    pub height: u32,
    /// Whether the window is resizable.
    pub resizable: bool,
    /// Whether to enable vsync (hint to the GPU layer).
    pub vsync: bool,
    /// Whether to start in fullscreen mode.
    pub fullscreen: bool,
    /// Minimum window size (width, height) in logical pixels.
    pub min_size: Option<(u32, u32)>,
    /// Maximum window size (width, height) in logical pixels.
    pub max_size: Option<(u32, u32)>,
}

impl Default for WindowConfig {
    fn default() -> Self {
        Self {
            title: "Sable Engine".to_string(),
            width: 1280,
            height: 720,
            resizable: true,
            vsync: true,
            fullscreen: false,
            min_size: Some((320, 240)),
            max_size: None,
        }
    }
}

impl WindowConfig {
    /// Create a new window configuration with a title.
    #[must_use]
    pub fn new(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            ..Default::default()
        }
    }

    /// Set the window size.
    #[must_use]
    pub fn with_size(mut self, width: u32, height: u32) -> Self {
        self.width = width;
        self.height = height;
        self
    }

    /// Set whether the window is resizable.
    #[must_use]
    pub fn with_resizable(mut self, resizable: bool) -> Self {
        self.resizable = resizable;
        self
    }

    /// Set whether vsync is enabled.
    #[must_use]
    pub fn with_vsync(mut self, vsync: bool) -> Self {
        self.vsync = vsync;
        self
    }

    /// Set whether to start in fullscreen mode.
    #[must_use]
    pub fn with_fullscreen(mut self, fullscreen: bool) -> Self {
        self.fullscreen = fullscreen;
        self
    }

    /// Set the minimum window size.
    #[must_use]
    pub fn with_min_size(mut self, width: u32, height: u32) -> Self {
        self.min_size = Some((width, height));
        self
    }

    /// Set the maximum window size.
    #[must_use]
    pub fn with_max_size(mut self, width: u32, height: u32) -> Self {
        self.max_size = Some((width, height));
        self
    }
}

/// A cross-platform window.
///
/// The window is reference-counted internally and can be cloned cheaply.
#[derive(Debug, Clone)]
pub struct Window {
    inner: Arc<winit::window::Window>,
    vsync: bool,
}

impl Window {
    /// Create a new window with the given configuration.
    ///
    /// # Errors
    ///
    /// Returns an error if the window could not be created.
    pub fn new(event_loop: &winit::event_loop::ActiveEventLoop, config: &WindowConfig) -> Result<Self> {
        let mut attributes = WindowAttributes::default()
            .with_title(&config.title)
            .with_inner_size(LogicalSize::new(config.width, config.height))
            .with_resizable(config.resizable);

        if let Some((min_w, min_h)) = config.min_size {
            attributes = attributes.with_min_inner_size(LogicalSize::new(min_w, min_h));
        }

        if let Some((max_w, max_h)) = config.max_size {
            attributes = attributes.with_max_inner_size(LogicalSize::new(max_w, max_h));
        }

        if config.fullscreen {
            attributes =
                attributes.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
        }

        let window = event_loop.create_window(attributes)?;

        Ok(Self {
            inner: Arc::new(window),
            vsync: config.vsync,
        })
    }

    /// Get the current inner size of the window in physical pixels.
    #[must_use]
    pub fn inner_size(&self) -> PhysicalSize<u32> {
        self.inner.inner_size()
    }

    /// Get the window's scale factor (DPI scaling).
    #[must_use]
    pub fn scale_factor(&self) -> f64 {
        self.inner.scale_factor()
    }

    /// Get whether vsync is enabled for this window.
    #[must_use]
    pub fn vsync(&self) -> bool {
        self.vsync
    }

    /// Set the window title.
    pub fn set_title(&self, title: &str) {
        self.inner.set_title(title);
    }

    /// Request a redraw of the window.
    pub fn request_redraw(&self) {
        self.inner.request_redraw();
    }

    /// Get the raw window handle for GPU surface creation.
    ///
    /// # Errors
    ///
    /// Returns an error if the window handle cannot be obtained.
    pub fn window_handle(
        &self,
    ) -> std::result::Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError>
    {
        self.inner.window_handle()
    }

    /// Get the raw display handle for GPU surface creation.
    ///
    /// # Errors
    ///
    /// Returns an error if the display handle cannot be obtained.
    pub fn display_handle(
        &self,
    ) -> std::result::Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError>
    {
        self.inner.display_handle()
    }

    /// Get a reference to the underlying winit window.
    ///
    /// This is useful for advanced use cases that need direct winit access.
    #[must_use]
    pub fn winit_window(&self) -> &winit::window::Window {
        &self.inner
    }

    /// Get the window ID.
    #[must_use]
    pub fn id(&self) -> winit::window::WindowId {
        self.inner.id()
    }
}

// Safety: Window is Send + Sync because Arc<winit::window::Window> is.
unsafe impl Send for Window {}
unsafe impl Sync for Window {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_window_config_default() {
        let config = WindowConfig::default();
        assert_eq!(config.title, "Sable Engine");
        assert_eq!(config.width, 1280);
        assert_eq!(config.height, 720);
        assert!(config.resizable);
        assert!(config.vsync);
        assert!(!config.fullscreen);
        assert_eq!(config.min_size, Some((320, 240)));
        assert_eq!(config.max_size, None);
    }

    #[test]
    fn test_window_config_builder() {
        let config = WindowConfig::new("Test Window")
            .with_size(800, 600)
            .with_resizable(false)
            .with_vsync(false)
            .with_fullscreen(true)
            .with_min_size(400, 300)
            .with_max_size(1920, 1080);

        assert_eq!(config.title, "Test Window");
        assert_eq!(config.width, 800);
        assert_eq!(config.height, 600);
        assert!(!config.resizable);
        assert!(!config.vsync);
        assert!(config.fullscreen);
        assert_eq!(config.min_size, Some((400, 300)));
        assert_eq!(config.max_size, Some((1920, 1080)));
    }

    #[test]
    fn test_window_config_into_string() {
        let config = WindowConfig::new(String::from("Dynamic Title"));
        assert_eq!(config.title, "Dynamic Title");
    }
}