Skip to main content

sable_platform/
window.rs

1//! Window management for the Sable engine.
2//!
3//! Provides a cross-platform window abstraction built on winit.
4
5use std::sync::Arc;
6
7use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
8use winit::dpi::{LogicalSize, PhysicalSize};
9use winit::window::WindowAttributes;
10
11use crate::Result;
12
13/// Configuration for creating a window.
14#[derive(Debug, Clone)]
15pub struct WindowConfig {
16    /// Window title.
17    pub title: String,
18    /// Initial width in logical pixels.
19    pub width: u32,
20    /// Initial height in logical pixels.
21    pub height: u32,
22    /// Whether the window is resizable.
23    pub resizable: bool,
24    /// Whether to enable vsync (hint to the GPU layer).
25    pub vsync: bool,
26    /// Whether to start in fullscreen mode.
27    pub fullscreen: bool,
28    /// Minimum window size (width, height) in logical pixels.
29    pub min_size: Option<(u32, u32)>,
30    /// Maximum window size (width, height) in logical pixels.
31    pub max_size: Option<(u32, u32)>,
32}
33
34impl Default for WindowConfig {
35    fn default() -> Self {
36        Self {
37            title: "Sable Engine".to_string(),
38            width: 1280,
39            height: 720,
40            resizable: true,
41            vsync: true,
42            fullscreen: false,
43            min_size: Some((320, 240)),
44            max_size: None,
45        }
46    }
47}
48
49impl WindowConfig {
50    /// Create a new window configuration with a title.
51    #[must_use]
52    pub fn new(title: impl Into<String>) -> Self {
53        Self {
54            title: title.into(),
55            ..Default::default()
56        }
57    }
58
59    /// Set the window size.
60    #[must_use]
61    pub fn with_size(mut self, width: u32, height: u32) -> Self {
62        self.width = width;
63        self.height = height;
64        self
65    }
66
67    /// Set whether the window is resizable.
68    #[must_use]
69    pub fn with_resizable(mut self, resizable: bool) -> Self {
70        self.resizable = resizable;
71        self
72    }
73
74    /// Set whether vsync is enabled.
75    #[must_use]
76    pub fn with_vsync(mut self, vsync: bool) -> Self {
77        self.vsync = vsync;
78        self
79    }
80
81    /// Set whether to start in fullscreen mode.
82    #[must_use]
83    pub fn with_fullscreen(mut self, fullscreen: bool) -> Self {
84        self.fullscreen = fullscreen;
85        self
86    }
87
88    /// Set the minimum window size.
89    #[must_use]
90    pub fn with_min_size(mut self, width: u32, height: u32) -> Self {
91        self.min_size = Some((width, height));
92        self
93    }
94
95    /// Set the maximum window size.
96    #[must_use]
97    pub fn with_max_size(mut self, width: u32, height: u32) -> Self {
98        self.max_size = Some((width, height));
99        self
100    }
101}
102
103/// A cross-platform window.
104///
105/// The window is reference-counted internally and can be cloned cheaply.
106#[derive(Debug, Clone)]
107pub struct Window {
108    inner: Arc<winit::window::Window>,
109    vsync: bool,
110}
111
112impl Window {
113    /// Create a new window with the given configuration.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if the window could not be created.
118    pub fn new(event_loop: &winit::event_loop::ActiveEventLoop, config: &WindowConfig) -> Result<Self> {
119        let mut attributes = WindowAttributes::default()
120            .with_title(&config.title)
121            .with_inner_size(LogicalSize::new(config.width, config.height))
122            .with_resizable(config.resizable);
123
124        if let Some((min_w, min_h)) = config.min_size {
125            attributes = attributes.with_min_inner_size(LogicalSize::new(min_w, min_h));
126        }
127
128        if let Some((max_w, max_h)) = config.max_size {
129            attributes = attributes.with_max_inner_size(LogicalSize::new(max_w, max_h));
130        }
131
132        if config.fullscreen {
133            attributes =
134                attributes.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
135        }
136
137        let window = event_loop.create_window(attributes)?;
138
139        Ok(Self {
140            inner: Arc::new(window),
141            vsync: config.vsync,
142        })
143    }
144
145    /// Get the current inner size of the window in physical pixels.
146    #[must_use]
147    pub fn inner_size(&self) -> PhysicalSize<u32> {
148        self.inner.inner_size()
149    }
150
151    /// Get the window's scale factor (DPI scaling).
152    #[must_use]
153    pub fn scale_factor(&self) -> f64 {
154        self.inner.scale_factor()
155    }
156
157    /// Get whether vsync is enabled for this window.
158    #[must_use]
159    pub fn vsync(&self) -> bool {
160        self.vsync
161    }
162
163    /// Set the window title.
164    pub fn set_title(&self, title: &str) {
165        self.inner.set_title(title);
166    }
167
168    /// Request a redraw of the window.
169    pub fn request_redraw(&self) {
170        self.inner.request_redraw();
171    }
172
173    /// Get the raw window handle for GPU surface creation.
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if the window handle cannot be obtained.
178    pub fn window_handle(
179        &self,
180    ) -> std::result::Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError>
181    {
182        self.inner.window_handle()
183    }
184
185    /// Get the raw display handle for GPU surface creation.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the display handle cannot be obtained.
190    pub fn display_handle(
191        &self,
192    ) -> std::result::Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError>
193    {
194        self.inner.display_handle()
195    }
196
197    /// Get a reference to the underlying winit window.
198    ///
199    /// This is useful for advanced use cases that need direct winit access.
200    #[must_use]
201    pub fn winit_window(&self) -> &winit::window::Window {
202        &self.inner
203    }
204
205    /// Get the window ID.
206    #[must_use]
207    pub fn id(&self) -> winit::window::WindowId {
208        self.inner.id()
209    }
210}
211
212// Safety: Window is Send + Sync because Arc<winit::window::Window> is.
213unsafe impl Send for Window {}
214unsafe impl Sync for Window {}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_window_config_default() {
222        let config = WindowConfig::default();
223        assert_eq!(config.title, "Sable Engine");
224        assert_eq!(config.width, 1280);
225        assert_eq!(config.height, 720);
226        assert!(config.resizable);
227        assert!(config.vsync);
228        assert!(!config.fullscreen);
229        assert_eq!(config.min_size, Some((320, 240)));
230        assert_eq!(config.max_size, None);
231    }
232
233    #[test]
234    fn test_window_config_builder() {
235        let config = WindowConfig::new("Test Window")
236            .with_size(800, 600)
237            .with_resizable(false)
238            .with_vsync(false)
239            .with_fullscreen(true)
240            .with_min_size(400, 300)
241            .with_max_size(1920, 1080);
242
243        assert_eq!(config.title, "Test Window");
244        assert_eq!(config.width, 800);
245        assert_eq!(config.height, 600);
246        assert!(!config.resizable);
247        assert!(!config.vsync);
248        assert!(config.fullscreen);
249        assert_eq!(config.min_size, Some((400, 300)));
250        assert_eq!(config.max_size, Some((1920, 1080)));
251    }
252
253    #[test]
254    fn test_window_config_into_string() {
255        let config = WindowConfig::new(String::from("Dynamic Title"));
256        assert_eq!(config.title, "Dynamic Title");
257    }
258}
259