Class TextArea

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

public class TextArea extends Widget
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. 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.

    • softWrap

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

      public TextArea onChange(Consumer<String> listener)
      Called with the full text after every edit, typed or programmatic.
    • insertText

      public TextArea insertText(String text)
      Inserts text at the cursor (replacing any selection), as if typed. UI thread.
    • setValidation

      public TextArea setValidation(TextField.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.
    • 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.
    • model

      public TextEditModel model()
      The editing model: caret, selection and undo. Mutating it directly bypasses onChange.
    • 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
    • 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
    • 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
    • onPaint

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

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

      protected void onKeyEvent(KeyEvent event)
      Description copied from class: Widget
      Key events (focused widget first, then ancestors).
      Overrides:
      onKeyEvent in class Widget
    • 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
    • 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
    • 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
    • caretRect

      protected 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
    • onFocusGained

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

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