Skip to main content

optic_window/
window.rs

1use optic_core::{Coord2D, CoordOffset, Size2D};
2use winit::dpi::{PhysicalPosition, PhysicalSize};
3use winit::window::{CursorGrabMode, Fullscreen, Window as WinitWindow};
4
5use crate::ScreenInfo;
6
7/// A winit window wrapper with frame-tracking and cursor management.
8///
9/// Owns an optional [`Arc<WinitWindow>`]. When closed, the inner handle is
10/// set to `None` and all methods become no-ops returning default values.
11///
12/// # Frame tracking
13///
14/// Call [`update_frame`](Window::update_frame) once per frame after
15/// processing events. This computes:
16/// - Cursor delta (movement since last frame)
17/// - Window position delta
18/// - Cursor loopback wrapping
19///
20/// # Cursor modes
21///
22/// The window supports three cursor modes:
23/// - **Grab** — cursor is locked to the window (hidden, infinite movement)
24/// - **Confine** — cursor is confined to the window area (visible, clamped)
25/// - **Loopback** — cursor position wraps at window edges (software, for raw
26///   input in first-person controls)
27///
28/// Grab and confine are winit-level operations; loopback is implemented by
29/// [`update_frame`](Window::update_frame) warping the cursor.
30#[derive(Debug)]
31pub struct Window {
32    inner: Option<std::sync::Arc<WinitWindow>>,
33    prev_cursor_pos: Coord2D,
34    cursor_delta: CoordOffset,
35    prev_position: Coord2D,
36    position_delta: CoordOffset,
37    prev_size: Size2D,
38    cursor_inside: bool,
39    tracking_started: bool,
40    cursor_pos: Coord2D,
41    cursor_visible: bool,
42    cursor_grabbed: bool,
43    cursor_confined: bool,
44    cursor_loopback: bool,
45    min_size: Option<Size2D>,
46    max_size: Option<Size2D>,
47}
48
49impl Window {
50    /// Create a new window.
51    ///
52    /// The window starts hidden — call [`set_visible`](Window::set_visible)`(true)`
53    /// when ready, or let the game loop manage visibility.
54    #[allow(deprecated)]
55    pub fn new(el: &winit::event_loop::EventLoop<()>, title: &str, size: Size2D) -> Self {
56        let attrs = WinitWindow::default_attributes()
57            .with_title(title)
58            .with_inner_size(PhysicalSize::new(size.w, size.h))
59            .with_visible(false);
60        let w = el.create_window(attrs).unwrap();
61        let arc = std::sync::Arc::new(w);
62        let window = Self {
63            inner: Some(arc),
64            prev_cursor_pos: Coord2D::empty(),
65            cursor_delta: CoordOffset::empty(),
66            prev_position: Coord2D::empty(),
67            position_delta: CoordOffset::empty(),
68            prev_size: size,
69            cursor_inside: true,
70            tracking_started: false,
71            cursor_pos: Coord2D::empty(),
72            cursor_visible: true,
73            cursor_grabbed: false,
74            cursor_confined: false,
75            cursor_loopback: false,
76            min_size: None,
77            max_size: None,
78        };
79        window
80    }
81
82    /// Create a new transparent window (requires a compositor that supports transparency).
83    ///
84    /// Same as [`new`](Window::new) but sets `with_transparent(true)` on the winit window.
85    #[allow(deprecated)]
86    pub fn new_transparent(el: &winit::event_loop::EventLoop<()>, title: &str, size: Size2D) -> Self {
87        let attrs = WinitWindow::default_attributes()
88            .with_title(title)
89            .with_inner_size(PhysicalSize::new(size.w, size.h))
90            .with_visible(false)
91            .with_transparent(true);
92        let w = el.create_window(attrs).unwrap();
93        let arc = std::sync::Arc::new(w);
94        let window = Self {
95            inner: Some(arc),
96            prev_cursor_pos: Coord2D::empty(),
97            cursor_delta: CoordOffset::empty(),
98            prev_position: Coord2D::empty(),
99            position_delta: CoordOffset::empty(),
100            prev_size: size,
101            cursor_inside: true,
102            tracking_started: false,
103            cursor_pos: Coord2D::empty(),
104            cursor_visible: true,
105            cursor_grabbed: false,
106            cursor_confined: false,
107            cursor_loopback: false,
108            min_size: None,
109            max_size: None,
110        };
111        window
112    }
113
114    /// Close the window. The inner winit handle is dropped.
115    pub fn close(&mut self) {
116        self.inner = None;
117    }
118
119    /// True if the window has been closed.
120    pub fn is_closed(&self) -> bool {
121        self.inner.is_none()
122    }
123
124    /// True if the window is still open.
125    pub fn is_running(&self) -> bool {
126        self.inner.is_some()
127    }
128
129    // ── Identity ──────────────────────────────────────────────────────────
130
131    /// Raw window handle for EGL surface creation.
132    pub fn raw_handle(&self) -> Option<raw_window_handle::RawWindowHandle> {
133        use raw_window_handle::HasWindowHandle;
134        self.inner.as_ref().map(|w| w.window_handle().unwrap().as_raw())
135    }
136
137    /// Raw display handle for EGL display connection.
138    pub fn raw_display_handle(&self) -> Option<raw_window_handle::RawDisplayHandle> {
139        use raw_window_handle::HasDisplayHandle;
140        self.inner.as_ref().map(|w| w.display_handle().unwrap().as_raw())
141    }
142
143    /// The winit window ID.
144    pub fn id(&self) -> Option<winit::window::WindowId> {
145        self.inner.as_ref().map(|w| w.id())
146    }
147
148    // ── Sizing ────────────────────────────────────────────────────────────
149
150    /// Current inner size (live winit query).
151    pub fn size(&self) -> Size2D {
152        self.inner.as_ref().map_or(self.prev_size, |w| {
153            let s = w.inner_size();
154            Size2D::from(s.width, s.height)
155        })
156    }
157
158    /// Request a new inner size.
159    ///
160    /// The OS may not honor the exact request — check [`size()`](Window::size) later.
161    pub fn set_size(&self, size: Size2D) {
162        if let Some(ref w) = self.inner {
163            let _ = w.request_inner_size(PhysicalSize::new(size.w, size.h));
164        }
165    }
166
167    /// The size cached at the last [`update_frame`](Window::update_frame) call.
168    pub fn prev_size(&self) -> Size2D {
169        self.prev_size
170    }
171
172    /// Minimum window size (cached value, not a live winit query).
173    pub fn min_size(&self) -> Option<Size2D> {
174        self.min_size
175    }
176
177    /// Set the minimum window size.
178    pub fn set_min_size(&mut self, size: Option<Size2D>) {
179        self.min_size = size;
180        if let Some(ref w) = self.inner {
181            w.set_min_inner_size(size.map(|s| PhysicalSize::new(s.w, s.h)));
182        }
183    }
184
185    /// Maximum window size (cached value, not a live winit query).
186    pub fn max_size(&self) -> Option<Size2D> {
187        self.max_size
188    }
189
190    /// Set the maximum window size.
191    pub fn set_max_size(&mut self, size: Option<Size2D>) {
192        self.max_size = size;
193        if let Some(ref w) = self.inner {
194            w.set_max_inner_size(size.map(|s| PhysicalSize::new(s.w, s.h)));
195        }
196    }
197
198    /// True if the window is resizable (live winit query).
199    pub fn resizable(&self) -> bool {
200        self.inner.as_ref().map_or(true, |w| w.is_resizable())
201    }
202
203    /// Enable or disable resizing.
204    pub fn set_resizable(&self, enable: bool) {
205        if let Some(ref w) = self.inner {
206            w.set_resizable(enable);
207        }
208    }
209
210    // ── Desktop Position ──────────────────────────────────────────────────
211
212    /// Desktop position of the window (live winit query via `outer_position`).
213    pub fn position(&self) -> Coord2D {
214        self.inner.as_ref().and_then(|w| {
215            w.outer_position().ok().map(|p| Coord2D::from(p.x as f64, p.y as f64))
216        }).unwrap_or(self.prev_position)
217    }
218
219    /// Set the desktop position.
220    pub fn set_position(&self, pos: Coord2D) {
221        if let Some(ref w) = self.inner {
222            let _ = w.set_outer_position(PhysicalPosition::new(pos.x as i32, pos.y as i32));
223        }
224    }
225
226    /// Center the window on the current monitor.
227    pub fn center_on_screen(&self) {
228        if let Some(ref w) = self.inner {
229            if let Some(monitor) = w.current_monitor() {
230                let mon_size = monitor.size();
231                let win_size = w.outer_size();
232                let x = (mon_size.width.saturating_sub(win_size.width)) / 2;
233                let y = (mon_size.height.saturating_sub(win_size.height)) / 2;
234                let _ = w.set_outer_position(PhysicalPosition::new(x as i32, y as i32));
235            }
236        }
237    }
238
239    /// Desktop position cached at the last [`update_frame`](Window::update_frame) call.
240    pub fn prev_position(&self) -> Coord2D {
241        self.prev_position
242    }
243
244    /// Cumulative window position delta since the last call to this method (reset on read).
245    pub fn position_delta(&mut self) -> CoordOffset {
246        let d = self.position_delta;
247        self.position_delta = CoordOffset::empty();
248        d
249    }
250
251    // ── Title ─────────────────────────────────────────────────────────────
252
253    /// Current window title (live winit query).
254    pub fn title(&self) -> String {
255        self.inner.as_ref().map_or(String::new(), |w| w.title())
256    }
257
258    /// Set the window title.
259    pub fn set_title(&self, title: &str) {
260        if let Some(ref w) = self.inner {
261            w.set_title(title);
262        }
263    }
264
265    // ── Fullscreen ────────────────────────────────────────────────────────
266
267    /// True if the window is currently fullscreen (live winit query).
268    pub fn is_fullscreen(&self) -> bool {
269        self.inner.as_ref().and_then(|w| w.fullscreen()).is_some()
270    }
271
272    /// Enter or exit borderless fullscreen.
273    pub fn set_fullscreen(&self, enable: bool) {
274        if let Some(ref w) = self.inner {
275            if enable {
276                w.set_fullscreen(Some(Fullscreen::Borderless(None)));
277            } else {
278                w.set_fullscreen(None);
279            }
280        }
281    }
282
283    /// Toggle fullscreen.
284    pub fn toggle_fullscreen(&self) {
285        self.set_fullscreen(!self.is_fullscreen());
286    }
287
288    // ── Window State ──────────────────────────────────────────────────────
289
290    /// True if the window is visible (live winit query).
291    pub fn is_visible(&self) -> bool {
292        self.inner.as_ref().and_then(|w| w.is_visible()).unwrap_or(false)
293    }
294
295    /// Show or hide the window.
296    pub fn set_visible(&self, visible: bool) {
297        if let Some(ref w) = self.inner {
298            w.set_visible(visible);
299        }
300    }
301
302    /// True if the window is minimized (live winit query).
303    pub fn is_minimized(&self) -> bool {
304        self.inner.as_ref().and_then(|w| w.is_minimized()).unwrap_or(false)
305    }
306
307    /// Minimize the window.
308    pub fn minimize(&self) {
309        if let Some(ref w) = self.inner {
310            w.set_minimized(true);
311        }
312    }
313
314    /// Restore from minimized.
315    pub fn restore(&self) {
316        if let Some(ref w) = self.inner {
317            w.set_minimized(false);
318        }
319    }
320
321    /// True if the window is maximized (live winit query).
322    pub fn is_maximized(&self) -> bool {
323        self.inner.as_ref().map_or(false, |w| w.is_maximized())
324    }
325
326    /// Maximize the window.
327    pub fn maximize(&self) {
328        if let Some(ref w) = self.inner {
329            w.set_maximized(true);
330        }
331    }
332
333    /// Unmaximize the window (restore).
334    pub fn unmaximize(&self) {
335        if let Some(ref w) = self.inner {
336            w.set_maximized(false);
337        }
338    }
339
340    /// True if the window has focus (live winit query).
341    pub fn has_focus(&self) -> bool {
342        self.inner.as_ref().map_or(false, |w| w.has_focus())
343    }
344
345    /// Request focus.
346    pub fn focus(&self) {
347        if let Some(ref w) = self.inner {
348            w.focus_window();
349        }
350    }
351
352    // ── Frame Control ─────────────────────────────────────────────────────
353
354    /// Request a redraw from winit.
355    pub fn request_redraw(&self) {
356        if let Some(ref w) = self.inner {
357            w.request_redraw();
358        }
359    }
360
361    // ── Screen ────────────────────────────────────────────────────────
362
363    /// Information about the monitor this window is on.
364    pub fn screen_info(&self) -> Option<ScreenInfo> {
365        self.inner.as_ref().and_then(|w| {
366            w.current_monitor().map(|m| ScreenInfo::from_handle(&m))
367        })
368    }
369
370    // ── Cursor ────────────────────────────────────────────────────────────
371
372    /// Cached cursor position (updated via events or `set_cursor_pos`).
373    pub fn cursor_pos(&self) -> Coord2D {
374        self.cursor_pos
375    }
376
377    /// Move the OS cursor and update the cached position.
378    pub fn set_cursor_pos(&mut self, pos: Coord2D) {
379        self.cursor_pos = pos;
380        if let Some(ref w) = self.inner {
381            let _ = w.set_cursor_position(PhysicalPosition::new(pos.x, pos.y));
382        }
383    }
384
385    /// Cursor delta since the last frame (computed by [`update_frame`](Window::update_frame)).
386    ///
387    /// Y is inverted (positive = up) to match screen coordinates.
388    pub fn cursor_delta(&self) -> CoordOffset {
389        self.cursor_delta
390    }
391
392    /// Cursor position normalized to 0..1 where (0,0) = bottom-left, (1,1) = top-right.
393    pub fn cursor_pos_normalized(&self) -> Coord2D {
394        let sz = self.size();
395        if sz.w == 0 || sz.h == 0 {
396            return Coord2D::empty();
397        }
398        Coord2D::from(self.cursor_pos.x / sz.w as f64, 1.0 - self.cursor_pos.y / sz.h as f64)
399    }
400
401    /// True if the cursor is inside the window client area (updated via events).
402    pub fn is_cursor_inside(&self) -> bool {
403        self.cursor_inside
404    }
405
406    /// True if the cursor is visible (cached, not a live winit query).
407    pub fn is_cursor_visible(&self) -> bool {
408        self.cursor_visible
409    }
410
411    /// Show or hide the cursor.
412    pub fn set_cursor_visible(&mut self, visible: bool) {
413        self.cursor_visible = visible;
414        if let Some(ref w) = self.inner {
415            w.set_cursor_visible(visible);
416        }
417    }
418
419    /// Toggle cursor visibility.
420    pub fn toggle_cursor_visible(&mut self) {
421        self.set_cursor_visible(!self.cursor_visible);
422    }
423
424    /// True if the cursor is grabbed (locked + hidden, for raw input).
425    pub fn is_cursor_grabbed(&self) -> bool {
426        self.cursor_grabbed
427    }
428
429    /// Lock or release the cursor (grab mode). Returns `Err(())` if the platform
430    /// does not support cursor locking.
431    pub fn set_cursor_grab(&mut self, grab: bool) -> Result<(), ()> {
432        let result = match self.inner.as_ref() {
433            Some(w) => w.set_cursor_grab(if grab { CursorGrabMode::Locked } else { CursorGrabMode::None })
434                .map_err(|_| ()),
435            None => Err(()),
436        };
437        if result.is_ok() {
438            self.cursor_grabbed = grab;
439        }
440        result
441    }
442
443    /// Toggle cursor grab.
444    pub fn toggle_cursor_grab(&mut self) {
445        let _ = self.set_cursor_grab(!self.cursor_grabbed);
446    }
447
448    /// True if the cursor is confined (clamped to window, visible).
449    pub fn is_cursor_confined(&self) -> bool {
450        self.cursor_confined
451    }
452
453    /// Confine or release the cursor. Returns `Err(())` if unsupported.
454    pub fn set_cursor_confine(&mut self, confine: bool) -> Result<(), ()> {
455        let result = match self.inner.as_ref() {
456            Some(w) => w.set_cursor_grab(if confine { CursorGrabMode::Confined } else { CursorGrabMode::None })
457                .map_err(|_| ()),
458            None => Err(()),
459        };
460        if result.is_ok() {
461            self.cursor_confined = confine;
462        }
463        result
464    }
465
466    /// Toggle cursor confine.
467    pub fn toggle_cursor_confine(&mut self) {
468        let _ = self.set_cursor_confine(!self.cursor_confined);
469    }
470
471    /// True if cursor loopback (software edge-wrapping) is enabled.
472    pub fn is_cursor_loopback(&self) -> bool {
473        self.cursor_loopback
474    }
475
476    /// Enable or disable loopback mode. When enabled, [`update_frame`](Window::update_frame)
477    /// will wrap the cursor position at window edges, useful for first-person camera control.
478    pub fn set_cursor_loopback(&mut self, loopback: bool) {
479        self.cursor_loopback = loopback;
480    }
481
482    // ── Frame Update ──────────────────────────────────────────────────────
483
484    /// Call once per frame after processing events.
485    ///
486    /// Snapshots cursor position, computes cursor/window deltas, and applies
487    /// loopback wrapping if enabled.
488    pub fn update_frame(&mut self) {
489        let new_pos = self.position();
490        if !self.tracking_started {
491            self.prev_cursor_pos = self.cursor_pos;
492            self.prev_position = new_pos;
493            self.prev_size = self.size();
494            self.position_delta = CoordOffset::empty();
495            self.cursor_delta = CoordOffset::empty();
496            self.tracking_started = true;
497        } else {
498            let raw = self.cursor_pos - self.prev_cursor_pos;
499            self.cursor_delta = CoordOffset { x: raw.x, y: -raw.y };
500            self.prev_cursor_pos = self.cursor_pos;
501            self.position_delta = self.position_delta + (new_pos - self.prev_position);
502            self.prev_position = new_pos;
503            self.prev_size = self.size();
504        }
505
506        if self.cursor_loopback {
507            let sz = self.size();
508            if sz.w > 0 && sz.h > 0 {
509                let mut wrapped = false;
510                let mut new_pos = self.cursor_pos;
511                if new_pos.x <= 0.0 { new_pos.x = (sz.w - 1) as f64; wrapped = true; }
512                else if new_pos.x >= (sz.w - 1) as f64 { new_pos.x = 0.0; wrapped = true; }
513                if new_pos.y <= 0.0 { new_pos.y = (sz.h - 1) as f64; wrapped = true; }
514                else if new_pos.y >= (sz.h - 1) as f64 { new_pos.y = 0.0; wrapped = true; }
515                if wrapped {
516                    self.set_cursor_pos(new_pos);
517                    self.prev_cursor_pos = self.cursor_pos;
518                }
519            }
520        }
521    }
522
523    // ── Internal ──────────────────────────────────────────────────────────
524
525    #[doc(hidden)]
526    pub fn notify_cursor_moved(&mut self, pos: Coord2D) {
527        self.cursor_pos = pos;
528    }
529
530    #[doc(hidden)]
531    pub fn notify_cursor_inside(&mut self, inside: bool) {
532        self.cursor_inside = inside;
533    }
534}