TextSelectionConf.java

package swingtree.style;

import com.google.errorprone.annotations.Immutable;
import org.jspecify.annotations.Nullable;
import swingtree.UI;

import java.awt.Color;
import java.util.Objects;
import java.util.Optional;

/**
 *  Describes how the selected characters of a styled text look, inside the text style
 *  of a component:
 *  <pre>{@code
 *  UI.box()
 *  .withStyle( it -> it
 *      .text("body", t -> t
 *          .content("Hello world")
 *          .selection( s -> s.color(new Color(80, 140, 255, 90)).borderRadius(6) )
 *      )
 *  )
 *  .withTextSelection("body", selection);
 *  }</pre>
 *  The selection itself, which says which characters are selected, is not part of this
 *  configuration. It is held by the property which you pass to
 *  {@link swingtree.UIForAnySwing#withTextSelection(swingtree.UI.Layer, String, sprouts.Var)}.
 *  **So please note that a component without {@code withTextSelection} paints no selection at all.**
 *  <p>
 *  SwingTree paints the selection colour, and the border if there is one, behind the selected
 *  characters and then paints the characters on top of it, in their own colour.
 */
@Immutable
@SuppressWarnings("Immutable")
public final class TextSelectionConf
{
    private static final Color _DEFAULT_COLOR = new Color(51, 153, 255, 90);
    private static final float _SATURATION_KEPT_WHILE_UNFOCUSED = 0.2f;
    private static final int   _NAVIGATE_WITH_KEYBOARD           = 1;
    private static final int   _SELECT_WORD_ON_DOUBLE_CLICK      = 1 << 1;
    private static final int   _SELECT_PARAGRAPH_ON_TRIPLE_CLICK = 1 << 2;
    private static final int   _SELECT_WORD_ON_LONG_PRESS        = 1 << 3;
    private static final int   _COPY_TO_CLIPBOARD                = 1 << 4;
    private static final int   _DEFAULT_BEHAVIOURS               = _NAVIGATE_WITH_KEYBOARD | _SELECT_WORD_ON_DOUBLE_CLICK | _SELECT_PARAGRAPH_ON_TRIPLE_CLICK | _COPY_TO_CLIPBOARD;
    private static final TextSelectionConf _NONE = new TextSelectionConf(null, _DEFAULT_COLOR, null, false, 0f, 0f, 0f, null, null, false, null, UI.CaretShape.BAR, 1f, 0f, true, _DEFAULT_BEHAVIOURS);

    static TextSelectionConf none() { return _NONE; }

    private final @Nullable TextSelection _range;
    private final Color           _color;
    private final @Nullable Color _unfocusedColor;
    private final boolean         _mergeLines;
    private final float           _convexRadius;
    private final float           _concaveRadius;
    private final float           _borderWidth;
    private final @Nullable Color _borderColor;
    private final @Nullable Color _unfocusedBorderColor;
    private final boolean         _showCaret;
    private final @Nullable Color _caretColor;
    private final UI.CaretShape   _caretShape;
    private final float           _caretWidth;
    private final float           _caretRadius;
    private final boolean         _useUnfocusedLook;
    private final int             _behaviours;

    private TextSelectionConf(
        @Nullable TextSelection range,
        Color                   color,
        @Nullable Color         unfocusedColor,
        boolean                 mergeLines,
        float                   convexRadius,
        float                   concaveRadius,
        float                   borderWidth,
        @Nullable Color         borderColor,
        @Nullable Color         unfocusedBorderColor,
        boolean                 showCaret,
        @Nullable Color         caretColor,
        UI.CaretShape           caretShape,
        float                   caretWidth,
        float                   caretRadius,
        boolean                 useUnfocusedLook,
        int                     behaviours
    ) {
        _range                = range;
        _color                = Objects.requireNonNull(color);
        _unfocusedColor       = unfocusedColor;
        _mergeLines           = mergeLines;
        _convexRadius         = convexRadius;
        _concaveRadius        = concaveRadius;
        _borderWidth          = borderWidth;
        _borderColor          = borderColor;
        _unfocusedBorderColor = unfocusedBorderColor;
        _showCaret            = showCaret;
        _caretColor           = caretColor;
        _caretShape           = Objects.requireNonNull(caretShape);
        _caretWidth           = caretWidth;
        _caretRadius          = caretRadius;
        _useUnfocusedLook     = useUnfocusedLook;
        _behaviours           = behaviours;
    }

    private static TextSelectionConf of(
        @Nullable TextSelection range,
        Color                   color,
        @Nullable Color         unfocusedColor,
        boolean                 mergeLines,
        float                   convexRadius,
        float                   concaveRadius,
        float                   borderWidth,
        @Nullable Color         borderColor,
        @Nullable Color         unfocusedBorderColor,
        boolean                 showCaret,
        @Nullable Color         caretColor,
        UI.CaretShape           caretShape,
        float                   caretWidth,
        float                   caretRadius,
        boolean                 useUnfocusedLook,
        int                     behaviours
    ) {
        final TextSelectionConf conf = new TextSelectionConf(range, color, unfocusedColor, mergeLines, convexRadius, concaveRadius, borderWidth, borderColor, unfocusedBorderColor, showCaret, caretColor, caretShape, caretWidth, caretRadius, useUnfocusedLook, behaviours);
        return conf.equals(_NONE) ? _NONE : conf;
    }

    Color color() { return _color; }

    boolean mergeLines() { return _mergeLines; }

    float convexRadius() { return _convexRadius; }

    float concaveRadius() { return _concaveRadius; }

    float borderWidth() { return _borderWidth; }

    Optional<Color> borderColor() { return Optional.ofNullable(_borderColor); }

    boolean showCaret() { return _showCaret; }

    Optional<Color> caretColor() { return Optional.ofNullable(_caretColor); }

    UI.CaretShape caretShape() { return _caretShape; }

    float caretWidth() { return _caretWidth; }

    float caretRadius() { return _caretRadius; }

    boolean navigateWithKeyboard() { return _does(_NAVIGATE_WITH_KEYBOARD); }

    boolean selectWordOnDoubleClick() { return _does(_SELECT_WORD_ON_DOUBLE_CLICK); }

    boolean selectParagraphOnTripleClick() { return _does(_SELECT_PARAGRAPH_ON_TRIPLE_CLICK); }

    boolean selectWordOnLongPress() { return _does(_SELECT_WORD_ON_LONG_PRESS); }

    boolean copyToClipboard() { return _does(_COPY_TO_CLIPBOARD); }

    private boolean _does( int behaviour ) { return ( _behaviours & behaviour ) != 0; }

    private TextSelectionConf _withBehaviour( int behaviour, boolean on ) {
        final int behaviours = on ? _behaviours | behaviour : _behaviours & ~behaviour;
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, behaviours);
    }

    TextSelectionConf caretGeometry() {
        return of(null, _DEFAULT_COLOR, null, false, 0f, 0f, 0f, null, null, false, null, _caretShape, _caretWidth, 0f, true, _DEFAULT_BEHAVIOURS);
    }

    @Nullable TextSelection rangeToPaint() {
        return _range == null || !_range.isEnabled() || ( _range.isEmpty() && !_showCaret ) ? null : _range;
    }

    TextSelectionConf unfocused() {
        if ( !_useUnfocusedLook )
            return this;
        final Color           unfocusedColor       = _unfocusedColor != null ? _unfocusedColor : _greyed(_color);
        final @Nullable Color unfocusedBorderColor = _borderColor == null ? null : _unfocusedBorderColor != null ? _unfocusedBorderColor : _greyed(_borderColor);
        return of(_range, unfocusedColor, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, unfocusedBorderColor, _unfocusedBorderColor, false, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    private static Color _greyed( Color color ) {
        final float grey = 0.299f * color.getRed() + 0.587f * color.getGreen() + 0.114f * color.getBlue();
        return new Color(
                    _towardsGrey(color.getRed(),   grey),
                    _towardsGrey(color.getGreen(), grey),
                    _towardsGrey(color.getBlue(),  grey),
                    color.getAlpha()
                );
    }

    private static int _towardsGrey( int channel, float grey ) {
        return Math.round(grey + (channel - grey) * _SATURATION_KEPT_WHILE_UNFOCUSED);
    }

    TextSelectionConf range( TextSelection range ) {
        return of(range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Sets the colour which SwingTree paints behind the selected characters.
     *  A partly transparent colour lets the background of the component shine through.
     *  Without this method, the colour is a partly transparent light blue.
     *
     * @param color The colour behind the selected characters.
     * @return A new selection configuration with the given colour.
     */
    public TextSelectionConf color( Color color ) {
        Objects.requireNonNull(color);
        return of(_range, color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Rounds every corner of the area which SwingTree paints behind the selected characters,
     *  with the same radius for the convex and the concave corners. It is the same as
     *  {@code convexRadius(radius).concaveRadius(radius)}, see {@link #convexRadius(double)}
     *  and {@link #concaveRadius(double)}. With a radius of 0, which is the default, the corners are square.
     *
     * @param radius The radius of the quarter circle of every corner, in pixels before the UI scale is applied.
     * @return A new selection configuration with the given corner radius.
     */
    public TextSelectionConf borderRadius( double radius ) {
        return convexRadius(radius).concaveRadius(radius);
    }

    /**
     *  Rounds the convex corners of the area which SwingTree paints behind the selected characters.
     *  A convex corner points out of the area, like each of the four corners of a rectangle.
     *  Without {@link #mergeLines(boolean)}, the area behind the selected characters of every line
     *  is a rectangle, so all of its corners are convex. With a border, the outer edge of the border
     *  has this radius at a convex corner, and the inner edge of the border has this radius minus the
     *  width of the border, or a square corner when the border is wider than the radius.
     *  With a radius of 0, which is the default, the convex corners are square.
     *
     * @param radius The radius of the quarter circle of every convex corner, in pixels before the UI scale is applied.
     * @return A new selection configuration with the given radius for convex corners.
     */
    public TextSelectionConf convexRadius( double radius ) {
        return of(_range, _color, _unfocusedColor, _mergeLines, (float) Math.max(0, radius), _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Rounds the concave corners of the shape which {@link #mergeLines(boolean)} makes from the
     *  selected parts of several lines. A concave corner points into the shape, like the corner where
     *  the end of a short line meets the longer line below it. With a radius above 0, SwingTree fills
     *  that corner with a curve, so the outline bends from the edge of the short line into the edge of
     *  the longer line. With a border, the outer edge of the border has this radius
     *  at a concave corner, and the inner edge of the border has this radius plus the width of the
     *  border, so the border is equally wide all along the corner. Without {@code mergeLines(true)},
     *  every area is a rectangle without concave corners, and this radius changes nothing.
     *  With a radius of 0, which is the default, the concave corners are square.
     *
     * @param radius The radius of the quarter circle of every concave corner, in pixels before the UI scale is applied.
     * @return A new selection configuration with the given radius for concave corners.
     */
    public TextSelectionConf concaveRadius( double radius ) {
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, (float) Math.max(0, radius), _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Sets whether SwingTree paints the selected parts of several lines as one shape.
     *  Without this, which is the default, SwingTree paints a separate rectangle behind the selected
     *  characters of every line, each with its own rounded corners and its own border. With
     *  {@code mergeLines(true)}, the rectangles of two neighbouring lines are joined into one shape
     *  where they overlap horizontally, and the border runs only along the outline of that shape, never
     *  between two lines. Rectangles which do not touch, for example the rectangles on the two sides
     *  of an obstacle on the same line, are not joined to each other, and the shape never covers an obstacle.
     *  The corners of the shape are rounded with {@link #convexRadius(double)} and {@link #concaveRadius(double)}.
     *
     * @param merge {@code true} to paint the selected parts of neighbouring lines as one shape.
     * @return A new selection configuration which joins the lines or not.
     */
    public TextSelectionConf mergeLines( boolean merge ) {
        return of(_range, _color, _unfocusedColor, merge, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Paints a border of the given width and colour along the inner edge of every area which
     *  SwingTree paints behind the selected characters of a line. The border takes its width from
     *  the area: the outer edge of the border is where the edge of the area would be without a border,
     *  so a selection with a border is exactly as large as a selection without one. The selection
     *  colour is painted only inside the border and never under it, so a partly transparent border
     *  colour shows the background of the component behind it. How the corners of the border are rounded
     *  is described in {@link #convexRadius(double)} and {@link #concaveRadius(double)}.
     *  A width of 0, which is the default, paints no border.
     *
     * @param width The width of the border, in pixels before the UI scale is applied.
     * @param color The colour of the border.
     * @return A new selection configuration with the given border.
     */
    public TextSelectionConf border( double width, Color color ) {
        return borderWidth(width).borderColor(color);
    }

    /**
     *  Sets the width of the border which SwingTree paints along the inner edge of the selection,
     *  see {@link #border(double, Color)}. The border is only painted when it also has a colour,
     *  which you set with {@link #borderColor(Color)}.
     *
     * @param width The width of the border, in pixels before the UI scale is applied.
     * @return A new selection configuration with the given border width.
     */
    public TextSelectionConf borderWidth( double width ) {
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, (float) Math.max(0, width), _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Sets the colour of the border which SwingTree paints along the inner edge of the selection,
     *  see {@link #border(double, Color)}. The border is only painted when it also has a width
     *  larger than 0, which you set with {@link #borderWidth(double)}.
     *
     * @param color The colour of the border.
     * @return A new selection configuration with the given border colour.
     */
    public TextSelectionConf borderColor( Color color ) {
        Objects.requireNonNull(color);
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, color, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Decides whether SwingTree paints the caret when no character is selected.
     *  The caret marks the gap where a click with the shift key held down would continue the selection.
     *  It is a thin vertical bar unless you choose another shape with {@link #caretShape(UI.CaretShape)}.
     *  It does not blink. By default, it is not painted,
     *  because most styled texts are labels and descriptions which nobody types into.
     *  From the moment the component loses the keyboard focus until it gets it back, SwingTree does not
     *  paint the caret either, unless you pass {@code false} to {@link #useUnfocusedLook(boolean)}.
     *
     * @param show {@code true} to paint the caret when no character is selected.
     * @return A new selection configuration which paints the caret or not.
     */
    public TextSelectionConf showCaret( boolean show ) {
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, show, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Sets the colour of the caret, which SwingTree paints when {@link #showCaret(boolean)}
     *  is enabled and no character is selected. Without this method, the caret has the colour
     *  of the font of the text, or black if the font has no single colour.
     *
     * @param color The colour of the caret.
     * @return A new selection configuration with the given caret colour.
     */
    public TextSelectionConf caretColor( Color color ) {
        Objects.requireNonNull(color);
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, color, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Sets the shape of the caret, which SwingTree paints when {@link #showCaret(boolean)} is enabled
     *  and no character is selected. A {@link UI.CaretShape#BAR} is a vertical bar in the gap,
     *  a {@link UI.CaretShape#BLOCK} covers the character behind the gap, and a {@link UI.CaretShape#UNDERLINE}
     *  is a line under that character. Behind the last character of a line, where no character follows
     *  the gap, a block and an underline are half as wide as the line is tall.
     *  SwingTree paints the caret behind the characters, so give a block a colour in which the character
     *  on top of it stays readable, see {@link #caretColor(Color)}.
     *  Without this method, the caret is a {@link UI.CaretShape#BAR}.
     *
     * @param shape The shape of the caret.
     * @return A new selection configuration with the given caret shape.
     */
    public TextSelectionConf caretShape( UI.CaretShape shape ) {
        Objects.requireNonNull(shape);
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, shape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Sets how thick the caret is, which SwingTree paints when {@link #showCaret(boolean)} is enabled
     *  and no character is selected. It is the width of a {@link UI.CaretShape#BAR}, which grows to
     *  the right of the gap, and the height of an {@link UI.CaretShape#UNDERLINE}, which grows upwards
     *  from the bottom of the line. A {@link UI.CaretShape#BLOCK} is as wide as the character it covers,
     *  so this width does not change it. However thin you make it, SwingTree paints the caret at least
     *  one pixel thick on the screen. Without this method, the caret is 1 pixel thick.
     *
     * @param width The thickness of the caret, in pixels before the UI scale is applied.
     * @return A new selection configuration with the given caret width.
     */
    public TextSelectionConf caretWidth( double width ) {
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, (float) Math.max(0, width), _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Rounds the corners of the caret, which SwingTree paints when {@link #showCaret(boolean)} is enabled
     *  and no character is selected. Every corner of the caret becomes a quarter circle with this radius,
     *  or with half the length of the shorter side of the caret when this radius is larger. So any radius of
     *  at least half of the {@link #caretWidth(double)} turns a {@link UI.CaretShape#BAR} into a thin pill
     *  with round ends, and an {@link UI.CaretShape#UNDERLINE} into a flat pill.
     *  The rounding only cuts the corners off, so the caret never reaches outside of
     *  {@link TextSelection#caretBounds()}. With a radius of 0, which is the default, the corners are square:
     *  <pre>{@code
     *  .selection( s -> s.showCaret(true).caretWidth(3).caretRadius(1.5) )
     *  }</pre>
     *
     * @param radius The radius of the quarter circle of every corner of the caret, in pixels before the UI scale is applied.
     * @return A new selection configuration with the given corner radius for the caret.
     */
    public TextSelectionConf caretRadius( double radius ) {
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, (float) Math.max(0, radius), _useUnfocusedLook, _behaviours);
    }

    /**
     *  Sets the colour which SwingTree paints behind the selected characters from the moment the component
     *  loses the keyboard focus until it gets it back. Many programs, like the text fields of web browsers
     *  and of macOS, grey a selection when the keyboard focus goes elsewhere, so that your users can tell
     *  at a glance where the keys they type go to:
     *  <pre>{@code
     *  .selection( s -> s
     *      .color(new Color(80, 140, 255, 90))
     *      .unfocusedColor(new Color(160, 160, 160, 90))
     *  )
     *  }</pre>
     *  A component which never had the keyboard focus looks focused.
     *  Without this method, SwingTree takes the colour of {@link #color(Color)} and removes most of its
     *  saturation while it keeps its transparency and its brightness, so the selection turns grey but stays
     *  as easy to see as before. With {@link #useUnfocusedLook(boolean) useUnfocusedLook(false)},
     *  SwingTree never paints this colour.
     *
     * @param color The colour behind the selected characters after the component lost the keyboard focus.
     * @return A new selection configuration with the given colour for an unfocused component.
     */
    public TextSelectionConf unfocusedColor( Color color ) {
        Objects.requireNonNull(color);
        return of(_range, _color, color, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Sets the colour of the border of the selection from the moment the component loses the keyboard
     *  focus until it gets it back, see {@link #border(double, Color)} and {@link #unfocusedColor(Color)}. This colour only
     *  replaces the colour of a border which SwingTree paints while the component has the focus, so a
     *  selection without a border colour or without a border width gets no border from it.
     *  Without this method, SwingTree takes the colour of {@link #borderColor(Color)} and removes most of
     *  its saturation while it keeps its transparency and its brightness.
     *
     * @param color The colour of the border after the component lost the keyboard focus.
     * @return A new selection configuration with the given border colour for an unfocused component.
     */
    public TextSelectionConf unfocusedBorderColor( Color color ) {
        Objects.requireNonNull(color);
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, color, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    /**
     *  Decides whether the selection looks different from the moment the component loses the keyboard focus
     *  until it gets it back. A component which never had the keyboard focus always looks focused. By default, which is {@code true}, SwingTree paints the selection in the colours of
     *  {@link #unfocusedColor(Color)} and {@link #unfocusedBorderColor(Color)} and paints no caret from the
     *  moment the component loses the keyboard focus until it gets it back. With {@code false}, the keyboard
     *  focus changes nothing that your users can see: the selection keeps the colours of {@link #color(Color)}
     *  and {@link #borderColor(Color)}, and the caret of {@link #showCaret(boolean)} stays where it is.
     *  This suits an application whose look should not change with the keyboard focus, and a test which
     *  compares pictures of the component taken while the keyboard focus is somewhere else.
     *
     * @param use {@code true} to grey the selection and hide the caret after the component lost the keyboard focus.
     * @return A new selection configuration which changes its look with the keyboard focus or not.
     */
    public TextSelectionConf useUnfocusedLook( boolean use ) {
        return of(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, use, _behaviours);
    }

    /**
     *  Decides whether your users can move the caret with the keyboard, like in a text editor.
     *  By default, which is {@code true}, the keys of a component made selectable with
     *  {@link swingtree.UIForAnySwing#withTextSelection(String, sprouts.Var)} do this:
     *  <ul>
     *      <li>The left and the right arrow key move the caret by one character. When characters are selected,
     *          they put the caret at the start or the end of the selected characters instead.</li>
     *      <li>With Ctrl held down, or Alt on macOS, the left and the right arrow key move the caret to the
     *          start of the word in front of it or to the end of the word behind it.</li>
     *      <li>The up and the down arrow key move the caret to the line above or below, to the gap nearest to
     *          the place where the caret was. When your users press them several times in a row, the caret
     *          stays near the place where the first of these presses started, even across shorter lines.
     *          On the first line, the up arrow key moves the caret to the start of the text,
     *          and on the last line, the down arrow key moves it to the end of the text.</li>
     *      <li>Home and End move the caret to the start or the end of the painted line which holds it,
     *          and with Ctrl held down, or Alt on macOS, to the start or the end of the text. In an empty
     *          paragraph, between two line breaks in a row, the start and the end of the line are the same
     *          gap, so a caret there stays where it is.</li>
     *  </ul>
     *  With the shift key held down, each of these keys keeps the anchor where it is and moves only the caret,
     *  so your users select the characters which the caret passes over. The keys only reach the component
     *  while it has the keyboard focus, which it gets when your users click into the text.
     *  With {@code false}, SwingTree leaves these keys alone, for example because your code moves the caret
     *  with keys of its own. The keyboard shortcuts for selecting everything and for copying work either way.
     *  <pre>{@code
     *  .selection( s -> s.showCaret(true).navigateWithKeyboard(false) )
     *  }</pre>
     *
     * @param navigate {@code true} to let the arrow keys, Home and End move the caret.
     * @return A new selection configuration which moves the caret with the keyboard or not.
     */
    public TextSelectionConf navigateWithKeyboard( boolean navigate ) {
        return _withBehaviour(_NAVIGATE_WITH_KEYBOARD, navigate);
    }

    /**
     *  Decides whether a double click selects the word under the mouse. A word is what
     *  {@link java.text.BreakIterator#getWordInstance()} finds around the character under the mouse, which
     *  follows the word rules of many languages, so "don't" is one word. The arrow keys with Ctrl held down,
     *  see {@link #navigateWithKeyboard(boolean)}, jump over words by the same rules.
     *  By default, which is {@code true}, a double click selects the word. With {@code false}, the second
     *  click of a double click does what a single click does: it puts the caret into the gap nearest to the mouse.
     *
     * @param select {@code true} to select the word under the mouse on a double click.
     * @return A new selection configuration which selects a word on a double click or not.
     */
    public TextSelectionConf selectWordOnDoubleClick( boolean select ) {
        return _withBehaviour(_SELECT_WORD_ON_DOUBLE_CLICK, select);
    }

    /**
     *  Decides whether a triple click selects the paragraph under the mouse. A paragraph is everything between
     *  two line breaks of the text, the {@code '\n'} characters, however many painted lines it wraps over.
     *  The line breaks themselves are not selected, so a paragraph which your users copy does not end with an
     *  empty line.
     *  By default, which is {@code true}, a triple click selects the paragraph. With {@code false}, the third
     *  click of a triple click does what a single click does: it puts the caret into the gap nearest to the mouse.
     *  <pre>{@code
     *  .selection( s -> s.selectParagraphOnTripleClick(false) )
     *  }</pre>
     *
     * @param select {@code true} to select the paragraph under the mouse on a triple click.
     * @return A new selection configuration which selects a paragraph on a triple click or not.
     */
    public TextSelectionConf selectParagraphOnTripleClick( boolean select ) {
        return _withBehaviour(_SELECT_PARAGRAPH_ON_TRIPLE_CLICK, select);
    }

    /**
     *  Decides whether holding the left mouse button down on a word, without moving the mouse, selects
     *  that word, like a long press of a finger on the screen of a phone. After about half a second
     *  the word under the mouse is selected. A movement of up to 5 pixels before then is allowed, because a
     *  finger never holds perfectly still. Like every size of a style, these 5 pixels are multiplied by the
     *  UI scale, so they are 10 pixels on the screen at a UI scale of 2. Moving further, releasing the button earlier, or the component
     *  losing the keyboard focus cancels it, and then a press only puts the caret, as it does without a long press.
     *  By default, which is {@code false}, holding the button down does nothing more than a click, because
     *  on a desktop your users select a word with a double click.
     *  <pre>{@code
     *  .selection( s -> s.selectWordOnLongPress(true) )
     *  }</pre>
     *
     * @param select {@code true} to select the word under the mouse when the button is held down without moving.
     * @return A new selection configuration which selects a word on a long press or not.
     */
    public TextSelectionConf selectWordOnLongPress( boolean select ) {
        return _withBehaviour(_SELECT_WORD_ON_LONG_PRESS, select);
    }

    /**
     *  Decides whether your users can copy the selected characters. By default, which is {@code true},
     *  Ctrl+C, or Command+C on macOS, puts the selected characters onto the clipboard of the computer, and so
     *  does the {@link javax.swing.TransferHandler} of the component when your code calls
     *  {@link javax.swing.TransferHandler#exportToClipboard(javax.swing.JComponent, java.awt.datatransfer.Clipboard, int)}.
     *  With {@code false}, nothing reaches any clipboard, and whatever is on the clipboard stays there, for
     *  example for a text which your users may read and select but should type themselves.
     *  Your users can still select the characters, also everything at once with Ctrl+A or Command+A.
     *
     * @param copy {@code true} to let your users copy the selected characters.
     * @return A new selection configuration which lets your users copy or not.
     */
    public TextSelectionConf copyToClipboard( boolean copy ) {
        return _withBehaviour(_COPY_TO_CLIPBOARD, copy);
    }

    TextSelectionConf _scale( double scale ) {
        if ( _convexRadius == 0f && _concaveRadius == 0f && _borderWidth == 0f && _caretRadius == 0f )
            return this;
        return of(_range, _color, _unfocusedColor, _mergeLines, (float) (_convexRadius * scale), (float) (_concaveRadius * scale), (float) (_borderWidth * scale), _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, (float) (_caretRadius * scale), _useUnfocusedLook, _behaviours);
    }

    @Override
    public boolean equals( Object other ) {
        if ( this == other )
            return true;
        if ( !(other instanceof TextSelectionConf) )
            return false;
        final TextSelectionConf that = (TextSelectionConf) other;
        return Objects.equals(_range, that._range) &&
               _color.equals(that._color) &&
               Objects.equals(_unfocusedColor, that._unfocusedColor) &&
               _mergeLines == that._mergeLines &&
               Float.floatToIntBits(_convexRadius)  == Float.floatToIntBits(that._convexRadius) &&
               Float.floatToIntBits(_concaveRadius) == Float.floatToIntBits(that._concaveRadius) &&
               Float.floatToIntBits(_borderWidth)  == Float.floatToIntBits(that._borderWidth) &&
               Objects.equals(_borderColor, that._borderColor) &&
               Objects.equals(_unfocusedBorderColor, that._unfocusedBorderColor) &&
               _showCaret == that._showCaret &&
               Objects.equals(_caretColor, that._caretColor) &&
               _caretShape == that._caretShape &&
               Float.floatToIntBits(_caretWidth) == Float.floatToIntBits(that._caretWidth) &&
               Float.floatToIntBits(_caretRadius) == Float.floatToIntBits(that._caretRadius) &&
               _useUnfocusedLook == that._useUnfocusedLook &&
               _behaviours == that._behaviours;
    }

    @Override
    public int hashCode() {
        return Objects.hash(_range, _color, _unfocusedColor, _mergeLines, _convexRadius, _concaveRadius, _borderWidth, _borderColor, _unfocusedBorderColor, _showCaret, _caretColor, _caretShape, _caretWidth, _caretRadius, _useUnfocusedLook, _behaviours);
    }

    @Override
    public String toString() {
        if ( this.equals(_NONE) )
            return getClass().getSimpleName() + "[NONE]";
        return getClass().getSimpleName() + "[" +
                    "range=" + _range + ", " +
                    "color=" + _color + ", " +
                    "unfocusedColor=" + _unfocusedColor + ", " +
                    "mergeLines=" + _mergeLines + ", " +
                    "convexRadius=" + _convexRadius + ", " +
                    "concaveRadius=" + _concaveRadius + ", " +
                    "borderWidth=" + _borderWidth + ", " +
                    "borderColor=" + _borderColor + ", " +
                    "unfocusedBorderColor=" + _unfocusedBorderColor + ", " +
                    "showCaret=" + _showCaret + ", " +
                    "caretColor=" + _caretColor + ", " +
                    "caretShape=" + _caretShape + ", " +
                    "caretWidth=" + _caretWidth + ", " +
                    "caretRadius=" + _caretRadius + ", " +
                    "useUnfocusedLook=" + _useUnfocusedLook + ", " +
                    "navigateWithKeyboard=" + navigateWithKeyboard() + ", " +
                    "selectWordOnDoubleClick=" + selectWordOnDoubleClick() + ", " +
                    "selectParagraphOnTripleClick=" + selectParagraphOnTripleClick() + ", " +
                    "selectWordOnLongPress=" + selectWordOnLongPress() + ", " +
                    "copyToClipboard=" + copyToClipboard() +
                "]";
    }
}