Skip to main content

devela/sys/os/browser/web/
window.rs

1// devela/src/sys/os/browser/web/window.rs
2//
3//! Defines [`WebWindow`], [`WebWindowState`].
4//!
5//
6
7#[allow(unused_imports, reason = "used by _js_method_str_alloc!")]
8#[cfg(feature = "alloc")]
9use devela::String;
10use devela::{_js_doc, Distance, Extent, Float, offset_of};
11#[allow(unused_imports, reason = "not(windows)")]
12use devela::{
13    _js_extern, _js_method_str_alloc, Js, JsTimeout, WebDocument, js_bool, js_int32, js_number,
14    js_uint32,
15};
16
17#[doc = crate::_tags!(ui web)]
18/// Handle to the browser's global [Window] and [Screen] associated APIs.
19#[doc = crate::_doc_meta!{location("sys/os/browser/web")}]
20///
21/// [Window]: https://developer.mozilla.org/en-US/docs/Web/API/Window
22/// [Screen]: https://developer.mozilla.org/en-US/docs/Web/API/Window/screen
23#[repr(C)]
24#[derive(Copy, Clone, Debug)]
25pub struct WebWindow;
26
27#[rustfmt::skip]
28#[cfg(not(feature = "safe_lang"))]
29#[cfg(all(feature = "unsafe_ffi", not(windows)))]
30#[cfg_attr(nightly_doc, doc(cfg(feature = "unsafe_ffi")))]
31#[cfg_attr(nightly_doc, doc(cfg(target_arch = "wasm32")))]
32impl WebWindow {
33    #[doc = _js_doc!("Window", "document")]
34    /// Returns the `document` object.
35    pub fn document(&self) -> WebDocument { WebDocument }
36
37    /// Returns a new up-to-date `WebWindowState`.
38    pub fn state() -> WebWindowState { WebWindowState::new() }
39
40    #[doc = _js_doc!("Window", "closed")]
41    /// Whether the current window is closed or not.
42    pub fn is_closed() -> js_bool { window_is_closed() }
43
44    #[doc = _js_doc!("Window", "crossOriginIsolated")]
45    /// Whether the website is in a cross-origin isolation state.
46    pub fn is_coi() -> js_bool { window_is_coi() }
47
48    #[doc = _js_doc!("Window", "isSecureContext")]
49    /// Whether the current [context is secure][0].
50    ///
51    /// [0]: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
52    pub fn is_secure() -> js_bool { window_is_secure() }
53
54    #[doc = _js_doc!("Window", "locationbar")]
55    /// Whether the window is a popup or not.
56    pub fn is_popup() -> js_bool { window_is_popup() }
57
58    /* texts */
59
60    _js_method_str_alloc! {
61        #[doc = _js_doc!("Window", "name")]
62        /// Gets the window name.
63        name, window_name
64    }
65
66    #[doc = _js_doc!("Window", "name")]
67    /// Sets the current window `name`.
68    pub fn set_name(name: &str) { unsafe { window_set_name(name.as_ptr(), name.len() as u32); } }
69
70    /* timeout */
71
72    #[doc = _js_doc!("Window", "setTimeout")]
73    /// Calls a function after a delay in milliseconds.
74    pub fn set_timeout(callback: extern "C" fn(), delay_ms: js_uint32) -> JsTimeout {
75        JsTimeout { id: unsafe { window_set_timeout(callback as usize, delay_ms) } } }
76
77    #[doc = _js_doc!("Window", "setInterval")]
78    /// Calls a function repeatedly at a fixed interval in milliseconds.
79    pub fn set_interval(callback: extern "C" fn(), interval_ms: js_uint32) -> JsTimeout {
80        JsTimeout { id: unsafe { window_set_interval(callback as usize, interval_ms) } } }
81
82    #[doc = _js_doc!("Window", "clearTimeout")]
83    #[doc = _js_doc!("Window", "clearInterval")]
84    /// Cancels a timeout or interval.
85    pub fn clear_timeout(id: JsTimeout) { window_clear_timeout(id.id()); }
86
87    /* eval */
88
89    /// Executes JavaScript code immediately.
90    /// ## Security Warning
91    /// - Avoid passing untrusted input, as this executes arbitrary JS.
92    /// - Ensure all evaluated code is **safe and controlled**.
93    pub fn eval(js_code: &str) { unsafe { window_eval(js_code.as_ptr(), js_code.len()); } }
94
95    #[doc = _js_doc!("Window", "setTimeout")]
96    /// Executes JavaScript code after a delay in milliseconds.
97    pub fn eval_timeout(js_code: &str, delay_ms: js_uint32) -> JsTimeout { JsTimeout {
98        id: unsafe { window_eval_timeout(js_code.as_ptr(), js_code.len(), delay_ms) } } }
99
100    #[doc = _js_doc!("Window", "setInterval")]
101    /// Executes JavaScript code repeatedly at a fixed interval in milliseconds.
102    pub fn eval_interval(js_code: &str, interval_ms: js_uint32) -> JsTimeout { JsTimeout {
103        id: unsafe { window_eval_interval(js_code.as_ptr(), js_code.len(), interval_ms) } } }
104
105    /* animation */
106    // TODO
107
108    #[doc = _js_doc!("Window", "requestAnimationFrame")]
109    /// Requests an animation frame, executing the given `callback`.
110    pub fn request_animation_frame(callback: extern "C" fn()) -> js_uint32 {
111        unsafe { window_request_animation_frame(callback as usize) } }
112    /// Cancels a request for an animation frame.
113    pub fn cancel_animation_frame(id: js_uint32) { window_cancel_animation_frame(id); }
114}
115// (in sync with ./js/window.js)
116_js_extern! {
117    [module: "api_window"]
118    unsafe fn window_state(data: *mut u8);
119    safe fn window_is_closed() -> js_bool;
120    safe fn window_is_coi() -> js_bool;
121    safe fn window_is_secure() -> js_bool;
122    safe fn window_is_popup() -> js_bool;
123    // texts
124    unsafe fn window_name(buf_ptr: *mut u8, max_len: js_uint32) -> js_int32;
125    unsafe fn window_set_name(str_ptr: *const u8, str_len: js_uint32);
126    // timeout
127    unsafe fn window_set_timeout(callback_ptr: usize, delay_ms: js_uint32) -> js_uint32;
128    unsafe fn window_set_interval(callback_ptr: usize, interval_ms: js_uint32) -> js_uint32;
129    safe fn window_clear_timeout(timeout_id: js_uint32);
130    // eval
131    unsafe fn window_eval(js_code_ptr: *const u8, js_code_len: usize);
132    unsafe fn window_eval_timeout(js_code_ptr: *const u8, js_code_len: usize, delay_ms: js_uint32)
133        -> js_uint32;
134    unsafe fn window_eval_interval(js_code_ptr: *const u8, js_code_len: usize,
135        interval_ms: js_uint32) -> js_uint32;
136    // animation
137    unsafe fn window_request_animation_frame(callback_ptr: usize) -> js_uint32;
138    safe fn window_cancel_animation_frame(requestId: js_uint32);
139}
140
141#[doc = crate::_tags!(ui web)]
142/// Aggregates the live state of a [`WebWindow`], including geometry and screen context.
143#[doc = crate::_doc_meta!{location("sys/os/browser/web")}]
144///
145/// It has a size of 52 Bytes.
146///
147/// ### Performance
148/// All fields are fetched in a single JS→Rust call.
149#[repr(C)]
150#[derive(Clone, Copy, Default, PartialEq)] // manual: Debug
151pub struct WebWindowState {
152    /* window */
153    #[doc = _js_doc!("Window", "innerWidth")]
154    #[doc = _js_doc!("Window", "innerHeight")]
155    /// The extent in pixels of the content of the browser window including any rendered scrollbars.
156    pub inner_size: Extent<u32, 2>,
157
158    #[doc = _js_doc!("Window", "outerWidth")]
159    #[doc = _js_doc!("Window", "outerHeight")]
160    /// The extent in pixels of the outside of the browser window.
161    pub outer_size: Extent<u32, 2>,
162
163    /* screen */
164    #[doc = _js_doc!("Window", "screenLeft")]
165    #[doc = _js_doc!("Window", "screenTop")]
166    /// The window's offset in pixels from the screen's top-left origin.
167    pub screen_offset: Distance<i32, 2>,
168
169    #[doc = _js_doc!("Screen", "width")]
170    #[doc = _js_doc!("Screen", "height")]
171    /// The extent of the screen in pixels.
172    pub screen_size: Extent<u32, 2>,
173
174    #[doc = _js_doc!("Screen", "availWidth")]
175    #[doc = _js_doc!("Screen", "availHeight")]
176    /// The extent of the screen in pixels, minus user interface features displayed.
177    pub screen_usable_size: Extent<u32, 2>,
178
179    /* misc. */
180    #[doc = _js_doc!("Window", "devicePixelRatio")]
181    /// The device pixel ratio of the resolution in physical pixels to the resolution in CSS pixels.
182    ///
183    /// The value changes with the zoom on desktops yet remains static on mobile devices.
184    pub dpr: f32,
185
186    #[doc = _js_doc!("Screen", "colorDepth")]
187    /// The screen color depth, in bits per single pixel. It could be 8, 16, 24, 32 or 64.
188    pub bpp: u8,
189
190    // TODO: add bitpacked flags (is_popup, is_secure, etc.)
191    //
192    /// Explicit padding to align.
193    _pad: [u8; 3],
194}
195impl WebWindowState {
196    const __ASSERT_FIELD_OFFSETS: () = const {
197        assert!(offset_of!(Self, inner_size) == 0);
198        assert!(offset_of!(Self, outer_size) == 8);
199        assert!(offset_of!(Self, screen_offset) == 16);
200        assert!(offset_of!(Self, screen_size) == 24);
201        assert!(offset_of!(Self, screen_usable_size) == 32);
202        assert!(offset_of!(Self, dpr) == 40);
203        assert!(offset_of!(Self, bpp) == 44);
204    };
205
206    /// Returns a new up-to-date `WebWindowState`.
207    ///
208    /// # Safety
209    /// - JavaScript must write all non-padding fields at correct offsets.
210    #[cfg(not(feature = "safe_lang"))]
211    #[cfg(feature = "unsafe_ffi")]
212    #[cfg_attr(nightly_doc, doc(cfg(feature = "unsafe_ffi")))]
213    pub fn new() -> WebWindowState {
214        let mut state = WebWindowState::default();
215        unsafe {
216            window_state(&mut state as *mut WebWindowState as *mut u8);
217        }
218        state
219    }
220
221    /// Overwrites this `WebWindowState` with the latest live metrics.
222    #[cfg(not(feature = "safe_lang"))]
223    #[cfg(feature = "unsafe_ffi")]
224    #[cfg_attr(nightly_doc, doc(cfg(feature = "unsafe_ffi")))]
225    pub fn update(&mut self) {
226        unsafe { window_state(self as *mut Self as *mut u8) };
227    }
228
229    /// Validates the internal consistency of window metrics.
230    ///
231    /// Returns `true` if all these conditions hold:
232    /// - No dimensions are zero (invalid window state)
233    /// - Inner size ≤ outer size (logical constraint)
234    /// - Outer size ≤ screen size (unless multi-monitor)
235    /// - Device pixel ratio is sane (0.2 <= dpr <= 10.0)
236    /// - Screen color depth is plausible (8 <= depth <= 64)
237    // - Popup flags don't contradict window dimensions
238    pub const fn is_valid(&self) -> bool {
239        // 1. Non-zero dimensions
240        let non_zero = self.inner_size.x() > 0
241            && self.inner_size.y() > 0
242            && self.outer_size.x() > 0
243            && self.outer_size.y() > 0;
244
245        // 2. Inner <= Outer
246        let inner_le_outer = self.inner_size.dim[0] <= self.outer_size.dim[0]
247            && self.inner_size.dim[1] <= self.outer_size.dim[1];
248
249        // 3. Outer <= Screen (with 10px tolerance for window chrome)
250        let outer_le_screen = (self.outer_size.dim[0] <= self.screen_size.dim[0] + 10)
251            && (self.outer_size.dim[1] <= self.screen_size.dim[1] + 10);
252
253        // 4. Sane DPR range
254        let sane_dpr = self.dpr >= 0.2 && self.dpr <= 10.0;
255
256        // 5. Plausible color depth
257        let sane_bpp = self.bpp >= 8 && self.bpp <= 64;
258
259        // // 6. Popup consistency
260        // let valid_popup = !self.is_popup() || (
261        //     // Popups shouldn't fill the screen
262        //     self.outer_size.dim[0] < self.screen_usable_size.dim[0] - 10 &&
263        //     self.outer_size.dim[1] < self.screen_usable_size.dim[1] - 10
264        // );
265
266        non_zero && inner_le_outer && outer_le_screen && sane_dpr && sane_bpp // && valid_popup
267    }
268
269    /* derived metrics */
270
271    /// Returns the thickness of the window chrome (frame, scrollbars, etc.) in logical pixels.
272    ///
273    /// This is the difference between the outer and inner window sizes.
274    pub const fn chrome_size(&self) -> Extent<u32, 2> {
275        Extent::new([
276            self.outer_size.x() - self.inner_size.x(),
277            self.outer_size.y() - self.inner_size.y(),
278        ])
279    }
280    /// Checks if the window is approximately maximized (fills the available screen space).
281    ///
282    /// Tolerance: The window must be within 1 pixel of the screen's usable size.
283    pub const fn is_maximized(&self) -> bool {
284        self.outer_size.x() >= self.screen_usable_size.x()
285            && self.outer_size.y() >= self.screen_usable_size.y()
286    }
287    /// Checks if the window is in a portrait orientation (height > width).
288    pub const fn is_portrait(&self) -> bool {
289        self.inner_size.y() > self.inner_size.x()
290    }
291
292    /// Returns the physical size of the window in hardware pixels, truncating fractional values.
293    ///
294    /// Computed as `(inner_size * device_pixel_ratio)`.
295    ///
296    /// For rounded values, use [`physical_size_rounded()`][Self::physical_size_rounded].
297    pub const fn physical_size(&self) -> Extent<u32, 2> {
298        Extent::new([
299            (self.inner_size.x() as f32 * self.dpr) as u32,
300            (self.inner_size.y() as f32 * self.dpr) as u32,
301        ])
302    }
303    /// Returns the physical size of the window in hardware pixels, rounded to the nearest integer.
304    ///
305    /// Computed as `(inner_size * device_pixel_ratio).round()`.
306    ///
307    /// It's more accurate and expensive than [`physical_size()`][Self::physical_size].
308    pub const fn physical_size_rounded(&self) -> Extent<u32, 2> {
309        Extent::new([
310            Float(self.inner_size.x() as f32 * self.dpr).const_round().0 as u32,
311            Float(self.inner_size.y() as f32 * self.dpr).const_round().0 as u32,
312        ])
313    }
314
315    /// Returns the window's distance to each screen edge in logical pixels.
316    ///
317    /// Order: `[left, top, right, bottom]`. Negative values mean the window is outside the screen.
318    pub const fn screen_margins(&self) -> [i32; 4] {
319        [
320            self.screen_offset.dim[0],
321            self.screen_offset.dim[1],
322            (self.screen_size.x() as i32)
323                - (self.screen_offset.dim[0] + self.outer_size.x() as i32),
324            (self.screen_size.y() as i32)
325                - (self.screen_offset.dim[1] + self.outer_size.y() as i32),
326        ]
327    }
328}
329
330impl crate::Debug for WebWindowState {
331    fn fmt(&self, f: &mut crate::Formatter<'_>) -> crate::FmtResult<()> {
332        let mut state = f.debug_struct("WebWindowState");
333        /* fields */
334        state
335            .field("inner_size", &self.inner_size)
336            .field("outer_size", &self.outer_size)
337            .field("screen_offset", &self.screen_offset)
338            .field("screen_size", &self.screen_size)
339            .field("screen_usable_size", &self.screen_usable_size)
340            .field("dpr", &self.dpr)
341            .field("bpp", &self.bpp)
342            /* derived */
343            .field("chrome_size()", &self.chrome_size())
344            .field("is_maximized()", &self.is_maximized())
345            .field("is_portrait()", &self.is_portrait())
346            .field("is_valid()", &self.is_valid())
347            .field("physical_size()", &self.physical_size());
348        state.field("physical_size_rounded()", &self.physical_size_rounded());
349        state.field("screen_margins()", &self.screen_margins()).finish_non_exhaustive() // .finish()
350    }
351}