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);