// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use serde_json::Value; use web_sys::{ CanvasRenderingContext2d, Element, HtmlCanvasElement, HtmlElement, ImageBitmapRenderingContext }; use crate::app::App; use crate::event_loop::AppData; use crate::event_loop::surface::Insets; use super::run::Listener; /// A surface's browser half: the canvas it is shown on. pub( crate ) struct WebSurfaceState { pub canvas: HtmlCanvasElement, /// Takes the surface's input: the element forge/web gives above the canvas, or the canvas itself without a desktop. pub input: HtmlElement, /// Takes the frames painted on the shared WebGL context. pub bitmap: Option, /// Takes the software renderer's frames, without WebGL2. pub ctx2d: Option, /// The software renderer's frame, unpremultiplied for `putImageData`. pub pixels: Vec, /// The surface's id in forge/web's protocol. pub id: u32, /// The input region last sent to forge/web, in logical `[ x, y, width, height ]`. pub input_region: Option>>, /// The role last asked of forge/web, for an overlay. pub role: Option, /// The surface's input, removed with it. pub listeners: Vec, /// A frame has been shown, which forge/web was told of: its first, after which a launched window counts as activated. pub presented: bool, } impl WebSurfaceState { /// The surface shown on `canvas`, through the shared WebGL context when there is one, else in software. pub( super ) fn new( canvas: HtmlCanvasElement, input: HtmlElement, id: u32, gpu: bool, listeners: Vec ) -> Self { let context = |kind: &str| canvas.get_context( kind ).ok().flatten(); let bitmap = if gpu { context( "bitmaprenderer" ).and_then( |c| wasm_bindgen::JsCast::dyn_into( c ).ok() ) } else { None }; let ctx2d = if gpu { None } else { context( "2d" ).and_then( |c| wasm_bindgen::JsCast::dyn_into( c ).ok() ) }; Self { canvas, input, bitmap, ctx2d, pixels: Vec::new(), id, input_region: None, role: None, listeners, presented: false } } } /// The safe-area insets `probe` is padded by, in CSS pixels; none without a probe. fn safe_area( probe: Option<&Element> ) -> Insets { let ( Some( probe ), Some( window ) ) = ( probe, web_sys::window() ) else { return Insets::default() }; let Ok( Some( style ) ) = window.get_computed_style( probe ) else { return Insets::default() }; let side = |name: &str| style.get_property_value( name ).ok() .and_then( |value| value.trim().trim_end_matches( "px" ).parse::().ok() ) .unwrap_or( 0.0 ); Insets { top: side( "padding-top" ), right: side( "padding-right" ), bottom: side( "padding-bottom" ), left: side( "padding-left" ) } } impl AppData { /// Size the main surface to its canvas, whose CSS pixels are the surface's logical ones at the page's `devicePixelRatio`, so the canvas holds the display's own pixels, and keep the view out of what a phone's notch and bars cover. pub( super ) fn fit_canvas_element( &mut self ) { let insets = safe_area( self.platform.inset_probe.as_ref() ); if insets != self.main.insets { self.main.insets = insets; self.main.request_redraw(); } let canvas = &self.main.platform.canvas; let ( w, h ) = ( canvas.client_width().max( 1 ) as u32, canvas.client_height().max( 1 ) as u32 ); let scale = web_sys::window().map_or( 1.0, |w| w.device_pixel_ratio() ) as f32; self.size_main( w, h, scale ); } /// The size and scale forge/web gave the main surface. pub( super ) fn configure_main( &mut self, width: f64, height: f64, scale: f64 ) { self.size_main( width.round().max( 1.0 ) as u32, height.round().max( 1.0 ) as u32, scale as f32 ); } fn size_main( &mut self, w: u32, h: u32, scale: f32 ) { if self.main.configured && ( w, h ) == ( self.main.width, self.main.height ) && scale == self.main.scale_factor { return; } if scale != self.main.scale_factor { self.main.scale_factor = scale; if let Some( ref mut canvas ) = self.main.canvas { canvas.set_dpi_scale( scale ); } } self.main.set_size( w, h ); self.on_main_resized( w, h ); } }