From 2e30ad17fe9df8f52fcf2b5aceae0cc8be68a0c8 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Tue, 22 Nov 2022 11:04:21 +0900 Subject: [PATCH 01/13] Apply shibukawa's fix of GLFW for Windows This fix is based on shibukawa's fix: https://github.com/glfw/glfw/pull/658 https://github.com/shibukawa/glfw-1/commit/d36a164423c933948661f3f17576e5a6388ff251 Some minor coding style changes are made, but not yet follow glfw's one, and some comments doesn't follow recent changes. So further work is needed. Co-authored-by: Yoshiki Shibukawa Co-authored-by: Takuro Ashie --- include/GLFW/glfw3.h | 161 +++++++++++++++++++++++++++++++++++++++++-- src/CMakeLists.txt | 1 + src/input.c | 62 +++++++++++++++++ src/internal.h | 16 +++++ src/win32_window.c | 122 ++++++++++++++++++++++++++++++++ src/window.c | 5 ++ 6 files changed, 361 insertions(+), 6 deletions(-) diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index bed739dc..35d7c8dc 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -1155,6 +1155,7 @@ extern "C" { #define GLFW_LOCK_KEY_MODS 0x00033004 #define GLFW_RAW_MOUSE_MOTION 0x00033005 #define GLFW_UNLIMITED_MOUSE_BUTTONS 0x00033006 +#define GLFW_IME 0x00033007 #define GLFW_CURSOR_NORMAL 0x00034001 #define GLFW_CURSOR_HIDDEN 0x00034002 @@ -1945,6 +1946,37 @@ typedef void (* GLFWcharfun)(GLFWwindow* window, unsigned int codepoint); */ typedef void (* GLFWcharmodsfun)(GLFWwindow* window, unsigned int codepoint, int mods); +/*! @brief The function signature for preedit callbacks. + * + * This is the function signature for preedit callback functions. + * + * @param[in] window The window that received the event. + * @param[in] length Preedit string length. + * @param[in] string Preedit string. + * @param[in] count Attributed block count. + * @param[in] blocksizes List of attributed block size. + * @param[in] focusedblock Focused block index. + * + * @sa @ref preedit + * @sa glfwSetPreeditCallback + * + * @ingroup input + */ +typedef void (* GLFWpreeditfun)(GLFWwindow*,int,unsigned int*,int,int*,int); + +/*! @brief The function signature for IME status change callbacks. + * + * This is the function signature for IME status change callback functions. + * + * @param[in] window The window that received the event. + * + * @sa @ref preedit + * @sa glfwSetIMEStatusCallback + * + * @ingroup monitor + */ +typedef void (* GLFWimestatusfun)(GLFWwindow*); + /*! @brief The function pointer type for path drop callbacks. * * This is the function pointer type for path drop callbacks. A path drop @@ -4652,8 +4684,8 @@ GLFWAPI void glfwPostEmptyEvent(void); * * This function returns the value of an input option for the specified window. * The mode must be one of @ref GLFW_CURSOR, @ref GLFW_STICKY_KEYS, - * @ref GLFW_STICKY_MOUSE_BUTTONS, @ref GLFW_LOCK_KEY_MODS or - * @ref GLFW_RAW_MOUSE_MOTION. + * @ref GLFW_STICKY_MOUSE_BUTTONS, @ref GLFW_LOCK_KEY_MODS, + * @ref GLFW_RAW_MOUSE_MOTION or @ref GLFW_IME. * * @param[in] window The window to query. * @param[in] mode One of `GLFW_CURSOR`, `GLFW_STICKY_KEYS`, @@ -4677,8 +4709,9 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* window, int mode); * * This function sets an input mode option for the specified window. The mode * must be one of @ref GLFW_CURSOR, @ref GLFW_STICKY_KEYS, - * @ref GLFW_STICKY_MOUSE_BUTTONS, @ref GLFW_LOCK_KEY_MODS - * @ref GLFW_RAW_MOUSE_MOTION, or @ref GLFW_UNLIMITED_MOUSE_BUTTONS. + * @ref GLFW_STICKY_MOUSE_BUTTONS, @ref GLFW_LOCK_KEY_MODS, + * @ref GLFW_RAW_MOUSE_MOTION, @ref GLFW_UNLIMITED_MOUSE_BUTTONS, + * @ref GLFW_IME. * * If the mode is `GLFW_CURSOR`, the value must be one of the following cursor * modes: @@ -4723,10 +4756,13 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* window, int mode); * callback, or `GLFW_FALSE` to limit the mouse buttons sent to the callback * to the mouse button token values up to `GLFW_MOUSE_BUTTON_LAST`. * + * If the mode is `GLFW_IME`, the value must be either `GLFW_TRUE` to turn on + * IME, or `GLFW_FALSE` to turn off it. + * * @param[in] window The window whose input mode to set. * @param[in] mode One of `GLFW_CURSOR`, `GLFW_STICKY_KEYS`, - * `GLFW_STICKY_MOUSE_BUTTONS`, `GLFW_LOCK_KEY_MODS` or - * `GLFW_RAW_MOUSE_MOTION`. + * `GLFW_STICKY_MOUSE_BUTTONS`, `GLFW_LOCK_KEY_MODS`, + * `GLFW_RAW_MOUSE_MOTION` or `GLFW_IME`. * @param[in] value The new value of the specified input mode. * * @errors Possible errors include @ref GLFW_NOT_INITIALIZED, @ref @@ -5156,6 +5192,67 @@ GLFWAPI void glfwDestroyCursor(GLFWcursor* cursor); */ GLFWAPI void glfwSetCursor(GLFWwindow* window, GLFWcursor* cursor); +/*! @brief Retrieves the position of the text cursor relative to the client area of window. + * + * This function returns position hint to decide the candidate window. + * + * @param[in] window The window to set the text cursor for. + * @param[out] x The text cursor x position (relative position from window coordinates). + * @param[out] y The text cursor y position (relative position from window coordinates). + * @param[out] h The text cursor height. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref input_char + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwGetPreeditCursorPos(GLFWwindow* window, int *x, int *y, int *h); + +/*! @brief Notify the text cursor position to window system to decide the candidate window position. + * + * This function teach position hint to decide the candidate window. The candidate window + * is a part of IME(Input Method Editor) and show several candidate strings. + * + * Windows sytems decide proper pisition from text cursor geometry. + * You should call this function in preedit callback. + * + * @param[in] window The window to set the text cursor for. + * @param[in] x The text cursor x position (relative position from window coordinates). + * @param[in] y The text cursor y position (relative position from window coordinates). + * @param[in] h The text cursor height. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref input_char + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwSetPreeditCursorPos(GLFWwindow* window, int x, int y, int h); + +/*! @brief Reset IME input status. + * + * This function resets IME's preedit text. + * + * @param[in] window The window. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref preedit + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwResetPreeditText(GLFWwindow* window); + /*! @brief Sets the key callback. * * This function sets the key callback of the specified window, which is called @@ -5291,6 +5388,58 @@ GLFWAPI GLFWcharfun glfwSetCharCallback(GLFWwindow* window, GLFWcharfun callback */ GLFWAPI GLFWcharmodsfun glfwSetCharModsCallback(GLFWwindow* window, GLFWcharmodsfun callback); +/*! @brief Sets the preedit callback. + * + * This function sets the preedit callback of the specified + * window, which is called when an IME is processing text before commited. + * + * Callback receives relative position of input cursor inside preedit text and + * attributed text blocks. This callback is used for on-the-spot text editing + * with IME. + * + * @param[in] window The window whose callback to set. + * @param[in] cbfun The new callback, or `NULL` to remove the currently set + * callback. + * @return The previously set callback, or `NULL` if no callback was set or an + * error occurred. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref input_char + * + * @since Added in GLFW 3.X + * + * @ingroup input + */ +GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* window, GLFWpreeditfun cbfun); + +/*! @brief Sets the IME status change callback. + * + * This function sets the preedit callback of the specified + * window, which is called when an IME is processing text before commited. + * + * Callback receives relative position of input cursor inside preedit text and + * attributed text blocks. This callback is used for on-the-spot text editing + * with IME. + * + * @param[in] window The window whose callback to set. + * @param[in] cbfun The new callback, or `NULL` to remove the currently set + * callback. + * @return The previously set callback, or `NULL` if no callback was set or an + * error occurred. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref input_char + * + * @since Added in GLFW 3.X + * + * @ingroup input + */ +GLFWAPI GLFWimestatusfun glfwSetIMEStatusCallback(GLFWwindow* window, GLFWimestatusfun cbfun); + /*! @brief Sets the mouse button callback. * * This function sets the mouse button callback of the specified window, which diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1057a6f9..a9175cc0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -146,6 +146,7 @@ endif() if (GLFW_BUILD_WIN32) list(APPEND glfw_PKG_LIBS "-lgdi32") + list(APPEND glfw_LIBRARIES "imm32") endif() if (GLFW_BUILD_COCOA) diff --git a/src/input.c b/src/input.c index c619eefc..97b64714 100644 --- a/src/input.c +++ b/src/input.c @@ -328,6 +328,20 @@ void _glfwInputChar(_GLFWwindow* window, uint32_t codepoint, int mods, GLFWbool } } +void _glfwInputPreedit(_GLFWwindow* window, int focusedBlock) +{ + if (window->callbacks.preedit) { + window->callbacks.preedit((GLFWwindow*) window, window->ntext, window->preeditText, window->nblocks, window->preeditAttributeBlocks, focusedBlock); + } +} + +void _glfwInputIMEStatus(_GLFWwindow* window) +{ + if (window->callbacks.imestatus) { + window->callbacks.imestatus((GLFWwindow*) window); + } +} + // Notifies shared code of a scroll event // void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset) @@ -580,6 +594,8 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) return window->rawMouseMotion; case GLFW_UNLIMITED_MOUSE_BUTTONS: return window->disableMouseButtonLimit; + case GLFW_IME: + return _glfwPlatformGetIMEStatus(window); } _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode 0x%08X", mode); @@ -693,6 +709,12 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) window->disableMouseButtonLimit = value ? GLFW_TRUE : GLFW_FALSE; return; } + + case GLFW_IME: + { + _glfwPlatformSetIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); + return; + } } _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode 0x%08X", mode); @@ -953,6 +975,30 @@ GLFWAPI void glfwSetCursor(GLFWwindow* windowHandle, GLFWcursor* cursorHandle) _glfw.platform.setCursor(window, cursor); } +GLFWAPI void glfwGetPreeditCursorPos(GLFWwindow* handle, int *x, int *y, int *h) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + if (x) + *x = window->preeditCursorPosX; + if (y) + *y = window->preeditCursorPosY; + if (h) + *h = window->preeditCursorHeight; +} + +GLFWAPI void glfwSetPreeditCursorPos(GLFWwindow* handle, int x, int y, int h) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + window->preeditCursorPosX = x; + window->preeditCursorPosY = y; + window->preeditCursorHeight = h; +} + +GLFWAPI void glfwResetPreeditText(GLFWwindow* handle) { + _GLFWwindow* window = (_GLFWwindow*) handle; + _glfwPlatformResetPreeditText(window); +} + GLFWAPI GLFWkeyfun glfwSetKeyCallback(GLFWwindow* handle, GLFWkeyfun cbfun) { _GLFW_REQUIRE_INIT_OR_RETURN(NULL); @@ -986,6 +1032,22 @@ GLFWAPI GLFWcharmodsfun glfwSetCharModsCallback(GLFWwindow* handle, GLFWcharmods return cbfun; } +GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* handle, GLFWpreeditfun cbfun) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFW_REQUIRE_INIT_OR_RETURN(NULL); + _GLFW_SWAP(GLFWpreeditfun, window->callbacks.preedit, cbfun); + return cbfun; +} + +GLFWAPI GLFWimestatusfun glfwSetIMEStatusCallback(GLFWwindow* handle, GLFWimestatusfun cbfun) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFW_REQUIRE_INIT_OR_RETURN(NULL); + _GLFW_SWAP(GLFWimestatusfun, window->callbacks.imestatus, cbfun); + return cbfun; +} + GLFWAPI GLFWmousebuttonfun glfwSetMouseButtonCallback(GLFWwindow* handle, GLFWmousebuttonfun cbfun) { diff --git a/src/internal.h b/src/internal.h index 4f097aa8..cf87ac99 100644 --- a/src/internal.h +++ b/src/internal.h @@ -561,6 +561,15 @@ struct _GLFWwindow double virtualCursorPosX, virtualCursorPosY; GLFWbool rawMouseMotion; + // Preedit texts + unsigned int* preeditText; + int ntext; + int ctext; + int* preeditAttributeBlocks; + int nblocks; + int cblocks; + int preeditCursorPosX, preeditCursorPosY, preeditCursorHeight; + _GLFWcontext context; struct { @@ -580,6 +589,8 @@ struct _GLFWwindow GLFWkeyfun key; GLFWcharfun character; GLFWcharmodsfun charmods; + GLFWpreeditfun preedit; + GLFWimestatusfun imestatus; GLFWdropfun drop; } callbacks; @@ -934,6 +945,8 @@ void _glfwInputKey(_GLFWwindow* window, int key, int scancode, int action, int mods); void _glfwInputChar(_GLFWwindow* window, uint32_t codepoint, int mods, GLFWbool plain); +void _glfwInputPreedit(_GLFWwindow* window, int focusedBlock); +void _glfwInputIMEStatus(_GLFWwindow* window); void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset); void _glfwInputMouseClick(_GLFWwindow* window, int button, int action, int mods); void _glfwInputCursorPos(_GLFWwindow* window, double xpos, double ypos); @@ -954,6 +967,9 @@ void _glfwInputError(int code, const char* format, ...) void _glfwInputError(int code, const char* format, ...); #endif +void _glfwPlatformResetPreeditText(_GLFWwindow* window); +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active); +int _glfwPlatformGetIMEStatus(_GLFWwindow* window); ////////////////////////////////////////////////////////////////////////// ////// GLFW internal API ////// diff --git a/src/win32_window.c b/src/win32_window.c index d014944b..246318e8 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -35,6 +35,7 @@ #include #include #include +#include // Returns the window style for the specified window // @@ -529,6 +530,15 @@ static void maximizeWindowManually(_GLFWwindow* window) SWP_NOACTIVATE | SWP_NOZORDER | SWP_FRAMECHANGED); } +// Set cursor position to decide candidate window +static void _win32ChangeCursorPosition(HIMC hIMC, _GLFWwindow* window) { + int x = window->preeditCursorPosX; + int y = window->preeditCursorPosY; + int h = window->preeditCursorHeight; + CANDIDATEFORM excludeRect = {0, CFS_EXCLUDE, {x, y}, {x, y, x, y+h}}; + ImmSetCandidateWindow(hIMC, &excludeRect); +} + // Window procedure for user-created windows // static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) @@ -800,6 +810,93 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l break; } + case WM_IME_COMPOSITION: + { + if (lParam & GCS_RESULTSTR) { + window->nblocks = 0; + window->ntext = 0; + _glfwInputPreedit(window, 0); + return TRUE; + } + if (lParam & GCS_COMPSTR) { + HIMC hIMC = ImmGetContext(hWnd); + // get preedit data sizes + LONG preeditTextLength = ImmGetCompositionStringW(hIMC, GCS_COMPSTR, NULL, 0); + LONG attrLength = ImmGetCompositionString(hIMC, GCS_COMPATTR, NULL, 0); + LONG clauseLength = ImmGetCompositionString(hIMC, GCS_COMPCLAUSE, NULL, 0); + if (preeditTextLength > 0) { + // get preedit data + int length = preeditTextLength/sizeof(WCHAR); + LPWSTR buffer = (LPWSTR)_glfw_calloc(preeditTextLength, sizeof(WCHAR)); + LPSTR attributes = (LPSTR)_glfw_calloc(attrLength, 1); + DWORD *clauses = (DWORD*)_glfw_calloc(clauseLength, 1); + ImmGetCompositionStringW(hIMC, GCS_COMPSTR, buffer, preeditTextLength); + ImmGetCompositionString(hIMC, GCS_COMPATTR, attributes, attrLength); + ImmGetCompositionString(hIMC, GCS_COMPCLAUSE, clauses, clauseLength); + // store preedit text + int ctext = window->ctext; + while (ctext < length+1) { + ctext = (ctext == 0) ? 1 : ctext*2; + } + if (ctext != window->ctext) { + unsigned int* preeditText = _glfw_realloc(window->preeditText, sizeof(unsigned int)*ctext); + if (preeditText == NULL) { + return 0; + _glfw_free(buffer); + _glfw_free(attributes); + _glfw_free(clauses); + } + window->preeditText = preeditText; + window->ctext = ctext; + } + window->ntext = length; + window->preeditText[length] = 0; + int i; + for (i=0; i < length; i++) { + window->preeditText[i] = buffer[i]; + } + // store blocks + window->nblocks = clauseLength/sizeof(DWORD)-1; + // last element of clauses is a block count, but + // text length is convenient. + clauses[window->nblocks] = length; + int cblocks = window->cblocks; + while (cblocks < window->nblocks) { + cblocks = (cblocks == 0) ? 1 : cblocks*2; + } + if (cblocks != window->cblocks) { + int* blocks = _glfw_realloc(window->preeditAttributeBlocks, sizeof(int)*cblocks); + if (blocks == NULL) { + return 0; + _glfw_free(buffer); + _glfw_free(attributes); + _glfw_free(clauses); + } + window->preeditAttributeBlocks = blocks; + window->cblocks = cblocks; + } + int focusedBlock = 0; + for (i=0; i < window->nblocks; i++) { + window->preeditAttributeBlocks[i] = clauses[i+1]-clauses[i]; + if (attributes[clauses[i]] != ATTR_CONVERTED) { + focusedBlock = i; + } + } + _glfw_free(buffer); + _glfw_free(attributes); + _glfw_free(clauses); + _glfwInputPreedit(window, focusedBlock); + _win32ChangeCursorPosition(hIMC, window); + } + ImmReleaseContext(hWnd, hIMC); + return TRUE; + } + break; + } + case WM_IME_NOTIFY: + if (wParam == IMN_SETOPENSTATUS) + _glfwInputIMEStatus(window); + break; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: @@ -2575,6 +2672,31 @@ VkResult _glfwCreateWindowSurfaceWin32(VkInstance instance, return err; } +void _glfwPlatformResetPreeditText(_GLFWwindow* window) +{ + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0); + ImmReleaseContext(hWnd, hIMC); +} + +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) +{ + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + ImmSetOpenStatus(hIMC, active ? TRUE : FALSE); + ImmReleaseContext(hWnd, hIMC); +} + +int _glfwPlatformGetIMEStatus(_GLFWwindow* window) +{ + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + BOOL result = ImmGetOpenStatus(hIMC); + ImmReleaseContext(hWnd, hIMC); + return result ? GLFW_TRUE : GLFW_FALSE; +} + GLFWAPI HWND glfwGetWin32Window(GLFWwindow* handle) { _GLFW_REQUIRE_INIT_OR_RETURN(NULL); diff --git a/src/window.c b/src/window.c index e03121a4..2f4ca43d 100644 --- a/src/window.c +++ b/src/window.c @@ -495,6 +495,11 @@ GLFWAPI void glfwDestroyWindow(GLFWwindow* handle) *prev = window->next; } + // Clear memory for preedit text + if (window->preeditText) + _glfw_free(window->preeditText); + if (window->preeditAttributeBlocks) + _glfw_free(window->preeditAttributeBlocks); _glfw_free(window->title); _glfw_free(window); } From 9802d73c96993b24f699561ee0f67648f7563f58 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Thu, 15 Dec 2022 12:01:14 +0900 Subject: [PATCH 02/13] Win32: Support IME This commit re-organizes 9d9af132610829f295c34ceb81b17af8b567b76f. * Use dynamic load for Imm32. * Generalize platform-specific features to _GLFWplatform. * Add caret-position info to preedit-callback. * Add cursorWidth to preeditCursor and related APIs. * Handle UTF16 data correctly. * Handle GCS_RESULTSTR so that committed texts are processed correctly. * Handle WM_IME_ENDCOMPOSITION to clear preedit. * Handle WM_IME_SETCONTEXT. * https://learn.microsoft.com/en-us/windows/win32/intl/wm-ime-setcontext#remarks * Refactor code shapes and variable names. Co-authored-by: Takuro Ashie --- include/GLFW/glfw3.h | 103 ++++++---- src/CMakeLists.txt | 1 - src/cocoa_init.m | 4 + src/cocoa_platform.h | 5 + src/cocoa_window.m | 17 ++ src/input.c | 62 ++++-- src/internal.h | 36 ++-- src/null_init.c | 4 + src/null_platform.h | 5 + src/null_window.c | 17 ++ src/win32_init.c | 30 +++ src/win32_platform.h | 39 ++++ src/win32_window.c | 437 ++++++++++++++++++++++++++++++------------- src/window.c | 13 +- src/wl_init.c | 4 + src/wl_platform.h | 5 + src/wl_window.c | 17 ++ src/x11_init.c | 4 + src/x11_platform.h | 5 + src/x11_window.c | 17 ++ 20 files changed, 621 insertions(+), 204 deletions(-) diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 35d7c8dc..76a51257 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -1946,27 +1946,34 @@ typedef void (* GLFWcharfun)(GLFWwindow* window, unsigned int codepoint); */ typedef void (* GLFWcharmodsfun)(GLFWwindow* window, unsigned int codepoint, int mods); -/*! @brief The function signature for preedit callbacks. +/*! @brief The function pointer type for preedit callbacks. * - * This is the function signature for preedit callback functions. + * This is the function pointer type for preedit callback functions. * * @param[in] window The window that received the event. - * @param[in] length Preedit string length. - * @param[in] string Preedit string. - * @param[in] count Attributed block count. - * @param[in] blocksizes List of attributed block size. - * @param[in] focusedblock Focused block index. + * @param[in] preedit_count Preedit string count. + * @param[in] preedit_string Preedit string. + * @param[in] block_count Attributed block count. + * @param[in] block_sizes List of attributed block size. + * @param[in] focused_block Focused block index. + * @param[in] caret Caret position. * * @sa @ref preedit * @sa glfwSetPreeditCallback * * @ingroup input */ -typedef void (* GLFWpreeditfun)(GLFWwindow*,int,unsigned int*,int,int*,int); +typedef void (* GLFWpreeditfun)(GLFWwindow* window, + int preedit_count, + unsigned int* preedit_string, + int block_count, + int* block_sizes, + int focused_block, + int caret); -/*! @brief The function signature for IME status change callbacks. +/*! @brief The function pointer type for IME status change callbacks. * - * This is the function signature for IME status change callback functions. + * This is the function pointer type for IME status change callback functions. * * @param[in] window The window that received the event. * @@ -1975,7 +1982,7 @@ typedef void (* GLFWpreeditfun)(GLFWwindow*,int,unsigned int*,int,int*,int); * * @ingroup monitor */ -typedef void (* GLFWimestatusfun)(GLFWwindow*); +typedef void (* GLFWimestatusfun)(GLFWwindow* window); /*! @brief The function pointer type for path drop callbacks. * @@ -4689,8 +4696,8 @@ GLFWAPI void glfwPostEmptyEvent(void); * * @param[in] window The window to query. * @param[in] mode One of `GLFW_CURSOR`, `GLFW_STICKY_KEYS`, - * `GLFW_STICKY_MOUSE_BUTTONS`, `GLFW_LOCK_KEY_MODS` or - * `GLFW_RAW_MOUSE_MOTION`. + * `GLFW_STICKY_MOUSE_BUTTONS`, `GLFW_LOCK_KEY_MODS`, `GLFW_RAW_MOUSE_MOTION`, + * or `GLFW_IME`. * * @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref * GLFW_INVALID_ENUM. @@ -5192,14 +5199,16 @@ GLFWAPI void glfwDestroyCursor(GLFWcursor* cursor); */ GLFWAPI void glfwSetCursor(GLFWwindow* window, GLFWcursor* cursor); -/*! @brief Retrieves the position of the text cursor relative to the client area of window. +/*! @brief Retrieves the area of the preedit text cursor. * - * This function returns position hint to decide the candidate window. + * This area is used to decide the position of the candidate window. + * The cursor position is relative to the window. * - * @param[in] window The window to set the text cursor for. - * @param[out] x The text cursor x position (relative position from window coordinates). - * @param[out] y The text cursor y position (relative position from window coordinates). - * @param[out] h The text cursor height. + * @param[in] window The window to set the preedit text cursor for. + * @param[out] x The preedit text cursor x position (relative position from window coordinates). + * @param[out] y The preedit text cursor y position (relative position from window coordinates). + * @param[out] w The preedit text cursor width. + * @param[out] h The preedit text cursor height. * * @par Thread Safety * This function may only be called from the main thread. @@ -5210,20 +5219,18 @@ GLFWAPI void glfwSetCursor(GLFWwindow* window, GLFWcursor* cursor); * * @ingroup input */ -GLFWAPI void glfwGetPreeditCursorPos(GLFWwindow* window, int *x, int *y, int *h); +GLFWAPI void glfwGetPreeditCursorRectangle(GLFWwindow* window, int* x, int* y, int* w, int* h); -/*! @brief Notify the text cursor position to window system to decide the candidate window position. +/*! @brief Sets the area of the preedit text cursor. * - * This function teach position hint to decide the candidate window. The candidate window - * is a part of IME(Input Method Editor) and show several candidate strings. - * - * Windows sytems decide proper pisition from text cursor geometry. - * You should call this function in preedit callback. + * This area is used to decide the position of the candidate window. + * The cursor position is relative to the window. * * @param[in] window The window to set the text cursor for. - * @param[in] x The text cursor x position (relative position from window coordinates). - * @param[in] y The text cursor y position (relative position from window coordinates). - * @param[in] h The text cursor height. + * @param[in] x The preedit text cursor x position (relative position from window coordinates). + * @param[in] y The preedit text cursor y position (relative position from window coordinates). + * @param[in] w The preedit text cursor width. + * @param[in] h The preedit text cursor height. * * @par Thread Safety * This function may only be called from the main thread. @@ -5234,9 +5241,9 @@ GLFWAPI void glfwGetPreeditCursorPos(GLFWwindow* window, int *x, int *y, int *h) * * @ingroup input */ -GLFWAPI void glfwSetPreeditCursorPos(GLFWwindow* window, int x, int y, int h); +GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int w, int h); -/*! @brief Reset IME input status. +/*! @brief Resets IME input status. * * This function resets IME's preedit text. * @@ -5390,11 +5397,11 @@ GLFWAPI GLFWcharmodsfun glfwSetCharModsCallback(GLFWwindow* window, GLFWcharmods /*! @brief Sets the preedit callback. * - * This function sets the preedit callback of the specified - * window, which is called when an IME is processing text before commited. + * This function sets the preedit callback of the specified + * window, which is called when an IME is processing text before committed. * * Callback receives relative position of input cursor inside preedit text and - * attributed text blocks. This callback is used for on-the-spot text editing + * attributed text blocks. This callback is used for on-the-spot text editing * with IME. * * @param[in] window The window whose callback to set. @@ -5403,6 +5410,19 @@ GLFWAPI GLFWcharmodsfun glfwSetCharModsCallback(GLFWwindow* window, GLFWcharmods * @return The previously set callback, or `NULL` if no callback was set or an * error occurred. * + * @callback_signature + * @code + * void function_name(GLFWwindow* window, + int preedit_count, + unsigned int* preedit_string, + int block_count, + int* block_sizes, + int focused_block, + int caret) + * @endcode + * For more information about the callback parameters, see the + * [function pointer type](@ref GLFWpreeditfun). + * * @par Thread Safety * This function may only be called from the main thread. * @@ -5416,12 +5436,8 @@ GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* window, GLFWpreeditfun /*! @brief Sets the IME status change callback. * - * This function sets the preedit callback of the specified - * window, which is called when an IME is processing text before commited. - * - * Callback receives relative position of input cursor inside preedit text and - * attributed text blocks. This callback is used for on-the-spot text editing - * with IME. + * This function sets the IME status callback of the specified + * window, which is called when an IME is switched on and off. * * @param[in] window The window whose callback to set. * @param[in] cbfun The new callback, or `NULL` to remove the currently set @@ -5429,6 +5445,13 @@ GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* window, GLFWpreeditfun * @return The previously set callback, or `NULL` if no callback was set or an * error occurred. * + * @callback_signature + * @code + * void function_name(GLFWwindow* window) + * @endcode + * For more information about the callback parameters, see the + * [function pointer type](@ref GLFWimestatusfun). + * * @par Thread Safety * This function may only be called from the main thread. * diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a9175cc0..1057a6f9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -146,7 +146,6 @@ endif() if (GLFW_BUILD_WIN32) list(APPEND glfw_PKG_LIBS "-lgdi32") - list(APPEND glfw_LIBRARIES "imm32") endif() if (GLFW_BUILD_COCOA) diff --git a/src/cocoa_init.m b/src/cocoa_init.m index 15dc4ec4..43730cb9 100644 --- a/src/cocoa_init.m +++ b/src/cocoa_init.m @@ -509,6 +509,10 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform) .getKeyScancode = _glfwGetKeyScancodeCocoa, .setClipboardString = _glfwSetClipboardStringCocoa, .getClipboardString = _glfwGetClipboardStringCocoa, + .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleCocoa, + .resetPreeditText = _glfwResetPreeditTextCocoa, + .setIMEStatus = _glfwSetIMEStatusCocoa, + .getIMEStatus = _glfwGetIMEStatusCocoa, .initJoysticks = _glfwInitJoysticksCocoa, .terminateJoysticks = _glfwTerminateJoysticksCocoa, .pollJoystick = _glfwPollJoystickCocoa, diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h index 4d1d66ae..da177656 100644 --- a/src/cocoa_platform.h +++ b/src/cocoa_platform.h @@ -268,6 +268,11 @@ void _glfwSetCursorCocoa(_GLFWwindow* window, _GLFWcursor* cursor); void _glfwSetClipboardStringCocoa(const char* string); const char* _glfwGetClipboardStringCocoa(void); +void _glfwUpdatePreeditCursorRectangleCocoa(_GLFWwindow* window); +void _glfwResetPreeditTextCocoa(_GLFWwindow* window); +void _glfwSetIMEStatusCocoa(_GLFWwindow* window, int active); +int _glfwGetIMEStatusCocoa(_GLFWwindow* window); + EGLenum _glfwGetEGLPlatformCocoa(EGLint** attribs); EGLNativeDisplayType _glfwGetEGLNativeDisplayCocoa(void); EGLNativeWindowType _glfwGetEGLNativeWindowCocoa(_GLFWwindow* window); diff --git a/src/cocoa_window.m b/src/cocoa_window.m index 780bc7d3..194ab72e 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -1881,6 +1881,23 @@ const char* _glfwGetClipboardStringCocoa(void) } // autoreleasepool } +void _glfwUpdatePreeditCursorRectangleCocoa(_GLFWwindow* window) +{ +} + +void _glfwResetPreeditTextCocoa(_GLFWwindow* window) +{ +} + +void _glfwSetIMEStatusCocoa(_GLFWwindow* window, int active) +{ +} + +int _glfwGetIMEStatusCocoa(_GLFWwindow* window) +{ + return GLFW_FALSE; +} + EGLenum _glfwGetEGLPlatformCocoa(EGLint** attribs) { if (_glfw.egl.ANGLE_platform_angle) diff --git a/src/input.c b/src/input.c index 97b64714..be0d63c9 100644 --- a/src/input.c +++ b/src/input.c @@ -328,16 +328,29 @@ void _glfwInputChar(_GLFWwindow* window, uint32_t codepoint, int mods, GLFWbool } } -void _glfwInputPreedit(_GLFWwindow* window, int focusedBlock) +// Notifies shared code of a preedit event +// +void _glfwInputPreedit(_GLFWwindow* window) { - if (window->callbacks.preedit) { - window->callbacks.preedit((GLFWwindow*) window, window->ntext, window->preeditText, window->nblocks, window->preeditAttributeBlocks, focusedBlock); + if (window->callbacks.preedit) + { + _GLFWpreedit *preedit = &window->preedit; + window->callbacks.preedit((GLFWwindow*) window, + preedit->textCount, + preedit->text, + preedit->blockSizesCount, + preedit->blockSizes, + preedit->focusedBlockIndex, + preedit->caretIndex); } } +// Notifies shared code of a IME status event +// void _glfwInputIMEStatus(_GLFWwindow* window) { - if (window->callbacks.imestatus) { + if (window->callbacks.imestatus) + { window->callbacks.imestatus((GLFWwindow*) window); } } @@ -595,7 +608,7 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) case GLFW_UNLIMITED_MOUSE_BUTTONS: return window->disableMouseButtonLimit; case GLFW_IME: - return _glfwPlatformGetIMEStatus(window); + return _glfw.platform.getIMEStatus(window); } _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode 0x%08X", mode); @@ -712,7 +725,7 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) case GLFW_IME: { - _glfwPlatformSetIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); + _glfw.platform.setIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); return; } } @@ -975,28 +988,45 @@ GLFWAPI void glfwSetCursor(GLFWwindow* windowHandle, GLFWcursor* cursorHandle) _glfw.platform.setCursor(window, cursor); } -GLFWAPI void glfwGetPreeditCursorPos(GLFWwindow* handle, int *x, int *y, int *h) +GLFWAPI void glfwGetPreeditCursorRectangle(GLFWwindow* handle, int* x, int* y, int* w, int* h) { _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFWpreedit* preedit = &window->preedit; if (x) - *x = window->preeditCursorPosX; + *x = preedit->cursorPosX; if (y) - *y = window->preeditCursorPosY; + *y = preedit->cursorPosY; + if (w) + *w = preedit->cursorWidth; if (h) - *h = window->preeditCursorHeight; + *h = preedit->cursorHeight; } -GLFWAPI void glfwSetPreeditCursorPos(GLFWwindow* handle, int x, int y, int h) +GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* handle, int x, int y, int w, int h) { _GLFWwindow* window = (_GLFWwindow*) handle; - window->preeditCursorPosX = x; - window->preeditCursorPosY = y; - window->preeditCursorHeight = h; + _GLFWpreedit* preedit = &window->preedit; + + if (x == preedit->cursorPosX && + y == preedit->cursorPosY && + w == preedit->cursorWidth && + h == preedit->cursorHeight) + { + return; + } + + preedit->cursorPosX = x; + preedit->cursorPosY = y; + preedit->cursorWidth = w; + preedit->cursorHeight = h; + + _glfw.platform.updatePreeditCursorRectangle(window); } -GLFWAPI void glfwResetPreeditText(GLFWwindow* handle) { +GLFWAPI void glfwResetPreeditText(GLFWwindow* handle) +{ _GLFWwindow* window = (_GLFWwindow*) handle; - _glfwPlatformResetPreeditText(window); + _glfw.platform.resetPreeditText(window); } GLFWAPI GLFWkeyfun glfwSetKeyCallback(GLFWwindow* handle, GLFWkeyfun cbfun) diff --git a/src/internal.h b/src/internal.h index cf87ac99..93722387 100644 --- a/src/internal.h +++ b/src/internal.h @@ -69,6 +69,7 @@ typedef struct _GLFWwndconfig _GLFWwndconfig; typedef struct _GLFWctxconfig _GLFWctxconfig; typedef struct _GLFWfbconfig _GLFWfbconfig; typedef struct _GLFWcontext _GLFWcontext; +typedef struct _GLFWpreedit _GLFWpreedit; typedef struct _GLFWwindow _GLFWwindow; typedef struct _GLFWplatform _GLFWplatform; typedef struct _GLFWlibrary _GLFWlibrary; @@ -525,6 +526,21 @@ struct _GLFWcontext GLFW_PLATFORM_CONTEXT_STATE }; +// Preedit structure for Input Method Editor/Engine +// +struct _GLFWpreedit +{ + unsigned int* text; + int textCount; + int textBufferCount; + int* blockSizes; + int blockSizesCount; + int blockSizesBufferCount; + int focusedBlockIndex; + int caretIndex; + int cursorPosX, cursorPosY, cursorWidth, cursorHeight; +}; + // Window and context structure // struct _GLFWwindow @@ -561,17 +577,10 @@ struct _GLFWwindow double virtualCursorPosX, virtualCursorPosY; GLFWbool rawMouseMotion; - // Preedit texts - unsigned int* preeditText; - int ntext; - int ctext; - int* preeditAttributeBlocks; - int nblocks; - int cblocks; - int preeditCursorPosX, preeditCursorPosY, preeditCursorHeight; - _GLFWcontext context; + _GLFWpreedit preedit; + struct { GLFWwindowposfun pos; GLFWwindowsizefun size; @@ -710,6 +719,10 @@ struct _GLFWplatform int (*getKeyScancode)(int); void (*setClipboardString)(const char*); const char* (*getClipboardString)(void); + void (*updatePreeditCursorRectangle)(_GLFWwindow*); + void (*resetPreeditText)(_GLFWwindow*); + void (*setIMEStatus)(_GLFWwindow*,int); + int (*getIMEStatus)(_GLFWwindow*); GLFWbool (*initJoysticks)(void); void (*terminateJoysticks)(void); GLFWbool (*pollJoystick)(_GLFWjoystick*,int); @@ -945,7 +958,7 @@ void _glfwInputKey(_GLFWwindow* window, int key, int scancode, int action, int mods); void _glfwInputChar(_GLFWwindow* window, uint32_t codepoint, int mods, GLFWbool plain); -void _glfwInputPreedit(_GLFWwindow* window, int focusedBlock); +void _glfwInputPreedit(_GLFWwindow* window); void _glfwInputIMEStatus(_GLFWwindow* window); void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset); void _glfwInputMouseClick(_GLFWwindow* window, int button, int action, int mods); @@ -967,9 +980,6 @@ void _glfwInputError(int code, const char* format, ...) void _glfwInputError(int code, const char* format, ...); #endif -void _glfwPlatformResetPreeditText(_GLFWwindow* window); -void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active); -int _glfwPlatformGetIMEStatus(_GLFWwindow* window); ////////////////////////////////////////////////////////////////////////// ////// GLFW internal API ////// diff --git a/src/null_init.c b/src/null_init.c index 8c10f5e6..f4250603 100644 --- a/src/null_init.c +++ b/src/null_init.c @@ -55,6 +55,10 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform) .getKeyScancode = _glfwGetKeyScancodeNull, .setClipboardString = _glfwSetClipboardStringNull, .getClipboardString = _glfwGetClipboardStringNull, + .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleNull, + .resetPreeditText = _glfwResetPreeditTextNull, + .setIMEStatus = _glfwSetIMEStatusNull, + .getIMEStatus = _glfwGetIMEStatusNull, .initJoysticks = _glfwInitJoysticksNull, .terminateJoysticks = _glfwTerminateJoysticksNull, .pollJoystick = _glfwPollJoystickNull, diff --git a/src/null_platform.h b/src/null_platform.h index dbcb835b..bbd3ee42 100644 --- a/src/null_platform.h +++ b/src/null_platform.h @@ -270,6 +270,11 @@ const char* _glfwGetClipboardStringNull(void); const char* _glfwGetScancodeNameNull(int scancode); int _glfwGetKeyScancodeNull(int key); +void _glfwUpdatePreeditCursorRectangleNull(_GLFWwindow* window); +void _glfwResetPreeditTextNull(_GLFWwindow* window); +void _glfwSetIMEStatusNull(_GLFWwindow* window, int active); +int _glfwGetIMEStatusNull(_GLFWwindow* window); + EGLenum _glfwGetEGLPlatformNull(EGLint** attribs); EGLNativeDisplayType _glfwGetEGLNativeDisplayNull(void); EGLNativeWindowType _glfwGetEGLNativeWindowNull(_GLFWwindow* window); diff --git a/src/null_window.c b/src/null_window.c index f0e1dcc9..efadbe18 100644 --- a/src/null_window.c +++ b/src/null_window.c @@ -551,6 +551,23 @@ const char* _glfwGetClipboardStringNull(void) return _glfw.null.clipboardString; } +void _glfwUpdatePreeditCursorRectangleNull(_GLFWwindow* window) +{ +} + +void _glfwResetPreeditTextNull(_GLFWwindow* window) +{ +} + +void _glfwSetIMEStatusNull(_GLFWwindow* window, int active) +{ +} + +int _glfwGetIMEStatusNull(_GLFWwindow* window) +{ + return GLFW_FALSE; +} + EGLenum _glfwGetEGLPlatformNull(EGLint** attribs) { if (_glfw.egl.EXT_platform_base && _glfw.egl.MESA_platform_surfaceless) diff --git a/src/win32_init.c b/src/win32_init.c index 77ab56ba..a3cd0d82 100644 --- a/src/win32_init.c +++ b/src/win32_init.c @@ -167,6 +167,29 @@ static GLFWbool loadLibraries(void) _glfwPlatformGetModuleSymbol(_glfw.win32.ntdll.instance, "RtlVerifyVersionInfo"); } + _glfw.win32.imm32.instance = _glfwPlatformLoadModule("imm32.dll"); + if (_glfw.win32.imm32.instance) + { + _glfw.win32.imm32.ImmGetCompositionStringW_ = (PFN_ImmGetCompositionStringW) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetCompositionStringW"); + _glfw.win32.imm32.ImmGetContext_ = (PFN_ImmGetContext) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetContext"); + _glfw.win32.imm32.ImmGetConversionStatus_ = (PFN_ImmGetConversionStatus) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetConversionStatus"); + _glfw.win32.imm32.ImmGetDescriptionW_ = (PFN_ImmGetDescriptionW) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetDescriptionW"); + _glfw.win32.imm32.ImmGetOpenStatus_ = (PFN_ImmGetOpenStatus) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetOpenStatus"); + _glfw.win32.imm32.ImmNotifyIME_ = (PFN_ImmNotifyIME) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmNotifyIME"); + _glfw.win32.imm32.ImmReleaseContext_ = (PFN_ImmReleaseContext) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmReleaseContext"); + _glfw.win32.imm32.ImmSetCandidateWindow_ = (PFN_ImmSetCandidateWindow) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmSetCandidateWindow"); + _glfw.win32.imm32.ImmSetOpenStatus_ = (PFN_ImmSetOpenStatus) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmSetOpenStatus"); + } + return GLFW_TRUE; } @@ -191,6 +214,9 @@ static void freeLibraries(void) if (_glfw.win32.ntdll.instance) _glfwPlatformFreeModule(_glfw.win32.ntdll.instance); + + if (_glfw.win32.imm32.instance) + _glfwPlatformFreeModule(_glfw.win32.imm32.instance); } // Create key code translation tables @@ -618,6 +644,10 @@ GLFWbool _glfwConnectWin32(int platformID, _GLFWplatform* platform) .getKeyScancode = _glfwGetKeyScancodeWin32, .setClipboardString = _glfwSetClipboardStringWin32, .getClipboardString = _glfwGetClipboardStringWin32, + .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleWin32, + .resetPreeditText = _glfwResetPreeditTextWin32, + .setIMEStatus = _glfwSetIMEStatusWin32, + .getIMEStatus = _glfwGetIMEStatusWin32, .initJoysticks = _glfwInitJoysticksWin32, .terminateJoysticks = _glfwTerminateJoysticksWin32, .pollJoystick = _glfwPollJoystickWin32, diff --git a/src/win32_platform.h b/src/win32_platform.h index a2f86852..aed08ca3 100644 --- a/src/win32_platform.h +++ b/src/win32_platform.h @@ -69,6 +69,7 @@ #include #include #include +#include // HACK: Define macros that some windows.h variants don't #ifndef WM_MOUSEHWHEEL @@ -316,6 +317,26 @@ typedef HRESULT (WINAPI * PFN_GetDpiForMonitor)(HMONITOR,MONITOR_DPI_TYPE,UINT*, typedef LONG (WINAPI * PFN_RtlVerifyVersionInfo)(OSVERSIONINFOEXW*,ULONG,ULONGLONG); #define RtlVerifyVersionInfo _glfw.win32.ntdll.RtlVerifyVersionInfo_ +// imm32 function pointer typedefs +typedef LONG (WINAPI * PFN_ImmGetCompositionStringW)(HIMC,DWORD,LPVOID,DWORD); +typedef HIMC (WINAPI * PFN_ImmGetContext)(HWND); +typedef BOOL (WINAPI * PFN_ImmGetConversionStatus)(HIMC,LPDWORD,LPDWORD); +typedef UINT (WINAPI * PFN_ImmGetDescriptionW)(HKL,LPWSTR,UINT); +typedef BOOL (WINAPI * PFN_ImmGetOpenStatus)(HIMC); +typedef BOOL (WINAPI * PFN_ImmNotifyIME)(HIMC,DWORD,DWORD,DWORD); +typedef BOOL (WINAPI * PFN_ImmReleaseContext)(HWND,HIMC); +typedef BOOL (WINAPI * PFN_ImmSetCandidateWindow)(HIMC,LPCANDIDATEFORM); +typedef BOOL (WINAPI * PFN_ImmSetOpenStatus)(HIMC,BOOL); +#define ImmGetCompositionStringW _glfw.win32.imm32.ImmGetCompositionStringW_ +#define ImmGetContext _glfw.win32.imm32.ImmGetContext_ +#define ImmGetConversionStatus _glfw.win32.imm32.ImmGetConversionStatus_ +#define ImmGetDescriptionW _glfw.win32.imm32.ImmGetDescriptionW_ +#define ImmGetOpenStatus _glfw.win32.imm32.ImmGetOpenStatus_ +#define ImmNotifyIME _glfw.win32.imm32.ImmNotifyIME_ +#define ImmReleaseContext _glfw.win32.imm32.ImmReleaseContext_ +#define ImmSetCandidateWindow _glfw.win32.imm32.ImmSetCandidateWindow_ +#define ImmSetOpenStatus _glfw.win32.imm32.ImmSetOpenStatus_ + // WGL extension pointer typedefs typedef BOOL (WINAPI * PFNWGLSWAPINTERVALEXTPROC)(int); typedef BOOL (WINAPI * PFNWGLGETPIXELFORMATATTRIBIVARBPROC)(HDC,int,int,UINT,const int*,int*); @@ -502,6 +523,19 @@ typedef struct _GLFWlibraryWin32 HINSTANCE instance; PFN_RtlVerifyVersionInfo RtlVerifyVersionInfo_; } ntdll; + + struct { + HINSTANCE instance; + PFN_ImmGetCompositionStringW ImmGetCompositionStringW_; + PFN_ImmGetContext ImmGetContext_; + PFN_ImmGetConversionStatus ImmGetConversionStatus_; + PFN_ImmGetDescriptionW ImmGetDescriptionW_; + PFN_ImmGetOpenStatus ImmGetOpenStatus_; + PFN_ImmNotifyIME ImmNotifyIME_; + PFN_ImmReleaseContext ImmReleaseContext_; + PFN_ImmSetCandidateWindow ImmSetCandidateWindow_; + PFN_ImmSetOpenStatus ImmSetOpenStatus_; + } imm32; } _GLFWlibraryWin32; // Win32-specific per-monitor data @@ -596,6 +630,11 @@ void _glfwSetCursorWin32(_GLFWwindow* window, _GLFWcursor* cursor); void _glfwSetClipboardStringWin32(const char* string); const char* _glfwGetClipboardStringWin32(void); +void _glfwUpdatePreeditCursorRectangleWin32(_GLFWwindow* window); +void _glfwResetPreeditTextWin32(_GLFWwindow* window); +void _glfwSetIMEStatusWin32(_GLFWwindow* window, int active); +int _glfwGetIMEStatusWin32(_GLFWwindow* window); + EGLenum _glfwGetEGLPlatformWin32(EGLint** attribs); EGLNativeDisplayType _glfwGetEGLNativeDisplayWin32(void); EGLNativeWindowType _glfwGetEGLNativeWindowWin32(_GLFWwindow* window); diff --git a/src/win32_window.c b/src/win32_window.c index 246318e8..eda2df6a 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -35,7 +35,41 @@ #include #include #include -#include + +// Converts utf16 units to Unicode code points (UTF32). +// Returns GLFW_TRUE when the converting completes and the result is assigned to +// the argument `codepoint`. +// Returns GLFW_FALSE when the converting is not yet completed (for +// Surrogate-pair processing) and the unit is assigned to the argument +// `highsurrogate`. It will be used in the next unit's processing. +// +static GLFWbool convertToUTF32FromUTF16(WCHAR utf16_unit, + WCHAR* highsurrogate, + uint32_t* codepoint) +{ + *codepoint = 0; + + if (utf16_unit >= 0xd800 && utf16_unit <= 0xdbff) + { + *highsurrogate = (WCHAR) utf16_unit; + return GLFW_FALSE; + } + + if (utf16_unit >= 0xdc00 && utf16_unit <= 0xdfff) + { + if (*highsurrogate) + { + *codepoint += (*highsurrogate - 0xd800) << 10; + *codepoint += (WCHAR) utf16_unit - 0xdc00; + *codepoint += 0x10000; + } + } + else + *codepoint = (WCHAR) utf16_unit; + + *highsurrogate = 0; + return GLFW_TRUE; +} // Returns the window style for the specified window // @@ -530,13 +564,205 @@ static void maximizeWindowManually(_GLFWwindow* window) SWP_NOACTIVATE | SWP_NOZORDER | SWP_FRAMECHANGED); } -// Set cursor position to decide candidate window -static void _win32ChangeCursorPosition(HIMC hIMC, _GLFWwindow* window) { - int x = window->preeditCursorPosX; - int y = window->preeditCursorPosY; - int h = window->preeditCursorHeight; - CANDIDATEFORM excludeRect = {0, CFS_EXCLUDE, {x, y}, {x, y, x, y+h}}; - ImmSetCandidateWindow(hIMC, &excludeRect); +// Get preedit texts of Imm32 and pass them to preedit-callback +// +static GLFWbool getImmPreedit(_GLFWwindow* window) +{ + _GLFWpreedit* preedit = &window->preedit; + HIMC hIMC = ImmGetContext(window->win32.handle); + // get preedit data sizes + LONG preeditBytes = ImmGetCompositionStringW(hIMC, GCS_COMPSTR, NULL, 0); + LONG attrBytes = ImmGetCompositionStringW(hIMC, GCS_COMPATTR, NULL, 0); + LONG clauseBytes = ImmGetCompositionStringW(hIMC, GCS_COMPCLAUSE, NULL, 0); + LONG cursorPos = ImmGetCompositionStringW(hIMC, GCS_CURSORPOS, NULL, 0); + + if (preeditBytes > 0) + { + int textBufferCount = preedit->textBufferCount; + int blockBufferCount = preedit->blockSizesBufferCount; + int textLen = preeditBytes / sizeof(WCHAR); + LPWSTR buffer = _glfw_calloc(preeditBytes, 1); + LPSTR attributes = _glfw_calloc(attrBytes, 1); + DWORD* clauses = _glfw_calloc(clauseBytes, 1); + + if (!buffer || (attrBytes > 0 && !attributes) || (clauseBytes > 0 && !clauses)) + { + _glfw_free(buffer); + _glfw_free(attributes); + _glfw_free(clauses); + ImmReleaseContext(window->win32.handle, hIMC); + return GLFW_FALSE; + } + + // get preedit data + ImmGetCompositionStringW(hIMC, GCS_COMPSTR, buffer, preeditBytes); + if (attributes) + ImmGetCompositionStringW(hIMC, GCS_COMPATTR, attributes, attrBytes); + if (clauses) + ImmGetCompositionStringW(hIMC, GCS_COMPCLAUSE, clauses, clauseBytes); + + // realloc preedit text + while (textBufferCount < textLen + 1) + textBufferCount = (textBufferCount == 0) ? 1 : textBufferCount * 2; + if (textBufferCount != preedit->textBufferCount) + { + size_t bufsize = sizeof(unsigned int) * textBufferCount; + unsigned int* preeditText = _glfw_realloc(preedit->text, + bufsize); + + if (preeditText == NULL) + { + _glfw_free(buffer); + _glfw_free(attributes); + _glfw_free(clauses); + ImmReleaseContext(window->win32.handle, hIMC); + return GLFW_FALSE; + } + preedit->text = preeditText; + preedit->textBufferCount = textBufferCount; + } + + // realloc blocks + preedit->blockSizesCount = clauses ? clauseBytes / sizeof(DWORD) - 1 : 1; + while (blockBufferCount < preedit->blockSizesCount) + blockBufferCount = (blockBufferCount == 0) ? 1 : blockBufferCount * 2; + if (blockBufferCount != preedit->blockSizesBufferCount) + { + size_t bufsize = sizeof(int) * blockBufferCount; + int* blocks = _glfw_realloc(preedit->blockSizes, + bufsize); + + if (blocks == NULL) + { + _glfw_free(buffer); + _glfw_free(attributes); + _glfw_free(clauses); + ImmReleaseContext(window->win32.handle, hIMC); + return GLFW_FALSE; + } + preedit->blockSizes = blocks; + preedit->blockSizesBufferCount = blockBufferCount; + } + + // store preedit text & block sizes + { + // Win32 API handles text data in UTF16, so we have to convert them + // to UTF32. Not only the encoding, but also the number of characters, + // the position of each block and the cursor. + int i; + uint32_t codepoint; + WCHAR highSurrogate = 0; + int convertedLength = 0; + int blockIndex = 0; + int currentBlockLength = 0; + + // The last element of clauses is a block count, but + // text length is convenient. + if (clauses) + clauses[preedit->blockSizesCount] = textLen; + + for (i = 0; i < textLen; i++) + { + if (clauses && clauses[blockIndex + 1] <= (DWORD) i) + { + preedit->blockSizes[blockIndex++] = currentBlockLength; + currentBlockLength = 0; + } + + if (convertToUTF32FromUTF16(buffer[i], + &highSurrogate, + &codepoint)) + { + preedit->text[convertedLength++] = codepoint; + currentBlockLength++; + } + else if ((LONG) i < cursorPos) + { + // A high surrogate appears before cursorPos, so needs to + // fix cursorPos on UTF16 for UTF32 + cursorPos--; + } + } + preedit->blockSizes[blockIndex] = currentBlockLength; + preedit->textCount = convertedLength; + preedit->text[convertedLength] = 0; + preedit->caretIndex = cursorPos; + + preedit->focusedBlockIndex = 0; + if (attributes && clauses) + { + for (i = 0; i < preedit->blockSizesCount; i++) + { + if (attributes[clauses[i]] == ATTR_TARGET_CONVERTED || + attributes[clauses[i]] == ATTR_TARGET_NOTCONVERTED) + { + preedit->focusedBlockIndex = i; + break; + } + } + } + } + + _glfw_free(buffer); + _glfw_free(attributes); + _glfw_free(clauses); + + _glfwInputPreedit(window); + } + + ImmReleaseContext(window->win32.handle, hIMC); + return GLFW_TRUE; +} + +// Clear peedit data +// +static void clearImmPreedit(_GLFWwindow* window) +{ + window->preedit.blockSizesCount = 0; + window->preedit.textCount = 0; + window->preedit.focusedBlockIndex = 0; + window->preedit.caretIndex = 0; + _glfwInputPreedit(window); +} + +// Commit the result texts of Imm32 to character-callback +// +static GLFWbool commitImmResultStr(_GLFWwindow* window) +{ + HIMC hIMC; + LONG bytes; + uint32_t codepoint; + WCHAR highSurrogate = 0; + + if (!window->callbacks.character) + return GLFW_FALSE; + + hIMC = ImmGetContext(window->win32.handle); + // get preedit data sizes + bytes = ImmGetCompositionStringW(hIMC, GCS_RESULTSTR, NULL, 0); + + if (bytes > 0) + { + int i; + int length = bytes / sizeof(WCHAR); + LPWSTR buffer = _glfw_calloc(bytes, 1); + + // get preedit data + ImmGetCompositionStringW(hIMC, GCS_RESULTSTR, buffer, bytes); + + for (i = 0; i < length; i++) + { + if (convertToUTF32FromUTF16(buffer[i], + &highSurrogate, + &codepoint)) + window->callbacks.character((GLFWwindow*) window, codepoint); + } + + _glfw_free(buffer); + } + + ImmReleaseContext(window->win32.handle, hIMC); + return GLFW_TRUE; } // Window procedure for user-created windows @@ -567,6 +793,14 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l switch (uMsg) { + case WM_IME_SETCONTEXT: + { + // To draw preedit text by an application side + if (lParam & ISC_SHOWUICOMPOSITIONWINDOW) + lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW; + break; + } + case WM_MOUSEACTIVATE: { // HACK: Postpone cursor disabling when the window was activated by @@ -672,27 +906,11 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l case WM_CHAR: case WM_SYSCHAR: { - if (wParam >= 0xd800 && wParam <= 0xdbff) - window->win32.highSurrogate = (WCHAR) wParam; - else - { - uint32_t codepoint = 0; - - if (wParam >= 0xdc00 && wParam <= 0xdfff) - { - if (window->win32.highSurrogate) - { - codepoint += (window->win32.highSurrogate - 0xd800) << 10; - codepoint += (WCHAR) wParam - 0xdc00; - codepoint += 0x10000; - } - } - else - codepoint = (WCHAR) wParam; - - window->win32.highSurrogate = 0; + uint32_t codepoint; + if (convertToUTF32FromUTF16((WCHAR) wParam, + &window->win32.highSurrogate, + &codepoint)) _glfwInputChar(window, codepoint, getKeyMods(), uMsg != WM_SYSCHAR); - } if (uMsg == WM_SYSCHAR && window->win32.keymenu) break; @@ -812,91 +1030,33 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l case WM_IME_COMPOSITION: { - if (lParam & GCS_RESULTSTR) { - window->nblocks = 0; - window->ntext = 0; - _glfwInputPreedit(window, 0); - return TRUE; - } - if (lParam & GCS_COMPSTR) { - HIMC hIMC = ImmGetContext(hWnd); - // get preedit data sizes - LONG preeditTextLength = ImmGetCompositionStringW(hIMC, GCS_COMPSTR, NULL, 0); - LONG attrLength = ImmGetCompositionString(hIMC, GCS_COMPATTR, NULL, 0); - LONG clauseLength = ImmGetCompositionString(hIMC, GCS_COMPCLAUSE, NULL, 0); - if (preeditTextLength > 0) { - // get preedit data - int length = preeditTextLength/sizeof(WCHAR); - LPWSTR buffer = (LPWSTR)_glfw_calloc(preeditTextLength, sizeof(WCHAR)); - LPSTR attributes = (LPSTR)_glfw_calloc(attrLength, 1); - DWORD *clauses = (DWORD*)_glfw_calloc(clauseLength, 1); - ImmGetCompositionStringW(hIMC, GCS_COMPSTR, buffer, preeditTextLength); - ImmGetCompositionString(hIMC, GCS_COMPATTR, attributes, attrLength); - ImmGetCompositionString(hIMC, GCS_COMPCLAUSE, clauses, clauseLength); - // store preedit text - int ctext = window->ctext; - while (ctext < length+1) { - ctext = (ctext == 0) ? 1 : ctext*2; - } - if (ctext != window->ctext) { - unsigned int* preeditText = _glfw_realloc(window->preeditText, sizeof(unsigned int)*ctext); - if (preeditText == NULL) { - return 0; - _glfw_free(buffer); - _glfw_free(attributes); - _glfw_free(clauses); - } - window->preeditText = preeditText; - window->ctext = ctext; - } - window->ntext = length; - window->preeditText[length] = 0; - int i; - for (i=0; i < length; i++) { - window->preeditText[i] = buffer[i]; - } - // store blocks - window->nblocks = clauseLength/sizeof(DWORD)-1; - // last element of clauses is a block count, but - // text length is convenient. - clauses[window->nblocks] = length; - int cblocks = window->cblocks; - while (cblocks < window->nblocks) { - cblocks = (cblocks == 0) ? 1 : cblocks*2; - } - if (cblocks != window->cblocks) { - int* blocks = _glfw_realloc(window->preeditAttributeBlocks, sizeof(int)*cblocks); - if (blocks == NULL) { - return 0; - _glfw_free(buffer); - _glfw_free(attributes); - _glfw_free(clauses); - } - window->preeditAttributeBlocks = blocks; - window->cblocks = cblocks; - } - int focusedBlock = 0; - for (i=0; i < window->nblocks; i++) { - window->preeditAttributeBlocks[i] = clauses[i+1]-clauses[i]; - if (attributes[clauses[i]] != ATTR_CONVERTED) { - focusedBlock = i; - } - } - _glfw_free(buffer); - _glfw_free(attributes); - _glfw_free(clauses); - _glfwInputPreedit(window, focusedBlock); - _win32ChangeCursorPosition(hIMC, window); - } - ImmReleaseContext(hWnd, hIMC); + if (lParam & (GCS_RESULTSTR | GCS_COMPSTR)) + { + if (lParam & GCS_RESULTSTR) + commitImmResultStr(window); + if (lParam & GCS_COMPSTR) + getImmPreedit(window); return TRUE; } break; } + + case WM_IME_ENDCOMPOSITION: + { + clearImmPreedit(window); + return TRUE; + } + case WM_IME_NOTIFY: + { if (wParam == IMN_SETOPENSTATUS) + { _glfwInputIMEStatus(window); + return TRUE; + } break; + } + case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: @@ -2560,6 +2720,48 @@ const char* _glfwGetClipboardStringWin32(void) return _glfw.win32.clipboardString; } +void _glfwUpdatePreeditCursorRectangleWin32(_GLFWwindow* window) +{ + _GLFWpreedit* preedit = &window->preedit; + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + + int x = preedit->cursorPosX; + int y = preedit->cursorPosY; + int w = preedit->cursorWidth; + int h = preedit->cursorHeight; + CANDIDATEFORM excludeRect = { 0, CFS_EXCLUDE, { x, y }, { x, y, x + w, y + h } }; + + ImmSetCandidateWindow(hIMC, &excludeRect); + + ImmReleaseContext(hWnd, hIMC); +} + +void _glfwResetPreeditTextWin32(_GLFWwindow* window) +{ + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0); + ImmReleaseContext(hWnd, hIMC); +} + +void _glfwSetIMEStatusWin32(_GLFWwindow* window, int active) +{ + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + ImmSetOpenStatus(hIMC, active ? TRUE : FALSE); + ImmReleaseContext(hWnd, hIMC); +} + +int _glfwGetIMEStatusWin32(_GLFWwindow* window) +{ + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + BOOL result = ImmGetOpenStatus(hIMC); + ImmReleaseContext(hWnd, hIMC); + return result ? GLFW_TRUE : GLFW_FALSE; +} + EGLenum _glfwGetEGLPlatformWin32(EGLint** attribs) { if (_glfw.egl.ANGLE_platform_angle) @@ -2672,31 +2874,6 @@ VkResult _glfwCreateWindowSurfaceWin32(VkInstance instance, return err; } -void _glfwPlatformResetPreeditText(_GLFWwindow* window) -{ - HWND hWnd = window->win32.handle; - HIMC hIMC = ImmGetContext(hWnd); - ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0); - ImmReleaseContext(hWnd, hIMC); -} - -void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) -{ - HWND hWnd = window->win32.handle; - HIMC hIMC = ImmGetContext(hWnd); - ImmSetOpenStatus(hIMC, active ? TRUE : FALSE); - ImmReleaseContext(hWnd, hIMC); -} - -int _glfwPlatformGetIMEStatus(_GLFWwindow* window) -{ - HWND hWnd = window->win32.handle; - HIMC hIMC = ImmGetContext(hWnd); - BOOL result = ImmGetOpenStatus(hIMC); - ImmReleaseContext(hWnd, hIMC); - return result ? GLFW_TRUE : GLFW_FALSE; -} - GLFWAPI HWND glfwGetWin32Window(GLFWwindow* handle) { _GLFW_REQUIRE_INIT_OR_RETURN(NULL); diff --git a/src/window.c b/src/window.c index 2f4ca43d..8d15bf96 100644 --- a/src/window.c +++ b/src/window.c @@ -244,6 +244,11 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, window->denom = GLFW_DONT_CARE; window->title = _glfw_strdup(title); + window->preedit.cursorPosX = 0; + window->preedit.cursorPosY = height; + window->preedit.cursorWidth = 0; + window->preedit.cursorHeight = 0; + if (!_glfw.platform.createWindow(window, &wndconfig, &ctxconfig, &fbconfig)) { glfwDestroyWindow((GLFWwindow*) window); @@ -496,10 +501,10 @@ GLFWAPI void glfwDestroyWindow(GLFWwindow* handle) } // Clear memory for preedit text - if (window->preeditText) - _glfw_free(window->preeditText); - if (window->preeditAttributeBlocks) - _glfw_free(window->preeditAttributeBlocks); + if (window->preedit.text) + _glfw_free(window->preedit.text); + if (window->preedit.blockSizes) + _glfw_free(window->preedit.blockSizes); _glfw_free(window->title); _glfw_free(window); } diff --git a/src/wl_init.c b/src/wl_init.c index 76054bc6..fce71039 100644 --- a/src/wl_init.c +++ b/src/wl_init.c @@ -452,6 +452,10 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform) .getKeyScancode = _glfwGetKeyScancodeWayland, .setClipboardString = _glfwSetClipboardStringWayland, .getClipboardString = _glfwGetClipboardStringWayland, + .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleWayland, + .resetPreeditText = _glfwResetPreeditTextWayland, + .setIMEStatus = _glfwSetIMEStatusWayland, + .getIMEStatus = _glfwGetIMEStatusWayland, #if defined(GLFW_BUILD_LINUX_JOYSTICK) .initJoysticks = _glfwInitJoysticksLinux, .terminateJoysticks = _glfwTerminateJoysticksLinux, diff --git a/src/wl_platform.h b/src/wl_platform.h index f3e8cba2..1023510f 100644 --- a/src/wl_platform.h +++ b/src/wl_platform.h @@ -664,6 +664,11 @@ void _glfwSetCursorWayland(_GLFWwindow* window, _GLFWcursor* cursor); void _glfwSetClipboardStringWayland(const char* string); const char* _glfwGetClipboardStringWayland(void); +void _glfwUpdatePreeditCursorRectangleWayland(_GLFWwindow* window); +void _glfwResetPreeditTextWayland(_GLFWwindow* window); +void _glfwSetIMEStatusWayland(_GLFWwindow* window, int active); +int _glfwGetIMEStatusWayland(_GLFWwindow* window); + EGLenum _glfwGetEGLPlatformWayland(EGLint** attribs); EGLNativeDisplayType _glfwGetEGLNativeDisplayWayland(void); EGLNativeWindowType _glfwGetEGLNativeWindowWayland(_GLFWwindow* window); diff --git a/src/wl_window.c b/src/wl_window.c index dc7dcd07..e3926e50 100644 --- a/src/wl_window.c +++ b/src/wl_window.c @@ -3195,6 +3195,23 @@ const char* _glfwGetClipboardStringWayland(void) return _glfw.wl.clipboardString; } +void _glfwUpdatePreeditCursorRectangleWayland(_GLFWwindow* window) +{ +} + +void _glfwResetPreeditTextWayland(_GLFWwindow* window) +{ +} + +void _glfwSetIMEStatusWayland(_GLFWwindow* window, int active) +{ +} + +int _glfwGetIMEStatusWayland(_GLFWwindow* window) +{ + return GLFW_FALSE; +} + EGLenum _glfwGetEGLPlatformWayland(EGLint** attribs) { if (_glfw.egl.EXT_platform_base && _glfw.egl.EXT_platform_wayland) diff --git a/src/x11_init.c b/src/x11_init.c index 982c526c..0ccf9454 100644 --- a/src/x11_init.c +++ b/src/x11_init.c @@ -1182,6 +1182,10 @@ GLFWbool _glfwConnectX11(int platformID, _GLFWplatform* platform) .getKeyScancode = _glfwGetKeyScancodeX11, .setClipboardString = _glfwSetClipboardStringX11, .getClipboardString = _glfwGetClipboardStringX11, + .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleX11, + .resetPreeditText = _glfwResetPreeditTextX11, + .setIMEStatus = _glfwSetIMEStatusX11, + .getIMEStatus = _glfwGetIMEStatusX11, #if defined(GLFW_BUILD_LINUX_JOYSTICK) .initJoysticks = _glfwInitJoysticksLinux, .terminateJoysticks = _glfwTerminateJoysticksLinux, diff --git a/src/x11_platform.h b/src/x11_platform.h index 30326c5b..ecc00c19 100644 --- a/src/x11_platform.h +++ b/src/x11_platform.h @@ -955,6 +955,11 @@ void _glfwSetCursorX11(_GLFWwindow* window, _GLFWcursor* cursor); void _glfwSetClipboardStringX11(const char* string); const char* _glfwGetClipboardStringX11(void); +void _glfwUpdatePreeditCursorRectangleX11(_GLFWwindow* window); +void _glfwResetPreeditTextX11(_GLFWwindow* window); +void _glfwSetIMEStatusX11(_GLFWwindow* window, int active); +int _glfwGetIMEStatusX11(_GLFWwindow* window); + EGLenum _glfwGetEGLPlatformX11(EGLint** attribs); EGLNativeDisplayType _glfwGetEGLNativeDisplayX11(void); EGLNativeWindowType _glfwGetEGLNativeWindowX11(_GLFWwindow* window); diff --git a/src/x11_window.c b/src/x11_window.c index 322349f0..447948e1 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -3084,6 +3084,23 @@ const char* _glfwGetClipboardStringX11(void) return getSelectionString(_glfw.x11.CLIPBOARD); } +void _glfwUpdatePreeditCursorRectangleX11(_GLFWwindow* window) +{ +} + +void _glfwResetPreeditTextX11(_GLFWwindow* window) +{ +} + +void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) +{ +} + +int _glfwGetIMEStatusX11(_GLFWwindow* window) +{ + return GLFW_FALSE; +} + EGLenum _glfwGetEGLPlatformX11(EGLint** attribs) { if (_glfw.egl.ANGLE_platform_angle) From e947cb7b52563829b668bf4a277a2e69652d23ce Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Thu, 17 Mar 2022 16:44:14 +0900 Subject: [PATCH 03/13] Apply shibukawa's fix of GLFW for MacOS This fix is based on shibukawa's fix: https://github.com/glfw/glfw/pull/658 https://github.com/shibukawa/glfw-1/commit/d36a164423c933948661f3f17576e5a6388ff251 Co-authored-by: Yoshiki Shibukawa Co-authored-by: Takuro Ashie --- src/CMakeLists.txt | 3 +- src/cocoa_window.m | 106 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1057a6f9..922cadca 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -150,11 +150,12 @@ endif() if (GLFW_BUILD_COCOA) target_link_libraries(glfw PRIVATE "-framework Cocoa" + "-framework Carbon" "-framework IOKit" "-framework CoreFoundation") set(glfw_PKG_DEPS "") - set(glfw_PKG_LIBS "-framework Cocoa -framework IOKit -framework CoreFoundation") + set(glfw_PKG_LIBS "-framework Cocoa -framework Carbon -framework IOKit -framework CoreFoundation") endif() if (GLFW_BUILD_WAYLAND) diff --git a/src/cocoa_window.m b/src/cocoa_window.m index 194ab72e..8b813643 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -321,6 +321,10 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; #endif } +- (void)imeStatusChangeNotified:(NSNotification *)notification { + _glfwInputIMEStatus(window); +} + @end @@ -677,6 +681,57 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; else markedText = [[NSMutableAttributedString alloc] initWithString:string]; + + NSString* markedTextString = markedText.string; + + NSUInteger i, length = [markedTextString length]; + int ctext = window->ctext; + while (ctext < length+1) { + ctext = (ctext == 0) ? 1 : ctext*2; + } + if (ctext != window->ctext) { + unsigned int* preeditText = realloc(window->preeditText, sizeof(unsigned int)*ctext); + if (preeditText == NULL) { + return; + } + window->preeditText = preeditText; + window->ctext = ctext; + } + window->ntext = length; + window->preeditText[length] = 0; + for (i = 0; i < length; i++) + { + const unichar codepoint = [markedTextString characterAtIndex:i]; + window->preeditText[i] = codepoint; + } + int focusedBlock = 0; + NSInteger offset = 0; + window->nblocks = 0; + while (offset < length) { + NSRange effectiveRange; + NSDictionary *attributes = [markedText attributesAtIndex:offset effectiveRange:&effectiveRange]; + + if (window->nblocks == window->cblocks) { + int cblocks = window->cblocks * 2; + int* blocks = realloc(window->preeditAttributeBlocks, sizeof(int)*cblocks); + if (blocks == NULL) { + return; + } + window->preeditAttributeBlocks = blocks; + window->cblocks = cblocks; + } + window->preeditAttributeBlocks[window->nblocks] = effectiveRange.length; + offset += effectiveRange.length; + if (effectiveRange.length == 0) { + break; + } + NSNumber* underline = (NSNumber*) [attributes objectForKey:@"NSUnderline"]; + if ([underline intValue] != 1) { + focusedBlock = window->nblocks; + } + window->nblocks++; + } + _glfwInputPreedit(window, focusedBlock); } - (void)unmarkText @@ -982,6 +1037,12 @@ GLFWbool _glfwCreateWindowCocoa(_GLFWwindow* window, } } + [[NSNotificationCenter defaultCenter] + addObserver: window->ns.delegate + selector:@selector(imeStatusChangeNotified:) + name:NSTextInputContextKeyboardSelectionDidChangeNotification + object: nil]; + return GLFW_TRUE; } // autoreleasepool @@ -994,6 +1055,8 @@ void _glfwDestroyWindowCocoa(_GLFWwindow* window) if (_glfw.ns.disabledCursorWindow == window) _glfw.ns.disabledCursorWindow = NULL; + [[NSNotificationCenter defaultCenter] removeObserver: window->ns.delegate]; + [window->ns.object orderOut:nil]; if (window->monitor) @@ -2051,6 +2114,49 @@ VkResult _glfwCreateWindowSurfaceCocoa(VkInstance instance, } // autoreleasepool } +void _glfwPlatformResetPreeditText(_GLFWwindow* window) +{ + NSTextInputContext *context = [NSTextInputContext currentInputContext]; + [context discardMarkedText]; + [window->ns.view unmarkText]; +} + +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) +{ + // Mac OS has several input sources. + // this code assumes input methods not in ascii capable inputs using IME. + NSArray* asciiInputSources = CFBridgingRelease(TISCreateASCIICapableInputSourceList()); + TISInputSourceRef asciiSource = (__bridge TISInputSourceRef)([asciiInputSources firstObject]); + if (active) { + NSArray* allInputSources = CFBridgingRelease(TISCreateInputSourceList(NULL, false)); + NSString* asciiSourceID = (__bridge NSString *)(TISGetInputSourceProperty(asciiSource, kTISPropertyInputSourceID)); + int i; + int count = [allInputSources count]; + for (i = 0; i < count; i++) { + TISInputSourceRef source = (__bridge TISInputSourceRef)([allInputSources objectAtIndex: i]); + NSString* sourceID = (__bridge NSString *)(TISGetInputSourceProperty(source, kTISPropertyInputSourceID)); + if ([asciiSourceID compare: sourceID] != NSOrderedSame) { + TISSelectInputSource(source); + break; + } + } + } else if (asciiSource) { + TISSelectInputSource(asciiSource); + } +} + +int _glfwPlatformGetIMEStatus(_GLFWwindow* window) +{ + TISInputSourceRef currentSource = TISCopyCurrentKeyboardInputSource(); + NSString* currentSourceID = (__bridge NSString *)(TISGetInputSourceProperty(currentSource, kTISPropertyInputSourceID)); + NSArray* asciiInputSources = CFBridgingRelease(TISCreateASCIICapableInputSourceList()); + TISInputSourceRef asciiSource = (__bridge TISInputSourceRef)([asciiInputSources firstObject]); + if (asciiSource) { + NSString* asciiSourceID = (__bridge NSString *)(TISGetInputSourceProperty(asciiSource, kTISPropertyInputSourceID)); + return ([asciiSourceID compare: currentSourceID] == NSOrderedSame) ? GLFW_FALSE : GLFW_TRUE; + } + return GLFW_FALSE; +} ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// From 2368112f9851fac125b7c67531459161f817fba6 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Fri, 13 Jan 2023 18:42:59 +0900 Subject: [PATCH 04/13] macOS: Support IME This commit re-organizes 31b12b7f79a5aa8bd8f8eb1488a050ab894ca289. * Use dynamic load for TIS functions and stop using Carbon. * Generalize platform-specific features to _GLFWplatform. * Add caret-position info to preedit-callback. * Handle UTF16 data correctly. * Implement `firstRectForCharacterRange:actualRange:` to display preedit candidate window correctly. * Suppress _glfwInputKey during preediting. * Ensure preedit cleared after committed. * Fix wrong length of markedRange. * Improve IME status APIs. * Refactor code shapes and variable names. Co-authored-by: Takuro Ashie Co-authored-by: xfangfang <2553041586@qq.com> --- src/CMakeLists.txt | 3 +- src/cocoa_init.m | 62 +++++++++- src/cocoa_platform.h | 30 +++++ src/cocoa_window.m | 288 +++++++++++++++++++++++++++++-------------- 4 files changed, 286 insertions(+), 97 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 922cadca..1057a6f9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -150,12 +150,11 @@ endif() if (GLFW_BUILD_COCOA) target_link_libraries(glfw PRIVATE "-framework Cocoa" - "-framework Carbon" "-framework IOKit" "-framework CoreFoundation") set(glfw_PKG_DEPS "") - set(glfw_PKG_LIBS "-framework Cocoa -framework Carbon -framework IOKit -framework CoreFoundation") + set(glfw_PKG_LIBS "-framework Cocoa -framework IOKit -framework CoreFoundation") endif() if (GLFW_BUILD_WAYLAND) diff --git a/src/cocoa_init.m b/src/cocoa_init.m index 43730cb9..869b4246 100644 --- a/src/cocoa_init.m +++ b/src/cocoa_init.m @@ -349,22 +349,70 @@ static GLFWbool initializeTIS(void) return GLFW_FALSE; } + CFStringRef* kCategoryKeyboardInputSource = + CFBundleGetDataPointerForName(_glfw.ns.tis.bundle, + CFSTR("kTISCategoryKeyboardInputSource")); + CFStringRef* kPropertyInputSourceCategory = + CFBundleGetDataPointerForName(_glfw.ns.tis.bundle, + CFSTR("kTISPropertyInputSourceCategory")); + CFStringRef* kPropertyInputSourceID = + CFBundleGetDataPointerForName(_glfw.ns.tis.bundle, + CFSTR("kTISPropertyInputSourceID")); + CFStringRef* kPropertyInputSourceIsSelectCapable = + CFBundleGetDataPointerForName(_glfw.ns.tis.bundle, + CFSTR("kTISPropertyInputSourceIsSelectCapable")); + CFStringRef* kPropertyInputSourceType = + CFBundleGetDataPointerForName(_glfw.ns.tis.bundle, + CFSTR("kTISPropertyInputSourceType")); CFStringRef* kPropertyUnicodeKeyLayoutData = CFBundleGetDataPointerForName(_glfw.ns.tis.bundle, CFSTR("kTISPropertyUnicodeKeyLayoutData")); + CFStringRef* kTypeKeyboardInputMethodModeEnabled = + CFBundleGetDataPointerForName(_glfw.ns.tis.bundle, + CFSTR("kTISTypeKeyboardInputMethodModeEnabled")); + _glfw.ns.tis.CopyCurrentASCIICapableKeyboardInputSource = + CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle, + CFSTR("TISCopyCurrentASCIICapableKeyboardInputSource")); + _glfw.ns.tis.CopyCurrentKeyboardInputSource = + CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle, + CFSTR("TISCopyCurrentKeyboardInputSource")); _glfw.ns.tis.CopyCurrentKeyboardLayoutInputSource = CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle, CFSTR("TISCopyCurrentKeyboardLayoutInputSource")); + _glfw.ns.tis.CopyInputSourceForLanguage = + CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle, + CFSTR("TISCopyInputSourceForLanguage")); + _glfw.ns.tis.CreateASCIICapableInputSourceList = + CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle, + CFSTR("TISCreateASCIICapableInputSourceList")); + _glfw.ns.tis.CreateInputSourceList = + CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle, + CFSTR("TISCreateInputSourceList")); _glfw.ns.tis.GetInputSourceProperty = CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle, CFSTR("TISGetInputSourceProperty")); + _glfw.ns.tis.SelectInputSource = + CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle, + CFSTR("TISSelectInputSource")); _glfw.ns.tis.GetKbdType = CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle, CFSTR("LMGetKbdType")); - if (!kPropertyUnicodeKeyLayoutData || + if (!kCategoryKeyboardInputSource|| + !kPropertyInputSourceCategory || + !kPropertyInputSourceID || + !kPropertyInputSourceIsSelectCapable|| + !kPropertyInputSourceType|| + !kPropertyUnicodeKeyLayoutData || + !kTypeKeyboardInputMethodModeEnabled || + !TISCopyCurrentASCIICapableKeyboardInputSource || + !TISCopyCurrentKeyboardInputSource || !TISCopyCurrentKeyboardLayoutInputSource || + !TISCopyInputSourceForLanguage || + !TISCreateASCIICapableInputSourceList || + !TISCreateInputSourceList || !TISGetInputSourceProperty || + !TISSelectInputSource || !LMGetKbdType) { _glfwInputError(GLFW_PLATFORM_ERROR, @@ -372,8 +420,20 @@ static GLFWbool initializeTIS(void) return GLFW_FALSE; } + _glfw.ns.tis.kCategoryKeyboardInputSource = + *kCategoryKeyboardInputSource; + _glfw.ns.tis.kPropertyInputSourceCategory = + *kPropertyInputSourceCategory; + _glfw.ns.tis.kPropertyInputSourceID = + *kPropertyInputSourceID; + _glfw.ns.tis.kPropertyInputSourceIsSelectCapable = + *kPropertyInputSourceIsSelectCapable; + _glfw.ns.tis.kPropertyInputSourceType = + *kPropertyInputSourceType; _glfw.ns.tis.kPropertyUnicodeKeyLayoutData = *kPropertyUnicodeKeyLayoutData; + _glfw.ns.tis.kTypeKeyboardInputMethodModeEnabled = + *kTypeKeyboardInputMethodModeEnabled; return updateUnicodeData(); } diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h index da177656..b2207324 100644 --- a/src/cocoa_platform.h +++ b/src/cocoa_platform.h @@ -109,11 +109,29 @@ typedef VkResult (APIENTRY *PFN_vkCreateMetalSurfaceEXT)(VkInstance,const VkMeta #define GLFW_NSGL_LIBRARY_CONTEXT_STATE _GLFWlibraryNSGL nsgl; // HIToolbox.framework pointer typedefs +#define kTISCategoryKeyboardInputSource _glfw.ns.tis.kCategoryKeyboardInputSource +#define kTISPropertyInputSourceCategory _glfw.ns.tis.kPropertyInputSourceCategory +#define kTISPropertyInputSourceID _glfw.ns.tis.kPropertyInputSourceID +#define kTISPropertyInputSourceIsSelectCapable _glfw.ns.tis.kPropertyInputSourceIsSelectCapable +#define kTISPropertyInputSourceType _glfw.ns.tis.kPropertyInputSourceType #define kTISPropertyUnicodeKeyLayoutData _glfw.ns.tis.kPropertyUnicodeKeyLayoutData +#define kTISTypeKeyboardInputMethodModeEnabled _glfw.ns.tis.kTypeKeyboardInputMethodModeEnabled +typedef TISInputSourceRef (*PFN_TISCopyCurrentASCIICapableKeyboardInputSource)(void); +#define TISCopyCurrentASCIICapableKeyboardInputSource _glfw.ns.tis.CopyCurrentASCIICapableKeyboardInputSource +typedef TISInputSourceRef (*PFN_TISCopyCurrentKeyboardInputSource)(void); +#define TISCopyCurrentKeyboardInputSource _glfw.ns.tis.CopyCurrentKeyboardInputSource typedef TISInputSourceRef (*PFN_TISCopyCurrentKeyboardLayoutInputSource)(void); #define TISCopyCurrentKeyboardLayoutInputSource _glfw.ns.tis.CopyCurrentKeyboardLayoutInputSource +typedef TISInputSourceRef (*PFN_TISCopyInputSourceForLanguage)(CFStringRef); +#define TISCopyInputSourceForLanguage _glfw.ns.tis.CopyInputSourceForLanguage +typedef CFArrayRef (*PFN_TISCreateASCIICapableInputSourceList)(void); +#define TISCreateASCIICapableInputSourceList _glfw.ns.tis.CreateASCIICapableInputSourceList +typedef CFArrayRef (*PEN_TISCreateInputSourceList)(CFDictionaryRef,Boolean); +#define TISCreateInputSourceList _glfw.ns.tis.CreateInputSourceList typedef void* (*PFN_TISGetInputSourceProperty)(TISInputSourceRef,CFStringRef); #define TISGetInputSourceProperty _glfw.ns.tis.GetInputSourceProperty +typedef OSStatus (*PFN_TISSelectInputSource)(TISInputSourceRef); +#define TISSelectInputSource _glfw.ns.tis.SelectInputSource typedef UInt8 (*PFN_LMGetKbdType)(void); #define LMGetKbdType _glfw.ns.tis.GetKbdType @@ -184,10 +202,22 @@ typedef struct _GLFWlibraryNS struct { CFBundleRef bundle; + PFN_TISCopyCurrentASCIICapableKeyboardInputSource CopyCurrentASCIICapableKeyboardInputSource; + PFN_TISCopyCurrentKeyboardInputSource CopyCurrentKeyboardInputSource; PFN_TISCopyCurrentKeyboardLayoutInputSource CopyCurrentKeyboardLayoutInputSource; + PFN_TISCopyInputSourceForLanguage CopyInputSourceForLanguage; + PFN_TISCreateASCIICapableInputSourceList CreateASCIICapableInputSourceList; + PEN_TISCreateInputSourceList CreateInputSourceList; PFN_TISGetInputSourceProperty GetInputSourceProperty; + PFN_TISSelectInputSource SelectInputSource; PFN_LMGetKbdType GetKbdType; + CFStringRef kCategoryKeyboardInputSource; + CFStringRef kPropertyInputSourceCategory; + CFStringRef kPropertyInputSourceID; + CFStringRef kPropertyInputSourceIsSelectCapable; + CFStringRef kPropertyInputSourceType; CFStringRef kPropertyUnicodeKeyLayoutData; + CFStringRef kTypeKeyboardInputMethodModeEnabled; } tis; } _GLFWlibraryNS; diff --git a/src/cocoa_window.m b/src/cocoa_window.m index 8b813643..c97a97d6 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -321,7 +321,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; #endif } -- (void)imeStatusChangeNotified:(NSNotification *)notification { +- (void)imeStatusChangeNotified:(NSNotification *)notification +{ _glfwInputIMEStatus(window); } @@ -569,7 +570,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; const int key = translateKey([event keyCode]); const int mods = translateFlags([event modifierFlags]); - _glfwInputKey(window, key, [event keyCode], GLFW_PRESS, mods); + if (![self hasMarkedText]) + _glfwInputKey(window, key, [event keyCode], GLFW_PRESS, mods); [self interpretKeyEvents:@[event]]; } @@ -662,7 +664,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; - (NSRange)markedRange { if ([markedText length] > 0) - return NSMakeRange(0, [markedText length] - 1); + return NSMakeRange(0, [markedText length]); else return kEmptyRange; } @@ -684,59 +686,92 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; NSString* markedTextString = markedText.string; - NSUInteger i, length = [markedTextString length]; - int ctext = window->ctext; - while (ctext < length+1) { - ctext = (ctext == 0) ? 1 : ctext*2; - } - if (ctext != window->ctext) { - unsigned int* preeditText = realloc(window->preeditText, sizeof(unsigned int)*ctext); - if (preeditText == NULL) { - return; - } - window->preeditText = preeditText; - window->ctext = ctext; - } - window->ntext = length; - window->preeditText[length] = 0; - for (i = 0; i < length; i++) + NSUInteger textLen = [markedTextString length]; + _GLFWpreedit* preedit = &window->preedit; + int textBufferCount = preedit->textBufferCount; + while (textBufferCount < textLen + 1) + textBufferCount = textBufferCount == 0 ? 1 : textBufferCount * 2; + if (textBufferCount != preedit->textBufferCount) { - const unichar codepoint = [markedTextString characterAtIndex:i]; - window->preeditText[i] = codepoint; + unsigned int* preeditText = _glfw_realloc(preedit->text, + sizeof(unsigned int) * textBufferCount); + if (preeditText == NULL) + return; + preedit->text = preeditText; + preedit->textBufferCount = textBufferCount; } - int focusedBlock = 0; - NSInteger offset = 0; - window->nblocks = 0; - while (offset < length) { - NSRange effectiveRange; - NSDictionary *attributes = [markedText attributesAtIndex:offset effectiveRange:&effectiveRange]; - if (window->nblocks == window->cblocks) { - int cblocks = window->cblocks * 2; - int* blocks = realloc(window->preeditAttributeBlocks, sizeof(int)*cblocks); - if (blocks == NULL) { + // NSString handles text data in UTF16 by default, so we have to convert them + // to UTF32. Not only the encoding, but also the number of characters and + // the position of each block. + int currentBlockIndex = 0; + int currentBlockLength = 0; + int currentBlockLocation = 0; + int focusedBlockIndex = 0; + NSInteger preeditTextLength = 0; + NSRange range = NSMakeRange(0, textLen); + while (range.length) + { + uint32_t codepoint = 0; + NSRange currentBlockRange; + [markedText attributesAtIndex:range.location + effectiveRange:¤tBlockRange]; + + if (preedit->blockSizesBufferCount < 1 + currentBlockIndex) + { + int blockBufferCount = (preedit->blockSizesBufferCount == 0) + ? 1 : preedit->blockSizesBufferCount * 2; + int* blocks = _glfw_realloc(preedit->blockSizes, + sizeof(int) * blockBufferCount); + if (blocks == NULL) return; - } - window->preeditAttributeBlocks = blocks; - window->cblocks = cblocks; + preedit->blockSizes = blocks; + preedit->blockSizesBufferCount = blockBufferCount; } - window->preeditAttributeBlocks[window->nblocks] = effectiveRange.length; - offset += effectiveRange.length; - if (effectiveRange.length == 0) { - break; + + if (currentBlockLocation != currentBlockRange.location) + { + currentBlockLocation = currentBlockRange.location; + preedit->blockSizes[currentBlockIndex++] = currentBlockLength; + currentBlockLength = 0; + if (selectedRange.location == currentBlockRange.location) + focusedBlockIndex = currentBlockIndex; } - NSNumber* underline = (NSNumber*) [attributes objectForKey:@"NSUnderline"]; - if ([underline intValue] != 1) { - focusedBlock = window->nblocks; + + if ([markedTextString getBytes:&codepoint + maxLength:sizeof(codepoint) + usedLength:NULL + encoding:NSUTF32StringEncoding + options:0 + range:range + remainingRange:&range]) + { + if (codepoint >= 0xf700 && codepoint <= 0xf7ff) + continue; + + preedit->text[preeditTextLength++] = codepoint; + currentBlockLength++; } - window->nblocks++; } - _glfwInputPreedit(window, focusedBlock); + preedit->blockSizes[currentBlockIndex] = currentBlockLength; + preedit->blockSizesCount = 1 + currentBlockIndex; + preedit->textCount = preeditTextLength; + preedit->text[preeditTextLength] = 0; + preedit->focusedBlockIndex = focusedBlockIndex; + // The caret is always at the last of preedit in macOS. + preedit->caretIndex = preeditTextLength; + + _glfwInputPreedit(window); } - (void)unmarkText { [[markedText mutableString] setString:@""]; + window->preedit.blockSizesCount = 0; + window->preedit.textCount = 0; + window->preedit.focusedBlockIndex = 0; + window->preedit.caretIndex = 0; + _glfwInputPreedit(window); } - (NSArray*)validAttributesForMarkedText @@ -758,8 +793,19 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - const NSRect frame = [window->ns.view frame]; - return NSMakeRect(frame.origin.x, frame.origin.y, 0.0, 0.0); + int x = window->preedit.cursorPosX; + int y = window->preedit.cursorPosY; + int w = window->preedit.cursorWidth; + int h = window->preedit.cursorHeight; + + const NSRect frame = + [window->ns.object contentRectForFrameRect:[window->ns.object frame]]; + + return NSMakeRect(frame.origin.x + x, + // The y-axis is upward on macOS, so this conversion is needed. + frame.origin.y + frame.size.height - y - h, + w, + h); } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange @@ -793,6 +839,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; _glfwInputChar(window, codepoint, mods, plain); } } + + [self unmarkText]; } - (void)doCommandBySelector:(SEL)selector @@ -1038,10 +1086,10 @@ GLFWbool _glfwCreateWindowCocoa(_GLFWwindow* window, } [[NSNotificationCenter defaultCenter] - addObserver: window->ns.delegate - selector:@selector(imeStatusChangeNotified:) - name:NSTextInputContextKeyboardSelectionDidChangeNotification - object: nil]; + addObserver:window->ns.delegate + selector:@selector(imeStatusChangeNotified:) + name:NSTextInputContextKeyboardSelectionDidChangeNotification + object:nil]; return GLFW_TRUE; @@ -1055,7 +1103,7 @@ void _glfwDestroyWindowCocoa(_GLFWwindow* window) if (_glfw.ns.disabledCursorWindow == window) _glfw.ns.disabledCursorWindow = NULL; - [[NSNotificationCenter defaultCenter] removeObserver: window->ns.delegate]; + [[NSNotificationCenter defaultCenter] removeObserver:window->ns.delegate]; [window->ns.object orderOut:nil]; @@ -1946,19 +1994,114 @@ const char* _glfwGetClipboardStringCocoa(void) void _glfwUpdatePreeditCursorRectangleCocoa(_GLFWwindow* window) { + // Do nothing. Instead, implement `firstRectForCharacterRange` callback + // to update the position. } void _glfwResetPreeditTextCocoa(_GLFWwindow* window) { + @autoreleasepool { + + NSTextInputContext* context = [NSTextInputContext currentInputContext]; + [context discardMarkedText]; + [window->ns.view unmarkText]; + + } // autoreleasepool } void _glfwSetIMEStatusCocoa(_GLFWwindow* window, int active) { + @autoreleasepool { + + if (active) + { + NSArray* locales = CFBridgingRelease(CFLocaleCopyPreferredLanguages()); + // Select the most preferred locale. + CFStringRef locale = (__bridge CFStringRef) [locales firstObject]; + if (locale) + { + TISInputSourceRef source = TISCopyInputSourceForLanguage(locale); + if (source) + { + CFStringRef sourceType = TISGetInputSourceProperty(source, + kTISPropertyInputSourceType); + + if (sourceType != kTISTypeKeyboardInputMethodModeEnabled) + TISSelectInputSource(source); + else + { + // Some IMEs return a input-method that has input-method-modes for `TISCopyInputSourceForLanguage()`. + // We can't select these input-methods directly, but need to find + // a input-method-mode of the input-method. + // Example: + // - Input Method: com.apple.inputmethod.SCIM + // - Input Mode: com.apple.inputmethod.SCIM.ITABC + NSString* sourceID = + (__bridge NSString *) TISGetInputSourceProperty(source, kTISPropertyInputSourceID); + NSDictionary* properties = @{ + (__bridge NSString *) kTISPropertyInputSourceCategory: (__bridge NSString *) kTISCategoryKeyboardInputSource, + (__bridge NSString *) kTISPropertyInputSourceIsSelectCapable: @YES, + }; + NSArray* selectableSources = + CFBridgingRelease(TISCreateInputSourceList((__bridge CFDictionaryRef) properties, NO)); + for (id sourceCandidate in selectableSources) + { + TISInputSourceRef sourceCandidateRef = (__bridge TISInputSourceRef) sourceCandidate; + NSString* sourceCandidateID = + (__bridge NSString *) TISGetInputSourceProperty(sourceCandidateRef, kTISPropertyInputSourceID); + if ([sourceCandidateID hasPrefix:sourceID]) + { + TISSelectInputSource(sourceCandidateRef); + break; + } + } + } + + CFRelease(source); + } + } + } + else + { + TISInputSourceRef source = TISCopyCurrentASCIICapableKeyboardInputSource(); + TISSelectInputSource(source); + CFRelease(source); + } + + // `NSTextInputContextKeyboardSelectionDidChangeNotification` is sometimes + // not called immediately after this, so call the callback here. + _glfwInputIMEStatus(window); + + } // autoreleasepool } int _glfwGetIMEStatusCocoa(_GLFWwindow* window) { - return GLFW_FALSE; + @autoreleasepool { + + NSArray* asciiInputSources = + CFBridgingRelease(TISCreateASCIICapableInputSourceList()); + + TISInputSourceRef currentSource = TISCopyCurrentKeyboardInputSource(); + NSString* currentSourceID = + (__bridge NSString *) TISGetInputSourceProperty(currentSource, + kTISPropertyInputSourceID); + CFRelease(currentSource); + + for (int i = 0; i < [asciiInputSources count]; i++) + { + TISInputSourceRef asciiSource = + (__bridge TISInputSourceRef) [asciiInputSources objectAtIndex:i]; + NSString* asciiSourceID = + (__bridge NSString *) TISGetInputSourceProperty(asciiSource, + kTISPropertyInputSourceID); + if ([asciiSourceID compare:currentSourceID] == NSOrderedSame) + return GLFW_FALSE; + } + + return GLFW_TRUE; + + } // autoreleasepool } EGLenum _glfwGetEGLPlatformCocoa(EGLint** attribs) @@ -2114,49 +2257,6 @@ VkResult _glfwCreateWindowSurfaceCocoa(VkInstance instance, } // autoreleasepool } -void _glfwPlatformResetPreeditText(_GLFWwindow* window) -{ - NSTextInputContext *context = [NSTextInputContext currentInputContext]; - [context discardMarkedText]; - [window->ns.view unmarkText]; -} - -void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) -{ - // Mac OS has several input sources. - // this code assumes input methods not in ascii capable inputs using IME. - NSArray* asciiInputSources = CFBridgingRelease(TISCreateASCIICapableInputSourceList()); - TISInputSourceRef asciiSource = (__bridge TISInputSourceRef)([asciiInputSources firstObject]); - if (active) { - NSArray* allInputSources = CFBridgingRelease(TISCreateInputSourceList(NULL, false)); - NSString* asciiSourceID = (__bridge NSString *)(TISGetInputSourceProperty(asciiSource, kTISPropertyInputSourceID)); - int i; - int count = [allInputSources count]; - for (i = 0; i < count; i++) { - TISInputSourceRef source = (__bridge TISInputSourceRef)([allInputSources objectAtIndex: i]); - NSString* sourceID = (__bridge NSString *)(TISGetInputSourceProperty(source, kTISPropertyInputSourceID)); - if ([asciiSourceID compare: sourceID] != NSOrderedSame) { - TISSelectInputSource(source); - break; - } - } - } else if (asciiSource) { - TISSelectInputSource(asciiSource); - } -} - -int _glfwPlatformGetIMEStatus(_GLFWwindow* window) -{ - TISInputSourceRef currentSource = TISCopyCurrentKeyboardInputSource(); - NSString* currentSourceID = (__bridge NSString *)(TISGetInputSourceProperty(currentSource, kTISPropertyInputSourceID)); - NSArray* asciiInputSources = CFBridgingRelease(TISCreateASCIICapableInputSourceList()); - TISInputSourceRef asciiSource = (__bridge TISInputSourceRef)([asciiInputSources firstObject]); - if (asciiSource) { - NSString* asciiSourceID = (__bridge NSString *)(TISGetInputSourceProperty(asciiSource, kTISPropertyInputSourceID)); - return ([asciiSourceID compare: currentSourceID] == NSOrderedSame) ? GLFW_FALSE : GLFW_TRUE; - } - return GLFW_FALSE; -} ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// From 6981f7ae837f318c9f58b4932e21e72dc5ad9c99 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Tue, 22 Nov 2022 13:51:20 +0900 Subject: [PATCH 05/13] Apply shibukawa's fix of GLFW for X11 This fix is based on shibukawa's fix: https://github.com/glfw/glfw/pull/658 The differences is the following. * Remove `X_HAVE_UTF8_STRING` branching since the current logic doesn't use it * Replace `XNDestroyCallback` for `XNPreeditAttributes` in `XCreateIC` Co-authored-by: Yoshiki Shibukawa Co-authored-by: Takuro Ashie --- src/x11_platform.h | 11 ++ src/x11_window.c | 250 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 251 insertions(+), 10 deletions(-) diff --git a/src/x11_platform.h b/src/x11_platform.h index ecc00c19..e82c9f3c 100644 --- a/src/x11_platform.h +++ b/src/x11_platform.h @@ -546,6 +546,17 @@ typedef struct _GLFWwindowX11 // The time of the last KeyPress event per keycode, for discarding // duplicate key events generated for some keys by ibus Time keyPressTimes[256]; + + // Preedit callbacks + XIMCallback preeditStartCallback; + XIMCallback preeditDoneCallback; + XIMCallback preeditDrawCallback; + XIMCallback preeditCaretCallback; + XIMCallback statusStartCallback; + XIMCallback statusDoneCallback; + XIMCallback statusDrawCallback; + + int imeFocus; } _GLFWwindowX11; // X11-specific global data diff --git a/src/x11_window.c b/src/x11_window.c index 447948e1..1d00fb7c 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -553,12 +553,197 @@ static void enableCursor(_GLFWwindow* window) updateCursorImage(window); } +// TODO This callback is replaced by _createXIMPreeditCallbacks. Is there a possibility that this clearing process is necessary? // Clear its handle when the input context has been destroyed -// -static void inputContextDestroyCallback(XIC ic, XPointer clientData, XPointer callData) +// static void inputContextDestroyCallback(XIC ic, XPointer clientData, XPointer callData) +// { +// _GLFWwindow* window = (_GLFWwindow*) clientData; +// window->x11.ic = NULL; +// } + +// Update cursor position to decide candidate window +static void _ximChangeCursorPosition(XIC xic, _GLFWwindow* window) { - _GLFWwindow* window = (_GLFWwindow*) clientData; - window->x11.ic = NULL; + XVaNestedList preedit_attr; + XPoint spot; + + spot.x = window->preeditCursorPosX; + spot.y = window->preeditCursorPosY + window->preeditCursorHeight; + preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &spot, NULL); + XSetICValues(xic, XNPreeditAttributes, preedit_attr, NULL); + XFree(preedit_attr); +} + +// IME Start callback (do nothing) +static void _ximPreeditStartCallback(XIC xic, XPointer clientData, XPointer callData) +{ +} + +// IME Done callback (do nothing) +static void _ximPreeditDoneCallback(XIC xic, XPointer clientData, XPointer callData) +{ +} + +// IME Draw callback +static void _ximPreeditDrawCallback(XIC xic, XPointer clientData, XIMPreeditDrawCallbackStruct *callData) +{ + int i, j, length, ctext, rstart, rend; + XIMText* text; + const char* src; + unsigned int codePoint; + unsigned int* preeditText; + XIMFeedback f; + _GLFWwindow* window = (_GLFWwindow*)clientData; + + // keep cursor position to reduce API call + int cursorX = window->preeditCursorPosX; + int cursorY = window->preeditCursorPosY; + int cursorHeight = window->preeditCursorHeight; + + if (!callData->text) { + // preedit text is empty + window->ntext = 0; + window->nblocks = 0; + _glfwInputPreedit(window, 0); + return; + } else { + text = callData->text; + length = callData->chg_length; + if (text->encoding_is_wchar) { + // wchar is not supported + return; + } + ctext = window->ctext; + while (ctext < length+1) { + ctext = (ctext == 0) ? 1 : ctext * 2; + } + if (ctext != window->ctext) { + preeditText = _glfw_realloc(window->preeditText, sizeof(unsigned int)*ctext); + if (preeditText == NULL) { + return; + } + window->preeditText = preeditText; + window->ctext = ctext; + } + window->ntext = length; + window->preeditText[length] = 0; + if (window->cblocks == 0) { + window->preeditAttributeBlocks = _glfw_calloc(4, sizeof(int)); + window->cblocks = 4; + } + src = text->string.multi_byte; + rend = 0; + rstart = length; + for (i = 0, j = 0; i < text->length; i++) { + codePoint = decodeUTF8(&src); + if (i < callData->chg_first || callData->chg_first+length < i) { + continue; + } + window->preeditText[j++] = codePoint; + f = text->feedback[i]; + if ((f & XIMReverse) || (f & XIMHighlight)) { + rend = i; + if (i < rstart) { + rstart = i; + } + } + } + if (rstart == length) { + window->nblocks = 1; + window->preeditAttributeBlocks[0] = length; + window->preeditAttributeBlocks[1] = 0; + _glfwInputPreedit(window, 0); + } else if (rstart == 0) { + if (rend == length -1) { + window->nblocks = 1; + window->preeditAttributeBlocks[0] = length; + window->preeditAttributeBlocks[1] = 0; + _glfwInputPreedit(window, 0); + } else { + window->nblocks = 2; + window->preeditAttributeBlocks[0] = rend + 1; + window->preeditAttributeBlocks[1] = length - rend - 1; + window->preeditAttributeBlocks[2] = 0; + _glfwInputPreedit(window, 0); + } + } else if (rend == length -1) { + window->nblocks = 2; + window->preeditAttributeBlocks[0] = rstart; + window->preeditAttributeBlocks[1] = length - rstart; + window->preeditAttributeBlocks[2] = 0; + _glfwInputPreedit(window, 1); + } else { + window->nblocks = 3; + window->preeditAttributeBlocks[0] = rstart; + window->preeditAttributeBlocks[1] = rend - rstart + 1; + window->preeditAttributeBlocks[2] = length - rend - 1; + window->preeditAttributeBlocks[3] = 0; + _glfwInputPreedit(window, 1); + } + if ((cursorX != window->preeditCursorPosX) + || (cursorY != window->preeditCursorPosY) + || (cursorHeight != window->preeditCursorHeight)) { + _ximChangeCursorPosition(xic, window); + } + } +} + +// IME Caret callback (do nothing) +static void _ximPreeditCaretCallback(XIC xic, XPointer clientData, XPointer callData) +{ +} + +static void _ximStatusStartCallback(XIC xic, XPointer clientData, XPointer callData) +{ + _GLFWwindow* window = (_GLFWwindow*)clientData; + window->x11.imeFocus = GLFW_TRUE; +} + +static void _ximStatusDoneCallback(XIC xic, XPointer clientData, XPointer callData) +{ + _GLFWwindow* window = (_GLFWwindow*)clientData; + window->x11.imeFocus = GLFW_FALSE; +} + +static void _ximStatusDrawCallback(XIC xic, XPointer clientData, XIMStatusDrawCallbackStruct* callData) +{ + _GLFWwindow* window = (_GLFWwindow*)clientData; + _glfwInputIMEStatus(window); +} + +// Create XIM Preedit callback +static XVaNestedList _createXIMPreeditCallbacks(_GLFWwindow* window) +{ + window->x11.preeditStartCallback.client_data = (XPointer)window; + window->x11.preeditStartCallback.callback = (XIMProc)_ximPreeditStartCallback; + window->x11.preeditDoneCallback.client_data = (XPointer)window; + window->x11.preeditDoneCallback.callback = (XIMProc)_ximPreeditDoneCallback; + window->x11.preeditDrawCallback.client_data = (XPointer)window; + window->x11.preeditDrawCallback.callback = (XIMProc)_ximPreeditDrawCallback; + window->x11.preeditCaretCallback.client_data = (XPointer)window; + window->x11.preeditCaretCallback.callback = (XIMProc)_ximPreeditCaretCallback; + return XVaCreateNestedList (0, + XNPreeditStartCallback, &window->x11.preeditStartCallback.client_data, + XNPreeditDoneCallback, &window->x11.preeditDoneCallback.client_data, + XNPreeditDrawCallback, &window->x11.preeditDrawCallback.client_data, + XNPreeditCaretCallback, &window->x11.preeditCaretCallback.client_data, + NULL); +} + +// Create XIM status callback +static XVaNestedList _createXIMStatusCallbacks(_GLFWwindow* window) +{ + window->x11.statusStartCallback.client_data = (XPointer)window; + window->x11.statusStartCallback.callback = (XIMProc)_ximStatusStartCallback; + window->x11.statusDoneCallback.client_data = (XPointer)window; + window->x11.statusDoneCallback.callback = (XIMProc)_ximStatusDoneCallback; + window->x11.statusDrawCallback.client_data = (XPointer)window; + window->x11.statusDrawCallback.callback = (XIMProc)_ximStatusDrawCallback; + return XVaCreateNestedList (0, + XNStatusStartCallback, &window->x11.statusStartCallback.client_data, + XNStatusDoneCallback, &window->x11.statusDoneCallback.client_data, + XNStatusDrawCallback, &window->x11.statusDrawCallback.client_data, + NULL); } // Create the X11 window (and its colormap) @@ -1921,21 +2106,26 @@ void _glfwPushSelectionToManagerX11(void) void _glfwCreateInputContextX11(_GLFWwindow* window) { - XIMCallback callback; - callback.callback = (XIMProc) inputContextDestroyCallback; - callback.client_data = (XPointer) window; + XVaNestedList preeditList = _createXIMPreeditCallbacks(window); + XVaNestedList statusList = _createXIMStatusCallbacks(window); window->x11.ic = XCreateIC(_glfw.x11.im, XNInputStyle, - XIMPreeditNothing | XIMStatusNothing, + XIMPreeditCallbacks | XIMStatusCallbacks, XNClientWindow, window->x11.handle, XNFocusWindow, window->x11.handle, - XNDestroyCallback, - &callback, + XNPreeditAttributes, + preeditList, + XNStatusAttributes, + statusList, NULL); + XFree(preeditList); + XFree(statusList); + window->x11.imeFocus = GLFW_FALSE; + if (window->x11.ic) { XWindowAttributes attribs; @@ -3299,6 +3489,46 @@ VkResult _glfwCreateWindowSurfaceX11(VkInstance instance, } } +void _glfwPlatformResetPreeditText(_GLFWwindow* window) { + XIC ic = window->x11.ic; + + /* restore conversion state after resetting ic later */ + XIMPreeditState preedit_state = XIMPreeditUnKnown; + XVaNestedList preedit_attr; + char* result; + + if (window->ntext == 0) + return; + + preedit_attr = XVaCreateNestedList(0, XNPreeditState, &preedit_state, NULL); + XGetICValues(ic, XNPreeditAttributes, preedit_attr, NULL); + XFree(preedit_attr); + + result = XmbResetIC(ic); + + preedit_attr = XVaCreateNestedList(0, XNPreeditState, preedit_state, NULL); + XSetICValues(ic, XNPreeditAttributes, preedit_attr, NULL); + XFree(preedit_attr); + + window->ntext = 0; + window->nblocks = 0; + _glfwInputPreedit(window, 0); + + XFree (result); +} + +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) { + XIC ic = window->x11.ic; + if (active) { + XSetICFocus(ic); + } else { + XUnsetICFocus(ic); + } +} + +int _glfwPlatformGetIMEStatus(_GLFWwindow* window) { + return window->x11.imeFocus; +} ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// From b0506b7912d7829a4b49a8be8360ae9a7f1df8c5 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Tue, 6 Dec 2022 10:33:58 +0900 Subject: [PATCH 06/13] X11: Support IME This commit re-organizes 6e7f93916b96c643ca7abe45d09f72d841ff15ed. * Load missing XIM related function symbols. * Generalize platform-specific features to _GLFWplatform. * Change the defalut input style to over-the-spot style. * Rename `decodeUTF8()` to `_glfwDecodeUTF8()` to make it as internal API. * It will be also needed to implment input method for Wayland. * Refactor code shapes and variable names. About over-the-spot style and on-the-spot style on X11: * In over-the-spot mode, almost all APIs are disabled since applications only need to specify the preedit candidate window position by `glfwSetPreeditCursorPos()`. * We can change the style by enabling `GLFW_X11_ONTHESPOT` init hint, but it has the following problems. * Status APIs don't work because status callbacks don't work. (at least in my ibus environment). * Can't specify the candidate window position. Known problems: * Some keys (arrow, Enter, BackSpace, ...) are passed to applications during preediting. * This will be fixed in PR #1972 : https://github.com/glfw/glfw/pull/1972 Co-authored-by: Takuro Ashie --- include/GLFW/glfw3.h | 13 ++ src/init.c | 27 +++ src/internal.h | 2 + src/x11_init.c | 13 +- src/x11_platform.h | 14 ++ src/x11_window.c | 504 +++++++++++++++++++++++++------------------ 6 files changed, 362 insertions(+), 211 deletions(-) diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 76a51257..496bc017 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -1324,6 +1324,11 @@ extern "C" { * X11 specific [init hint](@ref GLFW_X11_XCB_VULKAN_SURFACE_hint). */ #define GLFW_X11_XCB_VULKAN_SURFACE 0x00052001 +/*! @brief X11 specific init hint. + * + * X11 specific [init hint](@ref GLFW_X11_ONTHESPOT_hint). + */ +#define GLFW_X11_ONTHESPOT 0x00052002 /*! @brief Wayland specific init hint. * * Wayland specific [init hint](@ref GLFW_WAYLAND_LIBDECOR_hint). @@ -5249,6 +5254,9 @@ GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int * * @param[in] window The window. * + * @remark @x11 Since over-the-spot style is used by default, you don't need + * to use this function. + * * @par Thread Safety * This function may only be called from the main thread. * @@ -5423,6 +5431,9 @@ GLFWAPI GLFWcharmodsfun glfwSetCharModsCallback(GLFWwindow* window, GLFWcharmods * For more information about the callback parameters, see the * [function pointer type](@ref GLFWpreeditfun). * + * @remark @x11 Since over-the-spot style is used by default, you don't need + * to use this function. + * * @par Thread Safety * This function may only be called from the main thread. * @@ -5452,6 +5463,8 @@ 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. + * * @par Thread Safety * This function may only be called from the main thread. * diff --git a/src/init.c b/src/init.c index dbd5a900..6619b0ec 100644 --- a/src/init.c +++ b/src/init.c @@ -61,6 +61,7 @@ static _GLFWinitconfig _glfwInitHints = .x11 = { .xcbVulkanSurface = GLFW_TRUE, + .onTheSpotIMStyle = GLFW_FALSE }, .wl = { @@ -175,6 +176,29 @@ size_t _glfwEncodeUTF8(char* s, uint32_t codepoint) return count; } +// Decode a Unicode code point from a UTF-8 stream +// Based on cutef8 by Jeff Bezanson (Public Domain) +// +uint32_t _glfwDecodeUTF8(const char** s) +{ + uint32_t codepoint = 0, count = 0; + static const uint32_t offsets[] = + { + 0x00000000u, 0x00003080u, 0x000e2080u, + 0x03c82080u, 0xfa082080u, 0x82082080u + }; + + do + { + codepoint = (codepoint << 6) + (unsigned char) **s; + (*s)++; + count++; + } while ((**s & 0xc0) == 0x80); + + assert(count <= 6); + return codepoint - offsets[count - 1]; +} + // Splits and translates a text/uri-list into separate file paths // NOTE: This function destroys the provided string // @@ -459,6 +483,9 @@ GLFWAPI void glfwInitHint(int hint, int value) case GLFW_X11_XCB_VULKAN_SURFACE: _glfwInitHints.x11.xcbVulkanSurface = value; return; + case GLFW_X11_ONTHESPOT: + _glfwInitHints.x11.onTheSpotIMStyle = value; + return; case GLFW_WAYLAND_LIBDECOR: _glfwInitHints.wl.libdecorMode = value; return; diff --git a/src/internal.h b/src/internal.h index 93722387..893f780c 100644 --- a/src/internal.h +++ b/src/internal.h @@ -385,6 +385,7 @@ struct _GLFWinitconfig } ns; struct { GLFWbool xcbVulkanSurface; + GLFWbool onTheSpotIMStyle; } x11; struct { int libdecorMode; @@ -1036,6 +1037,7 @@ void _glfwTerminateVulkan(void); const char* _glfwGetVulkanResultString(VkResult result); size_t _glfwEncodeUTF8(char* s, uint32_t codepoint); +uint32_t _glfwDecodeUTF8(const char** s); char** _glfwParseUriList(char* text, int* count); char* _glfw_strdup(const char* source); diff --git a/src/x11_init.c b/src/x11_init.c index 0ccf9454..1268296f 100644 --- a/src/x11_init.c +++ b/src/x11_init.c @@ -445,9 +445,14 @@ static GLFWbool hasUsableInputMethodStyle(void) if (XGetIMValues(_glfw.x11.im, XNQueryInputStyle, &styles, NULL) != NULL) return GLFW_FALSE; + if (_glfw.hints.init.x11.onTheSpotIMStyle) + _glfw.x11.imStyle = STYLE_ONTHESPOT; + else + _glfw.x11.imStyle = STYLE_OVERTHESPOT; + for (unsigned int i = 0; i < styles->count_styles; i++) { - if (styles->supported_styles[i] == (XIMPreeditNothing | XIMStatusNothing)) + if (styles->supported_styles[i] == _glfw.x11.imStyle) { found = GLFW_TRUE; break; @@ -1455,6 +1460,8 @@ int _glfwInitX11(void) _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XSetErrorHandler"); _glfw.x11.xlib.SetICFocus = (PFN_XSetICFocus) _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XSetICFocus"); + _glfw.x11.xlib.SetICValues = (PFN_XSetICValues) + _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XSetICValues"); _glfw.x11.xlib.SetIMValues = (PFN_XSetIMValues) _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XSetIMValues"); _glfw.x11.xlib.SetInputFocus = (PFN_XSetInputFocus) @@ -1485,6 +1492,8 @@ int _glfwInitX11(void) _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XUnmapWindow"); _glfw.x11.xlib.UnsetICFocus = (PFN_XUnsetICFocus) _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XUnsetICFocus"); + _glfw.x11.xlib.VaCreateNestedList = (PFN_XVaCreateNestedList) + _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XVaCreateNestedList"); _glfw.x11.xlib.VisualIDFromVisual = (PFN_XVisualIDFromVisual) _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XVisualIDFromVisual"); _glfw.x11.xlib.WarpPointer = (PFN_XWarpPointer) @@ -1517,6 +1526,8 @@ int _glfwInitX11(void) _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XrmUniqueQuark"); _glfw.x11.xlib.UnregisterIMInstantiateCallback = (PFN_XUnregisterIMInstantiateCallback) _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XUnregisterIMInstantiateCallback"); + _glfw.x11.xlib.mbResetIC = (PFN_XmbResetIC) + _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "XmbResetIC"); _glfw.x11.xlib.utf8LookupString = (PFN_Xutf8LookupString) _glfwPlatformGetModuleSymbol(_glfw.x11.xlib.handle, "Xutf8LookupString"); _glfw.x11.xlib.utf8SetWMProperties = (PFN_Xutf8SetWMProperties) diff --git a/src/x11_platform.h b/src/x11_platform.h index e82c9f3c..4764dbb3 100644 --- a/src/x11_platform.h +++ b/src/x11_platform.h @@ -91,6 +91,9 @@ #define GLX_CONTEXT_RELEASE_BEHAVIOR_FLUSH_ARB 0x2098 #define GLX_CONTEXT_OPENGL_NO_ERROR_ARB 0x31b3 +#define STYLE_OVERTHESPOT (XIMPreeditNothing | XIMStatusNothing) +#define STYLE_ONTHESPOT (XIMPreeditCallbacks | XIMStatusCallbacks) + typedef XID GLXWindow; typedef XID GLXDrawable; typedef struct __GLXFBConfig* GLXFBConfig; @@ -165,6 +168,7 @@ typedef Status (* PFN_XSendEvent)(Display*,Window,Bool,long,XEvent*); typedef int (* PFN_XSetClassHint)(Display*,Window,XClassHint*); typedef XErrorHandler (* PFN_XSetErrorHandler)(XErrorHandler); typedef void (* PFN_XSetICFocus)(XIC); +typedef char* (* PFN_XSetICValues)(XIC,...); typedef char* (* PFN_XSetIMValues)(XIM,...); typedef int (* PFN_XSetInputFocus)(Display*,Window,int,Time); typedef char* (* PFN_XSetLocaleModifiers)(const char*); @@ -180,6 +184,7 @@ typedef int (* PFN_XUndefineCursor)(Display*,Window); typedef int (* PFN_XUngrabPointer)(Display*,Time); typedef int (* PFN_XUnmapWindow)(Display*,Window); typedef void (* PFN_XUnsetICFocus)(XIC); +typedef XVaNestedList (* PFN_XVaCreateNestedList)(int,...); typedef VisualID (* PFN_XVisualIDFromVisual)(Visual*); typedef int (* PFN_XWarpPointer)(Display*,Window,Window,int,int,unsigned int,unsigned int,int,int); typedef void (* PFN_XkbFreeKeyboard)(XkbDescPtr,unsigned int,Bool); @@ -191,6 +196,7 @@ typedef KeySym (* PFN_XkbKeycodeToKeysym)(Display*,KeyCode,int,int); typedef Bool (* PFN_XkbQueryExtension)(Display*,int*,int*,int*,int*,int*); typedef Bool (* PFN_XkbSelectEventDetails)(Display*,unsigned int,unsigned int,unsigned long,unsigned long); typedef Bool (* PFN_XkbSetDetectableAutoRepeat)(Display*,Bool,Bool*); +typedef char* (* PFN_XmbResetIC)(XIC); typedef void (* PFN_XrmDestroyDatabase)(XrmDatabase); typedef Bool (* PFN_XrmGetResource)(XrmDatabase,const char*,const char*,char**,XrmValue*); typedef XrmDatabase (* PFN_XrmGetStringDatabase)(const char*); @@ -265,6 +271,7 @@ typedef void (* PFN_Xutf8SetWMProperties)(Display*,Window,const char*,const char #define XSetClassHint _glfw.x11.xlib.SetClassHint #define XSetErrorHandler _glfw.x11.xlib.SetErrorHandler #define XSetICFocus _glfw.x11.xlib.SetICFocus +#define XSetICValues _glfw.x11.xlib.SetICValues #define XSetIMValues _glfw.x11.xlib.SetIMValues #define XSetInputFocus _glfw.x11.xlib.SetInputFocus #define XSetLocaleModifiers _glfw.x11.xlib.SetLocaleModifiers @@ -280,6 +287,7 @@ typedef void (* PFN_Xutf8SetWMProperties)(Display*,Window,const char*,const char #define XUngrabPointer _glfw.x11.xlib.UngrabPointer #define XUnmapWindow _glfw.x11.xlib.UnmapWindow #define XUnsetICFocus _glfw.x11.xlib.UnsetICFocus +#define XVaCreateNestedList _glfw.x11.xlib.VaCreateNestedList #define XVisualIDFromVisual _glfw.x11.xlib.VisualIDFromVisual #define XWarpPointer _glfw.x11.xlib.WarpPointer #define XkbFreeKeyboard _glfw.x11.xkb.FreeKeyboard @@ -291,6 +299,7 @@ typedef void (* PFN_Xutf8SetWMProperties)(Display*,Window,const char*,const char #define XkbQueryExtension _glfw.x11.xkb.QueryExtension #define XkbSelectEventDetails _glfw.x11.xkb.SelectEventDetails #define XkbSetDetectableAutoRepeat _glfw.x11.xkb.SetDetectableAutoRepeat +#define XmbResetIC _glfw.x11.xlib.mbResetIC #define XrmDestroyDatabase _glfw.x11.xrm.DestroyDatabase #define XrmGetResource _glfw.x11.xrm.GetResource #define XrmGetStringDatabase _glfw.x11.xrm.GetStringDatabase @@ -577,6 +586,8 @@ typedef struct _GLFWlibraryX11 XContext context; // XIM input method XIM im; + // XIM input method style + XIMStyle imStyle; // The previous X error handler, to be restored later XErrorHandler errorHandler; // Most recent error code received by X error handler @@ -722,6 +733,7 @@ typedef struct _GLFWlibraryX11 PFN_XSetClassHint SetClassHint; PFN_XSetErrorHandler SetErrorHandler; PFN_XSetICFocus SetICFocus; + PFN_XSetICValues SetICValues; PFN_XSetIMValues SetIMValues; PFN_XSetInputFocus SetInputFocus; PFN_XSetLocaleModifiers SetLocaleModifiers; @@ -737,9 +749,11 @@ typedef struct _GLFWlibraryX11 PFN_XUngrabPointer UngrabPointer; PFN_XUnmapWindow UnmapWindow; PFN_XUnsetICFocus UnsetICFocus; + PFN_XVaCreateNestedList VaCreateNestedList; PFN_XVisualIDFromVisual VisualIDFromVisual; PFN_XWarpPointer WarpPointer; PFN_XUnregisterIMInstantiateCallback UnregisterIMInstantiateCallback; + PFN_XmbResetIC mbResetIC; PFN_Xutf8LookupString utf8LookupString; PFN_Xutf8SetWMProperties utf8SetWMProperties; } xlib; diff --git a/src/x11_window.c b/src/x11_window.c index 1d00fb7c..32d836e6 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -410,29 +410,6 @@ static void updateWindowMode(_GLFWwindow* window) } } -// Decode a Unicode code point from a UTF-8 stream -// Based on cutef8 by Jeff Bezanson (Public Domain) -// -static uint32_t decodeUTF8(const char** s) -{ - uint32_t codepoint = 0, count = 0; - static const uint32_t offsets[] = - { - 0x00000000u, 0x00003080u, 0x000e2080u, - 0x03c82080u, 0xfa082080u, 0x82082080u - }; - - do - { - codepoint = (codepoint << 6) + (unsigned char) **s; - (*s)++; - count++; - } while ((**s & 0xc0) == 0x80); - - assert(count <= 6); - return codepoint - offsets[count - 1]; -} - // Convert the specified Latin-1 string to UTF-8 // static char* convertLatin1toUTF8(const char* source) @@ -553,197 +530,241 @@ static void enableCursor(_GLFWwindow* window) updateCursorImage(window); } -// TODO This callback is replaced by _createXIMPreeditCallbacks. Is there a possibility that this clearing process is necessary? // Clear its handle when the input context has been destroyed -// static void inputContextDestroyCallback(XIC ic, XPointer clientData, XPointer callData) -// { -// _GLFWwindow* window = (_GLFWwindow*) clientData; -// window->x11.ic = NULL; -// } - -// Update cursor position to decide candidate window -static void _ximChangeCursorPosition(XIC xic, _GLFWwindow* window) +// +static void inputContextDestroyCallback(XIC ic, XPointer clientData, XPointer callData) { - XVaNestedList preedit_attr; - XPoint spot; - - spot.x = window->preeditCursorPosX; - spot.y = window->preeditCursorPosY + window->preeditCursorHeight; - preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &spot, NULL); - XSetICValues(xic, XNPreeditAttributes, preedit_attr, NULL); - XFree(preedit_attr); + _GLFWwindow* window = (_GLFWwindow*) clientData; + window->x11.ic = NULL; } // IME Start callback (do nothing) +// static void _ximPreeditStartCallback(XIC xic, XPointer clientData, XPointer callData) { } // IME Done callback (do nothing) +// static void _ximPreeditDoneCallback(XIC xic, XPointer clientData, XPointer callData) { } // IME Draw callback -static void _ximPreeditDrawCallback(XIC xic, XPointer clientData, XIMPreeditDrawCallbackStruct *callData) +// When using the dafault style: STYLE_OVERTHESPOT, this is not used since applications +// don't need to display preedit texts. +// +static void _ximPreeditDrawCallback(XIC xic, XPointer clientData, XIMPreeditDrawCallbackStruct* callData) { - int i, j, length, ctext, rstart, rend; - XIMText* text; - const char* src; - unsigned int codePoint; - unsigned int* preeditText; - XIMFeedback f; - _GLFWwindow* window = (_GLFWwindow*)clientData; + _GLFWwindow* window = (_GLFWwindow*) clientData; + _GLFWpreedit* preedit = &window->preedit; - // keep cursor position to reduce API call - int cursorX = window->preeditCursorPosX; - int cursorY = window->preeditCursorPosY; - int cursorHeight = window->preeditCursorHeight; - - if (!callData->text) { + if (!callData->text) + { // preedit text is empty - window->ntext = 0; - window->nblocks = 0; - _glfwInputPreedit(window, 0); + preedit->textCount = 0; + preedit->blockSizesCount = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = 0; + _glfwInputPreedit(window); return; - } else { - text = callData->text; - length = callData->chg_length; - if (text->encoding_is_wchar) { - // wchar is not supported - return; - } - ctext = window->ctext; - while (ctext < length+1) { - ctext = (ctext == 0) ? 1 : ctext * 2; - } - if (ctext != window->ctext) { - preeditText = _glfw_realloc(window->preeditText, sizeof(unsigned int)*ctext); - if (preeditText == NULL) { + } + else if (callData->text->encoding_is_wchar) + { + // wchar is not supported + return; + } + else + { + XIMText* text = callData->text; + int textLen = preedit->textCount + text->length - callData->chg_length; + int textBufferCount = preedit->textBufferCount; + int i, j, rstart, rend; + const char* src; + + // realloc preedit text + while (textBufferCount < textLen + 1) + textBufferCount = (textBufferCount == 0) ? 1 : textBufferCount * 2; + if (textBufferCount != preedit->textBufferCount) + { + unsigned int* preeditText = _glfw_realloc(preedit->text, + sizeof(unsigned int) * textBufferCount); + if (preeditText == NULL) return; - } - window->preeditText = preeditText; - window->ctext = ctext; + + preedit->text = preeditText; + preedit->textBufferCount = textBufferCount; } - window->ntext = length; - window->preeditText[length] = 0; - if (window->cblocks == 0) { - window->preeditAttributeBlocks = _glfw_calloc(4, sizeof(int)); - window->cblocks = 4; + preedit->textCount = textLen; + preedit->text[textLen] = 0; + + // realloc block sizes + if (preedit->blockSizesBufferCount == 0) + { + preedit->blockSizes = _glfw_calloc(4, sizeof(int)); + preedit->blockSizesBufferCount = 4; } + + // store preedit text src = text->string.multi_byte; rend = 0; - rstart = length; - for (i = 0, j = 0; i < text->length; i++) { - codePoint = decodeUTF8(&src); - if (i < callData->chg_first || callData->chg_first+length < i) { + rstart = textLen; + for (i = 0, j = callData->chg_first; i < text->length; i++) + { + XIMFeedback f; + + if (i < callData->chg_first || callData->chg_first + textLen < i) continue; - } - window->preeditText[j++] = codePoint; + + preedit->text[j++] = _glfwDecodeUTF8(&src); f = text->feedback[i]; - if ((f & XIMReverse) || (f & XIMHighlight)) { + if ((f & XIMReverse) || (f & XIMHighlight)) + { rend = i; - if (i < rstart) { + if (i < rstart) rstart = i; - } } } - if (rstart == length) { - window->nblocks = 1; - window->preeditAttributeBlocks[0] = length; - window->preeditAttributeBlocks[1] = 0; - _glfwInputPreedit(window, 0); - } else if (rstart == 0) { - if (rend == length -1) { - window->nblocks = 1; - window->preeditAttributeBlocks[0] = length; - window->preeditAttributeBlocks[1] = 0; - _glfwInputPreedit(window, 0); - } else { - window->nblocks = 2; - window->preeditAttributeBlocks[0] = rend + 1; - window->preeditAttributeBlocks[1] = length - rend - 1; - window->preeditAttributeBlocks[2] = 0; - _glfwInputPreedit(window, 0); - } - } else if (rend == length -1) { - window->nblocks = 2; - window->preeditAttributeBlocks[0] = rstart; - window->preeditAttributeBlocks[1] = length - rstart; - window->preeditAttributeBlocks[2] = 0; - _glfwInputPreedit(window, 1); - } else { - window->nblocks = 3; - window->preeditAttributeBlocks[0] = rstart; - window->preeditAttributeBlocks[1] = rend - rstart + 1; - window->preeditAttributeBlocks[2] = length - rend - 1; - window->preeditAttributeBlocks[3] = 0; - _glfwInputPreedit(window, 1); + + // store block sizes + // TODO: It doesn't care callData->chg_first != 0 case although it's quite rare. + if (rstart == textLen) + { + preedit->blockSizesCount = 1; + preedit->blockSizes[0] = textLen; + preedit->blockSizes[1] = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = callData->caret; + _glfwInputPreedit(window); } - if ((cursorX != window->preeditCursorPosX) - || (cursorY != window->preeditCursorPosY) - || (cursorHeight != window->preeditCursorHeight)) { - _ximChangeCursorPosition(xic, window); + else if (rstart == 0) + { + if (rend == textLen -1) + { + preedit->blockSizesCount = 1; + preedit->blockSizes[0] = textLen; + preedit->blockSizes[1] = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = callData->caret; + _glfwInputPreedit(window); + } + else + { + preedit->blockSizesCount = 2; + preedit->blockSizes[0] = rend + 1; + preedit->blockSizes[1] = textLen - rend - 1; + preedit->blockSizes[2] = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = callData->caret; + _glfwInputPreedit(window); + } + } + else if (rend == textLen - 1) + { + preedit->blockSizesCount = 2; + preedit->blockSizes[0] = rstart; + preedit->blockSizes[1] = textLen - rstart; + preedit->blockSizes[2] = 0; + preedit->focusedBlockIndex = 1; + preedit->caretIndex = callData->caret; + _glfwInputPreedit(window); + } + else + { + preedit->blockSizesCount = 3; + preedit->blockSizes[0] = rstart; + preedit->blockSizes[1] = rend - rstart + 1; + preedit->blockSizes[2] = textLen - rend - 1; + preedit->blockSizes[3] = 0; + preedit->focusedBlockIndex = 1; + preedit->caretIndex = callData->caret; + _glfwInputPreedit(window); } } } // IME Caret callback (do nothing) +// static void _ximPreeditCaretCallback(XIC xic, XPointer clientData, XPointer callData) { } +// IME Status Start callback +// When using the dafault style: STYLE_OVERTHESPOT, this is not used and the IME status +// can not be taken. +// static void _ximStatusStartCallback(XIC xic, XPointer clientData, XPointer callData) { - _GLFWwindow* window = (_GLFWwindow*)clientData; + _GLFWwindow* window = (_GLFWwindow*) clientData; window->x11.imeFocus = GLFW_TRUE; } +// IME Status Done callback +// When using the dafault style: STYLE_OVERTHESPOT, this is not used and the IME status +// can not be taken. +// static void _ximStatusDoneCallback(XIC xic, XPointer clientData, XPointer callData) { - _GLFWwindow* window = (_GLFWwindow*)clientData; + _GLFWwindow* window = (_GLFWwindow*) clientData; window->x11.imeFocus = GLFW_FALSE; } +// IME Status Draw callback +// When using the dafault style: STYLE_OVERTHESPOT, this is not used and the IME status +// can not be taken. +// static void _ximStatusDrawCallback(XIC xic, XPointer clientData, XIMStatusDrawCallbackStruct* callData) { - _GLFWwindow* window = (_GLFWwindow*)clientData; + _GLFWwindow* window = (_GLFWwindow*) clientData; _glfwInputIMEStatus(window); } // Create XIM Preedit callback +// When using the dafault style: STYLE_OVERTHESPOT, this is not used since applications +// don't need to display preedit texts. +// static XVaNestedList _createXIMPreeditCallbacks(_GLFWwindow* window) { - window->x11.preeditStartCallback.client_data = (XPointer)window; - window->x11.preeditStartCallback.callback = (XIMProc)_ximPreeditStartCallback; - window->x11.preeditDoneCallback.client_data = (XPointer)window; - window->x11.preeditDoneCallback.callback = (XIMProc)_ximPreeditDoneCallback; - window->x11.preeditDrawCallback.client_data = (XPointer)window; - window->x11.preeditDrawCallback.callback = (XIMProc)_ximPreeditDrawCallback; - window->x11.preeditCaretCallback.client_data = (XPointer)window; - window->x11.preeditCaretCallback.callback = (XIMProc)_ximPreeditCaretCallback; - return XVaCreateNestedList (0, - XNPreeditStartCallback, &window->x11.preeditStartCallback.client_data, - XNPreeditDoneCallback, &window->x11.preeditDoneCallback.client_data, - XNPreeditDrawCallback, &window->x11.preeditDrawCallback.client_data, - XNPreeditCaretCallback, &window->x11.preeditCaretCallback.client_data, - NULL); + window->x11.preeditStartCallback.client_data = (XPointer) window; + window->x11.preeditStartCallback.callback = (XIMProc) _ximPreeditStartCallback; + window->x11.preeditDoneCallback.client_data = (XPointer) window; + window->x11.preeditDoneCallback.callback = (XIMProc) _ximPreeditDoneCallback; + window->x11.preeditDrawCallback.client_data = (XPointer) window; + window->x11.preeditDrawCallback.callback = (XIMProc) _ximPreeditDrawCallback; + window->x11.preeditCaretCallback.client_data = (XPointer) window; + window->x11.preeditCaretCallback.callback = (XIMProc) _ximPreeditCaretCallback; + return XVaCreateNestedList(0, + XNPreeditStartCallback, + &window->x11.preeditStartCallback.client_data, + XNPreeditDoneCallback, + &window->x11.preeditDoneCallback.client_data, + XNPreeditDrawCallback, + &window->x11.preeditDrawCallback.client_data, + XNPreeditCaretCallback, + &window->x11.preeditCaretCallback.client_data, + NULL); } // Create XIM status callback +// When using the dafault style: STYLE_OVERTHESPOT, this is not used and the IME status +// can not be taken. +// static XVaNestedList _createXIMStatusCallbacks(_GLFWwindow* window) { - window->x11.statusStartCallback.client_data = (XPointer)window; - window->x11.statusStartCallback.callback = (XIMProc)_ximStatusStartCallback; - window->x11.statusDoneCallback.client_data = (XPointer)window; - window->x11.statusDoneCallback.callback = (XIMProc)_ximStatusDoneCallback; - window->x11.statusDrawCallback.client_data = (XPointer)window; - window->x11.statusDrawCallback.callback = (XIMProc)_ximStatusDrawCallback; - return XVaCreateNestedList (0, - XNStatusStartCallback, &window->x11.statusStartCallback.client_data, - XNStatusDoneCallback, &window->x11.statusDoneCallback.client_data, - XNStatusDrawCallback, &window->x11.statusDrawCallback.client_data, - NULL); + window->x11.statusStartCallback.client_data = (XPointer) window; + window->x11.statusStartCallback.callback = (XIMProc) _ximStatusStartCallback; + window->x11.statusDoneCallback.client_data = (XPointer) window; + window->x11.statusDoneCallback.callback = (XIMProc) _ximStatusDoneCallback; + window->x11.statusDrawCallback.client_data = (XPointer) window; + window->x11.statusDrawCallback.callback = (XIMProc) _ximStatusDrawCallback; + return XVaCreateNestedList(0, + XNStatusStartCallback, + &window->x11.statusStartCallback.client_data, + XNStatusDoneCallback, + &window->x11.statusDoneCallback.client_data, + XNStatusDrawCallback, + &window->x11.statusDrawCallback.client_data, + NULL); } // Create the X11 window (and its colormap) @@ -1475,7 +1496,7 @@ static void processEvent(XEvent *event) const char* c = chars; chars[count] = '\0'; while (c - chars < count) - _glfwInputChar(window, decodeUTF8(&c), mods, plain); + _glfwInputChar(window, _glfwDecodeUTF8(&c), mods, plain); } if (chars != buffer) @@ -2106,26 +2127,60 @@ void _glfwPushSelectionToManagerX11(void) void _glfwCreateInputContextX11(_GLFWwindow* window) { - XVaNestedList preeditList = _createXIMPreeditCallbacks(window); - XVaNestedList statusList = _createXIMStatusCallbacks(window); + XIMCallback callback; + callback.callback = (XIMProc) inputContextDestroyCallback; + callback.client_data = (XPointer) window; - window->x11.ic = XCreateIC(_glfw.x11.im, - XNInputStyle, - XIMPreeditCallbacks | XIMStatusCallbacks, - XNClientWindow, - window->x11.handle, - XNFocusWindow, - window->x11.handle, - XNPreeditAttributes, - preeditList, - XNStatusAttributes, - statusList, - NULL); - - XFree(preeditList); - XFree(statusList); window->x11.imeFocus = GLFW_FALSE; + if (_glfw.x11.imStyle == STYLE_ONTHESPOT) + { + // On X11, on-the-spot style is unstable. + // Status callbacks are not called and the preedit cursor position + // can not be changed. + XVaNestedList preeditList = _createXIMPreeditCallbacks(window); + XVaNestedList statusList = _createXIMStatusCallbacks(window); + + window->x11.ic = XCreateIC(_glfw.x11.im, + XNInputStyle, + _glfw.x11.imStyle, + XNClientWindow, + window->x11.handle, + XNFocusWindow, + window->x11.handle, + XNPreeditAttributes, + preeditList, + XNStatusAttributes, + statusList, + XNDestroyCallback, + &callback, + NULL); + + XFree(preeditList); + XFree(statusList); + } + else if (_glfw.x11.imStyle == STYLE_OVERTHESPOT) + { + window->x11.ic = XCreateIC(_glfw.x11.im, + XNInputStyle, + _glfw.x11.imStyle, + XNClientWindow, + window->x11.handle, + XNFocusWindow, + window->x11.handle, + XNDestroyCallback, + &callback, + NULL); + } + else + { + // (XIMPreeditNothing | XIMStatusNothing) is considered as STYLE_OVERTHESPOT. + // So this branch should not be used now. + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: Failed to create input context."); + return; + } + if (window->x11.ic) { XWindowAttributes attribs; @@ -3274,21 +3329,90 @@ const char* _glfwGetClipboardStringX11(void) return getSelectionString(_glfw.x11.CLIPBOARD); } +// When using STYLE_ONTHESPOT, this doesn't work and the cursor position can't be updated +// void _glfwUpdatePreeditCursorRectangleX11(_GLFWwindow* window) { + XVaNestedList preedit_attr; + XPoint spot; + _GLFWpreedit* preedit = &window->preedit; + + if (!window->x11.ic) + return; + + spot.x = preedit->cursorPosX + preedit->cursorWidth; + spot.y = preedit->cursorPosY + preedit->cursorHeight; + preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &spot, NULL); + XSetICValues(window->x11.ic, XNPreeditAttributes, preedit_attr, NULL); + XFree(preedit_attr); } void _glfwResetPreeditTextX11(_GLFWwindow* window) { + XIC ic = window->x11.ic; + _GLFWpreedit* preedit = &window->preedit; + + /* restore conversion state after resetting ic later */ + XIMPreeditState preedit_state = XIMPreeditUnKnown; + XVaNestedList preedit_attr; + char* result; + + if (!ic) + return; + + // Can not manage IME in the case of over-the-spot. + if (_glfw.x11.imStyle == STYLE_OVERTHESPOT) + return; + + if (preedit->textCount == 0) + return; + + preedit_attr = XVaCreateNestedList(0, XNPreeditState, &preedit_state, NULL); + XGetICValues(ic, XNPreeditAttributes, preedit_attr, NULL); + XFree(preedit_attr); + + result = XmbResetIC(ic); + + preedit_attr = XVaCreateNestedList(0, XNPreeditState, preedit_state, NULL); + XSetICValues(ic, XNPreeditAttributes, preedit_attr, NULL); + XFree(preedit_attr); + + preedit->textCount = 0; + preedit->blockSizesCount = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = 0; + _glfwInputPreedit(window); + + XFree (result); } void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) { + XIC ic = window->x11.ic; + + if (!ic) + return; + + // Can not manage IME in the case of over-the-spot. + if (_glfw.x11.imStyle == STYLE_OVERTHESPOT) + return; + + if (active) + XSetICFocus(ic); + else + XUnsetICFocus(ic); } int _glfwGetIMEStatusX11(_GLFWwindow* window) { - return GLFW_FALSE; + if (!window->x11.ic) + return GLFW_FALSE; + + // Can not manage IME in the case of over-the-spot. + if (_glfw.x11.imStyle == STYLE_OVERTHESPOT) + return GLFW_FALSE; + + return window->x11.imeFocus; } EGLenum _glfwGetEGLPlatformX11(EGLint** attribs) @@ -3489,46 +3613,6 @@ VkResult _glfwCreateWindowSurfaceX11(VkInstance instance, } } -void _glfwPlatformResetPreeditText(_GLFWwindow* window) { - XIC ic = window->x11.ic; - - /* restore conversion state after resetting ic later */ - XIMPreeditState preedit_state = XIMPreeditUnKnown; - XVaNestedList preedit_attr; - char* result; - - if (window->ntext == 0) - return; - - preedit_attr = XVaCreateNestedList(0, XNPreeditState, &preedit_state, NULL); - XGetICValues(ic, XNPreeditAttributes, preedit_attr, NULL); - XFree(preedit_attr); - - result = XmbResetIC(ic); - - preedit_attr = XVaCreateNestedList(0, XNPreeditState, preedit_state, NULL); - XSetICValues(ic, XNPreeditAttributes, preedit_attr, NULL); - XFree(preedit_attr); - - window->ntext = 0; - window->nblocks = 0; - _glfwInputPreedit(window, 0); - - XFree (result); -} - -void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) { - XIC ic = window->x11.ic; - if (active) { - XSetICFocus(ic); - } else { - XUnsetICFocus(ic); - } -} - -int _glfwPlatformGetIMEStatus(_GLFWwindow* window) { - return window->x11.imeFocus; -} ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// From 12d72f5e1ce7ae9e896fb9f807ab5ac47e999174 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Mon, 19 Feb 2024 09:37:37 +0900 Subject: [PATCH 07/13] 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. --- deps/wayland/text-input-unstable-v1.xml | 385 ++++++++++++++++++++ deps/wayland/text-input-unstable-v3.xml | 457 ++++++++++++++++++++++++ include/GLFW/glfw3.h | 4 +- src/CMakeLists.txt | 2 + src/wl_init.c | 28 ++ src/wl_platform.h | 9 + src/wl_window.c | 434 ++++++++++++++++++++++ 7 files changed, 1318 insertions(+), 1 deletion(-) create mode 100644 deps/wayland/text-input-unstable-v1.xml create mode 100644 deps/wayland/text-input-unstable-v3.xml diff --git a/deps/wayland/text-input-unstable-v1.xml b/deps/wayland/text-input-unstable-v1.xml new file mode 100644 index 00000000..6ee26652 --- /dev/null +++ b/deps/wayland/text-input-unstable-v1.xml @@ -0,0 +1,385 @@ + + + + + Copyright © 2012, 2013 Intel Corporation + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + An object used for text input. Adds support for text input and input + methods to applications. A text_input object is created from a + wl_text_input_manager and corresponds typically to a text entry in an + application. + + Requests are used to activate/deactivate the text_input object and set + state information like surrounding and selected text or the content type. + The information about entered text is sent to the text_input object via + the pre-edit and commit events. Using this interface removes the need + for applications to directly process hardware key events and compose text + out of them. + + Text is generally UTF-8 encoded, indices and lengths are in bytes. + + Serials are used to synchronize the state between the text input and + an input method. New serials are sent by the text input in the + commit_state request and are used by the input method to indicate + the known text input state in events like preedit_string, commit_string, + and keysym. The text input can then ignore events from the input method + which are based on an outdated state (for example after a reset). + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + Requests the text_input object to be activated (typically when the + text entry gets focus). + + The seat argument is a wl_seat which maintains the focus for this + activation. The surface argument is a wl_surface assigned to the + text_input object and tracked for focus lost. The enter event + is emitted on successful activation. + + + + + + + + Requests the text_input object to be deactivated (typically when the + text entry lost focus). The seat argument is a wl_seat which was used + for activation. + + + + + + + Requests input panels (virtual keyboard) to show. + + + + + + Requests input panels (virtual keyboard) to hide. + + + + + + Should be called by an editor widget when the input state should be + reset, for example after the text was changed outside of the normal + input method flow. + + + + + + Sets the plain surrounding text around the input position. Text is + UTF-8 encoded. Cursor is the byte offset within the + surrounding text. Anchor is the byte offset of the + selection anchor within the surrounding text. If there is no selected + text anchor, then it is the same as cursor. + + + + + + + + + Content hint is a bitmask to allow to modify the behavior of the text + input. + + + + + + + + + + + + + + + + + + + The content purpose allows to specify the primary purpose of a text + input. + + This allows an input method to show special purpose input panels with + extra characters or to disallow some characters. + + + + + + + + + + + + + + + + + + + Sets the content purpose and content hint. While the purpose is the + basic purpose of an input field, the hint flags allow to modify some + of the behavior. + + When no content type is explicitly set, a normal content purpose with + default hints (auto completion, auto correction, auto capitalization) + should be assumed. + + + + + + + + + + + + + + + Sets a specific language. This allows for example a virtual keyboard to + show a language specific layout. The "language" argument is an RFC-3066 + format language tag. + + It could be used for example in a word processor to indicate the + language of the currently edited document or in an instant message + application which tracks languages of contacts. + + + + + + + + + + + + + + + + Notify the text_input object when it received focus. Typically in + response to an activate request. + + + + + + + Notify the text_input object when it lost focus. Either in response + to a deactivate request or when the assigned surface lost focus or was + destroyed. + + + + + + Transfer an array of 0-terminated modifier names. The position in + the array is the index of the modifier as used in the modifiers + bitmask in the keysym event. + + + + + + + Notify when the visibility state of the input panel changed. + + + + + + + Notify when a new composing text (pre-edit) should be set around the + current cursor position. Any previously set composing text should + be removed. + + The commit text can be used to replace the preedit text on reset + (for example on unfocus). + + The text input should also handle all preedit_style and preedit_cursor + events occurring directly before preedit_string. + + + + + + + + + + + + + + + + + + + + Sets styling information on composing text. The style is applied for + length bytes from index relative to the beginning of the composing + text (as byte offset). Multiple styles can + be applied to a composing text by sending multiple preedit_styling + events. + + This event is handled as part of a following preedit_string event. + + + + + + + + + Sets the cursor position inside the composing text (as byte + offset) relative to the start of the composing text. When index is a + negative number no cursor is shown. + + This event is handled as part of a following preedit_string event. + + + + + + + Notify when text should be inserted into the editor widget. The text to + commit could be either just a single character after a key press or the + result of some composing (pre-edit). It could also be an empty text + when some text should be removed (see delete_surrounding_text) or when + the input cursor should be moved (see cursor_position). + + Any previously set composing text should be removed. + + + + + + + + Notify when the cursor or anchor position should be modified. + + This event should be handled as part of a following commit_string + event. + + + + + + + + Notify when the text around the current cursor position should be + deleted. + + Index is relative to the current cursor (in bytes). + Length is the length of deleted text (in bytes). + + This event should be handled as part of a following commit_string + event. + + + + + + + + Notify when a key event was sent. Key events should not be used + for normal text input operations, which should be done with + commit_string, delete_surrounding_text, etc. The key event follows + the wl_keyboard key event convention. Sym is an XKB keysym, state a + wl_keyboard key_state. Modifiers are a mask for effective modifiers + (where the modifier indices are set by the modifiers_map event) + + + + + + + + + + + Sets the language of the input text. The "language" argument is an + RFC-3066 format language tag. + + + + + + + + + + + + + + Sets the text direction of input text. + + It is mainly needed for showing an input cursor on the correct side of + the editor when there is no input done yet and making sure neutral + direction text is laid out properly. + + + + + + + + + A factory for text_input objects. This object is a global singleton. + + + + + Creates a new text_input object. + + + + + + diff --git a/deps/wayland/text-input-unstable-v3.xml b/deps/wayland/text-input-unstable-v3.xml new file mode 100644 index 00000000..1fae54d7 --- /dev/null +++ b/deps/wayland/text-input-unstable-v3.xml @@ -0,0 +1,457 @@ + + + + + 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. + + + + This protocol allows compositors to act as input methods and to send text + to applications. A text input object is used to manage state of what are + typically text entry fields in the application. + + This document adheres to the RFC 2119 when using words like "must", + "should", "may", etc. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + The zwp_text_input_v3 interface represents text input and input methods + associated with a seat. It provides enter/leave events to follow the + text input focus for a seat. + + Requests are used to enable/disable the text-input object and set + state information like surrounding and selected text or the content type. + The information about the entered text is sent to the text-input object + via the preedit_string and commit_string events. + + Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + must not point to middle bytes inside a code point: they must either + point to the first byte of a code point or to the end of the buffer. + Lengths must be measured between two valid indices. + + Focus moving throughout surfaces will result in the emission of + zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + surface must commit zwp_text_input_v3.enable and + zwp_text_input_v3.disable requests as the keyboard focus moves across + editable and non-editable elements of the UI. Those two requests are not + expected to be paired with each other, the compositor must be able to + handle consecutive series of the same request. + + State is sent by the state requests (set_surrounding_text, + set_content_type and set_cursor_rectangle) and a commit request. After an + enter event or disable request all state information is invalidated and + needs to be resent by the client. + + + + + Destroy the wp_text_input object. Also disables all surfaces enabled + through this wp_text_input object. + + + + + + Requests text input on the surface previously obtained from the enter + event. + + This request must be issued every time the active text input changes + to a new one, including within the current surface. Use + zwp_text_input_v3.disable when there is no longer any input focus on + the current surface. + + Clients must not enable more than one text input on the single seat + and should disable the current text input before enabling the new one. + At most one instance of text input may be in enabled state per instance, + Requests to enable the another text input when some text input is active + must be ignored by compositor. + + This request resets all state associated with previous enable, disable, + set_surrounding_text, set_text_change_cause, set_content_type, and + set_cursor_rectangle requests, as well as the state associated with + preedit_string, commit_string, and delete_surrounding_text events. + + The set_surrounding_text, set_content_type and set_cursor_rectangle + requests must follow if the text input supports the necessary + functionality. + + State set with this request is double-buffered. It will get applied on + the next zwp_text_input_v3.commit request, and stay valid until the + next committed enable or disable request. + + The changes must be applied by the compositor after issuing a + zwp_text_input_v3.commit request. + + + + + + Explicitly disable text input on the current surface (typically when + there is no focus on any text entry inside the surface). + + State set with this request is double-buffered. It will get applied on + the next zwp_text_input_v3.commit request. + + + + + + Sets the surrounding plain text around the input, excluding the preedit + text. + + The client should notify the compositor of any changes in any of the + values carried with this request, including changes caused by handling + incoming text-input events as well as changes caused by other + mechanisms like keyboard typing. + + If the client is unaware of the text around the cursor, it should not + issue this request, to signify lack of support to the compositor. + + Text is UTF-8 encoded, and should include the cursor position, the + complete selection and additional characters before and after them. + There is a maximum length of wayland messages, so text can not be + longer than 4000 bytes. + + Cursor is the byte offset of the cursor within text buffer. + + Anchor is the byte offset of the selection anchor within text buffer. + If there is no selected text, anchor is the same as cursor. + + If any preedit text is present, it is replaced with a cursor for the + purpose of this event. + + Values set with this request are double-buffered. They will get applied + on the next zwp_text_input_v3.commit request, and stay valid until the + next committed enable or disable request. + + The initial state for affected fields is empty, meaning that the text + input does not support sending surrounding text. If the empty values + get applied, subsequent attempts to change them may have no effect. + + + + + + + + + Reason for the change of surrounding text or cursor posision. + + + + + + + + Tells the compositor why the text surrounding the cursor changed. + + Whenever the client detects an external change in text, cursor, or + anchor posision, it must issue this request to the compositor. This + request is intended to give the input method a chance to update the + preedit text in an appropriate way, e.g. by removing it when the user + starts typing with a keyboard. + + cause describes the source of the change. + + The value set with this request is double-buffered. It must be applied + and reset to initial at the next zwp_text_input_v3.commit request. + + The initial value of cause is input_method. + + + + + + + Content hint is a bitmask to allow to modify the behavior of the text + input. + + + + + + + + + + + + + + + + + The content purpose allows to specify the primary purpose of a text + input. + + This allows an input method to show special purpose input panels with + extra characters or to disallow some characters. + + + + + + + + + + + + + + + + + + + + Sets the content purpose and content hint. While the purpose is the + basic purpose of an input field, the hint flags allow to modify some of + the behavior. + + Values set with this request are double-buffered. They will get applied + on the next zwp_text_input_v3.commit request. + Subsequent attempts to update them may have no effect. The values + remain valid until the next committed enable or disable request. + + The initial value for hint is none, and the initial value for purpose + is normal. + + + + + + + + Marks an area around the cursor as a x, y, width, height rectangle in + surface local coordinates. + + Allows the compositor to put a window with word suggestions near the + cursor, without obstructing the text being input. + + If the client is unaware of the position of edited text, it should not + issue this request, to signify lack of support to the compositor. + + Values set with this request are double-buffered. They will get applied + on the next zwp_text_input_v3.commit request, and stay valid until the + next committed enable or disable request. + + The initial values describing a cursor rectangle are empty. That means + the text input does not support describing the cursor area. If the + empty values get applied, subsequent attempts to change them may have + no effect. + + + + + + + + + + Atomically applies state changes recently sent to the compositor. + + The commit request establishes and updates the state of the client, and + must be issued after any changes to apply them. + + Text input state (enabled status, content purpose, content hint, + surrounding text and change cause, cursor rectangle) is conceptually + double-buffered within the context of a text input, i.e. between a + committed enable request and the following committed enable or disable + request. + + Protocol requests modify the pending state, as opposed to the current + state in use by the input method. A commit request atomically applies + all pending state, replacing the current state. After commit, the new + pending state is as documented for each related request. + + Requests are applied in the order of arrival. + + Neither current nor pending state are modified unless noted otherwise. + + The compositor must count the number of commit requests coming from + each zwp_text_input_v3 object and use the count as the serial in done + events. + + + + + + Notification that this seat's text-input focus is on a certain surface. + + If client has created multiple text input objects, compositor must send + this event to all of them. + + When the seat has the keyboard capability the text-input focus follows + the keyboard focus. This event sets the current surface for the + text-input object. + + + + + + + Notification that this seat's text-input focus is no longer on a + certain surface. The client should reset any preedit string previously + set. + + The leave notification clears the current surface. It is sent before + the enter notification for the new focus. After leave event, compositor + must ignore requests from any text input instances until next enter + event. + + When the seat has the keyboard capability the text-input focus follows + the keyboard focus. + + + + + + + Notify when a new composing text (pre-edit) should be set at the + current cursor position. Any previously set composing text must be + removed. Any previously existing selected text must be removed. + + The argument text contains the pre-edit string buffer. + + The parameters cursor_begin and cursor_end are counted in bytes + relative to the beginning of the submitted text buffer. Cursor should + be hidden when both are equal to -1. + + They could be represented by the client as a line if both values are + the same, or as a text highlight otherwise. + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.done event. + + The initial value of text is an empty string, and cursor_begin, + cursor_end and cursor_hidden are all 0. + + + + + + + + + Notify when text should be inserted into the editor widget. The text to + commit could be either just a single character after a key press or the + result of some composing (pre-edit). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.done event. + + The initial value of text is an empty string. + + + + + + + Notify when the text around the current cursor position should be + deleted. + + Before_length and after_length are the number of bytes before and after + the current cursor index (excluding the selection) to delete. + + If a preedit text is present, in effect before_length is counted from + the beginning of it, and after_length from its end (see done event + sequence). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.done event. + + The initial values of both before_length and after_length are 0. + + + + + + + + Instruct the application to apply changes to state requested by the + preedit_string, commit_string and delete_surrounding_text events. The + state relating to these events is double-buffered, and each one + modifies the pending state. This event replaces the current state with + the pending state. + + The application must proceed by evaluating the changes in the following + order: + + 1. Replace existing preedit string with the cursor. + 2. Delete requested surrounding text. + 3. Insert commit string with the cursor at its end. + 4. Calculate surrounding text to send. + 5. Insert new preedit text in cursor position. + 6. Place cursor inside preedit text. + + The serial number reflects the last state of the zwp_text_input_v3 + object known to the compositor. The value of the serial argument must + be equal to the number of commit requests already issued on that object. + + When the client receives a done event with a serial different than the + number of past commit requests, it must proceed with evaluating and + applying the changes as normal, except it should not change the current + state of the zwp_text_input_v3 object. All pending state requests + (set_surrounding_text, set_content_type and set_cursor_rectangle) on + the zwp_text_input_v3 object should be sent and committed after + receiving a zwp_text_input_v3.done event with a matching serial. + + + + + + + + A factory for text-input objects. This object is a global singleton. + + + + + Destroy the wp_text_input_manager object. + + + + + + Creates a new text-input object for a given seat. + + + + + + diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 496bc017..3c4879ac 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -5257,6 +5257,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. * @@ -5463,7 +5465,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 1057a6f9..373c4b43 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -104,6 +104,8 @@ if (GLFW_BUILD_WAYLAND) generate_wayland_protocol("fractional-scale-v1.xml") generate_wayland_protocol("xdg-activation-v1.xml") generate_wayland_protocol("xdg-decoration-unstable-v1.xml") + generate_wayland_protocol("text-input-unstable-v1.xml") + generate_wayland_protocol("text-input-unstable-v3.xml") endif() if (WIN32 AND GLFW_BUILD_SHARED_LIBRARY) diff --git a/src/wl_init.c b/src/wl_init.c index fce71039..5edb36a2 100644 --- a/src/wl_init.c +++ b/src/wl_init.c @@ -49,6 +49,8 @@ #include "fractional-scale-v1-client-protocol.h" #include "xdg-activation-v1-client-protocol.h" #include "idle-inhibit-unstable-v1-client-protocol.h" +#include "text-input-unstable-v1-client-protocol.h" +#include "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 @@ -91,6 +93,14 @@ #include "idle-inhibit-unstable-v1-client-protocol-code.h" #undef types +#define types _glfw_text_input_v1_types +#include "text-input-unstable-v1-client-protocol-code.h" +#undef types + +#define types _glfw_text_input_v3_types +#include "text-input-unstable-v3-client-protocol-code.h" +#undef types + static void wmBaseHandlePing(void* userData, struct xdg_wm_base* wmBase, uint32_t serial) @@ -208,6 +218,20 @@ static void registryHandleGlobal(void* userData, &wp_fractional_scale_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, @@ -988,6 +1012,10 @@ void _glfwTerminateWayland(void) xdg_activation_v1_destroy(_glfw.wl.activationManager); if (_glfw.wl.fractionalScaleManager) wp_fractional_scale_manager_v1_destroy(_glfw.wl.fractionalScaleManager); + 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 1023510f..4372ce8f 100644 --- a/src/wl_platform.h +++ b/src/wl_platform.h @@ -411,6 +411,13 @@ typedef struct _GLFWwindowWayland _GLFWfallbackEdgeWayland top, left, right, bottom; struct wl_surface* focus; } fallback; + + struct zwp_text_input_v1* textInputV1; + struct zwp_text_input_v3* textInputV3; + struct { + char* preeditText; + char* commitTextOnReset; + } textInputV1Context; } _GLFWwindowWayland; // Wayland-specific global data @@ -435,6 +442,8 @@ typedef struct _GLFWlibraryWayland struct zwp_idle_inhibit_manager_v1* idleInhibitManager; struct xdg_activation_v1* activationManager; struct wp_fractional_scale_manager_v1* fractionalScaleManager; + 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 e3926e50..cf348bfe 100644 --- a/src/wl_window.c +++ b/src/wl_window.c @@ -51,6 +51,8 @@ #include "xdg-activation-v1-client-protocol.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "fractional-scale-v1-client-protocol.h" +#include "text-input-unstable-v1-client-protocol.h" +#include "text-input-unstable-v3-client-protocol.h" #define GLFW_BORDER_SIZE 4 #define GLFW_CAPTION_HEIGHT 24 @@ -555,6 +557,22 @@ const struct wp_fractional_scale_v1_listener fractionalScaleListener = fractionalScaleHandlePreferredScale, }; +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, @@ -582,6 +600,7 @@ static void xdgToplevelHandleConfigure(void* userData, break; case XDG_TOPLEVEL_STATE_ACTIVATED: window->wl.pending.activated = GLFW_TRUE; + activateTextInputV1(window); break; } } @@ -1538,6 +1557,11 @@ 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 (window->wl.hovered) { _glfw.wl.serial = serial; @@ -2118,6 +2142,379 @@ 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; + + if (!window->callbacks.character) + return; + + 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 ////// @@ -2172,6 +2569,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; } @@ -2192,6 +2604,15 @@ void _glfwDestroyWindowWayland(_GLFWwindow* window) if (window->wl.activationToken) xdg_activation_token_v1_destroy(window->wl.activationToken); + 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); @@ -3197,6 +3618,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) From 2a7ab5b1a98969973d10e0f288cc9814e35793cf Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Tue, 6 Dec 2022 10:36:19 +0900 Subject: [PATCH 08/13] tests: Add tests for IME features Co-authored-by: Takuro Ashie --- tests/CMakeLists.txt | 14 +- tests/events.c | 55 +++ tests/input_text.c | 781 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 848 insertions(+), 2 deletions(-) create mode 100644 tests/input_text.c diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f81cfeb9..f28a53b6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(cursor cursor.c ${GLAD_GL}) add_executable(empty WIN32 MACOSX_BUNDLE empty.c ${TINYCTHREAD} ${GLAD_GL}) add_executable(gamma WIN32 MACOSX_BUNDLE gamma.c ${GLAD_GL}) add_executable(icon WIN32 MACOSX_BUNDLE icon.c ${GLAD_GL}) +add_executable(input_text WIN32 MACOSX_BUNDLE input_text.c ${GETOPT} ${GLAD_GL}) add_executable(inputlag WIN32 MACOSX_BUNDLE inputlag.c ${GETOPT} ${GLAD_GL}) add_executable(joysticks WIN32 MACOSX_BUNDLE joysticks.c ${GLAD_GL}) add_executable(tearing WIN32 MACOSX_BUNDLE tearing.c ${GLAD_GL}) @@ -48,8 +49,16 @@ if (RT_LIBRARY) target_link_libraries(threads "${RT_LIBRARY}") endif() -set(GUI_ONLY_BINARIES empty gamma icon inputlag joysticks tearing threads - timeout title triangle-vulkan window) +if (GLFW_BUILD_X11 OR GLFW_BUILD_WAYLAND) + find_package(Fontconfig) + if (FONTCONFIG_FOUND) + target_compile_definitions(input_text PRIVATE FONTCONFIG_ENABLED) + target_link_libraries(input_text fontconfig) + endif() +endif() + +set(GUI_ONLY_BINARIES empty gamma icon input_text inputlag joysticks tearing + threads timeout title triangle-vulkan window) set(CONSOLE_BINARIES allocator clipboard events msaa glfwinfo iconify monitors reopen cursor) @@ -70,6 +79,7 @@ endif() if (APPLE) set_target_properties(empty PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Empty Event") set_target_properties(gamma PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Gamma") + set_target_properties(input_text PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Input Text") set_target_properties(inputlag PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Input Lag") set_target_properties(joysticks PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Joysticks") set_target_properties(tearing PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Tearing") diff --git a/tests/events.c b/tests/events.c index ab3b99a7..0ea3a957 100644 --- a/tests/events.c +++ b/tests/events.c @@ -464,6 +464,59 @@ static void char_callback(GLFWwindow* window, unsigned int codepoint) counter++, slot->number, glfwGetTime(), codepoint, string); } +static void preedit_callback(GLFWwindow* window, int preeditCount, + unsigned int* preeditString, int blockCount, + int* blockSizes, int focusedBlock, int caret) +{ + Slot* slot = glfwGetWindowUserPointer(window); + int i, blockIndex = -1, remainingBlockSize = 0; + int width, height; + char encoded[5] = ""; + size_t encodedCount = 0; + printf("%08x to %i at %0.3f: Preedit text ", + counter++, slot->number, glfwGetTime()); + if (preeditCount == 0 || blockCount == 0) + { + printf("(empty)\n"); + } + else + { + for (i = 0; i < preeditCount; i++) + { + if (remainingBlockSize == 0) + { + if (blockIndex == focusedBlock) + printf("]"); + blockIndex++; + remainingBlockSize = blockSizes[blockIndex]; + printf("\n block %d: ", blockIndex); + if (blockIndex == focusedBlock) + printf("["); + } + if (i == caret) + printf("|"); + encodedCount = encode_utf8(encoded, preeditString[i]); + encoded[encodedCount] = '\0'; + printf("%s", encoded); + remainingBlockSize--; + } + if (blockIndex == focusedBlock) + printf("]"); + if (caret == preeditCount) + printf("|"); + printf("\n"); + glfwGetWindowSize(window, &width, &height); + glfwSetPreeditCursorRectangle(window, width/2, height/2, 1, 20); + } +} + +static void ime_callback(GLFWwindow* window) +{ + Slot* slot = glfwGetWindowUserPointer(window); + printf("%08x to %i at %0.3f: IME switched\n", + counter++, slot->number, glfwGetTime()); +} + static void drop_callback(GLFWwindow* window, int count, const char* paths[]) { int i; @@ -649,6 +702,8 @@ int main(int argc, char** argv) glfwSetScrollCallback(slots[i].window, scroll_callback); glfwSetKeyCallback(slots[i].window, key_callback); glfwSetCharCallback(slots[i].window, char_callback); + glfwSetPreeditCallback(slots[i].window, preedit_callback); + glfwSetIMEStatusCallback(slots[i].window, ime_callback); glfwSetDropCallback(slots[i].window, drop_callback); glfwMakeContextCurrent(slots[i].window); diff --git a/tests/input_text.c b/tests/input_text.c new file mode 100644 index 00000000..3cce0227 --- /dev/null +++ b/tests/input_text.c @@ -0,0 +1,781 @@ +//======================================================================== +// Input Test +// Copyright (c) Camilla Löwy +// Copyright (c) Daijiro Fukuda +// Copyright (c) Takuro Ashie +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would +// be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not +// be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source +// distribution. +// +//======================================================================== +// +// For font handiling, I reffered to https://github.com/Immediate-Mode-UI/Nuklear/wiki/Complete-font-guide. +// For nuklear handling, I reffered to tests/window.c. +// +// Currently, it is made for Japanese input only. +// You have to select the correct font to display Japanese texts. +// To handle other languages, you need to add correct ranges to nk_font_config. +// +// On X11 or Wayland, you can choose a font by GUI if "fontconfig" libarary is enabled. +// +// On Win32, "Yu Mincho" is selected by default if it is installed. This font is +// included in the FOD packages, so it will be installed automatically when you +// enable Japanese input on your environment, or you can install it by +// "Manage optional features" in "Apps & features". +// Refer: https://learn.microsoft.com/en-us/typography/fonts/windows_10_font_list#japanese-supplemental-fonts +// +// On macOS, "Arial Unicode MS" is selected by default if it is installed. +// I assume that this font is usually installed, but if it is not installed, +// please install it manually. +// +// You can also specify a TTF filepath and use your own favorite font by setting +// TTF_FONT_FILEPATH below. +// +//======================================================================== + +// Please comment out and set font filepath here to change default font +// #define TTF_FONT_FILEPATH "" + +#define GLAD_GL_IMPLEMENTATION +#include +#define GLFW_INCLUDE_NONE +#include + +#include + +#define NK_IMPLEMENTATION +#define NK_INCLUDE_STANDARD_IO +#define NK_KEYSTATE_BASED_INPUT +#define NK_INCLUDE_FIXED_TYPES +#define NK_INCLUDE_FONT_BAKING +#define NK_INCLUDE_DEFAULT_FONT +#define NK_INCLUDE_DEFAULT_ALLOCATOR +#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT +#define NK_INCLUDE_STANDARD_VARARGS +#define NK_BUTTON_TRIGGER_ON_RELEASE + +// To increase the number of characters that can be entered at one time +#define NK_INPUT_MAX 64 + +#include + +#define NK_GLFW_GL2_IMPLEMENTATION +#include + +#include +#include +#include +#include + +#include "getopt.h" + +#if defined(FONTCONFIG_ENABLED) + #include +#endif + +#define MAX_BUFFER_LEN 1024 + +// https://github.com/Immediate-Mode-UI/Nuklear/wiki/Complete-font-guide +// https://unicode-table.com +// To handle other languages, you need to fix these ranges. +static nk_rune rangesJapan[] = { + 0x0020, 0x007E, // Basic Latin + 0x2000, 0x206F, // General Punctuation + 0x3000, 0x303F, // CJK Symbols and Punctuation + 0x3041, 0x309F, // Hiragana + 0x30A0, 0x30FF, // Katakana + 0x4E00, 0x9FFF, // All Kanji + 0xFF01, 0xFFEF, // Halfwidth and Fullwidth Forms + 0 +}; + +#define MAX_FONTS_LEN 512 +#define MAX_FONT_FAMILY_NAME_LEN 128 +#define MAX_FONT_FILEPATH_LEN 256 + +static struct nk_font* currentFont; +static char** fontFamilyNames; +static char** fontFilePaths; +static int fontNum = 0; +static int currentFontIndex = 0; + +static int currentIMEStatus = GLFW_FALSE; +#define MAX_PREEDIT_LEN 128 +static char preeditBuf[MAX_PREEDIT_LEN] = ""; + +void usage(void) +{ + printf("Usage: input_text [-h] [-s]\n"); + printf("Options:\n"); + printf(" -s Use on-the-spot sytle on X11. This is ignored on other platforms.\n"); + printf(" -h Show this help\n"); +} + +static size_t encode_utf8(char* s, unsigned int ch) +{ + size_t count = 0; + + if (ch < 0x80) + s[count++] = (char) ch; + else if (ch < 0x800) + { + s[count++] = (ch >> 6) | 0xc0; + s[count++] = (ch & 0x3f) | 0x80; + } + else if (ch < 0x10000) + { + s[count++] = (ch >> 12) | 0xe0; + s[count++] = ((ch >> 6) & 0x3f) | 0x80; + s[count++] = (ch & 0x3f) | 0x80; + } + else if (ch < 0x110000) + { + s[count++] = (ch >> 18) | 0xf0; + s[count++] = ((ch >> 12) & 0x3f) | 0x80; + s[count++] = ((ch >> 6) & 0x3f) | 0x80; + s[count++] = (ch & 0x3f) | 0x80; + } + + return count; +} + +static int add_font(const char* familyName, const char* ttfFilePath, int checkExistence) +{ + if (MAX_FONTS_LEN <= fontNum) + return GLFW_FALSE; + + if (MAX_FONT_FAMILY_NAME_LEN <= strlen(familyName) || MAX_FONT_FILEPATH_LEN <= strlen(ttfFilePath)) + return GLFW_FALSE; + + if (checkExistence) + { + FILE* fp = fopen(ttfFilePath, "rb"); + if (!fp) + return GLFW_FALSE; + fclose(fp); + } + + fontFamilyNames[fontNum] = (char*) malloc(1 + strlen(familyName)); + assert(fontFamilyNames[fontNum]); + strcpy(fontFamilyNames[fontNum], familyName); + + fontFilePaths[fontNum] = (char*) malloc(1 + strlen(ttfFilePath)); + assert(fontFilePaths[fontNum]); + strcpy(fontFilePaths[fontNum], ttfFilePath); + + fontNum++; + + return GLFW_TRUE; +} + +static int replace_font(int index, const char* familyName, const char* ttfFilePath, int checkExistence) +{ + if (index == 0 || fontNum <= index) + return GLFW_FALSE; + if (MAX_FONT_FAMILY_NAME_LEN <= strlen(familyName) || MAX_FONT_FILEPATH_LEN <= strlen(ttfFilePath)) + return GLFW_FALSE; + + if (checkExistence) + { + FILE* fp = fopen(ttfFilePath, "rb"); + if (!fp) + return GLFW_FALSE; + fclose(fp); + } + + free(fontFamilyNames[index]); + free(fontFilePaths[index]); + + fontFamilyNames[index] = (char*) malloc(1 + strlen(familyName)); + assert(fontFamilyNames[index]); + strcpy(fontFamilyNames[index], familyName); + + fontFilePaths[index] = (char*) malloc(1 + strlen(ttfFilePath)); + assert(fontFilePaths[index]); + strcpy(fontFilePaths[index], ttfFilePath); + + return GLFW_TRUE; +} + +#if defined(TTF_FONT_FILEPATH) +static int load_custom_font() +{ + if (MAX_FONTS_LEN <= fontNum) + return GLFW_FALSE; + if (!(TTF_FONT_FILEPATH && *TTF_FONT_FILEPATH)) + return GLFW_FALSE; + + return add_font("Custom", TTF_FONT_FILEPATH, GLFW_TRUE); +} +#endif + +#if defined(FONTCONFIG_ENABLED) +static void load_font_list_by_fontconfig() +{ + FcConfig* config = FcInitLoadConfigAndFonts(); + FcFontSet* fontset = FcConfigGetFonts(config, FcSetSystem); + + if (!fontset) + { + printf("load_font_list_by_fontconfig failed.\n"); + FcConfigDestroy(config); + return; + } + + for (int i = 0; i < fontset->nfont; i++) + { + FcValue fvalue, dvalue; + if (FcResultMatch == FcPatternGet(fontset->fonts[i], FC_FAMILY, 0, &fvalue)) + { + if (FcResultMatch == FcPatternGet(fontset->fonts[i], FC_FILE, 0, &dvalue)) + { + const char* familyName = (const char*) fvalue.u.s; + const char* filePath = (const char*) dvalue.u.s; + int existsFamily = GLFW_FALSE; + int existingIndex = 0; + + if (!strstr(filePath, ".ttf")) + { + continue; + } + + for (int j = 1; j < fontNum; ++j) + { + if (strcmp(fontFamilyNames[j], familyName) == 0) + { + existsFamily = GLFW_TRUE; + existingIndex = j; + break; + } + } + + if (existsFamily) + { + // Prefer "regular" to the others. + if (strstr(filePath, "regular") || strstr(filePath, "Regular")) + replace_font(existingIndex, familyName, filePath, GLFW_FALSE); + } + else + add_font(familyName, filePath, GLFW_FALSE); + + if (MAX_FONTS_LEN <= fontNum) + { + printf("MAX_FONTS_LEN reached. Could not load some fonts.\n"); + break; + } + } + } + } + + FcConfigDestroy(config); +} +#endif + +static void load_default_font_for_each_platform() +{ + int hasSucceeded = GLFW_FALSE; + if (MAX_FONTS_LEN <= fontNum) + return; + + if (glfwGetPlatform() == GLFW_PLATFORM_COCOA) + hasSucceeded = add_font("Arial Unicode MS", "/Library/Fonts/Arial Unicode.ttf", GLFW_TRUE); + else if(glfwGetPlatform() == GLFW_PLATFORM_WIN32) + { + // Use "Yu Mincho" since it is the only TTF for Japanese in the FOD packages on Windows10 and Windows11. + // https://learn.microsoft.com/en-us/typography/fonts/windows_10_font_list#japanese-supplemental-fonts + char filepath[MAX_FONT_FILEPATH_LEN]; + char* winDir = getenv("systemroot"); + if (winDir) + snprintf(filepath, MAX_FONT_FILEPATH_LEN, "%s\\Fonts\\Yumin.ttf", winDir); + else + strcpy(filepath, "C:\\Windows\\Fonts\\Yumin.ttf"); + hasSucceeded = add_font("Yu Mincho Regular", filepath, GLFW_TRUE); + } + + if (hasSucceeded) + currentFontIndex = fontNum - 1; +} + +static void init_font_list() +{ + int useCustomFont = GLFW_FALSE; + int customFontIndex = 0; + + fontFamilyNames = (char**) malloc(sizeof(char*) * MAX_FONTS_LEN); + assert(fontFamilyNames); + fontFilePaths = (char**) malloc(sizeof(char*) * MAX_FONTS_LEN); + assert(fontFilePaths); + + fontFamilyNames[0] = "GLFW default"; + fontFilePaths[0] = ""; + fontNum++; + +#if defined(TTF_FONT_FILEPATH) + useCustomFont = load_custom_font(); + if (useCustomFont) + customFontIndex = fontNum - 1; +#endif + + load_default_font_for_each_platform(); + +#if defined(FONTCONFIG_ENABLED) + load_font_list_by_fontconfig(); +#endif + + if (useCustomFont) + currentFontIndex = customFontIndex; +} + +static void deinit_font_list() +{ + for (int i = 1; i < fontNum; ++i) + { + free(fontFamilyNames[i]); + free(fontFilePaths[i]); + } + + free(fontFamilyNames); + free(fontFilePaths); +} + +// https://github.com/Immediate-Mode-UI/Nuklear/wiki/Complete-font-guide +static void update_font(struct nk_context* nk, float height) +{ + struct nk_font_atlas* atlas; + + nk_glfw3_font_stash_begin(&atlas); + + if (currentFontIndex == 0) + { + currentFont = nk_font_atlas_add_default(atlas, height, 0); + } + else + { + struct nk_font* new_font; + struct nk_font_config cfg; + cfg = nk_font_config(0); + cfg.range = rangesJapan; + cfg.oversample_h = 1; + cfg.oversample_v = 1; + cfg.pixel_snap = true; + + new_font = nk_font_atlas_add_from_file(atlas, fontFilePaths[currentFontIndex], height, &cfg); + if (new_font) + { + currentFont = new_font; + printf("Succeeded to load font file: %s\n", fontFilePaths[currentFontIndex]); + } + else + printf("Failed to load font file: %s\n", fontFilePaths[currentFontIndex]); + } + + nk_glfw3_font_stash_end(); + nk_style_set_font(nk, ¤tFont->handle); +} + +static void set_menu_buttons(GLFWwindow* window, struct nk_context* nk, int height) +{ + static int windowedX, windowedY, windowedWidth, windowedHeight; + + nk_layout_row_dynamic(nk, height, 2); + if (nk_button_label(nk, "Toggle Fullscreen")) + { + if (glfwGetWindowMonitor(window)) + { + glfwSetWindowMonitor(window, NULL, + windowedX, windowedY, + windowedWidth, windowedHeight, 0); + } + else + { + GLFWmonitor* monitor = glfwGetPrimaryMonitor(); + const GLFWvidmode* mode = glfwGetVideoMode(monitor); + glfwGetWindowPos(window, &windowedX, &windowedY); + glfwGetWindowSize(window, &windowedWidth, &windowedHeight); + glfwSetWindowMonitor(window, monitor, + 0, 0, mode->width, mode->height, + mode->refreshRate); + } + } + + { + int auto_iconify = glfwGetWindowAttrib(window, GLFW_AUTO_ICONIFY); + if (nk_checkbox_label(nk, "Auto Iconify", &auto_iconify)) + glfwSetWindowAttrib(window, GLFW_AUTO_ICONIFY, auto_iconify); + } +} + +static int set_font_selecter(GLFWwindow* window, struct nk_context* nk, int height, int fontHeight) +{ + int newSelectedIndex; + + nk_layout_row_begin(nk, NK_DYNAMIC, height, 2); + + nk_layout_row_push(nk, 1.f / 3.f); + nk_label(nk, "Font", NK_TEXT_LEFT); + + nk_layout_row_push(nk, 2.f / 3.f); + newSelectedIndex = nk_combo(nk, (const char**) fontFamilyNames, fontNum, currentFontIndex, fontHeight, nk_vec2(300, 400)); + + nk_layout_row_end(nk); + + if (newSelectedIndex == currentFontIndex) + return GLFW_FALSE; + + currentFontIndex = newSelectedIndex; + return GLFW_TRUE; +} + +static void set_ime_buttons(GLFWwindow* window, struct nk_context* nk, int height) +{ + nk_layout_row_dynamic(nk, height, 2); + + if (nk_button_label(nk, "Toggle IME status")) + { + glfwSetInputMode(window, GLFW_IME, !currentIMEStatus); + } + + if (nk_button_label(nk, "Reset preedit text")) + { + glfwResetPreeditText(window); + } +} + +static void set_preedit_cursor_edit(GLFWwindow* window, struct nk_context* nk, int height, int* isAutoUpdating) +{ + static int lastX = -1, lastY = -1, lastW = -1, lastH = -1; + static char xBuf[12] = "", yBuf[12] = "", wBuf[12] = "", hBuf[12] = ""; + + const nk_flags flags = NK_EDIT_FIELD | + NK_EDIT_SIG_ENTER | + NK_EDIT_GOTO_END_ON_ACTIVATE; + nk_flags events; + int x, y, w, h; + + glfwGetPreeditCursorRectangle(window, &x, &y, &w, &h); + + if (x != lastX) + sprintf(xBuf, "%i", x); + if (y != lastY) + sprintf(yBuf, "%i", y); + if (w != lastW) + sprintf(wBuf, "%i", w); + if (h != lastH) + sprintf(hBuf, "%i", h); + + nk_layout_row_begin(nk, NK_DYNAMIC, height, 5); + + nk_layout_row_push(nk, 4.f / 9.f); + nk_label(nk, "Preedit cursor (x,y,w,h)", NK_TEXT_LEFT); + + nk_layout_row_push(nk, 1.f / 9.f); + events = nk_edit_string_zero_terminated(nk, flags, xBuf, + sizeof(xBuf), + nk_filter_decimal); + if (events & NK_EDIT_COMMITED) + { + x = atoi(xBuf); + *isAutoUpdating = GLFW_FALSE; + glfwSetPreeditCursorRectangle(window, x, y, w, h); + } + else if (events & NK_EDIT_DEACTIVATED) + sprintf(xBuf, "%i", x); + + nk_layout_row_push(nk, 1.f / 9.f); + events = nk_edit_string_zero_terminated(nk, flags, yBuf, + sizeof(yBuf), + nk_filter_decimal); + if (events & NK_EDIT_COMMITED) + { + y = atoi(yBuf); + *isAutoUpdating = GLFW_FALSE; + glfwSetPreeditCursorRectangle(window, x, y, w, h); + } + else if (events & NK_EDIT_DEACTIVATED) + sprintf(yBuf, "%i", y); + + nk_layout_row_push(nk, 1.f / 9.f); + events = nk_edit_string_zero_terminated(nk, flags, wBuf, + sizeof(wBuf), + nk_filter_decimal); + if (events & NK_EDIT_COMMITED) + { + w = atoi(wBuf); + *isAutoUpdating = GLFW_FALSE; + glfwSetPreeditCursorRectangle(window, x, y, w, h); + } + else if (events & NK_EDIT_DEACTIVATED) + sprintf(wBuf, "%i", w); + + nk_layout_row_push(nk, 1.f / 9.f); + events = nk_edit_string_zero_terminated(nk, flags, hBuf, + sizeof(hBuf), + nk_filter_decimal); + if (events & NK_EDIT_COMMITED) + { + h = atoi(hBuf); + *isAutoUpdating = GLFW_FALSE; + glfwSetPreeditCursorRectangle(window, x, y, w, h); + } + else if (events & NK_EDIT_DEACTIVATED) + sprintf(hBuf, "%i", h); + + nk_layout_row_push(nk, 1.f / 9.f); + nk_checkbox_label(nk, "Auto", isAutoUpdating); + + nk_layout_row_end(nk); + + lastX = x; + lastY = y; + lastW = w; + lastH = h; +} + +static void set_ime_stauts_labels(GLFWwindow* window, struct nk_context* nk, int height) +{ + nk_layout_row_dynamic(nk, height, 1); + nk_value_bool(nk, "IME status", currentIMEStatus); +} + +static void set_preedit_labels(GLFWwindow* window, struct nk_context* nk, int height) +{ + nk_layout_row_begin(nk, NK_DYNAMIC, height, 5); + + nk_layout_row_push(nk, 1.f / 3.f); + nk_label(nk, "Preedit info:", NK_TEXT_LEFT); + + nk_layout_row_push(nk, 2.f / 3.f); + nk_label(nk, (const char*) preeditBuf, NK_TEXT_LEFT); + + nk_layout_row_end(nk); +} + +// If it is possible to take the text-cursor position calculated in `nk_do_edit` function in `deps/nuklear.h`, +// we can set preedit-cursor position more easily. +// However, there doesn't seem to be a way to do that, so this does a simplified calculation only for the end +// of the text. (Can not trace the cursor movement) +static void update_cursor_pos(GLFWwindow* window, struct nk_context* nk, struct nk_user_font* f, char* boxBuffer, int boxLen) +{ + float lineWidth = 0; + int totalLines = 1; + + const char* text; + int textPos = 0; + + struct nk_str nkString; + nk_str_init_fixed(&nkString, boxBuffer, (nk_size) MAX_BUFFER_LEN); + nkString.buffer.allocated = (nk_size) boxLen; + nkString.len = nk_utf_len(boxBuffer, boxLen); + + text = nk_str_get_const(&nkString); + + while (textPos < boxLen) + { + nk_rune unicode = 0; + int remainedBoxLen = boxLen - textPos; + int nextGlyphSize = nk_utf_decode(text + textPos, &unicode, remainedBoxLen); + if (!nextGlyphSize) + break; + + if (unicode == '\n') + { + textPos++; + totalLines++; + lineWidth = 0; + continue; + } + + textPos += nextGlyphSize; + lineWidth += f->width(f->userdata, f->height, text + textPos, nextGlyphSize); + } + + { + // I don't know how to get these info. + int widgetLayoutX = 10; + int widgetLayoutY = 220; + + int lineHeight = f->height + nk->style.edit.row_padding; + + int cursorPosX = widgetLayoutX + lineWidth; + int cursorPosY = widgetLayoutY + lineHeight * (totalLines - 1); + int cursorHeight = lineHeight; + int cursorWidth; + + // Keep the value of width since it doesn't need to be updated. + glfwGetPreeditCursorRectangle(window, NULL, NULL, &cursorWidth, NULL); + + glfwSetPreeditCursorRectangle(window, cursorPosX, cursorPosY, cursorWidth, cursorHeight); + } +} + +static void ime_callback(GLFWwindow* window) +{ + currentIMEStatus = glfwGetInputMode(window, GLFW_IME); + printf("IME switched: %s\n", currentIMEStatus ? "ON" : "OFF"); +} + +static void preedit_callback(GLFWwindow* window, int preeditCount, + unsigned int* preeditString, int blockCount, + int* blockSizes, int focusedBlock, int caret) +{ + int blockIndex = -1, remainingBlockSize = 0; + if (preeditCount == 0 || blockCount == 0) + { + strcpy(preeditBuf, "(empty)"); + return; + } + + strcpy(preeditBuf, ""); + + for (int i = 0; i < preeditCount; i++) + { + char encoded[5] = ""; + size_t encodedCount = 0; + + if (i == caret) + { + if (strlen(preeditBuf) + strlen("|") < MAX_PREEDIT_LEN) + strcat(preeditBuf, "|"); + } + if (remainingBlockSize == 0) + { + if (blockIndex == focusedBlock) + { + if (strlen(preeditBuf) + strlen("]") < MAX_PREEDIT_LEN) + strcat(preeditBuf, "]"); + } + blockIndex++; + remainingBlockSize = blockSizes[blockIndex]; + if (blockIndex == focusedBlock) + { + if (strlen(preeditBuf) + strlen("[") < MAX_PREEDIT_LEN) + strcat(preeditBuf, "["); + } + } + encodedCount = encode_utf8(encoded, preeditString[i]); + encoded[encodedCount] = '\0'; + if (strlen(preeditBuf) + strlen(encoded) < MAX_PREEDIT_LEN) + strcat(preeditBuf, encoded); + remainingBlockSize--; + } + if (blockIndex == focusedBlock) + { + if (strlen(preeditBuf) + strlen("]") < MAX_PREEDIT_LEN) + strcat(preeditBuf, "]"); + } + if (caret == preeditCount) + { + if (strlen(preeditBuf) + strlen("|") < MAX_PREEDIT_LEN) + strcat(preeditBuf, "|"); + } +} + +int main(int argc, char** argv) +{ + GLFWwindow* window; + struct nk_context* nk; + int width, height; + char boxBuffer[MAX_BUFFER_LEN] = "Input text here."; + int boxLen = strlen(boxBuffer); + int isAutoUpdatingCursorPosEnabled = GLFW_TRUE; + int ch; + + while ((ch = getopt(argc, argv, "hs")) != -1) + { + switch (ch) + { + case 'h': + usage(); + exit(EXIT_SUCCESS); + + case 's': + glfwInitHint(GLFW_X11_ONTHESPOT, GLFW_TRUE); + break; + } + } + + if (!glfwInit()) + exit(EXIT_FAILURE); + + glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE); + glfwWindowHint(GLFW_WIN32_KEYBOARD_MENU, GLFW_TRUE); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); + + window = glfwCreateWindow(600, 600, "Input Text", NULL, NULL); + if (!window) + { + glfwTerminate(); + exit(EXIT_FAILURE); + } + + currentIMEStatus = glfwGetInputMode(window, GLFW_IME); + glfwSetPreeditCursorRectangle(window, 0, 0, 1, 1); + glfwSetIMEStatusCallback(window, ime_callback); + glfwSetPreeditCallback(window, preedit_callback); + + glfwMakeContextCurrent(window); + gladLoadGL(glfwGetProcAddress); + glfwSwapInterval(0); + + nk = nk_glfw3_init(window, NK_GLFW3_INSTALL_CALLBACKS); + init_font_list(); + update_font(nk, 18); + + while (!glfwWindowShouldClose(window)) + { + struct nk_rect area; + + glfwGetWindowSize(window, &width, &height); + + area = nk_rect(0.f, 0.f, (float) width, (float) height); + nk_window_set_bounds(nk, "main", area); + + nk_glfw3_new_frame(); + if (nk_begin(nk, "main", area, 0)) + { + set_menu_buttons(window, nk, 30); + if (set_font_selecter(window, nk, 30, 18)) + update_font(nk, 18); + set_ime_buttons(window, nk, 30); + set_preedit_cursor_edit(window, nk, 30, &isAutoUpdatingCursorPosEnabled); + set_ime_stauts_labels(window, nk, 30); + set_preedit_labels(window, nk, 30); + + nk_layout_row_dynamic(nk, height - 250, 1); + nk_edit_string(nk, NK_EDIT_BOX, boxBuffer, &boxLen, MAX_BUFFER_LEN, nk_filter_default); + } + nk_end(nk); + + glClear(GL_COLOR_BUFFER_BIT); + nk_glfw3_render(NK_ANTI_ALIASING_ON); + glfwSwapBuffers(window); + + if (isAutoUpdatingCursorPosEnabled) + update_cursor_pos(window, nk, ¤tFont->handle, boxBuffer, boxLen); + + glfwWaitEvents(); + } + + deinit_font_list(); + + nk_glfw3_shutdown(); + glfwTerminate(); + exit(EXIT_SUCCESS); +} From d5795ba0a5d9abebf304c6768848949b9a2122bb Mon Sep 17 00:00:00 2001 From: Yoshiki Shibukawa Date: Fri, 20 May 2022 18:25:16 +0900 Subject: [PATCH 09/13] Apply shibukawa's document fix This fix is from shibukawa's fix: https://github.com/glfw/glfw/pull/658 https://github.com/shibukawa/glfw-1/commit/d36a164423c933948661f3f17576e5a6388ff251 --- docs/input.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/docs/input.md b/docs/input.md index 3ef1aebe..a18e3d75 100644 --- a/docs/input.md +++ b/docs/input.md @@ -229,6 +229,96 @@ void character_callback(GLFWwindow* window, unsigned int codepoint) } ``` +@subsection preedit IME Support + +All desktop operating systems support IME (Input Method Editor) to input characters +that are not mapped with physical keys. IME have been popular among Eeastern Asian people. +And some operating systems start supporting voice input via IME mechanism. + +GLFW provides IME support functions to help +you implement better text input features. You should add suitable visualization code for +preedit text. + +IME works in front of actual character input events (@ref input_char). +If your application uses text input and you want to support IME, +you should register preedit callback to receive preedit text before committed. + +@code +glfwSetPreeditCallback(window, preedit_callback); +@endcode + +The callback function receives chunk of text and focused block information. + +@code +static void preedit_callback(GLFWwindow* window, int strLength, unsigned int* string, int blockLength, int* blocks, int focusedBlock) { +} +@endcode + +strLength and string parameter reprsent whole preedit text. Each character of the preedit string is a codepoint like @ref input_char. + +If you want to type the text "寿司(sushi)", Usually the callback is called several times like the following sequence: + +-# key event: s +-# preedit: [string: "s", block: [1], focusedBlock: 0] +-# key event: u +-# preedit: [string: "す", block: [1], focusedBlock: 0] +-# key event: s +-# preedit: [string: "すs", block: [2], focusedBlock: 0] +-# key event: h +-# preedit: [string: "すsh", block: [2], focusedBlock: 0] +-# key event: i +-# preedit: [string: "すし", block: [2], focusedBlock: 0] +-# key event: ' ' +-# preedit: [string: "寿司", block: [2], focusedBlock: 0] +-# char: '寿' +-# char: '司' +-# preedit: [string: "", block: [], focusedBlock: 0] + +If preedit text includes several semantic blocks, preedit callbacks returns several blocks after a space key pressed: + +-# preedit: [string: "わたしはすしをたべます", block: [11], focusedBlock: 0] +-# preedit: [string: "私は寿司を食べます", block: [2, 7], focusedBlock: 1] + +"blocks" is a list of block length. The above case, it contains the following blocks and second block is focused. + +- 私は +- [寿司を食べます] + +commited text(passed via regular @ref input_char event), unfocused block, focused block should have different text style. + + +GLFW provides helper function to teach suitable position of the candidate window to window system. +Window system decides the best position from text cursor geometry (x, y coords and height). You should call this function +in the above preedit text callback function. + +@code +glfwSetPreeditCursorPos(window, x, y, h); +glfwGetPreeditCursorPos(window, &x, &y, &h); +@endcode + +Sometimes IME task is interrupted by user or application. There are several functions to support these situation. +You can receive notification about IME status change(on/off) by using the following function: + +@code +glfwSetIMEStatusCallback(window, imestatus_callback); +@endcode + +imestatus_callback has simple sigunature like this: + +@code +static void imestatus_callback(GLFWwindow* window) { +} +@endcode + +You can implement the code that resets or commits preedit text when IME status is changed and preedit text is not empty. + +When the focus is gone from text box, you can use the following functions to reset IME status: + +@code +void glfwResetPreeditText(GLFWwindow* window); +void glfwSetIMEStatus(GLFWwindow* window, int active) +int glfwGetIMEStatus(GLFWwindow* window) +@endcode ### Key names {#input_key_name} From 1ba8267f20d3fe7d8bad6b506180cb64c8f7da84 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Fri, 7 Apr 2023 17:50:26 +0900 Subject: [PATCH 10/13] Doc: Improve document about IME features --- docs/input.md | 258 ++++++++++++++++++++++++++++--------------- docs/intro.md | 10 ++ include/GLFW/glfw3.h | 14 +-- 3 files changed, 185 insertions(+), 97 deletions(-) diff --git a/docs/input.md b/docs/input.md index a18e3d75..3e881e84 100644 --- a/docs/input.md +++ b/docs/input.md @@ -229,96 +229,6 @@ void character_callback(GLFWwindow* window, unsigned int codepoint) } ``` -@subsection preedit IME Support - -All desktop operating systems support IME (Input Method Editor) to input characters -that are not mapped with physical keys. IME have been popular among Eeastern Asian people. -And some operating systems start supporting voice input via IME mechanism. - -GLFW provides IME support functions to help -you implement better text input features. You should add suitable visualization code for -preedit text. - -IME works in front of actual character input events (@ref input_char). -If your application uses text input and you want to support IME, -you should register preedit callback to receive preedit text before committed. - -@code -glfwSetPreeditCallback(window, preedit_callback); -@endcode - -The callback function receives chunk of text and focused block information. - -@code -static void preedit_callback(GLFWwindow* window, int strLength, unsigned int* string, int blockLength, int* blocks, int focusedBlock) { -} -@endcode - -strLength and string parameter reprsent whole preedit text. Each character of the preedit string is a codepoint like @ref input_char. - -If you want to type the text "寿司(sushi)", Usually the callback is called several times like the following sequence: - --# key event: s --# preedit: [string: "s", block: [1], focusedBlock: 0] --# key event: u --# preedit: [string: "す", block: [1], focusedBlock: 0] --# key event: s --# preedit: [string: "すs", block: [2], focusedBlock: 0] --# key event: h --# preedit: [string: "すsh", block: [2], focusedBlock: 0] --# key event: i --# preedit: [string: "すし", block: [2], focusedBlock: 0] --# key event: ' ' --# preedit: [string: "寿司", block: [2], focusedBlock: 0] --# char: '寿' --# char: '司' --# preedit: [string: "", block: [], focusedBlock: 0] - -If preedit text includes several semantic blocks, preedit callbacks returns several blocks after a space key pressed: - --# preedit: [string: "わたしはすしをたべます", block: [11], focusedBlock: 0] --# preedit: [string: "私は寿司を食べます", block: [2, 7], focusedBlock: 1] - -"blocks" is a list of block length. The above case, it contains the following blocks and second block is focused. - -- 私は -- [寿司を食べます] - -commited text(passed via regular @ref input_char event), unfocused block, focused block should have different text style. - - -GLFW provides helper function to teach suitable position of the candidate window to window system. -Window system decides the best position from text cursor geometry (x, y coords and height). You should call this function -in the above preedit text callback function. - -@code -glfwSetPreeditCursorPos(window, x, y, h); -glfwGetPreeditCursorPos(window, &x, &y, &h); -@endcode - -Sometimes IME task is interrupted by user or application. There are several functions to support these situation. -You can receive notification about IME status change(on/off) by using the following function: - -@code -glfwSetIMEStatusCallback(window, imestatus_callback); -@endcode - -imestatus_callback has simple sigunature like this: - -@code -static void imestatus_callback(GLFWwindow* window) { -} -@endcode - -You can implement the code that resets or commits preedit text when IME status is changed and preedit text is not empty. - -When the focus is gone from text box, you can use the following functions to reset IME status: - -@code -void glfwResetPreeditText(GLFWwindow* window); -void glfwSetIMEStatus(GLFWwindow* window, int active) -int glfwGetIMEStatus(GLFWwindow* window) -@endcode ### Key names {#input_key_name} @@ -336,6 +246,174 @@ ignored. This matches the behavior of the key callback, meaning the callback arguments can always be passed unmodified to this function. +@section ime_support IME support + +IME (Input Method Editor/Engine) is used to input characters not mapped with +physical keys. It is popular among East Asian people. + + +@subsection ime_style IME styles + +GLFW supports the following two styles of IME. + + - On-the-spot + - Over-the-spot + +On-the-spot style is supported on Windows, macOS and Wayland. On these platforms, +applications need to draw preedit text directly in their UI by using the preedit +callback (See [Preedit input](@ref input_preedit)). + +Over-the-spot style is supported on X11. On this platform, the IME displays preedit +text, and applications don't need to draw it. So the preedit callback doesn't work +on X11. + +In both styles, applications should manage the position of the candidate window. +See [Candidate window](@ref candidate_window) for details. + +@note +@x11 You can use on-the-spot style also on X11 by using @ref GLFW_X11_ONTHESPOT_hint. +In this case, the preedit callback also works on X11. However, on-the-spot style on +X11 is unstable, so it is not recommended. + + +@subsection input_preedit Preedit input + +When inputting text with IME, the text is temporarily inputted, then conversion +and other processing are performed and finally committed. The committed text is +inputted in the same way as input without IME (See [Text input](@ref input_char)). + +This temporary input is called "preedit" or "pre-edit". + +On Windows, macOS and Wayland, that use on-the-spot sytle, applications need to +take preedit information and draw it in their UI. + +You can register the preedit callback as follows. + +@code +glfwSetPreeditCallback(window, preedit_callback); +@endcode + +The callback receives the following information. + +@code +void preedit_callback(GLFWwindow* window, + int preedit_count, + unsigned int* preedit_string, + int block_count, + int* block_sizes, + int focused_block, + int caret) +{ +} +@endcode + +"preedit_count" and "preedit_string" parameter represent the whole preedit text. +Each character of the preedit string is a native endian UTF-32 like @ref input_char. + +If you want to type the text "寿司(sushi)", Usually the callback is called several +times like the following sequence: + +-# key event: s +-# preedit: [preedit_string: "s", block_sizes: [1], focused_block: 0] +-# key event: u +-# preedit: [preedit_string: "す", block_sizes: [1], focused_block: 0] +-# key event: s +-# preedit: [preedit_string: "すs", block_sizes: [2], focused_block: 0] +-# key event: h +-# preedit: [preedit_string: "すsh", block_sizes: [3], focused_block: 0] +-# key event: i +-# preedit: [preedit_string: "すし", block_sizes: [2], focused_block: 0] +-# key event: ' ' +-# preedit: [preedit_string: "寿司", block_sizes: [2], focused_block: 0] +-# char: '寿' +-# char: '司' +-# preedit: [preedit_string: "", block_sizes: [], focused_block: 0] + +If preedit text includes several semantic blocks, the callback returns several blocks: + +-# preedit: [preedit_string: "わたしはすしをたべます", block_sizes: [11], focused_block: 0] +-# preedit: [preedit_string: "私は寿司を食べます", block_sizes: [2, 7], focused_block: 1] + +"block_sizes" is a list of the sizes of each block. The above case, it contains the following +blocks and the second block is focused. + +- 私は +- [寿司を食べます] + +The application side should draw a focused block and unfocused blocks +in different styles. + +You can use the "caret" parameter to draw the caret of the preedit text. +The specification of this parameter depends on the specification of the input method. +The following is an example on Win32. + +- "あいうえお|" (caret: 5) +- key event: arrow-left +- "あいうえ|お" (caret: 4) +- ... +- "|あいうえお" (caret: 0) + + +@subsection candidate_window Candidate window + +The application has to manage the position of the candidate window that shows +the preedit candidate list. To do this, the application has to manage the area +of the preedit text cursor by the following functions. The IME displays the +candidate window in the appropriate position based on the area of the preedit +text cursor. + +@code +glfwSetPreeditCursorRectangle(window, x, y, w, h); +glfwGetPreeditCursorRectangle(window, &x, &y, &w, &h); +@endcode + + +@subsection ime_status IME status + +Sometimes, IME task needs to be interrupted by a user or an application. There +are several functions to support these situations. + +@note +@x11 @wayland This feature is not supported. + +You can receive notification about IME status change(on/off) by using the following +function: + +@code +glfwSetIMEStatusCallback(window, imestatus_callback); +@endcode + +The callback has a simple signature like this: + +@code +void imestatus_callback(GLFWwindow* window) +{ +} +@endcode + +@anchor GLFW_IME +You can get the current IME status by the following function: + +@code +glfwGetInputMode(window, GLFW_IME); +@endcode + +If you get GLFW_TRUE, it means the IME is on, and GLFW_FALSE means the IME is off. + +You can also change the IME status by the following function: + +@code +glfwSetInputMode(window, GLFW_IME, GLFW_TRUE); +glfwSetInputMode(window, GLFW_IME, GLFW_FALSE); +@endcode + +You can use the following function to clear the current preedit. + +@code +glfwResetPreeditText(window); +@endcode + + ## Mouse input {#input_mouse} Mouse input comes in many forms, including mouse motion, button presses and diff --git a/docs/intro.md b/docs/intro.md index 7aa75e31..486b9856 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -152,6 +152,15 @@ __GLFW_X11_XCB_VULKAN_SURFACE__ specifies whether to prefer the the `VK_KHR_xlib_surface` extension. Possible values are `GLFW_TRUE` and `GLFW_FALSE`. This is ignored on other platforms. +@anchor GLFW_X11_ONTHESPOT_hint +__GLFW_X11_ONTHESPOT__ specifies whether to use on-the-spot input method style. +On X11 platform, over-the-spot style is used if this hint is `GLFW_FALSE`, +which is the default value. You can set `GLFW_TRUE` to use on-the-spot style +as with other platforms. However, on-the-spot style on X11 is unstable, so +it is recommended not to use this hint in normal cases. Possible values are +`GLFW_TRUE` and `GLFW_FALSE`. This is ignored on other platforms. Please see +@ref ime_support for more information about IME support. + #### Supported and default values {#init_hints_values} @@ -164,6 +173,7 @@ Initialization hint | Default value | Supported v @ref GLFW_COCOA_MENUBAR | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` @ref GLFW_WAYLAND_LIBDECOR | `GLFW_WAYLAND_PREFER_LIBDECOR` | `GLFW_WAYLAND_PREFER_LIBDECOR` or `GLFW_WAYLAND_DISABLE_LIBDECOR` @ref GLFW_X11_XCB_VULKAN_SURFACE | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_X11_ONTHESPOT | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` ### Runtime platform selection {#platform} diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 3c4879ac..5b4df635 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -1963,7 +1963,7 @@ typedef void (* GLFWcharmodsfun)(GLFWwindow* window, unsigned int codepoint, int * @param[in] focused_block Focused block index. * @param[in] caret Caret position. * - * @sa @ref preedit + * @sa @ref ime_support * @sa glfwSetPreeditCallback * * @ingroup input @@ -1982,7 +1982,7 @@ typedef void (* GLFWpreeditfun)(GLFWwindow* window, * * @param[in] window The window that received the event. * - * @sa @ref preedit + * @sa @ref ime_support * @sa glfwSetIMEStatusCallback * * @ingroup monitor @@ -5218,7 +5218,7 @@ GLFWAPI void glfwSetCursor(GLFWwindow* window, GLFWcursor* cursor); * @par Thread Safety * This function may only be called from the main thread. * - * @sa @ref input_char + * @sa @ref ime_support * * @since Added in GLFW 3.X. * @@ -5240,7 +5240,7 @@ GLFWAPI void glfwGetPreeditCursorRectangle(GLFWwindow* window, int* x, int* y, i * @par Thread Safety * This function may only be called from the main thread. * - * @sa @ref input_char + * @sa @ref ime_support * * @since Added in GLFW 3.X. * @@ -5262,7 +5262,7 @@ GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int * @par Thread Safety * This function may only be called from the main thread. * - * @sa @ref preedit + * @sa @ref ime_support * * @since Added in GLFW 3.X. * @@ -5439,7 +5439,7 @@ GLFWAPI GLFWcharmodsfun glfwSetCharModsCallback(GLFWwindow* window, GLFWcharmods * @par Thread Safety * This function may only be called from the main thread. * - * @sa @ref input_char + * @sa @ref ime_support * * @since Added in GLFW 3.X * @@ -5470,7 +5470,7 @@ GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* window, GLFWpreeditfun * @par Thread Safety * This function may only be called from the main thread. * - * @sa @ref input_char + * @sa @ref ime_support * * @since Added in GLFW 3.X * From 069dc0df80caf80ad3510dc5581d456738c05dfa Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Fri, 7 Apr 2023 18:00:40 +0900 Subject: [PATCH 11/13] Add credit new - Daijiro Fukuda - Ryo Ichinose (from #658) - Yasutaka Kumei (from #658) - xfangfang already in the list - Takuro Ashie - Yoshiki Shibukawa --- CONTRIBUTORS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1371aedb..f841e20c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -75,6 +75,7 @@ video tutorials. - Jason Francis - Gerald Franz - Mário Freitas + - Daijiro Fukuda - GeO4d - Marcus Geelnard - Gegy @@ -104,6 +105,7 @@ video tutorials. - Charles Huber - Brent Huisman - Florian Hülsmann + - Ryo Ichinose - illustris - InKryption - IntellectualKitty @@ -122,6 +124,7 @@ video tutorials. - Cameron King - Peter Knut - Christoph Kubisch + - Yasutaka Kumei - Yuri Kunde Schlesner - Rokas Kupstys - Konstantin Käfer @@ -285,6 +288,7 @@ video tutorials. - Andy Williams - Joel Winarske - Richard A. Wilkes + - xfangfang - Tatsuya Yatagawa - Ryogo Yoshimura - Lukas Zanner From 79fe47e970bb362bf038a3f6534af1427657f630 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Mon, 10 Apr 2023 11:03:18 +0900 Subject: [PATCH 12/13] Add change log entries --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 2718e45e..7cd73568 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,21 @@ information on what to include when reporting a bug. - [Null] Added EGL context creation on Mesa via `EGL_MESA_platform_surfaceless` - [EGL] Allowed native access on Wayland with `GLFW_CONTEXT_CREATION_API` set to `GLFW_NATIVE_CONTEXT_API` (#2518) + - Added `glfwSetPreeditCallback` function and `GLFWpreeditfun` type for + preedit of input method (#2130) + - Added `glfwSetIMEStatusCallback` function and `GLFWimestatusfun` type for + status of input method (#2130) + - Added `glfwSetPreeditCursorRectangle` function to set the preedit cursor + area that is used to decide the position of the candidate window of input + method (#2130) + - Added `glfwGetPreeditCursorRectangle` function to get the preedit cursor + area (#2130) + - Added `glfwResetPreeditText` function to reset preedit of input method + (#2130) + - Added `GLFW_IME` input mode for `glfwGetInputMode` and `glfwSetInputMode` + (#2130) + - Added `GLFW_X11_ONTHESPOT` init hint for using on-the-spot input method + style on X11 (#2130) ## Contact From 4a2883b4e2d1e94ab6a1a04c8c057167f2b0e518 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Mon, 10 Apr 2023 11:05:26 +0900 Subject: [PATCH 13/13] Win32: Support preedit candidate feature You can use this feature when you need to manage the drawing of the preedit candidates on the application side. --- README.md | 7 +++ docs/input.md | 74 ++++++++++++++++++++++ docs/intro.md | 31 +++++++--- include/GLFW/glfw3.h | 101 ++++++++++++++++++++++++++++++ src/init.c | 4 ++ src/input.c | 39 ++++++++++++ src/internal.h | 68 +++++++++++++------- src/win32_init.c | 2 + src/win32_platform.h | 3 + src/win32_window.c | 143 ++++++++++++++++++++++++++++++++++++++++++- tests/input_text.c | 77 ++++++++++++++++++++--- 11 files changed, 503 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 7cd73568..8cfce3eb 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,17 @@ information on what to include when reporting a bug. area (#2130) - Added `glfwResetPreeditText` function to reset preedit of input method (#2130) + - Added `glfwSetPreeditCandidateCallback` function and + `GLFWpreeditcandidatefun` type for preedit candidates (#2130) + - Added `glfwGetPreeditCandidate` function to get a preeidt candidate text + (#2130) - Added `GLFW_IME` input mode for `glfwGetInputMode` and `glfwSetInputMode` (#2130) - Added `GLFW_X11_ONTHESPOT` init hint for using on-the-spot input method style on X11 (#2130) + - Added `GLFW_MANAGE_PREEDIT_CANDIDATE` init hint for displaying preedit + candidates on the application side (supported only on Windows currently) + (#2130) ## Contact diff --git a/docs/input.md b/docs/input.md index 3e881e84..a502ffa9 100644 --- a/docs/input.md +++ b/docs/input.md @@ -414,6 +414,80 @@ glfwResetPreeditText(window); @endcode +@subsection manage_preedit_candidate Manage preedit candidate + +By default, the IME manages the drawing of the preedit candidates, but +sometimes you need to do that on the application side for some reason. In such +a case, you can use +[GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) init hint. +By setting this to `GLFW_TRUE`, the IME stops managing the drawing of the +candidates and the application needs to manage it by using the following +functions. + +@note +@win32 Only the OS currently supports this hint. + +You can register the candidate callback as follows. + +@code +glfwSetPreeditCandidateCallback(window, candidate_callback); +@endcode + +The callback receives the following information. + +@code +void candidate_callback(GLFWwindow* window, + int candidates_count, + int selected_index, + int page_start, + int page_size) +{ +} +@endcode + +`candidates_count` is the number of total candidates. `selected_index` is the +index of the currently selected candidate. Normally all candidates should not +be displayed at once, but divided into pages. You can use `page_start` and +`page_size` to manage the pages. `page_start` is the index of the first +candidate on the current page. `page_size` is the number of the candidates on +the current page. + +You can get the text of the candidate on the specific index as follows. Each +character of the returned text is a native endian UTF-32. + +@code +int text_count; +unsigned int* text = glfwGetPreeditCandidate(window, index, &text_count); +@endcode + +A sample code to get all candidate texts on the current page is as follows. + +@code +void candidate_callback(GLFWwindow* window, int candidates_count, + int selected_index, int page_start, int page_size) +{ + int i, j; + for (i = 0; i < page_size; ++i) + { + int index = i + page_start; + int text_count; + unsigned int* text = glfwGetPreeditCandidate(window, index, &text_count); + if (index == selected_index) + printf("> "); + for (j = 0; j < text_count; ++j) + { + char encoded[5] = ""; + encode_utf8(encoded, text[j]); // Some kind of encoding process + printf("%s", encoded); + } + printf("\n"); + } +} + +glfwSetPreeditCandidateCallback(window, candidate_callback); +@endcode + + ## Mouse input {#input_mouse} Mouse input comes in many forms, including mouse motion, button presses and diff --git a/docs/intro.md b/docs/intro.md index 486b9856..9004460e 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -118,6 +118,16 @@ The ANGLE platform type is specified via the `EGL_ANGLE_platform_angle` extension. This extension is not used if this hint is `GLFW_ANGLE_PLATFORM_TYPE_NONE`, which is the default value. +@anchor GLFW_MANAGE_PREEDIT_CANDIDATE_hint +__GLFW_MANAGE_PREEDIT_CANDIDATE__ specifies whether to manage the preedit +candidates on the application side. Possible values are `GLFW_TRUE` and +`GLFW_FALSE`. The default is `GLFW_FALSE` and there is no need to manage +the candidates on the application side. When you need to do that on the +application side for some reason, you can enable this hint. Please see +@ref ime_support for more information about IME support. + +@win32 Only the OS currently supports this hint. + #### macOS specific init hints {#init_hints_osx} @@ -164,16 +174,17 @@ it is recommended not to use this hint in normal cases. Possible values are #### Supported and default values {#init_hints_values} -Initialization hint | Default value | Supported values --------------------------------- | ------------------------------- | ---------------- -@ref GLFW_PLATFORM | `GLFW_ANY_PLATFORM` | `GLFW_ANY_PLATFORM`, `GLFW_PLATFORM_WIN32`, `GLFW_PLATFORM_COCOA`, `GLFW_PLATFORM_WAYLAND`, `GLFW_PLATFORM_X11` or `GLFW_PLATFORM_NULL` -@ref GLFW_JOYSTICK_HAT_BUTTONS | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` -@ref GLFW_ANGLE_PLATFORM_TYPE | `GLFW_ANGLE_PLATFORM_TYPE_NONE` | `GLFW_ANGLE_PLATFORM_TYPE_NONE`, `GLFW_ANGLE_PLATFORM_TYPE_OPENGL`, `GLFW_ANGLE_PLATFORM_TYPE_OPENGLES`, `GLFW_ANGLE_PLATFORM_TYPE_D3D9`, `GLFW_ANGLE_PLATFORM_TYPE_D3D11`, `GLFW_ANGLE_PLATFORM_TYPE_VULKAN` or `GLFW_ANGLE_PLATFORM_TYPE_METAL` -@ref GLFW_COCOA_CHDIR_RESOURCES | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` -@ref GLFW_COCOA_MENUBAR | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` -@ref GLFW_WAYLAND_LIBDECOR | `GLFW_WAYLAND_PREFER_LIBDECOR` | `GLFW_WAYLAND_PREFER_LIBDECOR` or `GLFW_WAYLAND_DISABLE_LIBDECOR` -@ref GLFW_X11_XCB_VULKAN_SURFACE | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` -@ref GLFW_X11_ONTHESPOT | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` +Initialization hint | Default value | Supported values +---------------------------------- | ------------------------------- | ---------------- +@ref GLFW_PLATFORM | `GLFW_ANY_PLATFORM` | `GLFW_ANY_PLATFORM`, `GLFW_PLATFORM_WIN32`, `GLFW_PLATFORM_COCOA`, `GLFW_PLATFORM_WAYLAND`, `GLFW_PLATFORM_X11` or `GLFW_PLATFORM_NULL` +@ref GLFW_JOYSTICK_HAT_BUTTONS | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_ANGLE_PLATFORM_TYPE | `GLFW_ANGLE_PLATFORM_TYPE_NONE` | `GLFW_ANGLE_PLATFORM_TYPE_NONE`, `GLFW_ANGLE_PLATFORM_TYPE_OPENGL`, `GLFW_ANGLE_PLATFORM_TYPE_OPENGLES`, `GLFW_ANGLE_PLATFORM_TYPE_D3D9`, `GLFW_ANGLE_PLATFORM_TYPE_D3D11`, `GLFW_ANGLE_PLATFORM_TYPE_VULKAN` or `GLFW_ANGLE_PLATFORM_TYPE_METAL` +@ref GLFW_MANAGE_PREEDIT_CANDIDATE | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_COCOA_CHDIR_RESOURCES | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_COCOA_MENUBAR | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_WAYLAND_LIBDECOR | `GLFW_WAYLAND_PREFER_LIBDECOR` | `GLFW_WAYLAND_PREFER_LIBDECOR` or `GLFW_WAYLAND_DISABLE_LIBDECOR` +@ref GLFW_X11_XCB_VULKAN_SURFACE | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_X11_ONTHESPOT | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` ### Runtime platform selection {#platform} diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 5b4df635..4f388966 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -1309,6 +1309,11 @@ extern "C" { * Platform selection [init hint](@ref GLFW_PLATFORM). */ #define GLFW_PLATFORM 0x00050003 +/*! @brief Preedit candidate init hint. + * + * Preedit candidate [init hint](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint). + */ +#define GLFW_MANAGE_PREEDIT_CANDIDATE 0x00050004 /*! @brief macOS specific init hint. * * macOS specific [init hint](@ref GLFW_COCOA_CHDIR_RESOURCES_hint). @@ -1989,6 +1994,29 @@ typedef void (* GLFWpreeditfun)(GLFWwindow* window, */ typedef void (* GLFWimestatusfun)(GLFWwindow* window); +/*! @brief The function pointer type for preedit candidate callbacks. + * + * This is the function pointer type for preedit candidate callback functions. + * Use @ref glfwGetPreeditCandidate to get the candidate text for a specific index. + * + * @param[in] window The window that received the event. + * @param[in] candidates_count Candidates count. + * @param[in] selected_index.Index of selected candidate. + * @param[in] page_start Start index of candidate currently displayed. + * @param[in] page_size Count of candidates currently displayed. + * + * @sa @ref ime_support + * @sa @ref glfwSetPreeditCandidateCallback + * @sa @ref glfwGetPreeditCandidate + * + * @ingroup input + */ +typedef void (* GLFWpreeditcandidatefun)(GLFWwindow* window, + int candidates_count, + int selected_index, + int page_start, + int page_size); + /*! @brief The function pointer type for path drop callbacks. * * This is the function pointer type for path drop callbacks. A path drop @@ -5270,6 +5298,34 @@ GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int */ GLFWAPI void glfwResetPreeditText(GLFWwindow* window); +/*! @brief Returns the preedit candidate. + * + * This function returns the text and the text-count of the preedit candidate. + * + * By default, the IME manages the preedit candidates, so there is no need to + * use this function. See @ref glfwSetPreeditCandidateCallback and + * [GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) for details. + * + * @param[in] window The window. + * @param[in] index The index of the candidate. + * @param[out] textCount The text-count of the candidate. + * @return The text of the candidate as Unicode code points. + * + * @remark @macos @x11 @wayland Don't support this function. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref ime_support + * @sa @ref glfwSetPreeditCandidateCallback + * @sa [GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI unsigned int* glfwGetPreeditCandidate(GLFWwindow* window, int index, int* textCount); + /*! @brief Sets the key callback. * * This function sets the key callback of the specified window, which is called @@ -5478,6 +5534,51 @@ GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* window, GLFWpreeditfun */ GLFWAPI GLFWimestatusfun glfwSetIMEStatusCallback(GLFWwindow* window, GLFWimestatusfun cbfun); +/*! @brief Sets the preedit candidate change callback. + * + * This function sets the preedit candidate callback of the specified + * window, which is called when the candidates are updated and can be used + * to display them by the application side. + * + * By default, this callback is not called because the IME displays the + * candidates and there is nothing to do on the application side. Only when + * the application side needs to use this to manage the displaying of + * IME candidates, you can set + * [GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) init hint + * and stop the IME from managing it. + * + * @param[in] window The window whose callback to set. + * @param[in] cbfun The new callback, or `NULL` to remove the currently set + * callback. + * @return The previously set callback, or `NULL` if no callback was set or an + * error occurred. + * + * @callback_signature + * @code + * void function_name(GLFWwindow* window, + int candidates_count, + int selected_index, + int page_start, + int page_size) + * @endcode + * For more information about the callback parameters, see the + * [function pointer type](@ref GLFWpreeditcandidatefun). + * + * @remark @macos @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. + * + * @sa @ref ime_support + * @sa [GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) + * + * @since Added in GLFW 3.X + * + * @ingroup input + */ +GLFWAPI GLFWpreeditcandidatefun glfwSetPreeditCandidateCallback(GLFWwindow* window, GLFWpreeditcandidatefun cbfun); + /*! @brief Sets the mouse button callback. * * This function sets the mouse button callback of the specified window, which diff --git a/src/init.c b/src/init.c index 6619b0ec..372f71a3 100644 --- a/src/init.c +++ b/src/init.c @@ -52,6 +52,7 @@ static _GLFWinitconfig _glfwInitHints = .hatButtons = GLFW_TRUE, .angleType = GLFW_ANGLE_PLATFORM_TYPE_NONE, .platformID = GLFW_ANY_PLATFORM, + .managePreeditCandidate = GLFW_FALSE, .vulkanLoader = NULL, .ns = { @@ -474,6 +475,9 @@ GLFWAPI void glfwInitHint(int hint, int value) case GLFW_PLATFORM: _glfwInitHints.platformID = value; return; + case GLFW_MANAGE_PREEDIT_CANDIDATE: + _glfwInitHints.managePreeditCandidate = value; + return; case GLFW_COCOA_CHDIR_RESOURCES: _glfwInitHints.ns.chdir = value; return; diff --git a/src/input.c b/src/input.c index be0d63c9..cd9d0da2 100644 --- a/src/input.c +++ b/src/input.c @@ -355,6 +355,21 @@ void _glfwInputIMEStatus(_GLFWwindow* window) } } +// Notifies shared code of a preedit candidate event +// +void _glfwInputPreeditCandidate(_GLFWwindow* window) +{ + if (window->callbacks.preeditCandidate) + { + _GLFWpreedit* preedit = &window->preedit; + window->callbacks.preeditCandidate((GLFWwindow*) window, + preedit->candidateCount, + preedit->candidateSelection, + preedit->candidatePageStart, + preedit->candidatePageSize); + } +} + // Notifies shared code of a scroll event // void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset) @@ -1029,6 +1044,21 @@ GLFWAPI void glfwResetPreeditText(GLFWwindow* handle) _glfw.platform.resetPreeditText(window); } +GLFWAPI unsigned int* glfwGetPreeditCandidate(GLFWwindow* handle, int index, int* textCount) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFWpreedit* preedit = &window->preedit; + + if (preedit->candidateCount <= index) + return NULL; + + if (textCount) + *textCount = preedit->candidates[index].textCount; + + + return preedit->candidates[index].text; +} + GLFWAPI GLFWkeyfun glfwSetKeyCallback(GLFWwindow* handle, GLFWkeyfun cbfun) { _GLFW_REQUIRE_INIT_OR_RETURN(NULL); @@ -1078,6 +1108,15 @@ GLFWAPI GLFWimestatusfun glfwSetIMEStatusCallback(GLFWwindow* handle, GLFWimesta return cbfun; } +GLFWAPI GLFWpreeditcandidatefun glfwSetPreeditCandidateCallback(GLFWwindow* handle, + GLFWpreeditcandidatefun cbfun) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFW_REQUIRE_INIT_OR_RETURN(NULL); + _GLFW_SWAP(GLFWpreeditcandidatefun, window->callbacks.preeditCandidate, cbfun); + return cbfun; +} + GLFWAPI GLFWmousebuttonfun glfwSetMouseButtonCallback(GLFWwindow* handle, GLFWmousebuttonfun cbfun) { diff --git a/src/internal.h b/src/internal.h index 893f780c..80db7916 100644 --- a/src/internal.h +++ b/src/internal.h @@ -63,23 +63,24 @@ typedef int GLFWbool; typedef void (*GLFWproc)(void); -typedef struct _GLFWerror _GLFWerror; -typedef struct _GLFWinitconfig _GLFWinitconfig; -typedef struct _GLFWwndconfig _GLFWwndconfig; -typedef struct _GLFWctxconfig _GLFWctxconfig; -typedef struct _GLFWfbconfig _GLFWfbconfig; -typedef struct _GLFWcontext _GLFWcontext; -typedef struct _GLFWpreedit _GLFWpreedit; -typedef struct _GLFWwindow _GLFWwindow; -typedef struct _GLFWplatform _GLFWplatform; -typedef struct _GLFWlibrary _GLFWlibrary; -typedef struct _GLFWmonitor _GLFWmonitor; -typedef struct _GLFWcursor _GLFWcursor; -typedef struct _GLFWmapelement _GLFWmapelement; -typedef struct _GLFWmapping _GLFWmapping; -typedef struct _GLFWjoystick _GLFWjoystick; -typedef struct _GLFWtls _GLFWtls; -typedef struct _GLFWmutex _GLFWmutex; +typedef struct _GLFWerror _GLFWerror; +typedef struct _GLFWinitconfig _GLFWinitconfig; +typedef struct _GLFWwndconfig _GLFWwndconfig; +typedef struct _GLFWctxconfig _GLFWctxconfig; +typedef struct _GLFWfbconfig _GLFWfbconfig; +typedef struct _GLFWcontext _GLFWcontext; +typedef struct _GLFWpreedit _GLFWpreedit; +typedef struct _GLFWpreeditcandidate _GLFWpreeditcandidate; +typedef struct _GLFWwindow _GLFWwindow; +typedef struct _GLFWplatform _GLFWplatform; +typedef struct _GLFWlibrary _GLFWlibrary; +typedef struct _GLFWmonitor _GLFWmonitor; +typedef struct _GLFWcursor _GLFWcursor; +typedef struct _GLFWmapelement _GLFWmapelement; +typedef struct _GLFWmapping _GLFWmapping; +typedef struct _GLFWjoystick _GLFWjoystick; +typedef struct _GLFWtls _GLFWtls; +typedef struct _GLFWmutex _GLFWmutex; #define GL_VERSION 0x1f02 #define GL_NONE 0 @@ -378,6 +379,7 @@ struct _GLFWinitconfig GLFWbool hatButtons; int angleType; int platformID; + GLFWbool managePreeditCandidate; PFN_vkGetInstanceProcAddr vulkanLoader; struct { GLFWbool menubar; @@ -530,16 +532,34 @@ struct _GLFWcontext // Preedit structure for Input Method Editor/Engine // struct _GLFWpreedit +{ + unsigned int* text; + int textCount; + int textBufferCount; + int* blockSizes; + int blockSizesCount; + int blockSizesBufferCount; + int focusedBlockIndex; + int caretIndex; + int cursorPosX, cursorPosY, cursorWidth, cursorHeight; + + // Used only when apps display candidates by themselves. + // Usually, OS displays them, so apps don't need to do it. + _GLFWpreeditcandidate* candidates; + int candidateCount; + int candidateBufferCount; + int candidateSelection; + int candidatePageStart; + int candidatePageSize; +}; + +// Preedit candidate structure +// +struct _GLFWpreeditcandidate { unsigned int* text; int textCount; int textBufferCount; - int* blockSizes; - int blockSizesCount; - int blockSizesBufferCount; - int focusedBlockIndex; - int caretIndex; - int cursorPosX, cursorPosY, cursorWidth, cursorHeight; }; // Window and context structure @@ -601,6 +621,7 @@ struct _GLFWwindow GLFWcharmodsfun charmods; GLFWpreeditfun preedit; GLFWimestatusfun imestatus; + GLFWpreeditcandidatefun preeditCandidate; GLFWdropfun drop; } callbacks; @@ -961,6 +982,7 @@ void _glfwInputChar(_GLFWwindow* window, uint32_t codepoint, int mods, GLFWbool plain); void _glfwInputPreedit(_GLFWwindow* window); void _glfwInputIMEStatus(_GLFWwindow* window); +void _glfwInputPreeditCandidate(_GLFWwindow* window); void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset); void _glfwInputMouseClick(_GLFWwindow* window, int button, int action, int mods); void _glfwInputCursorPos(_GLFWwindow* window, double xpos, double ypos); diff --git a/src/win32_init.c b/src/win32_init.c index a3cd0d82..8d0eb65e 100644 --- a/src/win32_init.c +++ b/src/win32_init.c @@ -170,6 +170,8 @@ static GLFWbool loadLibraries(void) _glfw.win32.imm32.instance = _glfwPlatformLoadModule("imm32.dll"); if (_glfw.win32.imm32.instance) { + _glfw.win32.imm32.ImmGetCandidateListW_ = (PFN_ImmGetCandidateListW) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetCandidateListW"); _glfw.win32.imm32.ImmGetCompositionStringW_ = (PFN_ImmGetCompositionStringW) _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetCompositionStringW"); _glfw.win32.imm32.ImmGetContext_ = (PFN_ImmGetContext) diff --git a/src/win32_platform.h b/src/win32_platform.h index aed08ca3..eae33340 100644 --- a/src/win32_platform.h +++ b/src/win32_platform.h @@ -318,6 +318,7 @@ typedef LONG (WINAPI * PFN_RtlVerifyVersionInfo)(OSVERSIONINFOEXW*,ULONG,ULONGLO #define RtlVerifyVersionInfo _glfw.win32.ntdll.RtlVerifyVersionInfo_ // imm32 function pointer typedefs +typedef DWORD (WINAPI * PFN_ImmGetCandidateListW)(HIMC,DWORD,LPCANDIDATELIST,DWORD); typedef LONG (WINAPI * PFN_ImmGetCompositionStringW)(HIMC,DWORD,LPVOID,DWORD); typedef HIMC (WINAPI * PFN_ImmGetContext)(HWND); typedef BOOL (WINAPI * PFN_ImmGetConversionStatus)(HIMC,LPDWORD,LPDWORD); @@ -327,6 +328,7 @@ typedef BOOL (WINAPI * PFN_ImmNotifyIME)(HIMC,DWORD,DWORD,DWORD); typedef BOOL (WINAPI * PFN_ImmReleaseContext)(HWND,HIMC); typedef BOOL (WINAPI * PFN_ImmSetCandidateWindow)(HIMC,LPCANDIDATEFORM); typedef BOOL (WINAPI * PFN_ImmSetOpenStatus)(HIMC,BOOL); +#define ImmGetCandidateListW _glfw.win32.imm32.ImmGetCandidateListW_ #define ImmGetCompositionStringW _glfw.win32.imm32.ImmGetCompositionStringW_ #define ImmGetContext _glfw.win32.imm32.ImmGetContext_ #define ImmGetConversionStatus _glfw.win32.imm32.ImmGetConversionStatus_ @@ -526,6 +528,7 @@ typedef struct _GLFWlibraryWin32 struct { HINSTANCE instance; + PFN_ImmGetCandidateListW ImmGetCandidateListW_; PFN_ImmGetCompositionStringW ImmGetCompositionStringW_; PFN_ImmGetContext ImmGetContext_; PFN_ImmGetConversionStatus ImmGetConversionStatus_; diff --git a/src/win32_window.c b/src/win32_window.c index eda2df6a..396c1771 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -35,6 +35,7 @@ #include #include #include +#include // Converts utf16 units to Unicode code points (UTF32). // Returns GLFW_TRUE when the converting completes and the result is assigned to @@ -564,6 +565,117 @@ static void maximizeWindowManually(_GLFWwindow* window) SWP_NOACTIVATE | SWP_NOZORDER | SWP_FRAMECHANGED); } +// Store candidate text from the buffer data +// +static void setCandidate(_GLFWpreeditcandidate* candidate, LPWSTR buffer) +{ + size_t bufferCount = wcslen(buffer); + int textBufferCount = candidate->textBufferCount; + uint32_t codepoint; + WCHAR highSurrogate = 0; + int convertedLength = 0; + int i; + + while ((size_t) textBufferCount < bufferCount + 1) + textBufferCount = (textBufferCount == 0) ? 1 : textBufferCount * 2; + if (textBufferCount != candidate->textBufferCount) + { + unsigned int* text = + _glfw_realloc(candidate->text, + sizeof(unsigned int) * textBufferCount); + if (text == NULL) + return; + candidate->text = text; + candidate->textBufferCount = textBufferCount; + } + + for (i = 0; (size_t) i < bufferCount; ++i) + { + if (convertToUTF32FromUTF16(buffer[i], + &highSurrogate, + &codepoint)) + candidate->text[convertedLength++] = codepoint; + } + + candidate->textCount = convertedLength; +} + +// Get preedit candidates of Imm32 and pass them to candidate-callback +// +static void getImmCandidates(_GLFWwindow* window) +{ + _GLFWpreedit* preedit = &window->preedit; + HIMC hIMC = ImmGetContext(window->win32.handle); + DWORD candidateListBytes = ImmGetCandidateListW(hIMC, 0, NULL, 0); + + if (candidateListBytes == 0) + { + ImmReleaseContext(window->win32.handle, hIMC); + return; + } + + { + int i; + int bufferCount = preedit->candidateBufferCount; + LPCANDIDATELIST candidateList = _glfw_calloc(candidateListBytes, 1); + if (candidateList == NULL) + { + ImmReleaseContext(window->win32.handle, hIMC); + return; + } + ImmGetCandidateListW(hIMC, 0, candidateList, candidateListBytes); + ImmReleaseContext(window->win32.handle, hIMC); + + while ((DWORD) bufferCount < candidateList->dwCount + 1) + bufferCount = (bufferCount == 0) ? 1 : bufferCount * 2; + if (bufferCount != preedit->candidateBufferCount) + { + _GLFWpreeditcandidate* candidates = + _glfw_realloc(preedit->candidates, + sizeof(_GLFWpreeditcandidate) * bufferCount); + if (candidates == NULL) + { + _glfw_free(candidateList); + return; + } + // `realloc` does not initialize the increased area with 0. + // This logic should be moved to a more appropriate place to share + // when other platforms support this feature. + for (i = preedit->candidateBufferCount; i < bufferCount; ++i) + { + candidates[i].text = NULL; + candidates[i].textCount = 0; + candidates[i].textBufferCount = 0; + } + preedit->candidates = candidates; + preedit->candidateBufferCount = bufferCount; + } + + for (i = 0; (DWORD) i < candidateList->dwCount; ++i) + setCandidate(&preedit->candidates[i], + (LPWSTR)((char*) candidateList + candidateList->dwOffset[i])); + + preedit->candidateCount = candidateList->dwCount; + preedit->candidateSelection = candidateList->dwSelection; + preedit->candidatePageStart = candidateList->dwPageStart; + preedit->candidatePageSize = candidateList->dwPageSize; + + _glfw_free(candidateList); + } + + _glfwInputPreeditCandidate(window); +} + +// Clear preedit candidates +static void clearImmCandidate(_GLFWwindow* window) +{ + window->preedit.candidateCount = 0; + window->preedit.candidateSelection = 0; + window->preedit.candidatePageStart = 0; + window->preedit.candidatePageSize = 0; + _glfwInputPreeditCandidate(window); +} + // Get preedit texts of Imm32 and pass them to preedit-callback // static GLFWbool getImmPreedit(_GLFWwindow* window) @@ -798,6 +910,12 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l // To draw preedit text by an application side if (lParam & ISC_SHOWUICOMPOSITIONWINDOW) lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW; + + if (_glfw.hints.init.managePreeditCandidate && + (lParam & ISC_SHOWUICANDIDATEWINDOW)) + { + lParam &= ~ISC_SHOWUICANDIDATEWINDOW; + } break; } @@ -1044,15 +1162,34 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l case WM_IME_ENDCOMPOSITION: { clearImmPreedit(window); + // Usually clearing candidates in IMN_CLOSECANDIDATE is sufficient. + // However, some IME need it here, e.g. Google Japanese Input. + clearImmCandidate(window); return TRUE; } case WM_IME_NOTIFY: { - if (wParam == IMN_SETOPENSTATUS) + switch (wParam) { - _glfwInputIMEStatus(window); - return TRUE; + case IMN_SETOPENSTATUS: + { + _glfwInputIMEStatus(window); + return TRUE; + } + + case IMN_OPENCANDIDATE: + case IMN_CHANGECANDIDATE: + { + getImmCandidates(window); + return TRUE; + } + + case IMN_CLOSECANDIDATE: + { + clearImmCandidate(window); + return TRUE; + } } break; } diff --git a/tests/input_text.c b/tests/input_text.c index 3cce0227..b2030f80 100644 --- a/tests/input_text.c +++ b/tests/input_text.c @@ -119,11 +119,16 @@ static int currentIMEStatus = GLFW_FALSE; #define MAX_PREEDIT_LEN 128 static char preeditBuf[MAX_PREEDIT_LEN] = ""; +// Assuming that the page-size is 10 at most. +static char candidateBuf[9][MAX_PREEDIT_LEN]; +static int candidatePageSize = 0; + void usage(void) { - printf("Usage: input_text [-h] [-s]\n"); + printf("Usage: input_text [-h] [-s] [-c]\n"); printf("Options:\n"); printf(" -s Use on-the-spot sytle on X11. This is ignored on other platforms.\n"); + printf(" -c Use manage-preedit-candidate on Win32. This is ignored on other platforms.\n"); printf(" -h Show this help\n"); } @@ -566,11 +571,32 @@ static void set_preedit_labels(GLFWwindow* window, struct nk_context* nk, int he nk_layout_row_end(nk); } +static void set_candidate_labels(GLFWwindow* window, struct nk_context* nk) +{ + for (int i = 0; i < 5; ++i) + { + nk_layout_row_begin(nk, NK_DYNAMIC, 30, 3); + nk_layout_row_push(nk, 1.f / 3.f); + if (i == 0) + nk_label(nk, "Candidates:", NK_TEXT_LEFT); + else + nk_label(nk, "", NK_TEXT_LEFT); + nk_layout_row_push(nk, 1.f / 3.f); + if (candidatePageSize > i) + nk_label(nk, (const char*) candidateBuf[i], NK_TEXT_LEFT); + nk_layout_row_push(nk, 1.f / 3.f); + if (candidatePageSize > i + 5) + nk_label(nk, (const char*) candidateBuf[i + 5], NK_TEXT_LEFT); + nk_layout_row_end(nk); + } +} + // If it is possible to take the text-cursor position calculated in `nk_do_edit` function in `deps/nuklear.h`, // we can set preedit-cursor position more easily. // However, there doesn't seem to be a way to do that, so this does a simplified calculation only for the end // of the text. (Can not trace the cursor movement) -static void update_cursor_pos(GLFWwindow* window, struct nk_context* nk, struct nk_user_font* f, char* boxBuffer, int boxLen) +static void update_cursor_pos(GLFWwindow* window, struct nk_context* nk, struct nk_user_font* f, + char* boxBuffer, int boxLen, int boxX, int boxY) { float lineWidth = 0; int totalLines = 1; @@ -606,14 +632,10 @@ static void update_cursor_pos(GLFWwindow* window, struct nk_context* nk, struct } { - // I don't know how to get these info. - int widgetLayoutX = 10; - int widgetLayoutY = 220; - int lineHeight = f->height + nk->style.edit.row_padding; - int cursorPosX = widgetLayoutX + lineWidth; - int cursorPosY = widgetLayoutY + lineHeight * (totalLines - 1); + int cursorPosX = boxX + lineWidth; + int cursorPosY = boxY + lineHeight * (totalLines - 1); int cursorHeight = lineHeight; int cursorWidth; @@ -686,6 +708,30 @@ static void preedit_callback(GLFWwindow* window, int preeditCount, } } +static void candidate_callback(GLFWwindow* window, int candidates_count, + int selected_index, int page_start, int page_size) +{ + int i, j; + candidatePageSize = page_size; + for (i = 0; i < page_size; ++i) + { + int index = i + page_start; + int textCount; + unsigned int* text = glfwGetPreeditCandidate(window, index, &textCount); + if (index == selected_index) + strcpy(candidateBuf[i], "> "); + else + strcpy(candidateBuf[i], ""); + for (j = 0; j < textCount; ++j) + { + char encoded[5] = ""; + encode_utf8(encoded, text[j]); + if (strlen(candidateBuf[i]) + strlen(encoded) < MAX_PREEDIT_LEN) + strcat(candidateBuf[i], encoded); + } + } +} + int main(int argc, char** argv) { GLFWwindow* window; @@ -694,9 +740,10 @@ int main(int argc, char** argv) char boxBuffer[MAX_BUFFER_LEN] = "Input text here."; int boxLen = strlen(boxBuffer); int isAutoUpdatingCursorPosEnabled = GLFW_TRUE; + int managePreeditCandidate = GLFW_FALSE; int ch; - while ((ch = getopt(argc, argv, "hs")) != -1) + while ((ch = getopt(argc, argv, "hsc")) != -1) { switch (ch) { @@ -707,6 +754,11 @@ int main(int argc, char** argv) case 's': glfwInitHint(GLFW_X11_ONTHESPOT, GLFW_TRUE); break; + + case 'c': + glfwInitHint(GLFW_MANAGE_PREEDIT_CANDIDATE, GLFW_TRUE); + managePreeditCandidate = GLFW_TRUE; + break; } } @@ -729,6 +781,7 @@ int main(int argc, char** argv) glfwSetPreeditCursorRectangle(window, 0, 0, 1, 1); glfwSetIMEStatusCallback(window, ime_callback); glfwSetPreeditCallback(window, preedit_callback); + glfwSetPreeditCandidateCallback(window, candidate_callback); glfwMakeContextCurrent(window); gladLoadGL(glfwGetProcAddress); @@ -757,6 +810,8 @@ int main(int argc, char** argv) set_preedit_cursor_edit(window, nk, 30, &isAutoUpdatingCursorPosEnabled); set_ime_stauts_labels(window, nk, 30); set_preedit_labels(window, nk, 30); + if (managePreeditCandidate) + set_candidate_labels(window, nk); nk_layout_row_dynamic(nk, height - 250, 1); nk_edit_string(nk, NK_EDIT_BOX, boxBuffer, &boxLen, MAX_BUFFER_LEN, nk_filter_default); @@ -768,7 +823,9 @@ int main(int argc, char** argv) glfwSwapBuffers(window); if (isAutoUpdatingCursorPosEnabled) - update_cursor_pos(window, nk, ¤tFont->handle, boxBuffer, boxLen); + // I don't know how to get the layout info of `nk_edit_string`. + update_cursor_pos(window, nk, ¤tFont->handle, boxBuffer, boxLen, 10, + managePreeditCandidate ? 385 : 220); glfwWaitEvents(); }