浏览代码

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 <[email protected]>
Jan Lupčík 1 年之前
父节点
当前提交
d2ce1326ce
共有 36 个文件被更改,包括 1212 次插入64 次删除
  1. 10 2
      Backends/RmlUi_Backend_Win32_GL2.cpp
  2. 10 2
      Backends/RmlUi_Backend_Win32_VK.cpp
  3. 240 1
      Backends/RmlUi_Platform_Win32.cpp
  4. 59 1
      Backends/RmlUi_Platform_Win32.h
  5. 8 1
      Include/RmlUi/Core/Context.h
  6. 3 1
      Include/RmlUi/Core/ContextInstancer.h
  7. 15 3
      Include/RmlUi/Core/Core.h
  8. 6 0
      Include/RmlUi/Core/Elements/ElementFormControlInput.h
  9. 5 0
      Include/RmlUi/Core/Elements/ElementFormControlTextArea.h
  10. 3 1
      Include/RmlUi/Core/Factory.h
  11. 6 0
      Include/RmlUi/Core/StringUtilities.h
  12. 90 0
      Include/RmlUi/Core/TextInputContext.h
  13. 66 0
      Include/RmlUi/Core/TextInputHandler.h
  14. 7 0
      Samples/basic/CMakeLists.txt
  15. 14 0
      Samples/basic/ime/CMakeLists.txt
  16. 72 0
      Samples/basic/ime/data/ime.rml
  17. 83 0
      Samples/basic/ime/src/SystemFontWin32.cpp
  18. 36 0
      Samples/basic/ime/src/SystemFontWin32.h
  19. 122 0
      Samples/basic/ime/src/main.cpp
  20. 1 0
      Samples/readme.md
  21. 2 0
      Source/Core/CMakeLists.txt
  22. 8 2
      Source/Core/Context.cpp
  23. 2 2
      Source/Core/ContextInstancerDefault.cpp
  24. 1 1
      Source/Core/ContextInstancerDefault.h
  25. 26 2
      Source/Core/Core.cpp
  26. 6 0
      Source/Core/Elements/ElementFormControlInput.cpp
  27. 5 0
      Source/Core/Elements/ElementFormControlTextArea.cpp
  28. 2 0
      Source/Core/Elements/InputType.cpp
  29. 3 0
      Source/Core/Elements/InputType.h
  30. 5 0
      Source/Core/Elements/InputTypeText.cpp
  31. 3 0
      Source/Core/Elements/InputTypeText.h
  32. 228 37
      Source/Core/Elements/WidgetTextInput.cpp
  33. 30 2
      Source/Core/Elements/WidgetTextInput.h
  34. 2 2
      Source/Core/Factory.cpp
  35. 27 0
      Source/Core/StringUtilities.cpp
  36. 6 4
      Tests/Source/UnitTests/StringUtilities.cpp

+ 10 - 2
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;

+ 10 - 2
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;

+ 240 - 1
Backends/RmlUi_Platform_Win32.cpp

@@ -29,9 +29,12 @@
 #include "RmlUi_Platform_Win32.h"
 #include "RmlUi_Include_Windows.h"
 #include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/Core.h>
 #include <RmlUi/Core/Input.h>
 #include <RmlUi/Core/StringUtilities.h>
 #include <RmlUi/Core/SystemInterface.h>
+#include <RmlUi/Core/TextInputContext.h>
+#include <RmlUi/Core/TextInputHandler.h>
 #include <string.h>
 
 // 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<TCHAR[]> 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);
+	}
+}

+ 59 - 1
Backends/RmlUi_Platform_Win32.h

@@ -31,13 +31,16 @@
 
 #include "RmlUi_Include_Windows.h"
 #include <RmlUi/Core/Input.h>
+#include <RmlUi/Core/StringUtilities.h>
 #include <RmlUi/Core/SystemInterface.h>
+#include <RmlUi/Core/TextInputHandler.h>
 #include <RmlUi/Core/Types.h>
 #include <string>
 
 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 @@ private:
 	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

+ 8 - 1
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 @@ public:
 	/// 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 @@ public:
 	/// 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 @@ private:
 
 	UniquePtr<DataTypeRegister> 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;

+ 3 - 1
Include/RmlUi/Core/ContextInstancer.h

@@ -35,6 +35,7 @@
 
 namespace Rml {
 
+class TextInputHandler;
 class RenderManager;
 class Context;
 class Event;
@@ -52,8 +53,9 @@ public:
 	/// 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.

+ 15 - 3
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.

+ 6 - 0
Include/RmlUi/Core/Elements/ElementFormControlInput.h

@@ -77,6 +77,12 @@ public:
 	/// @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;

+ 5 - 0
Include/RmlUi/Core/Elements/ElementFormControlTextArea.h

@@ -102,6 +102,11 @@ public:
 	/// @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;

+ 3 - 1
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 @@ public:
 	/// 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.

+ 6 - 0
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
 
 /*

+ 90 - 0
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 <RmlUi/Core/StringUtilities.h>
+
+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

+ 66 - 0
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

+ 7 - 0
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()

+ 14 - 0
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})

+ 72 - 0
Samples/basic/ime/data/ime.rml

@@ -0,0 +1,72 @@
+<rml>
+	<head>
+		<link href="../../../assets/rml.rcss" type="text/rcss" />
+		<title>IME</title>
+		<style>
+			body {
+				width: 500dp;
+				height: 500dp;
+				max-width: 100%;
+				max-height: 100%;
+				padding: 10dp 15dp;
+				box-sizing: border-box;
+				margin: auto;
+				background-color: white;
+				color: black;
+				font-family: LatoLatin;
+				font-weight: normal;
+				font-style: normal;
+				font-size: 15dp;
+				overflow: auto;
+			}
+
+			h1 {
+				font-size: 2em;
+				margin-bottom: .5em;
+				font-weight: bold;
+			}
+
+			p {
+				margin-bottom: .6em;
+			}
+
+			label {
+				display: block;
+				margin-bottom: 2dp;
+			}
+
+			input[type="text"], textarea {
+				width: 100%;
+				border: 1px gray;
+				margin-bottom: 10dp;
+			}
+
+			scrollbarvertical {
+				width: 0;
+			}
+		</style>
+	</head>
+	<body>
+		<h1>IME Sample</h1>
+		<p>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.</p>
+		<p>IME is also used for emojis or clipboard history (on Windows).</p>
+
+		<form>
+			<label for="text">Input text</label>
+			<input id="text" name="text" type="text" maxlength=100 value="😁" />
+
+			<label for="text_area">Type multi-line text</label>
+			<textarea id="text_area" name="text_area" rows=10>English: Hello, how are you?
+Japanese: こんにちは、お元気ですか?
+Korean: 안녕하세요, 어떻게 지내세요?
+Chinese (Simplified): 你好,你好吗?
+Chinese (Traditional): 你好,你好嗎?
+Vietnamese: Xin chào, bạn có khỏe không?
+Thai: สวัสดีครับ/ค่ะ สบายดีไหม?
+Arabic: مرحبًا، كيف حالك؟</textarea>
+		</form>
+	</body>
+</rml>

+ 83 - 0
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 <RmlUi/Core/Log.h>
+#include <RmlUi_Include_Windows.h>
+#include <ShlObj.h>
+
+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<Rml::String> GetSelectedSystemFonts()
+{
+	Rml::Vector<Rml::String> 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;
+}

+ 36 - 0
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 <RmlUi/Core/Types.h>
+
+Rml::Vector<Rml::String> GetSelectedSystemFonts();
+
+#endif

+ 122 - 0
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 <RmlUi/Core.h>
+#include <RmlUi/Debugger.h>
+#include <PlatformExtensions.h>
+#include <RmlUi_Backend.h>
+#include <RmlUi_Include_Windows.h>
+#include <Shell.h>
+
+#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<FontFace> 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;
+}

+ 1 - 0
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).

+ 2 - 0
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"

+ 8 - 2
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);

+ 2 - 2
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)

+ 1 - 1
Source/Core/ContextInstancerDefault.h

@@ -45,7 +45,7 @@ public:
 	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;

+ 26 - 2
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<SystemInterface> default_system_interface;
 static UniquePtr<FileInterface> default_file_interface;
 static UniquePtr<FontEngineInterface> default_font_interface;
+static UniquePtr<TextInputHandler> default_text_input_handler;
 
 static UniquePtr<SmallUnorderedMap<RenderInterface*, UniquePtr<RenderManager>>> render_managers;
 
@@ -124,6 +128,12 @@ bool Initialise()
 #endif
 	}
 
+	if (!text_input_handler)
+	{
+		default_text_input_handler = MakeUnique<TextInputHandler>();
+		text_input_handler = default_text_input_handler.get();
+	}
+
 	EventSpecificationInterface::Initialize();
 
 	render_managers = MakeUnique<SmallUnorderedMap<RenderInterface*, UniquePtr<RenderManager>>>();
@@ -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<RenderManager>(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());

+ 6 - 0
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);

+ 5 - 0
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"));

+ 2 - 0
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

+ 3 - 0
Source/Core/Elements/InputType.h

@@ -94,6 +94,9 @@ public:
 	/// 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;
 };

+ 5 - 0
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

+ 3 - 0
Source/Core/Elements/InputTypeText.h

@@ -83,6 +83,9 @@ public:
 	/// 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;
 

+ 228 - 37
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 <algorithm>
@@ -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<WidgetTextInputContext>(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<Segment> segments;
 
+	struct IMESegment {
+		Vector2f position;
+		int width;
+		int line_index;
+	};
+
+	Vector<IMESegment> 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())

+ 30 - 2
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 @@ public:
 	/// @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 @@ public:
 	/// @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 @@ protected:
 	/// 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 @@ private:
 	/// 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 @@ private:
 	/// @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 @@ private:
 	// 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<WidgetTextInputContext> text_input_context;
+
 	// Cursor visibility and timings.
 	float cursor_timer;
 	bool cursor_visible;

+ 2 - 2
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;

+ 27 - 0
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 = "";

+ 6 - 4
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);