Class TextArea

java.lang.Object
limn.scene.Widget<TextArea>
limn.components.TextArea

public final class TextArea extends Widget<TextArea>
Multiline text editor: hard line breaks (Enter), arrow navigation across lines with a sticky column, mouse/Shift+arrow selection spanning lines, clipboard shortcuts, blinking cursor, and both scrollbars: draggable thumbs and wheel-responsive. Long lines scroll horizontally by default; setSoftWrap(boolean) breaks them at the text column's edge instead, at the opportunities the UI language's line-break rule finds, and the horizontal axis goes quiet.

Colors come from the Theme, metrics from the SizeTokens row of the ControlSize step resolved on this widget, and every stroke from Strokes.

The content inset is anisotropic, and deliberately so. Horizontally it is fieldPadH, the same token TextField insets its text by, so a field and an area stacked in a form put their first character on the same column. Vertically it is areaPad: a text margin rather than a vertical-centring pad, which is why it is its own token and nothing outside this widget has to agree with it.

The preferred box is derived per step to hold a roughly constant amount of visible content rather than a constant box: an editor is where character count matters most.

Every geometric question is asked of a shaped line, one per visible line. Caret x, hit testing and the selection band all come from the ShapedText of the line they concern (shapedRow(int, limn.components.SizeTokens)) rather than from the measured width of a prefix of it, because with a shaper in the pipeline the width of the first n characters of a line is not the width of those characters inside it: they join, ligate, kern and reorder differently. The consequences a user sees are that a click lands on the character under the pointer in mixed text, that a selection crossing a direction boundary paints as the two or more boxes it really covers, and that Left and Right move the caret by what is next on the screen.

Left and Right are visual; everything else is logical. Home, End, word movement, Backspace, Delete, Page and Up/Down all step through the string, because each has to name a contiguous range: Shift+Home makes a selection and a selection is one range of the buffer. So in right-to-left text Left and Ctrl+Left move the caret in opposite directions, which is what Windows and GTK do and the lesser of the two evils.

Soft wrap is a row map, never a second copy of the document. Wrapping, each hard line holds the char offsets its rows start at (ints), a prefix sum turns a global row into a line and back, and every geometry question — caret, click, selection band, scroll extent — is asked of the row under it, through the same shaped-line window the unwrapped mode paints from (unwrapped, a line simply is its one row). Building the map costs one shaping per line and is paid when the text is replaced or the column width, font, ruler epoch or locale moves; an edit re-wraps only the lines it touched, told apart by TextEditModel.lineDamage(). That seam exists because re-deriving per-line state from textVersion() alone means re-shaping the document per keystroke — the cliff ADR 031 §8.2 measured at 22 ms per character typed — and a soft wrap that reintroduced it would be a regression dressed as a feature.

The scrollbars do not follow the step, so the overlay bars stay 15 pt wide while the pads shrink at the small steps and the bars float over more live text there. An accepted cost: at XSMALL, prefer a trailing right margin in the surrounding layout if the last column matters.

  • Constructor Details

    • TextArea

      public TextArea()
      An empty editor.
  • Method Details

    • text

      public String text()
      The full contents, lines joined by \n.
    • setText

      public TextArea setText(String text)
      Replaces the contents, clearing the selection and undo history. Announces a TEXT edit as CODE when the text moved, and reaches no handler; the undo history and the caret are reset even when it is handed the string it already holds. UI thread only.
    • setSoftWrap

      public TextArea setSoftWrap(boolean wrap)
      Turns soft wrap on or off (default off: long lines scroll). Wrapping, a line wider than the text column breaks at the opportunities BreakIterator finds under the UI language — the same walk Label wraps with, so a paragraph breaks in an editor exactly where it breaks in the prose beside it, unspaced CJK and dictionary-segmented Thai included — and the horizontal axis goes quiet: nothing overflows it, scrollXOffset() pins at 0, and the horizontal bar shows no thumb. Reading right to left every row is flush against the edge reading starts from, the right one, exactly as unwrapped lines are.

      Wrapped, Up, Down and Page move by visual row on a sticky goal x, where unwrapped they move by hard line on the model's sticky goal column: a column is the right invariant while a line is one row and meaningless once it is several. Left and Right step onto the neighbouring row when the current one runs out, exactly as they already step onto the neighbouring line. Home, End and Shift+Home stay hard-line and logical, as everywhere else in the toolkit, so a selection is always one range of the buffer.

      A caret whose index sits exactly on a soft break is two places on screen — the end of one row and the start of the next — and the caret's side says which, the same side that already disambiguates a direction boundary. The whitespace a break drops is not deleted: it hangs past the margin, undrawn, and a caret inside it shows at the row's drawn end.

      Toggling resets the horizontal scroll, because the axis changes meaning; the vertical offset keeps its value and re-clamps against the new extent. UI thread only.

    • isSoftWrap

      public boolean isSoftWrap()
      Whether long lines wrap at the text column's edge instead of scrolling.
    • onChange

      public TextArea onChange(@Nullable Consumer<String> listener)
      The application's response to the user editing the text: typing, pasting, undo and redo, an IME commit, an assistive technology's set. Never for setText(java.lang.String) or insertText(java.lang.String), which are a caller's writes; to hear every edit whatever caused it, watch the area, which also hands over the edit's offsets.
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this area
      Throws:
      IllegalStateException - if a handler is already registered
    • handleUserChange

      protected void handleUserChange(Change.Aspect aspect)
      Description copied from class: Widget
      Runs this widget's application handler for a change it has just announced. Called by Widget.runHandler(limn.scene.Change), and so by Widget.notifyChange(limn.scene.Change), only for a USER origin and after every watcher has run.

      A component overrides it once and switches on the aspect, reading its own accessors for the payload: a slider passes value(), a combo box selectedIndex(), a text field text(). A component with no handler does not override it.

      An override that does not recognise the aspect must call super, because a subclass of a component that has a handler inherits that component's dispatch through this method: a SearchField that overrode this for its own aspect and did not chain would silently delete TextField's. The ordering lives in the base class, this is the one link in it a subclass can break, and the contract test drives every subclass through its parent's gestures to catch one that does.

      Overrides:
      handleUserChange in class Widget<TextArea>
      Parameters:
      aspect - what changed; the origin is USER or this is not called
    • insertText

      public TextArea insertText(@Nullable String text)
      Inserts text at the cursor (replacing any selection): a caller's write, announced as a TEXT edit at CODE. UI thread.
    • setValidation

      public TextArea setValidation(Validation state)
      Sets the validation state; colors the border danger/warning/success.
    • setError

      public TextArea setError(boolean error)
      Convenience: ERROR when true, NONE when false.
    • validation

      public Validation validation()
      The current validation state, which drives the border's colour, as a text field's does.
    • setPreferredSize

      public TextArea setPreferredSize(float width, float height)
      Overrides the step's areaWidth/areaHeight. A negative value on either axis restores that axis's token, so setPreferredSize(-1, 150) pins the height and lets the width follow the step.
    • caretPosition

      public int caretPosition()
      Returns:
      where the caret is, as a char offset into text()
    • selectionStart

      public int selectionStart()
      Returns:
      where the selection starts, or the caret's offset when nothing is selected
    • selectionEnd

      public int selectionEnd()
      Returns:
      where the selection ends, or the caret's offset when nothing is selected
    • selectedText

      public String selectedText()
      Returns:
      the selected text, empty when nothing is selected
    • setCaretPosition

      public TextArea setCaretPosition(int offset)
      Puts the caret at an offset and selects nothing. See select(int, int).
      Parameters:
      offset - a char offset, from 0 to the text's length
      Returns:
      this TextArea
      Throws:
      IndexOutOfBoundsException - when offset is outside the text
    • select

      public TextArea select(int anchor, int caret)
      Selects from anchor to caret, and the caret ends at caret, so a caret before the anchor is a selection made backwards, as Shift+Left makes one. An offset inside a grapheme cluster (a combining mark, an emoji sequence) moves to the start of that cluster, because a caret is never drawn inside one. Announces SELECTION as CODE when the caret or the selection moved, and reaches no handler. UI thread only.
      Parameters:
      anchor - where the selection starts, a char offset from 0 to the text's length
      caret - where it ends and the caret goes, likewise
      Returns:
      this TextArea
      Throws:
      IndexOutOfBoundsException - when either offset is outside the text
    • selectAll

      public TextArea selectAll()
      Selects the whole text, the caret at its end. See select(int, int).
      Returns:
      this TextArea
    • scrollXOffset

      public float scrollXOffset()
      Horizontal scroll offset in logical points, 0 at the leading edge: the left edge in a left-to-right subtree and the right edge in a right-to-left one. The range is [0, maxScrollX] in both, so "scrolled to the start" is 0 either way and a reset on a content change needs no branch. Under soft wrap nothing overflows this axis and the offset stays 0.
    • scrollYOffset

      public float scrollYOffset()
      Vertical scroll offset in logical points, 0 at the first line.
    • scrollBy

      public TextArea scrollBy(float dx, float dy)
      Scrolls by a delta in logical points (clamped to the content). UI thread only.
    • setBarLayout

      public TextArea setBarLayout(ScrollGutters.Layout layout)
      Sets whether the bars float over the text or reserve strips of their own (default ScrollGutters.Layout.OVERLAY, which is what prose wants).
    • barLayout

      public ScrollGutters.Layout barLayout()
      Whether the scrollbars overlay the text or reserve a gutter.
    • onMeasure

      protected Size onMeasure(Constraints constraints)
      Description copied from class: Widget
      Reports the size this widget wants within constraints. Called once per layout pass, and the result is cached against the constraints and the resolved axes (size step, layout direction, locale), so it must be a pure function of them and of this widget's own state.

      Resolve the ControlSize and the LayoutDirection once each here and thread them down; never read either in a constructor. The locale needs no threading: it is in scope, and I18n.locale() answers it wherever text is resolved.

      Specified by:
      onMeasure in class Widget<TextArea>
    • baselineOffset

      protected float baselineOffset()
      The first line's baseline, the very expression onPaint(limn.graphics.Canvas) draws line 0 with, taken at scrollY == 0. Deliberately not scroll-dependent: the scroll offset is view state, and a BASELINE row that re-aligned itself as the user scrolled would jitter.

      No empty-text guard here, unlike the single-line controls. An editor always paints a line box (the caret sits in it) whether or not the model holds text, so falling back to super.baselineOffset() (the bottom edge) would make typing the first character jump a whole ~140 pt row.

      Overrides:
      baselineOffset in class Widget<TextArea>
    • onLayout

      protected void onLayout()
      Description copied from class: Widget
      Containers position children here (measure + Widget.layoutBox(float, float, float, float) per child).
      Overrides:
      onLayout in class Widget<TextArea>
    • onPaint

      protected void onPaint(Canvas canvas)
      Description copied from class: Widget
      Widget's own background/content, in local coordinates.
      Overrides:
      onPaint in class Widget<TextArea>
    • onMouseEvent

      protected void onMouseEvent(MouseEvent event)
      Description copied from class: Widget
      Mouse events (bubbling); call event.consume() when handled.
      Overrides:
      onMouseEvent in class Widget<TextArea>
    • onCanEdit

      protected boolean onCanEdit(EditCommand command)
      Description copied from class: Widget
      Whether this widget can do an editing command now: copy with something selected, paste with something to paste, undo with something to take back. Scene.canEdit(limn.scene.EditCommand) asks the widget that has the keyboard and then each of its ancestors, and a menu's item of an editing role is enabled by the answer.

      A widget answers for the commands it takes from the keyboard, and does for the command what it does for the key.

      Overrides:
      onCanEdit in class Widget<TextArea>
      Returns:
      false here: a widget edits nothing until it says so
    • onEdit

      protected boolean onEdit(EditCommand command)
      Description copied from class: Widget
      Does an editing command, as the user's own act. Called on the first widget, from the one with the keyboard upward, whose Widget.onCanEdit(limn.scene.EditCommand) answered true.
      Overrides:
      onEdit in class Widget<TextArea>
      Returns:
      whether it was done
    • onKeyEvent

      protected void onKeyEvent(KeyEvent event)
      Description copied from class: Widget
      Key events (focused widget first, then ancestors).
      Overrides:
      onKeyEvent in class Widget<TextArea>
    • showContextMenu

      protected void showContextMenu(float localX, float localY)
      Raises the Cut/Copy/Paste/Select All menu at a point in this widget's own coordinates: the same menu, and the same enabled rules, as TextField's.
    • showContextMenuForFocus

      protected void showContextMenuForFocus()
      Raises the same menu for a request that carries no point: the Menu key or Shift+F10, where the caret is the only place the user can have meant. Protected for the same reason as its pointer twin, and a subclass suppressing one has to suppress both.
    • onCharTyped

      protected void onCharTyped(CharEvent event)
      Description copied from class: Widget
      Committed text input (focused widget first, then ancestors).
      Overrides:
      onCharTyped in class Widget<TextArea>
    • acceptsTextInput

      protected boolean acceptsTextInput()
      Description copied from class: Widget
      Whether this widget edits text and should drive the platform input method (IME): the scene enables the IME while such a widget holds focus and disables it otherwise, so composition never intercepts keys meant for non-text UI. Text widgets override this to true. Default: false.
      Overrides:
      acceptsTextInput in class Widget<TextArea>
    • composingText

      public String composingText()
      Returns:
      the text currently being composed by the IME (empty when not composing)
    • onPreedit

      protected void onPreedit(PreeditEvent event)
      Description copied from class: Widget
      In-progress IME composition ("preedit") for this widget while it is focused: the still-composing text, shown inline but not yet committed (the commit later arrives as Widget.onCharTyped(limn.scene.event.CharEvent)). Only widgets that accept text input receive it. Default: ignored.
      Overrides:
      onPreedit in class Widget<TextArea>
    • caretRect

      protected @Nullable Rect caretRect()
      Description copied from class: Widget
      The caret rectangle in scene coordinates (logical points) used to place the IME candidate window; null when there is no caret to anchor (or the widget is not laid out yet). Consulted by the scene only while this widget is focused and Widget.acceptsTextInput() is true.
      Overrides:
      caretRect in class Widget<TextArea>
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      One multi-line editor: what it is, what is in it, where the caret and the selection are, how far its two axes have scrolled, and that it raises a menu.

      No name of its own, and that is a fact about the widget rather than an omission here. This class holds no I18nString of any kind — there is no placeholder to fall back on the way a TextField has one — so the only names an area can have are the walk's tooltip default and an application's setAccessibleName. A focusable node with neither is a real gap, and it is the widget's, not the tree's.

      No description either: what setValidation(limn.components.Validation) holds is a colour, and the message beside an area is a Label the application owns.

      MULTI_LINE is unconditional and is not about wrapping. It is a fact about the model this widget holds — new TextEditModel(false), whose sanitize keeps every newline — and about Enter inserting one. Soft wrap decides how a line is drawn and says nothing about how many lines there are.

      The two ScrollBar children are real widgets and publish themselves through the ordinary walk, so nothing is declared about them here. Each ignores itself while its axis fits, which is why an area whose text fits is a childless leaf and one that overflows grows one or two SCROLL_BAR children.

      Overrides:
      onAccessibility in class Widget<TextArea>
      Parameters:
      a - the node being described
    • onAccessibilityAction

      protected boolean onAccessibilityAction(Accessible.Action action, Accessible.Argument arg)
      Raises the context menu, replaces the contents, moves the caret or sets the selection, each through the path the user's own gesture takes; the rules are TextAccessibility's.
      Overrides:
      onAccessibilityAction in class Widget<TextArea>
      Parameters:
      action - what is being asked
      arg - the text for SET_TEXT and the range for the other two
      Returns:
      whether this widget did it
    • onFocusGained

      protected void onFocusGained()
      Description copied from class: Widget
      Called when this widget takes keyboard focus. Default: nothing.
      Overrides:
      onFocusGained in class Widget<TextArea>
    • onFocusLost

      protected void onFocusLost()
      Description copied from class: Widget
      Called when this widget loses keyboard focus. Default: nothing.
      Overrides:
      onFocusLost in class Widget<TextArea>