External text input for fields ltk does not own; TextEdit::color and TextEdit::on_focus
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s

Two additions driven by apps that host another UI inside an ltk surface: trust, whose pages have their own form fields, and rustdroid, which projects Android views into ltk widgets.
External text input. ltk only ever enabled `zwp_text_input_v3` for its own `TextEdit`s, so nothing told the compositor that an editable inside an embedded web view had the focus: the on-screen keyboard never came up inside a page, which makes a browser unusable on a phone without a hardware keyboard. Three hooks on `App` close that. `external_text_input() -> Option<u64>` asks for the input method on behalf of such a field: while it returns `Some( serial )` and no ltk text widget holds the focus, the runtime keeps a text-input enabled, and the compositor raises its keyboard. `on_external_text_commit( text )` and `on_external_text_delete( before, after )` receive what the input method sends, the latter in UTF-8 bytes as the protocol counts them. Keys such as Backspace and Enter keep arriving as key events, which hosts already forward.
The request is a serial rather than a `bool` so that it can be renewed. A compositor that folds its keyboard while the field keeps the focus, which forge does on back, has nothing to observe when the user taps that same field again, because the text-input never stopped being active; a change of serial makes the runtime send `enable` again through `reenable_text_input`, the same signal ltk now sends for its own fields. The host chooses what bumps it; trust counts taps on the page, so dragging to scroll does not bring a folded keyboard back.
`AppData::sync_external_text_input`, in `text_editing/ime.rs`, is polled after every batch of `update`s next to `sync_fullscreen`. An ltk text widget always wins: while one has the focus the function only clears its record, and `set_focus` manages the text-input as before. When that widget loses the focus `set_focus` destroys the text-input, and the next poll finds the request still standing with no text-input and enables a new one, so the two owners hand it back and forth without either knowing about the other. The `zwp_text_input_v3` dispatch routes `commit_string` and `delete_surrounding_text` to the app only when the external request is held and no ltk text field is focused; that second check covers the one iteration between a widget taking the focus and the next poll. The empty-string filter on `commit_string` is folded into the `Option` it already lived in.
`TextEdit::color( c )`. The colour of a field's value always came from the theme. A field placed over a surface the theme knows nothing about, an Android `EditText` with its own text colour on its own background, could end up unreadable. `color` sets the value's colour in both draw paths, single-line and multiline; the placeholder keeps the theme's, and a field without it draws exactly as before.
`TextEdit::on_focus( msg )`. `App::on_text_input_focused( bool )` says that some text field is focused, not which. An app that mirrors focus into another model needs to know which: rustdroid keeps Android's single focused view in step, and could only learn the field from its first `on_change`, so `View.isFocused()` stayed false until the user typed. A `WidgetId` cannot carry that, being a `&'static str` while such handles are created at run time. A per-widget message can, since the app builds it and puts in it whatever it needs. It rides the `WidgetHandlers::TextEdit` snapshot, and `set_focus` pushes it when the focus actually moves to the field, from a press, Tab or `App::take_focus_request`, and not again for a press on the field that already holds it. Every other match on that handler variant uses `..`, so the new field touches its definition, its clone and the snapshot in `element.rs`.
Documented in the `App` rustdoc, the trait surface list of `docs/architecture.md`, the `text_edit` entry of `docs/widgets.md` and the changelog. Neither path has a test: both need a live Wayland connection and a compositor to exercise.
This commit is contained in:
2026-09-21 12:45:09 +02:00
parent 96a9e88051
commit 2af281520b
13 changed files with 95 additions and 11 deletions
+2
View File
@@ -6,6 +6,8 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
### Added
- **`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`.
- **`App::back_at_root()` / `BackAtRoot`** — what back does with nothing left to go back to: `Minimize` (default; `xdg_toplevel.set_minimized`, the app and its state stay alive), `Close` (through `on_close_requested`) or `Ignore`. Only `ShellMode::Window` surfaces react; layer-shell and session-lock surfaces never do.
- **`NavStack<R>`** — navigation state for an app made of screens, kept in the app's model: `push` / `pop` / `replace` / `pop_to_root` / `reset` / `current` / `iter`, never empty, serialisable whenever `R` is. `back( msg )` answers `on_back`, and persisting the same value from `save_state` makes back retrace the same screens after a session restore.