CaretTable.java
package swingtree.style;
import com.google.errorprone.annotations.Immutable;
import org.jspecify.annotations.Nullable;
import swingtree.UI;
import swingtree.layout.Bounds;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@Immutable
@SuppressWarnings("Immutable")
final class CaretTable
{
static final CaretTable EMPTY = new CaretTable("", "", Collections.emptyList(), UI.CaretShape.BAR, 1f);
private final String _component;
private final String _text;
private final List<Line> _lines;
private final UI.CaretShape _caretShape;
private final float _caretWidth;
private final int _hashCode;
private CaretTable( String component, String text, List<Line> lines, UI.CaretShape caretShape, float caretWidth ) {
_component = component;
_text = text;
_lines = lines;
_caretShape = caretShape;
_caretWidth = caretWidth;
_hashCode = Objects.hash(component, text, lines, caretShape, caretWidth);
}
static CaretTable of( String component, String text, List<TextLayoutEngine.PlacedFragment> fragments, TextSelectionConf look ) {
final List<Line> lines = new ArrayList<>(fragments.size());
for ( TextLayoutEngine.PlacedFragment fragment : fragments ) {
final float[] xs = new float[fragment.charEnd() - fragment.charStart + 1];
for ( int i = 0; i < xs.length; i++ )
xs[i] = fragment.xAt(fragment.charStart + i);
lines.add(new Line(fragment.charStart, fragment.top(), fragment.bottom(), xs));
}
return new CaretTable(component, text, Collections.unmodifiableList(lines), look.caretShape(), look.caretWidth());
}
static List<Rectangle2D> areasBetween( List<TextLayoutEngine.PlacedFragment> fragments, int start, int end ) {
final List<Rectangle2D> areas = new ArrayList<>();
for ( TextLayoutEngine.PlacedFragment fragment : fragments ) {
final int from = Math.max(start, fragment.charStart);
final int to = Math.min(end, fragment.charEnd());
if ( from < to ) {
final float fromX = fragment.xAt(from);
final float toX = fragment.xAt(to);
areas.add(new Rectangle2D.Float(Math.min(fromX, toX), fragment.top(), Math.abs(toX - fromX), fragment.bottom() - fragment.top()));
}
}
return areas;
}
static Optional<Bounds> caretBoundsOf( List<TextLayoutEngine.PlacedFragment> fragments, int position, boolean atLineEnd, TextSelectionConf look ) {
final TextLayoutEngine.@Nullable PlacedFragment holder = _holderOf(fragments, position, atLineEnd);
if ( holder == null )
return Optional.empty();
final float nextX = position < holder.charEnd() ? holder.xAt(position + 1) : Float.NaN;
return Optional.of(_caretBounds(look.caretShape(), look.caretWidth(), holder.xAt(position), nextX, holder.top(), holder.bottom()));
}
private static TextLayoutEngine.@Nullable PlacedFragment _holderOf( List<TextLayoutEngine.PlacedFragment> fragments, int position, boolean atLineEnd ) {
if ( atLineEnd )
for ( TextLayoutEngine.PlacedFragment fragment : fragments )
if ( fragment.charStart < position && position == fragment.charEnd() )
return fragment;
TextLayoutEngine.PlacedFragment holder = null;
for ( TextLayoutEngine.PlacedFragment fragment : fragments ) {
if ( fragment.charStart <= position && position < fragment.charEnd() )
return fragment;
if ( fragment.charEnd() == position )
holder = fragment;
}
return holder;
}
static float thicknessOf( float caretWidth ) {
return Math.max(1f, caretWidth * UI.scale());
}
private static Bounds _caretBounds( UI.CaretShape shape, float caretWidth, float x, float nextX, float top, float bottom ) {
final float height = bottom - top;
final float advance = Float.isNaN(nextX) ? 0 : Math.abs(nextX - x);
final float width = shape == UI.CaretShape.BAR ? thicknessOf(caretWidth) : advance > 0 ? advance : height / 2;
return Bounds.of(Math.round(x), top, width, height);
}
String component() { return _component; }
String text() { return _text; }
Optional<Bounds> boundsOf( int position, boolean atLineEnd ) {
final int line = lineOf(position, atLineEnd);
return line < 0 ? Optional.empty() : _boundsOf(_lines.get(line), position);
}
int lineOf( int position, boolean atLineEnd ) {
if ( atLineEnd )
for ( int i = 0; i < _lines.size(); i++ )
if ( _lines.get(i).endsAt(position) )
return i;
int holder = -1;
for ( int i = 0; i < _lines.size(); i++ ) {
final Line line = _lines.get(i);
if ( line.charStart() <= position && position < line.charEnd() )
return i;
if ( line.charEnd() == position )
holder = i;
}
return holder;
}
int startOfLine( int line ) { return _lines.get(line).charStart(); }
int endOfLine( int line ) { return _lines.get(line).charEnd(); }
float xOf( int line, int position ) { return _lines.get(line).xAt(position); }
int positionOnLine( int line, float x ) {
final Line holder = _lines.get(line);
return holder.charStart() + holder.gapNearest(x);
}
boolean endsLine( int line, int position ) { return _lines.get(line).endsAt(position); }
int lineNextTo( int line, int direction, float x ) {
final Line origin = _lines.get(line);
final float originMiddle = origin.middle();
int nearest = -1;
float nearestDy = Float.MAX_VALUE;
float nearestDx = Float.MAX_VALUE;
for ( int i = 0; i < _lines.size(); i++ ) {
final Line candidate = _lines.get(i);
final boolean onRequiredSide = direction < 0 ? candidate.middle() < origin.top() : candidate.middle() > origin.bottom();
if ( !onRequiredSide )
continue;
final float dy = Math.abs(candidate.middle() - originMiddle);
final float dx = _distance(x, candidate.left(), candidate.right());
if ( dy < nearestDy || ( dy == nearestDy && dx < nearestDx ) ) {
nearest = i;
nearestDy = dy;
nearestDx = dx;
}
}
return nearest;
}
boolean isLineEnd( int position ) {
for ( Line line : _lines )
if ( line.endsAt(position) )
return true;
return false;
}
boolean endsLineAt( float x, float y ) {
final Line line = _lineNearest(x, y);
return line != null && line.endsAt(line.charStart() + line.gapNearest(x));
}
private Optional<Bounds> _boundsOf( Line line, int position ) {
final float nextX = position < line.charEnd() ? line.xAt(position + 1) : Float.NaN;
return Optional.of(_caretBounds(_caretShape, _caretWidth, line.xAt(position), nextX, line.top(), line.bottom()));
}
int positionAt( float x, float y ) {
final Line line = _lineNearest(x, y);
return line == null ? -1 : line.charStart() + line.gapNearest(x);
}
int characterAt( float x, float y ) {
final Line line = _lineNearest(x, y);
return line == null ? -1 : line.charStart() + line.characterAt(x);
}
boolean isOverText( float x, float y ) {
for ( Line line : _lines )
if ( _distance(y, line.top(), line.bottom()) == 0 && _distance(x, line.left(), line.right()) == 0 )
return true;
return false;
}
private @Nullable Line _lineNearest( float x, float y ) {
Line nearest = null;
float nearestDy = Float.MAX_VALUE;
float nearestDx = Float.MAX_VALUE;
for ( Line line : _lines ) {
final float dy = _distance(y, line.top(), line.bottom());
final float dx = _distance(x, line.left(), line.right());
if ( dy < nearestDy || ( dy == nearestDy && dx < nearestDx ) ) {
nearest = line;
nearestDy = dy;
nearestDx = dx;
}
}
return nearest;
}
private static float _distance( float value, float from, float to ) {
return value < from ? from - value : value > to ? value - to : 0;
}
@Override
public boolean equals( Object other ) {
if ( this == other )
return true;
if ( !(other instanceof CaretTable) )
return false;
final CaretTable that = (CaretTable) other;
return _hashCode == that._hashCode &&
_caretShape == that._caretShape &&
Float.floatToIntBits(_caretWidth) == Float.floatToIntBits(that._caretWidth) &&
_component.equals(that._component) &&
_text.equals(that._text) &&
_lines.equals(that._lines);
}
@Override
public int hashCode() {
return _hashCode;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[component=" + _component + ", lines=" + _lines + ", caretShape=" + _caretShape + ", caretWidth=" + _caretWidth + "]";
}
@Immutable
@SuppressWarnings("Immutable")
private static final class Line
{
private final int _charStart;
private final float _top;
private final float _bottom;
private final float[] _xs;
private final float _left;
private final float _right;
Line( int charStart, float top, float bottom, float[] xs ) {
float left = xs[0];
float right = xs[0];
for ( float x : xs ) {
left = Math.min(left, x);
right = Math.max(right, x);
}
_charStart = charStart;
_top = top;
_bottom = bottom;
_xs = xs;
_left = left;
_right = right;
}
int charStart() { return _charStart; }
int charEnd() { return _charStart + _xs.length - 1; }
float top() { return _top; }
float bottom() { return _bottom; }
float middle() { return ( _top + _bottom ) / 2; }
boolean endsAt( int position ) { return _charStart < position && position == charEnd(); }
float left() { return _left; }
float right() { return _right; }
float xAt( int position ) { return _xs[position - _charStart]; }
int gapNearest( float x ) {
int nearest = 0;
for ( int i = 1; i < _xs.length; i++ )
if ( Math.abs(_xs[i] - x) < Math.abs(_xs[nearest] - x) )
nearest = i;
return nearest;
}
int characterAt( float x ) {
int nearest = 0;
float nearestDistance = Float.MAX_VALUE;
for ( int i = 0; i < _xs.length - 1; i++ ) {
final float distance = _distance(x, Math.min(_xs[i], _xs[i + 1]), Math.max(_xs[i], _xs[i + 1]));
if ( distance < nearestDistance ) {
nearest = i;
nearestDistance = distance;
}
}
return nearest;
}
@Override
public boolean equals( Object other ) {
if ( !(other instanceof Line) )
return false;
final Line that = (Line) other;
return _charStart == that._charStart &&
Float.floatToIntBits(_top) == Float.floatToIntBits(that._top) &&
Float.floatToIntBits(_bottom) == Float.floatToIntBits(that._bottom) &&
Arrays.equals(_xs, that._xs);
}
@Override
public int hashCode() {
return Objects.hash(_charStart, _top, _bottom, Arrays.hashCode(_xs));
}
@Override
public String toString() {
return "Line[charStart=" + _charStart + ", top=" + _top + ", bottom=" + _bottom + ", xs=" + Arrays.toString(_xs) + "]";
}
}
}