devela/lang/ffi/js/web/
window.rs

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