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