FileDocCategorySizeDatePackage
TextLayout.javaAPI DocJava SE 5 API102502Fri Aug 26 14:56:52 BST 2005java.awt.font

TextLayout

public final class TextLayout extends Object implements Cloneable
TextLayout is an immutable graphical representation of styled character data.

It provides the following capabilities:

  • implicit bidirectional analysis and reordering,
  • cursor positioning and movement, including split cursors for mixed directional text,
  • highlighting, including both logical and visual highlighting for mixed directional text,
  • multiple baselines (roman, hanging, and centered),
  • hit testing,
  • justification,
  • default font substitution,
  • metric information such as ascent, descent, and advance, and
  • rendering

A TextLayout object can be rendered using its draw method.

TextLayout can be constructed either directly or through the use of a {@link LineBreakMeasurer}. When constructed directly, the source text represents a single paragraph. LineBreakMeasurer allows styled text to be broken into lines that fit within a particular width. See the LineBreakMeasurer documentation for more information.

TextLayout construction logically proceeds as follows:

  • paragraph attributes are extracted and examined,
  • text is analyzed for bidirectional reordering, and reordering information is computed if needed,
  • text is segmented into style runs
  • fonts are chosen for style runs, first by using a font if the attribute {@link TextAttribute#FONT} is present, otherwise by computing a default font using the attributes that have been defined
  • if text is on multiple baselines, the runs or subruns are further broken into subruns sharing a common baseline,
  • glyphvectors are generated for each run using the chosen font,
  • final bidirectional reordering is performed on the glyphvectors

All graphical information returned from a TextLayout object's methods is relative to the origin of the TextLayout, which is the intersection of the TextLayout object's baseline with its left edge. Also, coordinates passed into a TextLayout object's methods are assumed to be relative to the TextLayout object's origin. Clients usually need to translate between a TextLayout object's coordinate system and the coordinate system in another object (such as a {@link java.awt.Graphics Graphics} object).

TextLayout objects are constructed from styled text, but they do not retain a reference to their source text. Thus, changes in the text previously used to generate a TextLayout do not affect the TextLayout.

Three methods on a TextLayout object (getNextRightHit, getNextLeftHit, and hitTestChar) return instances of {@link TextHitInfo}. The offsets contained in these TextHitInfo objects are relative to the start of the TextLayout, not to the text used to create the TextLayout. Similarly, TextLayout methods that accept TextHitInfo instances as parameters expect the TextHitInfo object's offsets to be relative to the TextLayout, not to any underlying text storage model.

Examples:

Constructing and drawing a TextLayout and its bounding rectangle:

Graphics2D g = ...;
Point2D loc = ...;
Font font = Font.getFont("Helvetica-bold-italic");
FontRenderContext frc = g.getFontRenderContext();
TextLayout layout = new TextLayout("This is a string", font, frc);
layout.draw(g, (float)loc.getX(), (float)loc.getY());

Rectangle2D bounds = layout.getBounds();
bounds.setRect(bounds.getX()+loc.getX(),
bounds.getY()+loc.getY(),
bounds.getWidth(),
bounds.getHeight());
g.draw(bounds);

Hit-testing a TextLayout (determining which character is at a particular graphical location):

Point2D click = ...;
TextHitInfo hit = layout.hitTestChar(
(float) (click.getX() - loc.getX()),
(float) (click.getY() - loc.getY()));

Responding to a right-arrow key press:

int insertionIndex = ...;
TextHitInfo next = layout.getNextRightHit(insertionIndex);
if (next != null) {
// translate graphics to origin of layout on screen
g.translate(loc.getX(), loc.getY());
Shape[] carets = layout.getCaretShapes(next.getInsertionIndex());
g.draw(carets[0]);
if (carets[1] != null) {
g.draw(carets[1]);
}
}

Drawing a selection range corresponding to a substring in the source text. The selected area may not be visually contiguous:

// selStart, selLimit should be relative to the layout,
// not to the source text

int selStart = ..., selLimit = ...;
Color selectionColor = ...;
Shape selection = layout.getLogicalHighlightShape(selStart, selLimit);
// selection may consist of disjoint areas
// graphics is assumed to be tranlated to origin of layout
g.setColor(selectionColor);
g.fill(selection);

Drawing a visually contiguous selection range. The selection range may correspond to more than one substring in the source text. The ranges of the corresponding source text substrings can be obtained with getLogicalRangesForVisualSelection():

TextHitInfo selStart = ..., selLimit = ...;
Shape selection = layout.getVisualHighlightShape(selStart, selLimit);
g.setColor(selectionColor);
g.fill(selection);
int[] ranges = getLogicalRangesForVisualSelection(selStart, selLimit);
// ranges[0], ranges[1] is the first selection range,
// ranges[2], ranges[3] is the second selection range, etc.

see
LineBreakMeasurer
see
TextAttribute
see
TextHitInfo

Fields Summary
private int
characterCount
private boolean
isVerticalLine
private byte
baseline
private float[]
baselineOffsets
private TextLine
textLine
private java.awt.font.TextLine.TextLineMetrics
lineMetrics
private float
visibleAdvance
private int
hashCodeCache
private OptInfo
optInfo
private boolean
cacheIsValid
private float
justifyRatio
private static final float
ALREADY_JUSTIFIED
private static float
dx
private static float
dy
private Rectangle2D
naturalBounds
private Rectangle2D
boundsRect
private boolean
caretsInLigaturesAreAllowed
public static final CaretPolicy
DEFAULT_CARET_POLICY
This CaretPolicy is used when a policy is not specified by the client. With this policy, a hit on a character whose direction is the same as the line direction is stronger than a hit on a counterdirectional character. If the characters' directions are the same, a hit on the leading edge of a character is stronger than a hit on the trailing edge of a character.
Constructors Summary
public TextLayout(String string, Font font, FontRenderContext frc)
Constructs a TextLayout from a String and a {@link Font}. All the text is styled using the specified Font.

The String must specify a single paragraph of text, because an entire paragraph is required for the bidirectional algorithm.

param
string the text to display
param
font a Font used to style the text
param
frc contains information about a graphics device which is needed to measure the text correctly. Text measurements can vary slightly depending on the device resolution, and attributes such as antialiasing. This parameter does not specify a translation between the TextLayout and user space.


                                                                                                                                             
           

        if (font == null) {
            throw new IllegalArgumentException("Null font passed to TextLayout constructor.");
        }

        if (string == null) {
            throw new IllegalArgumentException("Null string passed to TextLayout constructor.");
        }

        if (string.length() == 0) {
            throw new IllegalArgumentException("Zero length string passed to TextLayout constructor.");
        }

        char[] text = string.toCharArray();
        if (sameBaselineUpTo(font, text, 0, text.length) == text.length) {
            fastInit(text, font, null, frc);
        } else {
            AttributedString as = new AttributedString(string);
            as.addAttribute(TextAttribute.FONT, font);
            standardInit(as.getIterator(), text, frc);
        }
    
public TextLayout(String string, Map attributes, FontRenderContext frc)
Constructs a TextLayout from a String and an attribute set.

All the text is styled using the provided attributes.

string must specify a single paragraph of text because an entire paragraph is required for the bidirectional algorithm.

param
string the text to display
param
attributes the attributes used to style the text
param
frc contains information about a graphics device which is needed to measure the text correctly. Text measurements can vary slightly depending on the device resolution, and attributes such as antialiasing. This parameter does not specify a translation between the TextLayout and user space.


        if (string == null) {
            throw new IllegalArgumentException("Null string passed to TextLayout constructor.");
        }

        if (attributes == null) {
            throw new IllegalArgumentException("Null map passed to TextLayout constructor.");
        }

        if (string.length() == 0) {
            throw new IllegalArgumentException("Zero length string passed to TextLayout constructor.");
        }

        char[] text = string.toCharArray();
        Font font = singleFont(text, 0, text.length, attributes);
        if (font != null) {
            fastInit(text, font, attributes, frc);
        } else {
            AttributedString as = new AttributedString(string, attributes);
            standardInit(as.getIterator(), text, frc);
        }
    
public TextLayout(AttributedCharacterIterator text, FontRenderContext frc)
Constructs a TextLayout from an iterator over styled text.

The iterator must specify a single paragraph of text because an entire paragraph is required for the bidirectional algorithm.

param
text the styled text to display
param
frc contains information about a graphics device which is needed to measure the text correctly. Text measurements can vary slightly depending on the device resolution, and attributes such as antialiasing. This parameter does not specify a translation between the TextLayout and user space.


        if (text == null) {
            throw new IllegalArgumentException("Null iterator passed to TextLayout constructor.");
        }

        int start = text.getBeginIndex();
        int limit = text.getEndIndex();
        if (start == limit) {
            throw new IllegalArgumentException("Zero length iterator passed to TextLayout constructor.");
        }

        int len = limit - start;
        text.first();
        char[] chars = new char[len];
        int n = 0;
        for (char c = text.first(); c != text.DONE; c = text.next()) {
            chars[n++] = c;
        }

        text.first();
        if (text.getRunLimit() == limit) {

            Map attributes = text.getAttributes();
            Font font = singleFont(chars, 0, len, attributes);
            if (font != null) {
                fastInit(chars, font, attributes, frc);
                return;
            }
        }

        standardInit(text, chars, frc);
    
TextLayout(TextLine textLine, byte baseline, float[] baselineOffsets, float justifyRatio)
Creates a TextLayout from a {@link TextLine} and some paragraph data. This method is used by {@link TextMeasurer}.

param
textLine the line measurement attributes to apply to the the resulting TextLayout
param
baseline the baseline of the text
param
baselineOffsets the baseline offsets for this TextLayout. This should already be normalized to baseline
param
justifyRatio 0 if the TextLayout cannot be justified; 1 otherwise.


        this.characterCount = textLine.characterCount();
        this.baseline = baseline;
        this.baselineOffsets = baselineOffsets;
        this.textLine = textLine;
        this.justifyRatio = justifyRatio;
    
Methods Summary
private java.awt.geom.GeneralPathboundingShape(double[] path0, double[] path1)


        // Really, we want the path to be a convex hull around all of the
        // points in path0 and path1.  But we can get by with less than
        // that.  We do need to prevent the two segments which
        // join path0 to path1 from crossing each other.  So, if we
        // traverse path0 from top to bottom, we'll traverse path1 from
        // bottom to top (and vice versa).

        GeneralPath result = pathToShape(path0, false);

        boolean sameDirection;
                
        if (isVerticalLine) {
            sameDirection = (path0[1] > path0[path0.length-1]) ==
                            (path1[1] > path1[path1.length-1]);
        }
        else {
            sameDirection = (path0[0] > path0[path0.length-2]) ==
                            (path1[0] > path1[path1.length-2]);
        }

        int start;
        int limit;
        int increment;

        if (sameDirection) {
            start = path1.length-2;
            limit = -2;
            increment = -2;
        }
        else {
            start = 0;
            limit = path1.length;
            increment = 2;
        }

        for (int i = start; i != limit; i += increment) {
            result.lineTo((float)path1[i], (float)path1[i+1]);
        }

        result.closePath();

        return result;
    
private voidbuildCache()

        if (textLine == null) {
            initTextLine();
        }

        lineMetrics = textLine.getMetrics();

        // compute visibleAdvance
        if (textLine.isDirectionLTR()) {

            int lastNonSpace = characterCount-1;
            while (lastNonSpace != -1) {
                int logIndex = textLine.visualToLogical(lastNonSpace);
                if (!textLine.isCharSpace(logIndex)) {
                    break;
                }
                else {
                    --lastNonSpace;
                }
            }
            if (lastNonSpace == characterCount-1) {
                visibleAdvance = lineMetrics.advance;
            }
            else if (lastNonSpace == -1) {
                visibleAdvance = 0;
            }
            else {
                int logIndex = textLine.visualToLogical(lastNonSpace);
                visibleAdvance = textLine.getCharLinePosition(logIndex)
                                        + textLine.getCharAdvance(logIndex);
            }
        }
        else {

            int leftmostNonSpace = 0;
            while (leftmostNonSpace != characterCount) {
                int logIndex = textLine.visualToLogical(leftmostNonSpace);
                if (!textLine.isCharSpace(logIndex)) {
                    break;
                }
                else {
                    ++leftmostNonSpace;
                }
            }
            if (leftmostNonSpace == characterCount) {
                visibleAdvance = 0;
            }
            else if (leftmostNonSpace == 0) {
                visibleAdvance = lineMetrics.advance;
            }
            else {
                int logIndex = textLine.visualToLogical(leftmostNonSpace);
                float pos = textLine.getCharLinePosition(logIndex);
                visibleAdvance = lineMetrics.advance - pos;
            }
        }

        // naturalBounds, boundsRect will be generated on demand
        naturalBounds = null;
        boundsRect = null;

        // hashCode will be regenerated on demand
        hashCodeCache = 0;

        cacheIsValid = true;
    
private java.awt.geom.GeneralPathcaretBoundingShape(int caret0, int caret1, java.awt.geom.Rectangle2D bounds)


        if (caret0 > caret1) {
            int temp = caret0;
            caret0 = caret1;
            caret1 = temp;
        }

        return boundingShape(getCaretPath(caret0, bounds, true),
                             getCaretPath(caret1, bounds, true));
    
private booleancaretIsValid(int caret)


        if (caret == characterCount || caret == 0) {
            return true;
        }

        int offset = textLine.visualToLogical(caret);

        if (!textLine.isCharLTR(offset)) {
            offset = textLine.visualToLogical(caret-1);
            if (textLine.isCharLTR(offset)) {
                return true;
            }
        }

        // At this point, the leading edge of the character
        // at offset is at the given caret.

        return textLine.caretAtOffsetIsValid(offset);
    
private java.awt.font.TextHitInfocaretToHit(int caret)
Given a caret index, return a hit whose caret is at the index. The hit is NOT guaranteed to be strong!!!

param
caret a caret index.
return
a hit on this layout whose strong caret is at the requested index.


        if (caret == 0 || caret == characterCount) {

            if ((caret == characterCount) == textLine.isDirectionLTR()) {
                return TextHitInfo.leading(characterCount);
            }
            else {
                return TextHitInfo.trailing(-1);
            }
        }
        else {

            int charIndex = textLine.visualToLogical(caret);
            boolean leading = textLine.isCharLTR(charIndex);

            return leading? TextHitInfo.leading(charIndex)
                            : TextHitInfo.trailing(charIndex);
        }
    
private floatcaretToPointDistance(float[] caretInfo, float x, float y)
Returns the distance from the point (x, y) to the caret along the line direction defined in caretInfo. Distance is negative if the point is to the left of the caret on a horizontal line, or above the caret on a vertical line. Utility for use by hitTestChar.

        // distanceOffBaseline is negative if you're 'above' baseline

        float lineDistance = isVerticalLine? y : x;
        float distanceOffBaseline = isVerticalLine? -x : y;

        return lineDistance - caretInfo[0] +
            (distanceOffBaseline*caretInfo[1]);
    
private voidcheckTextHit(java.awt.font.TextHitInfo hit)

        if (hit == null) {
            throw new IllegalArgumentException("TextHitInfo is null.");
        }

        if (hit.getInsertionIndex() < 0 ||
            hit.getInsertionIndex() > characterCount) {
            throw new IllegalArgumentException("TextHitInfo is out of range");
        }
    
protected java.lang.Objectclone()
Creates a copy of this TextLayout.

        /*
         * !!! I think this is safe.  Once created, nothing mutates the
         * glyphvectors or arrays.  But we need to make sure.
         * {jbr} actually, that's not quite true.  The justification code
         * mutates after cloning.  It doesn't actually change the glyphvectors
         * (that's impossible) but it replaces them with justified sets.  This
         * is a problem for GlyphIterator creation, since new GlyphIterators
         * are created by cloning a prototype.  If the prototype has outdated
         * glyphvectors, so will the new ones.  A partial solution is to set the
         * prototypical GlyphIterator to null when the glyphvectors change.  If
         * you forget this one time, you're hosed.
         */
        try {
            return super.clone();
        }
        catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    
public voiddraw(java.awt.Graphics2D g2, float x, float y)
Renders this TextLayout at the specified location in the specified {@link java.awt.Graphics2D Graphics2D} context. The origin of the layout is placed at x, y. Rendering may touch any point within getBounds() of this position. This leaves the g2 unchanged.

param
g2 the Graphics2D context into which to render the layout
param
x, y the coordinates of the origin of this TextLayout
see
#getBounds()


        if (g2 == null) {
            throw new IllegalArgumentException("Null Graphics2D passed to TextLayout.draw()");
        }

        if (optInfo != null) {
            if (optInfo.draw(g2, x, y)) { // might fail to draw because of frc change
                return;
            }
            // replace with TextLine and fall through
            initTextLine();
        }
        textLine.draw(g2, x - dx, y - dy);
    
private voidensureCache()

        if (!cacheIsValid) {
            buildCache();
        }
    
public booleanequals(java.lang.Object obj)
Returns true if the specified Object is a TextLayout object and if the specified Object equals this TextLayout.

param
obj an Object to test for equality
return
true if the specified Object equals this TextLayout; false otherwise.

        return (obj instanceof TextLayout) && equals((TextLayout)obj);
    
public booleanequals(java.awt.font.TextLayout rhs)
Returns true if the two layouts are equal. Two layouts are equal if they contain equal glyphvectors in the same order.

param
rhs the TextLayout to compare to this TextLayout
return
true if the specified TextLayout equals this TextLayout.


        if (rhs == null) {
            return false;
        }
        if (rhs == this) {
            return true;
        }

	ensureCache();
        return textLine.equals(rhs.textLine);
    
private voidfastInit(char[] chars, java.awt.Font font, java.util.Map attrs, java.awt.font.FontRenderContext frc)

        // Object vf = attrs.get(TextAttribute.ORIENTATION);
        // isVerticalLine = TextAttribute.ORIENTATION_VERTICAL.equals(vf);
        isVerticalLine = false;

        LineMetrics lm = font.getLineMetrics(chars, 0, chars.length, frc);
        CoreMetrics cm = CoreMetrics.get(lm);
        byte glyphBaseline = (byte) cm.baselineIndex;

        if (attrs == null) {
            baseline = glyphBaseline;
            baselineOffsets = cm.baselineOffsets;
            justifyRatio = 1.0f;
        } else {
            paragraphInit(glyphBaseline, cm, attrs, chars);
        }

        characterCount = chars.length;

        optInfo = OptInfo.create(frc, chars, font, cm, attrs);
        if (optInfo == null) {
            textLine = TextLine.fastCreateTextLine(frc, chars, font, cm, attrs);
        }
    
public floatgetAdvance()
Returns the advance of this TextLayout. The advance is the distance from the origin to the advance of the rightmost (bottommost) character measuring in the line direction.

return
the advance of this TextLayout.

        if (optInfo != null) {
            try {
                return optInfo.getAdvance();
            }
            catch (Error e) {
                // cache was flushed under optInfo
            }
        }
        ensureCache();
        return lineMetrics.advance;
    
public floatgetAscent()
Returns the ascent of this TextLayout. The ascent is the distance from the top (right) of the TextLayout to the baseline. It is always either positive or zero. The ascent is sufficient to accomodate superscripted text and is the maximum of the sum of the ascent, offset, and baseline of each glyph.

return
the ascent of this TextLayout.

        if (optInfo != null) {
            return optInfo.getCoreMetrics().ascent;
        }
        ensureCache();
        return lineMetrics.ascent;
    
public bytegetBaseline()
Returns the baseline for this TextLayout. The baseline is one of the values defined in Font, which are roman, centered and hanging. Ascent and descent are relative to this baseline. The baselineOffsets are also relative to this baseline.

return
the baseline of this TextLayout.
see
#getBaselineOffsets()
see
Font

        return baseline;
    
static bytegetBaselineFromGraphic(java.awt.font.GraphicAttribute graphic)


        byte alignment = (byte) graphic.getAlignment();

        if (alignment == GraphicAttribute.BOTTOM_ALIGNMENT ||
                alignment == GraphicAttribute.TOP_ALIGNMENT) {

            return (byte)GraphicAttribute.ROMAN_BASELINE;
        }
        else {
            return alignment;
        }
    
public float[]getBaselineOffsets()
Returns the offsets array for the baselines used for this TextLayout.

The array is indexed by one of the values defined in Font, which are roman, centered and hanging. The values are relative to this TextLayout object's baseline, so that getBaselineOffsets[getBaseline()] == 0. Offsets are added to the position of the TextLayout object's baseline to get the position for the new baseline.

return
the offsets array containing the baselines used for this TextLayout.
see
#getBaseline()
see
Font

        float[] offsets = new float[baselineOffsets.length];
        System.arraycopy(baselineOffsets, 0, offsets, 0, offsets.length);
        return offsets;
    
public java.awt.ShapegetBlackBoxBounds(int firstEndpoint, int secondEndpoint)
Returns the black box bounds of the characters in the specified range. The black box bounds is an area consisting of the union of the bounding boxes of all the glyphs corresponding to the characters between start and limit. This path may be disjoint.

param
firstEndpoint one end of the character range
param
secondEndpoint the other end of the character range. Can be less than firstEndpoint.
return
a path enclosing the black box bounds.

        ensureCache();

        if (firstEndpoint > secondEndpoint) {
            int t = firstEndpoint;
            firstEndpoint = secondEndpoint;
            secondEndpoint = t;
        }

        if (firstEndpoint < 0 || secondEndpoint > characterCount) {
            throw new IllegalArgumentException("Invalid range passed to TextLayout.getBlackBoxBounds()");
        }

        /*
         * return an area that consists of the bounding boxes of all the
         * characters from firstEndpoint to limit
         */

        GeneralPath result = new GeneralPath(GeneralPath.WIND_NON_ZERO);

        if (firstEndpoint < characterCount) {
            for (int logIndex = firstEndpoint;
                        logIndex < secondEndpoint;
                        logIndex++) {

		Rectangle2D r = textLine.getCharBounds(logIndex);
		if (!r.isEmpty()) {
		    result.append(r, false);
                }
            }
        }

        if (dx != 0 || dy != 0) {
            AffineTransform translate = new AffineTransform();
            translate.setToTranslation(dx, dy);
            result = (GeneralPath) result.createTransformedShape(translate);
        }

        //return new Highlight(result, false);
        return result;
    
public java.awt.geom.Rectangle2DgetBounds()
Returns the bounds of this TextLayout. The bounds contains all of the pixels the TextLayout can draw. It might not coincide exactly with the ascent, descent, origin or advance of the TextLayout.

return
a {@link Rectangle2D} that is the bounds of this TextLayout.

	if (optInfo != null) {
	    return optInfo.getVisualBounds();
	}

        ensureCache();

        if (boundsRect == null) {
            Rectangle2D lineBounds = textLine.getBounds();
            if (dx != 0 || dy != 0) {
                lineBounds.setRect(lineBounds.getX() - dx,
                                   lineBounds.getY() - dy,
                                   lineBounds.getWidth(),
                                   lineBounds.getHeight());
            }
            boundsRect = lineBounds;
        }

        Rectangle2D bounds = new Rectangle2D.Float();
        bounds.setRect(boundsRect);

        return bounds;
    
private float[]getCaretInfo(int caret, java.awt.geom.Rectangle2D bounds, float[] info)


        float top1X, top2X;
        float bottom1X, bottom2X;

        if (caret == 0 || caret == characterCount) {

            float pos;
            int logIndex;
            if (caret == characterCount) {
                logIndex = textLine.visualToLogical(characterCount-1);
                pos = textLine.getCharLinePosition(logIndex)
                                        + textLine.getCharAdvance(logIndex);
            }
            else {
                logIndex = textLine.visualToLogical(caret);
                pos = textLine.getCharLinePosition(logIndex);
            }
            float angle = textLine.getCharAngle(logIndex);
            float shift = textLine.getCharShift(logIndex);
            pos += angle * shift;
            top1X = top2X = pos + angle*textLine.getCharAscent(logIndex);
            bottom1X = bottom2X = pos - angle*textLine.getCharDescent(logIndex);
        }
        else {

            {
                int logIndex = textLine.visualToLogical(caret-1);
                float angle1 = textLine.getCharAngle(logIndex);
                float pos1 = textLine.getCharLinePosition(logIndex)
                                    + textLine.getCharAdvance(logIndex);
                if (angle1 != 0) {
                    pos1 += angle1 * textLine.getCharShift(logIndex);
                    top1X = pos1 + angle1*textLine.getCharAscent(logIndex);
                    bottom1X = pos1 - angle1*textLine.getCharDescent(logIndex);
                }
                else {
                    top1X = bottom1X = pos1;
                }
            }
            {
                int logIndex = textLine.visualToLogical(caret);
                float angle2 = textLine.getCharAngle(logIndex);
                float pos2 = textLine.getCharLinePosition(logIndex);
                if (angle2 != 0) {
                    pos2 += angle2*textLine.getCharShift(logIndex);
                    top2X = pos2 + angle2*textLine.getCharAscent(logIndex);
                    bottom2X = pos2 - angle2*textLine.getCharDescent(logIndex);
                }
                else {
                    top2X = bottom2X = pos2;
                }
            }
        }

        float topX = (top1X + top2X) / 2;
        float bottomX = (bottom1X + bottom2X) / 2;

        if (info == null) {
            info = new float[2];
        }

        if (isVerticalLine) {
            info[1] = (float) ((topX - bottomX) / bounds.getWidth());
            info[0] = (float) (topX + (info[1]*bounds.getX()));
        }
        else {
            info[1] = (float) ((topX - bottomX) / bounds.getHeight());
            info[0] = (float) (bottomX + (info[1]*bounds.getMaxY()));
        }

        return info;
    
public float[]getCaretInfo(java.awt.font.TextHitInfo hit, java.awt.geom.Rectangle2D bounds)
Returns information about the caret corresponding to hit. The first element of the array is the intersection of the caret with the baseline. The second element of the array is the inverse slope (run/rise) of the caret.

This method is meant for informational use. To display carets, it is better to use getCaretShapes.

param
hit a hit on a character in this TextLayout
param
bounds the bounds to which the caret info is constructed
return
a two-element array containing the position and slope of the caret.
see
#getCaretShapes(int, Rectangle2D, TextLayout.CaretPolicy)
see
Font#getItalicAngle

        ensureCache();
        checkTextHit(hit);

        return getCaretInfoTestInternal(hit, bounds);
    
public float[]getCaretInfo(java.awt.font.TextHitInfo hit)
Returns information about the caret corresponding to hit. This method is a convenience overload of getCaretInfo and uses the natural bounds of this TextLayout.

param
hit a hit on a character in this TextLayout
return
the information about a caret corresponding to a hit.


        return getCaretInfo(hit, getNaturalBounds());
    
private float[]getCaretInfoTestInternal(java.awt.font.TextHitInfo hit, java.awt.geom.Rectangle2D bounds)

        ensureCache();
        checkTextHit(hit);

        float[] info = new float[6];

        // get old data first
        getCaretInfo(hitToCaret(hit), bounds, info);

        // then add our new data
        double iangle, ixbase, p1x, p1y, p2x, p2y;

        int charix = hit.getCharIndex();
        boolean lead = hit.isLeadingEdge();
        boolean ltr = textLine.isDirectionLTR();
        boolean horiz = !isVertical();

        if (charix == -1 || charix == characterCount) {
            // !!! note: want non-shifted, baseline ascent and descent here!
            // TextLine should return appropriate line metrics object for these values
            TextLineMetrics m = textLine.getMetrics();
            boolean low = ltr == (charix == -1);
            iangle = 0;
            if (horiz) {
                p1x = p2x = low ? 0 : m.advance;
                p1y = -m.ascent;
                p2y = m.descent;
            } else {
                p1y = p2y = low ? 0 : m.advance;
                p1x = m.descent;
                p2x = m.ascent;
            }
        } else {
            CoreMetrics thiscm = textLine.getCoreMetricsAt(charix);
            iangle = thiscm.italicAngle;
            ixbase = textLine.getCharLinePosition(charix, lead);
	    if (thiscm.baselineIndex < 0) {
		// this is a graphic, no italics, use entire line height for caret
		TextLineMetrics m = textLine.getMetrics();
		if (horiz) {
		    p1x = p2x = ixbase;
		    if (thiscm.baselineIndex == GraphicAttribute.TOP_ALIGNMENT) {
			p1y = -m.ascent;
			p2y = p1y + thiscm.height;
		    } else {
			p2y = m.descent;
			p1y = p2y - thiscm.height;
		    }
		} else {
		    p1y = p2y = ixbase;
		    p1x = m.descent;
		    p2x = m.ascent;
		    // !!! top/bottom adjustment not implemented for vertical
		}
	    } else {
		float bo = baselineOffsets[thiscm.baselineIndex];
		if (horiz) {
		    ixbase += iangle * thiscm.ssOffset;
		    p1x = ixbase + iangle * thiscm.ascent;
		    p2x = ixbase - iangle * thiscm.descent;
		    p1y = bo - thiscm.ascent;
		    p2y = bo + thiscm.descent;
		} else {
		    ixbase -= iangle * thiscm.ssOffset;
		    p1y = ixbase + iangle * thiscm.ascent;
		    p2y = ixbase - iangle * thiscm.descent;
		    p1x = bo + thiscm.ascent;
		    p2x = bo + thiscm.descent;
		}
	    }
        }

        info[2] = (float)p1x;
        info[3] = (float)p1y;
        info[4] = (float)p2x;
        info[5] = (float)p2y;

        return info;
    
private double[]getCaretPath(java.awt.font.TextHitInfo hit, java.awt.geom.Rectangle2D bounds)

        float[] info = getCaretInfo(hit, bounds);
        return new double[] { info[2], info[3], info[4], info[5] };
    
private double[]getCaretPath(int caret, java.awt.geom.Rectangle2D bounds, boolean clipToBounds)
Return an array of four floats corresponding the endpoints of the caret x0, y0, x1, y1. This creates a line along the slope of the caret intersecting the baseline at the caret position, and extending from ascent above the baseline to descent below it.


        float[] info = getCaretInfo(caret, bounds, null);

        double pos = info[0];
        double slope = info[1];

        double x0, y0, x1, y1;
        double x2 = -3141.59, y2 = -2.7; // values are there to make compiler happy

        double left = bounds.getX();
        double right = left + bounds.getWidth();
        double top = bounds.getY();
        double bottom = top + bounds.getHeight();

        boolean threePoints = false;

        if (isVerticalLine) {

            if (slope >= 0) {
                x0 = left;
                x1 = right;
            }
            else {
                x1 = left;
                x0 = right;
            }

            y0 = pos + x0 * slope;
            y1 = pos + x1 * slope;

            // y0 <= y1, always

            if (clipToBounds) {
                if (y0 < top) {
                    if (slope <= 0 || y1 <= top) {
                        y0 = y1 = top;
                    }
                    else {
                        threePoints = true;
                        y0 = top;
                        y2 = top;
                        x2 = x1 + (top-y1)/slope;
                        if (y1 > bottom) {
                            y1 = bottom;
                        }
                    }
                }
                else if (y1 > bottom) {
                    if (slope >= 0 || y0 >= bottom) {
                        y0 = y1 = bottom;
                    }
                    else {
                        threePoints = true;
                        y1 = bottom;
                        y2 = bottom;
                        x2 = x0 + (bottom-x1)/slope;
                    }
                }
            }

        }
        else {

            if (slope >= 0) {
                y0 = bottom;
                y1 = top;
            }
            else {
                y1 = bottom;
                y0 = top;
            }

            x0 = pos - y0 * slope;
            x1 = pos - y1 * slope;
            
            // x0 <= x1, always

            if (clipToBounds) {
                if (x0 < left) {
                    if (slope <= 0 || x1 <= left) {
                        x0 = x1 = left;
                    }
                    else {
                        threePoints = true;
                        x0 = left;
                        x2 = left;
                        y2 = y1 - (left-x1)/slope;
                        if (x1 > right) {
                            x1 = right;
                        }
                    }
                }
                else if (x1 > right) {
                    if (slope >= 0 || x0 >= right) {
                        x0 = x1 = right;
                    }
                    else {
                        threePoints = true;
                        x1 = right;
                        x2 = right;
                        y2 = y0 - (right-x0)/slope;
                    }
                }
            }
        }

        return threePoints?
                    new double[] { x0, y0, x2, y2, x1, y1 } :
                    new double[] { x0, y0, x1, y1 };
    
public java.awt.ShapegetCaretShape(java.awt.font.TextHitInfo hit, java.awt.geom.Rectangle2D bounds)
Returns a {@link Shape} representing the caret at the specified hit inside the specified bounds.

param
hit the hit at which to generate the caret
param
bounds the bounds of the TextLayout to use in generating the caret.
return
a Shape representing the caret.

	ensureCache();
        checkTextHit(hit);

        if (bounds == null) {
            throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getCaret()");
        }

//          int hitCaret = hitToCaret(hit);
//          GeneralPath hitShape =
//                      pathToShape(getCaretPath(hitCaret, bounds, false), false);

        return pathToShape(getCaretPath(hit, bounds), false);

        //return new Highlight(hitShape, true);
//          return hitShape;
    
public java.awt.ShapegetCaretShape(java.awt.font.TextHitInfo hit)
Returns a Shape representing the caret at the specified hit inside the natural bounds of this TextLayout.

param
hit the hit at which to generate the caret
return
a Shape representing the caret.


        return getCaretShape(hit, getNaturalBounds());
    
public java.awt.Shape[]getCaretShapes(int offset, java.awt.geom.Rectangle2D bounds, java.awt.font.TextLayout$CaretPolicy policy)
Returns two paths corresponding to the strong and weak caret.

param
offset an offset in this TextLayout
param
bounds the bounds to which to extend the carets
param
policy the specified CaretPolicy
return
an array of two paths. Element zero is the strong caret. If there are two carets, element one is the weak caret, otherwise it is null.


        ensureCache();

        if (offset < 0 || offset > characterCount) {
            throw new IllegalArgumentException("Offset out of bounds in TextLayout.getCaretShapes()");
        }

        if (bounds == null) {
            throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getCaretShapes()");
        }

        if (policy == null) {
            throw new IllegalArgumentException("Null CaretPolicy passed to TextLayout.getCaretShapes()");
        }

        Shape[] result = new Shape[2];

        TextHitInfo hit = TextHitInfo.afterOffset(offset);

        int hitCaret = hitToCaret(hit);
//          Shape hitShape =
//                      pathToShape(getCaretPath(hitCaret, bounds, false), false);

        Shape hitShape = pathToShape(getCaretPath(hit, bounds), false);
        TextHitInfo otherHit = hit.getOtherHit();
        int otherCaret = hitToCaret(otherHit);

        if (hitCaret == otherCaret) {
            result[0] = hitShape;
        }
        else { // more than one caret
//              Shape otherShape =
//                  pathToShape(getCaretPath(otherCaret, bounds, false), false);

            Shape otherShape = pathToShape(getCaretPath(otherHit, bounds), false);

            TextHitInfo strongHit = policy.getStrongCaret(hit, otherHit, this);
            boolean hitIsStrong = strongHit.equals(hit);

            if (hitIsStrong) {// then other is weak
                result[0] = hitShape;
                result[1] = otherShape;
            }
            else {
                result[0] = otherShape;
                result[1] = hitShape;
            }
        }

        return result;
    
public java.awt.Shape[]getCaretShapes(int offset, java.awt.geom.Rectangle2D bounds)
Returns two paths corresponding to the strong and weak caret. This method is a convenience overload of getCaretShapes that uses the default caret policy.

param
offset an offset in this TextLayout
param
bounds the bounds to which to extend the carets
return
two paths corresponding to the strong and weak caret as defined by the DEFAULT_CARET_POLICY

        // {sfb} parameter checking is done in overloaded version
        return getCaretShapes(offset, bounds, DEFAULT_CARET_POLICY);
    
public java.awt.Shape[]getCaretShapes(int offset)
Returns two paths corresponding to the strong and weak caret. This method is a convenience overload of getCaretShapes that uses the default caret policy and this TextLayout object's natural bounds.

param
offset an offset in this TextLayout
return
two paths corresponding to the strong and weak caret as defined by the DEFAULT_CARET_POLICY

        // {sfb} parameter checking is done in overloaded version
        return getCaretShapes(offset, getNaturalBounds(), DEFAULT_CARET_POLICY);
    
public intgetCharacterCount()
Returns the number of characters represented by this TextLayout.

return
the number of characters in this TextLayout.

        return characterCount;
    
public bytegetCharacterLevel(int index)
Returns the level of the character at index. Indices -1 and characterCount are assigned the base level of this TextLayout.

param
index the index of the character from which to get the level
return
the level of the character at the specified index.


        // hmm, allow indices at endpoints?  For now, yes.
        if (index < -1 || index > characterCount) {
            throw new IllegalArgumentException("Index is out of range in getCharacterLevel.");
        }

        if (optInfo != null) {
            return 0;
        }

	ensureCache();
        if (index == -1 || index == characterCount) {
             return (byte) (textLine.isDirectionLTR()? 0 : 1);
        }

        return textLine.getCharLevel(index);
    
public floatgetDescent()
Returns the descent of this TextLayout. The descent is the distance from the baseline to the bottom (left) of the TextLayout. It is always either positive or zero. The descent is sufficient to accomodate subscripted text and is the maximum of the sum of the descent, offset, and baseline of each glyph.

return
the descent of this TextLayout.

        if (optInfo != null) {
            return optInfo.getCoreMetrics().descent;
        }
        ensureCache();
        return lineMetrics.descent;
    
public java.awt.font.TextLayoutgetJustifiedLayout(float justificationWidth)
Creates a copy of this TextLayout justified to the specified width.

If this TextLayout has already been justified, an exception is thrown. If this TextLayout object's justification ratio is zero, a TextLayout identical to this TextLayout is returned.

param
justificationWidth the width to use when justifying the line. For best results, it should not be too different from the current advance of the line.
return
a TextLayout justified to the specified width.
exception
Error if this layout has already been justified, an Error is thrown.


        if (justificationWidth <= 0) {
            throw new IllegalArgumentException("justificationWidth <= 0 passed to TextLayout.getJustifiedLayout()");
        }

        if (justifyRatio == ALREADY_JUSTIFIED) {
            throw new Error("Can't justify again.");
        }

	ensureCache(); // make sure textLine is not null

        // default justification range to exclude trailing logical whitespace
        int limit = characterCount;
        while (limit > 0 && textLine.isCharWhitespace(limit-1)) {
            --limit;
        }

        TextLine newLine = textLine.getJustifiedLine(justificationWidth, justifyRatio, 0, limit);
        if (newLine != null) {
            return new TextLayout(newLine, baseline, baselineOffsets, ALREADY_JUSTIFIED);
        }

        return this;
    
public floatgetLeading()
Returns the leading of the TextLayout. The leading is the suggested interline spacing for this TextLayout.

The leading is computed from the leading, descent, and baseline of all glyphvectors in the TextLayout. The algorithm is roughly as follows:

maxD = 0;
maxDL = 0;
for (GlyphVector g in all glyphvectors) {
maxD = max(maxD, g.getDescent() + offsets[g.getBaseline()]);
maxDL = max(maxDL, g.getDescent() + g.getLeading() +
offsets[g.getBaseline()]);
}
return maxDL - maxD;

return
the leading of this TextLayout.

        if (optInfo != null) {
            return optInfo.getCoreMetrics().leading;
        }
        ensureCache();
        return lineMetrics.leading;
    
public java.awt.ShapegetLogicalHighlightShape(int firstEndpoint, int secondEndpoint, java.awt.geom.Rectangle2D bounds)
Returns a Shape enclosing the logical selection in the specified range, extended to the specified bounds.

If the selection range includes the first logical character, the selection is extended to the portion of bounds before the start of this TextLayout. If the range includes the last logical character, the selection is extended to the portion of bounds after the end of this TextLayout. The height (width on vertical lines) of the selection is always extended to bounds.

The selection can be discontiguous on lines with mixed-direction text. Only those characters in the logical range between start and limit appear selected. For example, consider the text 'ABCdef' where capital letters indicate right-to-left text, rendered on a right-to-left line, with a logical selection from 0 to 4 ('ABCd'). The text appears as follows, with bold standing in for the selection, and underlining for the extension:

defCBA 
The selection is discontiguous because the selected characters are visually discontiguous. Also note that since the range includes the first logical character (A), the selection is extended to the portion of the bounds before the start of the layout, which in this case (a right-to-left line) is the right portion of the bounds.

param
firstEndpoint an endpoint in the range of characters to select
param
secondEndpoint the other endpoint of the range of characters to select. Can be less than firstEndpoint. The range includes the character at min(firstEndpoint, secondEndpoint), but excludes max(firstEndpoint, secondEndpoint).
param
bounds the bounding rectangle to which to extend the selection
return
an area enclosing the selection.
see
#getVisualHighlightShape(TextHitInfo, TextHitInfo, Rectangle2D)

        if (bounds == null) {
            throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getLogicalHighlightShape()");
        }

        ensureCache();

        if (firstEndpoint > secondEndpoint) {
            int t = firstEndpoint;
            firstEndpoint = secondEndpoint;
            secondEndpoint = t;
        }

        if(firstEndpoint < 0 || secondEndpoint > characterCount) {
            throw new IllegalArgumentException("Range is invalid in TextLayout.getLogicalHighlightShape()");
        }

        GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD);

        int[] carets = new int[10]; // would this ever not handle all cases?
        int count = 0;

        if (firstEndpoint < secondEndpoint) {
            int logIndex = firstEndpoint;
            do {
                carets[count++] = hitToCaret(TextHitInfo.leading(logIndex));
                boolean ltr = textLine.isCharLTR(logIndex);

                do {
                    logIndex++;
                } while (logIndex < secondEndpoint && textLine.isCharLTR(logIndex) == ltr);

                int hitCh = logIndex;
                carets[count++] = hitToCaret(TextHitInfo.trailing(hitCh - 1));

                if (count == carets.length) {
                    int[] temp = new int[carets.length + 10];
                    System.arraycopy(carets, 0, temp, 0, count);
                    carets = temp;
                }
            } while (logIndex < secondEndpoint);
        }
        else {
            count = 2;
            carets[0] = carets[1] = hitToCaret(TextHitInfo.leading(firstEndpoint));
        }

        // now create paths for pairs of carets

        for (int i = 0; i < count; i += 2) {
            result.append(caretBoundingShape(carets[i], carets[i+1], bounds),
                          false);
        }

        if (firstEndpoint != secondEndpoint) {
            if ((textLine.isDirectionLTR() && firstEndpoint == 0) || (!textLine.isDirectionLTR() &&
                                                                      secondEndpoint == characterCount)) {
                result.append(leftShape(bounds), false);
            }

            if ((textLine.isDirectionLTR() && secondEndpoint == characterCount) ||
                (!textLine.isDirectionLTR() && firstEndpoint == 0)) {
                result.append(rightShape(bounds), false);
            }
        }

        return result;
    
public java.awt.ShapegetLogicalHighlightShape(int firstEndpoint, int secondEndpoint)
Returns a Shape enclosing the logical selection in the specified range, extended to the natural bounds of this TextLayout. This method is a convenience overload of getLogicalHighlightShape that uses the natural bounds of this TextLayout.

param
firstEndpoint an endpoint in the range of characters to select
param
secondEndpoint the other endpoint of the range of characters to select. Can be less than firstEndpoint. The range includes the character at min(firstEndpoint, secondEndpoint), but excludes max(firstEndpoint, secondEndpoint).
return
a Shape enclosing the selection.


        return getLogicalHighlightShape(firstEndpoint, secondEndpoint, getNaturalBounds());
    
public int[]getLogicalRangesForVisualSelection(java.awt.font.TextHitInfo firstEndpoint, java.awt.font.TextHitInfo secondEndpoint)
Returns the logical ranges of text corresponding to a visual selection.

param
firstEndpoint an endpoint of the visual range
param
secondEndpoint the other endpoint of the visual range. This endpoint can be less than firstEndpoint.
return
an array of integers representing start/limit pairs for the selected ranges.
see
#getVisualHighlightShape(TextHitInfo, TextHitInfo, Rectangle2D)

        ensureCache();

        checkTextHit(firstEndpoint);
        checkTextHit(secondEndpoint);

        // !!! probably want to optimize for all LTR text

        boolean[] included = new boolean[characterCount];

        int startIndex = hitToCaret(firstEndpoint);
        int limitIndex = hitToCaret(secondEndpoint);

        if (startIndex > limitIndex) {
            int t = startIndex;
            startIndex = limitIndex;
            limitIndex = t;
        }

        /*
         * now we have the visual indexes of the glyphs at the start and limit
         * of the selection range walk through runs marking characters that
         * were included in the visual range there is probably a more efficient
         * way to do this, but this ought to work, so hey
         */

        if (startIndex < limitIndex) {
            int visIndex = startIndex;
            while (visIndex < limitIndex) {
                included[textLine.visualToLogical(visIndex)] = true;
                ++visIndex;
            }
        }

        /*
         * count how many runs we have, ought to be one or two, but perhaps
         * things are especially weird
         */
        int count = 0;
        boolean inrun = false;
        for (int i = 0; i < characterCount; i++) {
            if (included[i] != inrun) {
                inrun = !inrun;
                if (inrun) {
                    count++;
                }
            }
        }

        int[] ranges = new int[count * 2];
        count = 0;
        inrun = false;
        for (int i = 0; i < characterCount; i++) {
            if (included[i] != inrun) {
                ranges[count++] = i;
                inrun = !inrun;
            }
        }
        if (inrun) {
            ranges[count++] = characterCount;
        }

        return ranges;
    
private java.awt.geom.Rectangle2DgetNaturalBounds()
The 'natural bounds' encloses all the carets the layout can draw.

        ensureCache();

        if (naturalBounds == null) {
            naturalBounds = textLine.getItalicBounds();
        }

        return naturalBounds;
    
public java.awt.font.TextHitInfogetNextLeftHit(java.awt.font.TextHitInfo hit)
Returns the hit for the next caret to the left (top); if no such hit, returns null. If the hit character index is out of bounds, an IllegalArgumentException is thrown.

param
hit a hit on a character in this TextLayout.
return
a hit whose caret appears at the next position to the left (top) of the caret of the provided hit, or null.

        ensureCache();
        checkTextHit(hit);

        int caret = hitToCaret(hit);

        if (caret == 0) {
            return null;
        }

        do {
            --caret;
        } while(!caretIsValid(caret));

        return caretToHit(caret);
    
public java.awt.font.TextHitInfogetNextLeftHit(int offset, java.awt.font.TextLayout$CaretPolicy policy)
Returns the hit for the next caret to the left (top); if no such hit, returns null. The hit is to the left of the strong caret at the specified offset, as determined by the specified policy. The returned hit is the stronger of the two possible hits, as determined by the specified policy.

param
offset an insertion offset in this TextLayout. Cannot be less than 0 or greater than this TextLayout object's character count.
param
policy the policy used to select the strong caret
return
a hit whose caret appears at the next position to the left (top) of the caret of the provided hit, or null.


        if (policy == null) {
            throw new IllegalArgumentException("Null CaretPolicy passed to TextLayout.getNextLeftHit()");
        }

        if (offset < 0 || offset > characterCount) {
            throw new IllegalArgumentException("Offset out of bounds in TextLayout.getNextLeftHit()");
        }

        TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
        TextHitInfo hit2 = hit1.getOtherHit();

        TextHitInfo nextHit = getNextLeftHit(policy.getStrongCaret(hit1, hit2, this));

        if (nextHit != null) {
            TextHitInfo otherHit = getVisualOtherHit(nextHit);
            return policy.getStrongCaret(otherHit, nextHit, this);
        }
        else {
            return null;
        }
    
public java.awt.font.TextHitInfogetNextLeftHit(int offset)
Returns the hit for the next caret to the left (top); if no such hit, returns null. The hit is to the left of the strong caret at the specified offset, as determined by the default policy. The returned hit is the stronger of the two possible hits, as determined by the default policy.

param
offset an insertion offset in this TextLayout. Cannot be less than 0 or greater than this TextLayout object's character count.
return
a hit whose caret appears at the next position to the left (top) of the caret of the provided hit, or null.


        return getNextLeftHit(offset, DEFAULT_CARET_POLICY);
    
public java.awt.font.TextHitInfogetNextRightHit(java.awt.font.TextHitInfo hit)
Returns the hit for the next caret to the right (bottom); if there is no such hit, returns null. If the hit character index is out of bounds, an {@link IllegalArgumentException} is thrown.

param
hit a hit on a character in this layout
return
a hit whose caret appears at the next position to the right (bottom) of the caret of the provided hit or null.

        ensureCache();
        checkTextHit(hit);

        int caret = hitToCaret(hit);

        if (caret == characterCount) {
            return null;
        }

        do {
            ++caret;
        } while (!caretIsValid(caret));

        return caretToHit(caret);
    
public java.awt.font.TextHitInfogetNextRightHit(int offset, java.awt.font.TextLayout$CaretPolicy policy)
Returns the hit for the next caret to the right (bottom); if no such hit, returns null. The hit is to the right of the strong caret at the specified offset, as determined by the specified policy. The returned hit is the stronger of the two possible hits, as determined by the specified policy.

param
offset an insertion offset in this TextLayout. Cannot be less than 0 or greater than this TextLayout object's character count.
param
policy the policy used to select the strong caret
return
a hit whose caret appears at the next position to the right (bottom) of the caret of the provided hit, or null.


        if (offset < 0 || offset > characterCount) {
            throw new IllegalArgumentException("Offset out of bounds in TextLayout.getNextRightHit()");
        }

        if (policy == null) {
            throw new IllegalArgumentException("Null CaretPolicy passed to TextLayout.getNextRightHit()");
        }

        TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
        TextHitInfo hit2 = hit1.getOtherHit();

        TextHitInfo nextHit = getNextRightHit(policy.getStrongCaret(hit1, hit2, this));

        if (nextHit != null) {
            TextHitInfo otherHit = getVisualOtherHit(nextHit);
            return policy.getStrongCaret(otherHit, nextHit, this);
        }
        else {
            return null;
        }
    
public java.awt.font.TextHitInfogetNextRightHit(int offset)
Returns the hit for the next caret to the right (bottom); if no such hit, returns null. The hit is to the right of the strong caret at the specified offset, as determined by the default policy. The returned hit is the stronger of the two possible hits, as determined by the default policy.

param
offset an insertion offset in this TextLayout. Cannot be less than 0 or greater than the TextLayout object's character count.
return
a hit whose caret appears at the next position to the right (bottom) of the caret of the provided hit, or null.


        return getNextRightHit(offset, DEFAULT_CARET_POLICY);
    
public java.awt.ShapegetOutline(java.awt.geom.AffineTransform tx)
Returns a Shape representing the outline of this TextLayout.

param
tx an optional {@link AffineTransform} to apply to the outline of this TextLayout.
return
a Shape that is the outline of this TextLayout.

	ensureCache();
        return textLine.getOutline(tx);
    
private final java.awt.font.TextHitInfogetStrongHit(java.awt.font.TextHitInfo hit1, java.awt.font.TextHitInfo hit2)
Return the "stronger" of the TextHitInfos. The TextHitInfos should be logical or visual counterparts. They are not checked for validity.


        // right now we're using the following rule for strong hits:
        // A hit on a character with a lower level
        // is stronger than one on a character with a higher level.
        // If this rule ties, the hit on the leading edge of a character wins.
        // If THIS rule ties, hit1 wins.  Both rules shouldn't tie, unless the
        // infos aren't counterparts of some sort.

        byte hit1Level = getCharacterLevel(hit1.getCharIndex());
        byte hit2Level = getCharacterLevel(hit2.getCharIndex());

        if (hit1Level == hit2Level) {
            if (hit2.isLeadingEdge() && !hit1.isLeadingEdge()) {
                return hit2;
            }
            else {
                return hit1;
            }
        }
        else {
            return (hit1Level < hit2Level)? hit1 : hit2;
        }
    
java.awt.font.TextLinegetTextLineForTesting()
Package-only method for testing ONLY. Please don't abuse.


        return textLine;
    
public floatgetVisibleAdvance()
Returns the advance of this TextLayout, minus trailing whitespace.

return
the advance of this TextLayout without the trailing whitespace.
see
#getAdvance()

        ensureCache();
        return visibleAdvance;
    
public java.awt.ShapegetVisualHighlightShape(java.awt.font.TextHitInfo firstEndpoint, java.awt.font.TextHitInfo secondEndpoint, java.awt.geom.Rectangle2D bounds)
Returns a path enclosing the visual selection in the specified range, extended to bounds.

If the selection includes the leftmost (topmost) position, the selection is extended to the left (top) of bounds. If the selection includes the rightmost (bottommost) position, the selection is extended to the right (bottom) of the bounds. The height (width on vertical lines) of the selection is always extended to bounds.

Although the selection is always contiguous, the logically selected text can be discontiguous on lines with mixed-direction text. The logical ranges of text selected can be retrieved using getLogicalRangesForVisualSelection. For example, consider the text 'ABCdef' where capital letters indicate right-to-left text, rendered on a right-to-left line, with a visual selection from 0L (the leading edge of 'A') to 3T (the trailing edge of 'd'). The text appears as follows, with bold underlined areas representing the selection:

defCBA 
The logical selection ranges are 0-3, 4-6 (ABC, ef) because the visually contiguous text is logically discontiguous. Also note that since the rightmost position on the layout (to the right of 'A') is selected, the selection is extended to the right of the bounds.

param
firstEndpoint one end of the visual selection
param
secondEndpoint the other end of the visual selection
param
bounds the bounding rectangle to which to extend the selection
return
a Shape enclosing the selection.
see
#getLogicalRangesForVisualSelection(TextHitInfo, TextHitInfo)
see
#getLogicalHighlightShape(int, int, Rectangle2D)

        ensureCache();

        checkTextHit(firstEndpoint);
        checkTextHit(secondEndpoint);

        if(bounds == null) {
                throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getVisualHighlightShape()");
        }

        GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD);

        int firstCaret = hitToCaret(firstEndpoint);
        int secondCaret = hitToCaret(secondEndpoint);

        result.append(caretBoundingShape(firstCaret, secondCaret, bounds),
                      false);

        if (firstCaret == 0 || secondCaret == 0) {
            result.append(leftShape(bounds), false);
        }

        if (firstCaret == characterCount || secondCaret == characterCount) {
            result.append(rightShape(bounds), false);
        }

        //return new Highlight(result, false);
        return  result;
    
public java.awt.ShapegetVisualHighlightShape(java.awt.font.TextHitInfo firstEndpoint, java.awt.font.TextHitInfo secondEndpoint)
Returns a Shape enclosing the visual selection in the specified range, extended to the bounds. This method is a convenience overload of getVisualHighlightShape that uses the natural bounds of this TextLayout.

param
firstEndpoint one end of the visual selection
param
secondEndpoint the other end of the visual selection
return
a Shape enclosing the selection.

        return getVisualHighlightShape(firstEndpoint, secondEndpoint, getNaturalBounds());
    
public java.awt.font.TextHitInfogetVisualOtherHit(java.awt.font.TextHitInfo hit)
Returns the hit on the opposite side of the specified hit's caret.

param
hit the specified hit
return
a hit that is on the opposite side of the specified hit's caret.


        ensureCache();
        checkTextHit(hit);

        int hitCharIndex = hit.getCharIndex();

        int charIndex;
        boolean leading;

        if (hitCharIndex == -1 || hitCharIndex == characterCount) {

            int visIndex;
            if (textLine.isDirectionLTR() == (hitCharIndex == -1)) {
                visIndex = 0;
            }
            else {
                visIndex = characterCount-1;
            }

            charIndex = textLine.visualToLogical(visIndex);

            if (textLine.isDirectionLTR() == (hitCharIndex == -1)) {
                // at left end
                leading = textLine.isCharLTR(charIndex);
            }
            else {
                // at right end
                leading = !textLine.isCharLTR(charIndex);
            }
        }
        else {

            int visIndex = textLine.logicalToVisual(hitCharIndex);

            boolean movedToRight;
            if (textLine.isCharLTR(hitCharIndex) == hit.isLeadingEdge()) {
                --visIndex;
                movedToRight = false;
            }
            else {
                ++visIndex;
                movedToRight = true;
            }

            if (visIndex > -1 && visIndex < characterCount) {
                charIndex = textLine.visualToLogical(visIndex);
                leading = movedToRight == textLine.isCharLTR(charIndex);
            }
            else {
                charIndex =
                    (movedToRight == textLine.isDirectionLTR())? characterCount : -1;
                leading = charIndex == characterCount;
            }
        }

        return leading? TextHitInfo.leading(charIndex) :
                                TextHitInfo.trailing(charIndex);
    
protected voidhandleJustify(float justificationWidth)
Justify this layout. Overridden by subclassers to control justification (if there were subclassers, that is...) The layout will only justify if the paragraph attributes (from the source text, possibly defaulted by the layout attributes) indicate a non-zero justification ratio. The text will be justified to the indicated width. The current implementation also adjusts hanging punctuation and trailing whitespace to overhang the justification width. Once justified, the layout may not be rejustified.

Some code may rely on immutablity of layouts. Subclassers should not call this directly, but instead should call getJustifiedLayout, which will call this method on a clone of this layout, preserving the original.

param
justificationWidth the width to use when justifying the line. For best results, it should not be too different from the current advance of the line.
see
#getJustifiedLayout(float)

      // never called
    
public inthashCode()
Returns the hash code of this TextLayout.

return
the hash code of this TextLayout.

        if (hashCodeCache == 0) {
	    ensureCache();
            hashCodeCache = textLine.hashCode();
        }
        return hashCodeCache;
    
public java.awt.font.TextHitInfohitTestChar(float x, float y, java.awt.geom.Rectangle2D bounds)
Returns a TextHitInfo corresponding to the specified point. Coordinates outside the bounds of the TextLayout map to hits on the leading edge of the first logical character, or the trailing edge of the last logical character, as appropriate, regardless of the position of that character in the line. Only the direction along the baseline is used to make this evaluation.

param
x the x offset from the origin of this TextLayout
param
y the y offset from the origin of this TextLayout
param
bounds the bounds of the TextLayout
return
a hit describing the character and edge (leading or trailing) under the specified point.

        // check boundary conditions

        if (isVertical()) {
            if (y < bounds.getMinY()) {
                return TextHitInfo.leading(0);
            } else if (y >= bounds.getMaxY()) {
                return TextHitInfo.trailing(characterCount-1);
            }
        } else {
            if (x < bounds.getMinX()) {
                return isLeftToRight() ? TextHitInfo.leading(0) : TextHitInfo.trailing(characterCount-1);
            } else if (x >= bounds.getMaxX()) {
                return isLeftToRight() ? TextHitInfo.trailing(characterCount-1) : TextHitInfo.leading(0);
            }
        }
 
        // revised hit test
        // the original seems too complex and fails miserably with italic offsets
        // the natural tendency is to move towards the character you want to hit
        // so we'll just measure distance to the center of each character's visual
        // bounds, pick the closest one, then see which side of the character's
        // center line (italic) the point is on.
        // this tends to make it easier to hit narrow characters, which can be a
        // bit odd if you're visually over an adjacent wide character. this makes
        // a difference with bidi, so perhaps i need to revisit this yet again.

        double distance = Double.MAX_VALUE;
        int index = 0;
        int trail = -1;
        CoreMetrics lcm = null;
        float icx = 0, icy = 0, ia = 0, cy = 0, dya = 0, ydsq = 0;

        for (int i = 0; i < characterCount; ++i) {
            if (!textLine.caretAtOffsetIsValid(i)) {
                continue;
            }
            if (trail == -1) {
                trail = i;
            }
            CoreMetrics cm = textLine.getCoreMetricsAt(i);
            if (cm != lcm) {
                lcm = cm;
                // just work around baseline mess for now
                if (cm.baselineIndex == GraphicAttribute.TOP_ALIGNMENT) {
                    cy = -(textLine.getMetrics().ascent - cm.ascent) + cm.ssOffset;
                } else if (cm.baselineIndex == GraphicAttribute.BOTTOM_ALIGNMENT) {
                    cy = textLine.getMetrics().descent - cm.descent + cm.ssOffset;
                } else {
                    cy = cm.effectiveBaselineOffset(baselineOffsets) + cm.ssOffset;
                }
                float dy = (cm.descent - cm.ascent) / 2 - cy;
                dya = dy * cm.italicAngle;
                cy += dy;
                ydsq = (cy - y)*(cy - y);
            }
            float cx = textLine.getCharXPosition(i);
            float ca = textLine.getCharAdvance(i);
            float dx = ca / 2;
            cx += dx - dya;
 
            // proximity in x (along baseline) is two times as important as proximity in y
            double nd = Math.sqrt(4*(cx - x)*(cx - x) + ydsq);
            if (nd < distance) {
                distance = nd;
                index = i;
                trail = -1;
                icx = cx; icy = cy; ia = cm.italicAngle;
            }
        }
        boolean left = x < icx - (y - icy) * ia;
        boolean leading = textLine.isCharLTR(index) == left;
        if (trail == -1) {
            trail = characterCount;
        }
        TextHitInfo result = leading ? TextHitInfo.leading(index) : 
            TextHitInfo.trailing(trail-1);
        return result;
    
public java.awt.font.TextHitInfohitTestChar(float x, float y)
Returns a TextHitInfo corresponding to the specified point. This method is a convenience overload of hitTestChar that uses the natural bounds of this TextLayout.

param
x the x offset from the origin of this TextLayout
param
y the y offset from the origin of this TextLayout
return
a hit describing the character and edge (leading or trailing) under the specified point.


        return hitTestChar(x, y, getNaturalBounds());
    
private inthitToCaret(java.awt.font.TextHitInfo hit)
Returns a caret index corresponding to hit. Carets are numbered from left to right (top to bottom) starting from zero. This always places carets next to the character hit, on the indicated side of the character.

param
hit a hit on a character in this TextLayout
return
a caret index corresponding to the specified hit.


        int hitIndex = hit.getCharIndex();

        if (hitIndex < 0) {
            return textLine.isDirectionLTR() ? 0 : characterCount;
        } else if (hitIndex >= characterCount) {
            return textLine.isDirectionLTR() ? characterCount : 0;
        }

        int visIndex = textLine.logicalToVisual(hitIndex);

        if (hit.isLeadingEdge() != textLine.isCharLTR(hitIndex)) {
            ++visIndex;
        }

        return visIndex;
    
private voidinitTextLine()

        textLine = optInfo.createTextLine();
        optInfo = null;
    
public booleanisLeftToRight()
Returns true if this TextLayout has a left-to-right base direction or false if it has a right-to-left base direction. The TextLayout has a base direction of either left-to-right (LTR) or right-to-left (RTL). The base direction is independent of the actual direction of text on the line, which may be either LTR, RTL, or mixed. Left-to-right layouts by default should position flush left. If the layout is on a tabbed line, the tabs run left to right, so that logically successive layouts position left to right. The opposite is true for RTL layouts. By default they should position flush left, and tabs run right-to-left.

return
true if the base direction of this TextLayout is left-to-right; false otherwise.

        return (optInfo != null) || textLine.isDirectionLTR();
    
public booleanisVertical()
Returns true if this TextLayout is vertical.

return
true if this TextLayout is vertical; false otherwise.

        return isVerticalLine;
    
private java.awt.geom.GeneralPathleftShape(java.awt.geom.Rectangle2D bounds)


        double[] path0;
        if (isVerticalLine) {
            path0 = new double[] { bounds.getX(), bounds.getY(),
                                       bounds.getX() + bounds.getWidth(),
                                       bounds.getY() };
        } else {
            path0 = new double[] { bounds.getX(),
                                       bounds.getY() + bounds.getHeight(),
                                       bounds.getX(), bounds.getY() };
        }

        double[] path1 = getCaretPath(0, bounds, true);

        return boundingShape(path0, path1);
    
private voidparagraphInit(byte aBaseline, sun.font.CoreMetrics lm, java.util.Map paragraphAttrs, char[] text)
Initialize the paragraph-specific data.

        
        baseline = aBaseline;

        // normalize to current baseline
        baselineOffsets = TextLine.getNormalizedOffsets(lm.baselineOffsets, baseline);
        
        justifyRatio = TextLine.getJustifyRatio(paragraphAttrs);

	if (paragraphAttrs != null) {
	    Object o = paragraphAttrs.get(TextAttribute.NUMERIC_SHAPING);
	    if (o != null) {
		try {
		  NumericShaper shaper = (NumericShaper)o;
		  shaper.shape(text, 0, text.length);
		}
		catch (ClassCastException e) {
		}
	    }
	}
    
private static java.awt.geom.GeneralPathpathToShape(double[] path, boolean close)

        GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD, path.length);
        result.moveTo((float)path[0], (float)path[1]);
        for (int i = 2; i < path.length; i += 2) {
            result.lineTo((float)path[i], (float)path[i+1]);
        }
        if (close) {
            result.closePath();
        }

        return result;
    
private java.awt.geom.GeneralPathrightShape(java.awt.geom.Rectangle2D bounds)

        double[] path1;
        if (isVerticalLine) {
            path1 = new double[] {
                bounds.getX(),
                bounds.getY() + bounds.getHeight(),
                bounds.getX() + bounds.getWidth(),
                bounds.getY() + bounds.getHeight()
            };
        } else {
            path1 = new double[] {
                bounds.getX() + bounds.getWidth(),
                bounds.getY() + bounds.getHeight(),
                bounds.getX() + bounds.getWidth(),
                bounds.getY()
            };
        }

        double[] path0 = getCaretPath(characterCount, bounds, true);

        return boundingShape(path0, path1);
    
private static intsameBaselineUpTo(java.awt.Font font, char[] text, int start, int limit)
Return the index of the first character with a different baseline from the character at start, or limit if all characters between start and limit have the same baseline.

        // current implementation doesn't support multiple baselines
        return limit;
        /*
        byte bl = font.getBaselineFor(text[start++]);
        while (start < limit && font.getBaselineFor(text[start]) == bl) {
            ++start;
        }
        return start;
        */
    
private static java.awt.FontsingleFont(char[] text, int start, int limit, java.util.Map attributes)


        if (attributes.get(TextAttribute.CHAR_REPLACEMENT) != null) {
            return null;
        }

        Font font = (Font)attributes.get(TextAttribute.FONT);
        if (font == null) {
            if (attributes.get(TextAttribute.FAMILY) != null) {            
                font = Font.getFont(attributes);
                if (font.canDisplayUpTo(text, start, limit) != -1) {
                    return null;
                }
            } else {
                FontResolver resolver = FontResolver.getInstance();
		CodePointIterator iter = CodePointIterator.create(text, start, limit);
		int fontIndex = resolver.nextFontRunIndex(iter);
		if (iter.charIndex() == limit) {
		    font = resolver.getFont(fontIndex, attributes);
		}
            }
        }

        if (sameBaselineUpTo(font, text, start, limit) != limit) {
            return null;
        }

        return font;
    
private voidstandardInit(java.text.AttributedCharacterIterator text, char[] chars, java.awt.font.FontRenderContext frc)


        characterCount = chars.length;

        // set paragraph attributes
        {
            // If there's an embedded graphic at the start of the
            // paragraph, look for the first non-graphic character
            // and use it and its font to initialize the paragraph.
            // If not, use the first graphic to initialize.

            Map paragraphAttrs = text.getAttributes();

            boolean haveFont = TextLine.advanceToFirstFont(text);

            if (haveFont) {
                Font defaultFont = TextLine.getFontAtCurrentPos(text);
                int charsStart = text.getIndex() - text.getBeginIndex();
                LineMetrics lm = defaultFont.getLineMetrics(chars, charsStart, charsStart+1, frc);
                CoreMetrics cm = CoreMetrics.get(lm);
                paragraphInit((byte)cm.baselineIndex, cm, paragraphAttrs, chars);
            }
            else {
                // hmmm what to do here?  Just try to supply reasonable
                // values I guess.

                GraphicAttribute graphic = (GraphicAttribute)
                                paragraphAttrs.get(TextAttribute.CHAR_REPLACEMENT);
                byte defaultBaseline = getBaselineFromGraphic(graphic);
		CoreMetrics cm = GraphicComponent.createCoreMetrics(graphic);
                paragraphInit(defaultBaseline, cm, paragraphAttrs, chars);
            }
        }

        textLine = TextLine.standardCreateTextLine(frc, text, chars, baselineOffsets);
    
public java.lang.StringtoString()
Returns debugging information for this TextLayout.

return
the textLine of this TextLayout as a String.

	ensureCache();
        return textLine.toString();