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 gnu.java.lang.CPStringBuilder; 042 043 import java.awt.Font; 044 import java.awt.Graphics2D; 045 import java.awt.Shape; 046 import java.awt.geom.AffineTransform; 047 import java.awt.geom.Line2D; 048 import java.awt.geom.Rectangle2D; 049 import java.awt.geom.GeneralPath; 050 import java.awt.geom.Point2D; 051 import java.text.CharacterIterator; 052 import java.text.AttributedCharacterIterator; 053 import java.text.Bidi; 054 import java.util.ArrayList; 055 import java.util.Map; 056 057 /** 058 * @author Sven de Marothy 059 */ 060 public final class TextLayout implements Cloneable 061 { 062 /** 063 * Holds the layout data that belongs to one run of characters. 064 */ 065 private class Run 066 { 067 /** 068 * The actual glyph vector. 069 */ 070 GlyphVector glyphVector; 071 072 /** 073 * The font for this text run. 074 */ 075 Font font; 076 077 /** 078 * The start of the run. 079 */ 080 int runStart; 081 082 /** 083 * The end of the run. 084 */ 085 int runEnd; 086 087 /** 088 * The layout location of the beginning of the run. 089 */ 090 float location; 091 092 /** 093 * Initializes the Run instance. 094 * 095 * @param gv the glyph vector 096 * @param start the start index of the run 097 * @param end the end index of the run 098 */ 099 Run(GlyphVector gv, Font f, int start, int end) 100 { 101 glyphVector = gv; 102 font = f; 103 runStart = start; 104 runEnd = end; 105 } 106 107 /** 108 * Returns <code>true</code> when this run is left to right, 109 * <code>false</code> otherwise. 110 * 111 * @return <code>true</code> when this run is left to right, 112 * <code>false</code> otherwise 113 */ 114 boolean isLeftToRight() 115 { 116 return (glyphVector.getLayoutFlags() & GlyphVector.FLAG_RUN_RTL) == 0; 117 } 118 } 119 120 /** 121 * The laid out character runs. 122 */ 123 private Run[] runs; 124 125 private FontRenderContext frc; 126 private char[] string; 127 private int offset; 128 private int length; 129 private Rectangle2D boundsCache; 130 private LineMetrics lm; 131 132 /** 133 * The total advance of this text layout. This is cache for maximum 134 * performance. 135 */ 136 private float totalAdvance = -1F; 137 138 /** 139 * The cached natural bounds. 140 */ 141 private Rectangle2D naturalBounds; 142 143 /** 144 * Character indices. 145 * Fixt index is the glyphvector, second index is the (first) glyph. 146 */ 147 private int[][] charIndices; 148 149 /** 150 * Base directionality, determined from the first char. 151 */ 152 private boolean leftToRight; 153 154 /** 155 * Whether this layout contains whitespace or not. 156 */ 157 private boolean hasWhitespace = false; 158 159 /** 160 * The {@link Bidi} object that is used for reordering and by 161 * {@link #getCharacterLevel(int)}. 162 */ 163 private Bidi bidi; 164 165 /** 166 * Mpas the logical position of each individual character in the original 167 * string to its visual position. 168 */ 169 private int[] logicalToVisual; 170 171 /** 172 * Maps visual positions of a character to its logical position 173 * in the original string. 174 */ 175 private int[] visualToLogical; 176 177 /** 178 * The cached hashCode. 179 */ 180 private int hash; 181 182 /** 183 * The default caret policy. 184 */ 185 public static final TextLayout.CaretPolicy DEFAULT_CARET_POLICY = 186 new CaretPolicy(); 187 188 /** 189 * Constructs a TextLayout. 190 */ 191 public TextLayout (String str, Font font, FontRenderContext frc) 192 { 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); 198 199 // Get base direction and whitespace info 200 getStringProperties(); 201 202 if (Bidi.requiresBidi(string, offset, offset + length)) 203 { 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); 210 211 runs = new Run[rc]; 212 for(int i = 0; i < rc; i++) 213 { 214 int start = bidi.getRunStart(i); 215 int end = bidi.getRunLimit(i); 216 if(start != end) // no empty runs. 217 { 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); 223 } 224 } 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++) 229 { 230 if (runs[i] != null) 231 cleaned.add(runs[i]); 232 } 233 runs = new Run[cleaned.size()]; 234 runs = (Run[]) cleaned.toArray(runs); 235 } 236 else 237 { 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 }; 243 } 244 setCharIndices(); 245 setupMappings(); 246 layoutRuns(); 247 } 248 249 public TextLayout (String string, 250 Map<? extends AttributedCharacterIterator.Attribute, ?> attributes, 251 FontRenderContext frc) 252 { 253 this( string, new Font( attributes ), frc ); 254 } 255 256 public TextLayout (AttributedCharacterIterator text, FontRenderContext frc) 257 { 258 // FIXME: Very rudimentary. 259 this(getText(text), getFont(text), frc); 260 } 261 262 /** 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. 266 */ 267 TextLayout(TextLayout t, int startIndex, int endIndex) 268 { 269 frc = t.frc; 270 boundsCache = null; 271 lm = t.lm; 272 leftToRight = t.leftToRight; 273 274 if( endIndex > t.getCharacterCount() ) 275 endIndex = t.getCharacterCount(); 276 string = t.string; 277 offset = startIndex + offset; 278 length = endIndex - startIndex; 279 280 int startingRun = t.charIndices[startIndex][0]; 281 int nRuns = 1 + t.charIndices[endIndex - 1][0] - startingRun; 282 283 runs = new Run[nRuns]; 284 for( int i = 0; i < nRuns; i++ ) 285 { 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; 293 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); 298 } 299 runs[nRuns - 1].runEnd = endIndex - 1; 300 301 setCharIndices(); 302 setupMappings(); 303 determineWhiteSpace(); 304 layoutRuns(); 305 } 306 307 private void setCharIndices() 308 { 309 charIndices = new int[ getCharacterCount() ][2]; 310 int i = 0; 311 int currentChar = 0; 312 for(int run = 0; run < runs.length; run++) 313 { 314 currentChar = -1; 315 Run current = runs[run]; 316 GlyphVector gv = current.glyphVector; 317 for( int gi = 0; gi < gv.getNumGlyphs(); gi++) 318 { 319 if( gv.getGlyphCharIndex( gi ) != currentChar ) 320 { 321 charIndices[ i ][0] = run; 322 charIndices[ i ][1] = gi; 323 currentChar = gv.getGlyphCharIndex( gi ); 324 i++; 325 } 326 } 327 } 328 } 329 330 /** 331 * Initializes the logicalToVisual and visualToLogial maps. 332 */ 333 private void setupMappings() 334 { 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++) 342 { 343 Run run = runs[i]; 344 if (run.isLeftToRight()) 345 { 346 for (lIndex = run.runStart; lIndex < run.runEnd; lIndex++) 347 { 348 logicalToVisual[lIndex] = vIndex; 349 visualToLogical[vIndex] = lIndex; 350 vIndex++; 351 } 352 } 353 else 354 { 355 for (lIndex = run.runEnd - 1; lIndex >= run.runStart; lIndex--) 356 { 357 logicalToVisual[lIndex] = vIndex; 358 visualToLogical[vIndex] = lIndex; 359 vIndex++; 360 } 361 } 362 } 363 } 364 365 private static String getText(AttributedCharacterIterator iter) 366 { 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(); 373 } 374 375 private static Font getFont(AttributedCharacterIterator iter) 376 { 377 Font f = (Font)iter.getAttribute(TextAttribute.FONT); 378 if( f == null ) 379 { 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 ); 387 } 388 return f; 389 } 390 391 /** 392 * Scan the character run for the first strongly directional character, 393 * which in turn defines the base directionality of the whole layout. 394 */ 395 private void getStringProperties() 396 { 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++]) ) 403 { 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; 409 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; 417 } 418 determineWhiteSpace(); 419 } 420 421 private void determineWhiteSpace() 422 { 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; 433 } 434 435 protected Object clone () 436 { 437 return new TextLayout( this, 0, length); 438 } 439 440 public void draw (Graphics2D g2, float x, float y) 441 { 442 for(int i = 0; i < runs.length; i++) 443 { 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(); 449 } 450 } 451 452 public boolean equals (Object obj) 453 { 454 if( !( obj instanceof TextLayout) ) 455 return false; 456 457 return equals( (TextLayout) obj ); 458 } 459 460 public boolean equals (TextLayout tl) 461 { 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; 469 } 470 471 public float getAdvance () 472 { 473 if (totalAdvance == -1F) 474 { 475 totalAdvance = 0f; 476 for(int i = 0; i < runs.length; i++) 477 { 478 Run run = runs[i]; 479 GlyphVector gv = run.glyphVector; 480 totalAdvance += gv.getLogicalBounds().getWidth(); 481 } 482 } 483 return totalAdvance; 484 } 485 486 public float getAscent () 487 { 488 return lm.getAscent(); 489 } 490 491 public byte getBaseline () 492 { 493 return (byte)lm.getBaselineIndex(); 494 } 495 496 public float[] getBaselineOffsets () 497 { 498 return lm.getBaselineOffsets(); 499 } 500 501 public Shape getBlackBoxBounds (int firstEndpoint, int secondEndpoint) 502 { 503 if( secondEndpoint - firstEndpoint <= 0 ) 504 return new Rectangle2D.Float(); // Hmm? 505 506 if( firstEndpoint < 0 || secondEndpoint > getCharacterCount()) 507 return new Rectangle2D.Float(); 508 509 GeneralPath gp = new GeneralPath(); 510 511 int ri = charIndices[ firstEndpoint ][0]; 512 int gi = charIndices[ firstEndpoint ][1]; 513 514 double advance = 0; 515 516 for( int i = 0; i < ri; i++ ) 517 { 518 Run run = runs[i]; 519 GlyphVector gv = run.glyphVector; 520 advance += gv.getLogicalBounds().getWidth(); 521 } 522 523 for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ ) 524 { 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; 532 533 for( int j = 0; j <= dg; j++ ) 534 { 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); 541 } 542 543 advance += gv.getLogicalBounds().getWidth(); 544 } 545 return gp; 546 } 547 548 public Rectangle2D getBounds() 549 { 550 if( boundsCache == null ) 551 boundsCache = getOutline(new AffineTransform()).getBounds(); 552 return boundsCache; 553 } 554 555 public float[] getCaretInfo (TextHitInfo hit) 556 { 557 return getCaretInfo(hit, getNaturalBounds()); 558 } 559 560 public float[] getCaretInfo (TextHitInfo hit, Rectangle2D bounds) 561 { 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; 567 568 if (index >= length) 569 { 570 info[0] = getAdvance(); 571 info[1] = 0; 572 } 573 else 574 { 575 if (index < 0) 576 { 577 run = runs[0]; 578 index = 0; 579 leading = true; 580 } 581 else 582 run = findRunAtIndex(index); 583 584 int glyphIndex = index - run.runStart; 585 Shape glyphBounds = run.glyphVector.getGlyphLogicalBounds(glyphIndex); 586 Rectangle2D glyphRect = glyphBounds.getBounds2D(); 587 if (isVertical()) 588 { 589 if (leading) 590 info[0] = (float) glyphRect.getMinY(); 591 else 592 info[0] = (float) glyphRect.getMaxY(); 593 } 594 else 595 { 596 if (leading) 597 info[0] = (float) glyphRect.getMinX(); 598 else 599 info[0] = (float) glyphRect.getMaxX(); 600 } 601 info[0] += run.location; 602 info[1] = run.font.getItalicAngle(); 603 } 604 return info; 605 } 606 607 public Shape getCaretShape(TextHitInfo hit) 608 { 609 return getCaretShape(hit, getBounds()); 610 } 611 612 public Shape getCaretShape(TextHitInfo hit, Rectangle2D bounds) 613 { 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) 621 { 622 // Shift x1 and x2 according to the slope. 623 x1 -= y1 * info[1]; 624 x2 -= y2 * info[1]; 625 } 626 GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 2); 627 path.moveTo(x1, y1); 628 path.lineTo(x2, y2); 629 return path; 630 } 631 632 public Shape[] getCaretShapes(int offset) 633 { 634 return getCaretShapes(offset, getNaturalBounds()); 635 } 636 637 public Shape[] getCaretShapes(int offset, Rectangle2D bounds) 638 { 639 return getCaretShapes(offset, bounds, DEFAULT_CARET_POLICY); 640 } 641 642 public Shape[] getCaretShapes(int offset, Rectangle2D bounds, 643 CaretPolicy policy) 644 { 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) 653 { 654 carets[0] = getCaretShape(hit1); 655 carets[1] = null; // The RI returns null in this seldom case. 656 } 657 else 658 { 659 Shape caret1 = getCaretShape(hit1); 660 Shape caret2 = getCaretShape(hit2); 661 TextHitInfo strong = policy.getStrongCaret(hit1, hit2, this); 662 if (strong == hit1) 663 { 664 carets[0] = caret1; 665 carets[1] = caret2; 666 } 667 else 668 { 669 carets[0] = caret2; 670 carets[1] = caret1; 671 } 672 } 673 return carets; 674 } 675 676 public int getCharacterCount () 677 { 678 return length; 679 } 680 681 public byte getCharacterLevel (int index) 682 { 683 byte level; 684 if( bidi == null ) 685 level = 0; 686 else 687 level = (byte) bidi.getLevelAt(index); 688 return level; 689 } 690 691 public float getDescent () 692 { 693 return lm.getDescent(); 694 } 695 696 public TextLayout getJustifiedLayout (float justificationWidth) 697 { 698 TextLayout newLayout = (TextLayout)clone(); 699 700 if( hasWhitespace ) 701 newLayout.handleJustify( justificationWidth ); 702 703 return newLayout; 704 } 705 706 public float getLeading () 707 { 708 return lm.getLeading(); 709 } 710 711 public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint) 712 { 713 return getLogicalHighlightShape( firstEndpoint, secondEndpoint, 714 getBounds() ); 715 } 716 717 public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint, 718 Rectangle2D bounds) 719 { 720 if( secondEndpoint - firstEndpoint <= 0 ) 721 return new Rectangle2D.Float(); // Hmm? 722 723 if( firstEndpoint < 0 || secondEndpoint > getCharacterCount()) 724 return new Rectangle2D.Float(); 725 726 Rectangle2D r = null; 727 int ri = charIndices[ firstEndpoint ][0]; 728 int gi = charIndices[ firstEndpoint ][1]; 729 730 double advance = 0; 731 732 for( int i = 0; i < ri; i++ ) 733 advance += runs[i].glyphVector.getLogicalBounds().getWidth(); 734 735 for( int i = ri; i <= charIndices[ secondEndpoint - 1 ][0]; i++ ) 736 { 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; 744 745 for(; gi <= dg; gi++ ) 746 { 747 Rectangle2D r2 = (gv.getGlyphLogicalBounds( gi )). 748 getBounds2D(); 749 if( r == null ) 750 r = r2; 751 else 752 r = r.createUnion(r2); 753 } 754 gi = 0; // reset glyph index into run for next run. 755 756 advance += gv.getLogicalBounds().getWidth(); 757 } 758 759 return r; 760 } 761 762 public int[] getLogicalRangesForVisualSelection (TextHitInfo firstEndpoint, 763 TextHitInfo secondEndpoint) 764 { 765 // Check parameters. 766 checkHitInfo(firstEndpoint); 767 checkHitInfo(secondEndpoint); 768 769 // Convert to visual and order correctly. 770 int start = hitToCaret(firstEndpoint); 771 int end = hitToCaret(secondEndpoint); 772 if (start > end) 773 { 774 // Swap start and end so that end >= start. 775 int temp = start; 776 start = end; 777 end = temp; 778 } 779 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++) 783 { 784 include[visualToLogical[i]] = true; 785 } 786 787 // Count included runs. 788 int numRuns = 0; 789 boolean in = false; 790 for (int i = 0; i < length; i++) 791 { 792 if (include[i] != in) // At each run in/out point we toggle the in var. 793 { 794 in = ! in; 795 if (in) // At each run start we count up. 796 numRuns++; 797 } 798 } 799 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++) 805 { 806 if (include[i] != in) 807 { 808 ranges[index] = i; 809 index++; 810 in = ! in; 811 } 812 } 813 // If the last run ends at the very end, include that last bit too. 814 if (in) 815 ranges[index] = length; 816 817 return ranges; 818 } 819 820 public TextHitInfo getNextLeftHit(int offset) 821 { 822 return getNextLeftHit(offset, DEFAULT_CARET_POLICY); 823 } 824 825 public TextHitInfo getNextLeftHit(int offset, CaretPolicy policy) 826 { 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"); 831 832 TextHitInfo hit1 = TextHitInfo.afterOffset(offset); 833 TextHitInfo hit2 = hit1.getOtherHit(); 834 835 TextHitInfo strong = policy.getStrongCaret(hit1, hit2, this); 836 TextHitInfo next = getNextLeftHit(strong); 837 TextHitInfo ret = null; 838 if (next != null) 839 { 840 TextHitInfo next2 = getVisualOtherHit(next); 841 ret = policy.getStrongCaret(next2, next, this); 842 } 843 return ret; 844 } 845 846 public TextHitInfo getNextLeftHit (TextHitInfo hit) 847 { 848 checkHitInfo(hit); 849 int index = hitToCaret(hit); 850 TextHitInfo next = null; 851 if (index != 0) 852 { 853 index--; 854 next = caretToHit(index); 855 } 856 return next; 857 } 858 859 public TextHitInfo getNextRightHit(int offset) 860 { 861 return getNextRightHit(offset, DEFAULT_CARET_POLICY); 862 } 863 864 public TextHitInfo getNextRightHit(int offset, CaretPolicy policy) 865 { 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"); 870 871 TextHitInfo hit1 = TextHitInfo.afterOffset(offset); 872 TextHitInfo hit2 = hit1.getOtherHit(); 873 874 TextHitInfo next = getNextRightHit(policy.getStrongCaret(hit1, hit2, this)); 875 TextHitInfo ret = null; 876 if (next != null) 877 { 878 TextHitInfo next2 = getVisualOtherHit(next); 879 ret = policy.getStrongCaret(next2, next, this); 880 } 881 return ret; 882 } 883 884 public TextHitInfo getNextRightHit(TextHitInfo hit) 885 { 886 checkHitInfo(hit); 887 int index = hitToCaret(hit); 888 TextHitInfo next = null; 889 if (index < length) 890 { 891 index++; 892 next = caretToHit(index); 893 } 894 return next; 895 } 896 897 public Shape getOutline (AffineTransform tx) 898 { 899 float x = 0f; 900 GeneralPath gp = new GeneralPath(); 901 for(int i = 0; i < runs.length; i++) 902 { 903 GlyphVector gv = runs[i].glyphVector; 904 gp.append( gv.getOutline( x, 0f ), false ); 905 Rectangle2D r = gv.getLogicalBounds(); 906 x += r.getWidth(); 907 } 908 if( tx != null ) 909 gp.transform( tx ); 910 return gp; 911 } 912 913 public float getVisibleAdvance () 914 { 915 float totalAdvance = 0f; 916 917 if( runs.length <= 0 ) 918 return 0f; 919 920 // No trailing whitespace 921 if( !Character.isWhitespace( string[offset + length - 1]) ) 922 return getAdvance(); 923 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(); 927 928 int lastRun = runs[runs.length - 1].runStart; 929 int j = length - 1; 930 while( j >= lastRun && Character.isWhitespace( string[j] ) ) j--; 931 932 if( j < lastRun ) 933 return totalAdvance; // entire last run is whitespace 934 935 int lastNonWSChar = j - lastRun; 936 j = 0; 937 while( runs[ runs.length - 1 ].glyphVector.getGlyphCharIndex( j ) 938 <= lastNonWSChar ) 939 { 940 totalAdvance += runs[ runs.length - 1 ].glyphVector 941 .getGlyphLogicalBounds( j ) 942 .getBounds2D().getWidth(); 943 j ++; 944 } 945 946 return totalAdvance; 947 } 948 949 public Shape getVisualHighlightShape (TextHitInfo firstEndpoint, 950 TextHitInfo secondEndpoint) 951 { 952 return getVisualHighlightShape( firstEndpoint, secondEndpoint, 953 getBounds() ); 954 } 955 956 public Shape getVisualHighlightShape (TextHitInfo firstEndpoint, 957 TextHitInfo secondEndpoint, 958 Rectangle2D bounds) 959 { 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) 969 { 970 path.append(left(bounds), false); 971 } 972 // Append right (bottom) bounds if necessary. 973 if (c1 == length || c2 == length) 974 { 975 path.append(right(bounds), false); 976 } 977 return path.getBounds2D(); 978 } 979 980 /** 981 * Returns the shape that makes up the left (top) edge of this text layout. 982 * 983 * @param b the bounds 984 * 985 * @return the shape that makes up the left (top) edge of this text layout 986 */ 987 private Shape left(Rectangle2D b) 988 { 989 GeneralPath left = new GeneralPath(GeneralPath.WIND_EVEN_ODD); 990 left.append(getCaretShape(TextHitInfo.beforeOffset(0)), false); 991 if (isVertical()) 992 { 993 float y = (float) b.getMinY(); 994 left.append(new Line2D.Float((float) b.getMinX(), y, 995 (float) b.getMaxX(), y), false); 996 } 997 else 998 { 999 float x = (float) b.getMinX(); 1000 left.append(new Line2D.Float(x, (float) b.getMinY(), 1001 x, (float) b.getMaxY()), false); 1002 } 1003 return left.getBounds2D(); 1004 } 1005 1006 /** 1007 * Returns the shape that makes up the right (bottom) edge of this text 1008 * layout. 1009 * 1010 * @param b the bounds 1011 * 1012 * @return the shape that makes up the right (bottom) edge of this text 1013 * layout 1014 */ 1015 private Shape right(Rectangle2D b) 1016 { 1017 GeneralPath right = new GeneralPath(GeneralPath.WIND_EVEN_ODD); 1018 right.append(getCaretShape(TextHitInfo.afterOffset(length)), false); 1019 if (isVertical()) 1020 { 1021 float y = (float) b.getMaxY(); 1022 right.append(new Line2D.Float((float) b.getMinX(), y, 1023 (float) b.getMaxX(), y), false); 1024 } 1025 else 1026 { 1027 float x = (float) b.getMaxX(); 1028 right.append(new Line2D.Float(x, (float) b.getMinY(), 1029 x, (float) b.getMaxY()), false); 1030 } 1031 return right.getBounds2D(); 1032 } 1033 1034 public TextHitInfo getVisualOtherHit (TextHitInfo hit) 1035 { 1036 checkHitInfo(hit); 1037 int hitIndex = hit.getCharIndex(); 1038 1039 int index; 1040 boolean leading; 1041 if (hitIndex == -1 || hitIndex == length) 1042 { 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. 1054 } 1055 else 1056 { 1057 // Normal case. 1058 int visual = logicalToVisual[hitIndex]; 1059 boolean b; 1060 if (isCharacterLTR(hitIndex) == hit.isLeadingEdge()) 1061 { 1062 visual--; 1063 b = false; 1064 } 1065 else 1066 { 1067 visual++; 1068 b = true; 1069 } 1070 if (visual >= 0 && visual < length) 1071 { 1072 index = visualToLogical[visual]; 1073 leading = b == isLeftToRight(); 1074 } 1075 else 1076 { 1077 index = b == isLeftToRight() ? length : -1; 1078 leading = index == length; 1079 } 1080 } 1081 return leading ? TextHitInfo.leading(index) : TextHitInfo.trailing(index); 1082 } 1083 1084 /** 1085 * This is a protected method of a <code>final</code> class, meaning 1086 * it exists only to taunt you. 1087 */ 1088 protected void handleJustify (float justificationWidth) 1089 { 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 1094 1095 // determine last non-whitespace char. 1096 int lastNWS = offset + length - 1; 1097 while( Character.isWhitespace( string[lastNWS] ) ) lastNWS--; 1098 1099 // locations of the glyphs. 1100 int[] wsglyphs = new int[length * 10]; 1101 for(int run = 0; run < runs.length; run++ ) 1102 { 1103 Run current = runs[run]; 1104 for(int i = 0; i < current.glyphVector.getNumGlyphs(); i++ ) 1105 { 1106 int cindex = current.runStart 1107 + current.glyphVector.getGlyphCharIndex( i ); 1108 if( Character.isWhitespace( string[cindex] ) ) 1109 // && cindex < lastNWS ) 1110 { 1111 wsglyphs[ nglyphs * 2 ] = run; 1112 wsglyphs[ nglyphs * 2 + 1] = i; 1113 nglyphs++; 1114 } 1115 } 1116 } 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++ ) 1122 { 1123 Run current = runs[run]; 1124 for(int i = 0; i < current.glyphVector.getNumGlyphs(); i++ ) 1125 { 1126 if( wsglyphs[ cws * 2 ] == run && wsglyphs[ cws * 2 + 1 ] == i ) 1127 { 1128 cws++; // update 'current whitespace' 1129 w += deltaW; // increment the shift 1130 } 1131 Point2D p = current.glyphVector.getGlyphPosition( i ); 1132 p.setLocation( p.getX() + w, p.getY() ); 1133 current.glyphVector.setGlyphPosition( i, p ); 1134 } 1135 } 1136 } 1137 1138 public TextHitInfo hitTestChar (float x, float y) 1139 { 1140 return hitTestChar(x, y, getNaturalBounds()); 1141 } 1142 1143 /** 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. 1149 * 1150 * @param x the X location to test 1151 * @param y the Y location to test 1152 * @param bounds the bounds to test against 1153 * 1154 * @return the character hit at the specified point 1155 */ 1156 public TextHitInfo hitTestChar (float x, float y, Rectangle2D bounds) 1157 { 1158 // Check bounds. 1159 if (isVertical()) 1160 { 1161 if (y < bounds.getMinY()) 1162 return TextHitInfo.leading(0); 1163 else if (y > bounds.getMaxY()) 1164 return TextHitInfo.trailing(getCharacterCount() - 1); 1165 } 1166 else 1167 { 1168 if (x < bounds.getMinX()) 1169 return TextHitInfo.leading(0); 1170 else if (x > bounds.getMaxX()) 1171 return TextHitInfo.trailing(getCharacterCount() - 1); 1172 } 1173 1174 TextHitInfo hitInfo = null; 1175 if (isVertical()) 1176 { 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++) 1183 { 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; 1189 } 1190 // Now we have (hopefully) found a run that hits. Now find the 1191 // right character. 1192 if (hitRun != null) 1193 { 1194 GlyphVector gv = hitRun.glyphVector; 1195 for (int i = hitRun.runStart; 1196 i < hitRun.runEnd && hitInfo == null; i++) 1197 { 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) 1203 { 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); 1210 } 1211 } 1212 } 1213 } 1214 else 1215 { 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++) 1222 { 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; 1228 } 1229 // Now we have (hopefully) found a run that hits. Now find the 1230 // right character. 1231 if (hitRun != null) 1232 { 1233 GlyphVector gv = hitRun.glyphVector; 1234 for (int i = hitRun.runStart; 1235 i < hitRun.runEnd && hitInfo == null; i++) 1236 { 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) 1242 { 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); 1249 } 1250 } 1251 } 1252 } 1253 return hitInfo; 1254 } 1255 1256 public boolean isLeftToRight () 1257 { 1258 return leftToRight; 1259 } 1260 1261 public boolean isVertical () 1262 { 1263 return false; // FIXME: How do you create a vertical layout? 1264 } 1265 1266 public int hashCode () 1267 { 1268 // This is implemented in sync to equals(). 1269 if (hash == 0 && runs.length > 0) 1270 { 1271 hash = runs.length; 1272 for (int i = 0; i < runs.length; i++) 1273 hash ^= runs[i].glyphVector.hashCode(); 1274 } 1275 return hash; 1276 } 1277 1278 public String toString () 1279 { 1280 return "TextLayout [string:"+ new String(string, offset, length) 1281 +" Rendercontext:"+ 1282 frc+"]"; 1283 } 1284 1285 /** 1286 * Returns the natural bounds of that text layout. This is made up 1287 * of the ascent plus descent and the text advance. 1288 * 1289 * @return the natural bounds of that text layout 1290 */ 1291 private Rectangle2D getNaturalBounds() 1292 { 1293 if (naturalBounds == null) 1294 naturalBounds = new Rectangle2D.Float(0.0F, -getAscent(), getAdvance(), 1295 getAscent() + getDescent()); 1296 return naturalBounds; 1297 } 1298 1299 private void checkHitInfo(TextHitInfo hit) 1300 { 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"); 1306 } 1307 1308 private int hitToCaret(TextHitInfo hit) 1309 { 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 1317 { 1318 ret = logicalToVisual[index]; 1319 if (hit.isLeadingEdge() != isCharacterLTR(index)) 1320 ret++; 1321 } 1322 return ret; 1323 } 1324 1325 private TextHitInfo caretToHit(int index) 1326 { 1327 TextHitInfo hit; 1328 if (index == 0 || index == length) 1329 { 1330 if ((index == length) == isLeftToRight()) 1331 hit = TextHitInfo.leading(length); 1332 else 1333 hit = TextHitInfo.trailing(-1); 1334 } 1335 else 1336 { 1337 int logical = visualToLogical[index]; 1338 boolean leading = isCharacterLTR(logical); // LTR. 1339 hit = leading ? TextHitInfo.leading(logical) 1340 : TextHitInfo.trailing(logical); 1341 } 1342 return hit; 1343 } 1344 1345 private boolean isCharacterLTR(int index) 1346 { 1347 byte level = getCharacterLevel(index); 1348 return (level & 1) == 0; 1349 } 1350 1351 /** 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. 1354 * 1355 * @param index the index of the character to find 1356 * 1357 * @return the run that holds the specified character 1358 */ 1359 private Run findRunAtIndex(int index) 1360 { 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++) 1364 { 1365 Run run = runs[i]; 1366 if (run.runStart <= index && run.runEnd > index) 1367 found = run; 1368 } 1369 return found; 1370 } 1371 1372 /** 1373 * Computes the layout locations for each run. 1374 */ 1375 private void layoutRuns() 1376 { 1377 float loc = 0.0F; 1378 float lastWidth = 0.0F; 1379 for (int i = 0; i < runs.length; i++) 1380 { 1381 runs[i].location = loc; 1382 Rectangle2D bounds = runs[i].glyphVector.getLogicalBounds(); 1383 loc += isVertical() ? bounds.getHeight() : bounds.getWidth(); 1384 } 1385 } 1386 1387 /** 1388 * Inner class describing a caret policy 1389 */ 1390 public static class CaretPolicy 1391 { 1392 public CaretPolicy() 1393 { 1394 } 1395 1396 public TextHitInfo getStrongCaret(TextHitInfo hit1, 1397 TextHitInfo hit2, 1398 TextLayout layout) 1399 { 1400 byte l1 = layout.getCharacterLevel(hit1.getCharIndex()); 1401 byte l2 = layout.getCharacterLevel(hit2.getCharIndex()); 1402 TextHitInfo strong; 1403 if (l1 == l2) 1404 { 1405 if (hit2.isLeadingEdge() && ! hit1.isLeadingEdge()) 1406 strong = hit2; 1407 else 1408 strong = hit1; 1409 } 1410 else 1411 { 1412 if (l1 < l2) 1413 strong = hit1; 1414 else 1415 strong = hit2; 1416 } 1417 return strong; 1418 } 1419 } 1420 } 1421 1422