Text Editing
Input supports full caret/selection editing, multiline (textarea) mode,
password masking, and a clipboard seam — all driven through a keycode-agnostic
EditCommand model, so core never has to know whether you’re running under
SDL, GLFW, or anything else.
Multiline
Input()
.value(body)
.multiline()
.onChange([&](const std::string& v) { setBody(v); })
.backgroundColor(0x1E1E1EFF);
.multiline() turns Input into a textarea: the value soft-wraps at the
content width, Enter inserts \n (a single-line Input’s Enter fires
onSubmit instead), and the box grows to fit its wrapped lines via a Yoga
measure function — the same measure/paint agreement the rest of layout relies
on (see Architecture — rendering).
.password(true) wins over .multiline(true) — a node with both renders
as a single-line password field, since masked textareas (stars with line
structure) aren’t a coherent thing to support. Don’t rely on the two
composing.
Fonts
Text("Heading").font("mono");
Input().value(code).font("mono").multiline();
Both Text and Input take an optional .font(name), resolved against the
renderer’s registered font faces by name — not a raw handle — so it stays
backend-agnostic and survives a GL context rebuild. An empty/unset font uses
the default. This threads all the way through every measure/draw call in the
render seam (ITextMeasurer::measureRun/fontMetrics,
IRenderBackend::drawTextRun) so a run always draws in the face it was
measured in.
Caret, selection, and EditCommand
Editing operations are represented as yui::EditCommand, not raw keycodes:
enum class EditCommand {
MoveLeft, MoveRight, MoveLineStart, MoveLineEnd, MoveUp, MoveDown,
SelectAll,
DeleteBackward, DeleteForward,
Cut, Copy, Paste,
InsertNewline,
};
A platform shim maps raw keycodes to EditCommands; core owns what each
command means. InsertNewline is the Enter mapping for both modes — core
decides the behavior: a multiline input inserts '\n', a single-line input
fires onSubmit. A command that doesn’t apply to the focused input’s current
mode (e.g. MoveUp/MoveDown sent to a single-line input) is reported as
not consumed, so a shim can route the key elsewhere instead of it being
silently swallowed.
Route input through the 5-argument Host::handleKeyDown overload, which owns
the full priority order:
bool handleKeyDown(int keyCode, uint16_t mods, bool repeat,
std::optional<EditCommand> edit, bool focusNav = false);
Priority: (1) the focused Input’s edit command (if edit is set and
applicable), (2) focus navigation (focusNav — your shim’s Tab
detection), (3) your app’s own onKeyDown handlers. An edit command the
focused input didn’t consume falls through to focus nav, which falls through
to app dispatch. See Focus for the focusNav half of
this contract.
// platform shim (sketch): map SDL keycodes to EditCommand, then route through Host
std::optional<EditCommand> mapKey(SDL_Keycode k, uint16_t mods) {
switch (k) {
case SDLK_LEFT: return EditCommand::MoveLeft;
case SDLK_RIGHT: return EditCommand::MoveRight;
case SDLK_BACKSPACE: return EditCommand::DeleteBackward;
case SDLK_DELETE: return EditCommand::DeleteForward;
case SDLK_RETURN: return EditCommand::InsertNewline;
case SDLK_a: if (mods & KeyMod_Ctrl) return EditCommand::SelectAll; break;
case SDLK_x: if (mods & KeyMod_Ctrl) return EditCommand::Cut; break;
case SDLK_c: if (mods & KeyMod_Ctrl) return EditCommand::Copy; break;
case SDLK_v: if (mods & KeyMod_Ctrl) return EditCommand::Paste; break;
default: return std::nullopt;
}
return std::nullopt;
}
host.handleKeyDown(keyCode, mods, repeat, mapKey(keyCode, mods),
/*focusNav=*/ keyCode == SDLK_TAB);
For direct control without going through the routing overload, call
Host::handleEditCommand(EditCommand cmd, bool extend = false) yourself —
extend is the Shift-held selection modifier: a Move* command with
extend=true moves only the caret, leaving the selection anchor in place to
span the new selection.
Password masking is per code point, not per byte — a masked multi-byte
UTF-8 character still renders as exactly one *.
Clipboard
Cut/Copy/Paste route through a platform clipboard seam:
struct IClipboard {
virtual std::string getText() = 0;
virtual void setText(const std::string& text) = 0;
};
host.setClipboard(myClipboard); // install once; nullptr = uninstalled
With no clipboard installed, Cut/Copy/Paste report as unconsumed rather
than silently discarding data. Lifetime is self-managed in both directions —
either the Host or the IClipboard may be destroyed first, and each side
detaches cleanly (the same pattern ITextMeasurer uses for the text-measurer
link). A bundled yui::sdl::SdlClipboard is available for SDL apps
(<yui/sdl/SdlClipboard.hpp>); apps embedded inside a host application need
to supply their own IClipboard, since there’s no OS clipboard access to
piggyback on in that setting.