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

@ -15,6 +15,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 <wayland-client.h>
#include <wayland-client-protocol.h>
#include <stdio.h>
@ -303,6 +304,10 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name
if (dataDeviceManager != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
}
if (textInputManager != nullptr && textInput == nullptr) {
textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat);
zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr);
}
} else if (compositor == nullptr && strcmp(interface, wl_compositor_interface.name) == 0) {
compositor = reinterpret_cast<wl_compositor*>(wl_registry_bind(registry, name, &wl_compositor_interface, 3));
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
@ -324,9 +329,84 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name
if (seat != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
}
} else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) {
textInputManager = reinterpret_cast<zwp_text_input_manager_v3*>(wl_registry_bind(registry, name, &zwp_text_input_manager_v3_interface, 1));
if (seat != nullptr && textInput == nullptr) {
textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat);
zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr);
}
}
}
void Device::text_input_enter(void*, zwp_text_input_v3*, wl_surface* surface) {
textInputWindow = nullptr;
for (Window* window : windows) {
if (window->surface == surface) textInputWindow = window;
}
if (textInputWindow != nullptr && textInputWindow->textInputActive_) ApplyTextInput(textInputWindow);
}
void Device::text_input_leave(void*, zwp_text_input_v3*, wl_surface*) {
textInputWindow = nullptr;
textInputHasPreedit = textInputHasCommit = textInputHasDelete = false;
textInputPreedit.clear();
textInputShownPreedit.clear();
}
void Device::text_input_preedit(void*, zwp_text_input_v3*, const char* text, std::int32_t, std::int32_t) {
textInputPreedit = text == nullptr ? "" : text;
textInputHasPreedit = true;
}
void Device::text_input_commit(void*, zwp_text_input_v3*, const char* text) {
textInputCommit = text == nullptr ? "" : text;
textInputHasCommit = true;
}
void Device::text_input_delete(void*, zwp_text_input_v3*, std::uint32_t before, std::uint32_t after) {
textInputDeleteBefore = before;
textInputDeleteAfter = after;
textInputHasDelete = true;
}
// The protocol's order: the deletion around the cursor, then the committed
// text at the cursor, then the composing text after it. The pending values
// reset with every done, so a round that set no preedit means an empty one;
// the window hears about it only when that differs from what it shows.
void Device::text_input_done(void*, zwp_text_input_v3*, std::uint32_t) {
Window* window = textInputWindow;
if (!textInputHasPreedit) textInputPreedit.clear();
if (window != nullptr) {
if (textInputHasDelete && (textInputDeleteBefore > 0 || textInputDeleteAfter > 0)) window->onTextInputDelete.Invoke({ textInputDeleteBefore, textInputDeleteAfter });
if (textInputHasCommit && !textInputCommit.empty()) window->onTextInput.Invoke(textInputCommit);
if (textInputPreedit != textInputShownPreedit || (textInputHasCommit && !textInputCommit.empty())) {
textInputShownPreedit = textInputPreedit;
window->onPreedit.Invoke(textInputPreedit);
}
}
textInputHasPreedit = textInputHasCommit = textInputHasDelete = false;
textInputCommit.clear();
}
void Device::ApplyTextInput(Window* window) {
if (textInput == nullptr || window == nullptr || textInputWindow != window) return;
if (window->textInputActive_) {
zwp_text_input_v3_enable(textInput);
const bool phone = window->textInputPurpose_ == TextInputPurpose::Phone;
zwp_text_input_v3_set_content_type(textInput,
phone ? ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE : (ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION | ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK | ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION),
phone ? ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE : ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL);
const float s = window->scale > 0.0f ? window->scale : 1.0f;
zwp_text_input_v3_set_cursor_rectangle(textInput,
static_cast<std::int32_t>(window->textInputRect_[0] / s), static_cast<std::int32_t>(window->textInputRect_[1] / s),
std::max(1, static_cast<std::int32_t>(window->textInputRect_[2] / s)), std::max(1, static_cast<std::int32_t>(window->textInputRect_[3] / s)));
} else {
zwp_text_input_v3_disable(textInput);
}
zwp_text_input_v3_commit(textInput);
wl_display_flush(display);
}
void Device::handle_global_remove(void* data, wl_registry* registry, uint32_t name) {
}

View file

@ -11,6 +11,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 <cassert>
#include <linux/input.h>
@ -561,6 +562,7 @@ Window::~Window() {
Device::touchWindow = nullptr;
Device::touchPositions.clear();
}
if (Device::textInputWindow == this) Device::textInputWindow = nullptr;
#endif
}
#endif
@ -618,6 +620,18 @@ void Window::SetTitle(const std::string_view title) {
#endif
}
void Window::SetTextInput(bool active, std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height, TextInputPurpose purpose) {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
textInputActive_ = active;
textInputRect_[0] = x;
textInputRect_[1] = y;
textInputRect_[2] = width;
textInputRect_[3] = height;
textInputPurpose_ = purpose;
Device::ApplyTextInput(this);
#endif
}
void Window::SetCursorImage(std::uint16_t width, std::uint16_t height,
std::uint16_t hotspotX, std::uint16_t hotspotY,
const std::uint8_t* pixels) {