From d2ce1326ce7a56a1034f2b0f323650bfb28deccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Lup=C4=8D=C3=ADk?= Date: Tue, 2 Jul 2024 23:14:15 +0200 Subject: [PATCH] Add Input Method Editor (IME) support (#541) Adds `TextInputHandler` abstract class which users can implement to integrate IME support. Users can override the abstract methods and install it with `Rml::SetTextInputHandler` or pass it during context construction `Rml::CreateContext`. The text input handler is notified when a text field is activated, and receives a pointer to a `TextInputContext` interface. The library implements this class for its text input elements. Through this class, the user can query the text field for information such as selection and bounding box, and apply the composition text directly directly into the text field. IME support is currently implemented in the Win32 backend which uses the system IME in Windows. New sample `rmlui_sample_ime` added for IME testing. Loads system fonts to support a wide selection of languages and color emojis. --------- Co-authored-by: Michael Ragazzon --- Backends/RmlUi_Backend_Win32_GL2.cpp | 12 +- Backends/RmlUi_Backend_Win32_VK.cpp | 12 +- Backends/RmlUi_Platform_Win32.cpp | 241 +++++++++++++++- Backends/RmlUi_Platform_Win32.h | 60 +++- Include/RmlUi/Core/Context.h | 9 +- Include/RmlUi/Core/ContextInstancer.h | 4 +- Include/RmlUi/Core/Core.h | 18 +- .../Core/Elements/ElementFormControlInput.h | 6 + .../Elements/ElementFormControlTextArea.h | 5 + Include/RmlUi/Core/Factory.h | 4 +- Include/RmlUi/Core/StringUtilities.h | 6 + Include/RmlUi/Core/TextInputContext.h | 90 ++++++ Include/RmlUi/Core/TextInputHandler.h | 66 +++++ Samples/basic/CMakeLists.txt | 7 + Samples/basic/ime/CMakeLists.txt | 14 + Samples/basic/ime/data/ime.rml | 72 +++++ Samples/basic/ime/src/SystemFontWin32.cpp | 83 ++++++ Samples/basic/ime/src/SystemFontWin32.h | 36 +++ Samples/basic/ime/src/main.cpp | 122 ++++++++ Samples/readme.md | 1 + Source/Core/CMakeLists.txt | 2 + Source/Core/Context.cpp | 10 +- Source/Core/ContextInstancerDefault.cpp | 4 +- Source/Core/ContextInstancerDefault.h | 2 +- Source/Core/Core.cpp | 28 +- .../Core/Elements/ElementFormControlInput.cpp | 6 + .../Elements/ElementFormControlTextArea.cpp | 5 + Source/Core/Elements/InputType.cpp | 2 + Source/Core/Elements/InputType.h | 3 + Source/Core/Elements/InputTypeText.cpp | 5 + Source/Core/Elements/InputTypeText.h | 3 + Source/Core/Elements/WidgetTextInput.cpp | 265 +++++++++++++++--- Source/Core/Elements/WidgetTextInput.h | 32 ++- Source/Core/Factory.cpp | 4 +- Source/Core/StringUtilities.cpp | 27 ++ Tests/Source/UnitTests/StringUtilities.cpp | 10 +- 36 files changed, 1212 insertions(+), 64 deletions(-) create mode 100644 Include/RmlUi/Core/TextInputContext.h create mode 100644 Include/RmlUi/Core/TextInputHandler.h create mode 100644 Samples/basic/ime/CMakeLists.txt create mode 100644 Samples/basic/ime/data/ime.rml create mode 100644 Samples/basic/ime/src/SystemFontWin32.cpp create mode 100644 Samples/basic/ime/src/SystemFontWin32.h create mode 100644 Samples/basic/ime/src/main.cpp diff --git a/Backends/RmlUi_Backend_Win32_GL2.cpp b/Backends/RmlUi_Backend_Win32_GL2.cpp index f08bd3a3e..9b14542de 100644 --- a/Backends/RmlUi_Backend_Win32_GL2.cpp +++ b/Backends/RmlUi_Backend_Win32_GL2.cpp @@ -107,6 +107,7 @@ static void DetachFromNative(HWND window_handle, HDC device_context, HGLRC rende struct BackendData { SystemInterface_Win32 system_interface; RenderInterface_GL2 render_interface; + TextInputMethodEditor_Win32 text_input_method_editor; HINSTANCE instance_handle = nullptr; std::wstring instance_name; @@ -158,6 +159,9 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al ::SetForegroundWindow(window_handle); ::SetFocus(window_handle); + // Provide a backend-specific text input handler to manage the IME. + Rml::SetTextInputHandler(&data->text_input_method_editor); + return true; } @@ -165,6 +169,10 @@ void Backend::Shutdown() { RMLUI_ASSERT(data); + // As we forcefully override the global text input handler, we must reset it before the data is destroyed to avoid any potential use-after-free. + if (Rml::GetTextInputHandler() == &data->text_input_method_editor) + Rml::SetTextInputHandler(nullptr); + DetachFromNative(data->window_handle, data->device_context, data->render_context); ::DestroyWindow(data->window_handle); @@ -307,7 +315,7 @@ static LRESULT CALLBACK WindowProcedureHandler(HWND window_handle, UINT message, if (key_down_callback && !key_down_callback(context, rml_key, rml_modifier, native_dp_ratio, true)) return 0; // Otherwise, hand the event over to the context by calling the input handler as normal. - if (!RmlWin32::WindowProcedure(context, window_handle, message, w_param, l_param)) + if (!RmlWin32::WindowProcedure(context, data->text_input_method_editor, window_handle, message, w_param, l_param)) return 0; // The key was not consumed by the context either, try keyboard shortcuts of lower priority. if (key_down_callback && !key_down_callback(context, rml_key, rml_modifier, native_dp_ratio, false)) @@ -318,7 +326,7 @@ static LRESULT CALLBACK WindowProcedureHandler(HWND window_handle, UINT message, default: { // Submit it to the platform handler for default input handling. - if (!RmlWin32::WindowProcedure(data->context, window_handle, message, w_param, l_param)) + if (!RmlWin32::WindowProcedure(data->context, data->text_input_method_editor, window_handle, message, w_param, l_param)) return 0; } break; diff --git a/Backends/RmlUi_Backend_Win32_VK.cpp b/Backends/RmlUi_Backend_Win32_VK.cpp index c3cbcb46d..90f66279d 100644 --- a/Backends/RmlUi_Backend_Win32_VK.cpp +++ b/Backends/RmlUi_Backend_Win32_VK.cpp @@ -106,6 +106,7 @@ static bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* out_surface); struct BackendData { SystemInterface_Win32 system_interface; RenderInterface_VK render_interface; + TextInputMethodEditor_Win32 text_input_method_editor; HINSTANCE instance_handle = nullptr; std::wstring instance_name; @@ -160,6 +161,9 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al ::SetForegroundWindow(window_handle); ::SetFocus(window_handle); + // Provide a backend-specific text input handler to manage the IME. + Rml::SetTextInputHandler(&data->text_input_method_editor); + return true; } @@ -167,6 +171,10 @@ void Backend::Shutdown() { RMLUI_ASSERT(data); + // As we forcefully override the global text input handler, we must reset it before the data is destroyed to avoid any potential use-after-free. + if (Rml::GetTextInputHandler() == &data->text_input_method_editor) + Rml::SetTextInputHandler(nullptr); + data->render_interface.Shutdown(); ::DestroyWindow(data->window_handle); @@ -315,7 +323,7 @@ static LRESULT CALLBACK WindowProcedureHandler(HWND window_handle, UINT message, if (key_down_callback && !key_down_callback(context, rml_key, rml_modifier, native_dp_ratio, true)) return 0; // Otherwise, hand the event over to the context by calling the input handler as normal. - if (!RmlWin32::WindowProcedure(context, window_handle, message, w_param, l_param)) + if (!RmlWin32::WindowProcedure(context, data->text_input_method_editor, window_handle, message, w_param, l_param)) return 0; // The key was not consumed by the context either, try keyboard shortcuts of lower priority. if (key_down_callback && !key_down_callback(context, rml_key, rml_modifier, native_dp_ratio, false)) @@ -326,7 +334,7 @@ static LRESULT CALLBACK WindowProcedureHandler(HWND window_handle, UINT message, default: { // Submit it to the platform handler for default input handling. - if (!RmlWin32::WindowProcedure(data->context, window_handle, message, w_param, l_param)) + if (!RmlWin32::WindowProcedure(data->context, data->text_input_method_editor, window_handle, message, w_param, l_param)) return 0; } break; diff --git a/Backends/RmlUi_Platform_Win32.cpp b/Backends/RmlUi_Platform_Win32.cpp index 489a4581a..8c2f22fcb 100644 --- a/Backends/RmlUi_Platform_Win32.cpp +++ b/Backends/RmlUi_Platform_Win32.cpp @@ -29,9 +29,12 @@ #include "RmlUi_Platform_Win32.h" #include "RmlUi_Include_Windows.h" #include +#include #include #include #include +#include +#include #include // Used to interact with the input method editor (IME). Users of MinGW should manually link to this. @@ -57,6 +60,8 @@ SystemInterface_Win32::SystemInterface_Win32() cursor_unavailable = LoadCursor(nullptr, IDC_NO); } +SystemInterface_Win32::~SystemInterface_Win32() = default; + void SystemInterface_Win32::SetWindow(HWND in_window_handle) { window_handle = in_window_handle; @@ -185,13 +190,53 @@ std::wstring RmlWin32::ConvertToUTF16(const Rml::String& str) return wstr; } -bool RmlWin32::WindowProcedure(Rml::Context* context, HWND window_handle, UINT message, WPARAM w_param, LPARAM l_param) +static int IMEGetCursorPosition(HIMC context) +{ + return ImmGetCompositionString(context, GCS_CURSORPOS, nullptr, 0); +} + +static std::wstring IMEGetCompositionString(HIMC context, bool finalize) +{ + DWORD type = finalize ? GCS_RESULTSTR : GCS_COMPSTR; + int len_bytes = ImmGetCompositionString(context, type, nullptr, 0); + + if (len_bytes <= 0) + return {}; + + int len_chars = len_bytes / sizeof(TCHAR); + Rml::UniquePtr buffer(new TCHAR[len_chars + 1]); + ImmGetCompositionString(context, type, buffer.get(), len_bytes); + +#ifdef UNICODE + return std::wstring(buffer.get(), len_chars); +#else + return RmlWin32::ConvertToUTF16(Rml::String(buffer.get(), len_chars)); +#endif +} + +static void IMECompleteComposition(HWND window_handle) +{ + if (HIMC context = ImmGetContext(window_handle)) + { + ImmNotifyIME(context, NI_COMPOSITIONSTR, CPS_COMPLETE, NULL); + ImmReleaseContext(window_handle, context); + } +} + +bool RmlWin32::WindowProcedure(Rml::Context* context, TextInputMethodEditor_Win32& text_input_method_editor, HWND window_handle, UINT message, + WPARAM w_param, LPARAM l_param) { if (!context) return true; static bool tracking_mouse_leave = false; + // If the user tries to interact with the window by using the mouse in any way, end the + // composition by committing the current string. This behavior is identical to other + // browsers and is expected, yet, Windows does not send any IME messages in such a case. + if (text_input_method_editor.IsComposing() && message >= WM_LBUTTONDOWN && message <= WM_MBUTTONDBLCLK) + IMECompleteComposition(window_handle); + bool result = true; switch (message) @@ -266,6 +311,67 @@ bool RmlWin32::WindowProcedure(Rml::Context* context, HWND window_handle, UINT m } } break; + case WM_IME_STARTCOMPOSITION: + text_input_method_editor.StartComposition(); + // Prevent the native composition window from appearing by capturing the message. + result = false; + break; + case WM_IME_ENDCOMPOSITION: + if (text_input_method_editor.IsComposing()) + text_input_method_editor.ConfirmComposition(Rml::StringView()); + break; + case WM_IME_COMPOSITION: + { + HIMC imm_context = ImmGetContext(window_handle); + + // Not every IME starts a composition. + if (!text_input_method_editor.IsComposing()) + text_input_method_editor.StartComposition(); + + if (!!(l_param & GCS_CURSORPOS)) + { + // The cursor position is the wchar_t offset in the composition string. Because we + // work with UTF-8 and not UTF-16, we will have to convert the character offset. + int cursor_pos = IMEGetCursorPosition(imm_context); + + std::wstring composition = IMEGetCompositionString(imm_context, false); + Rml::String converted = RmlWin32::ConvertToUTF8(composition.substr(0, cursor_pos)); + cursor_pos = (int)Rml::StringUtilities::LengthUTF8(converted); + + text_input_method_editor.SetCursorPosition(cursor_pos, true); + } + + if (!!(l_param & CS_NOMOVECARET)) + { + // Suppress the cursor position update. CS_NOMOVECARET is always a part of a more + // complex message which means that the cursor is updated from a different event. + text_input_method_editor.SetCursorPosition(-1, false); + } + + if (!!(l_param & GCS_RESULTSTR)) + { + std::wstring composition = IMEGetCompositionString(imm_context, true); + text_input_method_editor.ConfirmComposition(RmlWin32::ConvertToUTF8(composition)); + } + + if (!!(l_param & GCS_COMPSTR)) + { + std::wstring composition = IMEGetCompositionString(imm_context, false); + text_input_method_editor.SetComposition(RmlWin32::ConvertToUTF8(composition)); + } + + // The composition has been canceled. + if (!l_param) + text_input_method_editor.CancelComposition(); + + ImmReleaseContext(window_handle, imm_context); + } + break; + case WM_IME_CHAR: + case WM_IME_REQUEST: + // Ignore WM_IME_CHAR and WM_IME_REQUEST to block the system from appending the composition string. + result = false; + break; default: break; } @@ -510,3 +616,136 @@ Rml::Input::KeyIdentifier RmlWin32::ConvertKey(int win32_key_code) return Rml::Input::KI_UNKNOWN; } + +TextInputMethodEditor_Win32::TextInputMethodEditor_Win32() : + input_context(nullptr), composing(false), cursor_pos(-1), composition_range_start(0), composition_range_end(0) +{} + +void TextInputMethodEditor_Win32::OnActivate(Rml::TextInputContext* _input_context) +{ + input_context = _input_context; +} + +void TextInputMethodEditor_Win32::OnDeactivate(Rml::TextInputContext* _input_context) +{ + if (input_context == _input_context) + input_context = nullptr; +} + +void TextInputMethodEditor_Win32::OnDestroy(Rml::TextInputContext* _input_context) +{ + if (input_context == _input_context) + input_context = nullptr; +} + +bool TextInputMethodEditor_Win32::IsComposing() const +{ + return composing; +} + +void TextInputMethodEditor_Win32::StartComposition() +{ + RMLUI_ASSERT(!composing); + composing = true; +} + +void TextInputMethodEditor_Win32::EndComposition() +{ + if (input_context != nullptr) + input_context->SetCompositionRange(0, 0); + + RMLUI_ASSERT(composing); + composing = false; + + composition_range_start = 0; + composition_range_end = 0; +} + +void TextInputMethodEditor_Win32::CancelComposition() +{ + RMLUI_ASSERT(IsComposing()); + + if (input_context != nullptr) + { + // Purge the current composition string. + input_context->SetText(Rml::StringView(), composition_range_start, composition_range_end); + // Move the cursor back to where the composition began. + input_context->SetCursorPosition(composition_range_start); + } + + EndComposition(); +} + +void TextInputMethodEditor_Win32::SetComposition(Rml::StringView composition) +{ + RMLUI_ASSERT(IsComposing()); + + SetCompositionString(composition); + UpdateCursorPosition(); + + // Update the composition range only if the cursor can be moved around. Editors working with a single + // character (e.g., Hangul IME) should have no visual feedback; they use a selection range instead. + if (cursor_pos != -1 && input_context != nullptr) + input_context->SetCompositionRange(composition_range_start, composition_range_end); +} + +void TextInputMethodEditor_Win32::ConfirmComposition(Rml::StringView composition) +{ + RMLUI_ASSERT(IsComposing()); + + SetCompositionString(composition); + + if (input_context != nullptr) + { + input_context->SetCompositionRange(composition_range_start, composition_range_end); + input_context->CommitComposition(); + } + + // Move the cursor to the end of the string. + SetCursorPosition(composition_range_end - composition_range_start, true); + + EndComposition(); +} + +void TextInputMethodEditor_Win32::SetCursorPosition(int _cursor_pos, bool update) +{ + RMLUI_ASSERT(IsComposing()); + + cursor_pos = _cursor_pos; + + if (update) + UpdateCursorPosition(); +} + +void TextInputMethodEditor_Win32::SetCompositionString(Rml::StringView composition) +{ + if (input_context == nullptr) + return; + + // Retrieve the composition range if it is missing. + if (composition_range_start == 0 && composition_range_end == 0) + input_context->GetSelectionRange(composition_range_start, composition_range_end); + + input_context->SetText(composition, composition_range_start, composition_range_end); + + size_t length = Rml::StringUtilities::LengthUTF8(composition); + composition_range_end = composition_range_start + (int)length; +} + +void TextInputMethodEditor_Win32::UpdateCursorPosition() +{ + // Cursor position update happens before a composition is set; ignore this event. + if (input_context == nullptr || (composition_range_start == 0 && composition_range_end == 0)) + return; + + if (cursor_pos != -1) + { + int position = composition_range_start + cursor_pos; + input_context->SetCursorPosition(position); + } + else + { + // If the API reports no cursor position, select the entire composition string for a better UX. + input_context->SetSelectionRange(composition_range_start, composition_range_end); + } +} diff --git a/Backends/RmlUi_Platform_Win32.h b/Backends/RmlUi_Platform_Win32.h index b040c3501..f1d8855eb 100644 --- a/Backends/RmlUi_Platform_Win32.h +++ b/Backends/RmlUi_Platform_Win32.h @@ -31,13 +31,16 @@ #include "RmlUi_Include_Windows.h" #include +#include #include +#include #include #include class SystemInterface_Win32 : public Rml::SystemInterface { public: SystemInterface_Win32(); + ~SystemInterface_Win32(); // Optionally, provide or change the window to be used for setting the mouse cursor, clipboard text and IME position. void SetWindow(HWND window_handle); @@ -68,6 +71,8 @@ class SystemInterface_Win32 : public Rml::SystemInterface { HCURSOR cursor_unavailable = nullptr; }; +class TextInputMethodEditor_Win32; + /** Optional helper functions for the Win32 plaform. */ @@ -79,7 +84,8 @@ std::wstring ConvertToUTF16(const Rml::String& str); // Window event handler to submit default input behavior to the context. // @return True if the event is still propagating, false if it was handled by the context. -bool WindowProcedure(Rml::Context* context, HWND window_handle, UINT message, WPARAM w_param, LPARAM l_param); +bool WindowProcedure(Rml::Context* context, TextInputMethodEditor_Win32& text_input_method_editor, HWND window_handle, UINT message, WPARAM w_param, + LPARAM l_param); // Converts the key from Win32 key code to RmlUi key. Rml::Input::KeyIdentifier ConvertKey(int win32_key_code); @@ -89,4 +95,56 @@ int GetKeyModifierState(); } // namespace RmlWin32 +/** + Custom backend implementation of TextInputHandler to handle the system's Input Method Editor (IME). + This version supports only one active text input context. + */ +class TextInputMethodEditor_Win32 final : public Rml::TextInputHandler { +public: + TextInputMethodEditor_Win32(); + + void OnActivate(Rml::TextInputContext* input_context) override; + void OnDeactivate(Rml::TextInputContext* input_context) override; + void OnDestroy(Rml::TextInputContext* input_context) override; + + /// Check that a composition is currently active. + /// @return True if we are composing, false otherwise. + bool IsComposing() const; + + void StartComposition(); + void CancelComposition(); + + /// Set the composition string. + /// @param[in] composition A string to be set. + void SetComposition(Rml::StringView composition); + + /// End the current composition by confirming the composition string. + /// @param[in] composition A string to confirm. + void ConfirmComposition(Rml::StringView composition); + + /// Set the cursor position within the composition. + /// @param[in] cursor_pos A character position of the cursor within the composition string. + /// @param[in] update Update the cursor position within active input contexts. + void SetCursorPosition(int cursor_pos, bool update); + +private: + void EndComposition(); + void SetCompositionString(Rml::StringView composition); + + void UpdateCursorPosition(); + +private: + // An actively used text input method context. + Rml::TextInputContext* input_context; + + // A flag to mark a composition is currently active. + bool composing; + // Character position of the cursor in the composition string. + int cursor_pos; + + // Composition range (character position) relative to the text input value. + int composition_range_start; + int composition_range_end; +}; + #endif diff --git a/Include/RmlUi/Core/Context.h b/Include/RmlUi/Core/Context.h index 8f0759c3e..ffc26a96f 100644 --- a/Include/RmlUi/Core/Context.h +++ b/Include/RmlUi/Core/Context.h @@ -47,6 +47,7 @@ class DataModelConstructor; class DataTypeRegister; class ScrollController; class RenderManager; +class TextInputHandler; enum class EventId : uint16_t; /** @@ -60,7 +61,8 @@ class RMLUICORE_API Context : public ScriptInterface { /// Constructs a new, uninitialised context. This should not be called directly, use CreateContext() instead. /// @param[in] name The name of the context. /// @param[in] render_manager The render manager used for this context. - Context(const String& name, RenderManager* render_manager); + /// @param[in] text_input_handler The text input handler used for this context. + Context(const String& name, RenderManager* render_manager, TextInputHandler* text_input_handler); /// Destroys a context. virtual ~Context(); @@ -250,6 +252,9 @@ class RMLUICORE_API Context : public ScriptInterface { /// Retrieves the render manager which can be used to submit changes to the render state. RenderManager& GetRenderManager(); + /// Obtains the text input handler. + TextInputHandler* GetTextInputHandler() const; + /// Sets the instancer to use for releasing this object. /// @param[in] instancer The context's instancer. void SetInstancer(ContextInstancer* instancer); @@ -372,6 +377,8 @@ class RMLUICORE_API Context : public ScriptInterface { UniquePtr default_data_type_register; + TextInputHandler* text_input_handler; + // Time in seconds until Update and Render should be called again. This allows applications to only redraw the ui if needed. // See RequestNextUpdate() and NextUpdateRequested() for details. double next_update_timeout = 0; diff --git a/Include/RmlUi/Core/ContextInstancer.h b/Include/RmlUi/Core/ContextInstancer.h index 7d1859a02..1f931c015 100644 --- a/Include/RmlUi/Core/ContextInstancer.h +++ b/Include/RmlUi/Core/ContextInstancer.h @@ -35,6 +35,7 @@ namespace Rml { +class TextInputHandler; class RenderManager; class Context; class Event; @@ -52,8 +53,9 @@ class RMLUICORE_API ContextInstancer : public Releasable { /// Instances a context. /// @param[in] name Name of this context. /// @param[in] render_manager The render manager used for this context. + /// @param[in] text_input_handler The text input handler used for this context. /// @return The instanced context. - virtual ContextPtr InstanceContext(const String& name, RenderManager* render_manager) = 0; + virtual ContextPtr InstanceContext(const String& name, RenderManager* render_manager, TextInputHandler* text_input_handler) = 0; /// Releases a context previously created by this context. /// @param[in] context The context to release. diff --git a/Include/RmlUi/Core/Core.h b/Include/RmlUi/Core/Core.h index 20b3c1f5e..1cd1990b0 100644 --- a/Include/RmlUi/Core/Core.h +++ b/Include/RmlUi/Core/Core.h @@ -42,6 +42,7 @@ class FileInterface; class FontEngineInterface; class RenderInterface; class SystemInterface; +class TextInputHandler; enum class DefaultActionPhase; /** @@ -92,14 +93,25 @@ RMLUICORE_API void SetFontEngineInterface(FontEngineInterface* font_interface); /// Returns RmlUi's font interface. RMLUICORE_API FontEngineInterface* GetFontEngineInterface(); +/// Sets the implementation for handling text input events. This is not required to be called. +/// @param[in] text_input_handler A non-owning pointer to the application-specified implementation of a text input handler. +/// @lifetime The instance must be kept alive until after the call to Rml::Shutdown. +/// @note Be aware that you might be overriding a custom backend implementation. +RMLUICORE_API void SetTextInputHandler(TextInputHandler* text_input_handler); +/// Returns RmlUi's default implementation of a text input handler. +RMLUICORE_API TextInputHandler* GetTextInputHandler(); + /// Creates a new element context. /// @param[in] name The new name of the context. This must be unique. /// @param[in] dimensions The initial dimensions of the new context. /// @param[in] render_interface The custom render interface to use, or nullptr to use the default. -/// @lifetime If specified, the render interface must be kept alive until after the call to Rml::Shutdown. Alternatively, the render interface can be -/// destroyed after all contexts it belongs to have been destroyed and a subsequent call has been made to Rml::ReleaseTextures. +/// @param[in] text_input_handler The custom text input handler to use, or nullptr to use the default. +/// @lifetime If specified, the render interface and the text input handler must be kept alive until after the call to +/// Rml::Shutdown. Alternatively, the render interface can be destroyed after all contexts it belongs to have been +/// destroyed and a subsequent call has been made to Rml::ReleaseTextures. /// @return A non-owning pointer to the new context, or nullptr if the context could not be created. -RMLUICORE_API Context* CreateContext(const String& name, Vector2i dimensions, RenderInterface* render_interface = nullptr); +RMLUICORE_API Context* CreateContext(const String& name, Vector2i dimensions, RenderInterface* render_interface = nullptr, + TextInputHandler* text_input_handler = nullptr); /// Removes and destroys a context. /// @param[in] name The name of the context to remove. /// @return True if name is a valid context, false otherwise. diff --git a/Include/RmlUi/Core/Elements/ElementFormControlInput.h b/Include/RmlUi/Core/Elements/ElementFormControlInput.h index ea7c3223d..f2bd0fe80 100644 --- a/Include/RmlUi/Core/Elements/ElementFormControlInput.h +++ b/Include/RmlUi/Core/Elements/ElementFormControlInput.h @@ -77,6 +77,12 @@ class RMLUICORE_API ElementFormControlInput : public ElementFormControl { /// @note Only applies to text and password input types. void GetSelection(int* selection_start, int* selection_end, String* selected_text) const; + /// Sets visual feedback used for the IME composition in the range. + /// @param[in] range_start The first character to be selected. + /// @param[in] range_end The first character *after* the selection. + /// @note Only applies to text and password input types. + void SetCompositionRange(int range_start, int range_end); + protected: /// Updates the element's underlying type. void OnUpdate() override; diff --git a/Include/RmlUi/Core/Elements/ElementFormControlTextArea.h b/Include/RmlUi/Core/Elements/ElementFormControlTextArea.h index fd3d8bb8a..b9a555db7 100644 --- a/Include/RmlUi/Core/Elements/ElementFormControlTextArea.h +++ b/Include/RmlUi/Core/Elements/ElementFormControlTextArea.h @@ -102,6 +102,11 @@ class RMLUICORE_API ElementFormControlTextArea : public ElementFormControl { /// @param[out] selected_text The selected text. void GetSelection(int* selection_start, int* selection_end, String* selected_text) const; + /// Sets visual feedback used for the IME composition in the range. + /// @param[in] range_start The first character to be selected. + /// @param[in] range_end The first character *after* the selection. + void SetCompositionRange(int range_start, int range_end); + /// Returns the control's inherent size, based on the length of the input field and the current font size. /// @return True. bool GetIntrinsicDimensions(Vector2f& dimensions, float& ratio) override; diff --git a/Include/RmlUi/Core/Factory.h b/Include/RmlUi/Core/Factory.h index a7e641eb6..435b639e4 100644 --- a/Include/RmlUi/Core/Factory.h +++ b/Include/RmlUi/Core/Factory.h @@ -56,6 +56,7 @@ class PropertyDictionary; class PropertySpecification; class DecoratorInstancerInterface; class RenderManager; +class TextInputHandler; enum class EventId : uint16_t; /** @@ -81,8 +82,9 @@ class RMLUICORE_API Factory { /// Instances a new context. /// @param[in] name The name of the new context. /// @param[in] render_manager The render manager used for the new context. + /// @param[in] text_input_handler The text input handler used for the new context. /// @return The new context, or nullptr if no context could be created. - static ContextPtr InstanceContext(const String& name, RenderManager* render_manager); + static ContextPtr InstanceContext(const String& name, RenderManager* render_manager, TextInputHandler* text_input_handler); /// Registers a non-owning pointer to the element instancer that will be used to instance an element when the specified tag is encountered. /// @param[in] name Name of the instancer; elements with this as their tag will use this instancer. diff --git a/Include/RmlUi/Core/StringUtilities.h b/Include/RmlUi/Core/StringUtilities.h index 70f6305b3..a81533c02 100644 --- a/Include/RmlUi/Core/StringUtilities.h +++ b/Include/RmlUi/Core/StringUtilities.h @@ -133,6 +133,12 @@ namespace StringUtilities { --p; return p; } + + /// Converts a character position in a UTF-8 string to a byte offset. + RMLUICORE_API int ConvertCharacterOffsetToByteOffset(StringView string, int character_offset); + + /// Converts a byte offset of a UTF-8 string to a character position. + RMLUICORE_API int ConvertByteOffsetToCharacterOffset(StringView string, int byte_offset); } // namespace StringUtilities /* diff --git a/Include/RmlUi/Core/TextInputContext.h b/Include/RmlUi/Core/TextInputContext.h new file mode 100644 index 000000000..e06789c80 --- /dev/null +++ b/Include/RmlUi/Core/TextInputContext.h @@ -0,0 +1,90 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2024 The RmlUi Team, and contributors + * + * 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 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. + * + */ + +#ifndef RMLUI_CORE_TEXTINPUTCONTEXT_H +#define RMLUI_CORE_TEXTINPUTCONTEXT_H + +#include + +namespace Rml { + +/** + Interface for an editable text area. + + Methods of this class are used for the internal IME implementation. Nonetheless, this interface + provides extra methods that can be used for a custom IME system or any other work with text inputs. + + To capture the context of a text input, create a custom implementation of TextInputHandler. + See the documentation of the handler for more details. + + The lifetime of RmlUi's implementations is equal to the element's lifetime. + + @see Rml::TextInputHandler + @see Rml::SetTextInputHandler() + */ +class RMLUICORE_API TextInputContext { +public: + virtual ~TextInputContext() {} + + /// Retrieve the screen-space bounds of the text area (in px). + /// @param[out] out_rectangle The resulting rectangle covering the projected element's box (in px). + /// @return True if the bounds can be successfully retrieved, false otherwise. + virtual bool GetBoundingBox(Rectanglef& out_rectangle) const = 0; + + /// Retrieve the selection range. + /// @param[out] start The first character selected. + /// @param[out] end The first character *after* the selection. + virtual void GetSelectionRange(int& start, int& end) const = 0; + + /// Select the text in the given character range. + /// @param[in] start The first character to be selected. + /// @param[in] end The first character *after* the selection. + virtual void SetSelectionRange(int start, int end) = 0; + + /// Move the cursor caret to after a specific character. + /// @param[in] position The character position after which the cursor should be moved. + virtual void SetCursorPosition(int position) = 0; + + /// Replace a text in the given character range. + /// @param[in] text The string to replace the character range with. + /// @param[in] start The first character to be replaced. + /// @param[in] end The first character *after* the range. + virtual void SetText(StringView text, int start, int end) = 0; + + /// Update the range of the text being composed (for IME). + /// @param[in] start The first character in the range. + /// @param[in] end The first character *after* the range. + virtual void SetCompositionRange(int start, int end) = 0; + + /// Commit the current IME composition. + virtual void CommitComposition() = 0; +}; + +} // namespace Rml + +#endif diff --git a/Include/RmlUi/Core/TextInputHandler.h b/Include/RmlUi/Core/TextInputHandler.h new file mode 100644 index 000000000..981e18973 --- /dev/null +++ b/Include/RmlUi/Core/TextInputHandler.h @@ -0,0 +1,66 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2024 The RmlUi Team, and contributors + * + * 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 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. + * + */ + +#ifndef RMLUI_CORE_TEXTINPUTHANDLER_H +#define RMLUI_CORE_TEXTINPUTHANDLER_H + +namespace Rml { + +class TextInputContext; + +/** + Handler of changes to text editable areas. Implement this interface to pick up these events, and pass + the custom implementation to a context (via its constructor) or globally (via SetTextInputHandler). + + Be aware that backends might provide their custom handler to, for example, handle the IME. + + The lifetime of a text input context is ended with the call of OnDestroy(). + + @see Rml::TextInputContext + @see Rml::SetTextInputHandler() + */ +class RMLUICORE_API TextInputHandler : public NonCopyMoveable { +public: + virtual ~TextInputHandler() {} + + /// Called when a text input area is activated (e.g., focused). + /// @param[in] input_context The input context to be activated. + virtual void OnActivate(TextInputContext* /*input_context*/) {} + + /// Called when a text input area is deactivated (e.g., by losing focus). + /// @param[in] input_context The input context to be deactivated. + virtual void OnDeactivate(TextInputContext* /*input_context*/) {} + + /// Invoked when the context of a text input area is destroyed (e.g., when the element is being removed). + /// @param[in] input_context The input context to be destroyed. + virtual void OnDestroy(TextInputContext* /*input_context*/) {} +}; + +} // namespace Rml + +#endif diff --git a/Samples/basic/CMakeLists.txt b/Samples/basic/CMakeLists.txt index 20ab87f38..fe77dc516 100644 --- a/Samples/basic/CMakeLists.txt +++ b/Samples/basic/CMakeLists.txt @@ -29,4 +29,11 @@ if(RMLUI_FONT_ENGINE_ENABLED) else() message(STATUS "SVG sample disabled due to RMLUI_SVG_PLUGIN=OFF") endif() + + # Enable the IME sample only for Windows backends; no other platform backend is currently supported. + if(RMLUI_BACKEND MATCHES "^Win32") + add_subdirectory("ime") + else() + message(STATUS "IME sample disabled due to RMLUI_BACKEND not being prefixed by Win32") + endif() endif() diff --git a/Samples/basic/ime/CMakeLists.txt b/Samples/basic/ime/CMakeLists.txt new file mode 100644 index 000000000..b2aab4a43 --- /dev/null +++ b/Samples/basic/ime/CMakeLists.txt @@ -0,0 +1,14 @@ +set(SAMPLE_NAME "ime") +set(TARGET_NAME "${RMLUI_SAMPLE_PREFIX}${SAMPLE_NAME}") + +add_executable(${TARGET_NAME} WIN32 + src/SystemFontWin32.cpp + src/SystemFontWin32.h + src/main.cpp +) + +set_common_target_options(${TARGET_NAME}) + +target_link_libraries(${TARGET_NAME} PRIVATE rmlui_shell) + +install_sample_target(${TARGET_NAME}) diff --git a/Samples/basic/ime/data/ime.rml b/Samples/basic/ime/data/ime.rml new file mode 100644 index 000000000..adedd1ca8 --- /dev/null +++ b/Samples/basic/ime/data/ime.rml @@ -0,0 +1,72 @@ + + + + IME + + + +

IME Sample

+

Input Method Editor (IME) is a software component that allows the user to type characters not otherwise + available on a standard QWERTY keyboard. This is crucial for languages using a writing system different + from Latin, such as Japanese, Chinese, Vietnamese, and others. You must add the language in the system + options to use such a keyboard.

+

IME is also used for emojis or clipboard history (on Windows).

+ +
+ + + + + +
+ +
diff --git a/Samples/basic/ime/src/SystemFontWin32.cpp b/Samples/basic/ime/src/SystemFontWin32.cpp new file mode 100644 index 000000000..09d62dea6 --- /dev/null +++ b/Samples/basic/ime/src/SystemFontWin32.cpp @@ -0,0 +1,83 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2024 The RmlUi Team, and contributors + * + * 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 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. + * + */ + +#include "SystemFontWin32.h" +#include +#include +#include + +static Rml::String GetSystemFontDirectory() +{ + Rml::String font_path; + PWSTR fonts_path_wide; + if (SHGetKnownFolderPath(FOLDERID_Fonts, 0, NULL, &fonts_path_wide) == S_OK) + { + int buffer_size = WideCharToMultiByte(CP_ACP, 0, fonts_path_wide, -1, NULL, 0, NULL, NULL); + font_path.resize(std::max(buffer_size - 1, 0)); + WideCharToMultiByte(CP_ACP, 0, fonts_path_wide, -1, &font_path[0], buffer_size, NULL, NULL); + + CoTaskMemFree(fonts_path_wide); + } + + return font_path; +} + +Rml::Vector GetSelectedSystemFonts() +{ + Rml::Vector result; + + const Rml::String system_font_directory = GetSystemFontDirectory(); + if (!system_font_directory.empty()) + { + // Partly based on: https://stackoverflow.com/a/57362436/2555318 + const char* system_font_files[] = { + "segoeui.ttf ", // Segoe UI (Latin; Greek; Cyrillic; Armenian; Georgian; Georgian Khutsuri; Arabic; Hebrew; Fraser) + "tahoma.ttf ", // Tahoma (Latin; Greek; Cyrillic; Armenian; Hebrew; Arabic; Thai) + "meiryo.ttc ", // Meiryo UI (Japanese) + "msgothic.ttc", // MS Gothic (Japanese) + "msjh.ttc", // Microsoft JhengHei (Chinese Traditional; Han; Han with Bopomofo) + "msyh.ttc", // Microsoft YaHei (Chinese Simplified; Han) + "malgun.ttf ", // Malgun Gothic (Korean) + "simsun.ttc ", // SimSun (Han Simplified) + "seguiemj.ttf ", // Segoe UI (Latin; Greek; Cyrillic; Armenian; Georgian; Georgian Khutsuri; Arabic; Hebrew; Fraser) + }; + + for (const char* font_file : system_font_files) + { + Rml::String path = system_font_directory + '\\' + font_file; + DWORD attributes = GetFileAttributesA(path.c_str()); + + if (attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY)) + result.push_back(path); + else + Rml::Log::Message(Rml::Log::LT_INFO, "Could not find system font file '%s', skipping.", path.c_str()); + } + } + + return result; +} diff --git a/Samples/basic/ime/src/SystemFontWin32.h b/Samples/basic/ime/src/SystemFontWin32.h new file mode 100644 index 000000000..c84e62299 --- /dev/null +++ b/Samples/basic/ime/src/SystemFontWin32.h @@ -0,0 +1,36 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2024 The RmlUi Team, and contributors + * + * 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 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. + * + */ + +#ifndef RMLUI_SAMPLES_IME_SYSTEMFONTWIN32_H +#define RMLUI_SAMPLES_IME_SYSTEMFONTWIN32_H + +#include + +Rml::Vector GetSelectedSystemFonts(); + +#endif diff --git a/Samples/basic/ime/src/main.cpp b/Samples/basic/ime/src/main.cpp new file mode 100644 index 000000000..adf0529ff --- /dev/null +++ b/Samples/basic/ime/src/main.cpp @@ -0,0 +1,122 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2024 The RmlUi Team, and contributors + * + * 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 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. + * + */ + +#include "SystemFontWin32.h" +#include +#include +#include +#include +#include +#include + +#if !defined RMLUI_PLATFORM_WIN32 + #error "This sample works only on Windows!" +#endif + +static void LoadFonts() +{ + struct FontFace { + Rml::String filename; + bool fallback_face; + }; + Rml::Vector font_faces = { + {"assets/LatoLatin-Regular.ttf", false}, + {"assets/LatoLatin-Italic.ttf", false}, + {"assets/LatoLatin-Bold.ttf", false}, + {"assets/LatoLatin-BoldItalic.ttf", false}, + }; + + for (const Rml::String& path : GetSelectedSystemFonts()) + font_faces.push_back({path, true}); + + for (const FontFace& face : font_faces) + Rml::LoadFontFace(face.filename, face.fallback_face); +} + +int APIENTRY WinMain(HINSTANCE /*instance_handle*/, HINSTANCE /*previous_instance_handle*/, char* /*command_line*/, int /*command_show*/) +{ + const int window_width = 1024; + const int window_height = 768; + + // Initializes the shell which provides common functionality used by the included samples. + if (!Shell::Initialize()) + return -1; + + // Constructs the system and render interfaces, creates a window, and attaches the renderer. + if (!Backend::Initialize("IME Sample", window_width, window_height, true)) + { + Shell::Shutdown(); + return -1; + } + + // Install the custom interfaces constructed by the backend before initializing RmlUi. + Rml::SetSystemInterface(Backend::GetSystemInterface()); + Rml::SetRenderInterface(Backend::GetRenderInterface()); + + // RmlUi initialisation. + Rml::Initialise(); + + // Create the main RmlUi context. + Rml::Context* context = Rml::CreateContext("main", Rml::Vector2i(window_width, window_height)); + if (!context) + { + Rml::Shutdown(); + Backend::Shutdown(); + Shell::Shutdown(); + return -1; + } + + Rml::Debugger::Initialise(context); + + // Load required fonts with support for most character sets. + LoadFonts(); + + // Load and show the demo document. + Rml::ElementDocument* document = context->LoadDocument("basic/ime/data/ime.rml"); + if (document) + document->Show(); + + bool running = true; + while (running) + { + running = Backend::ProcessEvents(context, &Shell::ProcessKeyDownShortcuts, true); + + context->Update(); + + Backend::BeginFrame(); + context->Render(); + Backend::PresentFrame(); + } + + Rml::Shutdown(); + + Backend::Shutdown(); + Shell::Shutdown(); + + return 0; +} diff --git a/Samples/readme.md b/Samples/readme.md index 6cd29e848..799701551 100644 --- a/Samples/readme.md +++ b/Samples/readme.md @@ -21,6 +21,7 @@ This directory contains basic applications that demonstrate initialisation, usag - `drag` Dragging elements between containers. - `effects` Advanced rendering effects, including filters, gradients and box shadows. Only enabled with supported backends. - `harfbuzz` Advanced text shaping. Only enabled when [HarfBuzz](https://harfbuzz.github.io/) is enabled. +- `ime` A showcase of Input Method Editor (IME) with fallback fonts to support different writing systems. Available only when using a Windows backend. - `load_document` Loading your first document. - `lottie` Playing Lottie animations, only enabled with the [Lottie plugin](https://mikke89.github.io/RmlUiDoc/pages/cpp_manual/lottie.html). - `svg` Render SVG images, only enabled with the [SVG plugin](https://mikke89.github.io/RmlUiDoc/pages/cpp_manual/svg.html). diff --git a/Source/Core/CMakeLists.txt b/Source/Core/CMakeLists.txt index c377ff490..99bebaa98 100644 --- a/Source/Core/CMakeLists.txt +++ b/Source/Core/CMakeLists.txt @@ -314,6 +314,8 @@ target_sources(rmlui_core PRIVATE "${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/StyleSheetTypes.h" "${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/StyleTypes.h" "${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/SystemInterface.h" + "${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/TextInputContext.h" + "${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/TextInputHandler.h" "${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/TextShapingContext.h" "${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Texture.h" "${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Traits.h" diff --git a/Source/Core/Context.cpp b/Source/Core/Context.cpp index 39345c8cb..9afc801a7 100644 --- a/Source/Core/Context.cpp +++ b/Source/Core/Context.cpp @@ -31,6 +31,7 @@ #include "../../Include/RmlUi/Core/ContextInstancer.h" #include "../../Include/RmlUi/Core/Core.h" #include "../../Include/RmlUi/Core/DataModelHandle.h" +#include "../../Include/RmlUi/Core/Debug.h" #include "../../Include/RmlUi/Core/ElementDocument.h" #include "../../Include/RmlUi/Core/ElementUtilities.h" #include "../../Include/RmlUi/Core/Factory.h" @@ -38,7 +39,6 @@ #include "../../Include/RmlUi/Core/RenderManager.h" #include "../../Include/RmlUi/Core/StreamMemory.h" #include "../../Include/RmlUi/Core/SystemInterface.h" -#include "../../Include/RmlUi/Core/Debug.h" #include "DataModel.h" #include "EventDispatcher.h" #include "PluginRegistry.h" @@ -54,7 +54,8 @@ static constexpr float DOUBLE_CLICK_TIME = 0.5f; // [s] static constexpr float DOUBLE_CLICK_MAX_DIST = 3.f; // [dp] static constexpr float UNIT_SCROLL_LENGTH = 80.f; // [dp] -Context::Context(const String& name, RenderManager* render_manager) : name(name), render_manager(render_manager) +Context::Context(const String& name, RenderManager* render_manager, TextInputHandler* text_input_handler) : + name(name), render_manager(render_manager), text_input_handler(text_input_handler) { instancer = nullptr; @@ -857,6 +858,11 @@ RenderManager& Context::GetRenderManager() return *render_manager; } +TextInputHandler* Context::GetTextInputHandler() const +{ + return text_input_handler; +} + void Context::SetInstancer(ContextInstancer* _instancer) { RMLUI_ASSERT(instancer == nullptr); diff --git a/Source/Core/ContextInstancerDefault.cpp b/Source/Core/ContextInstancerDefault.cpp index 351160b7b..d9dcf1c53 100644 --- a/Source/Core/ContextInstancerDefault.cpp +++ b/Source/Core/ContextInstancerDefault.cpp @@ -35,9 +35,9 @@ ContextInstancerDefault::ContextInstancerDefault() {} ContextInstancerDefault::~ContextInstancerDefault() {} -ContextPtr ContextInstancerDefault::InstanceContext(const String& name, RenderManager* render_manager) +ContextPtr ContextInstancerDefault::InstanceContext(const String& name, RenderManager* render_manager, TextInputHandler* text_input_handler) { - return ContextPtr(new Context(name, render_manager)); + return ContextPtr(new Context(name, render_manager, text_input_handler)); } void ContextInstancerDefault::ReleaseContext(Context* context) diff --git a/Source/Core/ContextInstancerDefault.h b/Source/Core/ContextInstancerDefault.h index afffdd71a..cb19cd807 100644 --- a/Source/Core/ContextInstancerDefault.h +++ b/Source/Core/ContextInstancerDefault.h @@ -45,7 +45,7 @@ class ContextInstancerDefault : public ContextInstancer { virtual ~ContextInstancerDefault(); /// Instances a context. - ContextPtr InstanceContext(const String& name, RenderManager* render_manager) override; + ContextPtr InstanceContext(const String& name, RenderManager* render_manager, TextInputHandler* text_input_handler) override; /// Releases a context previously created by this context. void ReleaseContext(Context* context) override; diff --git a/Source/Core/Core.cpp b/Source/Core/Core.cpp index eb3941f83..08722f407 100644 --- a/Source/Core/Core.cpp +++ b/Source/Core/Core.cpp @@ -37,6 +37,7 @@ #include "../../Include/RmlUi/Core/RenderManager.h" #include "../../Include/RmlUi/Core/StyleSheetSpecification.h" #include "../../Include/RmlUi/Core/SystemInterface.h" +#include "../../Include/RmlUi/Core/TextInputHandler.h" #include "../../Include/RmlUi/Core/Types.h" #include "EventSpecification.h" #include "FileInterfaceDefault.h" @@ -71,11 +72,14 @@ static SystemInterface* system_interface = nullptr; static FileInterface* file_interface = nullptr; // RmlUi's font engine interface. static FontEngineInterface* font_interface = nullptr; +// RmlUi's text input handler implementation. +static TextInputHandler* text_input_handler = nullptr; // Default interfaces should be created and destroyed on Initialise and Shutdown, respectively. static UniquePtr default_system_interface; static UniquePtr default_file_interface; static UniquePtr default_font_interface; +static UniquePtr default_text_input_handler; static UniquePtr>> render_managers; @@ -124,6 +128,12 @@ bool Initialise() #endif } + if (!text_input_handler) + { + default_text_input_handler = MakeUnique(); + text_input_handler = default_text_input_handler.get(); + } + EventSpecificationInterface::Initialize(); render_managers = MakeUnique>>(); @@ -235,7 +245,18 @@ FontEngineInterface* GetFontEngineInterface() return font_interface; } -Context* CreateContext(const String& name, const Vector2i dimensions, RenderInterface* render_interface_for_context) +void SetTextInputHandler(TextInputHandler* _text_input_handler) +{ + text_input_handler = _text_input_handler; +} + +TextInputHandler* GetTextInputHandler() +{ + return text_input_handler; +} + +Context* CreateContext(const String& name, const Vector2i dimensions, RenderInterface* render_interface_for_context, + TextInputHandler* text_input_handler_for_context) { if (!initialised) return nullptr; @@ -243,6 +264,9 @@ Context* CreateContext(const String& name, const Vector2i dimensions, RenderInte if (!render_interface_for_context) render_interface_for_context = render_interface; + if (!text_input_handler_for_context) + text_input_handler_for_context = text_input_handler; + if (!render_interface_for_context) { Log::Message(Log::LT_WARNING, "Failed to create context '%s', no render interface specified and no default render interface exists.", @@ -261,7 +285,7 @@ Context* CreateContext(const String& name, const Vector2i dimensions, RenderInte if (!render_manager) render_manager = MakeUnique(render_interface_for_context); - ContextPtr new_context = Factory::InstanceContext(name, render_manager.get()); + ContextPtr new_context = Factory::InstanceContext(name, render_manager.get(), text_input_handler_for_context); if (!new_context) { Log::Message(Log::LT_WARNING, "Failed to instance context '%s', instancer returned nullptr.", name.c_str()); diff --git a/Source/Core/Elements/ElementFormControlInput.cpp b/Source/Core/Elements/ElementFormControlInput.cpp index 732a70f59..2769be767 100644 --- a/Source/Core/Elements/ElementFormControlInput.cpp +++ b/Source/Core/Elements/ElementFormControlInput.cpp @@ -81,6 +81,12 @@ void ElementFormControlInput::GetSelection(int* selection_start, int* selection_ type->GetSelection(selection_start, selection_end, selected_text); } +void ElementFormControlInput::SetCompositionRange(int range_start, int range_end) +{ + RMLUI_ASSERT(type); + type->SetCompositionRange(range_start, range_end); +} + void ElementFormControlInput::OnUpdate() { RMLUI_ASSERT(type); diff --git a/Source/Core/Elements/ElementFormControlTextArea.cpp b/Source/Core/Elements/ElementFormControlTextArea.cpp index 041fe4074..f0503b84f 100644 --- a/Source/Core/Elements/ElementFormControlTextArea.cpp +++ b/Source/Core/Elements/ElementFormControlTextArea.cpp @@ -119,6 +119,11 @@ void ElementFormControlTextArea::GetSelection(int* selection_start, int* selecti widget->GetSelection(selection_start, selection_end, selected_text); } +void ElementFormControlTextArea::SetCompositionRange(int range_start, int range_end) +{ + widget->SetCompositionRange(range_start, range_end); +} + bool ElementFormControlTextArea::GetIntrinsicDimensions(Vector2f& dimensions, float& /*ratio*/) { dimensions.x = (float)(GetNumColumns() * ElementUtilities::GetStringWidth(this, "m")); diff --git a/Source/Core/Elements/InputType.cpp b/Source/Core/Elements/InputType.cpp index 753f68920..3de450977 100644 --- a/Source/Core/Elements/InputType.cpp +++ b/Source/Core/Elements/InputType.cpp @@ -70,4 +70,6 @@ void InputType::SetSelectionRange(int /*selection_start*/, int /*selection_end*/ void InputType::GetSelection(int* /*selection_start*/, int* /*selection_end*/, String* /*selected_text*/) const {} +void InputType::SetCompositionRange(int /*range_start*/, int /*range_end*/) {} + } // namespace Rml diff --git a/Source/Core/Elements/InputType.h b/Source/Core/Elements/InputType.h index 477eee506..49d971f08 100644 --- a/Source/Core/Elements/InputType.h +++ b/Source/Core/Elements/InputType.h @@ -94,6 +94,9 @@ class InputType { /// Retrieves the selection range and text. virtual void GetSelection(int* selection_start, int* selection_end, String* selected_text) const; + /// Sets visual feedback for the IME composition in the given character range. + virtual void SetCompositionRange(int range_start, int range_end); + protected: ElementFormControlInput* element; }; diff --git a/Source/Core/Elements/InputTypeText.cpp b/Source/Core/Elements/InputTypeText.cpp index b379b2e23..b4cf485a0 100644 --- a/Source/Core/Elements/InputTypeText.cpp +++ b/Source/Core/Elements/InputTypeText.cpp @@ -132,4 +132,9 @@ void InputTypeText::GetSelection(int* selection_start, int* selection_end, Strin widget->GetSelection(selection_start, selection_end, selected_text); } +void InputTypeText::SetCompositionRange(int range_start, int range_end) +{ + widget->SetCompositionRange(range_start, range_end); +} + } // namespace Rml diff --git a/Source/Core/Elements/InputTypeText.h b/Source/Core/Elements/InputTypeText.h index 5143e4967..912fa949c 100644 --- a/Source/Core/Elements/InputTypeText.h +++ b/Source/Core/Elements/InputTypeText.h @@ -83,6 +83,9 @@ class InputTypeText : public InputType { /// Retrieves the selection range and text. void GetSelection(int* selection_start, int* selection_end, String* selected_text) const override; + /// Sets visual feedback for the IME composition in the given character range. + void SetCompositionRange(int range_start, int range_end) override; + private: int size = 20; diff --git a/Source/Core/Elements/WidgetTextInput.cpp b/Source/Core/Elements/WidgetTextInput.cpp index c7d361985..4793e3c48 100644 --- a/Source/Core/Elements/WidgetTextInput.cpp +++ b/Source/Core/Elements/WidgetTextInput.cpp @@ -41,6 +41,8 @@ #include "../../../Include/RmlUi/Core/MeshUtilities.h" #include "../../../Include/RmlUi/Core/StringUtilities.h" #include "../../../Include/RmlUi/Core/SystemInterface.h" +#include "../../../Include/RmlUi/Core/TextInputContext.h" +#include "../../../Include/RmlUi/Core/TextInputHandler.h" #include "../Clock.h" #include "ElementTextSelection.h" #include @@ -62,46 +64,122 @@ static CharacterClass GetCharacterClass(char c) return CharacterClass::Whitespace; } -static int ConvertCharacterOffsetToByteOffset(const String& value, int character_offset) +// Clamps the value to the given maximum number of unicode code points. Returns true if the value was changed. +static bool ClampValue(String& value, int max_length) { - if (character_offset >= (int)value.size()) - return (int)value.size(); - - int character_count = 0; - for (auto it = StringIteratorU8(value); it; ++it) + if (max_length >= 0) { - character_count += 1; - if (character_count > character_offset) - return (int)it.offset(); + int max_byte_length = StringUtilities::ConvertCharacterOffsetToByteOffset(value, max_length); + if (max_byte_length < (int)value.size()) + { + value.erase((size_t)max_byte_length); + return true; + } } - return (int)value.size(); + return false; } -static int ConvertByteOffsetToCharacterOffset(const String& value, int byte_offset) +class WidgetTextInputContext final : public TextInputContext { +public: + WidgetTextInputContext(TextInputHandler* handler, WidgetTextInput* _owner, ElementFormControl* _element); + ~WidgetTextInputContext(); + + bool GetBoundingBox(Rectanglef& out_rectangle) const override; + void GetSelectionRange(int& start, int& end) const override; + void SetSelectionRange(int start, int end) override; + void SetCursorPosition(int position) override; + void SetText(StringView text, int start, int end) override; + void SetCompositionRange(int start, int end) override; + void CommitComposition() override; + +private: + TextInputHandler* handler; + WidgetTextInput* owner; + ElementFormControl* element; + String composition; +}; + +WidgetTextInputContext::WidgetTextInputContext(TextInputHandler* handler, WidgetTextInput* owner, ElementFormControl* element) : + handler(handler), owner(owner), element(element) +{} + +WidgetTextInputContext::~WidgetTextInputContext() { - int character_count = 0; - for (auto it = StringIteratorU8(value); it; ++it) - { - if (it.offset() >= byte_offset) - break; - character_count += 1; - } - return character_count; + handler->OnDestroy(this); } -// Clamps the value to the given maximum number of unicode code points. Returns true if the value was changed. -static bool ClampValue(String& value, int max_length) +bool WidgetTextInputContext::GetBoundingBox(Rectanglef& out_rectangle) const { - if (max_length >= 0) + return ElementUtilities::GetBoundingBox(out_rectangle, element, BoxArea::Border); +} + +void WidgetTextInputContext::GetSelectionRange(int& start, int& end) const +{ + owner->GetSelection(&start, &end, nullptr); +} + +void WidgetTextInputContext::SetSelectionRange(int start, int end) +{ + owner->SetSelectionRange(start, end); +} + +void WidgetTextInputContext::SetCursorPosition(int position) +{ + SetSelectionRange(position, position); +} + +void WidgetTextInputContext::SetText(StringView text, int start, int end) +{ + String value = owner->GetAttributeValue(); + + start = StringUtilities::ConvertCharacterOffsetToByteOffset(value, start); + end = StringUtilities::ConvertCharacterOffsetToByteOffset(value, end); + + RMLUI_ASSERTMSG(end >= start, "Invalid end character offset."); + value.replace(start, end - start, text.begin(), text.size()); + + element->SetValue(value); + + composition = String(text); +} + +void WidgetTextInputContext::SetCompositionRange(int start, int end) +{ + owner->SetCompositionRange(start, end); +} + +void WidgetTextInputContext::CommitComposition() +{ + int start_byte, end_byte; + owner->GetCompositionRange(start_byte, end_byte); + + // No composition to commit. + if (start_byte == 0 && end_byte == 0) + return; + + String value = owner->GetAttributeValue(); + + // If the text input has a length restriction, we have to shorten the composition string. + if (owner->GetMaxLength() >= 0) { - int max_byte_length = ConvertCharacterOffsetToByteOffset(value, max_length); - if (max_byte_length < (int)value.size()) + int start = StringUtilities::ConvertByteOffsetToCharacterOffset(value, start_byte); + int end = StringUtilities::ConvertByteOffsetToCharacterOffset(value, end_byte); + + int value_length = (int)StringUtilities::LengthUTF8(value); + int composition_length = (int)StringUtilities::LengthUTF8(composition); + + // The requested text value would exceed the length restriction after replacing the original value. + if (value_length + composition_length - (start - end) > owner->GetMaxLength()) { - value.erase((size_t)max_byte_length); - return true; + int new_length = owner->GetMaxLength() - (value_length - composition_length); + composition.erase(StringUtilities::ConvertCharacterOffsetToByteOffset(composition, new_length)); } } - return false; + + RMLUI_ASSERTMSG(end_byte >= start_byte, "Invalid end character offset."); + value.replace(start_byte, end_byte - start_byte, composition.data(), composition.size()); + + element->SetValue(value); } WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) : @@ -162,6 +240,9 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) : selection_begin_index = 0; selection_length = 0; + ime_composition_begin_index = 0; + ime_composition_end_index = 0; + last_update_time = 0; ShowCursor(false); @@ -209,6 +290,10 @@ void WidgetTextInput::SetValue(String value) text_element->SetText(value); + // Reset the IME composition range when the value changes. + ime_composition_begin_index = 0; + ime_composition_end_index = 0; + FormatElement(); UpdateCursorPosition(true); } @@ -250,8 +335,8 @@ void WidgetTextInput::SetSelectionRange(int selection_start, int selection_end) return; const String& value = GetValue(); - const int byte_start = ConvertCharacterOffsetToByteOffset(value, selection_start); - const int byte_end = ConvertCharacterOffsetToByteOffset(value, selection_end); + const int byte_start = StringUtilities::ConvertCharacterOffsetToByteOffset(value, selection_start); + const int byte_end = StringUtilities::ConvertCharacterOffsetToByteOffset(value, selection_end); const bool is_selecting = (byte_start != byte_end); cursor_wrap_down = true; @@ -279,13 +364,39 @@ void WidgetTextInput::GetSelection(int* selection_start, int* selection_end, Str { const String& value = GetValue(); if (selection_start) - *selection_start = ConvertByteOffsetToCharacterOffset(value, selection_begin_index); + *selection_start = StringUtilities::ConvertByteOffsetToCharacterOffset(value, selection_begin_index); if (selection_end) - *selection_end = ConvertByteOffsetToCharacterOffset(value, selection_begin_index + selection_length); + *selection_end = StringUtilities::ConvertByteOffsetToCharacterOffset(value, selection_begin_index + selection_length); if (selected_text) *selected_text = value.substr(Math::Min((size_t)selection_begin_index, (size_t)value.size()), (size_t)selection_length); } +void WidgetTextInput::SetCompositionRange(int range_start, int range_end) +{ + const String& value = GetValue(); + const int byte_start = StringUtilities::ConvertCharacterOffsetToByteOffset(value, range_start); + const int byte_end = StringUtilities::ConvertCharacterOffsetToByteOffset(value, range_end); + + if (byte_end > byte_start) + { + ime_composition_begin_index = byte_start; + ime_composition_end_index = byte_end; + } + else + { + ime_composition_begin_index = 0; + ime_composition_end_index = 0; + } + + FormatText(); +} + +void WidgetTextInput::GetCompositionRange(int& range_start, int& range_end) const +{ + range_start = ime_composition_begin_index; + range_end = ime_composition_end_index; +} + void WidgetTextInput::UpdateSelectionColours() { // Determine what the colour of the selected text is. If our 'selection' element has the 'color' @@ -359,6 +470,7 @@ void WidgetTextInput::OnRender() Vector2f text_translation = parent->GetAbsoluteOffset() - Vector2f(parent->GetScrollLeft(), parent->GetScrollTop()); selection_geometry.Render(text_translation); + ime_composition_geometry.Render(text_translation); if (cursor_visible && !parent->IsDisabled()) { @@ -385,6 +497,13 @@ Element* WidgetTextInput::GetElement() const return parent; } +TextInputHandler* WidgetTextInput::GetTextInputHandler() const +{ + if (Context* context = parent->GetContext()) + return context->GetTextInputHandler(); + return nullptr; +} + bool WidgetTextInput::IsFocused() const { return cursor_timer > 0; @@ -540,6 +659,15 @@ void WidgetTextInput::ProcessEvent(Event& event) if (UpdateSelection(false)) FormatElement(); ShowCursor(true, false); + + if (TextInputHandler* handler = GetTextInputHandler()) + { + // Lazily instance the text input context for this widget. + if (!text_input_context) + text_input_context = MakeUnique(handler, this, parent); + + handler->OnActivate(text_input_context.get()); + } } } break; @@ -547,6 +675,8 @@ void WidgetTextInput::ProcessEvent(Event& event) { if (event.GetTargetElement() == parent) { + if (TextInputHandler* handler = GetTextInputHandler()) + handler->OnDeactivate(text_input_context.get()); if (ClearSelection()) FormatElement(); ShowCursor(false, false); @@ -1080,6 +1210,8 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) if (!font_handle) return content_area; + const FontMetrics& font_metrics = GetFontEngineInterface()->GetFontMetrics(font_handle); + // Clear the old lines, and all the lines in the text elements. lines.clear(); text_element->ClearLines(); @@ -1087,9 +1219,9 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) // Determine the line-height of the text element. const float line_height = parent->GetLineHeight(); - const float font_baseline = GetFontEngineInterface()->GetFontMetrics(font_handle).ascent; - // When the selection contains endlines we expand the selection area by this width. - const int endline_selection_width = int(0.4f * parent->GetComputedValues().font_size()); + const float font_baseline = font_metrics.ascent; + // When the selection contains endlines, we expand the selection area by this width. + const int endline_font_width = int(0.4f * parent->GetComputedValues().font_size()); const float client_width = parent->GetClientWidth(); int line_begin = 0; @@ -1108,6 +1240,14 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) Vector segments; + struct IMESegment { + Vector2f position; + int width; + int line_index; + }; + + Vector ime_segments; + // Keep generating lines until all the text content is placed. do { @@ -1217,6 +1357,18 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) segments.push_back({line_position, width, post_selection, false, (int)lines.size()}); } + // We fetch the IME composition on the new line to highlight it. + String ime_pre_composition, ime_composition; + GetLineIMEComposition(ime_pre_composition, ime_composition, line_content, line_begin); + + // If there is any IME composition string on the line, create a segment for its underline. + if (!ime_composition.empty()) + { + const int composition_width = ElementUtilities::GetStringWidth(text_element, ime_composition); + const Vector2f composition_position(float(ElementUtilities::GetStringWidth(text_element, ime_pre_composition)), line_position.y); + ime_segments.push_back({composition_position, composition_width, (int)lines.size()}); + } + // Update variables for the next line. line_begin += line.size; line_position.x = 0; @@ -1240,7 +1392,7 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) // Transform segments according to text alignment for (auto& it : segments) { - auto const& line = lines[it.line_index]; + const auto& line = lines[it.line_index]; const char* p_begin = GetValue().data() + line.value_offset; float offset = GetAlignmentSpecificTextOffset(p_begin, it.line_index); @@ -1249,7 +1401,7 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) if (it.selected) { const bool selection_contains_endline = (selection_begin_index + selection_length > line_begin + lines[it.line_index].editable_length); - const Vector2f selection_size(float(it.width + (selection_contains_endline ? endline_selection_width : 0)), line_height); + const Vector2f selection_size(float(it.width + (selection_contains_endline ? endline_font_width : 0)), line_height); MeshUtilities::GenerateQuad(selection_mesh, it.position - Vector2f(0, font_baseline), selection_size, selection_colour); @@ -1261,6 +1413,27 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) selection_geometry = parent->GetRenderManager()->MakeGeometry(std::move(selection_mesh)); + // Clear the IME composition geometry, and get the vertices and indices so the new geometry can be generated. + Mesh ime_composition_mesh = ime_composition_geometry.Release(Geometry::ReleaseMode::ClearMesh); + + // Transform IME segments according to text alignment. + for (auto& it : ime_segments) + { + const auto& line = lines[it.line_index]; + const char* p_begin = GetValue().data() + line.value_offset; + float offset = GetAlignmentSpecificTextOffset(p_begin, it.line_index); + + it.position.x += offset; + it.position.y += font_metrics.underline_position; + + const bool composition_contains_endline = (ime_composition_end_index > line_begin + lines[it.line_index].editable_length); + const Vector2f line_size(float(it.width + (composition_contains_endline ? endline_font_width : 0)), font_metrics.underline_thickness); + + MeshUtilities::GenerateLine(ime_composition_mesh, it.position, line_size, parent->GetComputedValues().color().ToPremultiplied()); + } + + ime_composition_geometry = parent->GetRenderManager()->MakeGeometry(std::move(ime_composition_mesh)); + return content_area; } @@ -1295,7 +1468,7 @@ void WidgetTextInput::UpdateCursorPosition(bool update_ideal_cursor_position) int cursor_line_index = 0, cursor_character_index = 0; GetRelativeCursorIndices(cursor_line_index, cursor_character_index); - auto const& line = lines[cursor_line_index]; + const auto& line = lines[cursor_line_index]; const char* p_begin = GetValue().data() + line.value_offset; cursor_position.x = (float)ElementUtilities::GetStringWidth(text_element, String(p_begin, cursor_character_index)); @@ -1393,6 +1566,24 @@ void WidgetTextInput::GetLineSelection(String& pre_selection, String& selection, post_selection = line.substr(Clamp(selection_end - line_begin, 0, line_length)); } +void WidgetTextInput::GetLineIMEComposition(String& pre_composition, String& ime_composition, const String& line, int line_begin) const +{ + const int composition_length = ime_composition_end_index - ime_composition_begin_index; + + // Check if the line has any text in the IME composition range at all. + if (composition_length <= 0 || ime_composition_end_index < line_begin || ime_composition_begin_index > line_begin + (int)line.size()) + { + pre_composition = line; + return; + } + + const int line_length = (int)line.size(); + + pre_composition = line.substr(0, Math::Max(0, ime_composition_begin_index - line_begin)); + ime_composition = line.substr(Math::Clamp(ime_composition_begin_index - line_begin, 0, line_length), + Math::Max(0, composition_length + Math::Min(0, ime_composition_begin_index - line_begin))); +} + void WidgetTextInput::SetKeyboardActive(bool active) { if (SystemInterface* system = GetSystemInterface()) diff --git a/Source/Core/Elements/WidgetTextInput.h b/Source/Core/Elements/WidgetTextInput.h index 1e699487e..957426c90 100644 --- a/Source/Core/Elements/WidgetTextInput.h +++ b/Source/Core/Elements/WidgetTextInput.h @@ -38,6 +38,8 @@ namespace Rml { class ElementText; class ElementFormControl; +class TextInputHandler; +class WidgetTextInputContext; /** An abstract widget for editing and navigating around a text field. @@ -54,6 +56,8 @@ class WidgetTextInput : public EventListener { /// @param[in] value The new value to set on the text field. /// @note The value will be sanitized and synchronized with the element's value attribute. void SetValue(String value); + /// Returns the underlying text from the element's value attribute. + String GetAttributeValue() const; /// Sets the maximum length (in characters) of this text field. /// @param[in] max_length The new maximum length of the text field. A number lower than zero will mean infinite characters. @@ -76,6 +80,13 @@ class WidgetTextInput : public EventListener { /// @param[out] selected_text The selected text. void GetSelection(int* selection_start, int* selection_end, String* selected_text) const; + /// Sets visual feedback used for the IME composition in the range. + /// @param[in] range_start The first character to be selected. + /// @param[in] range_end The first character *after* the selection. + void SetCompositionRange(int range_start, int range_end); + /// Obtains the IME composition byte range relative to the current value. + void GetCompositionRange(int& range_start, int& range_end) const; + /// Update the colours of the selected text. void UpdateSelectionColours(); /// Generates the text cursor. @@ -121,6 +132,9 @@ class WidgetTextInput : public EventListener { /// Gets the parent element containing the widget. Element* GetElement() const; + /// Obtains the text input handler of the parent element's context. + TextInputHandler* GetTextInputHandler() const; + /// Returns true if the text input element is currently focused. bool IsFocused() const; @@ -131,8 +145,6 @@ class WidgetTextInput : public EventListener { /// Returns the displayed value of the text field. /// @note For password fields this would only return the displayed asterisks '****', while the attribute value below contains the underlying text. const String& GetValue() const; - /// Returns the underlying text from the element's value attribute. - String GetAttributeValue() const; /// Moves the cursor along the current line. /// @param[in] movement Cursor movement operation. @@ -205,6 +217,12 @@ class WidgetTextInput : public EventListener { /// @param[in] line The text making up the line. /// @param[in] line_begin The absolute index at the beginning of the line. void GetLineSelection(String& pre_selection, String& selection, String& post_selection, const String& line, int line_begin) const; + /// Fetch the IME composition range on the line. + /// @param[out] pre_composition The section of text before the IME composition string on the line. + /// @param[out] ime_composition The IME composition string on the line. + /// @param[in] line The text making up the line. + /// @param[in] line_begin The absolute index at the beginning of the line. + void GetLineIMEComposition(String& pre_composition, String& ime_composition, const String& line, int line_begin) const; struct Line { // Offset into the text field's value. @@ -251,6 +269,16 @@ class WidgetTextInput : public EventListener { // The selection background. Geometry selection_geometry; + // IME composition range. The start and end indices are in absolute coordinates. + int ime_composition_begin_index; + int ime_composition_end_index; + + // The IME composition text highlighting. + Geometry ime_composition_geometry; + + // The IME context for this widget. + UniquePtr text_input_context; + // Cursor visibility and timings. float cursor_timer; bool cursor_visible; diff --git a/Source/Core/Factory.cpp b/Source/Core/Factory.cpp index fdeaef91c..1c18f787f 100644 --- a/Source/Core/Factory.cpp +++ b/Source/Core/Factory.cpp @@ -342,9 +342,9 @@ void Factory::RegisterContextInstancer(ContextInstancer* instancer) context_instancer = instancer; } -ContextPtr Factory::InstanceContext(const String& name, RenderManager* render_manager) +ContextPtr Factory::InstanceContext(const String& name, RenderManager* render_manager, TextInputHandler* text_input_handler) { - ContextPtr new_context = context_instancer->InstanceContext(name, render_manager); + ContextPtr new_context = context_instancer->InstanceContext(name, render_manager, text_input_handler); if (new_context) new_context->SetInstancer(context_instancer); return new_context; diff --git a/Source/Core/StringUtilities.cpp b/Source/Core/StringUtilities.cpp index b1ef0c005..63878115b 100644 --- a/Source/Core/StringUtilities.cpp +++ b/Source/Core/StringUtilities.cpp @@ -524,6 +524,33 @@ size_t StringUtilities::LengthUTF8(StringView string_view) return string_view.size() - num_continuation_bytes; } +int StringUtilities::ConvertCharacterOffsetToByteOffset(StringView string, int character_offset) +{ + if (character_offset >= (int)string.size()) + return (int)string.size(); + + int character_count = 0; + for (auto it = StringIteratorU8(string.begin(), string.begin(), string.end()); it; ++it) + { + character_count += 1; + if (character_count > character_offset) + return (int)it.offset(); + } + return (int)string.size(); +} + +int StringUtilities::ConvertByteOffsetToCharacterOffset(StringView string, int byte_offset) +{ + int character_count = 0; + for (auto it = StringIteratorU8(string.begin(), string.begin(), string.end()); it; ++it) + { + if (it.offset() >= byte_offset) + break; + character_count += 1; + } + return character_count; +} + StringView::StringView() { const char* empty_string = ""; diff --git a/Tests/Source/UnitTests/StringUtilities.cpp b/Tests/Source/UnitTests/StringUtilities.cpp index 352dcc549..08b956803 100644 --- a/Tests/Source/UnitTests/StringUtilities.cpp +++ b/Tests/Source/UnitTests/StringUtilities.cpp @@ -101,10 +101,10 @@ TEST_CASE("StringView") CHECK(StringView() == ""); } -#include "../../../Source/Core/Elements/WidgetTextInput.cpp" - -TEST_CASE("ConvertByteOffsetToCharacterOffset") +TEST_CASE("StringUtilities::ConvertByteOffsetToCharacterOffset") { + using namespace Rml::StringUtilities; + // clang-format off CHECK(ConvertByteOffsetToCharacterOffset("", 0) == 0); CHECK(ConvertByteOffsetToCharacterOffset("", 1) == 0); @@ -125,8 +125,10 @@ TEST_CASE("ConvertByteOffsetToCharacterOffset") // clang-format on } -TEST_CASE("ConvertCharacterOffsetToByteOffset") +TEST_CASE("StringUtilities::ConvertCharacterOffsetToByteOffset") { + using namespace Rml::StringUtilities; + // clang-format off CHECK(ConvertCharacterOffsetToByteOffset("", 0) == 0); CHECK(ConvertCharacterOffsetToByteOffset("", 1) == 0);