Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text Input Method Editor #541

Merged
merged 42 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e6c3b54
Add visual feedback for IME composition in text inputs
ShawnCZek Nov 17, 2023
8369f7c
Reset the IME composition range when the input value changes.
ShawnCZek Nov 18, 2023
b9a3220
Comment unused parameters.
ShawnCZek Nov 18, 2023
16614a6
Implement the WidgetTextInput::GetLineIMEComposition() method.
ShawnCZek Nov 18, 2023
25a3c0b
Fix implicit type conversion.
ShawnCZek Nov 18, 2023
b004492
Create a sample of an advanced use of IME.
ShawnCZek Feb 13, 2024
567ad45
Implement UTF-16 to UTF-8 conversion instead of using the Windows bac…
ShawnCZek Feb 13, 2024
ad1f30f
Move ConvertCharacterOffsetToByteOffset and ConvertByteOffsetToCharac…
ShawnCZek Mar 9, 2024
30be693
Rename UpdateCompositionRange to SetCompositionRange.
ShawnCZek Mar 9, 2024
d31daf8
Move the IME implementation from a sample to the library.
ShawnCZek Mar 16, 2024
a043fb9
Use GNU Unifont for the IME sample.
ShawnCZek Mar 16, 2024
2632627
Deactivate only the same text input method context.
ShawnCZek Mar 16, 2024
92d8f70
Properly export new classes and function.
ShawnCZek Mar 16, 2024
ca458c2
Add back TextInputMethodContext::GetScreenBounds().
ShawnCZek Mar 16, 2024
9c90052
Respect the text input length restriction with IME composition.
ShawnCZek Mar 16, 2024
8d57c25
Load Win32 system fonts in IME sample
mikke89 Apr 15, 2024
d31d8bd
Fix text disappearing from textarea.
ShawnCZek Apr 24, 2024
3f801d7
Add the IME sample only if a Win32 backend is used.
ShawnCZek Apr 25, 2024
2583895
Fix a conditional for locating system font files.
ShawnCZek May 14, 2024
a5c9995
Explain the IME sample.
ShawnCZek May 15, 2024
350e3f6
Use window_handle instead of GetActiveWindow().
ShawnCZek May 15, 2024
b8bd402
Remove coupling of IME contetx via friend keyword.
ShawnCZek May 15, 2024
e49dc87
Ensure that valid pointers are accessed during the lifetime of Widget…
ShawnCZek May 15, 2024
c1219a6
Remove inconsistent IME prefix from method names.
ShawnCZek May 15, 2024
853a777
Construct IME context only when in use.
ShawnCZek May 15, 2024
6699d57
Make TextInputMethodEditor::EndComposition() an implementation detail.
ShawnCZek May 15, 2024
a269d23
Make the text input handler multipurpose.
ShawnCZek Jun 9, 2024
f549e05
Rename SetIMERange to SetCompositionRange.
ShawnCZek Jun 9, 2024
045f781
Merge branch 'master' into ime_visual_feedback
ShawnCZek Jun 9, 2024
eb955c7
Comment out unused parameter variables.
ShawnCZek Jun 9, 2024
d8094b3
Revert "Construct IME context only when in use."
ShawnCZek Jun 27, 2024
c8df9a1
Rename instances of TextInputContext to input_context.
ShawnCZek Jun 27, 2024
25588b2
Fix header files of TextInputContext and TextInputHandler.
ShawnCZek Jun 27, 2024
d944506
Document TextInputContext and TextInputHandler classes.
ShawnCZek Jun 28, 2024
9abf64c
Move the ownership of the IME editor to backends.
ShawnCZek Jun 28, 2024
869f0d0
Complete the composition when the user tries to interact with other e…
ShawnCZek Jun 28, 2024
e11e96e
Rename OnBlur/OnFocus to OnActivate/OnDeactivate.
ShawnCZek Jun 28, 2024
f3bb2bc
Rename text_input_method_context to text_input_context.
ShawnCZek Jun 28, 2024
fa4d3bc
Fix the name of the include guard for TextInputContext and TextInputH…
ShawnCZek Jun 30, 2024
3a16064
Simplify the ownership of text input contexts.
ShawnCZek Jun 30, 2024
874b3f8
Add a missing class definition.
ShawnCZek Jun 30, 2024
3d9ec36
Store handler in the text widget input context
mikke89 Jun 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Backends/RmlUi_Backend_Win32_GL2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -158,13 +159,20 @@ 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;
}

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);
Expand Down Expand Up @@ -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))
Expand All @@ -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;
Expand Down
12 changes: 10 additions & 2 deletions Backends/RmlUi_Backend_Win32_VK.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -160,13 +161,20 @@ 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;
}

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);
Expand Down Expand Up @@ -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))
Expand All @@ -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;
Expand Down
241 changes: 240 additions & 1 deletion Backends/RmlUi_Platform_Win32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -57,6 +60,8 @@ SystemInterface_Win32::SystemInterface_Win32()
cursor_unavailable = LoadCursor(nullptr, IDC_NO);
}

SystemInterface_Win32::~SystemInterface_Win32() = default;
mikke89 marked this conversation as resolved.
Show resolved Hide resolved

void SystemInterface_Win32::SetWindow(HWND in_window_handle)
{
window_handle = in_window_handle;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}
}
Loading