TextSelection.java

package swingtree.style;

import com.google.errorprone.annotations.Immutable;
import swingtree.layout.Bounds;
import swingtree.layout.Position;

import java.util.Objects;
import java.util.Optional;

/**
 *  An immutable value describing which characters of a styled text are selected.
 *  Bind a property holding one to the styled text of a component through
 *  {@link swingtree.UIForAnySwing#withTextSelection(swingtree.UI.Layer, String, sprouts.Var)}:
 *  <pre>{@code
 *  Var<TextSelection> selection = Var.of(TextSelection.caretAt(0));
 *  UI.box()
 *  .withStyle( it -> it
 *      .text("body", t -> t.content("Hello world"))
 *  )
 *  .withTextSelection("body", selection);
 *  }</pre>
 *  A selection holds two positions in the text. A position is a gap between two characters:
 *  position 0 is the gap in front of the first character, position 1 is the gap behind
 *  the first character, and the position equal to the length of the text is the gap
 *  behind its last character. In "Hello world", {@code TextSelection.between(6, 11)}
 *  selects "world".
 *  <p>
 *  The <b>anchor</b> is the position where the selection began, for example where your user
 *  pressed the mouse button. The <b>caret</b> is the position which moves, for example where
 *  your user drags the mouse to. A click with the shift key held down keeps the anchor and
 *  moves the caret. Which of the two positions comes first in the text does not change which
 *  characters are selected: {@link #start()} is always the smaller and {@link #end()} always
 *  the larger of the two.
 *  <p>
 *  The two positions, {@link #isCaretAtLineEnd()} and {@link #isEnabled()} are the only parts of a
 *  selection which your code chooses. Everything else is read only, and SwingTree fills it in:
 *  after SwingTree has painted the text, it writes a selection into the property which holds the
 *  selected characters in
 *  {@link #selectedText()}, the pixel rectangles of the anchor and the caret in {@link #anchorBounds()}
 *  and {@link #caretBounds()}, and which converts a pixel coordinate into a position in
 *  {@link #positionAt(Position)}. SwingTree also moves a position which lies behind the end of the
 *  text to the end of the text, and sets {@link #isCaretAtLineEnd()} to {@code false} where the caret
 *  is not the end of a painted line. A selection which your code creates with {@link #caretAt(int)} or
 *  {@link #between(int, int)} has none of this until SwingTree has written it into the property.
 *  <p>
 *  Two selections are equal when their positions, their {@link #isCaretAtLineEnd()} and {@link #isEnabled()}
 *  flags and everything that SwingTree filled in are equal. So the property receives a new selection whenever the
 *  painted text moves, for example when the component gets a new size.
 */
@Immutable
public final class TextSelection
{
    private final int        _anchor;
    private final int        _caret;
    private final boolean    _caretAtLineEnd;
    private final boolean    _enabled;
    private final CaretTable _carets;

    private TextSelection( int anchor, int caret, boolean caretAtLineEnd, boolean enabled, CaretTable carets ) {
        if ( anchor < 0 || caret < 0 )
            throw new IllegalArgumentException(
                "A text position is never negative, but the anchor is " + anchor + " and the caret is " + caret + "."
            );
        _anchor         = anchor;
        _caret          = caret;
        _caretAtLineEnd = caretAtLineEnd;
        _enabled        = enabled;
        _carets         = Objects.requireNonNull(carets);
    }

    /**
     *  Creates an enabled selection of no characters, whose anchor and caret are both at the given position.
     *  Where a line wraps, the caret lies at the start of the next line, see {@link #isCaretAtLineEnd()}.
     *
     * @param position The gap between two characters at which both the anchor and the caret are,
     *                 counted from 0 for the gap in front of the first character.
     * @return A selection for which {@link #isEmpty()} is {@code true}.
     * @throws IllegalArgumentException If the position is negative.
     */
    public static TextSelection caretAt( int position ) {
        return new TextSelection(position, position, false, true, CaretTable.EMPTY);
    }

    /**
     *  Creates an enabled selection of the characters between the two given positions.
     *  The anchor may lie behind the caret, which is what a selection made by
     *  dragging the mouse backwards looks like. When the caret lies behind the anchor, so that the caret is
     *  the end of the selected characters, {@link #isCaretAtLineEnd()} is {@code true}, otherwise {@code false}.
     *
     * @param anchor The position where the selection began, which a shift-click keeps.
     * @param caret The position which moves, which a shift-click replaces.
     * @return A selection with the given anchor and caret.
     * @throws IllegalArgumentException If one of the positions is negative.
     */
    public static TextSelection between( int anchor, int caret ) {
        return new TextSelection(anchor, caret, anchor < caret, true, CaretTable.EMPTY);
    }

    /**
     *  Returns the position where the selection began, which a shift-click keeps.
     *
     * @return The anchor of this selection.
     */
    public int anchor() { return _anchor; }

    /**
     *  Returns the position which moves while your user drags the mouse, and which a shift-click replaces.
     *
     * @return The caret of this selection.
     */
    public int caret() { return _caret; }

    /**
     *  Returns the smaller of the anchor and the caret, which is the gap in front of the first selected character.
     *
     * @return The position where the selected characters start.
     */
    public int start() { return Math.min(_anchor, _caret); }

    /**
     *  Returns the larger of the anchor and the caret, which is the gap behind the last selected character.
     *
     * @return The position where the selected characters end.
     */
    public int end() { return Math.max(_anchor, _caret); }

    /**
     *  Tells whether no character is selected, because the anchor and the caret are the same position.
     *
     * @return {@code true} if the anchor and the caret are the same position.
     */
    public boolean isEmpty() { return _anchor == _caret; }

    /**
     *  Tells whether your users can select characters of the text. When this is {@code false},
     *  SwingTree paints no selection colour, no selection border and no caret, the mouse and
     *  the keyboard select and copy nothing, and the mouse cursor stays the default cursor.
     *  SwingTree still fills in {@link #selectedText()} and the rectangles of the anchor and the caret.
     *
     * @return {@code true} if your users can select characters, which is the default.
     */
    public boolean isEnabled() { return _enabled; }

    /**
     *  Returns a selection with the same positions which your users can or cannot change,
     *  see {@link #isEnabled()}.
     *
     * @param enabled {@code true} to let your users select characters, {@code false} to ignore the mouse and the keyboard.
     * @return A selection with the given flag.
     */
    public TextSelection withEnabled( boolean enabled ) {
        return enabled == _enabled ? this : new TextSelection(_anchor, _caret, _caretAtLineEnd, enabled, _carets);
    }

    /**
     *  Returns a selection with the same anchor and the given caret,
     *  which is what a click with the shift key held down makes of this selection.
     *  When the new caret lies behind the anchor, so that it is the end of the selected characters,
     *  {@link #isCaretAtLineEnd()} of the new selection is {@code true}, otherwise {@code false}.
     *  If the caret already is at the given position, this method returns this selection.
     *
     * @param caret The new position of the caret.
     * @return A selection from the anchor of this selection to the given caret.
     * @throws IllegalArgumentException If the caret is negative.
     */
    public TextSelection withCaret( int caret ) {
        return caret == _caret ? this : new TextSelection(_anchor, caret, _anchor < caret, _enabled, _carets);
    }

    /**
     *  Tells where the caret lies when its position is both the end of one painted line and the start of the
     *  next one. That happens where SwingTree wraps a long line, and on the two sides of an obstacle which the
     *  text flows around. With {@code true}, the caret lies at the end of the upper line, right next to the
     *  character in front of it. With {@code false}, it lies at the start of the next line, in front of the
     *  character behind it. At every other position there is only one place, and this flag changes nothing.
     *  Text editors know this as the affinity of the caret.
     *  <p>
     *  SwingTree sets the flag wherever your users put the caret: a click right of the end of a line, the End
     *  key and the arrow keys up and down landing on the end of a line put the caret at the end of that line,
     *  and a click in front of the first character of a line and the Home key put it at the start of the line.
     *  When SwingTree writes a selection into the property, it keeps this flag only where the caret really is
     *  the end of a painted line, and sets it to {@code false} everywhere else, so two carets in the same
     *  place are equal. A selection which your code creates holds the value which your code chose until
     *  SwingTree has written it into the property.
     *  {@link #caretBounds()} returns the place which this flag chooses:
     *  <pre>{@code
     *  // "HHHH HHHH" wraps behind the space, so position 5 ends the first line and starts the second:
     *  TextSelection atStartOfSecondLine = TextSelection.caretAt(5);
     *  TextSelection atEndOfFirstLine    = TextSelection.caretAt(5).withCaretAtLineEnd(true);
     *  }</pre>
     *
     * @return {@code true} if the caret lies at the end of the upper line where a position has two places.
     */
    public boolean isCaretAtLineEnd() { return _caretAtLineEnd; }

    /**
     *  Returns a selection with the same positions whose caret lies at the end of the upper line, or at the
     *  start of the next line, where its position is both, see {@link #isCaretAtLineEnd()}.
     *
     * @param atLineEnd {@code true} for the end of the upper line, {@code false} for the start of the next line.
     * @return A selection with the given flag.
     */
    public TextSelection withCaretAtLineEnd( boolean atLineEnd ) {
        return atLineEnd == _caretAtLineEnd ? this : new TextSelection(_anchor, _caret, atLineEnd, _enabled, _carets);
    }

    /**
     *  Returns the selected characters. SwingTree fills them in after it has painted the text,
     *  so this is an empty string for a selection which your code created and which SwingTree
     *  has not written into the property yet.
     *
     * @return The characters between {@link #start()} and {@link #end()}, up to the end of the text.
     */
    public String selectedText() {
        final String text = _carets.text();
        return text.substring(Math.min(start(), text.length()), Math.min(end(), text.length()));
    }

    /**
     *  Returns the pixel rectangle of the caret on the component, which is where SwingTree paints the
     *  caret when {@code showCaret(true)} is set in the style of the selection. The rectangle uses the
     *  same coordinates as the mouse events of the component: pixels, counted from the top left corner
     *  of the component, with the UI scale already applied. It reaches from the top to the bottom of the line of the caret,
     *  and its left edge is the gap between the two characters, whatever the shape of the caret is.
     *  For a {@link swingtree.UI.CaretShape#BAR}, which is the default, it is as wide as the {@code caretWidth} of the style
     *  of the selection, and at least one pixel. For a {@link swingtree.UI.CaretShape#BLOCK} and an
     *  {@link swingtree.UI.CaretShape#UNDERLINE}, it is as wide as the character behind the gap, and SwingTree
     *  paints an underline into the bottom of it, as thick as the {@code caretWidth}.
     *  <p>
     *  The position where a line wraps is both the end of that line and the start of the next line.
     *  There, {@link #isCaretAtLineEnd()} decides where the rectangle lies: at the end of the upper line
     *  when it is {@code true}, and in front of the character behind the caret when it is {@code false}.
     *  For example, when your users select up to the place where a line wraps, the rectangle of the caret
     *  lies at the right end of the upper line, next to the last selected character, and not in front of
     *  the first character of the next line. A caret made by {@link #caretAt(int)} at that place lies at
     *  the start of the next line.
     *  <p>
     *  Two line breaks in a row make an empty paragraph, which SwingTree paints as an empty line.
     *  The rectangle of a caret in it lies at the start of that empty line and is as tall as the font size.
     *  <p>
     *  It is empty for a selection which SwingTree has not written into the property yet, and for a
     *  caret on a line which is not painted because the component is too small to show it.
     *
     * @return The rectangle of the caret, or an empty {@link Optional}.
     */
    public Optional<Bounds> caretBounds() { return _carets.boundsOf(_caret, _caretAtLineEnd); }

    /**
     *  Returns the pixel rectangle of the anchor on the component, measured in the same way as
     *  {@link #caretBounds()}: when this selection is not empty and the anchor is its {@link #end()},
     *  which is the case for a selection made backwards, the rectangle lies at the end of the line which
     *  holds the last selected character, and otherwise in front of the character behind the anchor.
     *
     * @return The rectangle of the anchor, or an empty {@link Optional}.
     */
    public Optional<Bounds> anchorBounds() { return _carets.boundsOf(_anchor, !isEmpty() && _anchor == end()); }

    /**
     *  Returns the position nearest to the given pixel coordinate, by the rule which SwingTree
     *  uses for the mouse: the line nearest to the coordinate, and in that line the gap between
     *  two characters nearest to it. A coordinate left of the first or right of the last character
     *  of a line gives the first or the last position of that line.
     *  The coordinate uses the same coordinates as the mouse events of the component, see {@link #caretBounds()}.
     *
     * @param point A pixel coordinate on the component.
     * @return The position nearest to the coordinate, or an empty {@link Optional} for a selection
     *         which SwingTree has not written into the property yet.
     */
    public Optional<Integer> positionAt( Position point ) {
        final int position = _carets.positionAt(point.x(), point.y());
        return position < 0 ? Optional.empty() : Optional.of(position);
    }

    /**
     *  Returns a selection with the same anchor and the caret at the position nearest to the given
     *  pixel coordinate, see {@link #positionAt(Position)}. This is what dragging the mouse to the
     *  coordinate does. Where a line wraps, the caret lies on the line of the coordinate:
     *  {@link #isCaretAtLineEnd()} is {@code true} for a coordinate at the end of the upper line and
     *  {@code false} for a coordinate at the start of the next line. So a handle which your code paints at
     *  {@link #caretBounds()} stays under the finger of your users while they drag it across that place.
     *
     * @param point A pixel coordinate on the component.
     * @return A selection with the moved caret, or this selection if SwingTree has not written it into the property yet.
     */
    public TextSelection withCaretAt( Position point ) {
        return positionAt(point)
                .map( caret -> withCaret(caret).withCaretAtLineEnd(_carets.endsLineAt(point.x(), point.y())) )
                .orElse(this);
    }

    /**
     *  Returns a selection with the same caret and the anchor at the position nearest to the given
     *  pixel coordinate, see {@link #positionAt(Position)}.
     *
     * @param point A pixel coordinate on the component.
     * @return A selection with the moved anchor, or this selection if SwingTree has not written it into the property yet.
     */
    public TextSelection withAnchorAt( Position point ) {
        return positionAt(point)
                .map( anchor -> anchor == _anchor ? this : new TextSelection(anchor, _caret, _caretAtLineEnd, _enabled, _carets) )
                .orElse(this);
    }

    CaretTable carets() { return _carets; }

    @SuppressWarnings("ReferenceEquality")
    TextSelection resolvedWith( CaretTable carets ) {
        final int length = carets.text().length();
        final int anchor = Math.min(_anchor, length);
        final int     caret     = Math.min(_caret, length);
        final boolean atLineEnd = _caretAtLineEnd && carets.isLineEnd(caret);
        if ( anchor == _anchor && caret == _caret && atLineEnd == _caretAtLineEnd && carets == _carets )
            return this;
        return new TextSelection(anchor, caret, atLineEnd, _enabled, carets);
    }

    @Override
    public boolean equals( Object other ) {
        if ( this == other )
            return true;
        if ( !(other instanceof TextSelection) )
            return false;
        final TextSelection that = (TextSelection) other;
        return _anchor         == that._anchor         &&
               _caret          == that._caret          &&
               _caretAtLineEnd == that._caretAtLineEnd &&
               _enabled        == that._enabled        &&
               _carets.equals(that._carets);
    }

    @Override
    public int hashCode() {
        return Objects.hash(_anchor, _caret, _caretAtLineEnd, _enabled, _carets);
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" +
                    "anchor=" + _anchor + ", " +
                    "caret=" + _caret + ", " +
                    "caretAtLineEnd=" + _caretAtLineEnd + ", " +
                    "enabled=" + _enabled + ", " +
                    "selectedText='" + selectedText() + "', " +
                    "anchorBounds=" + anchorBounds().map(Object::toString).orElse("?") + ", " +
                    "caretBounds=" + caretBounds().map(Object::toString).orElse("?") +
                "]";
    }
}