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 diff --git a/README.md b/README.md index 10044faf..ff71ea6d 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,28 @@ 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 `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/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/docs/input.md b/docs/input.md index 3ef1aebe..a502ffa9 100644 --- a/docs/input.md +++ b/docs/input.md @@ -246,6 +246,248 @@ 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 + + +@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 7aa75e31..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} @@ -152,18 +162,29 @@ __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} -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` +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 79b06288..e14c07bc 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 @@ -1308,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). @@ -1323,6 +1329,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). @@ -1945,6 +1956,67 @@ typedef void (* GLFWcharfun)(GLFWwindow* window, unsigned int codepoint); */ typedef void (* GLFWcharmodsfun)(GLFWwindow* window, unsigned int codepoint, int mods); +/*! @brief The function pointer type for preedit callbacks. + * + * This is the function pointer type for preedit callback functions. + * + * @param[in] window The window that received the event. + * @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 ime_support + * @sa glfwSetPreeditCallback + * + * @ingroup input + */ +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 pointer type for IME status change callbacks. + * + * This is the function pointer type for IME status change callback functions. + * + * @param[in] window The window that received the event. + * + * @sa @ref ime_support + * @sa glfwSetIMEStatusCallback + * + * @ingroup monitor + */ +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 @@ -4652,13 +4724,13 @@ 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`, - * `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. @@ -4677,8 +4749,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 +4796,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 +5232,100 @@ GLFWAPI void glfwDestroyCursor(GLFWcursor* cursor); */ GLFWAPI void glfwSetCursor(GLFWwindow* window, GLFWcursor* cursor); +/*! @brief Retrieves the area of the preedit text cursor. + * + * 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 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. + * + * @sa @ref ime_support + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwGetPreeditCursorRectangle(GLFWwindow* window, int* x, int* y, int* w, int* h); + +/*! @brief Sets the area of the preedit text cursor. + * + * 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 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. + * + * @sa @ref ime_support + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int w, int h); + +/*! @brief Resets IME input status. + * + * This function resets IME's preedit text. + * + * @param[in] window The window. + * + * @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. + * + * @sa @ref ime_support + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +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 @@ -5291,6 +5461,124 @@ 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 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 + * 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. + * + * @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). + * + * @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. + * + * @sa @ref ime_support + * + * @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 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 + * 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) + * @endcode + * For more information about the callback parameters, see the + * [function pointer type](@ref GLFWimestatusfun). + * + * @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. + * + * @sa @ref ime_support + * + * @since Added in GLFW 3.X + * + * @ingroup input + */ +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/CMakeLists.txt b/src/CMakeLists.txt index 463b898d..6b5f14b4 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/cocoa_init.m b/src/cocoa_init.m index 15dc4ec4..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(); } @@ -509,6 +569,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..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; @@ -268,6 +298,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 e69b5fe0..5bd2aae1 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -321,6 +321,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; } } +- (void)imeStatusChangeNotified:(NSNotification *)notification +{ + _glfwInputIMEStatus(window); +} + @end @@ -565,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]]; } @@ -658,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; } @@ -677,11 +683,95 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; else markedText = [[NSMutableAttributedString alloc] initWithString:string]; + + NSString* markedTextString = markedText.string; + + 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) + { + unsigned int* preeditText = _glfw_realloc(preedit->text, + sizeof(unsigned int) * textBufferCount); + if (preeditText == NULL) + return; + preedit->text = preeditText; + preedit->textBufferCount = textBufferCount; + } + + // 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; + preedit->blockSizes = blocks; + preedit->blockSizesBufferCount = blockBufferCount; + } + + if (currentBlockLocation != currentBlockRange.location) + { + currentBlockLocation = currentBlockRange.location; + preedit->blockSizes[currentBlockIndex++] = currentBlockLength; + currentBlockLength = 0; + if (selectedRange.location == currentBlockRange.location) + focusedBlockIndex = currentBlockIndex; + } + + 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++; + } + } + 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 @@ -703,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 @@ -738,6 +839,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; _glfwInputChar(window, codepoint, mods, plain); } } + + [self unmarkText]; } - (void)doCommandBySelector:(SEL)selector @@ -982,6 +1085,12 @@ GLFWbool _glfwCreateWindowCocoa(_GLFWwindow* window, } } + [[NSNotificationCenter defaultCenter] + addObserver:window->ns.delegate + selector:@selector(imeStatusChangeNotified:) + name:NSTextInputContextKeyboardSelectionDidChangeNotification + object:nil]; + return GLFW_TRUE; } // autoreleasepool @@ -994,6 +1103,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) @@ -1881,6 +1992,118 @@ const char* _glfwGetClipboardStringCocoa(void) } // autoreleasepool } +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) +{ + @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) { if (_glfw.egl.ANGLE_platform_angle) diff --git a/src/init.c b/src/init.c index dbd5a900..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 = { @@ -61,6 +62,7 @@ static _GLFWinitconfig _glfwInitHints = .x11 = { .xcbVulkanSurface = GLFW_TRUE, + .onTheSpotIMStyle = GLFW_FALSE }, .wl = { @@ -175,6 +177,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 // @@ -450,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; @@ -459,6 +487,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/input.c b/src/input.c index c619eefc..cd9d0da2 100644 --- a/src/input.c +++ b/src/input.c @@ -328,6 +328,48 @@ void _glfwInputChar(_GLFWwindow* window, uint32_t codepoint, int mods, GLFWbool } } +// Notifies shared code of a preedit event +// +void _glfwInputPreedit(_GLFWwindow* window) +{ + 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) + { + window->callbacks.imestatus((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) @@ -580,6 +622,8 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) return window->rawMouseMotion; case GLFW_UNLIMITED_MOUSE_BUTTONS: return window->disableMouseButtonLimit; + case GLFW_IME: + return _glfw.platform.getIMEStatus(window); } _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode 0x%08X", mode); @@ -693,6 +737,12 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) window->disableMouseButtonLimit = value ? GLFW_TRUE : GLFW_FALSE; return; } + + case GLFW_IME: + { + _glfw.platform.setIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); + return; + } } _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode 0x%08X", mode); @@ -953,6 +1003,62 @@ GLFWAPI void glfwSetCursor(GLFWwindow* windowHandle, GLFWcursor* cursorHandle) _glfw.platform.setCursor(window, cursor); } +GLFWAPI void glfwGetPreeditCursorRectangle(GLFWwindow* handle, int* x, int* y, int* w, int* h) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFWpreedit* preedit = &window->preedit; + if (x) + *x = preedit->cursorPosX; + if (y) + *y = preedit->cursorPosY; + if (w) + *w = preedit->cursorWidth; + if (h) + *h = preedit->cursorHeight; +} + +GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* handle, int x, int y, int w, int h) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _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) +{ + _GLFWwindow* window = (_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); @@ -986,6 +1092,31 @@ 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 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 4f097aa8..80db7916 100644 --- a/src/internal.h +++ b/src/internal.h @@ -63,22 +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 _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 @@ -377,6 +379,7 @@ struct _GLFWinitconfig GLFWbool hatButtons; int angleType; int platformID; + GLFWbool managePreeditCandidate; PFN_vkGetInstanceProcAddr vulkanLoader; struct { GLFWbool menubar; @@ -384,6 +387,7 @@ struct _GLFWinitconfig } ns; struct { GLFWbool xcbVulkanSurface; + GLFWbool onTheSpotIMStyle; } x11; struct { int libdecorMode; @@ -525,6 +529,39 @@ 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; + + // 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; +}; + // Window and context structure // struct _GLFWwindow @@ -563,6 +600,8 @@ struct _GLFWwindow _GLFWcontext context; + _GLFWpreedit preedit; + struct { GLFWwindowposfun pos; GLFWwindowsizefun size; @@ -580,6 +619,9 @@ struct _GLFWwindow GLFWkeyfun key; GLFWcharfun character; GLFWcharmodsfun charmods; + GLFWpreeditfun preedit; + GLFWimestatusfun imestatus; + GLFWpreeditcandidatefun preeditCandidate; GLFWdropfun drop; } callbacks; @@ -699,6 +741,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); @@ -934,6 +980,9 @@ 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); +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); @@ -1010,6 +1059,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/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..8d0eb65e 100644 --- a/src/win32_init.c +++ b/src/win32_init.c @@ -167,6 +167,31 @@ 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.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) + _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 +216,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 +646,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..eae33340 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,28 @@ 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 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); +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 ImmGetCandidateListW _glfw.win32.imm32.ImmGetCandidateListW_ +#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 +525,20 @@ typedef struct _GLFWlibraryWin32 HINSTANCE instance; PFN_RtlVerifyVersionInfo RtlVerifyVersionInfo_; } ntdll; + + struct { + HINSTANCE instance; + PFN_ImmGetCandidateListW ImmGetCandidateListW_; + 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 +633,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 d014944b..396c1771 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -35,6 +35,42 @@ #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 // @@ -529,6 +565,318 @@ 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) +{ + _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 // static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) @@ -557,6 +905,20 @@ 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; + + if (_glfw.hints.init.managePreeditCandidate && + (lParam & ISC_SHOWUICANDIDATEWINDOW)) + { + lParam &= ~ISC_SHOWUICANDIDATEWINDOW; + } + break; + } + case WM_MOUSEACTIVATE: { // HACK: Postpone cursor disabling when the window was activated by @@ -662,27 +1024,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; @@ -800,6 +1146,54 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l break; } + case WM_IME_COMPOSITION: + { + 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); + // 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: + { + switch (wParam) + { + 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; + } + case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: @@ -2463,6 +2857,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) diff --git a/src/window.c b/src/window.c index e03121a4..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); @@ -495,6 +500,11 @@ GLFWAPI void glfwDestroyWindow(GLFWwindow* handle) *prev = window->next; } + // Clear memory for preedit text + 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..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, @@ -452,6 +476,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, @@ -984,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 f3e8cba2..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; @@ -664,6 +673,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 2e842aaa..7aea6a59 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; @@ -2120,6 +2144,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 ////// @@ -2174,6 +2571,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; } @@ -2194,6 +2606,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,36 @@ const char* _glfwGetClipboardStringWayland(void) return _glfw.wl.clipboardString; } +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) +{ +} + +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..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; @@ -1182,6 +1187,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, @@ -1451,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) @@ -1481,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) @@ -1513,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 30326c5b..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 @@ -546,6 +555,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 @@ -566,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 @@ -711,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; @@ -726,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; @@ -955,6 +980,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..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) @@ -561,6 +538,235 @@ static void inputContextDestroyCallback(XIC ic, XPointer clientData, XPointer ca 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 +// 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) +{ + _GLFWwindow* window = (_GLFWwindow*) clientData; + _GLFWpreedit* preedit = &window->preedit; + + if (!callData->text) + { + // preedit text is empty + preedit->textCount = 0; + preedit->blockSizesCount = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = 0; + _glfwInputPreedit(window); + return; + } + 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; + + preedit->text = preeditText; + preedit->textBufferCount = textBufferCount; + } + 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 = 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; + + preedit->text[j++] = _glfwDecodeUTF8(&src); + f = text->feedback[i]; + if ((f & XIMReverse) || (f & XIMHighlight)) + { + rend = i; + if (i < rstart) + rstart = i; + } + } + + // 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); + } + 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; + 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; + 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; + _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); +} + +// 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); +} + // Create the X11 window (and its colormap) // static GLFWbool createNativeWindow(_GLFWwindow* window, @@ -1290,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) @@ -1925,16 +2131,55 @@ void _glfwCreateInputContextX11(_GLFWwindow* window) callback.callback = (XIMProc) inputContextDestroyCallback; callback.client_data = (XPointer) window; - window->x11.ic = XCreateIC(_glfw.x11.im, - XNInputStyle, - XIMPreeditNothing | XIMStatusNothing, - XNClientWindow, - window->x11.handle, - XNFocusWindow, - window->x11.handle, - XNDestroyCallback, - &callback, - NULL); + 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) { @@ -3084,6 +3329,92 @@ 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) +{ + 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) { if (_glfw.egl.ANGLE_platform_angle) 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..b2030f80 --- /dev/null +++ b/tests/input_text.c @@ -0,0 +1,838 @@ +//======================================================================== +// 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] = ""; + +// 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] [-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"); +} + +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); +} + +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, int boxX, int boxY) +{ + 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); + } + + { + int lineHeight = f->height + nk->style.edit.row_padding; + + int cursorPosX = boxX + lineWidth; + int cursorPosY = boxY + 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, "|"); + } +} + +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; + struct nk_context* nk; + int width, height; + 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, "hsc")) != -1) + { + switch (ch) + { + case 'h': + usage(); + exit(EXIT_SUCCESS); + + case 's': + glfwInitHint(GLFW_X11_ONTHESPOT, GLFW_TRUE); + break; + + case 'c': + glfwInitHint(GLFW_MANAGE_PREEDIT_CANDIDATE, GLFW_TRUE); + managePreeditCandidate = 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); + glfwSetPreeditCandidateCallback(window, candidate_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); + 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); + } + nk_end(nk); + + glClear(GL_COLOR_BUFFER_BIT); + nk_glfw3_render(NK_ANTI_ALIASING_ON); + glfwSwapBuffers(window); + + if (isAutoUpdatingCursorPosEnabled) + // 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(); + } + + deinit_font_list(); + + nk_glfw3_shutdown(); + glfwTerminate(); + exit(EXIT_SUCCESS); +}