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:
@@ -18,6 +18,14 @@ const ROOT: &str = "/ltk";
|
||||
///
|
||||
/// The files are fetched at once, each with the manifest's hash in its URL, so a changed tree is never served from the browser's cache; the manifest itself always comes from the server.
|
||||
pub async fn load_resources( base: &str ) -> Result<(), String>
|
||||
{
|
||||
load_tree( base, ROOT ).await?;
|
||||
crate::resources::set_resource_dir( ROOT );
|
||||
Ok( () )
|
||||
}
|
||||
|
||||
/// Fetch the tree served at `base`, listed by its `MANIFEST` as ltk's resources are, into memory under `root`, an absolute path such as `"/apps"`, where [`crate::vfs`] reads it. An app loads its own files this way, as crustace does its catalog of applications.
|
||||
pub async fn load_tree( base: &str, root: &str ) -> Result<(), String>
|
||||
{
|
||||
let window = web_sys::window().ok_or( "no window" )?;
|
||||
let base = base.trim_end_matches( '/' );
|
||||
@@ -38,9 +46,8 @@ pub async fn load_resources( base: &str ) -> Result<(), String>
|
||||
for ( path, request ) in pending
|
||||
{
|
||||
let bytes = body( request ).await.map_err( |e| format!( "{path}: {e}" ) )?;
|
||||
crate::vfs::insert( Path::new( ROOT ).join( path ), bytes );
|
||||
crate::vfs::insert( Path::new( root ).join( path ), bytes );
|
||||
}
|
||||
crate::resources::set_resource_dir( ROOT );
|
||||
Ok( () )
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -73,7 +73,11 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
match input
|
||||
{
|
||||
WebInput::Pointer( pointer ) => self.pointer_input( &pointer ),
|
||||
WebInput::Pointer( pointer ) =>
|
||||
{
|
||||
if pointer.kind == PointerKind::Enter { self.point_cursor_at( pointer.focus ); }
|
||||
self.pointer_input( &pointer );
|
||||
}
|
||||
WebInput::Touch( touch ) => self.touch_input( &touch ),
|
||||
WebInput::TouchCancel => { self.reset_touch_state(); }
|
||||
WebInput::Key { key, action, ctrl, shift, alt } =>
|
||||
@@ -101,6 +105,19 @@ impl<A: App> AppData<A>
|
||||
WebInput::Clipboard( text ) => self.clipboard = text,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the cursor's shape on the surface `focus` names, which the pointer entered, as the cursor follows the pointer on Wayland.
|
||||
fn point_cursor_at( &mut self, focus: SurfaceFocus )
|
||||
{
|
||||
let input = match focus
|
||||
{
|
||||
SurfaceFocus::Main => Some( &self.main.platform.input ),
|
||||
SurfaceFocus::Overlay( id ) => self.overlays.get( &id ).map( |ss| &ss.platform.input ),
|
||||
};
|
||||
let Some( input ) = input.cloned() else { return };
|
||||
let _ = input.style().set_property( "cursor", self.platform.cursor );
|
||||
self.platform.cursor_target = input;
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen on `target`, which takes the input of the surface `focus` names, for every input the runtime takes. A press focuses `target` when `take_focus`; in a desktop the desktop gives the keyboard.
|
||||
|
||||
+12
-4
@@ -7,6 +7,7 @@
|
||||
//! | --- | --- |
|
||||
//! | `assets.rs` | The themes, fonts and keyboard layouts a page serves, fetched into memory as the app's resource directory |
|
||||
//! | `desktop.rs` | The connection to forge/web and the main surface's role |
|
||||
//! | `shell.rs` | The shell protocol over that connection, for a shell such as crustace: [`shell_send`] and [`set_shell_listener`] |
|
||||
//! | `overlays.rs` | The app's overlays as surfaces of the desktop, kept in step with `App::overlays()` |
|
||||
//! | `subsurfaces.rs` | The app's subsurfaces as surfaces of the desktop, painted when their content changes and otherwise only moved |
|
||||
//! | `log.rs` | Panics into the browser's console |
|
||||
@@ -30,12 +31,14 @@ mod log;
|
||||
mod overlays;
|
||||
mod present;
|
||||
pub( crate ) mod run;
|
||||
mod shell;
|
||||
mod state;
|
||||
mod subsurfaces;
|
||||
mod surface;
|
||||
|
||||
pub use assets::load_resources;
|
||||
pub use assets::{ load_resources, load_tree };
|
||||
pub use run::set_canvas;
|
||||
pub use shell::{ set_shell_listener, shell_send };
|
||||
pub( crate ) use state::WebPlatform;
|
||||
pub( crate ) use surface::WebSurfaceState;
|
||||
|
||||
@@ -69,10 +72,14 @@ impl<A: App> Platform for WebPlatform<A>
|
||||
|
||||
fn minimize_window( &self, _main: &WebSurfaceState ) {}
|
||||
|
||||
/// In a desktop a window's fullscreen is forge/web's to give, which it does not yet.
|
||||
/// In a desktop a window's fullscreen is forge/web's to give; alone, the page's canvas takes the browser's.
|
||||
fn set_fullscreen( &self, main: &WebSurfaceState, fullscreen: bool ) -> bool
|
||||
{
|
||||
if self.desktop.is_some() { return true; }
|
||||
if let Some( desktop ) = self.desktop.as_ref()
|
||||
{
|
||||
desktop.set_fullscreen( main.id, fullscreen );
|
||||
return true;
|
||||
}
|
||||
lock_keyboard( fullscreen );
|
||||
if fullscreen
|
||||
{
|
||||
@@ -113,7 +120,8 @@ impl<A: App> Platform for WebPlatform<A>
|
||||
|
||||
fn set_cursor_shape( &mut self, shape: CursorShape ) -> bool
|
||||
{
|
||||
self.cursor_target.style().set_property( "cursor", cursor::css( shape ) ).is_ok()
|
||||
self.cursor = cursor::css( shape );
|
||||
self.cursor_target.style().set_property( "cursor", self.cursor ).is_ok()
|
||||
}
|
||||
|
||||
fn text_input_active( &self ) -> bool
|
||||
|
||||
@@ -32,6 +32,7 @@ fn overlay_role<A: App>( app: &A, spec: &OverlaySpec<A::Message>, ( width, heigh
|
||||
"height": logical( height ),
|
||||
"exclusive_zone": zone,
|
||||
"keyboard": if spec.keyboard_exclusive { "exclusive" } else { "on_demand" },
|
||||
"namespace": app.overlay_namespace( spec.id ),
|
||||
} )
|
||||
}
|
||||
|
||||
@@ -86,7 +87,7 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
let mut platform = WebSurfaceState::new( canvas, id, gpu, listeners );
|
||||
let mut platform = WebSurfaceState::new( canvas, input, id, gpu, listeners );
|
||||
platform.role = Some( role );
|
||||
let mut ss = SurfaceState::new( platform, 0.0, String::new() );
|
||||
// The main surface's scale until forge/web configures this one, so the first frame lays out at the right size.
|
||||
|
||||
@@ -72,7 +72,16 @@ pub( super ) fn present<A: App>
|
||||
( _, _, Some( _ ) ) => present_software( surface, canvas ),
|
||||
_ => false,
|
||||
};
|
||||
if let Some( desktop ) = platform.desktop.as_ref() { sync_input_region( desktop, surface, frame ); }
|
||||
if let Some( desktop ) = platform.desktop.as_ref()
|
||||
{
|
||||
sync_input_region( desktop, surface, frame );
|
||||
if shown && !surface.presented
|
||||
{
|
||||
surface.presented = true;
|
||||
let request = json!( { "type": "presented", "surface": surface.id } );
|
||||
if let Err( e ) = desktop.request( &request ) { web_sys::console::warn_1( &format!( "[ltk] presented: {e}" ).into() ); }
|
||||
}
|
||||
}
|
||||
if let ( true, Some( focus ) ) = ( shown, frame.frame ) { platform.owe_frame( focus ); }
|
||||
shown
|
||||
}
|
||||
|
||||
+10
-2
@@ -15,7 +15,7 @@ use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::{ JsCast, JsValue };
|
||||
use web_sys::{ Document, Event, EventTarget, HtmlCanvasElement, HtmlElement, OffscreenCanvas, WebGl2RenderingContext };
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::{ App, ToplevelEvent };
|
||||
use crate::event_loop::{ AppData, RunError, Sources, SurfaceFocus, SurfaceState };
|
||||
|
||||
use super::desktop::{ main_role, Connection, MAIN };
|
||||
@@ -42,6 +42,8 @@ pub( super ) enum PageEvent
|
||||
{
|
||||
surface: u32,
|
||||
},
|
||||
/// A window of the desktop's, as `ext-foreign-toplevel-list` reports it.
|
||||
Toplevel( ToplevelEvent ),
|
||||
}
|
||||
|
||||
/// An event listener, registered for as long as it lives.
|
||||
@@ -129,6 +131,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
( Some( desktop ), _ ) =>
|
||||
{
|
||||
let ( canvas, input ) = desktop.create_surface( MAIN, main_role( &app ) ).map_err( RunError::Desktop )?;
|
||||
if app.start_fullscreen() { desktop.set_fullscreen( MAIN, true ); }
|
||||
( canvas, input, false )
|
||||
}
|
||||
( None, Some( canvas ) ) => ( canvas.clone(), HtmlElement::from( canvas ), false ),
|
||||
@@ -163,7 +166,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
if own { let _ = target.focus(); }
|
||||
|
||||
let coarse = window.match_media( "(pointer: coarse)" ).ok().flatten().is_some_and( |m| m.matches() );
|
||||
let main = SurfaceState::new( WebSurfaceState::new( canvas, MAIN, offscreen.is_some(), input ), 0.0, String::new() );
|
||||
let main = SurfaceState::new( WebSurfaceState::new( canvas, target.clone(), MAIN, offscreen.is_some(), input ), 0.0, String::new() );
|
||||
let platform = WebPlatform::new( desktop, offscreen, target, listeners, inset_probe );
|
||||
// ltk's own keyboard where the page is used by touch, since the page cannot raise the system's for a canvas.
|
||||
let mut data = AppData::new( app, platform, main, Sources::new( wake ), coarse, None );
|
||||
@@ -289,6 +292,10 @@ impl<A: App> AppData<A>
|
||||
if self.app.on_close_requested() { self.exit_requested = true; }
|
||||
}
|
||||
PageEvent::Close { surface } => self.dismiss_overlay( surface ),
|
||||
PageEvent::Toplevel( event ) =>
|
||||
{
|
||||
if let Some( msg ) = self.app.on_toplevel_event( event ) { self.pending_msgs.push( msg ); }
|
||||
}
|
||||
}
|
||||
}
|
||||
self.dispatch_sources();
|
||||
@@ -302,6 +309,7 @@ impl<A: App> AppData<A>
|
||||
}
|
||||
|
||||
self.before_draw();
|
||||
super::desktop::issue_activation_tokens( self );
|
||||
super::overlays::reconcile_overlays( self );
|
||||
let drawn = in_frame && self.draw_due();
|
||||
super::subsurfaces::reconcile_subsurfaces( self );
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
//! The shell protocol over the app's connection to forge/web (forge/web/DESIGN.md, section 7): the JSON a shell speaks to forge over its socket, sent as a `shell` request and received as a `shell` event. What is sent before the app has connected waits for the connection.
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use serde_json::{ json, Value };
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
thread_local!
|
||||
{
|
||||
static REQUEST: RefCell<Option<js_sys::Function>> = const { RefCell::new( None ) };
|
||||
static PENDING: RefCell<Vec<Value>> = const { RefCell::new( Vec::new() ) };
|
||||
static LISTENER: RefCell<Option<Box<dyn Fn( &str )>>> = const { RefCell::new( None ) };
|
||||
}
|
||||
|
||||
/// Send `message`, one of the shell protocol's JSON objects, to forge/web.
|
||||
pub fn shell_send( message: &str )
|
||||
{
|
||||
let message: Value = match serde_json::from_str( message )
|
||||
{
|
||||
Ok( m ) => m,
|
||||
Err( e ) =>
|
||||
{
|
||||
web_sys::console::warn_1( &format!( "[ltk] shell message is not JSON ({e}): {message}" ).into() );
|
||||
return;
|
||||
}
|
||||
};
|
||||
let request = REQUEST.with( |r| r.borrow().clone() );
|
||||
match request
|
||||
{
|
||||
Some( request ) => send( &request, message ),
|
||||
None => PENDING.with( |p| p.borrow_mut().push( message ) ),
|
||||
}
|
||||
}
|
||||
|
||||
/// Have `listener` receive each of forge/web's shell messages as JSON.
|
||||
pub fn set_shell_listener( listener: impl Fn( &str ) + 'static )
|
||||
{
|
||||
LISTENER.with( |l| *l.borrow_mut() = Some( Box::new( listener ) ) );
|
||||
}
|
||||
|
||||
fn send( request: &js_sys::Function, message: Value )
|
||||
{
|
||||
let json = json!( { "type": "shell", "message": message } ).to_string();
|
||||
if let Err( e ) = request.call1( &JsValue::NULL, &JsValue::from_str( &json ) )
|
||||
{
|
||||
web_sys::console::warn_2( &"[ltk] forge/web refused a shell message:".into(), &e );
|
||||
}
|
||||
}
|
||||
|
||||
/// The connection's `request`, once connected, and `None` once it has closed.
|
||||
pub( super ) fn connected( request: Option<js_sys::Function> )
|
||||
{
|
||||
if let Some( request ) = &request
|
||||
{
|
||||
for message in PENDING.with( |p| std::mem::take( &mut *p.borrow_mut() ) )
|
||||
{
|
||||
send( request, message );
|
||||
}
|
||||
}
|
||||
REQUEST.with( |r| *r.borrow_mut() = request );
|
||||
}
|
||||
|
||||
pub( super ) fn deliver( message: &Value )
|
||||
{
|
||||
let json = message.to_string();
|
||||
LISTENER.with( |l|
|
||||
{
|
||||
if let Some( listener ) = l.borrow().as_ref() { listener( &json ); }
|
||||
} );
|
||||
}
|
||||
@@ -28,8 +28,10 @@ pub( crate ) struct WebPlatform<A: App>
|
||||
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 under the pointer that takes the main surface's input.
|
||||
/// 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.
|
||||
@@ -61,6 +63,7 @@ impl<A: App> WebPlatform<A>
|
||||
gl,
|
||||
context_lost: false,
|
||||
cursor_target,
|
||||
cursor: "default",
|
||||
inset_probe,
|
||||
frame_due: RefCell::new( Vec::new() ),
|
||||
next_surface: Cell::new( super::desktop::MAIN + 1 ),
|
||||
|
||||
@@ -151,12 +151,12 @@ pub( super ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
||||
|
||||
fn create<Msg: Clone>( desktop: &Connection, id: u32, spec: &SubsurfaceSpec<Msg>, parent: &Parent, gpu: bool ) -> Option<WebSubsurface>
|
||||
{
|
||||
let ( canvas, _input ) = desktop.create_surface( id, role( spec.x, spec.y, parent ) ).map_err( |e| warn( spec.id, &e ) ).ok()?;
|
||||
let ( canvas, input ) = desktop.create_surface( id, role( spec.x, spec.y, parent ) ).map_err( |e| warn( spec.id, &e ) ).ok()?;
|
||||
// Input falls through to the parent, which keeps one gesture model for both.
|
||||
if let Err( e ) = desktop.request( &json!( { "type": "set_input_region", "surface": id, "rects": [] } ) ) { warn( spec.id, &e ); }
|
||||
Some( WebSubsurface
|
||||
{
|
||||
surface: WebSurfaceState::new( canvas, id, gpu, Vec::new() ),
|
||||
surface: WebSurfaceState::new( canvas, input, id, gpu, Vec::new() ),
|
||||
canvas: None,
|
||||
parent: spec.parent,
|
||||
parent_id: parent.id,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use serde_json::Value;
|
||||
use web_sys::{ CanvasRenderingContext2d, Element, HtmlCanvasElement, ImageBitmapRenderingContext };
|
||||
use web_sys::{ CanvasRenderingContext2d, Element, HtmlCanvasElement, HtmlElement, ImageBitmapRenderingContext };
|
||||
|
||||
use crate::app::App;
|
||||
use crate::event_loop::AppData;
|
||||
@@ -14,6 +14,8 @@ use super::run::Listener;
|
||||
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.
|
||||
@@ -28,17 +30,19 @@ pub( crate ) struct WebSurfaceState
|
||||
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, id: u32, gpu: bool, listeners: Vec<Listener> ) -> Self
|
||||
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, bitmap, ctx2d, pixels: Vec::new(), id, input_region: None, role: None, listeners }
|
||||
Self { canvas, input, bitmap, ctx2d, pixels: Vec::new(), id, input_region: None, role: None, listeners, presented: false }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user