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.
7//! [`publish_viewport`] copies the live size and device-pixel ratio here
8//! from the main world's [`Viewport`], which every window backend and the
9//! headless runner keep current; the setters write the requested title /
10//! size, which the backend applies to the real window when one exists.
11//! Headless runs have no window, so a setter followed by its getter
12//! round-trips through the cache.
13
14use std::sync::{Mutex, OnceLock};
15
16use bevy_ecs::prelude::*;
17
18use crate::render_world::Viewport;
19
20#[derive(Debug, Clone)]
21struct WindowState {
22    title: String,
23    width: f32,
24    height: f32,
25    dpr: f32,
26}
27
28impl Default for WindowState {
29    fn default() -> Self {
30        Self {
31            title: String::new(),
32            width: 0.0,
33            height: 0.0,
34            dpr: 1.0,
35        }
36    }
37}
38
39fn cell() -> &'static Mutex<WindowState> {
40    static STATE: OnceLock<Mutex<WindowState>> = OnceLock::new();
41    STATE.get_or_init(|| Mutex::new(WindowState::default()))
42}
43
44/// Current window title.
45pub fn title() -> String {
46    cell().lock().map(|s| s.title.clone()).unwrap_or_default()
47}
48
49/// Request a new window title (`window.set_title`).
50pub fn set_title(title: &str) {
51    if let Ok(mut s) = cell().lock() {
52        s.title = title.to_string();
53    }
54}
55
56/// Current window size in logical pixels (`window.size`).
57pub fn size() -> (f32, f32) {
58    cell()
59        .lock()
60        .map(|s| (s.width, s.height))
61        .unwrap_or((0.0, 0.0))
62}
63
64/// Request a new window size in logical pixels (`window.set_size`).
65pub fn set_size(width: f32, height: f32) {
66    if let Ok(mut s) = cell().lock() {
67        s.width = width;
68        s.height = height;
69    }
70}
71
72/// Current device-pixel ratio / scale factor (`window.dpr`).
73pub fn dpr() -> f32 {
74    cell().lock().map(|s| s.dpr).unwrap_or(1.0)
75}
76
77/// Publish the live device-pixel ratio (window backend, on scale change).
78pub fn set_dpr(dpr: f32) {
79    if let Ok(mut s) = cell().lock() {
80        s.dpr = dpr;
81    }
82}
83
84/// Copy the main world's [`Viewport`] into the cache whenever it changes,
85/// so `window.size()` and `window.dpr()` read what the app is running at.
86/// Registered by `App::new` at the start of each tick, ahead of every
87/// script handler.
88pub fn publish_viewport(viewport: Option<Res<Viewport>>) {
89    let Some(viewport) = viewport else {
90        return;
91    };
92    if !viewport.is_changed() {
93        return;
94    }
95    if let Ok(mut s) = cell().lock() {
96        s.width = viewport.size.x;
97        s.height = viewport.size.y;
98        s.dpr = viewport.scale_factor;
99    }
100}