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/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h" #include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h" #include "../lib/viewporter.h"
#include "../lib/text-input-unstable-v3-client-protocol.h"
#include <wayland-client.h> #include <wayland-client.h>
#include <wayland-client-protocol.h> #include <wayland-client-protocol.h>
#include <stdio.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) { if (dataDeviceManager != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat); 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) { } 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)); compositor = reinterpret_cast<wl_compositor*>(wl_registry_bind(registry, name, &wl_compositor_interface, 3));
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) { } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
@ -324,8 +329,83 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name
if (seat != nullptr && dataDevice == nullptr) { if (seat != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat); 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) { 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/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h" #include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h" #include "../lib/viewporter.h"
#include "../lib/text-input-unstable-v3-client-protocol.h"
#include <string.h> #include <string.h>
#include <cassert> #include <cassert>
#include <linux/input.h> #include <linux/input.h>
@ -561,6 +562,7 @@ Window::~Window() {
Device::touchWindow = nullptr; Device::touchWindow = nullptr;
Device::touchPositions.clear(); Device::touchPositions.clear();
} }
if (Device::textInputWindow == this) Device::textInputWindow = nullptr;
#endif #endif
} }
#endif #endif
@ -618,6 +620,18 @@ void Window::SetTitle(const std::string_view title) {
#endif #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, void Window::SetCursorImage(std::uint16_t width, std::uint16_t height,
std::uint16_t hotspotX, std::uint16_t hotspotY, std::uint16_t hotspotX, std::uint16_t hotspotY,
const std::uint8_t* pixels) { const std::uint8_t* pixels) {

View file

@ -12,6 +12,7 @@ module;
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h" #include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h" #include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h" #include "../lib/viewporter.h"
#include "../lib/text-input-unstable-v3-client-protocol.h"
#include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon.h>
#endif #endif
export module Crafter.Graphics:Device; export module Crafter.Graphics:Device;
@ -76,6 +77,22 @@ export namespace Crafter {
// whole detent yet is carried here so smooth-scroll devices add up // whole detent yet is carried here so smooth-scroll devices add up
// across events. Reset on pointer leave. // across events. Reset on pointer leave.
inline static double scrollDetentRemainder = 0.0; 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 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); 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_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_frame(void* data, wl_touch* touch);
static void touch_cancel(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 = { constexpr static wl_pointer_listener pointer_listener = {
.enter = PointerListenerHandleEnter, .enter = PointerListenerHandleEnter,
@ -113,6 +136,14 @@ export namespace Crafter {
.frame = touch_frame, .frame = touch_frame,
.cancel = touch_cancel, .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 = { constexpr static wl_keyboard_listener keyboard_listener = {
.keymap = keyboard_keymap, .keymap = keyboard_keymap,
.enter = keyboard_enter, .enter = keyboard_enter,

View file

@ -25,6 +25,15 @@ export namespace Crafter {
Vector<float, 2> position; 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 { struct ScaleData2D {
Vector<std::int16_t, 2, 2> position; Vector<std::int16_t, 2, 2> position;
Vector<std::int16_t, 2, 2> size; 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/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h" #include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h" #include "../lib/viewporter.h"
#include "../lib/text-input-unstable-v3-client-protocol.h"
#include <string.h> #include <string.h>
#include <linux/input.h> #include <linux/input.h>
#include <sys/mman.h> #include <sys/mman.h>
@ -106,6 +107,11 @@ export namespace Crafter {
Event<TouchPoint> onTouchUp; Event<TouchPoint> onTouchUp;
Event<void> onTouchCancel; Event<void> onTouchCancel;
std::int32_t primaryTouchId = -1; 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> currentMousePos;
Vector<float, 2> lastMousePos; Vector<float, 2> lastMousePos;
Vector<float, 2> mouseDelta; Vector<float, 2> mouseDelta;
@ -151,6 +157,11 @@ export namespace Crafter {
void StartUpdate(); void StartUpdate();
void StopUpdate(); void StopUpdate();
void SetTitle(const std::string_view title); 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 Resize(std::uint32_t width, std::uint32_t height);
void Render(); void Render();
void Update(); void Update();
@ -219,6 +230,9 @@ export namespace Crafter {
// Needed so `SetCursorImage` can re-issue `wl_pointer_set_cursor` // Needed so `SetCursorImage` can re-issue `wl_pointer_set_cursor`
// mid-session (the hotspot only updates when set_cursor is recalled). // mid-session (the hotspot only updates when set_cursor is recalled).
std::uint32_t lastPointerSerial_ = 0; 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 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); static void wl_surface_frame_done(void *data, wl_callback *cb, uint32_t time);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,103 @@
/* Generated by wayland-scanner 1.26.0 */
/*
* Copyright © 2012, 2013 Intel Corporation
* Copyright © 2015, 2016 Jan Arne Petersen
* Copyright © 2017, 2018 Red Hat, Inc.
* Copyright © 2018 Purism SPC
*
* Permission to use, copy, modify, distribute, and sell this
* software and its documentation for any purpose is hereby granted
* without fee, provided that the above copyright notice appear in
* all copies and that both that copyright notice and this permission
* notice appear in supporting documentation, and that the name of
* the copyright holders not be used in advertising or publicity
* pertaining to distribution of the software without specific,
* written prior permission. The copyright holders make no
* representations about the suitability of this software for any
* purpose. It is provided "as is" without express or implied
* warranty.
*
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
* SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
* THIS SOFTWARE.
*/
#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
#include "wayland-util.h"
#ifndef __has_attribute
# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
#endif
#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
#define WL_PRIVATE __attribute__ ((visibility("hidden")))
#else
#define WL_PRIVATE
#endif
extern const struct wl_interface wl_seat_interface;
extern const struct wl_interface wl_surface_interface;
extern const struct wl_interface zwp_text_input_v3_interface;
static const struct wl_interface *text_input_unstable_v3_types[] = {
NULL,
NULL,
NULL,
NULL,
&wl_surface_interface,
&wl_surface_interface,
&zwp_text_input_v3_interface,
&wl_seat_interface,
};
static const struct wl_message zwp_text_input_v3_requests[] = {
{ "destroy", "", text_input_unstable_v3_types + 0 },
{ "enable", "", text_input_unstable_v3_types + 0 },
{ "disable", "", text_input_unstable_v3_types + 0 },
{ "set_surrounding_text", "sii", text_input_unstable_v3_types + 0 },
{ "set_text_change_cause", "u", text_input_unstable_v3_types + 0 },
{ "set_content_type", "uu", text_input_unstable_v3_types + 0 },
{ "set_cursor_rectangle", "iiii", text_input_unstable_v3_types + 0 },
{ "commit", "", text_input_unstable_v3_types + 0 },
{ "set_available_actions", "2a", text_input_unstable_v3_types + 0 },
{ "show_input_panel", "2", text_input_unstable_v3_types + 0 },
{ "hide_input_panel", "2", text_input_unstable_v3_types + 0 },
};
static const struct wl_message zwp_text_input_v3_events[] = {
{ "enter", "o", text_input_unstable_v3_types + 4 },
{ "leave", "o", text_input_unstable_v3_types + 5 },
{ "preedit_string", "?sii", text_input_unstable_v3_types + 0 },
{ "commit_string", "?s", text_input_unstable_v3_types + 0 },
{ "delete_surrounding_text", "uu", text_input_unstable_v3_types + 0 },
{ "done", "u", text_input_unstable_v3_types + 0 },
{ "action", "2uu", text_input_unstable_v3_types + 0 },
{ "language", "2s", text_input_unstable_v3_types + 0 },
{ "preedit_hint", "2uuu", text_input_unstable_v3_types + 0 },
};
WL_PRIVATE const struct wl_interface zwp_text_input_v3_interface = {
"zwp_text_input_v3", 2,
11, zwp_text_input_v3_requests,
9, zwp_text_input_v3_events,
};
static const struct wl_message zwp_text_input_manager_v3_requests[] = {
{ "destroy", "", text_input_unstable_v3_types + 0 },
{ "get_text_input", "no", text_input_unstable_v3_types + 6 },
};
WL_PRIVATE const struct wl_interface zwp_text_input_manager_v3_interface = {
"zwp_text_input_manager_v3", 2,
2, zwp_text_input_manager_v3_requests,
0, NULL,
};

View file

@ -89,6 +89,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.cFiles.push_back("lib/wayland-xdg-decoration-unstable-v1-client-protocol"); cfg.cFiles.push_back("lib/wayland-xdg-decoration-unstable-v1-client-protocol");
cfg.cFiles.push_back("lib/fractional-scale-v1"); cfg.cFiles.push_back("lib/fractional-scale-v1");
cfg.cFiles.push_back("lib/viewporter"); cfg.cFiles.push_back("lib/viewporter");
cfg.cFiles.push_back("lib/text-input-unstable-v3-protocol");
} }
// Vulkan is the only renderer on native targets. Software fallback is // Vulkan is the only renderer on native targets. Software fallback is