Documentation
¶
Overview ¶
Package textinput is the contract between a text widget and an input method (an IME, a screen reader, a mobile keyboard), with no dependency beyond the standard library.
It holds three things:
- Client, the interface a text widget implements. It has the shape of AppKit's NSTextInputClient (insertText:replacementRange:, setMarkedText:selectedRange:replacementRange:, unmarkText, selectedRange, markedRange, firstRectForCharacterRange:actualRange:, characterIndexForPoint:), uses builtin types only, and counts offsets in UTF-16 code units over the whole document. NSRange, the DOM's selectionStart and Windows TSF all count that way.
- The pure UTF-16 helpers Len, ToByte, FromByte, LineCol, Offset, ToRune and FromRune, which convert between that unit, Go's UTF-8 byte offsets, rune offsets (AT-SPI counts characters) and (line, rune column) pairs.
- Reconcile, the diff fallback: given the text and selection last written to a host's editing element and the ones read back from it, it deduces the single Edit that explains the change. VS Code (TextAreaState.deduceInput) and Flutter web (inferDeltaState) do the same when an event's data cannot be trusted, as on Android.
Text model ¶
A document is a Go string, with "\n" as its only line end ("\r" is an ordinary character, so CRLF text keeps its "\r" at the end of each line). Invalid UTF-8 never makes a function fail: every invalid byte counts as one rune, U+FFFD, and so as one UTF-16 unit, and slicing keeps the original bytes. Offsets are clamped to the document; an offset that falls inside a surrogate pair (a rune outside the BMP counts for two units) rounds down to the start of that rune.
Cost ¶
Every function is linear in the length of its text arguments, and none of them allocates: results are numbers or slices of the arguments.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func FromByte ¶
FromByte converts the byte offset b in s into a UTF-16 offset. b is clamped to [0, len(s)], and only the runes that end at or before b count.
func FromRune ¶
FromRune is the inverse of ToRune: it converts the rune offset r into a UTF-16 offset. r is clamped to [0, runes in s].
func LineCol ¶
LineCol converts the UTF-16 offset off into a line (counted from 0, "\n" ends a line) and a column counted in runes from the start of that line. An offset inside a surrogate pair rounds down to the start of the rune.
func Offset ¶
Offset is the inverse of LineCol: it returns the UTF-16 offset of the rune column col of line line. A line past the end gives Len(s), and a column past the end of its line gives the offset of that line's end, just before its "\n". Negative arguments count as 0.
Types ¶
type Client ¶
type Client interface {
// InputText returns the whole document, with "\n" line ends.
InputText() string
// InputRevision changes on every edit, so a host can skip rewriting its
// editing element when nothing changed.
InputRevision() uint64
// InputSelection returns the selection, direction kept: anchor == focus
// is a caret. [selectedRange]
InputSelection() (anchor, focus int)
// InputMarked returns the marked (composition) range, or (-1, -1)
// without composition. [markedRange, hasMarkedText]
InputMarked() (start, end int)
// InputMultiline reports whether Enter inserts a line break.
InputMultiline() bool
// InputReadOnly reports whether the widget refuses edits.
InputReadOnly() bool
// SetInputSelection moves the selection.
SetInputSelection(anchor, focus int)
// ReplaceInput replaces [start, end) with text, as a committed, undoable
// edit. (-1, -1) means the current selection.
// [insertText:replacementRange:]
ReplaceInput(start, end int, text string)
// SetMarkedInput replaces [replStart, replEnd) with text and marks it as
// the composition preview; (-1, -1) means the current marked range, or
// the selection when there is none. selStart and selEnd are the
// selection inside text, relative to its start.
// [setMarkedText:selectedRange:replacementRange:]
SetMarkedInput(text string, selStart, selEnd, replStart, replEnd int)
// UnmarkInput commits the marked text as typed; it does nothing without
// marked text. [unmarkText]
UnmarkInput()
// InputRect returns the surface pixels of the first line of [start, end).
// [firstRectForCharacterRange:actualRange:]
InputRect(start, end int) (x, y, w, h int)
// InputIndexAt returns the offset nearest to the surface point (x, y).
// [characterIndexForPoint:]
InputIndexAt(x, y int) int
}
Client is a text widget as an input method sees it. Offsets count UTF-16 code units over InputText(): NSTextInputClient's NSRange, the DOM's selectionStart and TSF's ACP all count this way. -1 means "none".
The names keep the Input prefix because a widget already has methods such as Text, Selection and SetSelection with other meanings. The comment on each method gives the NSTextInputClient selector it corresponds to. doCommandBySelector: has no counterpart: command keys go through the widget's ordinary key handler, and attributedSubstringForProposedRange: is derived from InputText.
type Edit ¶
Edit replaces the UTF-16 range [Start, End) of a text with Text. It is what Reconcile deduces and what Client.ReplaceInput takes. The zero Edit changes nothing.
func Reconcile ¶
Reconcile deduces the one Edit that turns prev.Text into next.Text. It is the fallback of a host whose editing element changed without a trustworthy event (Android composition, autofill, an unusual IME): prev is what the host last wrote to the element, next is what it reads back. The same diff is VS Code's TextAreaState.deduceInput and Flutter web's inferDeltaState.
The result is minimal: it replaces the fewest UTF-16 units, found by trimming the longest common prefix and suffix. When several minimal edits exist (typing "a" after "a" gives "aa" whichever "a" is new), the one that starts nearest to the caret is chosen: the smaller of the two selection starts, which is the rule VS Code applies by bounding the prefix with both selections. A selection with a negative anchor or focus is ignored, and without any selection the edit is placed as far right as possible.
Start and End are offsets in prev.Text and never split a surrogate pair. Edit.Text is a slice of next.Text, so invalid UTF-8 is kept byte for byte, and prev.Text with the Edit applied is always next.Text. Equal texts give the zero Edit, whatever the selections. The marked range of both states is not used: it is the caller's business to re-mark after applying the Edit.
The cost is linear in the length of both texts, and Reconcile allocates nothing: the scans work on the UTF-8 bytes, and only the runes before the edit and inside the common suffix are counted, never copied.
type State ¶
State is a snapshot of a text widget: what a host last wrote to its editing element, or what it read back. Offsets are UTF-16 code units.
Anchor and Focus are the selection with its direction (Anchor == Focus is a caret); a negative value means "no selection". MarkStart and MarkEnd are the marked range, (-1, -1) without composition.