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:
@@ -6,6 +6,7 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
||||
|
||||
### Added
|
||||
|
||||
- **Browser backend, the `web` feature** — `ltk::run` on a page's canvas, built for `wasm32-unknown-unknown` with wasm-bindgen, js-sys and web-sys, optional dependencies on that target only. `build.rs` refuses it with another backend or for another target, so a web build uses `default-features = false, features = [ "web" ]`. A page's main thread must not block, so `run` sets the app up and returns, and `setTimeout` and `requestAnimationFrame` drive the loop; `Sources` (`event_loop/sources.rs`) gains a queue of timers and channels for it, and page events and `ChannelSender::send` only queue and schedule 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, or without it in software through `putImageData`; its logical size is the canvas's CSS size, its scale the page's `devicePixelRatio`, fractional as it may be, so the canvas holds the display's own pixels. Panics go to the browser's console. When the app exits, the canvas receives an `ltk-exit` event. The new `RunError::NoDocument` is returned where there is no page, as in a worker. The built-in keyboard serves text where the page is used by touch. Input comes from the canvas's Pointer Events, a mouse as the pointer and fingers and pens as touches, at `offsetX` / `offsetY` in CSS pixels; a press captures the pointer and focuses the canvas. A wheel in lines scrolls in detents, one in pixels continuously, and the page neither scrolls nor zooms under the canvas. Keys take their keysym from `KeyboardEvent.key`, their code from `.code` as the Linux input code Wayland reports, and repeat on the browser's clock; AltGr types its character rather than counting as Ctrl and Alt. ltk takes every key the browser gives a page, and while fullscreen also those `navigator.keyboard.lock()` gives where there is one. The cursor's shape is the canvas's CSS `cursor`, and on macOS Command counts as Ctrl. 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 as a Ctrl+V of its own. A lost WebGL 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. On ltk's own canvas the page's `env( safe-area-inset-* )`, read from a hidden element, become the main surface's insets. Resources are the tree an Android package carries, which `scripts/web-assets.sh` builds through `android-assets.sh` and `make web-assets` builds into `target/web/ltk/` with the in-tree default theme and the fallback fonts `WEB_FONTS` names; `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 now read through `resources::read_font`, so a path that does not exist is found by file name in the resource directory's `fonts/`, which is where a page's are. `wasm-bindgen-futures` joins the `web` feature. `examples/web_hello.rs` is the smoke test: `make web-check` typechecks the backend, `make web-example` builds the example and runs wasm-bindgen into `target/web/`, `make web-serve` serves it, and `make web-deps` installs Debian's std for wasm32 and wasm-bindgen-cli at the version of `Cargo.lock`. The text-scale watcher does not run in a browser, and the built-in keyboard's language is the page's. Not yet: overlays, secondary surfaces and subsurfaces, dead keys and input methods, the accessibility tree, the primary selection, and a canvas that changes size while the window does not.
|
||||
- **The built-in keyboard serves an app's own text input** — while `App::external_text_input` asks for text and no ltk text field has the focus, the built-in keyboard shows with `App::external_text_purpose` (a terminal gets its row of Escape, Tab, Ctrl, Alt and arrows), comes back when the serial changes, and hides when the request ends. Its keys reach the app as physical ones, pressed and released through `App::on_raw_key_event` with the panel's Ctrl and Alt held for them. Before, the built-in keyboard only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method.
|
||||
- **Android: the system clipboard, back that leaves, ltk's keyboard, `HOME` and `LANG`** — copies go to Android's `ClipboardManager` and its text is read back whenever the app gains the focus, through JNI on the activity (the `jni` crate joins the `android` feature). Back with nothing left to go back to minimises the app with `moveTaskToBack`, or closes it, as `App::back_at_root` says; the new `Platform::back_leaves` decides whether back may leave the main surface, which keeps Wayland's rule of the xdg-toplevel. Text is typed on ltk's own keyboard rather than the system's, from which a NativeActivity gets only key events. `set_android_app` sets `HOME` to the app's internal storage and, unless set, `SSL_CERT_DIR` to Android's CA certificates and `LANG` to the device's language, so desktop code finds its preferences, a TLS client its roots (rustls-native-certs knows no Android path) and the locale.
|
||||
- **Android: running in the background and closing** — `ltk::keep_running( Some( text ) )` starts the keep-alive service of an APK packaged with `KEEP_ALIVE=1`, a foreground service with `text` on an ongoing notification, so Android does not freeze the app once it leaves the screen; `None` stops it, and without the service nothing happens. When the app's last surface closes, ltk calls `finishAndRemoveTask` and waits for the activity to be destroyed: a second activity that Android starts in a process whose `android_main` returned never comes up, so the app should end the process after `run` returns rather than wait to be started again. `set_android_app` sets up the process only on its first call and keeps the latest activity on every call.
|
||||
@@ -56,6 +57,9 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
||||
|
||||
### Changed
|
||||
|
||||
- **A surface's scale may be fractional.** `SurfaceState::scale_factor` is an `f32`, the physical pixels per logical one, 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 (`physical_width`, `physical_height`), 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`. `App::on_scale_changed` keeps its `u32`, the scale rounded, and `App::on_resize` gives the exact physical size.
|
||||
- **`ltk::ChannelSender` is ltk's own type** instead of a re-export of calloop's `Sender`, with the same `send`, returning `Result<(), std::sync::mpsc::SendError<T>>`, and `Clone`, so apps that only send and clone it build unchanged; code that named calloop's type has to name ltk's. The runtime arms its timers and channels through `event_loop/sources.rs` instead of a calloop `LoopHandle`. calloop, khronos-egl and raw-window-handle are dependencies only off `wasm32`, where `egl_context`, `UiSurface::try_new_gles`, `UiSurface::from_current_gles_loader` and the native GL names of `BorrowedGlesTexture` (`texture_id`, `framebuffer_id`) do not exist; the monotonic clock is `crate::time::Instant`, which is `web_time::Instant` on `wasm32`, and `chrono` takes its `wasmbind` feature there.
|
||||
- **Themes, fonts, keyboard layouts and images are read through `vfs.rs`**, which is the file system natively and on `wasm32` a set of files held in memory, filled through `vfs::insert`. A wallpaper or an `Image::from_path` is now decoded from the file's bytes, so its format is told from its contents rather than its extension.
|
||||
- **Input reaches the runtime as platform-neutral events.** `input/event.rs` defines `KeyInput`, `PointerInput` and `TouchInput`, and the pointer, touch and keyboard handlers take them through a small set of entry points (`pointer_input`, `touch_input`, `key_press`, `key_release`, `key_repeated`, `modifiers_changed`, `keyboard_enter`, `keyboard_leave`, `set_repeat_info`) instead of smithay-client-toolkit's events. The `KeyboardHandler`, `PointerHandler` and `TouchHandler` implementations moved to `platform/wayland/input.rs`, which only records the serials later Wayland requests need, resolves surfaces and translates. What input handling asks of the windowing system (window move, resize, minimise and fullscreen, a bare frame request, the cursor's shape, the input method) is done by named methods in `platform/wayland/`, so nothing under `input/` names Wayland and no entry point or `set_focus` takes a `QueueHandle`; `ResizeEdge` is ltk's own. The protocol state moved out of `AppData` into `platform::wayland::WaylandPlatform`, held as `AppData::platform`, and the compositor, shell, seat and output handlers, the data device and the primary selection moved from `event_loop/` to `platform/wayland/`. Each surface's Wayland half (`SurfaceKind`, the SHM pool, the EGL window surface and what the compositor was last told) moved from `SurfaceState` into `platform::wayland::WaylandSurfaceState`, held as `SurfaceState::platform`, and the requests above form the `platform::Platform` trait, with the surface type as its associated `Surface`. Drawing a frame is split from presenting it: `draw/paint.rs` paints a surface's view in full or under its dirty rects, the same for the software and GPU canvases, in place of the four near-identical paths of `draw/software.rs` and `draw/gles.rs`, and the backend prepares the canvas and presents it (`Platform::prepare_canvas`, `present`, `precise_damage`), in `platform/wayland/present.rs`, which also took the opaque and input regions and `pick_shm_format`. The EGL context moved into `WaylandPlatform`. Publishing the clipboard and the primary selection goes through `Platform::publish_clipboard` and `publish_primary_selection`, while their text stays with the runtime in `AppData::clipboard` and the new `AppData::primary_selection`. The runtime is generic over the platform: `AppData<A, P: Platform = NativePlatform<A>>` and `SurfaceState<Msg, S = NativeSurface>`, resolved at compile time; the backend-neutral half of a configure is `SurfaceState::set_size` and `AppData::on_main_resized`, and the backend-neutral half of a loop iteration moved from `run.rs` to `event_loop/step.rs` (`process_messages`, `draw_due`, and around them `next_wakeup`, `before_draw` and `after_draw`, which carry the deadlines, the clipboard inbox, the accessibility tree and its actions, and the app's blur, copy, paste and focus requests), with `AppData::new` and `AppData::install_sources` (`event_loop/setup.rs`) building the state and the event sources every backend has. The run loop itself, overlay and subsurface reconciliation, session management, output probing and window fitting moved from `event_loop/` to `platform/wayland/`, with the main output, the subsurfaces and the drop inbox now in `WaylandPlatform`; `AppData::secondaries` pairs each surface with `Platform::Output`. Tests drive the runtime on `platform/mock.rs`, a platform that logs what is asked of it, through input, update, painting and presentation (`event_loop/tests.rs`). The behaviour is unchanged, but for a frame whose presentation fails, which now leaves the next frame to repaint in full. This is the first step towards backends other than Wayland, starting with Android; `docs/architecture.md` describes the events, their units and what stays with a backend.
|
||||
- **`ltk::Keysym` comes from `xkeysym`**, the crate smithay-client-toolkit re-exported it from, so it is the same type and no code using it changes.
|
||||
- **`XF86Back` is no longer an alias of `Escape`.** It used to run the Escape path: close transient UI, else drop the focus and reach `on_key_with_modifiers`. It is now a back request with its own order (see `App::on_back` under Added). Visible differences for an existing app: with a text input focused, back only drops the focus and the app sees the *next* request, where it used to get the key in the same press; the key is no longer reported through `App::on_raw_key`, so hosts forwarding raw keys into an embedded view stop leaking it to the page; it no longer auto-repeats by default (`App::key_repeats`); and a `ShellMode::Window` app that handles neither `on_back` nor the key is **minimised** on back at its root, where nothing happened before — return `BackAtRoot::Ignore` from `back_at_root` to keep the old behaviour. An app that binds `XF86_Back` in `on_key` / `on_key_with_modifiers` keeps receiving it. `Escape` itself is unchanged; its transient-UI step is now shared with back as `AppData::dismiss_transient`.
|
||||
|
||||
Reference in New Issue
Block a user