Commit Graph
6 Commits
Author SHA1 Message Date
admin 8f689ec479 An ltk app pastes what other applications copied, the middle button pastes the selection just made, a browser desktop's applications share the clipboard and the primary selection, and the office icons are drawn like the terminal's
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
Pasting from other applications. Neither Ctrl+V nor the middle button pasted into an ltk app anything copied in another application; only what was copied within the same process pasted. The Wayland backend bound its `wl_data_device` and its primary-selection device in `SeatHandler::new_seat`, which smithay-client-toolkit calls only for seats announced after start-up: `SeatState::new` binds the seats already in the registry without calling it, and on every compositor the seat is there when the app connects. The app therefore never had either device, the compositor never offered it another client's selection, and what it copied did not reach other applications through the compositor either. A Wayland trace of prompt shows both managers bound and neither `get_data_device` nor `get_device` sent. Both devices are now bound in `new_capability`, which comes for every seat, the first time it does; `new_seat` does nothing.
The middle button and a selection just made. A compositor offers the primary selection to the window it gives the keyboard, and gives it on the press, so a middle click on a window that did not have the keyboard arrives together with the offer. ltk reads an offer on a thread of its own and pasted at once what it held, so after selecting in another application an ltk app pasted the selection before that. `Platform::primary_selection_pending` says whether another application's selection is still being read: the Wayland backend counts the reader threads it starts and the selections that arrive (`PrimarySelection::pending`), and the other backends answer `false`. A middle-button paste, into the text field under the pointer (`PrimaryPaste::Field`) or for an app that asked with `SelectionKind::Primary` (`PrimaryPaste::App`), goes through `AppData::paste_primary`, which while a read is pending keeps it in `pending_primary_paste`; `check_primary_paste`, run before every draw, makes it once the selection has arrived or after 500 ms with what there is, and `next_primary_paste_wakeup` wakes the loop every 10 ms meanwhile, since the reading thread does not. A field paste is made only if the field still has the focus.
The clipboard and the primary selection in a browser desktop. On a page with forge/web, what an app copies goes to forge/web as a `set_selection` request as well as to the browser's clipboard, and the selection the middle button pastes, which did nothing in a browser, as `set_primary_selection` (`Connection::set_selection`). forge/web's `selection` and `primary_selection` events, which every client hears when one sets them and each hears on connecting, become `PageEvent::Selection` and so `AppData::clipboard` and `AppData::primary_selection`, as another client's selection does on Wayland. Copying, and selecting and pasting with the middle button, thus work between the page's applications whether or not the browser lets a page write its clipboard. A `paste` from the browser still brings what other applications of the computer copied; a browser has no primary selection a page can reach.
The office icons. The default theme's word processor, spreadsheet and presentation icons came from Figma with a `foreignObject` backdrop blur that resvg ignores, and showed as flat white discs with pale glyphs. They are drawn again as the terminal's are: a dark gradient disc in each one's colour (blue, green, orange), a bright glyph and the terminal's filter.
The doc comment of `ltk::files` links `write()` as a function, since `write` alone was ambiguous with the macro for rustdoc.
`CHANGELOG.md` describes the changes.
2026-10-23 19:20:10 +02:00
admin cf187a3dcc The user's files through ltk::files, alike natively and in a browser, and a shell's requests answered in a browser through ltk::web::shell_request
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
An app read the user's files with `std::fs`, which a browser does not have: there a page reaches a folder of the user's computer only through forge/web, which holds it through the File System Access API and answers asynchronously (forge/web/DESIGN.md, section 11). An app that builds for both, such as Crate, had no way to list, read or write a file in a browser, and a page's memory cannot hold a large file whole.
ltk::files. Asynchronous operations on the user's files whose results reach the app as messages through its `ChannelSender`: `list`, `stat`, `read`, `read_range`, `write`, `open_write`, `create_dir`, `rename`, `remove`, `remove_all`, `copy`, `copy_all`, `mount` and `unmount`, each taking the sender and a `done` that turns the `io::Result` into a message. Results come in no set order, so an app that has moved on checks that one is still the one it waits for. Errors are the same `io::ErrorKind`s on every platform, `Interrupted` for a cancelled copy. `Entry` and `Metadata` give a file's kind, size and times, the access time natively only. `ltk::vfs` stays what reads an app's own resources.
Natively. `std::fs` on one worker thread of ltk's, in the order the app asked (`files/native.rs`), and each copy on a thread of its own, so a long one holds nothing else up (`files/copy.rs`). A file written whole or through a `Writer` goes beside its target as a temporary file, synced and renamed over it on commit and removed if dropped before, so a file being written is never seen half done. `rename` never replaces what is at `to`, as in a browser.
In a browser. Each operation is one of forge/web's `fs_*` requests over the app's connection (`platform/web/files.rs`), carrying a number its `fs_reply` answers with; contents travel as a `Uint8Array` beside the JSON, the second argument of the connection's `request` and of the client's event handler, so `on_event` now takes it. What is asked before the app has connected is sent once it has; on a page without a desktop, or once the connection has closed, it fails with `Unsupported`. forge/web's error codes become their `io::ErrorKind`s, and `fs_progress` reaches a copy's `progress`. Without the web backend, on wasm32 every operation fails with `Unsupported` (`files/unsupported.rs`).
Chunks. Contents never travel whole unless they are small: a large file is read with `read_range` and written through a `Writer`, chunk by chunk, `files::CHUNK` (1 MiB) at a time, the next sent once the last is answered. A `Writer` dropped without `close` is aborted and the file left as it was; in a browser a new file shows empty until closed. A copy runs where the files are, its contents never passing through the app: it reports its progress every 100 ms, never replaces an existing file, and, cancelled through its `Operation` or failed, leaves nothing behind.
Mounts and the desktop's notices. `mount` asks the browser for a folder of the user's computer, from the user's press, and `unmount` lets it go; natively both fail with `Unsupported`. `App::on_files_changed`, `App::on_mounts_changed` and `App::on_open_files` hear of forge/web's `fs_changed`, `fs_mounts` and `open_files`, which the web backend queues as page events; natively they are not called yet.
Types. `files::mime_type( name )` gives a file's type by shared-mime-info's `globs2`, with its weights, its case-sensitive globs and fnmatch's `*`, `?` and bracket classes, from the resource directory's `mime/` first, then the user's and the system's. `scripts/android-assets.sh`, and so `web-assets.sh`, now copies shared-mime-info's `globs2` and `subclasses` into the tree's `mime/` (`MIME_DIR` overrides where from), so a page has them.
ltk::web::shell_request. A shell message that waits for forge/web's reply, such as `get_window_thumbnail`: sent as `shell_send` sends one and answered at once with forge/web's reply as JSON, or `None` before the app has connected, since a request is not kept for later, and when forge/web replies nothing. A shell's requests that wait for an answer natively got none in a browser. `shell.rs` parses a message once for both, and `send` returns forge/web's reply.
Tests cover writing and committing, aborting, listing, ranges, removing, copying a tree with its progress, a cancelled copy, a copy that would replace, and the globs. `CHANGELOG.md` describes the changes.
2026-10-22 00:15:39 +02:00
admin af4009374d 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.
2026-10-21 10:21:11 +02:00
admin cd1a233b2e A browser app joins the page's desktop: on a page with forge/web its main surface, overlays and subsurfaces are surfaces of that desktop, all painted through one WebGL context
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
The browser backend ran an app alone on a canvas over the page. An Eydos desktop in a browser needs crustace as its shell and other ltk apps as its windows on one page, managed by forge/web (forge's web/ directory), which publishes `globalThis.forgeWeb` and stacks, places and focuses the surfaces its clients ask for (forge/web/DESIGN.md).
The desktop's client. On a page where `globalThis.forgeWeb` exists and `set_canvas` named no canvas, `platform/web/desktop.rs` connects as the app's `app_id` and asks for the main surface in the role the app's hooks give, as a Wayland compositor reads them: `shell_mode`, and for a layer `layer_anchor`, `layer_size`, `exclusive_zone` and `keyboard_exclusive`, for a window `app_id` and `window_size_hint`, and for `SessionLock` a full overlay that holds the keyboard. The app draws into the canvas forge/web gives and listens on the input element above it, takes its size and scale from forge/web's `configure` rather than from the window's `resize`, sends its input region in logical pixels when it changes, and leaves the keyboard focus to the desktop, which honours the surfaces that hold it exclusively. A `close` of the main surface reaches `App::on_close_requested`; on exit the surface is destroyed and the connection closed. The new `RunError::Desktop` says forge/web refused the main surface. Fullscreen inside a desktop does nothing yet. A page without a desktop, or one that named a canvas, runs the app as before.
One WebGL context. A page holds few WebGL contexts, and the browser loses the oldest beyond them, while a shell has many surfaces. Every surface of an app now paints through one WebGL2 context on an `OffscreenCanvas`: the surface's FBO is blitted onto it, taken as an `ImageBitmap` and handed to the surface's own canvas, a `bitmaprenderer`, without a copy. Without WebGL2 each surface draws in software on a 2D context of its own. The context's loss is heard on the `OffscreenCanvas`, and now drops the GPU canvases of every surface, not only the main one's.
Overlays. `platform/web/overlays.rs` keeps the app's overlays in step with `App::overlays()`, as the Wayland backend's `overlays_reconcile.rs` does with layer-shell: a new one becomes a layer surface or, for `Layer::Window`, a window, with its size and exclusive zone in logical pixels; a changed one gets an `update_role`; one that went is destroyed; forge/web's `close` of one delivers its `on_dismiss`. Popups anchored to a widget and the tooltip wait for a popup role forge/web does not have, and without a desktop an app still shows no overlays. What every backend does as overlays come and go, resolving an `OverlaySpec::size` and moving a drag and the focuses off an overlay that goes, moved out of the Wayland backend into `event_loop/overlays.rs` (`AppData::overlay_size`, `AppData::remove_overlay`); Wayland's behaviour is unchanged.
Subsurfaces. `platform/web/subsurfaces.rs` makes each of `App::subsurfaces()` a surface of the desktop sized to its parent with an empty input region, painted on one shared GPU canvas only when it appears, its size changes or its `content_version` does, its frame then staying on its own canvas, and otherwise moved with an `update_role`, which the browser composes without painting. `App::on_subsurface_committed` follows each paint. A subsurface whose parent overlay was destroyed and made again is made again, since forge/web destroys a subsurface with its parent. `frame_presented` now knows whether a surface hosts subsurfaces, so while the app animates by subsurface motion only its frames keep coming without painting the parent, as Wayland keeps a parent's frame callbacks. Painting a view with no focus, hover or press on a canvas of its own, which both backends' subsurfaces do, moved out of the Wayland backend into `draw::paint_detached`.
Input per surface. `input::listen` takes the surface's `SurfaceFocus`, which pointer, touch and keyboard focus events carry; a surface's blur no longer takes the keyboard from the surface focused after it. Listeners remove themselves when dropped, and a surface's live in its `WebSurfaceState`, so they go with it. The frames owed at the next animation frame are a list of surfaces instead of one, which with several surfaces left the main one waiting on a frame that never came.
`examples/web_shell.rs` is a shell for the desktop: a background, a bar in the top layer with an exclusive zone, a panel that takes the keyboard, and a card on a subsurface that slides. `make web-example` builds it with web_hello (`WEB_EXAMPLES`); forge/web's `make hello` serves both at /shell.html. web-sys gains `OffscreenCanvas`, `ImageBitmap` and `ImageBitmapRenderingContext`. `CHANGELOG.md` describes the change.
Not done. Popups and the tooltip in a desktop, overlays and subsurfaces without one, fullscreen and window moves through forge/web, and the cursor's shape over an overlay, which is still set on the main surface's element. The offscreen canvas is resized for each surface it paints, which reallocates its drawing buffer when the surfaces differ in size. There are no tests for the browser backend.
2026-10-19 22:42:12 +02:00
admin 20dc21448e 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
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
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.
2026-10-19 20:20:12 +02:00
admin cacf871ddd An Android backend, a platform layer that keeps Wayland out of the runtime, the built-in keyboard for an app's own text input, and APKs packaged with Debian's own tools
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
ltk only ran on a Wayland compositor: the runtime, the input handlers and the draw paths named smithay-client-toolkit's types throughout, so no other windowing system could be added without touching all of them. Prompt, the Eydos terminal, has to run on an Android phone, with its sessions and its sharing, and needs a toolkit that runs there.
The platform layer. 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 few entry points (`pointer_input`, `touch_input`, `key_press`, `key_release`, `key_repeated`, `modifiers_changed`, `keyboard_enter`, `keyboard_leave`, `set_repeat_info`). The sctk handler implementations moved to `platform/wayland/input.rs`, which records the serials later requests need, resolves surfaces and translates. What the runtime asks of the windowing system (moving, resizing, minimising and fullscreening the window, a bare frame request, the cursor's shape, the input method, publishing the clipboard and the primary selection, preparing a canvas, presenting it and its damage) forms the `platform::Platform` trait, with its surface type as `Platform::Surface`. The protocol state moved from `AppData` into `platform::wayland::WaylandPlatform`, held as `AppData::platform`, and each surface's Wayland half (its kind, SHM pool, EGL window surface and what the compositor was last told) from `SurfaceState` into `WaylandSurfaceState`, held as `SurfaceState::platform`. The compositor, shell, seat and output handlers, the data device, the primary selection, the run loop, overlay and subsurface reconciliation, session management, output probing and window fitting moved from `event_loop/` to `platform/wayland/`. Drawing is split from presenting: `draw/paint.rs` paints a surface 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 presents it (`platform/wayland/present.rs`, which also took the opaque and input regions and `pick_shm_format`). 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 loop iteration is `event_loop/step.rs` (`process_messages`, `draw_due`, `next_wakeup`, `before_draw`, `after_draw`), and `event_loop/setup.rs` builds the state and the event sources every backend has; the accessibility tree and its actions moved to `event_loop/a11y.rs`. `platform/mock.rs` is a platform that logs what is asked of it, and `event_loop/tests.rs` drives the runtime on it through input, update, painting and presentation. The behaviour on Wayland is unchanged, except that a frame whose presentation fails leaves the next one to repaint in full. `ltk::Keysym` now comes from `xkeysym`, the crate sctk re-exported it from, so it is the same type.
Features. `wayland`, on by default, brings the Wayland backend and its dependencies, now optional, AT-SPI2 accessibility among them. `android` brings android-activity 0.6, ndk 0.9 and jni 0.21. A new `build.rs` refuses both at once and sets `cfg( backend )` when one is on, which builds the runtime behind `run` and `try_run`. Without a backend only the runtime-free core builds: `UiSurface`, widgets, layout, theming and rendering.
The Android backend. `ltk::run` runs inside a NativeActivity once `android_main` has handed its `AndroidApp`, re-exported as `ltk::AndroidApp`, to the new `ltk::set_android_app`; without it `run` fails with `RunError::NoAndroidApp`. The first call in the process sends standard output and error, panics included, to logcat, 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, and unpacks the bundled resources; every call keeps the latest activity, since Android starts the next one in the same process. The backend draws through EGL on the activity's window, or in software into a locked window buffer, and sizes the main surface at the display's density in whole multiples of 160 dpi. Fingers and styluses become touches, a mouse the pointer, and key events keysyms and text through the device's character map. calloop's descriptor rides on the activity's `ALooper`, so the app's channel and timers keep working. Text is typed on ltk's own keyboard, since a NativeActivity gets only key events from the system's. Through JNI on the activity (`platform/android/java.rs`): copies go to `ClipboardManager` and its text is read back whenever the app gains the focus; back with nothing left to go back to minimises the app with `moveTaskToBack` or closes it, as `App::back_at_root` says, which the new `Platform::back_leaves` decides per backend; and the window's insets come from `getRootWindowInsets()` on Android 11 and later, the status bar and any cutout on top and only bars that take taps on the other edges, so there is no gap above the view nor below it in gesture navigation, and a bar of buttons still keeps its room; earlier releases fall back to the content rect. When the app's last surface closes, ltk calls `finishAndRemoveTask` and waits for the activity to be destroyed, since a second activity started in a process whose `android_main` has returned never came up; an app should end the process after `run` returns. `ltk::keep_running( Some( text ) )` starts a foreground service with `text` on an ongoing notification, so Android does not freeze the app once it leaves the screen, and `None` stops it.
Surface insets. `SurfaceState::insets` holds the logical pixels the system covers along each edge; the view is laid out inside them while the background fills the whole surface, and positions stay absolute, so hit testing is unchanged. The built-in keyboard is placed inside them too, above a bar of navigation buttons. On Wayland they stay zero.
Resources in a package. `ltk::set_resource_dir( dir )` names an app's own copy of ltk's resources and `ltk::resource_dir()` returns it, so an app also finds its own bundled files there. Its `themes/` is searched after the user's themes and before `/usr/share/ltk/themes`, its `keyboards/` before `/usr/share`, and a font a theme names by an absolute path that does not exist is read from its `fonts/` by file name, so a theme written against Debian's paths works unchanged. On Android `set_android_app` unpacks `assets/ltk/` into the internal storage and uses it as the resource directory; `assets/ltk/MANIFEST` lists the tree, since the asset manager cannot list subdirectories, and its first line, a hash of the contents, decides whether to unpack again. On Android the per-script fallback fonts come from `/system/fonts` and the primary face is the embedded Sora.
APKs. `scripts/android-assets.sh` builds that tree from installed themes, without their cursors, with the fonts each `theme.json` names and ltk's keyboard layouts. `scripts/android-apk.sh OUT.apk LIB.so PACKAGE LABEL [ASSETS_DIR]` packages an app built as a `cdylib` with Debian's aapt, zipalign and apksigner, with no Gradle and no SDK download: it writes the manifest, with the library loaded as `libmain.so`, optional permissions and a theme without an action bar, which the content rect would otherwise leave room for; links it against `android-framework-res`; adds the assets and the library; aligns and signs, creating a debug keystore when there is none. `ICON` takes an SVG, rasterised with `rsvg-convert` for every density, or a PNG. `KEEP_ALIVE=1` compiles `java/net/liberux/ltk/KeepAlive.java` with javac and `dx` from art-standalone against the platform 34 `android.jar`, against which aapt then links too, since Debian's framework-res predates the `specialUse` service type, and declares the service with `KEEP_ALIVE_USE` as its reason and the permissions it needs. `examples/android_hello.rs` is the smoke test: `make android-check` typechecks the backend with Debian's rustc, `rust-src` and NDK through `-Zbuild-std`, `make android-apk` and `make android-install` package and install the example, `make deps` and `make android-deps` install what the targets need, and `make install` now copies `examples`, `scripts`, `java` and `build.rs` into the registry.
The built-in keyboard for 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`, so 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, it only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method.
Clippy. `make clippy` is clean again: derived `Default`s replace the hand-written ones, `div_ceil`, `then_some`, struct update syntax for strokes, the map entry API in the text renderer, function paths in place of closures in the examples and tests, and fewer casts and explicit lifetimes.
`README.md`, `docs/architecture.md` and `CHANGELOG.md` describe the features, the backend, the events and their units, and what stays with a backend.
Not done. On Android there are no overlays, secondary surfaces or subsurfaces, no dead keys from a physical keyboard and no saved state for the activity. Why a second activity in the same process never comes up was not found. The keep-alive service does not ask for the notification permission, so on Android 13 and later its notification may not show until it is granted in the app's settings.
2026-10-16 23:46:50 +02:00