Class TextEditModel
char offsets into the text; all movement and deletion step by
grapheme cluster (regex \X), so combining marks and ZWJ emoji
are never split; a surrogate pair is the simplest such cluster.
Line lookups (lineOf/lineCount/lineText/…) are
served from a line-start index that every edit keeps in step with itself,
so neither a paint-time query nor the edit before it rescans the buffer.
The caret is an index and a side, caret(), because on a direction boundary one
index is two points on the screen and only the side says which. The side lives here, beside the
cursor, rather than in the widget: every method below that writes cursor would otherwise
need a paired assignment at three call sites apiece, and undo() restores a cursor, so it
has to restore a side with it — an undo that put a correct index back on yesterday's side draws
the caret at the far end of the run, one keystroke later, with nothing to trace it to.
Two movement axes, and they are not interchangeable. moveVisualLeft(limn.graphics.ShapedText, int, boolean) and
moveVisualRight(limn.graphics.ShapedText, int, boolean) are what the arrow keys press: a step left or right on the screen,
which needs the shaped line to answer. Everything else here — Home, End, word movement,
Backspace, Delete, Up and Down — is logical, a step through the string, because each of
them has to name a contiguous range: Shift+Home makes a selection, and a selection is
(anchor, cursor). The consequence is stated rather than discovered: in right-to-left text
Left and Ctrl+Left move the caret in opposite directions, exactly as they do on Windows and in
GTK.
Taking a ShapedText costs this package no dependency it did not have: the type is an
immutable value whose accessors return nothing but primitives, Strings and records of
those, so this class still draws nothing and knows no widget, and every case below is pinned by a
test with no scene, no ruler and no window.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final recordThe lines edits have replaced: lines[firstLine, oldLastLine]of the text as it stood at the lastclearLineDamage()became lines[firstLine, newLastLine]of the text as it stands now. -
Constructor Summary
ConstructorsConstructorDescriptionTextEditModel(boolean singleLine) A model for one line (newlines rejected on insert) or for many. -
Method Summary
Modifier and TypeMethodDescriptionintalignToGrapheme(int index) Snapsindexto the start of the cluster containing it (hit testing).intalignToGraphemeForward(int index) Snapsindexforward to the cluster boundary at or after it (forward motion).voidDeletes the selection, or the grapheme cluster before the cursor.booleancanRedo()Whether a redo step is available; any new edit discards the redo stack.booleancanUndo()Whether an undo step is available.caret()The caret: the insertion index and which side of it the caret is on.voidForgets the recorded damage: the consumer has re-derived what it holds.voidDrops the selection, leaving the cursor where it is.intcursor()Caret position as acharoffset, always on a grapheme boundary.voidDeletes the selection, or the grapheme cluster after the cursor.booleanvoidDeletes the selection, or from the cursor back to the previous word boundary.voidDeletes the selection, or from the cursor forward to the next word boundary.booleanWhether a non-empty range is selected.voidInserts at the cursor, replacing any selection.voidinsertCodePoint(int codepoint) Committed keyboard input; consecutive calls coalesce into one undo step.intlength()Buffer length inchars, not in grapheme clusters.intNumber of lines; always at least 1, and 1 for a single-line model.What the edits since the lastclearLineDamage()touched, ornullwhen the text has not changed since.intlineEnd(int index) intlineOf(int index) intlineStart(int index) intlineStartOfLine(int line) lineText(int line) voidmoveDocumentEnd(boolean select) End of the whole text: Ctrl+End / Cmd+Down.voidmoveDocumentStart(boolean select) Start of the whole text: Ctrl+Home / Cmd+Up.voidmoveDown(boolean select) Down one line, keeping the sticky goal column.voidmoveEnd(boolean select) End of the current line (end of text on single-line models), in logical order: the mirror ofmoveHome(boolean), so in a right-to-left paragraph it moves the caret to the visual left.voidmoveHome(boolean select) Start of the current line (start of text on single-line models), in logical order — so in a right-to-left paragraph this moves the caret to the visual right.voidmoveLeft(boolean select) Moves the caret one grapheme cluster left in the string, extending the selection whenselectis set and collapsing it to the left edge when it is not.voidmoveRight(boolean select) Moves the caret one grapheme cluster right in the string, extending the selection whenselectis set and collapsing it to the right edge when it is not.voidmoveUp(boolean select) Up one line, keeping the sticky goal column.booleanmoveVisualLeft(ShapedText line, int lineStart, boolean select) Moves the caret one step left on the screen, overline: the Left arrow, whatever direction the text under it runs.booleanmoveVisualRight(ShapedText line, int lineStart, boolean select) Moves the caret one step right on the screen: the mirror ofmoveVisualLeft(limn.graphics.ShapedText, int, boolean)in every respect, collapsing a selection toselectionEnd()instead.voidmoveWordLeft(boolean select) Previous word boundary: Ctrl/Alt+Left.voidmoveWordRight(boolean select) Next word boundary: Ctrl/Alt+Right.intnextGrapheme(int index) intnextWordBoundary(int from) intpreviousGrapheme(int index) intpreviousWordBoundary(int from) booleanredo()voidSelects the whole buffer and leaves the cursor at its end.The selected text, or an empty string when there is no selection.intUpper selection bound; equalscursor()when nothing is selected.intLower selection bound; equalscursor()when nothing is selected.voidsetCaret(ShapedText.Position position, boolean select) Places the caret, side included: what a click, a drag and a visual arrow all produce.voidsetCursor(int index, boolean select) Places the cursor;selectextends/starts a selection from the old spot.voidReplaces the whole content programmatically; clears the undo history.text()The whole buffer.textRange(int from, int to) Substring[from, to)without copying the whole buffer (hot paint paths).longBumped by every mutation of the buffer, and by nothing else: a cursor move or a selection change leaves it alone.booleanundo()
-
Constructor Details
-
TextEditModel
public TextEditModel(boolean singleLine) A model for one line (newlines rejected on insert) or for many.
-
-
Method Details
-
text
The whole buffer. -
textRange
Substring[from, to)without copying the whole buffer (hot paint paths). -
length
public int length()Buffer length inchars, not in grapheme clusters. -
setText
Replaces the whole content programmatically; clears the undo history. The caret lands at the end of the buffer on itsShapedText.Affinity.DOWNSTREAMside, which is the paragraph's own end edge — the visual left for right-to-left content, where the next typed character goes. -
insert
Inserts at the cursor, replacing any selection. -
insertCodePoint
public void insertCodePoint(int codepoint) Committed keyboard input; consecutive calls coalesce into one undo step. -
backspace
public void backspace()Deletes the selection, or the grapheme cluster before the cursor. -
deleteForward
public void deleteForward()Deletes the selection, or the grapheme cluster after the cursor. -
hasSelection
public boolean hasSelection()Whether a non-empty range is selected. -
selectionStart
public int selectionStart()Lower selection bound; equalscursor()when nothing is selected. -
selectionEnd
public int selectionEnd()Upper selection bound; equalscursor()when nothing is selected. -
selectedText
The selected text, or an empty string when there is no selection. -
deleteSelection
public boolean deleteSelection()- Returns:
- whether a selection existed and was removed
-
selectAll
public void selectAll()Selects the whole buffer and leaves the cursor at its end. -
clearSelection
public void clearSelection()Drops the selection, leaving the cursor where it is. -
undo
public boolean undo()- Returns:
- whether there was anything to undo
-
redo
public boolean redo()- Returns:
- whether there was anything to redo
-
canUndo
public boolean canUndo()Whether an undo step is available. -
canRedo
public boolean canRedo()Whether a redo step is available; any new edit discards the redo stack. -
cursor
public int cursor()Caret position as acharoffset, always on a grapheme boundary. -
caret
The caret: the insertion index and which side of it the caret is on. The index alone is what an edit, the clipboard and the IME need; the pair is what the line needs, because on a direction boundary one index is two points on the screen and a caret drawn at the wrong one tells the user something false about where their next character lands.- Returns:
- where the caret is, index and side; never null
-
setCursor
public void setCursor(int index, boolean select) Places the cursor;selectextends/starts a selection from the old spot. The caret takesShapedText.Affinity.DOWNSTREAM, the side a programmatic placement has nothing better to go on than. A caller that does know the side — a click, a drag, a visual arrow — callssetCaret(limn.graphics.ShapedText.Position, boolean)instead.- Parameters:
index- where the caret goes, clamped into the bufferselect- whether this extends a selection
-
setCaret
Places the caret, side included: what a click, a drag and a visual arrow all produce.selectextends or starts a selection from the old spot, exactly assetCursor(int, boolean).- Parameters:
position- where the caret goes; its index is clamped into the bufferselect- whether this extends a selection- Throws:
NullPointerException- ifpositionis null
-
moveLeft
public void moveLeft(boolean select) Moves the caret one grapheme cluster left in the string, extending the selection whenselectis set and collapsing it to the left edge when it is not. This is the logical step, which is not what the Left arrow key does once anything reorders:moveVisualLeft(limn.graphics.ShapedText, int, boolean)is that.- Parameters:
select- whether this extends a selection
-
moveRight
public void moveRight(boolean select) Moves the caret one grapheme cluster right in the string, extending the selection whenselectis set and collapsing it to the right edge when it is not. The logical mirror ofmoveLeft(boolean), and not the Right arrow key; seemoveVisualRight(limn.graphics.ShapedText, int, boolean).- Parameters:
select- whether this extends a selection
-
moveHome
public void moveHome(boolean select) Start of the current line (start of text on single-line models), in logical order — so in a right-to-left paragraph this moves the caret to the visual right. That is what Windows edit controls, GTK'sDISPLAY_LINE_ENDSmovement and Cocoa'smoveToBeginningOfLine:all do, and it is forced anyway:Shift+Homehas to produce a selection, a selection is one contiguous range of the string, and the range from the caret to the visual left edge of a mixed line is not one.- Parameters:
select- whether this extends a selection
-
moveEnd
public void moveEnd(boolean select) End of the current line (end of text on single-line models), in logical order: the mirror ofmoveHome(boolean), so in a right-to-left paragraph it moves the caret to the visual left.- Parameters:
select- whether this extends a selection
-
moveVisualLeft
Moves the caret one step left on the screen, overline: the Left arrow, whatever direction the text under it runs.lineis the shaped form of the line the cursor sits on andlineStartis the buffer index that line begins at, so a single-line model passes0. A cursor outside the line is clamped into it rather than rejected: a caret restored from a stale view has to produce a position, not an exception.With a selection and
select == falsethis collapses toselectionStart()and moves no further, which is deliberately the LOGICAL end: a selection can span lines, its two ends can sit in different runs, and the visually left end of a multi-line range is not defined. On a line that reorders nothing the two answers coincide.- Parameters:
line- the shaped line the cursor is onlineStart- buffer index where that line startsselect- whether this extends a selection- Returns:
- whether anything moved;
falsemeans the caret was already at the line's left edge and a multi-line caller should change line - Throws:
NullPointerException- iflineis null
-
moveVisualRight
Moves the caret one step right on the screen: the mirror ofmoveVisualLeft(limn.graphics.ShapedText, int, boolean)in every respect, collapsing a selection toselectionEnd()instead.- Parameters:
line- the shaped line the cursor is onlineStart- buffer index where that line startsselect- whether this extends a selection- Returns:
- whether anything moved;
falsemeans the caret was already at the line's right edge and a multi-line caller should change line - Throws:
NullPointerException- iflineis null
-
moveUp
public void moveUp(boolean select) Up one line, keeping the sticky goal column. No-op on single-line models. -
moveDown
public void moveDown(boolean select) Down one line, keeping the sticky goal column. -
moveWordLeft
public void moveWordLeft(boolean select) Previous word boundary: Ctrl/Alt+Left. Extends the selection whenselect.Logical, and it stays logical:
deleteWordBackward()has to remove a contiguous range of the string, so the boundary this lands on has to be the end of one. In right-to-left text that means Ctrl+Left andmoveVisualLeft(limn.graphics.ShapedText, int, boolean)move the caret in opposite directions, which is what Windows and GTK do and the lesser of the two evils.- Parameters:
select- whether this extends a selection
-
moveWordRight
public void moveWordRight(boolean select) Next word boundary: Ctrl/Alt+Right. Extends the selection whenselect. Logical, for the reasonmoveWordLeft(boolean)gives.- Parameters:
select- whether this extends a selection
-
moveDocumentStart
public void moveDocumentStart(boolean select) Start of the whole text: Ctrl+Home / Cmd+Up. Logical, and so the visual right edge of a right-to-left first line; seemoveHome(boolean).- Parameters:
select- whether this extends a selection
-
moveDocumentEnd
public void moveDocumentEnd(boolean select) End of the whole text: Ctrl+End / Cmd+Down. Logical; seemoveEnd(boolean).- Parameters:
select- whether this extends a selection
-
deleteWordBackward
public void deleteWordBackward()Deletes the selection, or from the cursor back to the previous word boundary. -
deleteWordForward
public void deleteWordForward()Deletes the selection, or from the cursor forward to the next word boundary. -
nextWordBoundary
public int nextWordBoundary(int from) - Returns:
- the next word boundary at or after
from: skip whitespace, then one same-class run
-
previousWordBoundary
public int previousWordBoundary(int from) - Returns:
- the word boundary at or before
from: skip whitespace back, then one same-class run
-
nextGrapheme
public int nextGrapheme(int index) - Returns:
- the grapheme boundary after
index(≤length())
-
previousGrapheme
public int previousGrapheme(int index) - Returns:
- the start of the grapheme cluster ending at or containing
index(≥ 0)
-
alignToGraphemeForward
public int alignToGraphemeForward(int index) Snapsindexforward to the cluster boundary at or after it (forward motion). -
alignToGrapheme
public int alignToGrapheme(int index) Snapsindexto the start of the cluster containing it (hit testing). -
lineStart
public int lineStart(int index) - Returns:
- char index of the start of the line containing
index
-
lineEnd
public int lineEnd(int index) - Returns:
- char index of the end of the line containing
index(before the newline)
-
lineOf
public int lineOf(int index) - Returns:
- zero-based line number of
index
-
lineCount
public int lineCount()Number of lines; always at least 1, and 1 for a single-line model. -
lineStartOfLine
public int lineStartOfLine(int line) - Returns:
- char index where the zero-based
linestarts
-
lineText
- Returns:
- the text of the zero-based
line(no trailing newline)
-
textVersion
public long textVersion()Bumped by every mutation of the buffer, and by nothing else: a cursor move or a selection change leaves it alone. A view caching anything derived from the text (materialized lines, measured widths) compares this rather than re-deriving: it is the difference between a caret blink repainting and a caret blink rebuilding the screenful it repaints.- Returns:
- a value that changes whenever the text does; meaningless except compared to itself
-
lineDamage
What the edits since the lastclearLineDamage()touched, ornullwhen the text has not changed since. One splice is held precisely; a second edit before the clear widens the answer to the whole document rather than composing, because the consumer this exists for syncs after every edit — the widget's change handler runs before the next one can land — so composition is the rare path, and a conservative whole-document answer there is a correct re-derivation, never a wrong splice.setText(java.lang.String)andundo()/redo()answer the whole document for the same reason: what they replace is unbounded. -
clearLineDamage
public void clearLineDamage()Forgets the recorded damage: the consumer has re-derived what it holds.
-