Text input and IME composition
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Checked by reading TextInputEXT, the bridge, CNA::Input::Clipboard and the named tests at 009d40f5; the chat-box fragment was syntax-checked with g++ -fsyntax-only against TARGET headers and sharp-runtime next at 41b918c9 (not pinned by TARGET). Nothing was executed; real IME composition remains unverified.
Typed text is a different input from key state: layouts, dead keys, input methods and on-screen keyboards all sit between a key press and the character a player meant. This page gives the exact contract of TextInputEXT in CNA: its three event channels, the text mode and the window it needs, the input-type hint, composition placement, and the clipboard classes that belong next to a text field. It is for anyone writing a chat box, a console or a name-entry screen. The guide section is Input System: TextInputEXT; UTF-8 decoding and the per-backend event mapping are traced on Input internals: text input.
Origin and the three channels
TextInputEXT is not a CNA invention: XNA 4.0 had no portable text event, FNA added this static class as an extension, and CNA ports it with the same names, marking it with the CNAEXT macro (TextInputEXT.hpp). It has three multicast events, each dispatched on the game-loop thread during Game::PollEvents:
| Event | Signature | Meaning |
|---|---|---|
TextInput | System::MulticastAction<charcs> | Committed text, one UTF-16 code unit per call. A code point above U+FFFF (an emoji, for example) arrives as two calls, high surrogate then low surrogate, exactly as FNA's C# char events do. It supports key repeat and is not raised by non-character keys, with the exception of the synthesized control characters below. |
TextEditing | System::MulticastAction<const std::string&, int, int> | The uncommitted IME composition: the draft text as UTF-8, the cursor start and the selection length, both zero for empty text. The two integers are passed straight through from the platform's IME model, so index the string carefully: a byte offset is not a character count in multi-byte text. |
TextEditingCandidatesEXT | System::MulticastAction<const std::vector<std::string>&, int, bool> | CNA-only: the IME candidate list (UTF-8 strings), the pre-selected index or -1, and whether the list is horizontal, so a game can draw the CJK candidate popup itself. |
Subscribe with +=, replace every subscriber with =, or clear with = nullptr. When a subscription must be removed on its own, the sibling sharp-runtime's MulticastAction::Add returns a token for Remove; the example below uses that (checked against sharp-runtime next at 41b918c9, a revision TARGET does not pin).
Besides decoded text, the bridge synthesizes a few control characters from key presses so that text fields can edit: Home, End, Backspace, Tab, Enter and Delete arrive as 2, 3, 8, 9, 13 and 127, and Ctrl+V as 22 with the literal text of the paste suppressed until the key is released. That happens whether or not text input is active; the full rule and the UTF-8 decoding (invalid sequences become U+FFFD) are on Input internals: keyboard.
Text input is a mode tied to a window
StartTextInput() and StopTextInput() open and close the text-entry period; IsTextInputActive() reports it, and on a platform with an on-screen keyboard IsScreenKeyboardShown() reports the keyboard, which can close on its own while the mode stays active. Every call acts on the one window that GraphicsDevice published as the active input surface: the device publishes a native handle and its platform WindowId as a pair when it creates or adopts a window, and clears them when it destroys it (TextInputEXT.cpp). The consequences:
- With no published window, or on a platform without a text-input service (SDL2, Terminal, Headless), every call is a silent no-op and both queries report
false. - A native start, stop or placement failure is swallowed, preserving FNA's
voidcontract; a caller cannot distinguish "started" from "failed to start" except throughIsTextInputActive(). IsScreenKeyboardShown(window)answers only for the handle that was published; any other handle readsfalse, because the portable platform contract never interprets a raw native handle.- Start the mode before expecting events. On Win32 the mapper consumes every
WM_CHARwithout raisingTextInputuntil the window's text mode is on, and its source comment states that a game which never callsStartTextInput()sees the same on the other backends.
The input-type hint
The CNA-only StartTextInputWithTypeEXT(CNA::Input::TextInputTypeEXT) starts the same mode with a hint for the on-screen keyboard or IME: Text, TextName, TextEmail, TextUsername, TextPasswordHidden, TextPasswordVisible, Number, NumberPasswordHidden or NumberPasswordVisible, mirroring SDL3's text-input types. Plain StartTextInput() passes the platform's default type. Whether a hint changes anything depends on the platform; desktop keyboards generally ignore it.
Placing the composition window
SetInputRectangle(rectangle) tells the platform where text is being entered so the IME can place its composition and candidate windows nearby. The rectangle is passed through unchanged, relative to the window's client area, with a cursor offset of 0: FNA passes 0 too and marks the missing cursor information as an upstream FIXME, and CNA follows that rather than inventing an offset. No renderer transform is applied, so under a virtual resolution a game that knows its text box in logical coordinates should convert it to window coordinates itself.
Worked example: a chat box
A chat box consumes composed text instead of polling key state, subscribes when it opens and unsubscribes when it closes:
System::MulticastAction<charcs>::Token textInputToken =
System::MulticastAction<charcs>::InvalidToken;
void ChatBox::Open()
{
TextInputEXT::SetInputRectangle(chatBoxClientBounds); // window client coordinates
TextInputEXT::StartTextInput();
if (textInputToken == System::MulticastAction<charcs>::InvalidToken)
{
textInputToken = TextInputEXT::TextInput.Add(
[this](charcs c) {
if (c == u'\b') { if (!composedText.empty()) composedText.pop_back(); }
else if (c >= u' ') { composedText += c; } // skip other control characters
});
}
}
void ChatBox::Close()
{
TextInputEXT::StopTextInput();
TextInputEXT::TextInput.Remove(textInputToken);
textInputToken = System::MulticastAction<charcs>::InvalidToken;
composedText.clear();
}
Here composedText is a std::u16string. The fragment was syntax-checked against the TARGET headers and the sibling sharp-runtime checkout named above (not executed). A production text box would also subscribe to TextEditing to show the draft composition, and handle the synthesized Enter (13) to submit.
Clipboard next to a text field
Copy and paste in a default build go through CNA::Input::Clipboard, which is compiled unconditionally (Clipboard.cpp):
GetTextEXT(),SetTextEXT(text)andHasTextEXT()work on UTF-8 text. With no clipboard service the getters read as empty and the setter does nothing;SetTextEXTreturnsvoid, so a failed write is swallowed. The bool-returningCNA::Devices::Clipboardof the optional host-device layer is the surface that reports failure.GetPrimarySelectionTextEXT(),SetPrimarySelectionTextEXT()andHasPrimarySelectionTextEXT()address the X11 and Wayland primary selection (the middle-click buffer); elsewhere they read as empty and ignore writes.GetMimeTypesEXT(),HasDataEXT,GetDataEXTand twoSetDataEXToverloads carry formats other than text, including several formats at once (HTML together with its plain text, say). A clipboard that carries only text refuses these writes and reads them as empty.
Which platforms have a clipboard service is tabulated on Device extensions: which platforms provide which service.
Evidence
The text path is covered by the bridge's text and candidate suites and TextInputEXTTests.cpp, which drive injected events and a canned text-input service; CNA's own manual verification log records a real-window start, stop and IsTextInputActive round trip and UTF-8 decoding of Czech and astral text on an earlier revision, and explicitly not a real IME composition or physical typing. Real IME behaviour on any platform therefore remains unverified, and nothing on this page was executed.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Input System guide: TextInputEXT
- Deep dives
- The input model · Host devices