On a page with forge/web an ltk app had a main surface, overlays and subsurfaces, but none of what a shell such as crustace needs from the compositor: fullscreen did nothing, the app heard nothing of the other windows, an activation token it asked for never came, forge/web was never told when a launched window had shown, the shell could not speak its protocol, and the app had no way to read its own files in a browser. Each now goes through the app's connection to forge/web, in the shape of the Wayland protocol it stands for. Fullscreen. On a page with forge/web, `App::fullscreen` and `App::start_fullscreen` send a `set_fullscreen` request for the main window, as `xdg_toplevel.set_fullscreen` does, instead of doing nothing; alone on a page the canvas still takes the browser's fullscreen. `examples/web_hello.rs` gains a button that toggles it. The desktop's windows. On connecting, the backend asks for the list of toplevels, and forge/web's `toplevel_opened` and `toplevel_closed` events become `ToplevelEvent`s for `App::on_toplevel_event`, as `ext-foreign-toplevel-list-v1` does on Wayland, so a shell's dock knows which applications have windows open. Activation. The tokens an app asks for with `App::take_activation_requests` are issued before it draws: forge/web's, through its `activation_token` request, or empty without a desktop, as on Wayland without xdg-activation, and handed to `App::on_activation_token` with their messages processed. A browser app that launched others through tokens waited for one that never came. Each surface's first presented frame is reported with a `presented` request (`WebSurfaceState::presented`), so forge/web knows when a window it launched has shown and can report it activated, which dismisses crustace's splash. The shell protocol. `platform/web/shell.rs` carries the JSON a shell speaks to forge over its socket over the app's connection to forge/web: `ltk::web::shell_send( json )` sends it as a `shell` request, and each `shell` event's message reaches the listener `ltk::web::set_shell_listener` gives, as JSON. What is sent before the app has connected is kept and sent once it has, so a shell can tell forge/web its theme while it starts. An overlay's role now carries its `App::overlay_namespace`, as its layer-shell namespace does on Wayland, so forge/web can tell a shell's overlays apart. The cursor over every surface. The CSS `cursor` was set on the main surface's element only, so over an overlay the pointer kept the page's arrow. `WebSurfaceState` keeps the element that takes its input, the shape moves to that of the surface the pointer enters, and `WebPlatform::cursor` keeps the last one asked for. An app's own files. `ltk::vfs`, through which ltk reads its themes, fonts and keyboard layouts, is public: `read`, `read_to_string`, `read_dir`, `is_file` and `is_dir` read the file system natively and, in a browser, the files the page loaded into memory. `ltk::web::load_tree( base, root ).await` loads any tree served with a `MANIFEST` under `root`, and `load_resources` is that into `/ltk` with the directory made the resource directory. `ltk::time::Instant` is public too, `std`'s natively and `web_time`'s on wasm32, where `std::time::Instant::now` panics, so an app that builds for both measures time with ltk's clock. wasm-bindgen-cli. `scripts/wasm-bindgen-cli.sh TOOLS_DIR` builds wasm-bindgen-cli into `TOOLS_DIR/bin` at the version of the `wasm-bindgen` crate ltk pins in its `Cargo.toml`, from its sources with a lock resolved for Debian's rustc, which is what `make web-deps` did inline and now runs. Since `make install` copies `scripts/` into the registry, a project that builds for the browser against the installed ltk, such as forge/web or crustace, gets the same CLI without a checkout of ltk. The README and docs/architecture.md say so. Images named by their caller. Every draw through `draw_image_data` fingerprints its pixels to find them in the image caches, which for a terminal means hashing every glyph of every frame. `Canvas::draw_image_keyed( key, rgba, w, h, dest, opacity )` is `draw_image_data` with `key` naming the pixels in place of their fingerprint: every draw with the same key must pass the same pixels, which are read only when the image is not cached. Both backends' caches are keyed by `ImageId`, `Content` for a fingerprint and `Keyed` for a caller's key, so a key never names an image drawn by content even when it equals its fingerprint. `GlesCanvas` and `SoftwareCanvas` gain `draw_image_keyed`, and their `draw_image_data` and it share one private `draw_image`. Tests check that a keyed image draws as its pixels do and that a key equal to a fingerprint is another image. A held button is not also tapped. A button with `on_long_press` and no `on_drag_start` stayed a tap after its hold fired, so releasing it sent its `on_press` as well. `GestureState::long_press_msg_fired` is set when the hold fires `long_press_msg` and suppresses the press message on release; a test covers it. `CHANGELOG.md` describes these changes.
96 lines
4.1 KiB
Rust
96 lines
4.1 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
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<ImageBitmapRenderingContext>,
|
|
/// Takes the software renderer's frames, without WebGL2.
|
|
pub ctx2d: Option<CanvasRenderingContext2d>,
|
|
/// The software renderer's frame, unpremultiplied for `putImageData`.
|
|
pub pixels: Vec<u8>,
|
|
/// 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<Option<Vec<[ f64; 4 ]>>>,
|
|
/// The role last asked of forge/web, for an overlay.
|
|
pub role: Option<Value>,
|
|
/// The surface's input, removed with it.
|
|
pub listeners: Vec<Listener>,
|
|
/// 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<Listener> ) -> 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::<f32>().ok() )
|
|
.unwrap_or( 0.0 );
|
|
Insets { top: side( "padding-top" ), right: side( "padding-right" ), bottom: side( "padding-bottom" ), left: side( "padding-left" ) }
|
|
}
|
|
|
|
impl<A: App> AppData<A>
|
|
{
|
|
/// 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 );
|
|
}
|
|
}
|