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:
parent
aed6142cce
commit
678f38964f
8 changed files with 1355 additions and 0 deletions
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue