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}