Selkie--Widget--TextInput
NAME
Selkie::Widget::TextInput - Single-line text input with cursor and editing
SYNOPSIS
use Selkie::Widget::TextInput;
use Selkie::Sizing;
my $input = Selkie::Widget::TextInput.new(
sizing => Sizing.fixed(1),
placeholder => 'Search...',
);
$input.on-submit.tap: -> $text { run-search($text) };
$input.on-change.tap: -> $text { update-preview($text) };
# Password field: mask characters
my $pw = Selkie::Widget::TextInput.new(
sizing => Sizing.fixed(1),
mask-char => 'ā¢',
);DESCRIPTION
A one-line text input. Arrow keys, Home, End, Backspace, and Delete behave as you'd expect. Characters wider than the visible width horizontally scroll the view to follow the cursor.
Four Supplies:
on-submitā fires once when the user presses Enter, carrying the current texton-changeā fires on every keystroke that modifies the bufferon-copyā fires on Ctrl+C, carrying the currently-selected texton-cutā fires on Ctrl+X, carrying the cut text (which is also deleted from the buffer)
For programmatic updates that shouldn't re-dispatch (e.g. syncing from a store subscription), use set-text-silent ā it updates the buffer without emitting on on-change.
Mouse and selection
Click positions the caret. Drag selects from the press point to the current cursor cell ā the selection range is rendered with reverse-video. Double-click selects the word under the cursor; triple-click selects the entire buffer. has-selection, selection-range, and selected-text expose the current selection state.
Keyboard cooperates: Shift+Left / Shift+Right jump by word AND extend the selection (the legacy word-jump is now also a selection-extend); plain arrows clear any selection before moving. Ctrl+A selects all. Ctrl+C and Ctrl+X emit on the corresponding supplies ā Selkie does not own the system clipboard, so apps wire OSC 52 / notcurses paste-buffer in their handlers. Backspace and Delete delete an active selection if present; typing replaces it.
Decorations: highlight spans and ghost suggestions
Two optional pull-based hooks let an app decorate the input without ever desyncing from the buffer:
highlight-providerāCallable(Str $buffer)returning a list of Selkie::Widget::TextInput::HighlightSpan (half-open char ranges + styles). Each range is repainted over the base text during render, clipped to the visible window. Span styles merge onto the input's own style, so fg-only spans keep the field background.suggest-providerāCallable(Str $buffer, UInt $cursor --Str)> returning a completion tail to paint after end-of-buffer in theghost-style(falling back to the theme'sinput-ghostslot), fish-shell style. Only consulted while focused with the caret at the end of the buffer. The ghost is display-only; pressing Right arrow at end-of-buffer accepts it viainsert-text, emittingon-changeexactly once. Right anywhere else keeps its normal cursor-move behavior.
Both hooks are called during render ā they must be pure and cheap (memoise on the buffer string if not). Exceptions they throw are not caught. Because the widget pulls rather than being pushed, the decorations survive set-text-silent store-syncs, and external state changes (say, a new list of valid tokens) take effect by simply marking the input dirty ā e.g. by subscribing the input widget to the relevant store path. Both hooks are ignored while mask-char is set: masked inputs expose no content semantics.
use Selkie::Widget::TextInput::HighlightSpan;
$input.highlight-provider = -> Str $buf {
known-tokens($buf).map: -> $t {
Selkie::Widget::TextInput::HighlightSpan.new(
start => $t<from>, end => $t<to>,
style => Selkie::Style.new(fg => $t<ok> ?? 0x80E060 !! 0xE06060),
)
}
};
$input.suggest-provider = -> Str $buf, UInt $ {
best-completion-tail($buf) // ''
};Modifier bubbling
Modified keys (Ctrl, Alt, Super) bubble past the input so global keybinds still work ā except for Ctrl+A / C / X (selection-related, handled internally) and except when the OS keyboard layout has already composed the modifier into a different printable character (e.g. UK Mac Alt-3 ā #, US Mac Alt-2 ā ā¢). In that case the composed character is treated as typed input, since blocking it would make those characters untypeable on layouts that need a modifier to produce them. Bare characters are consumed for typing.
EXAMPLES
Store-synced input
$app.store.subscribe-with-callback(
'sync-name',
-> $s { ($s.get-in('form', 'name') // '').Str },
-> $v { $name-input.set-text-silent($v) if $name-input.text ne $v },
$name-input,
);
$name-input.on-change.tap: -> $v {
$app.store.dispatch('form/set', field => 'name', value => $v);
};SEE ALSO
Selkie::Widget::MultiLineInput ā multi-line variant with word wrap
Selkie::Widget::Button ā for commit-only actions
sub next-word-pos
sub next-word-pos(
Str:D $s,
Int:D $pos
) returns IntFind the position of the start of the next word at or after $pos in $s. Word = run of \w chars. Skips through the current char's class (word or non-word), then through any trailing non-word chars, landing at the first word char of the next word ā or $s.chars if there is no next word. Used by shift-right word-jump and by MultiLineInput's 2D variant.
sub prev-word-pos
sub prev-word-pos(
Str:D $s,
Int:D $pos
) returns IntFind the position of the start of the previous word at or before $pos in $s. Skips backwards through any non-word chars, then backwards through word chars, landing on the index of the first char of that word ā or 0 if we walked off the start. Used by shift-left and shift-backspace.
has Int $!sel-anchor
Selection anchor offset. -1 means "no selection" ā the cursor is a bare caret. When >= 0, the selection covers the half-open range from min(anchor, cursor) to max(anchor, cursor). The cursor is the movable end; the anchor stays put while extending.
has Callable &.highlight-provider
Optional pull-based highlight hook: a Callable(Str $buffer) returning a list of Selkie::Widget::TextInput::HighlightSpan. Consulted on every render, so it must be pure and cheap ā memoise on the buffer string if the computation is nontrivial. Because decorations are pulled at paint time they can never go stale against the buffer, including after set-text-silent (which deliberately skips on-change). Ignored while mask-char is set ā masked inputs expose no content semantics.
has Callable &.suggest-provider
Optional pull-based ghost-suggestion hook: a Callable(Str $buffer, UInt $cursor -- Str)> returning the tail to paint after the end of the buffer (fish-shell style), or an undefined/empty string for none. Only consulted while the input is focused with the caret at end-of-buffer, and never while mask-char is set. The ghost is display-only ā it lives outside the buffer and never affects editing or on-change ā until the user accepts it with Right arrow at end-of-buffer, which inserts the tail via insert-text (one on-change emission).
has Selkie::Style $.ghost-style
Style for the ghost-suggestion tail. Merged onto the input's base style, so a foreground-only style keeps the field background. Falls back to the theme's input-ghost slot when unset.
method text
method text() returns StrThe current buffer contents.
method has-selection
method has-selection() returns BoolTrue iff there is an active selection (anchor differs from cursor). A bare caret returns False.
method selection-range
method selection-range() returns RangeHalf-open offset range of the current selection, normalised to low..^high. Returns 0..^0 when there's no selection.
method selected-text
method selected-text() returns StrThe substring currently selected, or the empty string when there is no selection.
method clear-selection
method clear-selection() returns MuClear any active selection without moving the caret.
method on-copy
method on-copy() returns SupplySupply emitting the currently-selected text on Ctrl+C. The Selkie framework does not own the system clipboard ā apps wire this up themselves via OSC 52 or notcurses paste-buffer. The supply only fires when there's an active selection.
method on-cut
method on-cut() returns SupplySupply emitting on Ctrl+X. Like on-copy but the selection is also deleted from the buffer.
method set-text
method set-text(
Str:D $t
) returns MuReplace the buffer's contents and place the caret at the end. Emits on on-change. Use this for user-driven updates (e.g. a "load from history" button); for programmatic syncs from a store path use set-text-silent instead to avoid feedback loops.
method set-text-silent
method set-text-silent(
Str:D $t
) returns MuSilent variant of set-text ā updates the buffer without emitting on on-change. Wire this into store subscriptions that mirror external state into the input, so the input update doesn't dispatch an event that loops back through the store and re-fires the subscription.
method clear
method clear() returns MuEmpty the buffer. Equivalent to set-text('') ā emits on on-change.
method on-submit
method on-submit() returns SupplySupply emitting the current buffer contents when the user presses Enter.
method on-change
method on-change() returns SupplySupply emitting the new buffer contents on every user-driven edit (typing, paste, delete, cut, set-text). Does not fire for set-text-silent ā the silent variant is intended exactly to break the change-supplier ā store feedback loop.
method set-focused
method set-focused(
Bool $f
) returns MuSet the input's focus state. Called by Selkie::App's focus dispatcher. The caret is only painted while focused.
method insert-text
method insert-text(
Str:D $text
) returns NilInsert $text at the current cursor position in one operation. Equivalent to typing each character in turn, but does ONE buffer concat instead of one per char ā drops paste cost from O(n²) to O(n). Newlines and other control chars in $text are stripped (single-line input). Used by the App's paste-batching drain loop; application code can call it directly to programmatically insert text. If a selection is active, it is replaced (deleted then the new text is inserted at the deletion point) ā matches the canonical "type to overwrite selection" behavior of every text editor.