001 /* TextLayout.java --
002 Copyright (C) 2006 Free Software Foundation, Inc.
003
004 This file is part of GNU Classpath.
005
006 GNU Classpath is free software; you can redistribute it and/or modify
007 it under the terms of the GNU General Public License as published by
008 the Free Software Foundation; either version 2, or (at your option)
009 any later version.
010
011 GNU Classpath is distributed in the hope that it will be useful, but
012 WITHOUT ANY WARRANTY; without even the implied warranty of
013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 General Public License for more details.
015
016 You should have received a copy of the GNU General Public License
017 along with GNU Classpath; see the file COPYING. If not, write to the
018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019 02110-1301 USA.
020
021 Linking this library statically or dynamically with other modules is
022 making a combined work based on this library. Thus, the terms and
023 conditions of the GNU General Public License cover the whole
024 combination.
025
026 As a special exception, the copyright holders of this library give you
027 permission to link this library with independent modules to produce an
028 executable, regardless of the license terms of these independent
029 modules, and to copy and distribute the resulting executable under
030 terms of your choice, provided that you also meet, for each linked
031 independent module, the terms and conditions of the license of that
032 module. An independent module is a module which is not derived from
033 or based on this library. If you modify this library, you may extend
034 this exception to your version of the library, but you are not
035 obligated to do so. If you do not wish to do so, delete this
036 exception statement from your version. */
037
038
039 package java.awt.font;
040
041 import java.awt.Font;
042 import java.awt.Graphics2D;
043 import java.awt.Shape;
044 import java.awt.geom.AffineTransform;
045 import java.awt.geom.Line2D;
046 import java.awt.geom.Rectangle2D;
047 import java.awt.geom.GeneralPath;
048 import java.awt.geom.Point2D;
049 import java.text.CharacterIterator;
050 import java.text.AttributedCharacterIterator;
051 import java.text.Bidi;
052 import java.util.ArrayList;
053 import java.util.Map;
054
055 /**
056 * @author Sven de Marothy
057 */
058 public final class TextLayout implements Cloneable
059 {
060 /**
061 * Holds the layout data that belongs to one run of characters.
062 */
063 private class Run
064 {
065 /**
066 * The actual glyph vector.
067 */
068 GlyphVector glyphVector;
069
070 /**
071 * The font for this text run.
072 */
073 Font font;
074
075 /**
076 * The start of the run.
077 */
078 int runStart;
079
080 /**
081 * The end of the run.
082 */
083 int runEnd;
084
085 /**
086 * The layout location of the beginning of the run.
087 */
088 float location;
089
090 /**
091 * Initializes the Run instance.
092 *
093 * @param gv the glyph vector
094 * @param start the start index of the run
095 * @param end the end index of the run
096 */
097 Run(GlyphVector gv, Font f, int start, int end)
098 {
099 glyphVector = gv;
100 font = f;
101 runStart = start;
102 runEnd = end;
103 }
104
105 /**
106 * Returns <code>true</code> when this run is left to right,
107 * <code>false</code> otherwise.
108 *
109 * @return <code>true</code> when this run is left to right,
110 * <code>false</code> otherwise
111 */
112 boolean isLeftToRight()
113 {
114 return (glyphVector.getLayoutFlags() & GlyphVector.FLAG_RUN_RTL) == 0;
115 }
116 }
117
118 /**
119 * The laid out character runs.
120 */
121 private Run[] runs;
122
123 private FontRenderContext frc;
124 private char[] string;
125 private int offset;
126 private int length;
127 private Rectangle2D boundsCache;
128 private LineMetrics lm;
129
130 /**
131 * The total advance of this text layout. This is cache for maximum
132 * performance.
133 */
134 private float totalAdvance = -1F;
135
136 /**
137 * The cached natural bounds.
138 */
139 private Rectangle2D naturalBounds;
140
141 /**
142 * Character indices.
143 * Fixt index is the glyphvector, second index is the (first) glyph.
144 */
145 private int[][] charIndices;
146
147 /**
148 * Base directionality, determined from the first char.
149 */
150 private boolean leftToRight;
151
152 /**
153 * Whether this layout contains whitespace or not.
154 */
155 private boolean hasWhitespace = false;
156
157 /**
158 * The {@link Bidi} object that is used for reordering and by
159 * {@link #getCharacterLevel(int)}.
160 */
161 private Bidi bidi;
162
163 /**
164 * Mpas the logical position of each individual character in the original
165 * string to its visual position.
166 */
167 private int[] logicalToVisual;
168
169 /**
170 * Maps visual positions of a character to its logical position
171 * in the original string.
172 */
173 private int[] visualToLogical;
174
175 /**
176 * The cached hashCode.
177 */
178 private int hash;
179
180 /**
181 * The default caret policy.
182 */
183 public static final TextLayout.CaretPolicy DEFAULT_CARET_POLICY =
184 new CaretPolicy();
185
186 /**
187 * Constructs a TextLayout.
188 */
189 public TextLayout (String str, Font font, FontRenderContext frc)
190 {
191 this.frc = frc;
192 string = str.toCharArray();
193 offset = 0;
194 length = this.string.length;
195 lm = font.getLineMetrics(this.string, offset, length, frc);
196
197 // Get base direction and whitespace info
198 getStringProperties();
199
200 if (Bidi.requiresBidi(string, offset, offset + length))
201 {
202 bidi = new Bidi(str, leftToRight ? Bidi.DIRECTION_LEFT_TO_RIGHT
203 : Bidi.DIRECTION_RIGHT_TO_LEFT );
204 int rc = bidi.getRunCount();
205 byte[] table = new byte[ rc ];
206 for(int i = 0; i < table.length; i++)
207 table[i] = (byte)bidi.getRunLevel(i);
208
209 runs = new Run[rc];
210 for(int i = 0; i < rc; i++)
211 {
212 int start = bidi.getRunStart(i);
213 int end = bidi.getRunLimit(i);
214 if(start != end) // no empty runs.
215 {
216 GlyphVector gv = font.layoutGlyphVector(frc,
217 string, start, end,
218 ((table[i] & 1) == 0) ? Font.LAYOUT_LEFT_TO_RIGHT
219 : Font.LAYOUT_RIGHT_TO_LEFT );
220 runs[i] = new Run(gv, font, start, end);
221 }
222 }
223 Bidi.reorderVisually( table, 0, runs, 0, runs.length );
224 // Clean up null runs.
225 ArrayList cleaned = new ArrayList(rc);
226 for (int i = 0; i < rc; i++)
227 {
228 if (runs[i] != null)
229 cleaned.add(runs[i]);
230 }
231 runs = new Run[cleaned.size()];
232 runs = (Run[]) cleaned.toArray(runs);
233 }
234 else
235 {
236 GlyphVector gv = font.layoutGlyphVector( frc, string, offset, length,
237 leftToRight ? Font.LAYOUT_LEFT_TO_RIGHT
238 : Font.LAYOUT_RIGHT_TO_LEFT );
239 Run run = new Run(gv, font, 0, length);
240 runs = new Run[]{ run };
241 }
242 setCharIndices();
243 setupMappings();
244 layoutRuns();
245 }
246
247 public TextLayout (String string,
248 Map<? extends AttributedCharacterIterator.Attribute, ?> attributes,
249 FontRenderContext frc)
250 {
251 this( string, new Font( attributes ), frc );
252 }
253
254 public TextLayout (AttributedCharacterIterator text, FontRenderContext frc)
255 {
256 // FIXME: Very rudimentary.
257 this(getText(text), getFont(text), frc);
258 }
259
260 /**
261 * Package-private constructor to make a textlayout from an existing one.
262 * This is used by TextMeasurer for returning sub-layouts, and it
263 * saves a lot of time in not having to relayout the text.
264 */
265 TextLayout(TextLayout t, int startIndex, int endIndex)
266 {
267 frc = t.frc;
268 boundsCache = null;
269 lm = t.lm;
270 leftToRight = t.leftToRight;
271
272 if( endIndex > t.getCharacterCount() )
273 endIndex = t.getCharacterCount();
274 string = t.string;
275 offset = startIndex + offset;
276 length = endIndex - startIndex;
277
278 int startingRun = t.charIndices[startIndex][0];
279 int nRuns = 1 + t.charIndices[endIndex - 1][0] - startingRun;
280
281 runs = new Run[nRuns];
282 for( int i = 0; i < nRuns; i++ )
283 {
284 Run run = t.runs[i + startingRun];
285 GlyphVector gv = run.glyphVector;
286 Font font = run.font;
287 // Copy only the relevant parts of the first and last runs.
288 int beginGlyphIndex = (i > 0) ? 0 : t.charIndices[startIndex][1];
289 int numEntries = ( i < nRuns - 1) ? gv.getNumGlyphs() :
290 1 + t.charIndices[endIndex - 1][1] - beginGlyphIndex;
291
292 int[] codes = gv.getGlyphCodes(beginGlyphIndex, numEntries, null);
293 gv = font.createGlyphVector(frc, codes);
294 runs[i] = new Run(gv, font, run.runStart - startIndex,
295 run.runEnd - startIndex);
296 }
297 runs[nRuns - 1].runEnd = endIndex - 1;
298
299 setCharIndices();
300 setupMappings();
301 determineWhiteSpace();
302 layoutRuns();
303 }
304
305 private void setCharIndices()
306 {
307 charIndices = new int[ getCharacterCount() ][2];
308 int i = 0;
309 int currentChar = 0;
310 for(int run = 0; run < runs.length; run++)
311 {
312 currentChar = -1;
313 Run current = runs[run];
314 GlyphVector gv = current.glyphVector;
315 for( int gi = 0; gi < gv.getNumGlyphs(); gi++)
316 {
317 if( gv.getGlyphCharIndex( gi ) != currentChar )
318 {
319 charIndices[ i ][0] = run;
320 charIndices[ i ][1] = gi;
321 currentChar = gv.getGlyphCharIndex( gi );
322 i++;
323 }
324 }
325 }
326 }
327
328 /**
329 * Initializes the logicalToVisual and visualToLogial maps.
330 */
331 private void setupMappings()
332 {
333 int numChars = getCharacterCount();
334 logicalToVisual = new int[numChars];
335 visualToLogical = new int[numChars];
336 int lIndex = 0;
337 int vIndex = 0;
338 // We scan the runs in visual order and set the mappings accordingly.
339 for (int i = 0; i < runs.length; i++)
340 {
341 Run run = runs[i];
342 if (run.isLeftToRight())
343 {
344 for (lIndex = run.runStart; lIndex < run.runEnd; lIndex++)
345 {
346 logicalToVisual[lIndex] = vIndex;
347 visualToLogical[vIndex] = lIndex;
348 vIndex++;
349 }
350 }
351 else
352 {
353 for (lIndex = run.runEnd - 1; lIndex >= run.runStart; lIndex--)
354 {
355 logicalToVisual[lIndex] = vIndex;
356 visualToLogical[vIndex] = lIndex;
357 vIndex++;
358 }
359 }
360 }
361 }
362
363 private static String getText(AttributedCharacterIterator iter)
364 {
365 StringBuffer sb = new StringBuffer();
366 int idx = iter.getIndex();
367 for(char c = iter.first(); c != CharacterIterator.DONE; c = iter.next())
368 sb.append(c);
369 iter.setIndex( idx );
370 return sb.toString();
371 }
372
373 private static Font getFont(AttributedCharacterIterator iter)
374 {
375 Font f = (Font)iter.getAttribute(TextAttribute.FONT);
376 if( f == null )
377 {
378 int size;
379 Float i = (Float)iter.getAttribute(TextAttribute.SIZE);
380 if( i != null )
381 size = (int)i.floatValue();
382 else
383 size = 14;
384 f = new Font("Dialog", Font.PLAIN, size );
385 }
386 return f;
387 }
388
389 /**
390 * Scan the character run for the first strongly directional character,
391 * which in turn defines the base directionality of the whole layout.
392 */
393 private void getStringProperties()
394 {
395 boolean gotDirection = false;
396 int i = offset;
397 int endOffs = offset + length;
398 leftToRight = true;
399 while( i < endOffs && !gotDirection )
400 switch( Character.getDirectionality(string[i++]) )
401 {
402 case Character.DIRECTIONALITY_LEFT_TO_RIGHT:
403 case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING:
404 case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE:
405 gotDirection = true;
406 break;
407
408 case Character.DIRECTIONALITY_RIGHT_TO_LEFT:
409 case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
410 case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING:
411 case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE:
412 leftToRight = false;
413 gotDirection = true;
414 break;
415 }
416 determineWhiteSpace();
417 }
418
419 private void determineWhiteSpace()
420 {
421 // Determine if there's whitespace in the thing.
422 // Ignore trailing chars.
423 int i = offset + length - 1;
424 hasWhitespace = false;
425 while( i >= offset && Character.isWhitespace( string[i] ) )
426 i--;
427 // Check the remaining chars
428 while( i >= offset )
429 if( Character.isWhitespace( string[i--] ) )
430 hasWhitespace = true;
431 }
432
433 protected Object clone ()
434 {
435 return new TextLayout( this, 0, length);
436 }
437
438 public void draw (Graphics2D g2, float x, float y)
439 {
440 for(int i = 0; i < runs.length; i++)
441 {
442 Run run = runs[i];
443 GlyphVector gv = run.glyphVector;
444 g2.drawGlyphVector(gv, x, y);
445 Rectangle2D r = gv.getLogicalBounds();
446 x += r.getWidth();
447 }
448 }
449
450 public boolean equals (Object obj)
451 {
452 if( !( obj instanceof TextLayout) )
453 return false;
454
455 return equals( (TextLayout) obj );
456 }
457
458 public boolean equals (TextLayout tl)
459 {
460 if( runs.length != tl.runs.length )
461 return false;
462 // Compare all glyph vectors.
463 for( int i = 0; i < runs.length; i++ )
464 if( !runs[i].equals( tl.runs[i] ) )
465 return false;
466 return true;
467 }
468
469 public float getAdvance ()
470 {
471 if (totalAdvance == -1F)
472 {
473 totalAdvance = 0f;
474 for(int i = 0; i < runs.length; i++)
475 {
476 Run run = runs[i];
477 GlyphVector gv = run.glyphVector;
478 totalAdvance += gv.getLogicalBounds().getWidth();
479 }
480 }
481 return totalAdvance;
482 }
483
484 public float getAscent ()
485 {
486 return lm.getAscent();
487 }
488
489 public byte getBaseline ()
490 {
491 return (byte)lm.getBaselineIndex();
492 }
493
494 public float[] getBaselineOffsets ()
495 {
496 return lm.getBaselineOffsets();
497 }
498
499 public Shape getBlackBoxBounds (int firstEndpoint, int secondEndpoint)
500 {
501 if( secondEndpoint - firstEndpoint <= 0 )
502 return new Rectangle2D.Float(); // Hmm?
503
504 if( firstEndpoint < 0 || secondEndpoint > getCharacterCount())
505 return new Rectangle2D.Float();
506
507 GeneralPath gp = new GeneralPath();
508
509 int ri = charIndices[ firstEndpoint ][0];
510 int gi = charIndices[ firstEndpoint ][1];
511
512 double advance = 0;
513
514 for( int i = 0; i < ri; i++ )
515 {
516 Run run = runs[i];
517 GlyphVector gv = run.glyphVector;
518 advance += gv.getLogicalBounds().getWidth();
519 }
520
521 for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ )
522 {
523 Run run = runs[i];
524 GlyphVector gv = run.glyphVector;
525 int dg;
526 if( i == charIndices[ secondEndpoint - 1 ][0] )
527 dg = charIndices[ secondEndpoint - 1][1];
528 else
529 dg = gv.getNumGlyphs() - 1;
530
531 for( int j = 0; j <= dg; j++ )
532 {
533 Rectangle2D r2 = (gv.getGlyphVisualBounds( j )).
534 getBounds2D();
535 Point2D p = gv.getGlyphPosition( j );
536 r2.setRect( advance + r2.getX(), r2.getY(),
537 r2.getWidth(), r2.getHeight() );
538 gp.append(r2, false);
539 }
540
541 advance += gv.getLogicalBounds().getWidth();
542 }
543 return gp;
544 }
545
546 public Rectangle2D getBounds()
547 {
548 if( boundsCache == null )
549 boundsCache = getOutline(new AffineTransform()).getBounds();
550 return boundsCache;
551 }
552
553 public float[] getCaretInfo (TextHitInfo hit)
554 {
555 return getCaretInfo(hit, getNaturalBounds());
556 }
557
558 public float[] getCaretInfo (TextHitInfo hit, Rectangle2D bounds)
559 {
560 float[] info = new float[2];
561 int index = hit.getCharIndex();
562 boolean leading = hit.isLeadingEdge();
563 // For the boundary cases we return the boundary runs.
564 Run run;
565
566 if (index >= length)
567 {
568 info[0] = getAdvance();
569 info[1] = 0;
570 }
571 else
572 {
573 if (index < 0)
574 {
575 run = runs[0];
576 index = 0;
577 leading = true;
578 }
579 else
580 run = findRunAtIndex(index);
581
582 int glyphIndex = index - run.runStart;
583 Shape glyphBounds = run.glyphVector.getGlyphLogicalBounds(glyphIndex);
584 Rectangle2D glyphRect = glyphBounds.getBounds2D();
585 if (isVertical())
586 {
587 if (leading)
588 info[0] = (float) glyphRect.getMinY();
589 else
590 info[0] = (float) glyphRect.getMaxY();
591 }
592 else
593 {
594 if (leading)
595 info[0] = (float) glyphRect.getMinX();
596 else
597 info[0] = (float) glyphRect.getMaxX();
598 }
599 info[0] += run.location;
600 info[1] = run.font.getItalicAngle();
601 }
602 return info;
603 }
604
605 public Shape getCaretShape(TextHitInfo hit)
606 {
607 return getCaretShape(hit, getBounds());
608 }
609
610 public Shape getCaretShape(TextHitInfo hit, Rectangle2D bounds)
611 {
612 // TODO: Handle vertical shapes somehow.
613 float[] info = getCaretInfo(hit);
614 float x1 = info[0];
615 float y1 = (float) bounds.getMinY();
616 float x2 = info[0];
617 float y2 = (float) bounds.getMaxY();
618 if (info[1] != 0)
619 {
620 // Shift x1 and x2 according to the slope.
621 x1 -= y1 * info[1];
622 x2 -= y2 * info[1];
623 }
624 GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 2);
625 path.moveTo(x1, y1);
626 path.lineTo(x2, y2);
627 return path;
628 }
629
630 public Shape[] getCaretShapes(int offset)
631 {
632 return getCaretShapes(offset, getNaturalBounds());
633 }
634
635 public Shape[] getCaretShapes(int offset, Rectangle2D bounds)
636 {
637 return getCaretShapes(offset, bounds, DEFAULT_CARET_POLICY);
638 }
639
640 public Shape[] getCaretShapes(int offset, Rectangle2D bounds,
641 CaretPolicy policy)
642 {
643 // The RI returns a 2-size array even when there's only one
644 // shape in it.
645 Shape[] carets = new Shape[2];
646 TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
647 int caretHit1 = hitToCaret(hit1);
648 TextHitInfo hit2 = hit1.getOtherHit();
649 int caretHit2 = hitToCaret(hit2);
650 if (caretHit1 == caretHit2)
651 {
652 carets[0] = getCaretShape(hit1);
653 carets[1] = null; // The RI returns null in this seldom case.
654 }
655 else
656 {
657 Shape caret1 = getCaretShape(hit1);
658 Shape caret2 = getCaretShape(hit2);
659 TextHitInfo strong = policy.getStrongCaret(hit1, hit2, this);
660 if (strong == hit1)
661 {
662 carets[0] = caret1;
663 carets[1] = caret2;
664 }
665 else
666 {
667 carets[0] = caret2;
668 carets[1] = caret1;
669 }
670 }
671 return carets;
672 }
673
674 public int getCharacterCount ()
675 {
676 return length;
677 }
678
679 public byte getCharacterLevel (int index)
680 {
681 byte level;
682 if( bidi == null )
683 level = 0;
684 else
685 level = (byte) bidi.getLevelAt(index);
686 return level;
687 }
688
689 public float getDescent ()
690 {
691 return lm.getDescent();
692 }
693
694 public TextLayout getJustifiedLayout (float justificationWidth)
695 {
696 TextLayout newLayout = (TextLayout)clone();
697
698 if( hasWhitespace )
699 newLayout.handleJustify( justificationWidth );
700
701 return newLayout;
702 }
703
704 public float getLeading ()
705 {
706 return lm.getLeading();
707 }
708
709 public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint)
710 {
711 return getLogicalHighlightShape( firstEndpoint, secondEndpoint,
712 getBounds() );
713 }
714
715 public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint,
716 Rectangle2D bounds)
717 {
718 if( secondEndpoint - firstEndpoint <= 0 )
719 return new Rectangle2D.Float(); // Hmm?
720
721 if( firstEndpoint < 0 || secondEndpoint > getCharacterCount())
722 return new Rectangle2D.Float();
723
724 Rectangle2D r = null;
725 int ri = charIndices[ firstEndpoint ][0];
726 int gi = charIndices[ firstEndpoint ][1];
727
728 double advance = 0;
729
730 for( int i = 0; i < ri; i++ )
731 advance += runs[i].glyphVector.getLogicalBounds().getWidth();
732
733 for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ )
734 {
735 Run run = runs[i];
736 GlyphVector gv = run.glyphVector;
737 int dg; // last index in this run to use.
738 if( i == charIndices[ secondEndpoint - 1 ][0] )
739 dg = charIndices[ secondEndpoint - 1][1];
740 else
741 dg = gv.getNumGlyphs() - 1;
742
743 for(; gi <= dg; gi++ )
744 {
745 Rectangle2D r2 = (gv.getGlyphLogicalBounds( gi )).
746 getBounds2D();
747 if( r == null )
748 r = r2;
749 else
750 r = r.createUnion(r2);
751 }
752 gi = 0; // reset glyph index into run for next run.
753
754 advance += gv.getLogicalBounds().getWidth();
755 }
756
757 return r;
758 }
759
760 public int[] getLogicalRangesForVisualSelection (TextHitInfo firstEndpoint,
761 TextHitInfo secondEndpoint)
762 {
763 // Check parameters.
764 checkHitInfo(firstEndpoint);
765 checkHitInfo(secondEndpoint);
766
767 // Convert to visual and order correctly.
768 int start = hitToCaret(firstEndpoint);
769 int end = hitToCaret(secondEndpoint);
770 if (start > end)
771 {
772 // Swap start and end so that end >= start.
773 int temp = start;
774 start = end;
775 end = temp;
776 }
777
778 // Now walk through the visual indices and mark the included pieces.
779 boolean[] include = new boolean[length];
780 for (int i = start; i < end; i++)
781 {
782 include[visualToLogical[i]] = true;
783 }
784
785 // Count included runs.
786 int numRuns = 0;
787 boolean in = false;
788 for (int i = 0; i < length; i++)
789 {
790 if (include[i] != in) // At each run in/out point we toggle the in var.
791 {
792 in = ! in;
793 if (in) // At each run start we count up.
794 numRuns++;
795 }
796 }
797
798 // Put together the ranges array.
799 int[] ranges = new int[numRuns * 2];
800 int index = 0;
801 in = false;
802 for (int i = 0; i < length; i++)
803 {
804 if (include[i] != in)
805 {
806 ranges[index] = i;
807 index++;
808 in = ! in;
809 }
810 }
811 // If the last run ends at the very end, include that last bit too.
812 if (in)
813 ranges[index] = length;
814
815 return ranges;
816 }
817
818 public TextHitInfo getNextLeftHit(int offset)
819 {
820 return getNextLeftHit(offset, DEFAULT_CARET_POLICY);
821 }
822
823 public TextHitInfo getNextLeftHit(int offset, CaretPolicy policy)
824 {
825 if (policy == null)
826 throw new IllegalArgumentException("Null policy not allowed");
827 if (offset < 0 || offset > length)
828 throw new IllegalArgumentException("Offset out of bounds");
829
830 TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
831 TextHitInfo hit2 = hit1.getOtherHit();
832
833 TextHitInfo strong = policy.getStrongCaret(hit1, hit2, this);
834 TextHitInfo next = getNextLeftHit(strong);
835 TextHitInfo ret = null;
836 if (next != null)
837 {
838 TextHitInfo next2 = getVisualOtherHit(next);
839 ret = policy.getStrongCaret(next2, next, this);
840 }
841 return ret;
842 }
843
844 public TextHitInfo getNextLeftHit (TextHitInfo hit)
845 {
846 checkHitInfo(hit);
847 int index = hitToCaret(hit);
848 TextHitInfo next = null;
849 if (index != 0)
850 {
851 index--;
852 next = caretToHit(index);
853 }
854 return next;
855 }
856
857 public TextHitInfo getNextRightHit(int offset)
858 {
859 return getNextRightHit(offset, DEFAULT_CARET_POLICY);
860 }
861
862 public TextHitInfo getNextRightHit(int offset, CaretPolicy policy)
863 {
864 if (policy == null)
865 throw new IllegalArgumentException("Null policy not allowed");
866 if (offset < 0 || offset > length)
867 throw new IllegalArgumentException("Offset out of bounds");
868
869 TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
870 TextHitInfo hit2 = hit1.getOtherHit();
871
872 TextHitInfo next = getNextRightHit(policy.getStrongCaret(hit1, hit2, this));
873 TextHitInfo ret = null;
874 if (next != null)
875 {
876 TextHitInfo next2 = getVisualOtherHit(next);
877 ret = policy.getStrongCaret(next2, next, this);
878 }
879 return ret;
880 }
881
882 public TextHitInfo getNextRightHit(TextHitInfo hit)
883 {
884 checkHitInfo(hit);
885 int index = hitToCaret(hit);
886 TextHitInfo next = null;
887 if (index < length)
888 {
889 index++;
890 next = caretToHit(index);
891 }
892 return next;
893 }
894
895 public Shape getOutline (AffineTransform tx)
896 {
897 float x = 0f;
898 GeneralPath gp = new GeneralPath();
899 for(int i = 0; i < runs.length; i++)
900 {
901 GlyphVector gv = runs[i].glyphVector;
902 gp.append( gv.getOutline( x, 0f ), false );
903 Rectangle2D r = gv.getLogicalBounds();
904 x += r.getWidth();
905 }
906 if( tx != null )
907 gp.transform( tx );
908 return gp;
909 }
910
911 public float getVisibleAdvance ()
912 {
913 float totalAdvance = 0f;
914
915 if( runs.length <= 0 )
916 return 0f;
917
918 // No trailing whitespace
919 if( !Character.isWhitespace( string[offset + length - 1]) )
920 return getAdvance();
921
922 // Get length of all runs up to the last
923 for(int i = 0; i < runs.length - 1; i++)
924 totalAdvance += runs[i].glyphVector.getLogicalBounds().getWidth();
925
926 int lastRun = runs[runs.length - 1].runStart;
927 int j = length - 1;
928 while( j >= lastRun && Character.isWhitespace( string[j] ) ) j--;
929
930 if( j < lastRun )
931 return totalAdvance; // entire last run is whitespace
932
933 int lastNonWSChar = j - lastRun;
934 j = 0;
935 while( runs[ runs.length - 1 ].glyphVector.getGlyphCharIndex( j )
936 <= lastNonWSChar )
937 {
938 totalAdvance += runs[ runs.length - 1 ].glyphVector
939 .getGlyphLogicalBounds( j )
940 .getBounds2D().getWidth();
941 j ++;
942 }
943
944 return totalAdvance;
945 }
946
947 public Shape getVisualHighlightShape (TextHitInfo firstEndpoint,
948 TextHitInfo secondEndpoint)
949 {
950 return getVisualHighlightShape( firstEndpoint, secondEndpoint,
951 getBounds() );
952 }
953
954 public Shape getVisualHighlightShape (TextHitInfo firstEndpoint,
955 TextHitInfo secondEndpoint,
956 Rectangle2D bounds)
957 {
958 GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
959 Shape caret1 = getCaretShape(firstEndpoint, bounds);
960 path.append(caret1, false);
961 Shape caret2 = getCaretShape(secondEndpoint, bounds);
962 path.append(caret2, false);
963 // Append left (top) bounds to selection if necessary.
964 int c1 = hitToCaret(firstEndpoint);
965 int c2 = hitToCaret(secondEndpoint);
966 if (c1 == 0 || c2 == 0)
967 {
968 path.append(left(bounds), false);
969 }
970 // Append right (bottom) bounds if necessary.
971 if (c1 == length || c2 == length)
972 {
973 path.append(right(bounds), false);
974 }
975 return path.getBounds2D();
976 }
977
978 /**
979 * Returns the shape that makes up the left (top) edge of this text layout.
980 *
981 * @param b the bounds
982 *
983 * @return the shape that makes up the left (top) edge of this text layout
984 */
985 private Shape left(Rectangle2D b)
986 {
987 GeneralPath left = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
988 left.append(getCaretShape(TextHitInfo.beforeOffset(0)), false);
989 if (isVertical())
990 {
991 float y = (float) b.getMinY();
992 left.append(new Line2D.Float((float) b.getMinX(), y,
993 (float) b.getMaxX(), y), false);
994 }
995 else
996 {
997 float x = (float) b.getMinX();
998 left.append(new Line2D.Float(x, (float) b.getMinY(),
999 x, (float) b.getMaxY()), false);
1000 }
1001 return left.getBounds2D();
1002 }
1003
1004 /**
1005 * Returns the shape that makes up the right (bottom) edge of this text
1006 * layout.
1007 *
1008 * @param b the bounds
1009 *
1010 * @return the shape that makes up the right (bottom) edge of this text
1011 * layout
1012 */
1013 private Shape right(Rectangle2D b)
1014 {
1015 GeneralPath right = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
1016 right.append(getCaretShape(TextHitInfo.afterOffset(length)), false);
1017 if (isVertical())
1018 {
1019 float y = (float) b.getMaxY();
1020 right.append(new Line2D.Float((float) b.getMinX(), y,
1021 (float) b.getMaxX(), y), false);
1022 }
1023 else
1024 {
1025 float x = (float) b.getMaxX();
1026 right.append(new Line2D.Float(x, (float) b.getMinY(),
1027 x, (float) b.getMaxY()), false);
1028 }
1029 return right.getBounds2D();
1030 }
1031
1032 public TextHitInfo getVisualOtherHit (TextHitInfo hit)
1033 {
1034 checkHitInfo(hit);
1035 int hitIndex = hit.getCharIndex();
1036
1037 int index;
1038 boolean leading;
1039 if (hitIndex == -1 || hitIndex == length)
1040 {
1041 // Boundary case.
1042 int visual;
1043 if (isLeftToRight() == (hitIndex == -1))
1044 visual = 0;
1045 else
1046 visual = length - 1;
1047 index = visualToLogical[visual];
1048 if (isLeftToRight() == (hitIndex == -1))
1049 leading = isCharacterLTR(index); // LTR.
1050 else
1051 leading = ! isCharacterLTR(index); // RTL.
1052 }
1053 else
1054 {
1055 // Normal case.
1056 int visual = logicalToVisual[hitIndex];
1057 boolean b;
1058 if (isCharacterLTR(hitIndex) == hit.isLeadingEdge())
1059 {
1060 visual--;
1061 b = false;
1062 }
1063 else
1064 {
1065 visual++;
1066 b = true;
1067 }
1068 if (visual >= 0 && visual < length)
1069 {
1070 index = visualToLogical[visual];
1071 leading = b == isLeftToRight();
1072 }
1073 else
1074 {
1075 index = b == isLeftToRight() ? length : -1;
1076 leading = index == length;
1077 }
1078 }
1079 return leading ? TextHitInfo.leading(index) : TextHitInfo.trailing(index);
1080 }
1081
1082 /**
1083 * This is a protected method of a <code>final</code> class, meaning
1084 * it exists only to taunt you.
1085 */
1086 protected void handleJustify (float justificationWidth)
1087 {
1088 // We assume that the text has non-trailing whitespace.
1089 // First get the change in width to insert into the whitespaces.
1090 double deltaW = justificationWidth - getVisibleAdvance();
1091 int nglyphs = 0; // # of whitespace chars
1092
1093 // determine last non-whitespace char.
1094 int lastNWS = offset + length - 1;
1095 while( Character.isWhitespace( string[lastNWS] ) ) lastNWS--;
1096
1097 // locations of the glyphs.
1098 int[] wsglyphs = new int[length * 10];
1099 for(int run = 0; run < runs.length; run++ )
1100 {
1101 Run current = runs[run];
1102 for(int i = 0; i < current.glyphVector.getNumGlyphs(); i++ )
1103 {
1104 int cindex = current.runStart
1105 + current.glyphVector.getGlyphCharIndex( i );
1106 if( Character.isWhitespace( string[cindex] ) )
1107 // && cindex < lastNWS )
1108 {
1109 wsglyphs[ nglyphs * 2 ] = run;
1110 wsglyphs[ nglyphs * 2 + 1] = i;
1111 nglyphs++;
1112 }
1113 }
1114 }
1115 deltaW = deltaW / nglyphs; // Change in width per whitespace glyph
1116 double w = 0;
1117 int cws = 0;
1118 // Shift all characters
1119 for(int run = 0; run < runs.length; run++ )
1120 {
1121 Run current = runs[run];
1122 for(int i = 0; i < current.glyphVector.getNumGlyphs(); i++ )
1123 {
1124 if( wsglyphs[ cws * 2 ] == run && wsglyphs[ cws * 2 + 1 ] == i )
1125 {
1126 cws++; // update 'current whitespace'
1127 w += deltaW; // increment the shift
1128 }
1129 Point2D p = current.glyphVector.getGlyphPosition( i );
1130 p.setLocation( p.getX() + w, p.getY() );
1131 current.glyphVector.setGlyphPosition( i, p );
1132 }
1133 }
1134 }
1135
1136 public TextHitInfo hitTestChar (float x, float y)
1137 {
1138 return hitTestChar(x, y, getNaturalBounds());
1139 }
1140
1141 /**
1142 * Finds the character hit at the specified point. This 'clips' this
1143 * text layout against the specified <code>bounds</code> rectangle. That
1144 * means that in the case where a point is outside these bounds, this method
1145 * returns the leading edge of the first character or the trailing edge of
1146 * the last character.
1147 *
1148 * @param x the X location to test
1149 * @param y the Y location to test
1150 * @param bounds the bounds to test against
1151 *
1152 * @return the character hit at the specified point
1153 */
1154 public TextHitInfo hitTestChar (float x, float y, Rectangle2D bounds)
1155 {
1156 // Check bounds.
1157 if (isVertical())
1158 {
1159 if (y < bounds.getMinY())
1160 return TextHitInfo.leading(0);
1161 else if (y > bounds.getMaxY())
1162 return TextHitInfo.trailing(getCharacterCount() - 1);
1163 }
1164 else
1165 {
1166 if (x < bounds.getMinX())
1167 return TextHitInfo.leading(0);
1168 else if (x > bounds.getMaxX())
1169 return TextHitInfo.trailing(getCharacterCount() - 1);
1170 }
1171
1172 TextHitInfo hitInfo = null;
1173 if (isVertical())
1174 {
1175 // Search for the run at the location.
1176 // TODO: Perform binary search for maximum efficiency. However, we
1177 // need the run location laid out statically to do that.
1178 int numRuns = runs.length;
1179 Run hitRun = null;
1180 for (int i = 0; i < numRuns && hitRun == null; i++)
1181 {
1182 Run run = runs[i];
1183 Rectangle2D lBounds = run.glyphVector.getLogicalBounds();
1184 if (lBounds.getMinY() + run.location <= y
1185 && lBounds.getMaxY() + run.location >= y)
1186 hitRun = run;
1187 }
1188 // Now we have (hopefully) found a run that hits. Now find the
1189 // right character.
1190 if (hitRun != null)
1191 {
1192 GlyphVector gv = hitRun.glyphVector;
1193 for (int i = hitRun.runStart;
1194 i < hitRun.runEnd && hitInfo == null; i++)
1195 {
1196 int gi = i - hitRun.runStart;
1197 Rectangle2D lBounds = gv.getGlyphLogicalBounds(gi)
1198 .getBounds2D();
1199 if (lBounds.getMinY() + hitRun.location <= y
1200 && lBounds.getMaxY() + hitRun.location >= y)
1201 {
1202 // Found hit. Now check if we are leading or trailing.
1203 boolean leading = true;
1204 if (lBounds.getCenterY() + hitRun.location <= y)
1205 leading = false;
1206 hitInfo = leading ? TextHitInfo.leading(i)
1207 : TextHitInfo.trailing(i);
1208 }
1209 }
1210 }
1211 }
1212 else
1213 {
1214 // Search for the run at the location.
1215 // TODO: Perform binary search for maximum efficiency. However, we
1216 // need the run location laid out statically to do that.
1217 int numRuns = runs.length;
1218 Run hitRun = null;
1219 for (int i = 0; i < numRuns && hitRun == null; i++)
1220 {
1221 Run run = runs[i];
1222 Rectangle2D lBounds = run.glyphVector.getLogicalBounds();
1223 if (lBounds.getMinX() + run.location <= x
1224 && lBounds.getMaxX() + run.location >= x)
1225 hitRun = run;
1226 }
1227 // Now we have (hopefully) found a run that hits. Now find the
1228 // right character.
1229 if (hitRun != null)
1230 {
1231 GlyphVector gv = hitRun.glyphVector;
1232 for (int i = hitRun.runStart;
1233 i < hitRun.runEnd && hitInfo == null; i++)
1234 {
1235 int gi = i - hitRun.runStart;
1236 Rectangle2D lBounds = gv.getGlyphLogicalBounds(gi)
1237 .getBounds2D();
1238 if (lBounds.getMinX() + hitRun.location <= x
1239 && lBounds.getMaxX() + hitRun.location >= x)
1240 {
1241 // Found hit. Now check if we are leading or trailing.
1242 boolean leading = true;
1243 if (lBounds.getCenterX() + hitRun.location <= x)
1244 leading = false;
1245 hitInfo = leading ? TextHitInfo.leading(i)
1246 : TextHitInfo.trailing(i);
1247 }
1248 }
1249 }
1250 }
1251 return hitInfo;
1252 }
1253
1254 public boolean isLeftToRight ()
1255 {
1256 return leftToRight;
1257 }
1258
1259 public boolean isVertical ()
1260 {
1261 return false; // FIXME: How do you create a vertical layout?
1262 }
1263
1264 public int hashCode ()
1265 {
1266 // This is implemented in sync to equals().
1267 if (hash == 0 && runs.length > 0)
1268 {
1269 hash = runs.length;
1270 for (int i = 0; i < runs.length; i++)
1271 hash ^= runs[i].glyphVector.hashCode();
1272 }
1273 return hash;
1274 }
1275
1276 public String toString ()
1277 {
1278 return "TextLayout [string:"+ new String(string, offset, length)
1279 +" Rendercontext:"+
1280 frc+"]";
1281 }
1282
1283 /**
1284 * Returns the natural bounds of that text layout. This is made up
1285 * of the ascent plus descent and the text advance.
1286 *
1287 * @return the natural bounds of that text layout
1288 */
1289 private Rectangle2D getNaturalBounds()
1290 {
1291 if (naturalBounds == null)
1292 naturalBounds = new Rectangle2D.Float(0.0F, -getAscent(), getAdvance(),
1293 getAscent() + getDescent());
1294 return naturalBounds;
1295 }
1296
1297 private void checkHitInfo(TextHitInfo hit)
1298 {
1299 if (hit == null)
1300 throw new IllegalArgumentException("Null hit info not allowed");
1301 int index = hit.getInsertionIndex();
1302 if (index < 0 || index > length)
1303 throw new IllegalArgumentException("Hit index out of range");
1304 }
1305
1306 private int hitToCaret(TextHitInfo hit)
1307 {
1308 int index = hit.getCharIndex();
1309 int ret;
1310 if (index < 0)
1311 ret = isLeftToRight() ? 0 : length;
1312 else if (index >= length)
1313 ret = isLeftToRight() ? length : 0;
1314 else
1315 {
1316 ret = logicalToVisual[index];
1317 if (hit.isLeadingEdge() != isCharacterLTR(index))
1318 ret++;
1319 }
1320 return ret;
1321 }
1322
1323 private TextHitInfo caretToHit(int index)
1324 {
1325 TextHitInfo hit;
1326 if (index == 0 || index == length)
1327 {
1328 if ((index == length) == isLeftToRight())
1329 hit = TextHitInfo.leading(length);
1330 else
1331 hit = TextHitInfo.trailing(-1);
1332 }
1333 else
1334 {
1335 int logical = visualToLogical[index];
1336 boolean leading = isCharacterLTR(logical); // LTR.
1337 hit = leading ? TextHitInfo.leading(logical)
1338 : TextHitInfo.trailing(logical);
1339 }
1340 return hit;
1341 }
1342
1343 private boolean isCharacterLTR(int index)
1344 {
1345 byte level = getCharacterLevel(index);
1346 return (level & 1) == 0;
1347 }
1348
1349 /**
1350 * Finds the run that holds the specified (logical) character index. This
1351 * returns <code>null</code> when the index is not inside the range.
1352 *
1353 * @param index the index of the character to find
1354 *
1355 * @return the run that holds the specified character
1356 */
1357 private Run findRunAtIndex(int index)
1358 {
1359 Run found = null;
1360 // TODO: Can we do better than linear searching here?
1361 for (int i = 0; i < runs.length && found == null; i++)
1362 {
1363 Run run = runs[i];
1364 if (run.runStart <= index && run.runEnd > index)
1365 found = run;
1366 }
1367 return found;
1368 }
1369
1370 /**
1371 * Computes the layout locations for each run.
1372 */
1373 private void layoutRuns()
1374 {
1375 float loc = 0.0F;
1376 float lastWidth = 0.0F;
1377 for (int i = 0; i < runs.length; i++)
1378 {
1379 runs[i].location = loc;
1380 Rectangle2D bounds = runs[i].glyphVector.getLogicalBounds();
1381 loc += isVertical() ? bounds.getHeight() : bounds.getWidth();
1382 }
1383 }
1384
1385 /**
1386 * Inner class describing a caret policy
1387 */
1388 public static class CaretPolicy
1389 {
1390 public CaretPolicy()
1391 {
1392 }
1393
1394 public TextHitInfo getStrongCaret(TextHitInfo hit1,
1395 TextHitInfo hit2,
1396 TextLayout layout)
1397 {
1398 byte l1 = layout.getCharacterLevel(hit1.getCharIndex());
1399 byte l2 = layout.getCharacterLevel(hit2.getCharIndex());
1400 TextHitInfo strong;
1401 if (l1 == l2)
1402 {
1403 if (hit2.isLeadingEdge() && ! hit1.isLeadingEdge())
1404 strong = hit2;
1405 else
1406 strong = hit1;
1407 }
1408 else
1409 {
1410 if (l1 < l2)
1411 strong = hit1;
1412 else
1413 strong = hit2;
1414 }
1415 return strong;
1416 }
1417 }
1418 }
1419
1420