Input Method Editors
Relay OS input method compositions to a View and position candidate windows.
On this page
You can connect an OS input method editor (IME) to support typing in languages such as Chinese, Japanese, and Korean. The OS builds text in steps by displaying underlined in-progress text in the field, opening a candidate window to choose words, and committing the final text. You relay each step to the View's editor, which also reports where to place the candidate window.
📘 Input Methods in AppCore
AppCore windows run the OS input method automatically for their panels on Windows, macOS, and Linux, candidate window included. You only need these steps when you host a View directly or draw an input method yourself.
Turning On the Input Method
Turn on the OS input method only while both view_has_focus and is_mutable are true in the editable state— turn it off otherwise. To track the editable state, see Keyboard Focus and Editable State.
For password fields (EditableType::Password), you usually leave the input method turned off.
Displaying In-Progress Text
Call Editor::SetComposition() whenever the OS reports updated composition text:
#include <Ultralight/Ultralight.h>
#include <vector>
using namespace ultralight;
RefPtr<View> view;
///
/// Called by our OS layer each time the in-progress text changes. Each entry of
/// 'clause_ends' is where a clause ends (in UTF-16 code units), 'active' is the
/// clause being converted, and 'caret' is the caret's offset within the text.
///
void OnCompositionUpdate(const String& text, const uint32_t* clause_ends,
size_t num_clauses, size_t active, uint32_t caret) {
std::vector<CompositionSegment> segments(num_clauses);
uint32_t start = 0;
for (size_t i = 0; i < num_clauses; ++i) {
segments[i].range.start = start;
segments[i].range.end = clause_ends[i];
segments[i].thick = (i == active);
start = clause_ends[i];
}
CompositionRange selection;
selection.start = caret;
selection.end = caret;
view->editor()->SetComposition(text, segments.data(), segments.size(),
selection);
}
The initial call starts the composition and replaces any selected text on the page— later calls replace the previous in-progress text.
Styling Clauses
Each CompositionSegment styles one clause of the composition using UTF-16 character offsets. Setting thick to true marks the clause actively being converted. Leaving colors unset draws an underline in the text's own color, while passing nullptr for the segment array underlines the entire text with a single thin line.
Positioning the Caret
The selection argument sets the caret position or selection range inside the composition. Setting both bounds to the same offset places an insertion caret, while converting a specific clause highlights that clause's range.
Ending the Composition
When the input method delivers final text or cancels, notify the editor to complete or remove the composition:
///
/// The input method delivered its final text.
///
void OnCompositionResult(const String& result) {
view->editor()->CommitComposition(result);
}
///
/// The input method ended the composition without a result.
///
void OnCompositionEnd() {
view->editor()->CancelComposition();
}
The editor provides three methods to end a composition:
| Method | When to Call |
|---|---|
Editor::CommitComposition() |
The input method delivered final text. If no composition is active, this inserts the text at the caret. |
Editor::FinishComposition() |
Keeps the in-progress text as final text (eg, when focus moves away mid-composition). |
Editor::CancelComposition() |
Cancels the active composition and removes the in-progress text. |
Page Events
During a composition, the page receives standard DOM compositionstart, compositionupdate, and compositionend events. Every keydown event (not keyup) reports key code 229 (KeyCodes::GK_PROCESSKEY) with key value "Process", matching standard browser behavior.
Positioning the Candidate Window
Implement EditorListener::OnUpdateComposition() to reposition the OS candidate window whenever the composition changes:
class MyApp : public EditorListener {
public:
void OnUpdateComposition(View* caller) override {
Editor* editor = caller->editor();
///
/// Anchor on the composition's first character while one is active, else on
/// the caret.
///
Rect anchor = editor->HasComposition()
? editor->GetCompositionCharacterBounds(0)
: editor->GetCaretBounds();
///
/// The rect is in viewport CSS pixels-- scale it to device pixels, then add
/// the View's own position in your window.
///
double scale = caller->device_scale();
// Pseudo-code, move the OS candidate window below this point.
MoveCandidateWindow(anchor.left * scale, anchor.bottom * scale);
}
};
Anchor the candidate window to the first character of the composition rather than the caret. Because candidates vary in length, following the caret causes the window to jump around as the user cycles through choices.
To keep the candidate window from covering the text, call Editor::GetCompositionBounds() to retrieve the bounding rectangle for the entire composition.
Handling Discarded Compositions
Implement EditorListener::OnDiscardComposition() to cancel the OS input method when the page terminates an active composition on its own:
class MyApp : public EditorListener {
public:
///
/// The page dropped the composition on its own. Its text is already
/// committed, so only the OS side needs cancelling.
///
void OnDiscardComposition(View* caller) override {
// Pseudo-code, cancel the OS input method's composition (on Windows,
// ImmNotifyIME with NI_COMPOSITIONSTR and CPS_CANCEL).
CancelOSComposition();
}
};
The library calls OnDiscardComposition() when the page navigates or when the selection moves to another element (eg, when a click or script focuses a different element). Moving the caret or selection inside the composing field doesn't end the composition.
🚧 Do Not Recommit Text
The page keeps the in-progress text as final text when discarding a composition. Cancel the OS input method without committing or inserting the text again.
Answering OS Text Queries
Some OS input systems read and replace text surrounding the caret, such as the macOS accent picker:
///
/// Called when the OS replaces the character before the caret (eg, with an
/// accented version of it).
///
void ReplaceCharacterBeforeCaret(const String& text) {
uint32_t start, end;
if (view->editor()->GetSelectionOffsets(start, end) && start > 0)
view->editor()->ReplaceTextInRange(start - 1, start, text);
}
Text offset methods (Editor::GetSelectionOffsets(), Editor::GetTextInRange(), Editor::ReplaceTextInRange(), and Editor::GetCompositionOffsets()) share a single coordinate space measured in UTF-16 code units from the start of the focused editable element. These offsets match the DOM selectionStart and selectionEnd properties.
Calling Editor::ReplaceTextInRange() modifies text just like typing— the change creates a single undo step and places the caret immediately after the replacement text.