An on-screen keyboard of ltk's own, driven by layout files; TextEdit::purpose for the row a terminal needs; the wheel scrolls a multiline field; a notebook page fills its height
Text entry on a phone comes from `zwp_text_input_v3`: a focused field enables the protocol and the compositor's input method, squeekboard today, draws a keyboard and commits text through it. Eydos wants to drop that process, so the keyboard has to come from somewhere else, and the natural place is the toolkit that already draws everything else and knows which field has the focus and what it is for. This commit puts one in ltk, as data files plus a panel any host can drive: ltk's runtime shows it under a focused text field when no input method is there, and forge will draw the same panel in its chrome and deliver the keys to the focused client, which is the second half and is not in this commit. Along the way the demo that exercises it exposed two gaps in the widgets, fixed here too.
Layouts are data. `keyboards/<language>.json`, one file per layout, searched in `LTK_KEYBOARDS_DIR` when set, then `$XDG_DATA_HOME/ltk/keyboards` or `~/.local/share/ltk/keyboards`, then `/usr/share/ltk/keyboards`; `osk::layout::search_paths()`, `available()` and `load( name )` are the API, and the English layout is compiled in as the last resort. A file has `name`, a `label` for the layout-switch key, `layers` and `variants`. A layer is a list of rows; a row a list of keys; a key is a bare string for the text it types, or an object with an `action` (`shift`, `backspace`, `enter`, `space`, `layer`, `layout`, `hide`), the target `layer`, a `label`, and a `width` in key units, `1` by default, which share the row's width so a row of nine keys comes out centred under one of ten. `base` is mandatory; a missing `shift` layer is derived from it by uppercasing. `variants` maps a key's text to what a long press offers, and the shifted form of a key uses its base's, uppercased. A key may also be a named key, `{ "key": "Escape" }`, `Tab`, the arrows, `Home`, `End`, `Page_Up`, `Page_Down`, `Delete`, `Insert`, `F1` to `F12`, or a sticky modifier, `{ "modifier": "ctrl" }` or `alt`. `en.json` and `es.json` ship, with symbol layers and the accented variants each language needs. The files go into the registry copy through `make install` and into `ltk-theme-default` under `/usr/share/ltk/keyboards`.
The terminal row. A terminal needs Escape, Tab, Ctrl, Alt, the arrows and the shell's symbols, and no other application does, so those are not a layout but a row laid above any language's layout: `keyboards/bar-terminal.json`, made of the named keys and modifiers above plus `-`, `/`, `|`, `\` and `~` with their variants. Files named `bar-*.json` are rows, not layouts, and `available()` leaves them out, so the layout-switch key does not cycle onto one; the first version listed it and a tap on that key made the keyboard vanish behind a duplicated bar. Which applications get the row is decided by what the field says it is for: `TextEdit::purpose( InputPurpose::Terminal )`, snapshotted into the handlers like `secure`, which the built-in panel reads through `set_purpose` and which also goes out as the `terminal` content purpose of `zwp_text_input_v3`, the same one VTE-based terminals declare, so a compositor hosting the panel can pick the row from the protocol; for a client that declares nothing it can decide by `app_id`.
`osk::Panel` is a state machine a host drives. It holds the loaded layouts, the current layer, shift as off, one-shot or locked, the sticky modifiers, the purpose and its extra row, the geometry of all of it inside the rectangle the host gave it with `set_rect( rect, scale )`, and the press in flight. The host feeds it `press_down`, `press_move`, `press_up` and, from its own timer, `tick`, each returning an `Event`: `Text`, `Backspace`, `Enter`, `Hide`, `Key( keysym, modifiers )` for what text cannot carry, an arrow, Escape, Tab, or a character while Ctrl or Alt is held, or `Changed` when only the panel's look changed. A key types on release; a press held 450 ms opens its variants, which the finger slides onto and the release picks; backspace fires on press and repeats every 60 ms after the same delay; shift is one-shot and locks on a double tap within 350 ms; a modifier stays down for the next key and a character typed under it goes out as a `Key`, since text cannot carry Ctrl. `next_deadline()` says when `tick` next has work, `height( scale )` what the panel needs, `press_down` answers `None` for a press outside it so the host treats that as its own. `draw( canvas )` paints it with the active palette; the action keys show theme icons rather than glyphs, `keyboard/shift`, `keyboard/backspace`, `keyboard/enter`, `general/down-simple` for hide, `accessibility/languages` for the layout key, the `general` arrows for the terminal row, rasterised once per size and tint, because the first version drew `⇧` and `🌐` and the fallback chain has no glyph for the latter, which showed as a question mark; a layout key with no label of its own shows the active layout's. Nothing in `Panel` knows about text fields or Wayland, which is what lets a compositor host it. Fifteen tests cover the layout parser, the derived shift layer, variants following case, the bar filtering, and the panel's taps, one-shot and locked shift, variants, repeat, layer and layout switches and the terminal row.
The runtime shows it. `event_loop/osk.rs` puts a `Panel` on a surface when a text field takes the focus and `builtin_osk` holds: the compositor advertised no `zwp_text_input_manager_v3`, or `LTK_BUILTIN_OSK=1` is set, which is how to try it on a desktop that has an input method, or the app returns `true` from the new `App::builtin_keyboard()`. The panel is placed at the bottom of the surface by both renderers, and the widget tree is laid out in what is left above it, so the focused field is never covered. Presses, motion and releases from the pointer and from touch are offered to the panel before the gesture machine, a press on the already focused field brings a hidden panel back, and the run loop's timeout now takes the minimum of the long-press, tooltip and panel wakeups so variants and repeat fire on time. `Text` goes through the same insertion as a physical key, `Key` through the same dispatch, with Ctrl applied for its duration, `Enter` submits or inserts a newline in a multiline field, `Hide` keeps the focus and takes the panel away; losing the focus removes it. The layout is picked from `$LANG`. `ltk::InputPurpose` is exported, and `docs/architecture.md` gains the section that says all of this, with the compositor's part sketched.
The demo. `examples/inputs` becomes a notebook with the login page it had and a Shell page: one multiline field that holds a transcript, prompt included, declared a terminal, so the row appears; Enter runs the line under the last prompt (`echo`, `ls`, `pwd`, `clear`), the arrows move through the transcript, and a Ctrl combination interrupts the line the way a terminal shows it. Escape quits only from the login page, because a key the app answers never reaches the field, which is what first made the arrows dead.
Two widget gaps the demo found. `Notebook` gave its page the page's preferred height, so a page that fills its space, a scrolled list or a flex, got none and showed nothing: the active page is now wrapped in `flex` and gets the height under the strip. And the multiline field had no way to scroll: it chose its first visible line from the cursor alone on every draw, so the transcript ran under the keyboard and the wheel did nothing. `text_edit/scroll.rs` gives it a `TextScroll` the runtime keeps per field beside the cursor and the selection, in the frame state, the draw context and the `core` runtime alike: the first line asked for by hand and the cursor at that moment. The view holds there while the cursor stays put and, once it moves, follows it by the least it has to, as an editor does; without a request the result is exactly the old rule. `Window::resolve` computes the visible lines once for drawing, hit testing and the wheel, so they cannot disagree, and replaces the copy each of the first two had. In `on_pointer_axis` the vertical motion is offered to the multiline field under the pointer first, three lines per notch from `value120` or `discrete`, touchpad pixels accumulated until they add up to a line, and only when the field cannot move that way does it go to a `scroll` around it. `TextScroll` is public but opaque, since `Element::draw` is public and needs it; `Default` is the cursor-following behaviour. Six tests cover the hold, the follow, the clamping, the notch and the accumulation. A read-only field is not interactive and does not take the wheel; touch drags inside a field do not scroll it yet.
Not done. Forge does not host the panel yet, so on the phone squeekboard is still what appears. The panel's look is a first pass, sized in logical units with the theme's palette, to be restyled from mockups. Only `en` and `es` ship.
This commit is contained in:
@@ -37,6 +37,7 @@ Where things live under `src/`, one line each:
|
||||
- `gles_render/` — GPU backend (EGL + GLES2 / GLES3).
|
||||
- `input/` — pointer, keyboard and touch handling, the gesture machine, dispatch.
|
||||
- `layout/` — composable arrangers for `Element` trees.
|
||||
- `osk/` — the on-screen keyboard: layouts from JSON files, a host-driven `Panel`, and its drawing. See [On-screen keyboard](#on-screen-keyboard).
|
||||
- `protocol/` — in-tree `wayland-scanner` bindings for protocols `wayland-protocols` does not generate yet (`xdg-session-management-v1`).
|
||||
- `render/` — software rendering surface used by every widget.
|
||||
- `secure_mem.rs` — volatile wipe of secret buffers behind `TextEdit`'s secure mode.
|
||||
@@ -111,6 +112,7 @@ In practice, that model is easiest to adopt in three steps:
|
||||
- `take_focus_request()` → `Option<WidgetId>` — pull-once focus retargeting.
|
||||
- `on_text_input_focused(active)` — surface IME state.
|
||||
- `external_text_input()` → `Option<u64>` with `on_external_text_commit` / `on_external_text_delete` — hold the input method for a text field ltk does not own (an editable in an embedded web view); a new serial renews the request.
|
||||
- `builtin_keyboard()` → `bool` — draw ltk's own on-screen keyboard under a focused text field even when the compositor offers an input method. See [On-screen keyboard](#on-screen-keyboard).
|
||||
|
||||
**Implement for window / toplevel lifecycle**
|
||||
|
||||
@@ -160,6 +162,20 @@ Separate from overlays, `subsurfaces()` returns `SubsurfaceSpec` entries: input-
|
||||
If your application does not need overlays or layer-shell, you can ignore this
|
||||
entire section and stay in the `ltk::window` subset.
|
||||
|
||||
## On-screen keyboard
|
||||
|
||||
Text entry on a phone comes from `zwp_text_input_v3`: a focused `TextEdit` enables the protocol and the compositor's input method, a separate process such as squeekboard, draws a keyboard and commits text through it. ltk carries a keyboard of its own for the cases where that chain is not there, and for compositors that would rather draw it themselves. Everything lives in `ltk::osk`.
|
||||
|
||||
**Layouts are data.** A layout is one JSON file per language, `keyboards/es.json`, searched in `LTK_KEYBOARDS_DIR` (when set), `$XDG_DATA_HOME/ltk/keyboards` (or `~/.local/share/ltk/keyboards`) and `/usr/share/ltk/keyboards`, highest priority first; `ltk::osk::layout::search_paths()` returns the list, `available()` the names found, `load( name )` one layout. The English layout is compiled in as the last resort. A file has `name`, an optional `label` for the layout-switch key, `layers` and `variants`. A layer is a list of rows; a row is a list of keys; a key is a bare string for the text it types, or an object with `action` (`shift`, `backspace`, `enter`, `space`, `layer`, `layout`, `hide`), `layer` for the target of `layer`, `label`, and `width` in key units, `1` by default, which share the row's width. The `base` layer is mandatory; a missing `shift` layer is derived from `base` by uppercasing. `variants` maps a key's text to what a long press offers; the shifted form of a key uses its base's variants uppercased.
|
||||
|
||||
A key may also be a named key, `{ "key": "Escape" }`, `Tab`, the arrows, `Home`, `End`, `Page_Up`, `Page_Down`, `Delete`, `Insert` and `F1` to `F12`, or a sticky modifier, `{ "modifier": "ctrl" }` or `alt`. Those are what `keyboards/bar-terminal.json` is made of: the row a terminal needs, independent of the language, which the panel lays above the language's layout when its `Purpose` is `Terminal`. A `bar-*.json` file is a row, not a layout, and `available()` leaves it out of the layouts the layout-switch key cycles through.
|
||||
|
||||
**`Panel` is a state machine a host drives.** It holds the loaded layouts, the current layer, one-shot or locked shift, the sticky modifiers, the purpose and its extra row, the geometry of all of it inside the rectangle the host gave it with `set_rect( rect, scale )`, and the press in flight. The host feeds it `press_down`, `press_move`, `press_up` and, from its own timer, `tick`, each returning an `Event` the host acts on: `Text`, `Backspace`, `Enter`, `Hide`, `Key( keysym, modifiers )` for what text cannot carry, an arrow, Escape, Tab, or a character while Ctrl or Alt is held, or `Changed` when only the panel's own look changed. `set_purpose` switches the extra row; the runtime derives the purpose from the focused field, and a compositor takes it from the `content_type` a client sets on `zwp_text_input_v3`, which has a `terminal` purpose that terminal emulators built on VTE declare; for one that does not, the compositor can decide by `app_id`. `next_deadline()` says when `tick` next has work: the variants popup opens 450 ms into a press, backspace repeats after the same delay. `height( scale )` is what the panel needs for its current layer; `draw( canvas )` paints it with the active palette. Nothing in `Panel` knows about text fields or Wayland, which is what lets a compositor host it.
|
||||
|
||||
**The runtime shows it under a focused text field** when the compositor advertises no `zwp_text_input_manager_v3`, when `LTK_BUILTIN_OSK=1` is set (useful to try it on a desktop that has an input method), or when the app returns `true` from `builtin_keyboard()`. The panel sits at the bottom of the surface; the widget tree is laid out in the space above it, so the focused field is never covered, and presses inside it never reach the gesture machine. `Text` goes through the same insertion path as a physical key, `Key` through the same dispatch as one, with Ctrl applied for its duration, `Enter` submits the field or inserts a newline in a multiline one, `Hide` keeps the focus and takes the panel away until the field is tapped again; losing the focus removes it.
|
||||
|
||||
**A compositor** builds a `Panel` for its own chrome, draws it with a `UiSurface` canvas, and turns the events into `zwp_text_input_v3` commits and deletes for `Text`, and into key events on its seat for `Key`, which is the only way an arrow or a Ctrl combination reaches a client, while it shrinks the focused toplevel by the panel's height. Forge is meant to be that host.
|
||||
|
||||
## Theming
|
||||
|
||||
`ltk::theme` exposes a process-wide active theme. Three layers:
|
||||
|
||||
+3
-1
@@ -412,7 +412,7 @@ A text input with cursor, Backspace, Enter / Submit, clipboard
|
||||
support, a `secure( true )` password mode that masks the visible
|
||||
characters and zeroizes the buffer on drop, and a
|
||||
`password_toggle( visible, on_toggle )` builder that pins a show /
|
||||
hide-password eye icon to the right edge of the field. Single-line by default; `multiline( true )` switches to a multi-row editor whose visible height follows `rows( n )`.
|
||||
hide-password eye icon to the right edge of the field. Single-line by default; `multiline( true )` switches to a multi-row editor whose visible height follows `rows( n )`. A multiline field whose text overflows scrolls with the wheel or a touchpad, three lines per notch, and takes the motion ahead of any `scroll` around it while it can still move that way; once the cursor moves again the view follows it, by the least it has to.
|
||||
|
||||
**When**: login fields, chat inputs, search boxes.
|
||||
|
||||
@@ -446,6 +446,8 @@ of `secure( true )` regardless of the current visibility, so
|
||||
flipping the eye does not weaken the field's threat model at
|
||||
runtime — only what the user sees on screen changes.
|
||||
|
||||
`purpose( InputPurpose::Terminal )` declares what the field is for: the compositor's input method is told through the text-input content type, and ltk's own on-screen keyboard, when it is the one shown, adds its row of Escape, Tab, Ctrl, Alt and arrows above the layout. The `inputs` example's Shell page is the reference; see [On-screen keyboard](./architecture.md#on-screen-keyboard).
|
||||
|
||||
**See also**: the password recipe in
|
||||
[`docs/cookbook.md`](./cookbook.md#password-field-with-pam-submit).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user