Skip to main content

lumen_core/
window_state.rs

1//! Process-global window-state mirror for the `window` script namespace.
2//!
3//! The OS window lives inside the winit event loop, not in the ECS, so the
4//! `window.title()` / `window.size()` / `window.dpr()` getters and the
5//! `window.set_title` / `window.set_size` setters cannot reach it through a
6//! `&World`. They read and write this small cache instead. The window
7//! backend publishes the live size and device-pixel ratio here on resize
8//! and scale-factor changes; the setters write the requested title / size,
9//! which the backend applies to the real window when one exists. Headless
10//! runs have no window, so a setter followed by its getter round-trips
11//! through the cache; the observable contract the tests assert.
12
13use std::sync::{Mutex, OnceLock};
14
15#[derive(Debug, Clone)]
16struct WindowState {
17    title: String,
18    width: f32,
19    height: f32,
20    dpr: f32,
21}
22
23impl Default for WindowState {
24    fn default() -> Self {
25        Self {
26            title: String::new(),
27            width: 0.0,
28            height: 0.0,
29            dpr: 1.0,
30        }
31    }
32}
33
34fn cell() -> &'static Mutex<WindowState> {
35    static STATE: OnceLock<Mutex<WindowState>> = OnceLock::new();
36    STATE.get_or_init(|| Mutex::new(WindowState::default()))
37}
38
39/// Current window title.
40pub fn title() -> String {
41    cell().lock().map(|s| s.title.clone()).unwrap_or_default()
42}
43
44/// Request a new window title (`window.set_title`).
45pub fn set_title(title: &str) {
46    if let Ok(mut s) = cell().lock() {
47        s.title = title.to_string();
48    }
49}
50
51/// Current window size in logical pixels (`window.size`).
52pub fn size() -> (f32, f32) {
53    cell()
54        .lock()
55        .map(|s| (s.width, s.height))
56        .unwrap_or((0.0, 0.0))
57}
58
59/// Request a new window size in logical pixels (`window.set_size`).
60pub fn set_size(width: f32, height: f32) {
61    if let Ok(mut s) = cell().lock() {
62        s.width = width;
63        s.height = height;
64    }
65}
66
67/// Current device-pixel ratio / scale factor (`window.dpr`).
68pub fn dpr() -> f32 {
69    cell().lock().map(|s| s.dpr).unwrap_or(1.0)
70}
71
72/// Publish the live device-pixel ratio (window backend, on scale change).
73pub fn set_dpr(dpr: f32) {
74    if let Ok(mut s) = cell().lock() {
75        s.dpr = dpr;
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn title_and_size_round_trip() {
85        set_title("Hello");
86        assert_eq!(title(), "Hello");
87        set_size(640.0, 480.0);
88        assert_eq!(size(), (640.0, 480.0));
89        set_dpr(2.0);
90        assert_eq!(dpr(), 2.0);
91    }
92}