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