Files
ltk/src/platform/web/state.rs
T
admin af4009374d
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
A browser app takes part in the desktop as a Wayland client does: fullscreen, the desktop's windows, activation and the shell protocol through forge/web, its own files through ltk::vfs, and images its caller names
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.
2026-10-21 10:21:11 +02:00

92 lines
3.8 KiB
Rust

// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use std::cell::{ Cell, RefCell };
use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::Arc;
use web_sys::{ Element, HtmlElement, OffscreenCanvas, WebGl2RenderingContext };
use crate::app::{ App, SubsurfaceId };
use crate::event_loop::SurfaceFocus;
use crate::render::Canvas;
use super::desktop::Connection;
use super::run::Listener;
use super::subsurfaces::WebSubsurface;
/// What the runtime's requests go through in a browser: the page's desktop when it has one, the one WebGL context every surface draws through, and what the loop must report back to the runtime.
pub( crate ) struct WebPlatform<A: App>
{
/// forge/web, when the page has it; the app then has no canvas of its own over the page.
pub desktop: Option<Connection>,
/// Where every surface is painted on the GPU before its frame moves to the surface's own canvas, so the app holds one WebGL context however many surfaces it has. `None` without WebGL2: each surface is then drawn in software.
pub offscreen: Option<OffscreenCanvas>,
pub webgl: Option<WebGl2RenderingContext>,
/// `webgl` as the GPU canvas draws through it, wrapped anew when the context is restored.
pub gl: Option<Arc<glow::Context>>,
/// The browser took the WebGL context away; nothing is drawn until it gives it back.
pub context_lost: bool,
/// Where the cursor's shape is set: the element taking the input of the surface the pointer last entered, since CSS gives each element its own.
pub cursor_target: HtmlElement,
/// The cursor's shape as CSS names it, for the next surface the pointer enters.
pub cursor: &'static str,
/// A hidden element padded by `env( safe-area-inset-* )`, which tells what a phone's notch and bars cover; only on ltk's own canvas over the page.
pub inset_probe: Option<Element>,
/// The surfaces a frame was presented or asked for; the loop reports them at the next animation frame.
pub frame_due: RefCell<Vec<SurfaceFocus>>,
/// The id the next overlay's or subsurface's surface goes by in forge/web's protocol.
pub next_surface: Cell<u32>,
/// The subsurfaces of [`App::subsurfaces`], each a surface of the desktop.
pub subsurfaces: HashMap<SubsurfaceId, WebSubsurface>,
/// The GPU canvas every subsurface is painted on before its frame moves to its own canvas, which keeps it.
pub subsurface_canvas: Option<Canvas>,
/// Text input was asked for and not ended; ltk's own keyboard serves it.
pub text_input: bool,
/// The input of every surface, the window's `resize` and the context's loss, removed when the app exits.
pub listeners: Vec<Listener>,
_app: PhantomData<fn() -> A>,
}
impl<A: App> WebPlatform<A>
{
pub( crate ) fn new( desktop: Option<Connection>, offscreen: Option<( OffscreenCanvas, WebGl2RenderingContext )>, cursor_target: HtmlElement, listeners: Vec<Listener>, inset_probe: Option<Element> ) -> Self
{
let ( offscreen, webgl ) = offscreen.unzip();
let gl = webgl.clone().map( |context| Arc::new( glow::Context::from_webgl2_context( context ) ) );
Self
{
desktop,
offscreen,
webgl,
gl,
context_lost: false,
cursor_target,
cursor: "default",
inset_probe,
frame_due: RefCell::new( Vec::new() ),
next_surface: Cell::new( super::desktop::MAIN + 1 ),
subsurfaces: HashMap::new(),
subsurface_canvas: None,
text_input: false,
listeners,
_app: PhantomData,
}
}
/// Report a frame of `focus` at the next animation frame.
pub( super ) fn owe_frame( &self, focus: SurfaceFocus )
{
let mut due = self.frame_due.borrow_mut();
if !due.contains( &focus ) { due.push( focus ); }
}
pub( super ) fn surface_id( &self ) -> u32
{
let id = self.next_surface.get();
self.next_surface.set( id + 1 );
id
}
}