| /* TextLayout.java -- | |
| Copyright (C) 2006 Free Software Foundation, Inc. | |
| This file is part of GNU Classpath. | |
| GNU Classpath is free software; you can redistribute it and/or modify | |
| it under the terms of the GNU General Public License as published by | |
| the Free Software Foundation; either version 2, or (at your option) | |
| any later version. | |
| GNU Classpath is distributed in the hope that it will be useful, but | |
| WITHOUT ANY WARRANTY; without even the implied warranty of | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| General Public License for more details. | |
| You should have received a copy of the GNU General Public License | |
| along with GNU Classpath; see the file COPYING. If not, write to the | |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | |
| 02110-1301 USA. | |
| Linking this library statically or dynamically with other modules is | |
| making a combined work based on this library. Thus, the terms and | |
| conditions of the GNU General Public License cover the whole | |
| combination. | |
| As a special exception, the copyright holders of this library give you | |
| permission to link this library with independent modules to produce an | |
| executable, regardless of the license terms of these independent | |
| modules, and to copy and distribute the resulting executable under | |
| terms of your choice, provided that you also meet, for each linked | |
| independent module, the terms and conditions of the license of that | |
| module. An independent module is a module which is not derived from | |
| or based on this library. If you modify this library, you may extend | |
| this exception to your version of the library, but you are not | |
| obligated to do so. If you do not wish to do so, delete this | |
| 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.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.Map; | |
| /** | |
| * @author Sven de Marothy | |
| */ | |
| public final class TextLayout implements Cloneable | |
| { | |
| private GlyphVector[] runs; | |
| private Font font; | |
| private FontRenderContext frc; | |
| private String string; | |
| 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. | |
| */ | |
| private int[][] runIndices; | |
| /** | |
| * Character indices. | |
| * Fixt index is the glyphvector, second index is the (first) glyph. | |
| */ | |
| private int[][] charIndices; | |
| /** | |
| * Base directionality, determined from the first char. | |
| */ | |
| private boolean leftToRight; | |
| /** | |
| * Whether this layout contains whitespace or not. | |
| */ | |
| private boolean hasWhitespace = false; | |
| /** | |
| * The default caret policy. | |
| */ | |
| public static final TextLayout.CaretPolicy DEFAULT_CARET_POLICY = new CaretPolicy(); | |
| /** | |
| * Constructs a TextLayout. | |
| */ | |
| public TextLayout (String string, Font font, FontRenderContext frc) | |
| { | |
| this.font = font; | |
| this.frc = frc; | |
| this.string = string; | |
| lm = font.getLineMetrics(string, frc); | |
| // Get base direction and whitespace info | |
| getStringProperties(); | |
| if( Bidi.requiresBidi( string.toCharArray(), 0, string.length() ) ) | |
| { | |
| Bidi bidi = new Bidi( string, 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++) | |
| { | |
| runIndices[i][0] = bidi.getRunStart( i ); | |
| runIndices[i][1] = bidi.getRunLimit( i ); | |
| if( runIndices[i][0] != runIndices[i][1] ) // 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 ); | |
| } | |
| } | |
| Bidi.reorderVisually( table, 0, runs, 0, runs.length ); | |
| } | |
| 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 ); | |
| } | |
| setCharIndices(); | |
| } | |
| public TextLayout (String string, Map attributes, FontRenderContext frc) | |
| { | |
| this( string, new Font( attributes ), frc ); | |
| } | |
| public TextLayout (AttributedCharacterIterator text, FontRenderContext frc) | |
| { | |
| // FIXME: Very rudimentary. | |
| this(getText(text), getFont(text), frc); | |
| } | |
| /** | |
| * Package-private constructor to make a textlayout from an existing one. | |
| * This is used by TextMeasurer for returning sub-layouts, and it | |
| * saves a lot of time in not having to relayout the text. | |
| */ | |
| TextLayout(TextLayout t, int startIndex, int endIndex) | |
| { | |
| font = t.font; | |
| frc = t.frc; | |
| boundsCache = null; | |
| lm = t.lm; | |
| leftToRight = t.leftToRight; | |
| if( endIndex > t.getCharacterCount() ) | |
| endIndex = t.getCharacterCount(); | |
| string = t.string.substring( startIndex, endIndex ); | |
| int startingRun = t.charIndices[startIndex][0]; | |
| int nRuns = 1 + t.charIndices[endIndex - 1][0] - startingRun; | |
| runIndices = new int[ nRuns ][2]; | |
| runs = new GlyphVector[ nRuns ]; | |
| for( int i = 0; i < nRuns; i++ ) | |
| { | |
| GlyphVector run = t.runs[ i + startingRun ]; | |
| // 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() : | |
| 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; | |
| } | |
| runIndices[ nRuns - 1 ][1] = endIndex - 1; | |
| setCharIndices(); | |
| determineWhiteSpace(); | |
| } | |
| private void setCharIndices() | |
| { | |
| charIndices = new int[ getCharacterCount() ][2]; | |
| int i = 0; | |
| int currentChar = 0; | |
| 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++; | |
| } | |
| } | |
| } | |
| } | |
| private static String getText(AttributedCharacterIterator iter) | |
| { | |
| StringBuffer sb = new StringBuffer(); | |
| int idx = iter.getIndex(); | |
| for(char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) | |
| sb.append(c); | |
| iter.setIndex( idx ); | |
| return sb.toString(); | |
| } | |
| private static Font getFont(AttributedCharacterIterator iter) | |
| { | |
| Font f = (Font)iter.getAttribute(TextAttribute.FONT); | |
| if( f == null ) | |
| { | |
| int size; | |
| Float i = (Float)iter.getAttribute(TextAttribute.SIZE); | |
| if( i != null ) | |
| size = (int)i.floatValue(); | |
| else | |
| size = 14; | |
| f = new Font("Dialog", Font.PLAIN, size ); | |
| } | |
| return f; | |
| } | |
| /** | |
| * Scan the character run for the first strongly directional character, | |
| * which in turn defines the base directionality of the whole layout. | |
| */ | |
| private void getStringProperties() | |
| { | |
| boolean gotDirection = false; | |
| int i = 0; | |
| leftToRight = true; | |
| while( i < string.length() && !gotDirection ) | |
| switch( Character.getDirectionality( string.charAt( i++ ) ) ) | |
| { | |
| case Character.DIRECTIONALITY_LEFT_TO_RIGHT: | |
| case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: | |
| case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: | |
| gotDirection = true; | |
| break; | |
| case Character.DIRECTIONALITY_RIGHT_TO_LEFT: | |
| case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: | |
| case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: | |
| case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: | |
| leftToRight = false; | |
| gotDirection = true; | |
| break; | |
| } | |
| determineWhiteSpace(); | |
| } | |
| private void determineWhiteSpace() | |
| { | |
| // Determine if there's whitespace in the thing. | |
| // Ignore trailing chars. | |
| int i = string.length() - 1; | |
| hasWhitespace = false; | |
| while( i >= 0 && Character.isWhitespace( string.charAt(i) ) ) | |
| i--; | |
| // Check the remaining chars | |
| while( i >= 0 ) | |
| if( Character.isWhitespace( string.charAt(i--) ) ) | |
| hasWhitespace = true; | |
| } | |
| protected Object clone () | |
| { | |
| return new TextLayout( string, font, frc ); | |
| } | |
| 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(); | |
| } | |
| } | |
| public boolean equals (Object obj) | |
| { | |
| if( !( obj instanceof TextLayout) ) | |
| return false; | |
| return equals( (TextLayout) obj ); | |
| } | |
| public boolean equals (TextLayout tl) | |
| { | |
| if( runs.length != tl.runs.length ) | |
| return false; | |
| // Compare all glyph vectors. | |
| for( int i = 0; i < runs.length; i++ ) | |
| if( !runs[i].equals( tl.runs[i] ) ) | |
| return false; | |
| return true; | |
| } | |
| public float getAdvance () | |
| { | |
| float totalAdvance = 0f; | |
| for(int i = 0; i < runs.length; i++) | |
| totalAdvance += runs[i].getLogicalBounds().getWidth(); | |
| return totalAdvance; | |
| } | |
| public float getAscent () | |
| { | |
| return lm.getAscent(); | |
| } | |
| public byte getBaseline () | |
| { | |
| return (byte)lm.getBaselineIndex(); | |
| } | |
| public float[] getBaselineOffsets () | |
| { | |
| return lm.getBaselineOffsets(); | |
| } | |
| public Shape getBlackBoxBounds (int firstEndpoint, int secondEndpoint) | |
| { | |
| if( secondEndpoint - firstEndpoint <= 0 ) | |
| return new Rectangle2D.Float(); // Hmm? | |
| if( firstEndpoint < 0 || secondEndpoint > getCharacterCount()) | |
| return new Rectangle2D.Float(); | |
| GeneralPath gp = new GeneralPath(); | |
| int ri = charIndices[ firstEndpoint ][0]; | |
| int gi = charIndices[ firstEndpoint ][1]; | |
| double advance = 0; | |
| for( int i = 0; i < ri; i++ ) | |
| advance += runs[i].getLogicalBounds().getWidth(); | |
| for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ ) | |
| { | |
| int dg; | |
| if( i == charIndices[ secondEndpoint - 1 ][0] ) | |
| dg = charIndices[ secondEndpoint - 1][1]; | |
| else | |
| dg = runs[i].getNumGlyphs() - 1; | |
| for( int j = 0; j <= dg; j++ ) | |
| { | |
| Rectangle2D r2 = (runs[i].getGlyphVisualBounds( j )). | |
| getBounds2D(); | |
| Point2D p = runs[i].getGlyphPosition( j ); | |
| r2.setRect( advance + r2.getX(), r2.getY(), | |
| r2.getWidth(), r2.getHeight() ); | |
| gp.append(r2, false); | |
| } | |
| advance += runs[i].getLogicalBounds().getWidth(); | |
| } | |
| return gp; | |
| } | |
| public Rectangle2D getBounds() | |
| { | |
| if( boundsCache == null ) | |
| boundsCache = getOutline(new AffineTransform()).getBounds(); | |
| return boundsCache; | |
| } | |
| public float[] getCaretInfo (TextHitInfo hit) | |
| { | |
| return getCaretInfo(hit, getBounds()); | |
| } | |
| public float[] getCaretInfo (TextHitInfo hit, Rectangle2D bounds) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public Shape getCaretShape (TextHitInfo hit) | |
| { | |
| return getCaretShape( hit, getBounds() ); | |
| } | |
| public Shape getCaretShape (TextHitInfo hit, Rectangle2D bounds) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public Shape[] getCaretShapes (int offset) | |
| { | |
| return getCaretShapes( offset, getBounds() ); | |
| } | |
| public Shape[] getCaretShapes (int offset, Rectangle2D bounds) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public int getCharacterCount () | |
| { | |
| return string.length(); | |
| } | |
| public byte getCharacterLevel (int index) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public float getDescent () | |
| { | |
| return lm.getDescent(); | |
| } | |
| public TextLayout getJustifiedLayout (float justificationWidth) | |
| { | |
| TextLayout newLayout = (TextLayout)clone(); | |
| if( hasWhitespace ) | |
| newLayout.handleJustify( justificationWidth ); | |
| return newLayout; | |
| } | |
| public float getLeading () | |
| { | |
| return lm.getLeading(); | |
| } | |
| public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint) | |
| { | |
| return getLogicalHighlightShape( firstEndpoint, secondEndpoint, | |
| getBounds() ); | |
| } | |
| public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint, | |
| Rectangle2D bounds) | |
| { | |
| if( secondEndpoint - firstEndpoint <= 0 ) | |
| return new Rectangle2D.Float(); // Hmm? | |
| if( firstEndpoint < 0 || secondEndpoint > getCharacterCount()) | |
| return new Rectangle2D.Float(); | |
| Rectangle2D r = null; | |
| int ri = charIndices[ firstEndpoint ][0]; | |
| int gi = charIndices[ firstEndpoint ][1]; | |
| double advance = 0; | |
| for( int i = 0; i < ri; i++ ) | |
| advance += runs[i].getLogicalBounds().getWidth(); | |
| for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ ) | |
| { | |
| 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; | |
| for(; gi <= dg; gi++ ) | |
| { | |
| Rectangle2D r2 = (runs[i].getGlyphLogicalBounds( gi )). | |
| getBounds2D(); | |
| if( r == null ) | |
| r = r2; | |
| else | |
| r = r.createUnion(r2); | |
| } | |
| gi = 0; // reset glyph index into run for next run. | |
| advance += runs[i].getLogicalBounds().getWidth(); | |
| } | |
| return r; | |
| } | |
| public int[] getLogicalRangesForVisualSelection (TextHitInfo firstEndpoint, | |
| TextHitInfo secondEndpoint) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public TextHitInfo getNextLeftHit (int offset) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public TextHitInfo getNextLeftHit (TextHitInfo hit) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public TextHitInfo getNextRightHit (int offset) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public TextHitInfo getNextRightHit (TextHitInfo hit) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public Shape getOutline (AffineTransform tx) | |
| { | |
| float x = 0f; | |
| 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(); | |
| x += r.getWidth(); | |
| } | |
| if( tx != null ) | |
| gp.transform( tx ); | |
| return gp; | |
| } | |
| public float getVisibleAdvance () | |
| { | |
| float totalAdvance = 0f; | |
| if( runs.length <= 0 ) | |
| return 0f; | |
| // No trailing whitespace | |
| if( !Character.isWhitespace( string.charAt( string.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(); | |
| int lastRun = runIndices[ runs.length - 1 ][0]; | |
| int j = string.length() - 1; | |
| while( j >= lastRun && Character.isWhitespace( string.charAt( 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 ) | |
| <= lastNonWSChar ) | |
| { | |
| totalAdvance += runs[ runs.length - 1 ].getGlyphLogicalBounds( j ). | |
| getBounds2D().getWidth(); | |
| j ++; | |
| } | |
| return totalAdvance; | |
| } | |
| public Shape getVisualHighlightShape (TextHitInfo firstEndpoint, | |
| TextHitInfo secondEndpoint) | |
| { | |
| return getVisualHighlightShape( firstEndpoint, secondEndpoint, | |
| getBounds() ); | |
| } | |
| public Shape getVisualHighlightShape (TextHitInfo firstEndpoint, | |
| TextHitInfo secondEndpoint, | |
| Rectangle2D bounds) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public TextHitInfo getVisualOtherHit (TextHitInfo hit) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| /** | |
| * This is a protected method of a <code>final</code> class, meaning | |
| * it exists only to taunt you. | |
| */ | |
| protected void handleJustify (float justificationWidth) | |
| { | |
| // We assume that the text has non-trailing whitespace. | |
| // First get the change in width to insert into the whitespaces. | |
| double deltaW = justificationWidth - getVisibleAdvance(); | |
| int nglyphs = 0; // # of whitespace chars | |
| // determine last non-whitespace char. | |
| int lastNWS = string.length() - 1; | |
| while( Character.isWhitespace( string.charAt( lastNWS ) ) ) lastNWS--; | |
| // locations of the glyphs. | |
| int[] wsglyphs = new int[string.length() * 10]; | |
| for(int run = 0; run < runs.length; run++ ) | |
| for(int i = 0; i < runs[run].getNumGlyphs(); i++ ) | |
| { | |
| int cindex = runIndices[run][0] + runs[run].getGlyphCharIndex( i ); | |
| if( Character.isWhitespace( string.charAt( cindex ) ) ) | |
| // && cindex < lastNWS ) | |
| { | |
| wsglyphs[ nglyphs * 2 ] = run; | |
| wsglyphs[ nglyphs * 2 + 1] = i; | |
| 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 ); | |
| } | |
| } | |
| public TextHitInfo hitTestChar (float x, float y) | |
| { | |
| return hitTestChar(x, y, getBounds()); | |
| } | |
| public TextHitInfo hitTestChar (float x, float y, Rectangle2D bounds) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public boolean isLeftToRight () | |
| { | |
| return leftToRight; | |
| } | |
| public boolean isVertical () | |
| { | |
| return false; // FIXME: How do you create a vertical layout? | |
| } | |
| public int hashCode () | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| public String toString () | |
| { | |
| return "TextLayout [string:"+string+", Font:"+font+" Rendercontext:"+ | |
| frc+"]"; | |
| } | |
| /** | |
| * Inner class describing a caret policy | |
| */ | |
| public static class CaretPolicy | |
| { | |
| public CaretPolicy() | |
| { | |
| } | |
| public TextHitInfo getStrongCaret(TextHitInfo hit1, | |
| TextHitInfo hit2, | |
| TextLayout layout) | |
| throws NotImplementedException | |
| { | |
| throw new Error ("not implemented"); | |
| } | |
| } | |
| } | |