Selkie--Widget--MultiLineInput
NAME
Selkie::Widget::MultiLineInput - Multi-line text input with word-wrap and 2D cursor
SYNOPSIS
use Selkie::Widget::MultiLineInput;
use Selkie::Sizing;
my $area = Selkie::Widget::MultiLineInput.new(
sizing => Sizing.fixed(1), # grows up to max-lines as user types
max-lines => 6,
placeholder => 'Type a message... (Ctrl+Enter to send)',
);
$area.on-submit.tap: -> $text { send-message($text); $area.clear };
$area.on-change.tap: -> $text { save-draft($text) };DESCRIPTION
A multi-line text area with word-wrapping, a 2D cursor, and dynamic height that grows as the user types (up to max-lines). Plain Enter inserts a newline; Ctrl+Enter submits.
The height auto-adjusts via desired-height: if you pass sizing = Sizing.fixed(1)>, the parent layout sees the widget's desired height grow as content is added, bounded by max-lines.
set-text-silent updates the buffer without emitting on-change ā use this from store subscriptions to avoid feedback loops. Both set-text and set-text-silent normalise CRLF and lone CR to LF before splitting, so text loaded from Windows-authored files (e.g. CCv3 cards) does not strand a \r inside each line.
Mouse and selection
Click positions the caret. Drag selects across rows; the selection range is rendered with reverse-video and respects word-wrap (the highlight follows the wrapped layout, not raw offsets). Double-click selects the word under the cursor; triple-click selects the entire current logical line. Scroll-wheel moves the cursor up/down. Ctrl+A selects everything; Ctrl+C / Ctrl+X emit on on-copy / on-cut and (for cut) delete the selection. Backspace and Delete consume an active selection if present; typing replaces it.
EXAMPLES
Chat compose area
my $compose = Selkie::Widget::MultiLineInput.new(
sizing => Sizing.fixed(1),
max-lines => 5,
placeholder => 'Type a message ā Ctrl+Enter to send',
);
$compose.on-submit.tap: -> $text {
if $text.chars > 0 {
$app.store.dispatch('chat/send', :$text);
$compose.clear;
}
};SEE ALSO
Selkie::Widget::TextInput ā single-line variant
Selkie::Widget::TextStream ā append-only log (no editing)
has Int $!sel-anchor-row
Selection anchor in (logical-row, logical-col). -1 in $!sel-anchor-row means "no selection" ā cursor is a bare caret. When >= 0 the selection covers the half-open range from min(anchor, cursor) to max(anchor, cursor), walked across logical lines.
method has-selection
method has-selection() returns BoolTrue iff a selection is active (anchor differs from cursor). Bare caret returns False.
method selection-range
method selection-range() returns ListReturns the normalised selection range as a List of two pairs: (:row, :col) for the start and (:row, :col) for the end (half-open at end). Returns () when no selection.
method selected-text
method selected-text() returns StrThe text currently selected, walking line by line. \n joins successive logical lines. Empty string when no selection.
method clear-selection
method clear-selection() returns MuClear the active selection without moving the caret.
method on-copy
method on-copy() returns SupplySupply emitting the currently-selected text on Ctrl+C. Selkie does not own the system clipboard ā apps wire this up themselves via OSC 52 or notcurses paste-buffer. Fires only 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 text
method text() returns StrThe full buffer contents joined with \n. (The buffer is stored as an array of logical lines; this assembly is O(N) in total character count ā cache the result if calling per frame.)
method set-text
method set-text(
Str:D $t
) returns MuReplace the buffer contents and place the caret at the end. Emits on on-change. Use this for user-driven updates; 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('').
method on-submit
method on-submit() returns SupplySupply that emits the current buffer when the user presses Ctrl+Enter (Enter inserts a newline). Apps that want plain Enter as submit register their own keybind and call text directly.
method on-change
method on-change() returns SupplySupply that emits the new buffer contents on every user-driven edit (typing, paste, delete, cut, set-text). Does not fire for set-text-silent.
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 is-focused
method is-focused() returns BoolWhether the widget currently has focus.
method desired-height
method desired-height() returns UIntThe natural visual height for the buffer in cells, accounting for soft-wrap at the current width. Clamped to max-lines. Used by autosize containers (e.g. a chat compose area) to grow the input with its content.
method line-count
method line-count() returns UIntNumber of logical lines in the buffer (counts hard newlines, not soft-wraps). Always at least 1 ā an empty buffer counts as one empty line.
method cursor-row
method cursor-row() returns UIntCaret row in logical-line coordinates (0-based; counts hard newlines, not soft-wraps).
method cursor-col
method cursor-col() returns UIntCaret column on the current logical line (0-based; counts characters, not visual cells).
method visual-rows
method visual-rows() returns ArraySame shape as !visual-lines, but each entry is a hash with logical-row, logical-col-start, text. Used by the selection overlay to map visual rows back to logical (row, col) spans for highlighting.
method insert-text
method insert-text(
Str:D $text
) returns NilInsert $text at the current cursor position in one operation, splitting on \n so multi-line pasted content lays across multiple buffer lines. Equivalent to typing each character in turn but with one buffer rebuild instead of one per char ā O(n) total instead of O(n²). Used by the App's paste-batching drain loop.
method do-word-backspace
method do-word-backspace() returns MuShift-Backspace: delete from the cursor back to the previous word boundary. At column 0, falls through to the regular backspace semantics so the line above is joined ā matches what users expect from "delete previous word" in editors that also support multi-line.