A browser backend: ltk runs on a page's canvas through WebGL2, on a core that builds for wasm32, and a surface's scale may be fractional
ltk ran on Wayland and Android only. Running crustace and its apps in a browser needs, first, ltk apps running there; a window manager for the page is the next step. The runtime was already split from Wayland by the platform layer, so what kept it out of a browser was what the core and the runtime took from the operating system: calloop's epoll loop, EGL, a monotonic clock, a file system, threads and environment variables. The core builds for wasm32-unknown-unknown. `ltk::ChannelSender` is ltk's own type instead of a re-export of calloop's `Sender`, with the same `send` and `Clone`, so apps that only send and clone it build unchanged. The runtime arms its timers and channels through `event_loop/sources.rs` instead of a calloop `LoopHandle`: calloop's natively, and in a browser a queue the backend runs, which takes the due timers out, runs them on `AppData` and hands them back, and does not arm again one removed meanwhile. calloop, khronos-egl and raw-window-handle are dependencies off wasm32 only, where `egl_context.rs`, `UiSurface::try_new_gles` and `from_current_gles_loader` and the native GL names of `BorrowedGlesTexture` are left out; `sub_canvas` clones the uniform locations, which are `Copy` natively and only `Clone` on wasm. The monotonic clock is `crate::time::Instant`, `web_time`'s on wasm32, where `chrono` takes its `wasmbind` feature; the one subtraction that could go below the clock's origin, a surface's first `last_draw`, saturates. Themes, fonts, keyboard layouts and images are read through `vfs.rs`, the file system natively and on wasm32 files held in memory, so a wallpaper or an `Image::from_path` is now decoded from the file's bytes and its format told from its contents rather than its extension. The text-scale watcher, which runs gsettings on a thread, is left out on wasm32, and the built-in keyboard takes its language from `platform::language()`, `LANG` or the page's. criterion is a dev-dependency off wasm32 only, since rayon does not build there. The browser backend is the `web` feature, `platform/web/`, with wasm-bindgen, wasm-bindgen-futures, js-sys and web-sys. `build.rs` refuses it with another backend or for another target. A page's main thread must not block, so `ltk::run` sets the app up and returns, and the browser drives the loop: `AppData` lives in a closure a thread-local loop holds, which a `setTimeout` runs for messages, timers and page events and a `requestAnimationFrame` runs to draw. Event listeners and `ChannelSender::send` only queue and schedule, never touching `AppData`, so nothing they set off runs inside an iteration. The app draws on a canvas laid over the whole page, or the one `ltk::web::set_canvas` gives, through WebGL2 with the GLES canvas unchanged, its frame kept in its FBO, or without WebGL2 through `putImageData`. A lost context drops the GPU canvases and draws nothing until it is restored on a new `glow::Context`, so the glyph atlas and the caches of the old one are not reused. The main surface's logical size is the canvas's CSS size; the window's `resize` refits it, and on ltk's own canvas a hidden element padded by `env( safe-area-inset-* )` gives the insets. Input comes from Pointer Events, a mouse as the pointer and fingers and pens as touches, the wheel, in detents when it scrolls lines, and keys, whose keysym comes from `KeyboardEvent.key` and code from `.code` as the Linux input code Wayland reports; keys repeat on the browser's clock, AltGr types its character, and on macOS Command is Ctrl. ltk takes every key the browser lets a page have, and while fullscreen those `navigator.keyboard.lock()` adds in Chromium. Copies go to `navigator.clipboard.writeText`; a page reads the clipboard only from a `paste` event, so Ctrl+V is let through to the browser and held until its `paste` has put the text in `AppData::clipboard`, and a paste from the browser's menu comes with a Ctrl+V of its own. The cursor's shape is the canvas's CSS `cursor`. The built-in keyboard serves text where the page is used by touch. Panics go to the browser's console. When the app exits, the listeners are removed, the canvas receives an `ltk-exit` event and the app is dropped. The new `RunError::NoDocument` is returned where there is no page, as in a worker. Resources are the tree an Android package carries. `scripts/web-assets.sh` builds it through `android-assets.sh`, and `ltk::web::load_resources( base ).await` fetches its `MANIFEST` and every file it lists, with the manifest's hash in their URLs so a changed tree is not served from the browser's cache, into memory under `/ltk`, which becomes the resource directory. The per-script fallback fonts are read through `resources::read_font`, so a path that does not exist is found by file name in the resource directory's `fonts/`, where a page bundles them. A surface's scale may be fractional. `SurfaceState::scale_factor` is an `f32`, which `Platform::prepare_canvas`, `Presentation::scale` and the paint path take as it is; a surface's physical size is its logical size times the scale, rounded once, and `buffer_scale()` gives the whole number Wayland's buffer scale and regions need. Wayland and Android still set whole scales, so nothing changes there; the browser backend sets the page's `devicePixelRatio`, so the canvas holds the display's own pixels at any zoom. `App::on_scale_changed` keeps its `u32`, the scale rounded, and `App::on_resize` gives the exact physical size. The fallback-theme banner placed its text as if `draw_text` took the top of the line rather than its baseline, so most of the text fell above the banner, out of the surface; it is now centred as the title bar's is. Building. `make web-check` typechecks the backend. `make web-deps` installs Debian's std for wasm32 and wasm-bindgen-cli, whose version must be the one of the wasm-bindgen crate in `Cargo.lock`: the crate is pinned to 0.2.117, the last whose CLI builds with Debian's rustc 1.85, and since the lock the CLI was published with needs a newer rustc and `cargo install` ignores rust-version when it resolves, the CLI is built from its sources with a lock resolved for the rust-version it declares, into `target/tools`, where the Makefile finds it. `make web-assets` builds the resources with the in-tree default theme and the fallback fonts `WEB_FONTS` names. `examples/web_hello.rs`, a counter and a text field, and `web/index.html` are the smoke test: `make web-example` builds them into `target/web/` and `make web-serve` serves them. `make install` now copies `web/` into the registry. `README.md`, `docs/architecture.md` and `CHANGELOG.md` describe the backend, and `docs/cookbook.md` points to `ChannelSender`'s new home. Not done. In a browser there are no overlays, secondary surfaces or subsurfaces, no dead keys or input methods, no accessibility tree and no primary selection, and a canvas that changes size while the window does not is not refitted. Pasting relies on the browser sending `paste` to a focused canvas, which Chrome and Firefox do. The page fetches its whole resource tree, about 7 MB, before the app starts. There are no tests for the browser backend, `Sources`' queue or `vfs.rs`'s in-memory files, and fractional scales may leave one-pixel artefacts where edges fall between pixels.
This commit is contained in:
+22
-30
@@ -5,10 +5,6 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use calloop::LoopHandle;
|
||||
use calloop::channel::{ self, Event };
|
||||
use calloop::timer::{ TimeoutAction, Timer };
|
||||
|
||||
use crate::a11y::A11yState;
|
||||
use crate::app::App;
|
||||
use crate::platform::Platform;
|
||||
@@ -16,11 +12,12 @@ use crate::types::Point;
|
||||
|
||||
use super::{ AppData, RunError, SurfaceFocus, SurfaceState };
|
||||
use super::perf::PerfState;
|
||||
use super::sources::Sources;
|
||||
|
||||
impl<A: App, P: Platform> AppData<A, P>
|
||||
{
|
||||
/// `app` on `platform` with `main` as its only surface, nothing focused, pressed or queued.
|
||||
pub( crate ) fn new( app: A, platform: P, main: SurfaceState<A::Message, P::Surface>, loop_handle: LoopHandle<'static, Self>, builtin_osk: bool, a11y: Option<A11yState> ) -> Self
|
||||
pub( crate ) fn new( app: A, platform: P, main: SurfaceState<A::Message, P::Surface>, sources: Sources<Self>, builtin_osk: bool, a11y: Option<A11yState> ) -> Self
|
||||
{
|
||||
let fullscreen = app.start_fullscreen();
|
||||
Self
|
||||
@@ -39,7 +36,7 @@ impl<A: App, P: Platform> AppData<A, P>
|
||||
shift_pressed: false,
|
||||
ctrl_pressed: false,
|
||||
alt_pressed: false,
|
||||
loop_handle,
|
||||
sources,
|
||||
compositor_repeat_rate: 0,
|
||||
compositor_repeat_delay: 0,
|
||||
key_repeat: None,
|
||||
@@ -74,44 +71,39 @@ impl<A: App, P: Platform> AppData<A, P>
|
||||
/// Let the app send messages from any thread, follow the desktop's text scale and wake on [`App::poll_interval`].
|
||||
pub( crate ) fn install_sources( &mut self ) -> Result<(), RunError>
|
||||
{
|
||||
let handle = self.loop_handle.clone();
|
||||
|
||||
// Only queued: the run loop's `App::invalidate_after` decides which surfaces the message redraws.
|
||||
let ( sender, messages ) = channel::channel::<A::Message>();
|
||||
handle.insert_source( messages, |event, _, data: &mut Self|
|
||||
{
|
||||
if let Event::Msg( msg ) = event { data.pending_msgs.push( msg ); }
|
||||
} )
|
||||
.map_err( |e| RunError::EventLoop( format!( "channel insert_source: {e:?}" ) ) )?;
|
||||
let sender = self.sources.channel( |msg, data: &mut Self| data.pending_msgs.push( msg ) )
|
||||
.map_err( |e| RunError::EventLoop( format!( "channel insert_source: {e}" ) ) )?;
|
||||
self.app.set_channel_sender( sender );
|
||||
|
||||
// Font resolution multiplies by `text_scale()` at paint time, so a new factor only needs a repaint.
|
||||
let ( tx, factors ) = channel::channel::<f32>();
|
||||
handle.insert_source( factors, |event, _, data: &mut Self|
|
||||
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
||||
{
|
||||
let Event::Msg( factor ) = event else { return };
|
||||
if ( factor - crate::types::text_scale() ).abs() < f32::EPSILON { return; }
|
||||
crate::types::set_text_scale( factor );
|
||||
data.dirty_caches();
|
||||
data.main.request_redraw();
|
||||
for ss in data.overlays.values_mut()
|
||||
let tx = self.sources.channel( |factor: f32, data: &mut Self|
|
||||
{
|
||||
ss.request_redraw();
|
||||
}
|
||||
} )
|
||||
.map_err( |e| RunError::EventLoop( format!( "text-scale insert_source: {e:?}" ) ) )?;
|
||||
super::text_scale::spawn_watcher( tx );
|
||||
if ( factor - crate::types::text_scale() ).abs() < f32::EPSILON { return; }
|
||||
crate::types::set_text_scale( factor );
|
||||
data.dirty_caches();
|
||||
data.main.request_redraw();
|
||||
for ss in data.overlays.values_mut()
|
||||
{
|
||||
ss.request_redraw();
|
||||
}
|
||||
} )
|
||||
.map_err( |e| RunError::EventLoop( format!( "text-scale insert_source: {e}" ) ) )?;
|
||||
super::text_scale::spawn_watcher( tx );
|
||||
}
|
||||
|
||||
if let Some( dur ) = self.app.poll_interval()
|
||||
{
|
||||
self.perf.warn_poll_interval( dur );
|
||||
handle.insert_source( Timer::from_duration( dur ), |_, _, data: &mut Self|
|
||||
self.sources.insert_timer( dur, |data: &mut Self|
|
||||
{
|
||||
let msgs = data.app.poll_external();
|
||||
data.pending_msgs.extend( msgs );
|
||||
TimeoutAction::ToDuration( data.app.poll_interval().unwrap_or( std::time::Duration::from_secs( 1 ) ) )
|
||||
Some( data.app.poll_interval().unwrap_or( std::time::Duration::from_secs( 1 ) ) )
|
||||
} )
|
||||
.map_err( |e| RunError::EventLoop( format!( "poll timer insert_source: {e:?}" ) ) )?;
|
||||
.map_err( |e| RunError::EventLoop( format!( "poll timer insert_source: {e}" ) ) )?;
|
||||
}
|
||||
Ok( () )
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user