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
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s

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.
This commit is contained in:
2026-10-21 10:21:11 +02:00
parent cd1a233b2e
commit af4009374d
27 changed files with 369 additions and 69 deletions
+49
View File
@@ -962,6 +962,16 @@ impl Canvas
}
}
/// [`Self::draw_image_data`] with `key` naming the pixels instead of a fingerprint of them, so a caller that draws the same images on every frame, such as a terminal's glyphs, does not hash them each time. Every draw with the same `key` must pass the same pixels: they are read only when the image is not cached. A key never names an image drawn through `draw_image_data`.
pub fn draw_image_keyed( &mut self, key: u64, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
{
match self
{
Canvas::Software( c ) => c.draw_image_keyed( key, rgba_data, img_w, img_h, dest, opacity ),
Canvas::Gles( c ) => c.draw_image_keyed( key, rgba_data, img_w, img_h, dest, opacity ),
}
}
/// Zero pixels inside each rect — used by the partial-redraw
/// path when the surface background is fully transparent.
pub fn clear_rects_transparent( &mut self, rects: &[Rect] )
@@ -1305,3 +1315,42 @@ mod pixel_snap_tests
assert_ne!( at_int, at_high, "x+0.6 rounds up to the next column" );
}
}
#[ cfg( test ) ]
mod keyed_image_tests
{
use super::helpers::fingerprint_rgba;
use super::Canvas;
use crate::types::Rect;
const AT: Rect = Rect { x: 2.0, y: 2.0, width: 4.0, height: 4.0 };
fn pixels( canvas: &Canvas ) -> Vec<u8>
{
let Canvas::Software( c ) = canvas else { panic!( "software canvas" ) };
c.pixmap.data().to_vec()
}
#[ test ]
fn a_keyed_image_draws_as_its_pixels_do()
{
let red = [ 255u8, 0, 0, 255 ].repeat( 16 );
let mut by_content = Canvas::new( 16, 16 );
by_content.draw_image_data( &red, 4, 4, AT, 1.0 );
let mut by_key = Canvas::new( 16, 16 );
by_key.draw_image_keyed( 7, &red, 4, 4, AT, 1.0 );
assert_eq!( pixels( &by_content ), pixels( &by_key ) );
}
#[ test ]
fn a_key_equal_to_a_fingerprint_is_another_image()
{
let red = [ 255u8, 0, 0, 255 ].repeat( 16 );
let blue = [ 0u8, 0, 255, 255 ].repeat( 16 );
let mut canvas = Canvas::new( 16, 16 );
canvas.draw_image_data( &red, 4, 4, AT, 1.0 );
canvas.draw_image_keyed( fingerprint_rgba( &red ), &blue, 4, 4, AT, 1.0 );
let Canvas::Software( c ) = &canvas else { panic!( "software canvas" ) };
assert!( c.pixmap.pixel( 3, 3 ).unwrap().blue() > 200 );
}
}