From e93ec437069b2715e1906ab6930843b6042c18ce Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 17 Aug 2026 17:18:32 +0900 Subject: [PATCH] docs: the plan for a Wayland input method zwp_input_method_v2 in one new file, with its popup, drawn from the reply the engine already produced; the session picks the popup, so the engine, the IPC and the X11 side are untouched. Records what was measured, what fcitx5 does differently and why, and what was refused. Co-Authored-By: Claude Opus 5 (1M context) --- WAYLAND.md | 301 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 WAYLAND.md diff --git a/WAYLAND.md b/WAYLAND.md new file mode 100644 index 0000000..4d780e4 --- /dev/null +++ b/WAYLAND.md @@ -0,0 +1,301 @@ +# Wayland input method: design and plan + +Plan for work not yet written. Delete this file once the work has landed and +the README describes what is there. + +## 1. What is wrong today + +The popup is X11 only. `win.c` is the sole consumer of `drawc`, it connects +with `xcb_connect`, and when that fails it prints `popup disabled` and +returns (`win.c:206`). Under a Wayland session with no X server there are no +candidates at all, which makes Kanji, Hanja and Emoji unusable. Preedit still +works for clients that reach us over IBus or the GTK module. + +A second, smaller fault: libibus sends `SetCursorLocationRelative` instead of +`SetCursorLocation` when the client runs on Wayland (ibus +`client/gtk3/ibusimcontext.c:1767`). `ictab` (`ibus.c:904`) does not list it, +so libdbus answers `UnknownMethod` and the caret never arrives. + +## 2. What to implement, and what not + +Implement `zwp_input_method_v2` with `zwp_virtual_keyboard_v1`. One protocol +covers every compositor we can serve: + +| Compositor | Protocol | +| --- | --- | +| wlroots: sway, river, labwc, Wayfire, dwl | `zwp_input_method_v2` | +| smithay: niri, COSMIC | `zwp_input_method_v2` | +| KWin | `zwp_input_method_v1` only | +| GNOME | none | + +Not `zwp_input_method_v1`: it buys KWin alone, and KWin starts the input +method itself from its own configuration. + +Not GNOME: it has no input-method protocol, and our IBus endpoint writes the +address file that gnome-shell's own ibus-daemon owns. strans cannot work +there whatever we write. + +`experimental/xx-input-method` in wayland-protocols is where this is heading: +a popup positioner and a per-key consume/passthrough filter that matches our +engine exactly. No compositor implements it (checked: wlroots, smithay, KWin +trees have no trace of it). Revisit once v2 works and we have something to +say about it. + +## 3. Design + +### 3.1 One new file + +`wl.c`, about 600 lines, holding the frontend and its popup. Two protocol XML +files are vendored under `proto/` and turned into `imv2.[ch]` and `vkv1.[ch]` +by wayland-scanner at build time; only the XML is tracked. + +The popup lives in `wl.c` rather than beside `win.c` because +`get_input_popup_surface` is a request on the input-method object: the popup +surface must come from the same connection. Splitting it would mean exporting +the connection, which is a worse trade than breaking the symmetry. + +### 3.2 No drawing process + +`imhandlekey` produces the picture before it answers: + +```c + redraw(); /* strans.c:999, channbsend(drawc, &dc) */ + ... + chansend(kr->reply, &res); /* strans.c:1006 */ +``` + +So by the time a frontend has its `Keyres`, the picture it caused is already +in `drawc`, and `redraw` drains its own stale sends before pushing a new one +(`strans.c:257`), so `drawc` holds at most one, and that one is the engine's +current state. `wl.c` therefore draws inline: + +```c + sendrequest(Keypress, ks, mod, &res); + ... + if(channbrecv(drawc, &dc) > 0) + popup(&dc); +``` + +after `Keypress`, always take and draw; after the `Keycap` owner poll, draw +only when `res.eaten` says the engine is still ours, else hide. + +The consequence is the point: **one process touches libwayland**. It reads +events, dispatches them, draws, and handles `wl_buffer.release`, all in +order. No `wl_display_prepare_read` dance, no mutex, no pipe from a channel +into a poll loop. fcitx5 spends `waylandeventreader.cpp`, 144 lines of thread +plus mutex plus condition variable, on exactly this problem, because its +reader and its dispatcher are different threads. We do not have that problem +and must not invent it. + +`main.c` gains one `proccreate` and `fn.h` one line. `drawthread` is not +started under Wayland, so `drawc` keeps its single consumer. + +### 3.3 One popup per session + +```c + /* main.c */ + if(getenv("WAYLAND_DISPLAY") != nil) + proccreate(wlthread, nil, 32768); + else{ + proccreate(drawthread, nil, 16384); + if(getenv("DISPLAY") != nil) + proccreate(ximthread, nil, 32768); + } +``` + +`srvthread` and `ibusthread` start as they do now, in both cases. + +What this costs: XIM is not served under Wayland, and clients that reach us +over IBus or the GTK module while on Wayland get inline preedit with no +candidate list. Both are documented, and the answer for a user is the same +one sentence: **on Wayland, set none of the X11 variables.** GTK, Qt and +Firefox speak text-input-v3 themselves; `GTK_IM_MODULE` is what pushes them +off the path that works. Two exceptions to check while writing commit 5: +GTK 4 has been reported in 2026 not to bind `zwp_text_input_manager_v3` +unless `GTK_IM_MODULE=wayland` is set, and Chromium still defaults to +text-input-v1. + +### 3.4 Keys + +* `activate`, `deactivate` and `content_type` are pending state applied at + `done`; count `done` events, that count is the serial `commit` takes. +* Grab the keyboard on every activate, releasing the previous grab first. Two + activates can arrive without a deactivate between them when focus moves + between clients, and the second `grab_keyboard` then fails and leaves a + dead object. +* `keymap` gives an fd: compile it with `xkb_keymap_new_from_string` and hand + the same fd to `zwp_virtual_keyboard_v1.keymap`, so a key we forward means + what it meant. +* `key` gives an evdev code: `xkb_state_key_get_one_sym(state, code+8)`, then + `composekey` (shared with the XIM and IBus frontends, no new table), then + `ipckeysym` and the modifier mask to the engine. +* A key the engine did not eat goes back through + `zwp_virtual_keyboard_v1.key`. Record forwarded codes in a `u32int + sent[8]` bitmap: eat the release of a press we ate, and release whatever is + still down when the input method deactivates, or the client is left holding + a key. +* The virtual keyboard must be created on the same `wl_display` as the input + method. Nothing in the protocol says so; sway skips the grab for a virtual + keyboard whose client owns the grab (`sway/input/keyboard.c:419-431`, with + a TODO pointing at wlroots#2322), and on another connection our forwarded + keys would come straight back to us. fcitx5 relies on the same contract. +* Order within a key: `commit_string`, `set_preedit_string`, `commit(serial)`, + then `vk.key`, then one flush. Requests on one connection are processed in + order, which is what keeps a commit ahead of the key that caused it. + +We do not commit ordinary keys as text. fcitx5 does by default +(`waylandimserverbase.cpp:35-48`, unmodified printable keys become +`commit_string`), to dodge exactly the ordering question above. Our `eaten = +0` means "this key is not mine", and a key event is the honest answer to +that; committing text would lie to terminals and to anything with a +keybinding. If ordering does break on some compositor, that workaround is +known and can be added then. + +### 3.5 Popup + +* One `wl_surface` and one `zwp_input_popup_surface_v2`, created once. The + compositor makes them visible on activate and invisible on deactivate, and + places them at the text cursor: `popuparea` and `popupposition` are not + used here and stay X11-only. +* Two shm buffers, used in turn, each busy from `attach` until + `wl_buffer.release`. `win.c`'s single grow-only image would be redrawn + while the compositor was still reading it. +* `WL_SHM_FORMAT_XRGB8888` is `CAIRO_FORMAT_RGB24` on a little-endian host, + so `popupdraw` writes the buffer directly, as it does for X11. `win.c` + already checks the same assumption for the X server (`validformat`). +* Hide by attaching a nil buffer and committing. If a compositor keeps a + stale popup, destroy the surface instead, which is what fcitx5 does + (`waylandinputwindow.cpp:158-168`). +* `popuplayout` wants an area: take it from the `wl_output` the surface + entered, else the first output. +* Scale stays `popupscale` from `GDK_SCALE`, as on X11. + `wl_surface.preferred_buffer_scale` is the better answer, later. + +### 3.6 What does not change + +| | | +| --- | --- | +| `strans.c`, `Keyreq`, `Keyres`, `Drawcmd`, `ipc.*` | nothing | +| `popup_layout.c`, `font.c` | nothing | +| `ibus.c`, `srv.c`, `xim.c`, `gtk/` | nothing, beyond the one-line IBus fix | +| `dat.h` | `Ibuspurposepassword`, `Ibuspurposepin` move out of `ibus.c` as `Purposepassword`, `Purposepin`; both frontends read the same two numbers | +| `main.c` | frontend choice | +| `fn.h` | `void wlthread(void*);` | + +The engine needing no change is the argument that this is the right shape. +`Keyres.eaten` is already consume-or-pass, `Keyres.commit` is already +`commit_string`, `Keyres.preedit` is already `set_preedit_string`, and +`clientpre` is simply always true here, which leaves the popup showing +candidates and the mode mark and nothing else. + +## 4. Rejected + +**A popup per display, routed by context.** This is what fcitx5 does: +`uis_["x11:..."]` and `uis_["wayland:..."]` side by side +(`classicui.cpp:80-121`), chosen by the focus group's display name. It would +keep XIM alive under XWayland. It costs a channel in `Keyreq`, a `lastdraw` +per sink, blanking the loser on every owner change, and `font.c`'s single +Pango layout drawn from two processes. It also makes the frontends know about +each other: fcitx5 has to drop its X keyboard grab whenever a Wayland client +takes focus (`xcb/xcbconnection.cpp:443-460`). fcitx5 needs this because it +is a multi-seat, multi-display, multi-user daemon. strans is one user's. If +XIM under XWayland is ever wanted, the routing key to use is the one fcitx5 +uses, and it is about ten lines. + +**A second process for drawing, or a second connection.** See 3.2. The popup +surface cannot come from a second connection, and a second process sharing +the first buys only the locking rules we currently do not need. + +**A GNOME fallback to the X11 popup.** Machinery for a case that cannot work +anyway. + +**Hand-marshalling with `wl_proxy_marshal_flags`** instead of +wayland-scanner. Less code, unreadable. The generated files are 1448 lines +and none of them are ours to read. + +**Choosing candidates with the mouse.** fcitx5's popup takes clicks, hover, +wheel and touch. Ours eats clicks and does nothing, on X11 today; keep it +that way. + +## 5. Commit plan + +1. `ibus: SetCursorLocationRelative is a caret too` — one entry in `ictab`. + Unrelated to the rest, and wrong today. +2. `build: the input-method and virtual-keyboard protocols` — two XML files, + the scanner rules, the LICENSES note. No behaviour. +3. `wl: the input-method frontend` — activate, deactivate, content type, + done and its serial; grab per activate; keys through xkb and `composekey`; + commit and preedit; unhandled keys through the virtual keyboard, released + on deactivate. Started from `main.c` under `WAYLAND_DISPLAY`. No popup + yet, so the X11 popup still owns `drawc` and nothing collides. +4. `wl: the candidate popup` — surface, two shm buffers, drawn from `drawc` + where the reply arrives. In the same commit, `main.c` stops starting + `drawthread` and `ximthread` under Wayland, because that is where the two + popups would collide. +5. `docs: Wayland` — set nothing; no XIM; no candidates for IBus and GTK + module clients. + +Later, separately: key repeat, `preferred_buffer_scale`, reconversion through +`surrounding_text`, and a headless live test. + +## 6. Verification + +The toolchain question is already answered. In the container, wayland-scanner +output, libwayland-client, xkbcommon and Plan 9 port headers compile together +under `9c -Wall -Wextra` with no warnings, and `9l` links them with +`-lthread -lbio`: + +``` +imv2.c 133 imv2.h 955 vkv1.c 78 vkv1.h 282 (generated, not tracked) +9c -I/src $(pkg-config --cflags wayland-client xkbcommon) -Wall -Wextra -c wl.c +9l -o strans ... -lthread -lbio $(pkg-config --libs wayland-client xkbcommon) +``` + +`Dockerfile` needs `wayland` added, and `sway` (extra, 1.12) when the live +test arrives. + +Unit tests cover only what is pure: the xkb modifier mask and the forwarded +key bitmap, in the shape `xim_adapter_test` already uses, compiling `wl.c` +into the test and building a keymap from a string, no server. Everything else +is checked by running it under sway. + +A headless live test — a compositor, a text-input-v3 client, and a second +virtual keyboard to type with — is the size of `ibus_live_test.c`. Decide +after commits 3 and 4 work by hand; do not build it first. + +## 7. Known limits + +* A key we eat does not repeat. `zwp_input_method_v2`'s grab has no + compositor-side repeat unless the compositor sends `wl_keyboard`'s + `repeated` key state through it, which is opportunistic + (fcitx5 `waylandimserverv2.cpp:494-503`). Doing it ourselves is a timer + over `repeat_info`, twenty lines, later. +* Compositor keybindings run before the input method (sway + `keyboard.c:563`), so a chord like `Ctrl+N` can be taken from us. +* Text pending when the client loses focus may be lost: `deactivate` is the + first we hear of it and a `commit_string` afterwards can be dropped. +* The protocol gives one input context per seat, so we cannot tell which + application we are typing into. We keep no per-application state, so this + costs us nothing; fcitx5 tracks the focused window over a separate protocol + to work around it. + +## 8. References + +Checked at the versions current on 2026-08-17. + +* wayland-protocols 1.49: `unstable/text-input/text-input-unstable-v3.xml`, + `experimental/xx-input-method/xx-input-method-v2.xml`, + `experimental/xx-keyboard-filter/xx-keyboard-filter-v1.xml`. +* wlroots: `protocol/input-method-unstable-v2.xml`, + `protocol/virtual-keyboard-unstable-v1.xml`, the two files to vendor. +* sway `sway/input/keyboard.c:419-431`, the virtual keyboard client-identity + rule; `:563`, bindings before the grab. +* fcitx5 `a31cff7`: `frontend/waylandim/waylandimserverv2.cpp:170` serial, + `:186-193` releasing forwarded keys, `:213-218` regrabbing, + `:494-503` repeat; `waylandimserverbase.cpp:35-48` commit-as-text; + `ui/classic/classicui.cpp:80-121` a UI per display; + `ui/classic/waylandshmwindow.cpp:69-113` two buffers; + `ui/classic/waylandinputwindow.cpp:158-168` hiding; + `modules/wayland/waylandeventreader.cpp` the reader thread; + `modules/xcb/xcbconnection.cpp:443-460` the two frontends interfering. +* ibus `client/gtk3/ibusimcontext.c:1767`, the relative caret on Wayland.