summaryrefslogtreecommitdiffstats
path: root/libjava/classpath/java/awt/font/TextLayout.java
diff options
context:
space:
mode:
Diffstat (limited to 'libjava/classpath/java/awt/font/TextLayout.java')
-rw-r--r--libjava/classpath/java/awt/font/TextLayout.java1015
1 files changed, 861 insertions, 154 deletions
diff --git a/libjava/classpath/java/awt/font/TextLayout.java b/libjava/classpath/java/awt/font/TextLayout.java
index b1473f25564..dc0e537eba9 100644
--- a/libjava/classpath/java/awt/font/TextLayout.java
+++ b/libjava/classpath/java/awt/font/TextLayout.java
@@ -38,18 +38,18 @@ exception statement from your version. */
package java.awt.font;
-import gnu.classpath.NotImplementedException;
-
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.text.CharacterIterator;
import java.text.AttributedCharacterIterator;
import java.text.Bidi;
+import java.util.ArrayList;
import java.util.Map;
/**
@@ -57,19 +57,86 @@ import java.util.Map;
*/
public final class TextLayout implements Cloneable
{
- private GlyphVector[] runs;
- private Font font;
+ /**
+ * Holds the layout data that belongs to one run of characters.
+ */
+ private class Run
+ {
+ /**
+ * The actual glyph vector.
+ */
+ GlyphVector glyphVector;
+
+ /**
+ * The font for this text run.
+ */
+ Font font;
+
+ /**
+ * The start of the run.
+ */
+ int runStart;
+
+ /**
+ * The end of the run.
+ */
+ int runEnd;
+
+ /**
+ * The layout location of the beginning of the run.
+ */
+ float location;
+
+ /**
+ * Initializes the Run instance.
+ *
+ * @param gv the glyph vector
+ * @param start the start index of the run
+ * @param end the end index of the run
+ */
+ Run(GlyphVector gv, Font f, int start, int end)
+ {
+ glyphVector = gv;
+ font = f;
+ runStart = start;
+ runEnd = end;
+ }
+
+ /**
+ * Returns <code>true</code> when this run is left to right,
+ * <code>false</code> otherwise.
+ *
+ * @return <code>true</code> when this run is left to right,
+ * <code>false</code> otherwise
+ */
+ boolean isLeftToRight()
+ {
+ return (glyphVector.getLayoutFlags() & GlyphVector.FLAG_RUN_RTL) == 0;
+ }
+ }
+
+ /**
+ * The laid out character runs.
+ */
+ private Run[] runs;
+
private FontRenderContext frc;
- private String string;
+ private char[] string;
+ private int offset;
+ private int length;
private Rectangle2D boundsCache;
private LineMetrics lm;
/**
- * Start and end character indices of the runs.
- * First index is the run number, second is 0 or 1 for the starting
- * and ending character index of the run, respectively.
+ * The total advance of this text layout. This is cache for maximum
+ * performance.
+ */
+ private float totalAdvance = -1F;
+
+ /**
+ * The cached natural bounds.
*/
- private int[][] runIndices;
+ private Rectangle2D naturalBounds;
/**
* Character indices.
@@ -88,66 +155,98 @@ public final class TextLayout implements Cloneable
private boolean hasWhitespace = false;
/**
+ * The {@link Bidi} object that is used for reordering and by
+ * {@link #getCharacterLevel(int)}.
+ */
+ private Bidi bidi;
+
+ /**
+ * Mpas the logical position of each individual character in the original
+ * string to its visual position.
+ */
+ private int[] logicalToVisual;
+
+ /**
+ * Maps visual positions of a character to its logical position
+ * in the original string.
+ */
+ private int[] visualToLogical;
+
+ /**
+ * The cached hashCode.
+ */
+ private int hash;
+
+ /**
* The default caret policy.
*/
- public static final TextLayout.CaretPolicy DEFAULT_CARET_POLICY = new CaretPolicy();
+ public static final TextLayout.CaretPolicy DEFAULT_CARET_POLICY =
+ new CaretPolicy();
/**
* Constructs a TextLayout.
*/
- public TextLayout (String string, Font font, FontRenderContext frc)
+ public TextLayout (String str, Font font, FontRenderContext frc)
{
- this.font = font;
this.frc = frc;
- this.string = string;
- lm = font.getLineMetrics(string, frc);
+ string = str.toCharArray();
+ offset = 0;
+ length = this.string.length;
+ lm = font.getLineMetrics(this.string, offset, length, frc);
// Get base direction and whitespace info
getStringProperties();
- if( Bidi.requiresBidi( string.toCharArray(), 0, string.length() ) )
+ if (Bidi.requiresBidi(string, offset, offset + length))
{
- Bidi bidi = new Bidi( string, leftToRight ?
- Bidi.DIRECTION_LEFT_TO_RIGHT :
- Bidi.DIRECTION_RIGHT_TO_LEFT );
+ bidi = new Bidi(str, leftToRight ? Bidi.DIRECTION_LEFT_TO_RIGHT
+ : Bidi.DIRECTION_RIGHT_TO_LEFT );
int rc = bidi.getRunCount();
byte[] table = new byte[ rc ];
for(int i = 0; i < table.length; i++)
table[i] = (byte)bidi.getRunLevel(i);
- runs = new GlyphVector[ rc ];
- runIndices = new int[rc][2];
- for(int i = 0; i < runs.length; i++)
+ runs = new Run[rc];
+ for(int i = 0; i < rc; i++)
{
- runIndices[i][0] = bidi.getRunStart( i );
- runIndices[i][1] = bidi.getRunLimit( i );
- if( runIndices[i][0] != runIndices[i][1] ) // no empty runs.
+ int start = bidi.getRunStart(i);
+ int end = bidi.getRunLimit(i);
+ if(start != end) // no empty runs.
{
- runs[i] = font.layoutGlyphVector
- ( frc, string.toCharArray(),
- runIndices[i][0], runIndices[i][1],
- ((table[i] & 1) == 0) ? Font.LAYOUT_LEFT_TO_RIGHT :
- Font.LAYOUT_RIGHT_TO_LEFT );
- }
+ GlyphVector gv = font.layoutGlyphVector(frc,
+ string, start, end,
+ ((table[i] & 1) == 0) ? Font.LAYOUT_LEFT_TO_RIGHT
+ : Font.LAYOUT_RIGHT_TO_LEFT );
+ runs[i] = new Run(gv, font, start, end);
+ }
}
Bidi.reorderVisually( table, 0, runs, 0, runs.length );
+ // Clean up null runs.
+ ArrayList cleaned = new ArrayList(rc);
+ for (int i = 0; i < rc; i++)
+ {
+ if (runs[i] != null)
+ cleaned.add(runs[i]);
+ }
+ runs = new Run[cleaned.size()];
+ runs = (Run[]) cleaned.toArray(runs);
}
else
{
- runs = new GlyphVector[ 1 ];
- runIndices = new int[1][2];
- runIndices[0][0] = 0;
- runIndices[0][1] = string.length();
- runs[ 0 ] = font.layoutGlyphVector( frc, string.toCharArray(),
- 0, string.length(),
- leftToRight ?
- Font.LAYOUT_LEFT_TO_RIGHT :
- Font.LAYOUT_RIGHT_TO_LEFT );
+ GlyphVector gv = font.layoutGlyphVector( frc, string, offset, length,
+ leftToRight ? Font.LAYOUT_LEFT_TO_RIGHT
+ : Font.LAYOUT_RIGHT_TO_LEFT );
+ Run run = new Run(gv, font, 0, length);
+ runs = new Run[]{ run };
}
setCharIndices();
+ setupMappings();
+ layoutRuns();
}
- public TextLayout (String string, Map attributes, FontRenderContext frc)
+ public TextLayout (String string,
+ Map<? extends AttributedCharacterIterator.Attribute, ?> attributes,
+ FontRenderContext frc)
{
this( string, new Font( attributes ), frc );
}
@@ -165,7 +264,6 @@ public final class TextLayout implements Cloneable
*/
TextLayout(TextLayout t, int startIndex, int endIndex)
{
- font = t.font;
frc = t.frc;
boundsCache = null;
lm = t.lm;
@@ -173,30 +271,35 @@ public final class TextLayout implements Cloneable
if( endIndex > t.getCharacterCount() )
endIndex = t.getCharacterCount();
- string = t.string.substring( startIndex, endIndex );
+ string = t.string;
+ offset = startIndex + offset;
+ length = endIndex - startIndex;
int startingRun = t.charIndices[startIndex][0];
int nRuns = 1 + t.charIndices[endIndex - 1][0] - startingRun;
- runIndices = new int[ nRuns ][2];
- runs = new GlyphVector[ nRuns ];
+ runs = new Run[nRuns];
for( int i = 0; i < nRuns; i++ )
{
- GlyphVector run = t.runs[ i + startingRun ];
+ Run run = t.runs[i + startingRun];
+ GlyphVector gv = run.glyphVector;
+ Font font = run.font;
// Copy only the relevant parts of the first and last runs.
int beginGlyphIndex = (i > 0) ? 0 : t.charIndices[startIndex][1];
- int numEntries = ( i < nRuns - 1) ? run.getNumGlyphs() :
+ int numEntries = ( i < nRuns - 1) ? gv.getNumGlyphs() :
1 + t.charIndices[endIndex - 1][1] - beginGlyphIndex;
- int[] codes = run.getGlyphCodes(beginGlyphIndex, numEntries, null);
- runs[ i ] = font.createGlyphVector( frc, codes );
- runIndices[ i ][0] = t.runIndices[i + startingRun][0] - startIndex;
- runIndices[ i ][1] = t.runIndices[i + startingRun][1] - startIndex;
+ int[] codes = gv.getGlyphCodes(beginGlyphIndex, numEntries, null);
+ gv = font.createGlyphVector(frc, codes);
+ runs[i] = new Run(gv, font, run.runStart - startIndex,
+ run.runEnd - startIndex);
}
- runIndices[ nRuns - 1 ][1] = endIndex - 1;
+ runs[nRuns - 1].runEnd = endIndex - 1;
setCharIndices();
+ setupMappings();
determineWhiteSpace();
+ layoutRuns();
}
private void setCharIndices()
@@ -207,16 +310,53 @@ public final class TextLayout implements Cloneable
for(int run = 0; run < runs.length; run++)
{
currentChar = -1;
- for( int gi = 0; gi < runs[ run ].getNumGlyphs(); gi++)
- {
- if( runs[ run ].getGlyphCharIndex( gi ) != currentChar )
- {
- charIndices[ i ][0] = run;
- charIndices[ i ][1] = gi;
- currentChar = runs[ run ].getGlyphCharIndex( gi );
- i++;
- }
- }
+ Run current = runs[run];
+ GlyphVector gv = current.glyphVector;
+ for( int gi = 0; gi < gv.getNumGlyphs(); gi++)
+ {
+ if( gv.getGlyphCharIndex( gi ) != currentChar )
+ {
+ charIndices[ i ][0] = run;
+ charIndices[ i ][1] = gi;
+ currentChar = gv.getGlyphCharIndex( gi );
+ i++;
+ }
+ }
+ }
+ }
+
+ /**
+ * Initializes the logicalToVisual and visualToLogial maps.
+ */
+ private void setupMappings()
+ {
+ int numChars = getCharacterCount();
+ logicalToVisual = new int[numChars];
+ visualToLogical = new int[numChars];
+ int lIndex = 0;
+ int vIndex = 0;
+ // We scan the runs in visual order and set the mappings accordingly.
+ for (int i = 0; i < runs.length; i++)
+ {
+ Run run = runs[i];
+ if (run.isLeftToRight())
+ {
+ for (lIndex = run.runStart; lIndex < run.runEnd; lIndex++)
+ {
+ logicalToVisual[lIndex] = vIndex;
+ visualToLogical[vIndex] = lIndex;
+ vIndex++;
+ }
+ }
+ else
+ {
+ for (lIndex = run.runEnd - 1; lIndex >= run.runStart; lIndex--)
+ {
+ logicalToVisual[lIndex] = vIndex;
+ visualToLogical[vIndex] = lIndex;
+ vIndex++;
+ }
+ }
}
}
@@ -253,11 +393,11 @@ public final class TextLayout implements Cloneable
private void getStringProperties()
{
boolean gotDirection = false;
- int i = 0;
-
+ int i = offset;
+ int endOffs = offset + length;
leftToRight = true;
- while( i < string.length() && !gotDirection )
- switch( Character.getDirectionality( string.charAt( i++ ) ) )
+ while( i < endOffs && !gotDirection )
+ switch( Character.getDirectionality(string[i++]) )
{
case Character.DIRECTIONALITY_LEFT_TO_RIGHT:
case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING:
@@ -280,28 +420,30 @@ public final class TextLayout implements Cloneable
{
// Determine if there's whitespace in the thing.
// Ignore trailing chars.
- int i = string.length() - 1;
+ int i = offset + length - 1;
hasWhitespace = false;
- while( i >= 0 && Character.isWhitespace( string.charAt(i) ) )
+ while( i >= offset && Character.isWhitespace( string[i] ) )
i--;
// Check the remaining chars
- while( i >= 0 )
- if( Character.isWhitespace( string.charAt(i--) ) )
+ while( i >= offset )
+ if( Character.isWhitespace( string[i--] ) )
hasWhitespace = true;
}
protected Object clone ()
{
- return new TextLayout( string, font, frc );
+ return new TextLayout( this, 0, length);
}
public void draw (Graphics2D g2, float x, float y)
{
for(int i = 0; i < runs.length; i++)
{
- g2.drawGlyphVector(runs[i], x, y);
- Rectangle2D r = runs[i].getLogicalBounds();
- x += r.getWidth();
+ Run run = runs[i];
+ GlyphVector gv = run.glyphVector;
+ g2.drawGlyphVector(gv, x, y);
+ Rectangle2D r = gv.getLogicalBounds();
+ x += r.getWidth();
}
}
@@ -326,9 +468,16 @@ public final class TextLayout implements Cloneable
public float getAdvance ()
{
- float totalAdvance = 0f;
- for(int i = 0; i < runs.length; i++)
- totalAdvance += runs[i].getLogicalBounds().getWidth();
+ if (totalAdvance == -1F)
+ {
+ totalAdvance = 0f;
+ for(int i = 0; i < runs.length; i++)
+ {
+ Run run = runs[i];
+ GlyphVector gv = run.glyphVector;
+ totalAdvance += gv.getLogicalBounds().getWidth();
+ }
+ }
return totalAdvance;
}
@@ -363,27 +512,33 @@ public final class TextLayout implements Cloneable
double advance = 0;
for( int i = 0; i < ri; i++ )
- advance += runs[i].getLogicalBounds().getWidth();
+ {
+ Run run = runs[i];
+ GlyphVector gv = run.glyphVector;
+ advance += gv.getLogicalBounds().getWidth();
+ }
for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ )
{
+ Run run = runs[i];
+ GlyphVector gv = run.glyphVector;
int dg;
if( i == charIndices[ secondEndpoint - 1 ][0] )
dg = charIndices[ secondEndpoint - 1][1];
else
- dg = runs[i].getNumGlyphs() - 1;
+ dg = gv.getNumGlyphs() - 1;
for( int j = 0; j <= dg; j++ )
{
- Rectangle2D r2 = (runs[i].getGlyphVisualBounds( j )).
+ Rectangle2D r2 = (gv.getGlyphVisualBounds( j )).
getBounds2D();
- Point2D p = runs[i].getGlyphPosition( j );
+ Point2D p = gv.getGlyphPosition( j );
r2.setRect( advance + r2.getX(), r2.getY(),
r2.getWidth(), r2.getHeight() );
gp.append(r2, false);
}
- advance += runs[i].getLogicalBounds().getWidth();
+ advance += gv.getLogicalBounds().getWidth();
}
return gp;
}
@@ -397,46 +552,138 @@ public final class TextLayout implements Cloneable
public float[] getCaretInfo (TextHitInfo hit)
{
- return getCaretInfo(hit, getBounds());
+ return getCaretInfo(hit, getNaturalBounds());
}
public float[] getCaretInfo (TextHitInfo hit, Rectangle2D bounds)
- throws NotImplementedException
{
- throw new Error ("not implemented");
+ float[] info = new float[2];
+ int index = hit.getCharIndex();
+ boolean leading = hit.isLeadingEdge();
+ // For the boundary cases we return the boundary runs.
+ Run run;
+
+ if (index >= length)
+ {
+ info[0] = getAdvance();
+ info[1] = 0;
+ }
+ else
+ {
+ if (index < 0)
+ {
+ run = runs[0];
+ index = 0;
+ leading = true;
+ }
+ else
+ run = findRunAtIndex(index);
+
+ int glyphIndex = index - run.runStart;
+ Shape glyphBounds = run.glyphVector.getGlyphLogicalBounds(glyphIndex);
+ Rectangle2D glyphRect = glyphBounds.getBounds2D();
+ if (isVertical())
+ {
+ if (leading)
+ info[0] = (float) glyphRect.getMinY();
+ else
+ info[0] = (float) glyphRect.getMaxY();
+ }
+ else
+ {
+ if (leading)
+ info[0] = (float) glyphRect.getMinX();
+ else
+ info[0] = (float) glyphRect.getMaxX();
+ }
+ info[0] += run.location;
+ info[1] = run.font.getItalicAngle();
+ }
+ return info;
}
- public Shape getCaretShape (TextHitInfo hit)
+ public Shape getCaretShape(TextHitInfo hit)
{
- return getCaretShape( hit, getBounds() );
+ return getCaretShape(hit, getBounds());
}
- public Shape getCaretShape (TextHitInfo hit, Rectangle2D bounds)
- throws NotImplementedException
+ public Shape getCaretShape(TextHitInfo hit, Rectangle2D bounds)
{
- throw new Error ("not implemented");
+ // TODO: Handle vertical shapes somehow.
+ float[] info = getCaretInfo(hit);
+ float x1 = info[0];
+ float y1 = (float) bounds.getMinY();
+ float x2 = info[0];
+ float y2 = (float) bounds.getMaxY();
+ if (info[1] != 0)
+ {
+ // Shift x1 and x2 according to the slope.
+ x1 -= y1 * info[1];
+ x2 -= y2 * info[1];
+ }
+ GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 2);
+ path.moveTo(x1, y1);
+ path.lineTo(x2, y2);
+ return path;
}
- public Shape[] getCaretShapes (int offset)
+ public Shape[] getCaretShapes(int offset)
{
- return getCaretShapes( offset, getBounds() );
+ return getCaretShapes(offset, getNaturalBounds());
}
- public Shape[] getCaretShapes (int offset, Rectangle2D bounds)
- throws NotImplementedException
+ public Shape[] getCaretShapes(int offset, Rectangle2D bounds)
{
- throw new Error ("not implemented");
+ return getCaretShapes(offset, bounds, DEFAULT_CARET_POLICY);
+ }
+
+ public Shape[] getCaretShapes(int offset, Rectangle2D bounds,
+ CaretPolicy policy)
+ {
+ // The RI returns a 2-size array even when there's only one
+ // shape in it.
+ Shape[] carets = new Shape[2];
+ TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
+ int caretHit1 = hitToCaret(hit1);
+ TextHitInfo hit2 = hit1.getOtherHit();
+ int caretHit2 = hitToCaret(hit2);
+ if (caretHit1 == caretHit2)
+ {
+ carets[0] = getCaretShape(hit1);
+ carets[1] = null; // The RI returns null in this seldom case.
+ }
+ else
+ {
+ Shape caret1 = getCaretShape(hit1);
+ Shape caret2 = getCaretShape(hit2);
+ TextHitInfo strong = policy.getStrongCaret(hit1, hit2, this);
+ if (strong == hit1)
+ {
+ carets[0] = caret1;
+ carets[1] = caret2;
+ }
+ else
+ {
+ carets[0] = caret2;
+ carets[1] = caret1;
+ }
+ }
+ return carets;
}
public int getCharacterCount ()
{
- return string.length();
+ return length;
}
public byte getCharacterLevel (int index)
- throws NotImplementedException
{
- throw new Error ("not implemented");
+ byte level;
+ if( bidi == null )
+ level = 0;
+ else
+ level = (byte) bidi.getLevelAt(index);
+ return level;
}
public float getDescent ()
@@ -481,19 +728,21 @@ public final class TextLayout implements Cloneable
double advance = 0;
for( int i = 0; i < ri; i++ )
- advance += runs[i].getLogicalBounds().getWidth();
+ advance += runs[i].glyphVector.getLogicalBounds().getWidth();
for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ )
{
+ Run run = runs[i];
+ GlyphVector gv = run.glyphVector;
int dg; // last index in this run to use.
if( i == charIndices[ secondEndpoint - 1 ][0] )
dg = charIndices[ secondEndpoint - 1][1];
else
- dg = runs[i].getNumGlyphs() - 1;
+ dg = gv.getNumGlyphs() - 1;
for(; gi <= dg; gi++ )
{
- Rectangle2D r2 = (runs[i].getGlyphLogicalBounds( gi )).
+ Rectangle2D r2 = (gv.getGlyphLogicalBounds( gi )).
getBounds2D();
if( r == null )
r = r2;
@@ -502,7 +751,7 @@ public final class TextLayout implements Cloneable
}
gi = 0; // reset glyph index into run for next run.
- advance += runs[i].getLogicalBounds().getWidth();
+ advance += gv.getLogicalBounds().getWidth();
}
return r;
@@ -510,33 +759,137 @@ public final class TextLayout implements Cloneable
public int[] getLogicalRangesForVisualSelection (TextHitInfo firstEndpoint,
TextHitInfo secondEndpoint)
- throws NotImplementedException
{
- throw new Error ("not implemented");
+ // Check parameters.
+ checkHitInfo(firstEndpoint);
+ checkHitInfo(secondEndpoint);
+
+ // Convert to visual and order correctly.
+ int start = hitToCaret(firstEndpoint);
+ int end = hitToCaret(secondEndpoint);
+ if (start > end)
+ {
+ // Swap start and end so that end >= start.
+ int temp = start;
+ start = end;
+ end = temp;
+ }
+
+ // Now walk through the visual indices and mark the included pieces.
+ boolean[] include = new boolean[length];
+ for (int i = start; i < end; i++)
+ {
+ include[visualToLogical[i]] = true;
+ }
+
+ // Count included runs.
+ int numRuns = 0;
+ boolean in = false;
+ for (int i = 0; i < length; i++)
+ {
+ if (include[i] != in) // At each run in/out point we toggle the in var.
+ {
+ in = ! in;
+ if (in) // At each run start we count up.
+ numRuns++;
+ }
+ }
+
+ // Put together the ranges array.
+ int[] ranges = new int[numRuns * 2];
+ int index = 0;
+ in = false;
+ for (int i = 0; i < length; i++)
+ {
+ if (include[i] != in)
+ {
+ ranges[index] = i;
+ index++;
+ in = ! in;
+ }
+ }
+ // If the last run ends at the very end, include that last bit too.
+ if (in)
+ ranges[index] = length;
+
+ return ranges;
+ }
+
+ public TextHitInfo getNextLeftHit(int offset)
+ {
+ return getNextLeftHit(offset, DEFAULT_CARET_POLICY);
}
- public TextHitInfo getNextLeftHit (int offset)
- throws NotImplementedException
+ public TextHitInfo getNextLeftHit(int offset, CaretPolicy policy)
{
- throw new Error ("not implemented");
+ if (policy == null)
+ throw new IllegalArgumentException("Null policy not allowed");
+ if (offset < 0 || offset > length)
+ throw new IllegalArgumentException("Offset out of bounds");
+
+ TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
+ TextHitInfo hit2 = hit1.getOtherHit();
+
+ TextHitInfo strong = policy.getStrongCaret(hit1, hit2, this);
+ TextHitInfo next = getNextLeftHit(strong);
+ TextHitInfo ret = null;
+ if (next != null)
+ {
+ TextHitInfo next2 = getVisualOtherHit(next);
+ ret = policy.getStrongCaret(next2, next, this);
+ }
+ return ret;
}
public TextHitInfo getNextLeftHit (TextHitInfo hit)
- throws NotImplementedException
{
- throw new Error ("not implemented");
+ checkHitInfo(hit);
+ int index = hitToCaret(hit);
+ TextHitInfo next = null;
+ if (index != 0)
+ {
+ index--;
+ next = caretToHit(index);
+ }
+ return next;
}
- public TextHitInfo getNextRightHit (int offset)
- throws NotImplementedException
+ public TextHitInfo getNextRightHit(int offset)
{
- throw new Error ("not implemented");
+ return getNextRightHit(offset, DEFAULT_CARET_POLICY);
}
- public TextHitInfo getNextRightHit (TextHitInfo hit)
- throws NotImplementedException
+ public TextHitInfo getNextRightHit(int offset, CaretPolicy policy)
{
- throw new Error ("not implemented");
+ if (policy == null)
+ throw new IllegalArgumentException("Null policy not allowed");
+ if (offset < 0 || offset > length)
+ throw new IllegalArgumentException("Offset out of bounds");
+
+ TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
+ TextHitInfo hit2 = hit1.getOtherHit();
+
+ TextHitInfo next = getNextRightHit(policy.getStrongCaret(hit1, hit2, this));
+ TextHitInfo ret = null;
+ if (next != null)
+ {
+ TextHitInfo next2 = getVisualOtherHit(next);
+ ret = policy.getStrongCaret(next2, next, this);
+ }
+ return ret;
+ }
+
+ public TextHitInfo getNextRightHit(TextHitInfo hit)
+ {
+ checkHitInfo(hit);
+ int index = hitToCaret(hit);
+ TextHitInfo next = null;
+ if (index < length)
+ {
+ index++;
+ next = caretToHit(index);
+ }
+ return next;
}
public Shape getOutline (AffineTransform tx)
@@ -545,8 +898,9 @@ public final class TextLayout implements Cloneable
GeneralPath gp = new GeneralPath();
for(int i = 0; i < runs.length; i++)
{
- gp.append( runs[i].getOutline( x, 0f ), false );
- Rectangle2D r = runs[i].getLogicalBounds();
+ GlyphVector gv = runs[i].glyphVector;
+ gp.append( gv.getOutline( x, 0f ), false );
+ Rectangle2D r = gv.getLogicalBounds();
x += r.getWidth();
}
if( tx != null )
@@ -562,27 +916,28 @@ public final class TextLayout implements Cloneable
return 0f;
// No trailing whitespace
- if( !Character.isWhitespace( string.charAt( string.length() -1 ) ) )
+ if( !Character.isWhitespace( string[offset + length - 1]) )
return getAdvance();
// Get length of all runs up to the last
for(int i = 0; i < runs.length - 1; i++)
- totalAdvance += runs[i].getLogicalBounds().getWidth();
+ totalAdvance += runs[i].glyphVector.getLogicalBounds().getWidth();
- int lastRun = runIndices[ runs.length - 1 ][0];
- int j = string.length() - 1;
- while( j >= lastRun && Character.isWhitespace( string.charAt( j ) ) ) j--;
+ int lastRun = runs[runs.length - 1].runStart;
+ int j = length - 1;
+ while( j >= lastRun && Character.isWhitespace( string[j] ) ) j--;
if( j < lastRun )
return totalAdvance; // entire last run is whitespace
int lastNonWSChar = j - lastRun;
j = 0;
- while( runs[ runs.length - 1 ].getGlyphCharIndex( j )
+ while( runs[ runs.length - 1 ].glyphVector.getGlyphCharIndex( j )
<= lastNonWSChar )
{
- totalAdvance += runs[ runs.length - 1 ].getGlyphLogicalBounds( j ).
- getBounds2D().getWidth();
+ totalAdvance += runs[ runs.length - 1 ].glyphVector
+ .getGlyphLogicalBounds( j )
+ .getBounds2D().getWidth();
j ++;
}
@@ -599,15 +954,129 @@ public final class TextLayout implements Cloneable
public Shape getVisualHighlightShape (TextHitInfo firstEndpoint,
TextHitInfo secondEndpoint,
Rectangle2D bounds)
- throws NotImplementedException
{
- throw new Error ("not implemented");
+ GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
+ Shape caret1 = getCaretShape(firstEndpoint, bounds);
+ path.append(caret1, false);
+ Shape caret2 = getCaretShape(secondEndpoint, bounds);
+ path.append(caret2, false);
+ // Append left (top) bounds to selection if necessary.
+ int c1 = hitToCaret(firstEndpoint);
+ int c2 = hitToCaret(secondEndpoint);
+ if (c1 == 0 || c2 == 0)
+ {
+ path.append(left(bounds), false);
+ }
+ // Append right (bottom) bounds if necessary.
+ if (c1 == length || c2 == length)
+ {
+ path.append(right(bounds), false);
+ }
+ return path.getBounds2D();
+ }
+
+ /**
+ * Returns the shape that makes up the left (top) edge of this text layout.
+ *
+ * @param b the bounds
+ *
+ * @return the shape that makes up the left (top) edge of this text layout
+ */
+ private Shape left(Rectangle2D b)
+ {
+ GeneralPath left = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
+ left.append(getCaretShape(TextHitInfo.beforeOffset(0)), false);
+ if (isVertical())
+ {
+ float y = (float) b.getMinY();
+ left.append(new Line2D.Float((float) b.getMinX(), y,
+ (float) b.getMaxX(), y), false);
+ }
+ else
+ {
+ float x = (float) b.getMinX();
+ left.append(new Line2D.Float(x, (float) b.getMinY(),
+ x, (float) b.getMaxY()), false);
+ }
+ return left.getBounds2D();
+ }
+
+ /**
+ * Returns the shape that makes up the right (bottom) edge of this text
+ * layout.
+ *
+ * @param b the bounds
+ *
+ * @return the shape that makes up the right (bottom) edge of this text
+ * layout
+ */
+ private Shape right(Rectangle2D b)
+ {
+ GeneralPath right = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
+ right.append(getCaretShape(TextHitInfo.afterOffset(length)), false);
+ if (isVertical())
+ {
+ float y = (float) b.getMaxY();
+ right.append(new Line2D.Float((float) b.getMinX(), y,
+ (float) b.getMaxX(), y), false);
+ }
+ else
+ {
+ float x = (float) b.getMaxX();
+ right.append(new Line2D.Float(x, (float) b.getMinY(),
+ x, (float) b.getMaxY()), false);
+ }
+ return right.getBounds2D();
}
public TextHitInfo getVisualOtherHit (TextHitInfo hit)
- throws NotImplementedException
{
- throw new Error ("not implemented");
+ checkHitInfo(hit);
+ int hitIndex = hit.getCharIndex();
+
+ int index;
+ boolean leading;
+ if (hitIndex == -1 || hitIndex == length)
+ {
+ // Boundary case.
+ int visual;
+ if (isLeftToRight() == (hitIndex == -1))
+ visual = 0;
+ else
+ visual = length - 1;
+ index = visualToLogical[visual];
+ if (isLeftToRight() == (hitIndex == -1))
+ leading = isCharacterLTR(index); // LTR.
+ else
+ leading = ! isCharacterLTR(index); // RTL.
+ }
+ else
+ {
+ // Normal case.
+ int visual = logicalToVisual[hitIndex];
+ boolean b;
+ if (isCharacterLTR(hitIndex) == hit.isLeadingEdge())
+ {
+ visual--;
+ b = false;
+ }
+ else
+ {
+ visual++;
+ b = true;
+ }
+ if (visual >= 0 && visual < length)
+ {
+ index = visualToLogical[visual];
+ leading = b == isLeftToRight();
+ }
+ else
+ {
+ index = b == isLeftToRight() ? length : -1;
+ leading = index == length;
+ }
+ }
+ return leading ? TextHitInfo.leading(index) : TextHitInfo.trailing(index);
}
/**
@@ -622,16 +1091,19 @@ public final class TextLayout implements Cloneable
int nglyphs = 0; // # of whitespace chars
// determine last non-whitespace char.
- int lastNWS = string.length() - 1;
- while( Character.isWhitespace( string.charAt( lastNWS ) ) ) lastNWS--;
+ int lastNWS = offset + length - 1;
+ while( Character.isWhitespace( string[lastNWS] ) ) lastNWS--;
// locations of the glyphs.
- int[] wsglyphs = new int[string.length() * 10];
+ int[] wsglyphs = new int[length * 10];
for(int run = 0; run < runs.length; run++ )
- for(int i = 0; i < runs[run].getNumGlyphs(); i++ )
+ {
+ Run current = runs[run];
+ for(int i = 0; i < current.glyphVector.getNumGlyphs(); i++ )
{
- int cindex = runIndices[run][0] + runs[run].getGlyphCharIndex( i );
- if( Character.isWhitespace( string.charAt( cindex ) ) )
+ int cindex = current.runStart
+ + current.glyphVector.getGlyphCharIndex( i );
+ if( Character.isWhitespace( string[cindex] ) )
// && cindex < lastNWS )
{
wsglyphs[ nglyphs * 2 ] = run;
@@ -639,34 +1111,144 @@ public final class TextLayout implements Cloneable
nglyphs++;
}
}
-
+ }
deltaW = deltaW / nglyphs; // Change in width per whitespace glyph
double w = 0;
int cws = 0;
// Shift all characters
for(int run = 0; run < runs.length; run++ )
- for(int i = 0; i < runs[ run ].getNumGlyphs(); i++ )
- {
- if( wsglyphs[ cws * 2 ] == run && wsglyphs[ cws * 2 + 1 ] == i )
- {
- cws++; // update 'current whitespace'
- w += deltaW; // increment the shift
- }
- Point2D p = runs[ run ].getGlyphPosition( i );
- p.setLocation( p.getX() + w, p.getY() );
- runs[ run ].setGlyphPosition( i, p );
- }
+ {
+ Run current = runs[run];
+ for(int i = 0; i < current.glyphVector.getNumGlyphs(); i++ )
+ {
+ if( wsglyphs[ cws * 2 ] == run && wsglyphs[ cws * 2 + 1 ] == i )
+ {
+ cws++; // update 'current whitespace'
+ w += deltaW; // increment the shift
+ }
+ Point2D p = current.glyphVector.getGlyphPosition( i );
+ p.setLocation( p.getX() + w, p.getY() );
+ current.glyphVector.setGlyphPosition( i, p );
+ }
+ }
}
public TextHitInfo hitTestChar (float x, float y)
{
- return hitTestChar(x, y, getBounds());
+ return hitTestChar(x, y, getNaturalBounds());
}
+ /**
+ * Finds the character hit at the specified point. This 'clips' this
+ * text layout against the specified <code>bounds</code> rectangle. That
+ * means that in the case where a point is outside these bounds, this method
+ * returns the leading edge of the first character or the trailing edge of
+ * the last character.
+ *
+ * @param x the X location to test
+ * @param y the Y location to test
+ * @param bounds the bounds to test against
+ *
+ * @return the character hit at the specified point
+ */
public TextHitInfo hitTestChar (float x, float y, Rectangle2D bounds)
- throws NotImplementedException
{
- throw new Error ("not implemented");
+ // Check bounds.
+ if (isVertical())
+ {
+ if (y < bounds.getMinY())
+ return TextHitInfo.leading(0);
+ else if (y > bounds.getMaxY())
+ return TextHitInfo.trailing(getCharacterCount() - 1);
+ }
+ else
+ {
+ if (x < bounds.getMinX())
+ return TextHitInfo.leading(0);
+ else if (x > bounds.getMaxX())
+ return TextHitInfo.trailing(getCharacterCount() - 1);
+ }
+
+ TextHitInfo hitInfo = null;
+ if (isVertical())
+ {
+ // Search for the run at the location.
+ // TODO: Perform binary search for maximum efficiency. However, we
+ // need the run location laid out statically to do that.
+ int numRuns = runs.length;
+ Run hitRun = null;
+ for (int i = 0; i < numRuns && hitRun == null; i++)
+ {
+ Run run = runs[i];
+ Rectangle2D lBounds = run.glyphVector.getLogicalBounds();
+ if (lBounds.getMinY() + run.location <= y
+ && lBounds.getMaxY() + run.location >= y)
+ hitRun = run;
+ }
+ // Now we have (hopefully) found a run that hits. Now find the
+ // right character.
+ if (hitRun != null)
+ {
+ GlyphVector gv = hitRun.glyphVector;
+ for (int i = hitRun.runStart;
+ i < hitRun.runEnd && hitInfo == null; i++)
+ {
+ int gi = i - hitRun.runStart;
+ Rectangle2D lBounds = gv.getGlyphLogicalBounds(gi)
+ .getBounds2D();
+ if (lBounds.getMinY() + hitRun.location <= y
+ && lBounds.getMaxY() + hitRun.location >= y)
+ {
+ // Found hit. Now check if we are leading or trailing.
+ boolean leading = true;
+ if (lBounds.getCenterY() + hitRun.location <= y)
+ leading = false;
+ hitInfo = leading ? TextHitInfo.leading(i)
+ : TextHitInfo.trailing(i);
+ }
+ }
+ }
+ }
+ else
+ {
+ // Search for the run at the location.
+ // TODO: Perform binary search for maximum efficiency. However, we
+ // need the run location laid out statically to do that.
+ int numRuns = runs.length;
+ Run hitRun = null;
+ for (int i = 0; i < numRuns && hitRun == null; i++)
+ {
+ Run run = runs[i];
+ Rectangle2D lBounds = run.glyphVector.getLogicalBounds();
+ if (lBounds.getMinX() + run.location <= x
+ && lBounds.getMaxX() + run.location >= x)
+ hitRun = run;
+ }
+ // Now we have (hopefully) found a run that hits. Now find the
+ // right character.
+ if (hitRun != null)
+ {
+ GlyphVector gv = hitRun.glyphVector;
+ for (int i = hitRun.runStart;
+ i < hitRun.runEnd && hitInfo == null; i++)
+ {
+ int gi = i - hitRun.runStart;
+ Rectangle2D lBounds = gv.getGlyphLogicalBounds(gi)
+ .getBounds2D();
+ if (lBounds.getMinX() + hitRun.location <= x
+ && lBounds.getMaxX() + hitRun.location >= x)
+ {
+ // Found hit. Now check if we are leading or trailing.
+ boolean leading = true;
+ if (lBounds.getCenterX() + hitRun.location <= x)
+ leading = false;
+ hitInfo = leading ? TextHitInfo.leading(i)
+ : TextHitInfo.trailing(i);
+ }
+ }
+ }
+ }
+ return hitInfo;
}
public boolean isLeftToRight ()
@@ -680,18 +1262,127 @@ public final class TextLayout implements Cloneable
}
public int hashCode ()
- throws NotImplementedException
{
- throw new Error ("not implemented");
+ // This is implemented in sync to equals().
+ if (hash == 0 && runs.length > 0)
+ {
+ hash = runs.length;
+ for (int i = 0; i < runs.length; i++)
+ hash ^= runs[i].glyphVector.hashCode();
+ }
+ return hash;
}
public String toString ()
{
- return "TextLayout [string:"+string+", Font:"+font+" Rendercontext:"+
+ return "TextLayout [string:"+ new String(string, offset, length)
+ +" Rendercontext:"+
frc+"]";
}
/**
+ * Returns the natural bounds of that text layout. This is made up
+ * of the ascent plus descent and the text advance.
+ *
+ * @return the natural bounds of that text layout
+ */
+ private Rectangle2D getNaturalBounds()
+ {
+ if (naturalBounds == null)
+ naturalBounds = new Rectangle2D.Float(0.0F, -getAscent(), getAdvance(),
+ getAscent() + getDescent());
+ return naturalBounds;
+ }
+
+ private void checkHitInfo(TextHitInfo hit)
+ {
+ if (hit == null)
+ throw new IllegalArgumentException("Null hit info not allowed");
+ int index = hit.getInsertionIndex();
+ if (index < 0 || index > length)
+ throw new IllegalArgumentException("Hit index out of range");
+ }
+
+ private int hitToCaret(TextHitInfo hit)
+ {
+ int index = hit.getCharIndex();
+ int ret;
+ if (index < 0)
+ ret = isLeftToRight() ? 0 : length;
+ else if (index >= length)
+ ret = isLeftToRight() ? length : 0;
+ else
+ {
+ ret = logicalToVisual[index];
+ if (hit.isLeadingEdge() != isCharacterLTR(index))
+ ret++;
+ }
+ return ret;
+ }
+
+ private TextHitInfo caretToHit(int index)
+ {
+ TextHitInfo hit;
+ if (index == 0 || index == length)
+ {
+ if ((index == length) == isLeftToRight())
+ hit = TextHitInfo.leading(length);
+ else
+ hit = TextHitInfo.trailing(-1);
+ }
+ else
+ {
+ int logical = visualToLogical[index];
+ boolean leading = isCharacterLTR(logical); // LTR.
+ hit = leading ? TextHitInfo.leading(logical)
+ : TextHitInfo.trailing(logical);
+ }
+ return hit;
+ }
+
+ private boolean isCharacterLTR(int index)
+ {
+ byte level = getCharacterLevel(index);
+ return (level & 1) == 0;
+ }
+
+ /**
+ * Finds the run that holds the specified (logical) character index. This
+ * returns <code>null</code> when the index is not inside the range.
+ *
+ * @param index the index of the character to find
+ *
+ * @return the run that holds the specified character
+ */
+ private Run findRunAtIndex(int index)
+ {
+ Run found = null;
+ // TODO: Can we do better than linear searching here?
+ for (int i = 0; i < runs.length && found == null; i++)
+ {
+ Run run = runs[i];
+ if (run.runStart <= index && run.runEnd > index)
+ found = run;
+ }
+ return found;
+ }
+
+ /**
+ * Computes the layout locations for each run.
+ */
+ private void layoutRuns()
+ {
+ float loc = 0.0F;
+ float lastWidth = 0.0F;
+ for (int i = 0; i < runs.length; i++)
+ {
+ runs[i].location = loc;
+ Rectangle2D bounds = runs[i].glyphVector.getLogicalBounds();
+ loc += isVertical() ? bounds.getHeight() : bounds.getWidth();
+ }
+ }
+
+ /**
* Inner class describing a caret policy
*/
public static class CaretPolicy
@@ -703,9 +1394,25 @@ public final class TextLayout implements Cloneable
public TextHitInfo getStrongCaret(TextHitInfo hit1,
TextHitInfo hit2,
TextLayout layout)
- throws NotImplementedException
{
- throw new Error ("not implemented");
+ byte l1 = layout.getCharacterLevel(hit1.getCharIndex());
+ byte l2 = layout.getCharacterLevel(hit2.getCharIndex());
+ TextHitInfo strong;
+ if (l1 == l2)
+ {
+ if (hit2.isLeadingEdge() && ! hit1.isLeadingEdge())
+ strong = hit2;
+ else
+ strong = hit1;
+ }
+ else
+ {
+ if (l1 < l2)
+ strong = hit1;
+ else
+ strong = hit2;
+ }
+ return strong;
}
}
}
OpenPOWER on IntegriCloud