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.
This commit is contained in:
@@ -8,9 +8,11 @@ use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::{ JsCast, JsValue };
|
||||
use web_sys::{ HtmlCanvasElement, HtmlElement };
|
||||
|
||||
use crate::app::{ App, Layer, ShellMode };
|
||||
use crate::app::{ App, Layer, ShellMode, ToplevelEvent };
|
||||
use crate::event_loop::AppData;
|
||||
|
||||
use super::run::{ push, PageEvent };
|
||||
use super::shell;
|
||||
|
||||
/// The id the main surface goes by.
|
||||
pub( super ) const MAIN: u32 = 0;
|
||||
@@ -32,7 +34,13 @@ impl Connection
|
||||
let on_event = Closure::<dyn FnMut( String )>::new( on_event );
|
||||
let connection = connect.call2( &api, &JsValue::from_str( app_id ), on_event.as_ref() ).ok()?;
|
||||
let method = |name: &str| js_sys::Reflect::get( &connection, &name.into() ).ok().and_then( |f| f.dyn_into::<js_sys::Function>().ok() );
|
||||
Some( Self { request: method( "request" )?, disconnect: method( "disconnect" )?, _on_event: on_event } )
|
||||
let this = Self { request: method( "request" )?, disconnect: method( "disconnect" )?, _on_event: on_event };
|
||||
shell::connected( Some( this.request.clone() ) );
|
||||
if let Err( e ) = this.request( &json!( { "type": "list_toplevels" } ) )
|
||||
{
|
||||
web_sys::console::warn_1( &format!( "[ltk] list_toplevels: {e}" ).into() );
|
||||
}
|
||||
Some( this )
|
||||
}
|
||||
|
||||
pub( super ) fn request( &self, message: &Value ) -> Result<JsValue, String>
|
||||
@@ -51,8 +59,18 @@ impl Connection
|
||||
Ok( ( canvas, input ) )
|
||||
}
|
||||
|
||||
/// Ask for `surface`, a window, to fill the desktop or to go back.
|
||||
pub( super ) fn set_fullscreen( &self, surface: u32, on: bool )
|
||||
{
|
||||
if let Err( e ) = self.request( &json!( { "type": "set_fullscreen", "surface": surface, "on": on } ) )
|
||||
{
|
||||
web_sys::console::warn_1( &format!( "[ltk] set_fullscreen: {e}" ).into() );
|
||||
}
|
||||
}
|
||||
|
||||
pub( super ) fn close( &self )
|
||||
{
|
||||
shell::connected( None );
|
||||
let _ = self.disconnect.call0( &JsValue::NULL );
|
||||
}
|
||||
}
|
||||
@@ -71,10 +89,35 @@ fn on_event( json: String )
|
||||
scale: event[ "scale" ].as_f64().unwrap_or( 1.0 ),
|
||||
} ),
|
||||
Some( "close" ) => push( PageEvent::Close { surface } ),
|
||||
Some( "shell" ) => shell::deliver( &event[ "message" ] ),
|
||||
Some( "toplevel_opened" ) => push( PageEvent::Toplevel( ToplevelEvent::Opened
|
||||
{
|
||||
id: event[ "id" ].as_u64().unwrap_or( 0 ) as u32,
|
||||
app_id: event[ "app_id" ].as_str().unwrap_or_default().to_string(),
|
||||
identifier: event[ "identifier" ].as_str().unwrap_or_default().to_string(),
|
||||
title: event[ "title" ].as_str().unwrap_or_default().to_string(),
|
||||
} ) ),
|
||||
Some( "toplevel_closed" ) => push( PageEvent::Toplevel( ToplevelEvent::Closed { id: event[ "id" ].as_u64().unwrap_or( 0 ) as u32 } ) ),
|
||||
_ => web_sys::console::warn_1( &format!( "[ltk] forge/web: {json}" ).into() ),
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue the activation tokens the app asked for and hand them to it before it draws, as xdg-activation does on Wayland: forge/web's, or empty without a desktop, so the app still goes ahead.
|
||||
pub( super ) fn issue_activation_tokens<A: App>( data: &mut AppData<A> )
|
||||
{
|
||||
let tags = data.app.take_activation_requests();
|
||||
if tags.is_empty() { return; }
|
||||
for tag in tags
|
||||
{
|
||||
let token = data.platform.desktop.as_ref()
|
||||
.and_then( |d| d.request( &json!( { "type": "activation_token" } ) ).ok() )
|
||||
.and_then( |token| token.as_string() )
|
||||
.unwrap_or_default();
|
||||
if let Some( msg ) = data.app.on_activation_token( tag, token ) { data.pending_msgs.push( msg ); }
|
||||
}
|
||||
data.process_messages();
|
||||
}
|
||||
|
||||
pub( super ) fn layer_name( layer: Layer ) -> &'static str
|
||||
{
|
||||
match layer
|
||||
|
||||
Reference in New Issue
Block a user