From f50fd796cdb798cae130f23352da520870b33414 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Thu, 15 Dec 2022 15:02:51 +0900 Subject: [PATCH] Wayland: Support text_input_unstable_v3 and text_input_unstable_v1 They are wayland protocols to support input methods: https://cgit.freedesktop.org/wayland/wayland-protocols/tree/unstable/text-input/text-input-unstable-v3.xml https://cgit.freedesktop.org/wayland/wayland-protocols/tree/unstable/text-input/text-input-unstable-v1.xml text_input_unstable_v3 is widely supported by major desktop environment on GNU/Linux such as GNOME or KDE. text_input_unstable_v1 isn't so popular but Weston which is the reference Wayland implementation supports only it and doesn't support text_input_unstable_v3 so that we also implement it. --- include/GLFW/glfw3.h | 4 +- src/CMakeLists.txt | 6 + src/wl_init.c | 28 +++ src/wl_platform.h | 9 + src/wl_window.c | 432 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 478 insertions(+), 1 deletion(-) diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 73a0db0d..39f3751b 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -5147,6 +5147,8 @@ GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int * @remark @x11 Since over-the-spot style is used by default, you don't need * to use this function. * + * @remark @wayland This function is currently not supported. + * * @par Thread Safety * This function may only be called from the main thread. * @@ -5353,7 +5355,7 @@ GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* window, GLFWpreeditfun * For more information about the callback parameters, see the * [function pointer type](@ref GLFWimestatusfun). * - * @remark @x11 Don't support this function. The callback is not called. + * @remark @x11 @wayland Don't support this function. The callback is not called. * * @par Thread Safety * This function may only be called from the main thread. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 01f191c9..dfa06845 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -113,6 +113,12 @@ if (GLFW_BUILD_WAYLAND) wayland_generate( "${WAYLAND_PROTOCOLS_BASE}/unstable/idle-inhibit/idle-inhibit-unstable-v1.xml" "${GLFW_BINARY_DIR}/src/wayland-idle-inhibit-unstable-v1-client-protocol") + wayland_generate( + "${WAYLAND_PROTOCOLS_BASE}/unstable/text-input/text-input-unstable-v1.xml" + "${GLFW_BINARY_DIR}/src/wayland-text-input-unstable-v1-client-protocol") + wayland_generate( + "${WAYLAND_PROTOCOLS_BASE}/unstable/text-input/text-input-unstable-v3.xml" + "${GLFW_BINARY_DIR}/src/wayland-text-input-unstable-v3-client-protocol") endif() if (WIN32 AND GLFW_BUILD_SHARED_LIBRARY) diff --git a/src/wl_init.c b/src/wl_init.c index ca8440e7..5a2d64bd 100644 --- a/src/wl_init.c +++ b/src/wl_init.c @@ -48,6 +48,8 @@ #include "wayland-relative-pointer-unstable-v1-client-protocol.h" #include "wayland-pointer-constraints-unstable-v1-client-protocol.h" #include "wayland-idle-inhibit-unstable-v1-client-protocol.h" +#include "wayland-text-input-unstable-v1-client-protocol.h" +#include "wayland-text-input-unstable-v3-client-protocol.h" // NOTE: Versions of wayland-scanner prior to 1.17.91 named every global array of // wl_interface pointers 'types', making it impossible to combine several unmodified @@ -82,6 +84,14 @@ #include "wayland-idle-inhibit-unstable-v1-client-protocol-code.h" #undef types +#define types _glfw_text_input_v1_types +#include "wayland-text-input-unstable-v1-client-protocol-code.h" +#undef types + +#define types _glfw_text_input_v3_types +#include "wayland-text-input-unstable-v3-client-protocol-code.h" +#undef types + static void wmBaseHandlePing(void* userData, struct xdg_wm_base* wmBase, uint32_t serial) @@ -178,6 +188,20 @@ static void registryHandleGlobal(void* userData, &zwp_idle_inhibit_manager_v1_interface, 1); } + else if (strcmp(interface, "zwp_text_input_manager_v1") == 0) + { + _glfw.wl.textInputManagerV1 = + wl_registry_bind(registry, name, + &zwp_text_input_manager_v1_interface, + 1); + } + else if (strcmp(interface, "zwp_text_input_manager_v3") == 0) + { + _glfw.wl.textInputManagerV3 = + wl_registry_bind(registry, name, + &zwp_text_input_manager_v3_interface, + 1); + } } static void registryHandleGlobalRemove(void* userData, @@ -906,6 +930,10 @@ void _glfwTerminateWayland(void) zwp_pointer_constraints_v1_destroy(_glfw.wl.pointerConstraints); if (_glfw.wl.idleInhibitManager) zwp_idle_inhibit_manager_v1_destroy(_glfw.wl.idleInhibitManager); + if (_glfw.wl.textInputManagerV1) + zwp_text_input_manager_v1_destroy(_glfw.wl.textInputManagerV1); + if (_glfw.wl.textInputManagerV3) + zwp_text_input_manager_v3_destroy(_glfw.wl.textInputManagerV3); if (_glfw.wl.registry) wl_registry_destroy(_glfw.wl.registry); if (_glfw.wl.display) diff --git a/src/wl_platform.h b/src/wl_platform.h index 35520ba4..ee2dea02 100644 --- a/src/wl_platform.h +++ b/src/wl_platform.h @@ -412,6 +412,13 @@ typedef struct _GLFWwindowWayland _GLFWdecorationWayland top, left, right, bottom; _GLFWdecorationSideWayland focus; } decorations; + + struct zwp_text_input_v1* textInputV1; + struct zwp_text_input_v3* textInputV3; + struct { + char* preeditText; + char* commitTextOnReset; + } textInputV1Context; } _GLFWwindowWayland; // Wayland-specific global data @@ -434,6 +441,8 @@ typedef struct _GLFWlibraryWayland struct zwp_relative_pointer_manager_v1* relativePointerManager; struct zwp_pointer_constraints_v1* pointerConstraints; struct zwp_idle_inhibit_manager_v1* idleInhibitManager; + struct zwp_text_input_manager_v1* textInputManagerV1; + struct zwp_text_input_manager_v3* textInputManagerV3; _GLFWofferWayland* offers; unsigned int offerCount; diff --git a/src/wl_window.c b/src/wl_window.c index 7157d640..0ff4de75 100644 --- a/src/wl_window.c +++ b/src/wl_window.c @@ -50,6 +50,8 @@ #include "wayland-relative-pointer-unstable-v1-client-protocol.h" #include "wayland-pointer-constraints-unstable-v1-client-protocol.h" #include "wayland-idle-inhibit-unstable-v1-client-protocol.h" +#include "wayland-text-input-unstable-v1-client-protocol.h" +#include "wayland-text-input-unstable-v3-client-protocol.h" #define GLFW_BORDER_SIZE 4 #define GLFW_CAPTION_HEIGHT 24 @@ -483,6 +485,22 @@ static void releaseMonitor(_GLFWwindow* window) } } +static void activateTextInputV1(_GLFWwindow* window) +{ + if (!window->wl.textInputV1) + return; + zwp_text_input_v1_show_input_panel(window->wl.textInputV1); + zwp_text_input_v1_activate(window->wl.textInputV1, _glfw.wl.seat, window->wl.surface); +} + +static void deactivateTextInputV1(_GLFWwindow* window) +{ + if (!window->wl.textInputV1) + return; + zwp_text_input_v1_hide_input_panel(window->wl.textInputV1); + zwp_text_input_v1_deactivate(window->wl.textInputV1, _glfw.wl.seat); +} + static void xdgToplevelHandleConfigure(void* userData, struct xdg_toplevel* toplevel, int32_t width, @@ -510,6 +528,7 @@ static void xdgToplevelHandleConfigure(void* userData, break; case XDG_TOPLEVEL_STATE_ACTIVATED: window->wl.pending.activated = GLFW_TRUE; + activateTextInputV1(window); break; } } @@ -1425,6 +1444,12 @@ static void pointerHandleButton(void* userData, if (!window) return; + + // On weston, pressing the title bar will cause leave event and never emit + // enter event even though back to content area by pressing mouse button + // just after it. So activate it here explicitly. + activateTextInputV1(window); + if (button == BTN_LEFT) { switch (window->wl.decorations.focus) @@ -2006,6 +2031,376 @@ void _glfwAddDataDeviceListenerWayland(struct wl_data_device* device) wl_data_device_add_listener(device, &dataDeviceListener, NULL); } +// Callbacks for text_input_unstable_v3 protocol. +// +// This protocol is widely supported by major desktop environments such as GNOME +// or KDE. +// +static void textInputV3Enter(void* data, + struct zwp_text_input_v3* textInputV3, + struct wl_surface* surface) +{ + zwp_text_input_v3_enable(textInputV3); + zwp_text_input_v3_commit(textInputV3); +} + +static void textInputV3Reset(_GLFWwindow* window) +{ + _GLFWpreedit* preedit = &window->preedit; + + preedit->textCount = 0; + preedit->blockSizesCount = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = 0; + + _glfwInputPreedit(window); +} + +static void textInputV3Leave(void* data, + struct zwp_text_input_v3* textInputV3, + struct wl_surface* surface) +{ + _GLFWwindow* window = (_GLFWwindow*) data; + zwp_text_input_v3_disable(textInputV3); + zwp_text_input_v3_commit(textInputV3); + + // Although this should be handled by IM via preedit callback, it seems that + // the behavior varies depending on implemention. It's cleared by IM on + // Ubuntu 22.04 but not cleared on Ubuntu 20.04. + textInputV3Reset(window); +} + +static void textInputV3PreeditString(void* data, + struct zwp_text_input_v3* textInputV3, + const char* text, + int32_t cursorBegin, + int32_t cursorEnd) +{ + _GLFWwindow* window = (_GLFWwindow*) data; + _GLFWpreedit* preedit = &window->preedit; + const char* cur = text; + unsigned int cursorLength = 0; + + preedit->textCount = 0; + preedit->blockSizesCount = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = 0; + + // Store preedit text + while (cur && *cur) + { + uint32_t codepoint = _glfwDecodeUTF8(&cur); + + ++preedit->textCount; + + if (cur == text + cursorBegin) + preedit->caretIndex = preedit->textCount; + if (cursorBegin != cursorEnd && cur == text + cursorEnd) + cursorLength = preedit->textCount - cursorBegin; + + if (preedit->textBufferCount < preedit->textCount + 1) + { + int bufSize = preedit->textBufferCount; + + while (bufSize < preedit->textCount + 1) + bufSize = (bufSize == 0) ? 1 : bufSize * 2; + preedit->text = _glfw_realloc(preedit->text, + sizeof(unsigned int) * bufSize); + if (!preedit->text) + return; + preedit->textBufferCount = bufSize; + } + preedit->text[preedit->textCount - 1] = codepoint; + } + if (preedit->text) + preedit->text[preedit->textCount] = 0; + + // Store preedit blocks + if (preedit->textCount) + { + int* blocks = preedit->blockSizes; + int blockCount = preedit->blockSizesCount; + int cursorPos = preedit->caretIndex; + int textCount = preedit->textCount; + + if (!preedit->blockSizes) + { + int bufSize = 3; + + preedit->blockSizesBufferCount = bufSize; + preedit->blockSizes = _glfw_calloc(sizeof(int), bufSize); + if (!preedit->blockSizes) + return; + blocks = preedit->blockSizes; + } + + if (cursorLength && cursorPos) + blocks[blockCount++] = cursorPos; + + preedit->focusedBlockIndex = blockCount; + blocks[blockCount++] = cursorLength ? cursorLength : textCount; + + if (cursorLength && cursorPos + cursorLength != textCount) + blocks[blockCount++] = textCount - cursorPos - cursorLength; + + preedit->blockSizesCount = blockCount; + } +} + +static void textInputV3CommitString(void* data, + struct zwp_text_input_v3* textInputV3, + const char* text) +{ + _GLFWwindow* window = (_GLFWwindow*) data; + const char* cur = text; + + while (cur && *cur) + { + uint32_t codepoint = _glfwDecodeUTF8(&cur); + window->callbacks.character((GLFWwindow*) window, codepoint); + } +} + +static void textInputV3DeleteSurroundingText(void* data, + struct zwp_text_input_v3* textInputV3, + uint32_t beforeLength, + uint32_t afterLength) +{ +} + +static void textInputV3Done(void* data, + struct zwp_text_input_v3* textInputV3, + uint32_t serial) +{ + _GLFWwindow* window = (_GLFWwindow*) data; + _glfwUpdatePreeditCursorRectangleWayland(window); + _glfwInputPreedit(window); +} + +static const struct zwp_text_input_v3_listener textInputV3Listener = +{ + textInputV3Enter, + textInputV3Leave, + textInputV3PreeditString, + textInputV3CommitString, + textInputV3DeleteSurroundingText, + textInputV3Done +}; + +// Callbacks for text_input_unstable_v1 protocol +// +// This protocol isn't so popular but Weston which is the reference Wayland +// implementation supports only this protocol and doesn't support +// text_input_unstable_v3. +// +static void textInputV1Enter(void* data, + struct zwp_text_input_v1* textInputV1, + struct wl_surface* surface) +{ + _GLFWwindow* window = (_GLFWwindow*) data; + activateTextInputV1(window); +} + +static void textInputV1Reset(_GLFWwindow* window) +{ + _GLFWpreedit* preedit = &window->preedit; + + preedit->textCount = 0; + preedit->blockSizesCount = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = 0; + + _glfw_free(window->wl.textInputV1Context.preeditText); + _glfw_free(window->wl.textInputV1Context.commitTextOnReset); + window->wl.textInputV1Context.preeditText = NULL; + window->wl.textInputV1Context.commitTextOnReset = NULL; + + _glfwInputPreedit(window); +} + +static void textInputV1Leave(void* data, + struct zwp_text_input_v1* textInputV1) +{ + _GLFWwindow* window = (_GLFWwindow*) data; + char* commitText = window->wl.textInputV1Context.commitTextOnReset; + + textInputV3CommitString(data, NULL, commitText); + textInputV1Reset(window); + deactivateTextInputV1(window); +} + +static void textInputV1ModifiersMap(void* data, + struct zwp_text_input_v1* textInputV1, + struct wl_array* map) +{ +} + +static void textInputV1InputPanelState(void* data, + struct zwp_text_input_v1* textInputV1, + uint32_t state) +{ +} + +static void textInputV1PreeditString(void* data, + struct zwp_text_input_v1* textInputV1, + uint32_t serial, + const char* text, + const char* commit) +{ + _GLFWwindow* window = (_GLFWwindow*) data; + + _glfw_free(window->wl.textInputV1Context.preeditText); + _glfw_free(window->wl.textInputV1Context.commitTextOnReset); + window->wl.textInputV1Context.preeditText = strdup(text); + window->wl.textInputV1Context.commitTextOnReset = strdup(commit); + + textInputV3PreeditString(data, NULL, text, 0, 0); + _glfwInputPreedit(window); +} + +static void textInputV1PreeditStyling(void* data, + struct zwp_text_input_v1* textInputV1, + uint32_t index, + uint32_t length, + uint32_t style) +{ +} + +static void textInputV1PreeditCursor(void* data, + struct zwp_text_input_v1* textInputV1, + int32_t index) +{ + _GLFWwindow* window = (_GLFWwindow*) data; + _GLFWpreedit* preedit = &window->preedit; + const char* text = window->wl.textInputV1Context.preeditText; + const char* cur = text; + + preedit->caretIndex = 0; + if (index <= 0 || preedit->textCount == 0) + return; + + while (cur && *cur) + { + _glfwDecodeUTF8(&cur); + ++preedit->caretIndex; + if (cur >= text + index) + break; + if (preedit->caretIndex > preedit->textCount) + break; + } +} + +static void textInputV1CommitString(void* data, + struct zwp_text_input_v1* textInputV1, + uint32_t serial, + const char* text) +{ + _GLFWwindow* window = (_GLFWwindow*) data; + + textInputV1Reset(window); + textInputV3CommitString(data, NULL, text); +} + +static void textInputV1CursorPosition(void* data, + struct zwp_text_input_v1* textInputV1, + int32_t index, + int32_t anchor) +{ + // It's for surrounding text feature which isn't supported by GLFW. +} + +static void textInputV1DeleteSurroundingText(void* data, + struct zwp_text_input_v1* textInputV1, + int32_t index, + uint32_t length) +{ +} + +static void textInputV1Keysym(void* data, + struct zwp_text_input_v1* textInputV1, + uint32_t serial, + uint32_t time, + uint32_t sym, + uint32_t state, + uint32_t modifiers) +{ + uint32_t scancode; + + // This code supports only weston-keyboard because we aren't aware + // of any other input methods that actually support this API. + // Supporting all keysyms is overkill for now. + + switch (sym) + { + case XKB_KEY_Left: + scancode = KEY_LEFT; + break; + case XKB_KEY_Right: + scancode = KEY_RIGHT; + break; + case XKB_KEY_Up: + scancode = KEY_UP; + break; + case XKB_KEY_Down: + scancode = KEY_DOWN; + break; + case XKB_KEY_BackSpace: + scancode = KEY_BACKSPACE; + break; + case XKB_KEY_Tab: + scancode = KEY_TAB; + break; + case XKB_KEY_KP_Enter: + scancode = KEY_KPENTER; + break; + case XKB_KEY_Return: + scancode = KEY_ENTER; + break; + default: + return; + } + + _glfw.wl.xkb.modifiers = modifiers; + + keyboardHandleKey(data, + _glfw.wl.keyboard, + serial, + time, + scancode, + state); +} + +static void textInputV1Language(void* data, + struct zwp_text_input_v1* textInputV1, + uint32_t serial, + const char* language) +{ +} + +static void textInputV1TextDirection(void* data, + struct zwp_text_input_v1* textInputV1, + uint32_t serial, + uint32_t direction) +{ +} + +static const struct zwp_text_input_v1_listener textInputV1Listener = +{ + textInputV1Enter, + textInputV1Leave, + textInputV1ModifiersMap, + textInputV1InputPanelState, + textInputV1PreeditString, + textInputV1PreeditStyling, + textInputV1PreeditCursor, + textInputV1CommitString, + textInputV1CursorPosition, + textInputV1DeleteSurroundingText, + textInputV1Keysym, + textInputV1Language, + textInputV1TextDirection +}; + ////////////////////////////////////////////////////////////////////////// ////// GLFW platform API ////// @@ -2060,6 +2455,21 @@ GLFWbool _glfwCreateWindowWayland(_GLFWwindow* window, return GLFW_FALSE; } + if (_glfw.wl.textInputManagerV3) + { + window->wl.textInputV3 = + zwp_text_input_manager_v3_get_text_input(_glfw.wl.textInputManagerV3, _glfw.wl.seat); + zwp_text_input_v3_add_listener(window->wl.textInputV3, + &textInputV3Listener, window); + } + else if (_glfw.wl.textInputManagerV1) + { + window->wl.textInputV1 = + zwp_text_input_manager_v1_create_text_input(_glfw.wl.textInputManagerV1); + zwp_text_input_v1_add_listener(window->wl.textInputV1, + &textInputV1Listener, window); + } + return GLFW_TRUE; } @@ -2071,6 +2481,15 @@ void _glfwDestroyWindowWayland(_GLFWwindow* window) if (window == _glfw.wl.keyboardFocus) _glfw.wl.keyboardFocus = NULL; + if (window->wl.textInputV1) { + zwp_text_input_v1_destroy(window->wl.textInputV1); + _glfw_free(window->wl.textInputV1Context.preeditText); + _glfw_free(window->wl.textInputV1Context.commitTextOnReset); + } + + if (window->wl.textInputV3) + zwp_text_input_v3_destroy(window->wl.textInputV3); + if (window->wl.idleInhibitor) zwp_idle_inhibitor_v1_destroy(window->wl.idleInhibitor); @@ -3067,6 +3486,19 @@ const char* _glfwGetClipboardStringWayland(void) void _glfwUpdatePreeditCursorRectangleWayland(_GLFWwindow* window) { + _GLFWpreedit* preedit = &window->preedit; + int x = preedit->cursorPosX; + int y = preedit->cursorPosY; + int w = preedit->cursorWidth; + int h = preedit->cursorHeight; + + if (window->wl.textInputV3) + { + zwp_text_input_v3_set_cursor_rectangle(window->wl.textInputV3, x, y, w, h); + zwp_text_input_v3_commit(window->wl.textInputV3); + } + else if (window->wl.textInputV1) + zwp_text_input_v1_set_cursor_rectangle(window->wl.textInputV1, x, y, w, h); } void _glfwResetPreeditTextWayland(_GLFWwindow* window)