feat(wayland): text input through zwp_text_input_v3

A window can now say that a text field has the focus and where its caret
is — Window::SetTextInput(active, caret rectangle in physical pixels,
purpose) — which is what opens the on-screen keyboard on a phone (Maliit
under KWin, squeekboard under phoc). What the input method produces comes
back as events: committed text as onTextInput, the composing text as
onPreedit (empty clears it), deletions around the cursor as
onTextInputDelete; done() applies them in the protocol's order. The state
a window wants is remembered and sent again whenever the keyboard focus
returns to it. The protocol's client code is generated with wayland-scanner
1.26.0 from wayland-protocols' text-input-unstable-v3.xml, under lib/ like
the others.
This commit is contained in:
Jorijn van der Graaf 2026-10-18 01:50:39 +02:00
commit 678f38964f
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
8 changed files with 1355 additions and 0 deletions

View file

@ -12,6 +12,7 @@ module;
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h"
#include "../lib/text-input-unstable-v3-client-protocol.h"
#include <xkbcommon/xkbcommon.h>
#endif
export module Crafter.Graphics:Device;
@ -76,6 +77,22 @@ export namespace Crafter {
// whole detent yet is carried here so smooth-scroll devices add up
// across events. Reset on pointer leave.
inline static double scrollDetentRemainder = 0.0;
// zwp_text_input_v3: one object per seat; the compositor says which
// surface it follows (enter/leave), the events are state that done()
// applies as a whole.
inline static zwp_text_input_manager_v3* textInputManager = nullptr;
inline static zwp_text_input_v3* textInput = nullptr;
inline static Window* textInputWindow = nullptr;
inline static std::string textInputPreedit;
inline static std::string textInputShownPreedit; // what the window was last told
inline static std::string textInputCommit;
inline static std::uint32_t textInputDeleteBefore = 0;
inline static std::uint32_t textInputDeleteAfter = 0;
inline static bool textInputHasPreedit = false;
inline static bool textInputHasCommit = false;
inline static bool textInputHasDelete = false;
// Sends the window's wanted text-input state when it has the focus.
static void ApplyTextInput(Window* window);
static void seat_handle_capabilities(void* data, wl_seat* seat, uint32_t capabilities);
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
@ -98,6 +115,12 @@ export namespace Crafter {
static void touch_motion(void* data, wl_touch* touch, std::uint32_t time, std::int32_t id, wl_fixed_t x, wl_fixed_t y);
static void touch_frame(void* data, wl_touch* touch);
static void touch_cancel(void* data, wl_touch* touch);
static void text_input_enter(void* data, zwp_text_input_v3* textInput, wl_surface* surface);
static void text_input_leave(void* data, zwp_text_input_v3* textInput, wl_surface* surface);
static void text_input_preedit(void* data, zwp_text_input_v3* textInput, const char* text, std::int32_t cursorBegin, std::int32_t cursorEnd);
static void text_input_commit(void* data, zwp_text_input_v3* textInput, const char* text);
static void text_input_delete(void* data, zwp_text_input_v3* textInput, std::uint32_t before, std::uint32_t after);
static void text_input_done(void* data, zwp_text_input_v3* textInput, std::uint32_t serial);
constexpr static wl_pointer_listener pointer_listener = {
.enter = PointerListenerHandleEnter,
@ -113,6 +136,14 @@ export namespace Crafter {
.frame = touch_frame,
.cancel = touch_cancel,
};
constexpr static zwp_text_input_v3_listener text_input_listener = {
.enter = text_input_enter,
.leave = text_input_leave,
.preedit_string = text_input_preedit,
.commit_string = text_input_commit,
.delete_surrounding_text = text_input_delete,
.done = text_input_done,
};
constexpr static wl_keyboard_listener keyboard_listener = {
.keymap = keyboard_keymap,
.enter = keyboard_enter,

View file

@ -25,6 +25,15 @@ export namespace Crafter {
Vector<float, 2> position;
};
// The input method asks for bytes around the cursor to go (text-input-v3
// counts UTF-8 bytes).
struct TextDelete {
std::uint32_t beforeBytes;
std::uint32_t afterBytes;
};
enum class TextInputPurpose : std::uint8_t { Normal, Phone };
struct ScaleData2D {
Vector<std::int16_t, 2, 2> position;
Vector<std::int16_t, 2, 2> size;

View file

@ -13,6 +13,7 @@ module;
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h"
#include "../lib/text-input-unstable-v3-client-protocol.h"
#include <string.h>
#include <linux/input.h>
#include <sys/mman.h>
@ -106,6 +107,11 @@ export namespace Crafter {
Event<TouchPoint> onTouchUp;
Event<void> onTouchCancel;
std::int32_t primaryTouchId = -1;
// Text input (zwp_text_input_v3): the input method's composing text
// (an empty string clears it) and its deletions around the cursor;
// the text it commits arrives as onTextInput, like a key's.
Event<const std::string_view> onPreedit;
Event<TextDelete> onTextInputDelete;
Vector<float, 2> currentMousePos;
Vector<float, 2> lastMousePos;
Vector<float, 2> mouseDelta;
@ -151,6 +157,11 @@ export namespace Crafter {
void StartUpdate();
void StopUpdate();
void SetTitle(const std::string_view title);
// Tells the compositor's input method that a text field has focus
// (an on-screen keyboard opens) and where its caret is, in physical
// pixels, or that none has. Remembered: it is applied again whenever
// the keyboard focus comes back to this window.
void SetTextInput(bool active, std::int32_t x = 0, std::int32_t y = 0, std::int32_t width = 0, std::int32_t height = 0, TextInputPurpose purpose = TextInputPurpose::Normal);
void Resize(std::uint32_t width, std::uint32_t height);
void Render();
void Update();
@ -219,6 +230,9 @@ export namespace Crafter {
// Needed so `SetCursorImage` can re-issue `wl_pointer_set_cursor`
// mid-session (the hotspot only updates when set_cursor is recalled).
std::uint32_t lastPointerSerial_ = 0;
bool textInputActive_ = false;
std::int32_t textInputRect_[4] = { 0, 0, 0, 0 };
TextInputPurpose textInputPurpose_ = TextInputPurpose::Normal;
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
static void wl_surface_frame_done(void *data, wl_callback *cb, uint32_t time);