Commit Graph
4 Commits
Author SHA1 Message Date
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
admin 96a9e88051 Back as a navigation request: App::on_back, BackAtRoot, NavStack and docs/navigation.md; renew the text-input enable on a press; fit an oversized window_size_hint to the screen
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
Three changes that came out of making the shell's back button work across Eydos the way it does on Android. The first is the feature; the other two are gaps it exposed.
Back is no longer Escape. forge turns its back gesture and button into the `XF86Back` key for the focused client, and ltk treated that key as an alias of `Escape`: close transient UI, else drop the focus and reach `on_key_with_modifiers`. That is "cancel", and it is all an app could get: nothing navigated, and nothing ever left the app. `XF86Back` is now a back request with its own order, one layer per request, top-most first: transient UI the runtime owns (open xdg-popup overlays, the built-in context menu, a text selection, the top-most pressable carrying an `on_escape`, i.e. a dialog's cancel), then text-input focus, then the new `App::on_back`, then `on_key_with_modifiers( XF86_Back )` so an app that bound the key itself keeps receiving it (crustace does, for its launcher), then `App::back_at_root`. The first step is what Escape already did; it moves out of `handle_key_escape` into `AppData::dismiss_transient` in the new `input/keyboard/back.rs`, which both paths call, so Escape behaves exactly as before. Text focus comes before the app on purpose: with a keyboard covering half the screen, back means "put the keyboard away", and navigating instead would discard what the user was typing. A compositor that folds its keyboard itself, as forge now does, only lets the next request through, which makes this step the fallback for the ones that do not.
`App::on_back` returns a `BackResponse`: `Message( msg )` with the message that undoes the top-most piece of navigation state, `Unhandled` at the root, or `Consumed` for a host whose back stack lives elsewhere and cannot be inspected ahead of time. `Consumed` exists for rustdroid: an Android app keeps its activities, fragments and back callbacks in its own code, so the host can only hand the request over. `App::back_at_root` then picks `Minimize` (the default), `Close` (through `on_close_requested`, which can still veto) or `Ignore`. Minimising is what a root activity does on Android since version 12 and what suits an app the user comes back to: a browser keeps its tabs, a call goes on. It is also the single point where the two stacks meet: the navigation stack belongs to the app's model, the task stack to the compositor, and `xdg_toplevel.set_minimized` is the one standard request between them, so no custom protocol carries navigation state out of the process and clients that are not ltk apps keep working. Only `ShellMode::Window` surfaces react at the root; a panel or a lock screen that minimised itself would be a bug.
Three consequences of back being a navigation request rather than input: the key leaves the default `App::key_repeats` set, since a held button would empty the whole stack; it is no longer reported through `App::on_raw_key`, so a host forwarding raw keys into an embedded web view or guest toolkit stops leaking it as a keystroke; and the mouse's back button (`BTN_SIDE` / `BTN_BACK`) enters through the same `handle_back`. `focused_text_input` becomes `pub( crate )` for the new module.
The answer is derived, not recorded, and that is the design decision worth writing down. The runtime keeps no navigation stack of its own. Its entries would have to be messages, which are not serialisable (the trait puts no serde bound on `Message`, by design), so it could not survive a relaunch; and a list of "things to undo" drifts the first time the user leaves a screen some other way, through its own close button or a link, leaving a back press that does nothing visible. An answer computed from the model cannot disagree with it. This is also what ties back to the session persistence added earlier: navigation state is both what `on_back` reads and what `save_state` should persist, and keeping the two the same is what makes back retrace the same places after a session restore or a crash, with nothing extra to serialise. Persist less and a restored app opens on the right screen with nothing behind it; persist more and it restores a menu the user had closed.
`NavStack<R>` packages that rule for the common case, an app made of screens. It is a plain value the app keeps in its model, never seen by the runtime: `push`, `pop`, `replace` (the sideways move back should not retrace), `pop_to_root`, `reset`, `current`, `current_mut`, `iter`, `depth`, `can_pop`, and `back( msg )`, which answers `on_back` directly. It is stored as a root plus the routes above it rather than as a `Vec`, so "never empty" holds by construction, including after deserialising an old or damaged state file, and it derives serde whenever `R` does. Six unit tests cover it, a serde round trip among them, and its rustdoc example is a complete `App` where one object answers `on_back`, `save_state` and `restore_state`.
Documentation, since the point is for app authors to take navigation state into account. The new `docs/navigation.md` is the reference: transient UI versus navigation state and the test that separates them, the back order and the reason for each step, why the answer is derived, how it lines up with session state, `NavStack`, apps that embed a web view (whose page history is navigation state too, to be persisted with ltk-webkit's `session_state`), hosts with a foreign back stack, the root, who owns which stack, and an author's checklist. `architecture.md` gains a group in the trait surface and a paragraph in the state patterns, `onboarding.md` and the README index link to it, and in the cookbook the multi-screen recipe now uses `NavStack` with `on_back` and persists the stack without serde, which keeps it compiled by the Markdown doctest harness (that harness links `ltk` alone, so the serde example in `navigation.md` is marked `ignore` and compiled as `NavStack`'s own doctest instead). `BackResponse`, `BackAtRoot` and `NavStack` are exported at the crate root and in the `ltk::window` facade.
Behaviour change for existing apps, also recorded under Changed: a window app that implements neither `on_back` nor the key is now minimised on back at its root, where nothing happened before; `BackAtRoot::Ignore` restores that. 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.
Renew the text-input enable on a press. Moving the focus between two text fields, or pressing the one already focused, only committed the refreshed content type on the existing `zwp_text_input_v3`. `activate_text_input` 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, which is what back now does, has no other way to learn that the user wants it again: the text-input never stopped being active, so there was nothing for it to observe. ltk sends neither surrounding text nor a cursor rectangle, so the only state `enable` resets is the content type set right after it.
Fit an oversized `window_size_hint` to the screen. 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; rustdroid carried a dead branch that tried to avoid this from a screen size nothing could give it. The first configure is the earliest point the hint can be checked, and the protocol has the right input for it: `xdg_toplevel.configure_bounds`, exposed by sctk as `WindowConfigure::suggested_bounds` and ignored until now. The new `event_loop/window_fit.rs` holds `fit_within`, which scales a size down keeping its aspect ratio until it fits and leaves one that fits untouched (four unit tests), and `AppData::fit_size_hint`, run once behind the new `pending_size_hint_fit` latch: it uses the compositor's bounds when sent and otherwise 90 % of the first output's logical size, since panels and decorations are unknown from the client side. When the hint has to shrink, `min_size` (and `max_size` for a non-resizable window) is re-pinned to the fitted size, which is kept in `fitted_size_hint` and replaces the app's hint as the fallback for later configures that carry no size; without that the window would grow back on the next one. On that one configure the fitted size wins over the compositor's `new_size`, which was derived from the oversized pin. `App::window_size_hint` documents the rule with the worked example (720x1440 opens as 486x972 on a 1080p output).
The back order needs a live Wayland connection to exercise and has no test; the unit tests cover `NavStack` and `fit_within`.
2026-09-20 23:23:26 +02:00
admin 5b8ae35830 [ltk — src/input/gesture/mod.rs, src/input/gesture/tests.rs, src/widget/handlers.rs, src/input/keyboard/dispatch.rs, src/render/image.rs, docs/architecture.md, CHANGELOG.md] Keep a tap on a button while the finger rolls inside it, treat XF86Back like Escape, and convert ARGB8888 by whole pixels
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
Any travel past the slop turned a press into a vertical or horizontal drag and its release skipped the button, so a thumb that rolled on a large key lost the tap. `GestureState::on_move` now stays idle while the pressed widget fires on press (the new `WidgetHandlers::has_press`) and the finger is still inside its rect; the swipe machinery takes over from the original press point once the finger leaves. Presses inside a scroll viewport are unaffected, since a list of full-width rows is scrolled by dragging on them. Two tests cover the tap that survives motion inside the button and the swipe that arms once the finger leaves it; the architecture notes and the changelog describe the rule.
The compositor now sends XF86Back for the Android back gesture, an inward swipe from either side edge. With a text input focused that keysym fell into `handle_key_default` and was swallowed, so an app never saw it. `XF86Back` now dispatches through `handle_key_escape`, so every ltk app gets the back gesture with Escape's layering: open popups, then the context menu, then a selection, then a dialog's cancel, and finally the focus drop and `on_key_with_modifiers` with the keysym, where an app can act on it.
`SoftwareCanvas::write_to_wayland_buf` swapped red and blue with a byte-by-byte loop, unrolled by hand over sixteen bytes. It now reads each pixel as a `u32`, exchanges the two channels with masks and shifts, and leaves the vectorisation to the compiler. Compositors that offer `ABGR8888` never took this path.
2026-09-14 10:57:10 +02:00
admin 4aa3480b64 refactor: split every monolithic module into focused submodules
Each source file that had grown beyond a single concern is replaced by an identically-named directory containing focused submodules. `src/event_loop/mod.rs` (878 lines) becomes a directory with clipboard, context_menu, cursor_shape, drag, focus, handlers, invalidation, overlays_reconcile, repeat, run, surface, text_editing, and tooltip. Every widget, input handler, and theme component follows the same split. Public interfaces are unchanged — only the internal file layout moves.
image bumped from 0.25.2 to 0.25.9.
2026-05-15 23:46:56 +02:00