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.
This commit is contained in:
2026-10-16 23:46:50 +02:00
parent 85aa85a357
commit cacf871ddd
131 changed files with 6155 additions and 3492 deletions
+12
View File
@@ -6,6 +6,16 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
### Added
- **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.
- **`ltk::resource_dir()`** — the resource directory `set_resource_dir` gave, so an app finds its own bundled files (a terminal's monospace face) among the resources unpacked from the APK. `make install` now copies `scripts/` and `java/` into the registry, so apps package with the installed `android-apk.sh` and `android-assets.sh`.
- **Surface insets** — `SurfaceState::insets` holds the logical pixels the system covers along each edge of a surface; the view is laid out inside them while the background still fills the whole surface, and positions stay absolute, so hit testing is unchanged. The built-in keyboard is placed inside them too. On Android 11 and later they come from the window's `getRootWindowInsets()`: the status bar and any cutout on top, and on the other edges only bars that take taps, so gesture navigation leaves no gap at the bottom while a bar of buttons keeps its room; earlier releases fall back to the activity's content rect. On Wayland they stay zero.
- **APKs with Debian's own tools** — `scripts/android-apk.sh OUT.apk LIB.so PACKAGE LABEL [ASSETS_DIR]` packages an app built as a `cdylib` into a NativeActivity APK: it writes the manifest (the library loaded as `libmain.so`, optional permissions, a theme without an action bar, which the content rect would otherwise leave room for), compiles it with `aapt` against `android-framework-res`, adds the assets and the library, aligns with `zipalign` and signs with `apksigner`, creating a debug keystore in `~/.android/` when there is none; no Gradle or Android SDK download is involved. `ICON` gives the launcher icon, an SVG rasterised with `rsvg-convert` for every density, or a PNG. `KEEP_ALIVE=1` adds the only Java code, `java/net/liberux/ltk/KeepAlive.java`, compiled with `javac` and `dx` (art-standalone) against the platform 34 `android.jar` (google-android-platform-34-installer), which aapt then links against too, as Debian's framework-res predates the `specialUse` service type; the service is declared with `KEEP_ALIVE_USE` as its reason, with the permissions it needs. `examples/android_hello.rs`, a counter built as a `cdylib` only with the `android` feature, is the smoke test: `make android-apk` builds and packages it with the in-tree default theme, `make android-install` installs it through `adb`. On Android, `set_android_app` now also sends standard output and error, panics included, to logcat under the process's name.
- **`ltk::set_resource_dir( dir )`** — an app's own copy of ltk's resources, for a package with no system to install them into. Its `themes/` is searched after the user's themes and before `/usr/share/ltk/themes`, and its `keyboards/` before the system's layouts; 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 font paths works unchanged from the bundle.
- **Themes and fonts in an Android package** — `set_android_app` unpacks `assets/ltk/` into the app's internal storage and uses it as the resource directory. `assets/ltk/MANIFEST` lists the tree, one path per line, since the asset manager cannot list subdirectories; the tree is unpacked again only when the manifest changed, and its first line is a hash of the contents. `scripts/android-assets.sh` builds the tree from installed themes, without their cursors, and copies the fonts each `theme.json` names and ltk's keyboard layouts (`KEYBOARDS_DIR` overrides where they come from); `make android-assets` runs it on the in-tree default theme. On Android the per-script fallback fonts are read from `/system/fonts`, and the primary face is the embedded Sora. `make android-check` typechecks the Android backend with Debian's own rustc, `rust-src` and `google-android-ndk-r27c-installer`, through `-Zbuild-std`; `NDK`, `ANDROID_API` and `ANDROID_TARGET` override its defaults.
- **Android backend, the `android` feature** — `ltk::run` inside a NativeActivity through android-activity 0.6 and ndk 0.9, optional dependencies that build only for Android targets. The feature excludes `wayland`, and `build.rs` refuses both at once, so an Android build uses `default-features = false, features = [ "android" ]`. The app's `android_main` passes its `AndroidApp` (re-exported as `ltk::AndroidApp`) to the new `ltk::set_android_app` before `ltk::run`; without it `run` fails with the new `RunError::NoAndroidApp`. The backend draws through EGL on the activity's `ANativeWindow`, with `eglSwapBuffers` waiting for the display, or in software into a locked window buffer; sizes the main surface to the window at the display's density in whole multiples of 160 dpi; turns fingers and styluses into touches, a mouse into the pointer and key events into keysyms and text through the device's character map; and types on ltk's own keyboard. calloop's descriptor rides on the activity's `ALooper`, so the app's channel and timers keep working. Not yet: overlays, secondary surfaces and subsurfaces, dead keys from a physical keyboard and the activity's saved state. `EglContext` gains `new_android`, `create_native_surface` and `release_current`, and swaps with the interval its backend needs; `EglSurface::egl_window` and `resize` are Wayland's only. What a frame callback does is now the backend-neutral `AppData::frame_presented`, which both backends call. `session_state.rs` builds only with `wayland`, its only user, and so do the AT-SPI2 adapter's construction and action forwarding. The state only the Wayland backend reads moved out of the runtime's: the clipboard inbox, a drag's position and type, the first-configure latches (`pending_fullscreen`, `pending_size_hint_unpin`, `pending_size_hint_fit`, `fitted_size_hint`) and the overlay canvas pool from `AppData` into `WaylandPlatform`, and `titlebar_base` from `SurfaceState` into `WaylandSurfaceState`. A build with another backend does not warn about runtime code only Wayland drives; the Wayland build still does.
- **`wayland` feature, on by default** — the Wayland backend and its dependencies (smithay-client-toolkit, wayland-protocols, wayland-egl, wayland-scanner, calloop-wayland-source, and accesskit_unix for AT-SPI2 accessibility) are now optional. A new `build.rs` sets `cfg( backend )` when some backend is enabled, which builds the runtime: `run`, `try_run`, `RunError` and everything behind them. With `--no-default-features` only the runtime-free core builds (`UiSurface`, widgets, layout, theming, rendering, `EglOffscreenContext`); `EglContext` and `EglSurface` need `wayland`. The backend in use is named by `platform::NativePlatform` and `platform::NativeSurface`. `make install` now copies `build.rs` into the registry. Builds with default features are unchanged.
- **`TextEdit::color( c )` / `TextEdit::on_focus( msg )`** — `color` sets the colour of the value, which always came from the theme; the placeholder keeps the theme's. `on_focus` emits a message when the field takes the keyboard focus (press, Tab, `take_focus_request`), once per change and not for a press on the field that already holds it. `App::on_text_input_focused( bool )` only says that some text field is focused; an app that mirrors focus into another model (rustdroid keeps Android's focused view in step) needs to know which, and a per-widget message carries that without a `WidgetId`, which is a `&'static str` and cannot name a dynamic handle. The message rides the `WidgetHandlers::TextEdit` snapshot and is pushed from `set_focus`.
- **External text input: `App::external_text_input()`, `on_external_text_commit`, `on_external_text_delete`** — lets an app hold the input method for a text field ltk does not own: an editable inside an embedded web view, a guest toolkit's field. While `external_text_input` returns `Some( serial )` and no ltk text widget has the focus, the runtime keeps a `zwp_text_input_v3` enabled, so the compositor raises its on-screen keyboard, and routes `commit_string` / `delete_surrounding_text` to the two new hooks instead of to a widget; a change of `serial` renews the `enable`, which is how a host asks for a keyboard the user folded to come back. Polled after every batch of `update`s through `AppData::sync_external_text_input`; an ltk text widget that takes the focus wins and hands the text-input back when it loses it. Keys such as Backspace and Enter keep arriving as key events. Default `None`.
- **`App::on_back()` / `BackResponse`** — the app's answer to a back request (the shell's back button or gesture, delivered as `XF86Back`, or a mouse's `BTN_SIDE` / `BTN_BACK`). Back peels one layer per request: transient UI the runtime owns (xdg-popups, context menu, text selection, a dialog's `on_escape`), then text-input focus, then `on_back`, then `on_key_with_modifiers( XF86_Back )` for apps that bound the key themselves, then `back_at_root`. `BackResponse::Message` carries the message that undoes the top-most piece of navigation state, `Consumed` is for hosts whose back stack lives elsewhere (an embedded Android runtime), `Unhandled` means the root. Default `Unhandled`.
@@ -46,6 +56,8 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
### Changed
- **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`.
- **A `window_size_hint` larger than the screen is shrunk to fit.** The hint is pinned as `min_size == max_size` before the first commit, when no output is known yet, so an app asking for a phone-shaped 720x1440 window on a 1080p desktop got a toplevel taller than the screen. On the first configure the runtime now fits the hint, keeping its aspect ratio, inside the compositor's `xdg_toplevel.configure_bounds` (`WindowConfigure::suggested_bounds`) or, when it sends none, inside 90 % of the first output's logical size; `min_size` (and `max_size` for a non-resizable window) is re-pinned to the fitted size, which also replaces the hint as the fallback when later configures carry no size. A hint that fits is untouched. `event_loop/window_fit.rs`, with unit tests for the fitting rule.
- **A press on a text field renews the text-input `enable`.** Moving the focus between two text fields, or pressing the one already focused, used to commit only the refreshed content type on the existing `zwp_text_input_v3`. It now sends `enable` first, as GTK does when a focused entry is clicked: a compositor that folded its on-screen keyboard while the field kept the focus (the back button does) has no other way to learn that the user wants it again, since the text-input never stopped being active. ltk sends neither surrounding text nor a cursor rectangle, so the state `enable` resets is only the content type set right after it.