libjava/ChangeLog:
[official-gcc.git] / libjava / classpath / java / awt / font / TextLayout.java
blob4346ab91d5362887dd9da283a412ce0fef1b6786
1 /* TextLayout.java --
2 Copyright (C) 2006 Free Software Foundation, Inc.
4 This file is part of GNU Classpath.
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING. If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301 USA.
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library. Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module. An independent module is a module which is not derived from
33 or based on this library. If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so. If you do not wish to do so, delete this
36 exception statement from your version. */
39 package java.awt.font;
41 import gnu.java.lang.CPStringBuilder;
43 import java.awt.Font;
44 import java.awt.Graphics2D;
45 import java.awt.Shape;
46 import java.awt.geom.AffineTransform;
47 import java.awt.geom.Line2D;
48 import java.awt.geom.Rectangle2D;
49 import java.awt.geom.GeneralPath;
50 import java.awt.geom.Point2D;
51 import java.text.CharacterIterator;
52 import java.text.AttributedCharacterIterator;
53 import java.text.Bidi;
54 import java.util.ArrayList;
55 import java.util.Map;
57 /**
58 * @author Sven de Marothy
60 public final class TextLayout implements Cloneable
62 /**
63 * Holds the layout data that belongs to one run of characters.
65 private class Run
67 /**
68 * The actual glyph vector.
70 GlyphVector glyphVector;
72 /**
73 * The font for this text run.
75 Font font;
77 /**
78 * The start of the run.
80 int runStart;
82 /**
83 * The end of the run.
85 int runEnd;
87 /**
88 * The layout location of the beginning of the run.
90 float location;
92 /**
93 * Initializes the Run instance.
95 * @param gv the glyph vector
96 * @param start the start index of the run
97 * @param end the end index of the run
99 Run(GlyphVector gv, Font f, int start, int end)
101 glyphVector = gv;
102 font = f;
103 runStart = start;
104 runEnd = end;
108 * Returns <code>true</code> when this run is left to right,
109 * <code>false</code> otherwise.
111 * @return <code>true</code> when this run is left to right,
112 * <code>false</code> otherwise
114 boolean isLeftToRight()
116 return (glyphVector.getLayoutFlags() & GlyphVector.FLAG_RUN_RTL) == 0;
121 * The laid out character runs.
123 private Run[] runs;
125 private FontRenderContext frc;
126 private char[] string;
127 private int offset;
128 private int length;
129 private Rectangle2D boundsCache;
130 private LineMetrics lm;
133 * The total advance of this text layout. This is cache for maximum
134 * performance.
136 private float totalAdvance = -1F;
139 * The cached natural bounds.
141 private Rectangle2D naturalBounds;
144 * Character indices.
145 * Fixt index is the glyphvector, second index is the (first) glyph.
147 private int[][] charIndices;
150 * Base directionality, determined from the first char.
152 private boolean leftToRight;
155 * Whether this layout contains whitespace or not.
157 private boolean hasWhitespace = false;
160 * The {@link Bidi} object that is used for reordering and by
161 * {@link #getCharacterLevel(int)}.
163 private Bidi bidi;
166 * Mpas the logical position of each individual character in the original
167 * string to its visual position.
169 private int[] logicalToVisual;
172 * Maps visual positions of a character to its logical position
173 * in the original string.
175 private int[] visualToLogical;
178 * The cached hashCode.
180 private int hash;
183 * The default caret policy.
185 public static final TextLayout.CaretPolicy DEFAULT_CARET_POLICY =
186 new CaretPolicy();
189 * Constructs a TextLayout.
191 public TextLayout (String str, Font font, FontRenderContext frc)
193 this.frc = frc;
194 string = str.toCharArray();
195 offset = 0;
196 length = this.string.length;
197 lm = font.getLineMetrics(this.string, offset, length, frc);
199 // Get base direction and whitespace info
200 getStringProperties();
202 if (Bidi.requiresBidi(string, offset, offset + length))
204 bidi = new Bidi(str, leftToRight ? Bidi.DIRECTION_LEFT_TO_RIGHT
205 : Bidi.DIRECTION_RIGHT_TO_LEFT );
206 int rc = bidi.getRunCount();
207 byte[] table = new byte[ rc ];
208 for(int i = 0; i < table.length; i++)
209 table[i] = (byte)bidi.getRunLevel(i);
211 runs = new Run[rc];
212 for(int i = 0; i < rc; i++)
214 int start = bidi.getRunStart(i);
215 int end = bidi.getRunLimit(i);
216 if(start != end) // no empty runs.
218 GlyphVector gv = font.layoutGlyphVector(frc,
219 string, start, end,
220 ((table[i] & 1) == 0) ? Font.LAYOUT_LEFT_TO_RIGHT
221 : Font.LAYOUT_RIGHT_TO_LEFT );
222 runs[i] = new Run(gv, font, start, end);
225 Bidi.reorderVisually( table, 0, runs, 0, runs.length );
226 // Clean up null runs.
227 ArrayList cleaned = new ArrayList(rc);
228 for (int i = 0; i < rc; i++)
230 if (runs[i] != null)
231 cleaned.add(runs[i]);
233 runs = new Run[cleaned.size()];
234 runs = (Run[]) cleaned.toArray(runs);
236 else
238 GlyphVector gv = font.layoutGlyphVector( frc, string, offset, length,
239 leftToRight ? Font.LAYOUT_LEFT_TO_RIGHT
240 : Font.LAYOUT_RIGHT_TO_LEFT );
241 Run run = new Run(gv, font, 0, length);
242 runs = new Run[]{ run };
244 setCharIndices();
245 setupMappings();
246 layoutRuns();
249 public TextLayout (String string,
250 Map<? extends AttributedCharacterIterator.Attribute, ?> attributes,
251 FontRenderContext frc)
253 this( string, new Font( attributes ), frc );
256 public TextLayout (AttributedCharacterIterator text, FontRenderContext frc)
258 // FIXME: Very rudimentary.
259 this(getText(text), getFont(text), frc);
263 * Package-private constructor to make a textlayout from an existing one.
264 * This is used by TextMeasurer for returning sub-layouts, and it
265 * saves a lot of time in not having to relayout the text.
267 TextLayout(TextLayout t, int startIndex, int endIndex)
269 frc = t.frc;
270 boundsCache = null;
271 lm = t.lm;
272 leftToRight = t.leftToRight;
274 if( endIndex > t.getCharacterCount() )
275 endIndex = t.getCharacterCount();
276 string = t.string;
277 offset = startIndex + offset;
278 length = endIndex - startIndex;
280 int startingRun = t.charIndices[startIndex][0];
281 int nRuns = 1 + t.charIndices[endIndex - 1][0] - startingRun;
283 runs = new Run[nRuns];
284 for( int i = 0; i < nRuns; i++ )
286 Run run = t.runs[i + startingRun];
287 GlyphVector gv = run.glyphVector;
288 Font font = run.font;
289 // Copy only the relevant parts of the first and last runs.
290 int beginGlyphIndex = (i > 0) ? 0 : t.charIndices[startIndex][1];
291 int numEntries = ( i < nRuns - 1) ? gv.getNumGlyphs() :
292 1 + t.charIndices[endIndex - 1][1] - beginGlyphIndex;
294 int[] codes = gv.getGlyphCodes(beginGlyphIndex, numEntries, null);
295 gv = font.createGlyphVector(frc, codes);
296 runs[i] = new Run(gv, font, run.runStart - startIndex,
297 run.runEnd - startIndex);
299 runs[nRuns - 1].runEnd = endIndex - 1;
301 setCharIndices();
302 setupMappings();
303 determineWhiteSpace();
304 layoutRuns();
307 private void setCharIndices()
309 charIndices = new int[ getCharacterCount() ][2];
310 int i = 0;
311 int currentChar = 0;
312 for(int run = 0; run < runs.length; run++)
314 currentChar = -1;
315 Run current = runs[run];
316 GlyphVector gv = current.glyphVector;
317 for( int gi = 0; gi < gv.getNumGlyphs(); gi++)
319 if( gv.getGlyphCharIndex( gi ) != currentChar )
321 charIndices[ i ][0] = run;
322 charIndices[ i ][1] = gi;
323 currentChar = gv.getGlyphCharIndex( gi );
324 i++;
331 * Initializes the logicalToVisual and visualToLogial maps.
333 private void setupMappings()
335 int numChars = getCharacterCount();
336 logicalToVisual = new int[numChars];
337 visualToLogical = new int[numChars];
338 int lIndex = 0;
339 int vIndex = 0;
340 // We scan the runs in visual order and set the mappings accordingly.
341 for (int i = 0; i < runs.length; i++)
343 Run run = runs[i];
344 if (run.isLeftToRight())
346 for (lIndex = run.runStart; lIndex < run.runEnd; lIndex++)
348 logicalToVisual[lIndex] = vIndex;
349 visualToLogical[vIndex] = lIndex;
350 vIndex++;
353 else
355 for (lIndex = run.runEnd - 1; lIndex >= run.runStart; lIndex--)
357 logicalToVisual[lIndex] = vIndex;
358 visualToLogical[vIndex] = lIndex;
359 vIndex++;
365 private static String getText(AttributedCharacterIterator iter)
367 CPStringBuilder sb = new CPStringBuilder();
368 int idx = iter.getIndex();
369 for(char c = iter.first(); c != CharacterIterator.DONE; c = iter.next())
370 sb.append(c);
371 iter.setIndex( idx );
372 return sb.toString();
375 private static Font getFont(AttributedCharacterIterator iter)
377 Font f = (Font)iter.getAttribute(TextAttribute.FONT);
378 if( f == null )
380 int size;
381 Float i = (Float)iter.getAttribute(TextAttribute.SIZE);
382 if( i != null )
383 size = (int)i.floatValue();
384 else
385 size = 14;
386 f = new Font("Dialog", Font.PLAIN, size );
388 return f;
392 * Scan the character run for the first strongly directional character,
393 * which in turn defines the base directionality of the whole layout.
395 private void getStringProperties()
397 boolean gotDirection = false;
398 int i = offset;
399 int endOffs = offset + length;
400 leftToRight = true;
401 while( i < endOffs && !gotDirection )
402 switch( Character.getDirectionality(string[i++]) )
404 case Character.DIRECTIONALITY_LEFT_TO_RIGHT:
405 case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING:
406 case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE:
407 gotDirection = true;
408 break;
410 case Character.DIRECTIONALITY_RIGHT_TO_LEFT:
411 case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
412 case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING:
413 case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE:
414 leftToRight = false;
415 gotDirection = true;
416 break;
418 determineWhiteSpace();
421 private void determineWhiteSpace()
423 // Determine if there's whitespace in the thing.
424 // Ignore trailing chars.
425 int i = offset + length - 1;
426 hasWhitespace = false;
427 while( i >= offset && Character.isWhitespace( string[i] ) )
428 i--;
429 // Check the remaining chars
430 while( i >= offset )
431 if( Character.isWhitespace( string[i--] ) )
432 hasWhitespace = true;
435 protected Object clone ()
437 return new TextLayout( this, 0, length);
440 public void draw (Graphics2D g2, float x, float y)
442 for(int i = 0; i < runs.length; i++)
444 Run run = runs[i];
445 GlyphVector gv = run.glyphVector;
446 g2.drawGlyphVector(gv, x, y);
447 Rectangle2D r = gv.getLogicalBounds();
448 x += r.getWidth();
452 public boolean equals (Object obj)
454 if( !( obj instanceof TextLayout) )
455 return false;
457 return equals( (TextLayout) obj );
460 public boolean equals (TextLayout tl)
462 if( runs.length != tl.runs.length )
463 return false;
464 // Compare all glyph vectors.
465 for( int i = 0; i < runs.length; i++ )
466 if( !runs[i].equals( tl.runs[i] ) )
467 return false;
468 return true;
471 public float getAdvance ()
473 if (totalAdvance == -1F)
475 totalAdvance = 0f;
476 for(int i = 0; i < runs.length; i++)
478 Run run = runs[i];
479 GlyphVector gv = run.glyphVector;
480 totalAdvance += gv.getLogicalBounds().getWidth();
483 return totalAdvance;
486 public float getAscent ()
488 return lm.getAscent();
491 public byte getBaseline ()
493 return (byte)lm.getBaselineIndex();
496 public float[] getBaselineOffsets ()
498 return lm.getBaselineOffsets();
501 public Shape getBlackBoxBounds (int firstEndpoint, int secondEndpoint)
503 if( secondEndpoint - firstEndpoint <= 0 )
504 return new Rectangle2D.Float(); // Hmm?
506 if( firstEndpoint < 0 || secondEndpoint > getCharacterCount())
507 return new Rectangle2D.Float();
509 GeneralPath gp = new GeneralPath();
511 int ri = charIndices[ firstEndpoint ][0];
512 int gi = charIndices[ firstEndpoint ][1];
514 double advance = 0;
516 for( int i = 0; i < ri; i++ )
518 Run run = runs[i];
519 GlyphVector gv = run.glyphVector;
520 advance += gv.getLogicalBounds().getWidth();
523 for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ )
525 Run run = runs[i];
526 GlyphVector gv = run.glyphVector;
527 int dg;
528 if( i == charIndices[ secondEndpoint - 1 ][0] )
529 dg = charIndices[ secondEndpoint - 1][1];
530 else
531 dg = gv.getNumGlyphs() - 1;
533 for( int j = 0; j <= dg; j++ )
535 Rectangle2D r2 = (gv.getGlyphVisualBounds( j )).
536 getBounds2D();
537 Point2D p = gv.getGlyphPosition( j );
538 r2.setRect( advance + r2.getX(), r2.getY(),
539 r2.getWidth(), r2.getHeight() );
540 gp.append(r2, false);
543 advance += gv.getLogicalBounds().getWidth();
545 return gp;
548 public Rectangle2D getBounds()
550 if( boundsCache == null )
551 boundsCache = getOutline(new AffineTransform()).getBounds();
552 return boundsCache;
555 public float[] getCaretInfo (TextHitInfo hit)
557 return getCaretInfo(hit, getNaturalBounds());
560 public float[] getCaretInfo (TextHitInfo hit, Rectangle2D bounds)
562 float[] info = new float[2];
563 int index = hit.getCharIndex();
564 boolean leading = hit.isLeadingEdge();
565 // For the boundary cases we return the boundary runs.
566 Run run;
568 if (index >= length)
570 info[0] = getAdvance();
571 info[1] = 0;
573 else
575 if (index < 0)
577 run = runs[0];
578 index = 0;
579 leading = true;
581 else
582 run = findRunAtIndex(index);
584 int glyphIndex = index - run.runStart;
585 Shape glyphBounds = run.glyphVector.getGlyphLogicalBounds(glyphIndex);
586 Rectangle2D glyphRect = glyphBounds.getBounds2D();
587 if (isVertical())
589 if (leading)
590 info[0] = (float) glyphRect.getMinY();
591 else
592 info[0] = (float) glyphRect.getMaxY();
594 else
596 if (leading)
597 info[0] = (float) glyphRect.getMinX();
598 else
599 info[0] = (float) glyphRect.getMaxX();
601 info[0] += run.location;
602 info[1] = run.font.getItalicAngle();
604 return info;
607 public Shape getCaretShape(TextHitInfo hit)
609 return getCaretShape(hit, getBounds());
612 public Shape getCaretShape(TextHitInfo hit, Rectangle2D bounds)
614 // TODO: Handle vertical shapes somehow.
615 float[] info = getCaretInfo(hit);
616 float x1 = info[0];
617 float y1 = (float) bounds.getMinY();
618 float x2 = info[0];
619 float y2 = (float) bounds.getMaxY();
620 if (info[1] != 0)
622 // Shift x1 and x2 according to the slope.
623 x1 -= y1 * info[1];
624 x2 -= y2 * info[1];
626 GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 2);
627 path.moveTo(x1, y1);
628 path.lineTo(x2, y2);
629 return path;
632 public Shape[] getCaretShapes(int offset)
634 return getCaretShapes(offset, getNaturalBounds());
637 public Shape[] getCaretShapes(int offset, Rectangle2D bounds)
639 return getCaretShapes(offset, bounds, DEFAULT_CARET_POLICY);
642 public Shape[] getCaretShapes(int offset, Rectangle2D bounds,
643 CaretPolicy policy)
645 // The RI returns a 2-size array even when there's only one
646 // shape in it.
647 Shape[] carets = new Shape[2];
648 TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
649 int caretHit1 = hitToCaret(hit1);
650 TextHitInfo hit2 = hit1.getOtherHit();
651 int caretHit2 = hitToCaret(hit2);
652 if (caretHit1 == caretHit2)
654 carets[0] = getCaretShape(hit1);
655 carets[1] = null; // The RI returns null in this seldom case.
657 else
659 Shape caret1 = getCaretShape(hit1);
660 Shape caret2 = getCaretShape(hit2);
661 TextHitInfo strong = policy.getStrongCaret(hit1, hit2, this);
662 if (strong == hit1)
664 carets[0] = caret1;
665 carets[1] = caret2;
667 else
669 carets[0] = caret2;
670 carets[1] = caret1;
673 return carets;
676 public int getCharacterCount ()
678 return length;
681 public byte getCharacterLevel (int index)
683 byte level;
684 if( bidi == null )
685 level = 0;
686 else
687 level = (byte) bidi.getLevelAt(index);
688 return level;
691 public float getDescent ()
693 return lm.getDescent();
696 public TextLayout getJustifiedLayout (float justificationWidth)
698 TextLayout newLayout = (TextLayout)clone();
700 if( hasWhitespace )
701 newLayout.handleJustify( justificationWidth );
703 return newLayout;
706 public float getLeading ()
708 return lm.getLeading();
711 public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint)
713 return getLogicalHighlightShape( firstEndpoint, secondEndpoint,
714 getBounds() );
717 public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint,
718 Rectangle2D bounds)
720 if( secondEndpoint - firstEndpoint <= 0 )
721 return new Rectangle2D.Float(); // Hmm?
723 if( firstEndpoint < 0 || secondEndpoint > getCharacterCount())
724 return new Rectangle2D.Float();
726 Rectangle2D r = null;
727 int ri = charIndices[ firstEndpoint ][0];
728 int gi = charIndices[ firstEndpoint ][1];
730 double advance = 0;
732 for( int i = 0; i < ri; i++ )
733 advance += runs[i].glyphVector.getLogicalBounds().getWidth();
735 for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ )
737 Run run = runs[i];
738 GlyphVector gv = run.glyphVector;
739 int dg; // last index in this run to use.
740 if( i == charIndices[ secondEndpoint - 1 ][0] )
741 dg = charIndices[ secondEndpoint - 1][1];
742 else
743 dg = gv.getNumGlyphs() - 1;
745 for(; gi <= dg; gi++ )
747 Rectangle2D r2 = (gv.getGlyphLogicalBounds( gi )).
748 getBounds2D();
749 if( r == null )
750 r = r2;
751 else
752 r = r.createUnion(r2);
754 gi = 0; // reset glyph index into run for next run.
756 advance += gv.getLogicalBounds().getWidth();
759 return r;
762 public int[] getLogicalRangesForVisualSelection (TextHitInfo firstEndpoint,
763 TextHitInfo secondEndpoint)
765 // Check parameters.
766 checkHitInfo(firstEndpoint);
767 checkHitInfo(secondEndpoint);
769 // Convert to visual and order correctly.
770 int start = hitToCaret(firstEndpoint);
771 int end = hitToCaret(secondEndpoint);
772 if (start > end)
774 // Swap start and end so that end >= start.
775 int temp = start;
776 start = end;
777 end = temp;
780 // Now walk through the visual indices and mark the included pieces.
781 boolean[] include = new boolean[length];
782 for (int i = start; i < end; i++)
784 include[visualToLogical[i]] = true;
787 // Count included runs.
788 int numRuns = 0;
789 boolean in = false;
790 for (int i = 0; i < length; i++)
792 if (include[i] != in) // At each run in/out point we toggle the in var.
794 in = ! in;
795 if (in) // At each run start we count up.
796 numRuns++;
800 // Put together the ranges array.
801 int[] ranges = new int[numRuns * 2];
802 int index = 0;
803 in = false;
804 for (int i = 0; i < length; i++)
806 if (include[i] != in)
808 ranges[index] = i;
809 index++;
810 in = ! in;
813 // If the last run ends at the very end, include that last bit too.
814 if (in)
815 ranges[index] = length;
817 return ranges;
820 public TextHitInfo getNextLeftHit(int offset)
822 return getNextLeftHit(offset, DEFAULT_CARET_POLICY);
825 public TextHitInfo getNextLeftHit(int offset, CaretPolicy policy)
827 if (policy == null)
828 throw new IllegalArgumentException("Null policy not allowed");
829 if (offset < 0 || offset > length)
830 throw new IllegalArgumentException("Offset out of bounds");
832 TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
833 TextHitInfo hit2 = hit1.getOtherHit();
835 TextHitInfo strong = policy.getStrongCaret(hit1, hit2, this);
836 TextHitInfo next = getNextLeftHit(strong);
837 TextHitInfo ret = null;
838 if (next != null)
840 TextHitInfo next2 = getVisualOtherHit(next);
841 ret = policy.getStrongCaret(next2, next, this);
843 return ret;
846 public TextHitInfo getNextLeftHit (TextHitInfo hit)
848 checkHitInfo(hit);
849 int index = hitToCaret(hit);
850 TextHitInfo next = null;
851 if (index != 0)
853 index--;
854 next = caretToHit(index);
856 return next;
859 public TextHitInfo getNextRightHit(int offset)
861 return getNextRightHit(offset, DEFAULT_CARET_POLICY);
864 public TextHitInfo getNextRightHit(int offset, CaretPolicy policy)
866 if (policy == null)
867 throw new IllegalArgumentException("Null policy not allowed");
868 if (offset < 0 || offset > length)
869 throw new IllegalArgumentException("Offset out of bounds");
871 TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
872 TextHitInfo hit2 = hit1.getOtherHit();
874 TextHitInfo next = getNextRightHit(policy.getStrongCaret(hit1, hit2, this));
875 TextHitInfo ret = null;
876 if (next != null)
878 TextHitInfo next2 = getVisualOtherHit(next);
879 ret = policy.getStrongCaret(next2, next, this);
881 return ret;
884 public TextHitInfo getNextRightHit(TextHitInfo hit)
886 checkHitInfo(hit);
887 int index = hitToCaret(hit);
888 TextHitInfo next = null;
889 if (index < length)
891 index++;
892 next = caretToHit(index);
894 return next;
897 public Shape getOutline (AffineTransform tx)
899 float x = 0f;
900 GeneralPath gp = new GeneralPath();
901 for(int i = 0; i < runs.length; i++)
903 GlyphVector gv = runs[i].glyphVector;
904 gp.append( gv.getOutline( x, 0f ), false );
905 Rectangle2D r = gv.getLogicalBounds();
906 x += r.getWidth();
908 if( tx != null )
909 gp.transform( tx );
910 return gp;
913 public float getVisibleAdvance ()
915 float totalAdvance = 0f;
917 if( runs.length <= 0 )
918 return 0f;
920 // No trailing whitespace
921 if( !Character.isWhitespace( string[offset + length - 1]) )
922 return getAdvance();
924 // Get length of all runs up to the last
925 for(int i = 0; i < runs.length - 1; i++)
926 totalAdvance += runs[i].glyphVector.getLogicalBounds().getWidth();
928 int lastRun = runs[runs.length - 1].runStart;
929 int j = length - 1;
930 while( j >= lastRun && Character.isWhitespace( string[j] ) ) j--;
932 if( j < lastRun )
933 return totalAdvance; // entire last run is whitespace
935 int lastNonWSChar = j - lastRun;
936 j = 0;
937 while( runs[ runs.length - 1 ].glyphVector.getGlyphCharIndex( j )
938 <= lastNonWSChar )
940 totalAdvance += runs[ runs.length - 1 ].glyphVector
941 .getGlyphLogicalBounds( j )
942 .getBounds2D().getWidth();
943 j ++;
946 return totalAdvance;
949 public Shape getVisualHighlightShape (TextHitInfo firstEndpoint,
950 TextHitInfo secondEndpoint)
952 return getVisualHighlightShape( firstEndpoint, secondEndpoint,
953 getBounds() );
956 public Shape getVisualHighlightShape (TextHitInfo firstEndpoint,
957 TextHitInfo secondEndpoint,
958 Rectangle2D bounds)
960 GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
961 Shape caret1 = getCaretShape(firstEndpoint, bounds);
962 path.append(caret1, false);
963 Shape caret2 = getCaretShape(secondEndpoint, bounds);
964 path.append(caret2, false);
965 // Append left (top) bounds to selection if necessary.
966 int c1 = hitToCaret(firstEndpoint);
967 int c2 = hitToCaret(secondEndpoint);
968 if (c1 == 0 || c2 == 0)
970 path.append(left(bounds), false);
972 // Append right (bottom) bounds if necessary.
973 if (c1 == length || c2 == length)
975 path.append(right(bounds), false);
977 return path.getBounds2D();
981 * Returns the shape that makes up the left (top) edge of this text layout.
983 * @param b the bounds
985 * @return the shape that makes up the left (top) edge of this text layout
987 private Shape left(Rectangle2D b)
989 GeneralPath left = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
990 left.append(getCaretShape(TextHitInfo.beforeOffset(0)), false);
991 if (isVertical())
993 float y = (float) b.getMinY();
994 left.append(new Line2D.Float((float) b.getMinX(), y,
995 (float) b.getMaxX(), y), false);
997 else
999 float x = (float) b.getMinX();
1000 left.append(new Line2D.Float(x, (float) b.getMinY(),
1001 x, (float) b.getMaxY()), false);
1003 return left.getBounds2D();
1007 * Returns the shape that makes up the right (bottom) edge of this text
1008 * layout.
1010 * @param b the bounds
1012 * @return the shape that makes up the right (bottom) edge of this text
1013 * layout
1015 private Shape right(Rectangle2D b)
1017 GeneralPath right = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
1018 right.append(getCaretShape(TextHitInfo.afterOffset(length)), false);
1019 if (isVertical())
1021 float y = (float) b.getMaxY();
1022 right.append(new Line2D.Float((float) b.getMinX(), y,
1023 (float) b.getMaxX(), y), false);
1025 else
1027 float x = (float) b.getMaxX();
1028 right.append(new Line2D.Float(x, (float) b.getMinY(),
1029 x, (float) b.getMaxY()), false);
1031 return right.getBounds2D();
1034 public TextHitInfo getVisualOtherHit (TextHitInfo hit)
1036 checkHitInfo(hit);
1037 int hitIndex = hit.getCharIndex();
1039 int index;
1040 boolean leading;
1041 if (hitIndex == -1 || hitIndex == length)
1043 // Boundary case.
1044 int visual;
1045 if (isLeftToRight() == (hitIndex == -1))
1046 visual = 0;
1047 else
1048 visual = length - 1;
1049 index = visualToLogical[visual];
1050 if (isLeftToRight() == (hitIndex == -1))
1051 leading = isCharacterLTR(index); // LTR.
1052 else
1053 leading = ! isCharacterLTR(index); // RTL.
1055 else
1057 // Normal case.
1058 int visual = logicalToVisual[hitIndex];
1059 boolean b;
1060 if (isCharacterLTR(hitIndex) == hit.isLeadingEdge())
1062 visual--;
1063 b = false;
1065 else
1067 visual++;
1068 b = true;
1070 if (visual >= 0 && visual < length)
1072 index = visualToLogical[visual];
1073 leading = b == isLeftToRight();
1075 else
1077 index = b == isLeftToRight() ? length : -1;
1078 leading = index == length;
1081 return leading ? TextHitInfo.leading(index) : TextHitInfo.trailing(index);
1085 * This is a protected method of a <code>final</code> class, meaning
1086 * it exists only to taunt you.
1088 protected void handleJustify (float justificationWidth)
1090 // We assume that the text has non-trailing whitespace.
1091 // First get the change in width to insert into the whitespaces.
1092 double deltaW = justificationWidth - getVisibleAdvance();
1093 int nglyphs = 0; // # of whitespace chars
1095 // determine last non-whitespace char.
1096 int lastNWS = offset + length - 1;
1097 while( Character.isWhitespace( string[lastNWS] ) ) lastNWS--;
1099 // locations of the glyphs.
1100 int[] wsglyphs = new int[length * 10];
1101 for(int run = 0; run < runs.length; run++ )
1103 Run current = runs[run];
1104 for(int i = 0; i < current.glyphVector.getNumGlyphs(); i++ )
1106 int cindex = current.runStart
1107 + current.glyphVector.getGlyphCharIndex( i );
1108 if( Character.isWhitespace( string[cindex] ) )
1109 // && cindex < lastNWS )
1111 wsglyphs[ nglyphs * 2 ] = run;
1112 wsglyphs[ nglyphs * 2 + 1] = i;
1113 nglyphs++;
1117 deltaW = deltaW / nglyphs; // Change in width per whitespace glyph
1118 double w = 0;
1119 int cws = 0;
1120 // Shift all characters
1121 for(int run = 0; run < runs.length; run++ )
1123 Run current = runs[run];
1124 for(int i = 0; i < current.glyphVector.getNumGlyphs(); i++ )
1126 if( wsglyphs[ cws * 2 ] == run && wsglyphs[ cws * 2 + 1 ] == i )
1128 cws++; // update 'current whitespace'
1129 w += deltaW; // increment the shift
1131 Point2D p = current.glyphVector.getGlyphPosition( i );
1132 p.setLocation( p.getX() + w, p.getY() );
1133 current.glyphVector.setGlyphPosition( i, p );
1138 public TextHitInfo hitTestChar (float x, float y)
1140 return hitTestChar(x, y, getNaturalBounds());
1144 * Finds the character hit at the specified point. This 'clips' this
1145 * text layout against the specified <code>bounds</code> rectangle. That
1146 * means that in the case where a point is outside these bounds, this method
1147 * returns the leading edge of the first character or the trailing edge of
1148 * the last character.
1150 * @param x the X location to test
1151 * @param y the Y location to test
1152 * @param bounds the bounds to test against
1154 * @return the character hit at the specified point
1156 public TextHitInfo hitTestChar (float x, float y, Rectangle2D bounds)
1158 // Check bounds.
1159 if (isVertical())
1161 if (y < bounds.getMinY())
1162 return TextHitInfo.leading(0);
1163 else if (y > bounds.getMaxY())
1164 return TextHitInfo.trailing(getCharacterCount() - 1);
1166 else
1168 if (x < bounds.getMinX())
1169 return TextHitInfo.leading(0);
1170 else if (x > bounds.getMaxX())
1171 return TextHitInfo.trailing(getCharacterCount() - 1);
1174 TextHitInfo hitInfo = null;
1175 if (isVertical())
1177 // Search for the run at the location.
1178 // TODO: Perform binary search for maximum efficiency. However, we
1179 // need the run location laid out statically to do that.
1180 int numRuns = runs.length;
1181 Run hitRun = null;
1182 for (int i = 0; i < numRuns && hitRun == null; i++)
1184 Run run = runs[i];
1185 Rectangle2D lBounds = run.glyphVector.getLogicalBounds();
1186 if (lBounds.getMinY() + run.location <= y
1187 && lBounds.getMaxY() + run.location >= y)
1188 hitRun = run;
1190 // Now we have (hopefully) found a run that hits. Now find the
1191 // right character.
1192 if (hitRun != null)
1194 GlyphVector gv = hitRun.glyphVector;
1195 for (int i = hitRun.runStart;
1196 i < hitRun.runEnd && hitInfo == null; i++)
1198 int gi = i - hitRun.runStart;
1199 Rectangle2D lBounds = gv.getGlyphLogicalBounds(gi)
1200 .getBounds2D();
1201 if (lBounds.getMinY() + hitRun.location <= y
1202 && lBounds.getMaxY() + hitRun.location >= y)
1204 // Found hit. Now check if we are leading or trailing.
1205 boolean leading = true;
1206 if (lBounds.getCenterY() + hitRun.location <= y)
1207 leading = false;
1208 hitInfo = leading ? TextHitInfo.leading(i)
1209 : TextHitInfo.trailing(i);
1214 else
1216 // Search for the run at the location.
1217 // TODO: Perform binary search for maximum efficiency. However, we
1218 // need the run location laid out statically to do that.
1219 int numRuns = runs.length;
1220 Run hitRun = null;
1221 for (int i = 0; i < numRuns && hitRun == null; i++)
1223 Run run = runs[i];
1224 Rectangle2D lBounds = run.glyphVector.getLogicalBounds();
1225 if (lBounds.getMinX() + run.location <= x
1226 && lBounds.getMaxX() + run.location >= x)
1227 hitRun = run;
1229 // Now we have (hopefully) found a run that hits. Now find the
1230 // right character.
1231 if (hitRun != null)
1233 GlyphVector gv = hitRun.glyphVector;
1234 for (int i = hitRun.runStart;
1235 i < hitRun.runEnd && hitInfo == null; i++)
1237 int gi = i - hitRun.runStart;
1238 Rectangle2D lBounds = gv.getGlyphLogicalBounds(gi)
1239 .getBounds2D();
1240 if (lBounds.getMinX() + hitRun.location <= x
1241 && lBounds.getMaxX() + hitRun.location >= x)
1243 // Found hit. Now check if we are leading or trailing.
1244 boolean leading = true;
1245 if (lBounds.getCenterX() + hitRun.location <= x)
1246 leading = false;
1247 hitInfo = leading ? TextHitInfo.leading(i)
1248 : TextHitInfo.trailing(i);
1253 return hitInfo;
1256 public boolean isLeftToRight ()
1258 return leftToRight;
1261 public boolean isVertical ()
1263 return false; // FIXME: How do you create a vertical layout?
1266 public int hashCode ()
1268 // This is implemented in sync to equals().
1269 if (hash == 0 && runs.length > 0)
1271 hash = runs.length;
1272 for (int i = 0; i < runs.length; i++)
1273 hash ^= runs[i].glyphVector.hashCode();
1275 return hash;
1278 public String toString ()
1280 return "TextLayout [string:"+ new String(string, offset, length)
1281 +" Rendercontext:"+
1282 frc+"]";
1286 * Returns the natural bounds of that text layout. This is made up
1287 * of the ascent plus descent and the text advance.
1289 * @return the natural bounds of that text layout
1291 private Rectangle2D getNaturalBounds()
1293 if (naturalBounds == null)
1294 naturalBounds = new Rectangle2D.Float(0.0F, -getAscent(), getAdvance(),
1295 getAscent() + getDescent());
1296 return naturalBounds;
1299 private void checkHitInfo(TextHitInfo hit)
1301 if (hit == null)
1302 throw new IllegalArgumentException("Null hit info not allowed");
1303 int index = hit.getInsertionIndex();
1304 if (index < 0 || index > length)
1305 throw new IllegalArgumentException("Hit index out of range");
1308 private int hitToCaret(TextHitInfo hit)
1310 int index = hit.getCharIndex();
1311 int ret;
1312 if (index < 0)
1313 ret = isLeftToRight() ? 0 : length;
1314 else if (index >= length)
1315 ret = isLeftToRight() ? length : 0;
1316 else
1318 ret = logicalToVisual[index];
1319 if (hit.isLeadingEdge() != isCharacterLTR(index))
1320 ret++;
1322 return ret;
1325 private TextHitInfo caretToHit(int index)
1327 TextHitInfo hit;
1328 if (index == 0 || index == length)
1330 if ((index == length) == isLeftToRight())
1331 hit = TextHitInfo.leading(length);
1332 else
1333 hit = TextHitInfo.trailing(-1);
1335 else
1337 int logical = visualToLogical[index];
1338 boolean leading = isCharacterLTR(logical); // LTR.
1339 hit = leading ? TextHitInfo.leading(logical)
1340 : TextHitInfo.trailing(logical);
1342 return hit;
1345 private boolean isCharacterLTR(int index)
1347 byte level = getCharacterLevel(index);
1348 return (level & 1) == 0;
1352 * Finds the run that holds the specified (logical) character index. This
1353 * returns <code>null</code> when the index is not inside the range.
1355 * @param index the index of the character to find
1357 * @return the run that holds the specified character
1359 private Run findRunAtIndex(int index)
1361 Run found = null;
1362 // TODO: Can we do better than linear searching here?
1363 for (int i = 0; i < runs.length && found == null; i++)
1365 Run run = runs[i];
1366 if (run.runStart <= index && run.runEnd > index)
1367 found = run;
1369 return found;
1373 * Computes the layout locations for each run.
1375 private void layoutRuns()
1377 float loc = 0.0F;
1378 float lastWidth = 0.0F;
1379 for (int i = 0; i < runs.length; i++)
1381 runs[i].location = loc;
1382 Rectangle2D bounds = runs[i].glyphVector.getLogicalBounds();
1383 loc += isVertical() ? bounds.getHeight() : bounds.getWidth();
1388 * Inner class describing a caret policy
1390 public static class CaretPolicy
1392 public CaretPolicy()
1396 public TextHitInfo getStrongCaret(TextHitInfo hit1,
1397 TextHitInfo hit2,
1398 TextLayout layout)
1400 byte l1 = layout.getCharacterLevel(hit1.getCharIndex());
1401 byte l2 = layout.getCharacterLevel(hit2.getCharIndex());
1402 TextHitInfo strong;
1403 if (l1 == l2)
1405 if (hit2.isLeadingEdge() && ! hit1.isLeadingEdge())
1406 strong = hit2;
1407 else
1408 strong = hit1;
1410 else
1412 if (l1 < l2)
1413 strong = hit1;
1414 else
1415 strong = hit2;
1417 return strong;