1 /*
   2  * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.text;
  27 
  28 
  29 import javafx.scene.shape.LineTo;
  30 import javafx.scene.shape.MoveTo;
  31 import javafx.scene.shape.PathElement;
  32 import com.sun.javafx.font.CharToGlyphMapper;
  33 import com.sun.javafx.font.FontResource;
  34 import com.sun.javafx.font.FontStrike;
  35 import com.sun.javafx.font.Metrics;
  36 import com.sun.javafx.font.PGFont;
  37 import com.sun.javafx.font.PrismFontFactory;
  38 import com.sun.javafx.geom.BaseBounds;
  39 import com.sun.javafx.geom.Path2D;
  40 import com.sun.javafx.geom.Point2D;
  41 import com.sun.javafx.geom.RectBounds;
  42 import com.sun.javafx.geom.RoundRectangle2D;
  43 import com.sun.javafx.geom.Shape;
  44 import com.sun.javafx.geom.transform.BaseTransform;
  45 import com.sun.javafx.geom.transform.Translate2D;
  46 import com.sun.javafx.scene.text.GlyphList;
  47 import com.sun.javafx.scene.text.TextLayout;
  48 import com.sun.javafx.scene.text.TextSpan;
  49 import java.text.Bidi;
  50 import java.text.BreakIterator;
  51 import java.util.ArrayList;
  52 import java.util.Arrays;
  53 import java.util.Hashtable;
  54 
  55 public class PrismTextLayout implements TextLayout {
  56     private static final BaseTransform IDENTITY = BaseTransform.IDENTITY_TRANSFORM;
  57     private static final int X_MIN_INDEX = 0;
  58     private static final int Y_MIN_INDEX = 1;
  59     private static final int X_MAX_INDEX = 2;
  60     private static final int Y_MAX_INDEX = 3;
  61 
  62     private static final Hashtable<Integer, LayoutCache> stringCache = new Hashtable<>();
  63     private static final Object  CACHE_SIZE_LOCK = new Object();
  64     private static int cacheSize = 0;
  65     private static final int MAX_STRING_SIZE = 256;
  66     private static final int MAX_CACHE_SIZE = PrismFontFactory.cacheLayoutSize;
  67 
  68     private char[] text;
  69     private TextSpan[] spans;   /* Rich text  (null for single font text) */
  70     private PGFont font;        /* Single font text (null for rich text) */
  71     private FontStrike strike;  /* cached strike of font (identity) */
  72     private Integer cacheKey;
  73     private TextLine[] lines;
  74     private TextRun[] runs;
  75     private int runCount;
  76     private BaseBounds logicalBounds;
  77     private RectBounds visualBounds;
  78     private float layoutWidth, layoutHeight;
  79     private float wrapWidth, spacing;
  80     private LayoutCache layoutCache;
  81     private Shape shape;
  82     private int flags;
  83     private int tabSize = DEFAULT_TAB_SIZE;
  84 
  85     public PrismTextLayout() {
  86         logicalBounds = new RectBounds();
  87         flags = ALIGN_LEFT;
  88     }
  89 
  90     private void reset() {
  91         layoutCache = null;
  92         runs = null;
  93         flags &= ~ANALYSIS_MASK;
  94         relayout();
  95     }
  96 
  97     private void relayout() {
  98         logicalBounds.makeEmpty();
  99         visualBounds = null;
 100         layoutWidth = layoutHeight = 0;
 101         flags &= ~(FLAGS_WRAPPED | FLAGS_CACHED_UNDERLINE | FLAGS_CACHED_STRIKETHROUGH);
 102         lines = null;
 103         shape = null;
 104     }
 105 
 106     /***************************************************************************
 107      *                                                                         *
 108      *                            TextLayout API                               *
 109      *                                                                         *
 110      **************************************************************************/
 111 
 112     public boolean setContent(TextSpan[] spans) {
 113         if (spans == null && this.spans == null) return false;
 114         if (spans != null && this.spans != null) {
 115             if (spans.length == this.spans.length) {
 116                 int i = 0;
 117                 while (i < spans.length) {
 118                     if (spans[i] != this.spans[i]) break;
 119                     i++;
 120                 }
 121                 if (i == spans.length) return false;
 122             }
 123         }
 124 
 125         reset();
 126         this.spans = spans;
 127         this.font = null;
 128         this.strike = null;
 129         this.text = null;   /* Initialized in getText() */
 130         this.cacheKey = null;
 131         return true;
 132     }
 133 
 134     public boolean setContent(String text, Object font) {
 135         reset();
 136         this.spans = null;
 137         this.font = (PGFont)font;
 138         this.strike = ((PGFont)font).getStrike(IDENTITY);
 139         this.text = text.toCharArray();
 140         if (MAX_CACHE_SIZE > 0) {
 141             int length = text.length();
 142             if (0 < length && length <= MAX_STRING_SIZE) {
 143                 cacheKey = text.hashCode() * strike.hashCode();
 144             }
 145         }
 146         return true;
 147     }
 148 
 149     public boolean setDirection(int direction) {
 150         if ((flags & DIRECTION_MASK) == direction) return false;
 151         flags &= ~DIRECTION_MASK;
 152         flags |= (direction & DIRECTION_MASK);
 153         reset();
 154         return true;
 155     }
 156 
 157     public boolean setBoundsType(int type) {
 158         if ((flags & BOUNDS_MASK) == type) return false;
 159         flags &= ~BOUNDS_MASK;
 160         flags |= (type & BOUNDS_MASK);
 161         reset();
 162         return true;
 163     }
 164 
 165     public boolean setAlignment(int alignment) {
 166         int align = ALIGN_LEFT;
 167         switch (alignment) {
 168         case 0: align = ALIGN_LEFT; break;
 169         case 1: align = ALIGN_CENTER; break;
 170         case 2: align = ALIGN_RIGHT; break;
 171         case 3: align = ALIGN_JUSTIFY; break;
 172         }
 173         if ((flags & ALIGN_MASK) == align) return false;
 174         if (align == ALIGN_JUSTIFY || (flags & ALIGN_JUSTIFY) != 0) {
 175             reset();
 176         }
 177         flags &= ~ALIGN_MASK;
 178         flags |= align;
 179         relayout();
 180         return true;
 181     }
 182 
 183     public boolean setWrapWidth(float newWidth) {
 184         if (Float.isInfinite(newWidth)) newWidth = 0;
 185         if (Float.isNaN(newWidth)) newWidth = 0;
 186         float oldWidth = this.wrapWidth;
 187         this.wrapWidth = Math.max(0, newWidth);
 188 
 189         boolean needsLayout = true;
 190         if (lines != null && oldWidth != 0 && newWidth != 0) {
 191             if ((flags & ALIGN_LEFT) != 0) {
 192                 if (newWidth > oldWidth) {
 193                     /* If wrapping width is increasing and there is no
 194                      * wrapped lines then the text remains valid.
 195                      */
 196                     if ((flags & FLAGS_WRAPPED) == 0) {
 197                         needsLayout = false;
 198                     }
 199                 } else {
 200                     /* If wrapping width is decreasing but it is still
 201                      * greater than the max line width then the text
 202                      * remains valid.
 203                      */
 204                     if (newWidth >= layoutWidth) {
 205                         needsLayout = false;
 206                     }
 207                 }
 208             }
 209         }
 210         if (needsLayout) relayout();
 211         return needsLayout;
 212     }
 213 
 214     public boolean setLineSpacing(float spacing) {
 215         if (this.spacing == spacing) return false;
 216         this.spacing = spacing;
 217         relayout();
 218         return true;
 219     }
 220 
 221     private void ensureLayout() {
 222         if (lines == null) {
 223             layout();
 224         }
 225     }
 226 
 227     public com.sun.javafx.scene.text.TextLine[] getLines() {
 228         ensureLayout();
 229         return lines;
 230     }
 231 
 232     public GlyphList[] getRuns() {
 233         ensureLayout();
 234         GlyphList[] result = new GlyphList[runCount];
 235         int count = 0;
 236         for (int i = 0; i < lines.length; i++) {
 237             GlyphList[] lineRuns = lines[i].getRuns();
 238             int length = lineRuns.length;
 239             System.arraycopy(lineRuns, 0, result, count, length);
 240             count += length;
 241         }
 242         return result;
 243     }
 244 
 245     public BaseBounds getBounds() {
 246         ensureLayout();
 247         return logicalBounds;
 248     }
 249 
 250     public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) {
 251         ensureLayout();
 252         float left = Float.POSITIVE_INFINITY;
 253         float top = Float.POSITIVE_INFINITY;
 254         float right = Float.NEGATIVE_INFINITY;
 255         float bottom = Float.NEGATIVE_INFINITY;
 256         if (filter != null) {
 257             for (int i = 0; i < lines.length; i++) {
 258                 TextLine line = lines[i];
 259                 TextRun[] lineRuns = line.getRuns();
 260                 for (int j = 0; j < lineRuns.length; j++) {
 261                     TextRun run = lineRuns[j];
 262                     TextSpan span = run.getTextSpan();
 263                     if (span != filter) continue;
 264                     Point2D location = run.getLocation();
 265                     float runLeft = location.x;
 266                     if (run.isLeftBearing()) {
 267                         runLeft += line.getLeftSideBearing();
 268                     }
 269                     float runRight = location.x + run.getWidth();
 270                     if (run.isRightBearing()) {
 271                         runRight += line.getRightSideBearing();
 272                     }
 273                     float runTop = location.y;
 274                     float runBottom = location.y + line.getBounds().getHeight() + spacing;
 275                     if (runLeft < left) left = runLeft;
 276                     if (runTop < top) top = runTop;
 277                     if (runRight > right) right = runRight;
 278                     if (runBottom > bottom) bottom = runBottom;
 279                 }
 280             }
 281         } else {
 282             top = bottom = 0;
 283             for (int i = 0; i < lines.length; i++) {
 284                 TextLine line = lines[i];
 285                 RectBounds lineBounds = line.getBounds();
 286                 float lineLeft = lineBounds.getMinX() + line.getLeftSideBearing();
 287                 if (lineLeft < left) left = lineLeft;
 288                 float lineRight = lineBounds.getMaxX() + line.getRightSideBearing();
 289                 if (lineRight > right) right = lineRight;
 290                 bottom += lineBounds.getHeight();
 291             }
 292             if (isMirrored()) {
 293                 float width = getMirroringWidth();
 294                 float bearing = left;
 295                 left = width - right;
 296                 right = width - bearing;
 297             }
 298         }
 299         return bounds.deriveWithNewBounds(left, top, 0, right, bottom, 0);
 300     }
 301 
 302     public PathElement[] getCaretShape(int offset, boolean isLeading,
 303                                        float x, float y) {
 304         ensureLayout();
 305         int lineIndex = 0;
 306         int lineCount = getLineCount();
 307         while (lineIndex < lineCount - 1) {
 308             TextLine line = lines[lineIndex];
 309             int lineEnd = line.getStart() + line.getLength();
 310             if (lineEnd > offset) break;
 311             lineIndex++;
 312         }
 313         int sliptCaretOffset = -1;
 314         int level = 0;
 315         float lineX = 0, lineY = 0, lineHeight = 0;
 316         TextLine line = lines[lineIndex];
 317         TextRun[] runs = line.getRuns();
 318         int runCount = runs.length;
 319         int runIndex = -1;
 320         for (int i = 0; i < runCount; i++) {
 321             TextRun run = runs[i];
 322             int runStart = run.getStart();
 323             int runEnd = run.getEnd();
 324             if (runStart <= offset && offset < runEnd) {
 325                 if (!run.isLinebreak()) {
 326                     runIndex = i;
 327                 }
 328                 break;
 329             }
 330         }
 331         if (runIndex != -1) {
 332             TextRun run = runs[runIndex];
 333             int runStart = run.getStart();
 334             Point2D location = run.getLocation();
 335             lineX = location.x + run.getXAtOffset(offset - runStart, isLeading);
 336             lineY = location.y;
 337             lineHeight = line.getBounds().getHeight();
 338 
 339             if (isLeading) {
 340                 if (runIndex > 0 && offset == runStart) {
 341                     level = run.getLevel();
 342                     sliptCaretOffset = offset - 1;
 343                 }
 344             } else {
 345                 int runEnd = run.getEnd();
 346                 if (runIndex + 1 < runs.length && offset + 1 == runEnd) {
 347                     level = run.getLevel();
 348                     sliptCaretOffset = offset + 1;
 349                 }
 350             }
 351         } else {
 352             /* end of line (line break or offset>=charCount) */
 353             int maxOffset = 0;
 354 
 355             /* set run index to zero to handle empty line case (only break line) */
 356             runIndex = 0;
 357             for (int i = 0; i < runCount; i++) {
 358                 TextRun run = runs[i];
 359                 /*use the trailing edge of the last logical run*/
 360                 if (run.getStart() >= maxOffset && !run.isLinebreak()) {
 361                     maxOffset = run.getStart();
 362                     runIndex = i;
 363                 }
 364             }
 365             TextRun run = runs[runIndex];
 366             Point2D location = run.getLocation();
 367             lineX = location.x + (run.isLeftToRight() ? run.getWidth() : 0);
 368             lineY = location.y;
 369             lineHeight = line.getBounds().getHeight();
 370         }
 371         if (isMirrored()) {
 372             lineX = getMirroringWidth() - lineX;
 373         }
 374         lineX += x;
 375         lineY += y;
 376         if (sliptCaretOffset != -1) {
 377             for (int i = 0; i < runs.length; i++) {
 378                 TextRun run = runs[i];
 379                 int runStart = run.getStart();
 380                 int runEnd = run.getEnd();
 381                 if (runStart <= sliptCaretOffset && sliptCaretOffset < runEnd) {
 382                     if ((run.getLevel() & 1) != (level & 1)) {
 383                         Point2D location = run.getLocation();
 384                         float lineX2 = location.x;
 385                         if (isLeading) {
 386                             if ((level & 1) != 0) lineX2 += run.getWidth();
 387                         } else {
 388                             if ((level & 1) == 0) lineX2 += run.getWidth();
 389                         }
 390                         if (isMirrored()) {
 391                             lineX2 = getMirroringWidth() - lineX2;
 392                         }
 393                         lineX2 += x;
 394                         PathElement[] result = new PathElement[4];
 395                         result[0] = new MoveTo(lineX, lineY);
 396                         result[1] = new LineTo(lineX, lineY + lineHeight / 2);
 397                         result[2] = new MoveTo(lineX2, lineY + lineHeight / 2);
 398                         result[3] = new LineTo(lineX2, lineY + lineHeight);
 399                         return result;
 400                     }
 401                 }
 402             }
 403         }
 404         PathElement[] result = new PathElement[2];
 405         result[0] = new MoveTo(lineX, lineY);
 406         result[1] = new LineTo(lineX, lineY + lineHeight);
 407         return result;
 408     }
 409 
 410     public Hit getHitInfo(float x, float y) {
 411         int charIndex = -1;
 412         boolean leading = false;
 413 
 414         ensureLayout();
 415         int lineIndex = getLineIndex(y);
 416         if (lineIndex >= getLineCount()) {
 417             charIndex = getCharCount();
 418         } else {
 419             if (isMirrored()) {
 420                 x = getMirroringWidth() - x;
 421             }
 422             TextLine line = lines[lineIndex];
 423             TextRun[] runs = line.getRuns();
 424             RectBounds bounds = line.getBounds();
 425             TextRun run = null;
 426             x -= bounds.getMinX();
 427             //TODO binary search
 428             for (int i = 0; i < runs.length; i++) {
 429                 run = runs[i];
 430                 if (x < run.getWidth()) break;
 431                 if (i + 1 < runs.length) {
 432                     if (runs[i + 1].isLinebreak()) break;
 433                     x -= run.getWidth();
 434                 }
 435             }
 436             if (run != null) {
 437                 int[] trailing = new int[1];
 438                 charIndex = run.getStart() + run.getOffsetAtX(x, trailing);
 439                 leading = (trailing[0] == 0);
 440             } else {
 441                 //empty line, set to line break leading
 442                 charIndex = line.getStart();
 443                 leading = true;
 444             }
 445         }
 446         return new Hit(charIndex, -1, leading);
 447     }
 448 
 449     public PathElement[] getRange(int start, int end, int type,
 450                                   float x, float y) {
 451         ensureLayout();
 452         int lineCount = getLineCount();
 453         ArrayList<PathElement> result = new ArrayList<PathElement>();
 454         float lineY = 0;
 455 
 456         for  (int lineIndex = 0; lineIndex < lineCount; lineIndex++) {
 457             TextLine line = lines[lineIndex];
 458             RectBounds lineBounds = line.getBounds();
 459             int lineStart = line.getStart();
 460             if (lineStart >= end) break;
 461             int lineEnd = lineStart + line.getLength();
 462             if (start > lineEnd) {
 463                 lineY += lineBounds.getHeight() + spacing;
 464                 continue;
 465             }
 466 
 467             /* The list of runs in the line is visually ordered.
 468              * Thus, finding the run that includes the selection end offset
 469              * does not mean that all selected runs have being visited.
 470              * Instead, this implementation first computes the number of selected
 471              * characters in the current line, then iterates over the runs consuming
 472              * selected characters till all of them are found.
 473              */
 474             TextRun[] runs = line.getRuns();
 475             int count = Math.min(lineEnd, end) - Math.max(lineStart, start);
 476             int runIndex = 0;
 477             float left = -1;
 478             float right = -1;
 479             float lineX = lineBounds.getMinX();
 480             while (count > 0 && runIndex < runs.length) {
 481                 TextRun run = runs[runIndex];
 482                 int runStart = run.getStart();
 483                 int runEnd = run.getEnd();
 484                 float runWidth = run.getWidth();
 485                 int clmapStart = Math.max(runStart, Math.min(start, runEnd));
 486                 int clampEnd = Math.max(runStart, Math.min(end, runEnd));
 487                 int runCount = clampEnd - clmapStart;
 488                 if (runCount != 0) {
 489                     boolean ltr = run.isLeftToRight();
 490                     float runLeft;
 491                     if (runStart > start) {
 492                         runLeft = ltr ? lineX : lineX + runWidth;
 493                     } else {
 494                         runLeft = lineX + run.getXAtOffset(start - runStart, true);
 495                     }
 496                     float runRight;
 497                     if (runEnd < end) {
 498                         runRight = ltr ? lineX + runWidth : lineX;
 499                     } else {
 500                         runRight = lineX + run.getXAtOffset(end - runStart, true);
 501                     }
 502                     if (runLeft > runRight) {
 503                         float tmp = runLeft;
 504                         runLeft = runRight;
 505                         runRight = tmp;
 506                     }
 507                     count -= runCount;
 508                     float top = 0, bottom = 0;
 509                     switch (type) {
 510                     case TYPE_TEXT:
 511                         top = lineY;
 512                         bottom = lineY + lineBounds.getHeight();
 513                         break;
 514                     case TYPE_UNDERLINE:
 515                     case TYPE_STRIKETHROUGH:
 516                         FontStrike fontStrike = null;
 517                         if (spans != null) {
 518                             TextSpan span = run.getTextSpan();
 519                             PGFont font = (PGFont)span.getFont();
 520                             if (font == null) break;
 521                             fontStrike = font.getStrike(IDENTITY);
 522                         } else {
 523                             fontStrike = strike;
 524                         }
 525                         top = lineY - run.getAscent();
 526                         Metrics metrics = fontStrike.getMetrics();
 527                         if (type == TYPE_UNDERLINE) {
 528                             top += metrics.getUnderLineOffset();
 529                             bottom = top + metrics.getUnderLineThickness();
 530                         } else {
 531                             top += metrics.getStrikethroughOffset();
 532                             bottom = top + metrics.getStrikethroughThickness();
 533                         }
 534                         break;
 535                     }
 536 
 537                     /* Merge continuous rectangles */
 538                     if (runLeft != right) {
 539                         if (left != -1 && right != -1) {
 540                             float l = left, r = right;
 541                             if (isMirrored()) {
 542                                 float width = getMirroringWidth();
 543                                 l = width - l;
 544                                 r = width - r;
 545                             }
 546                             result.add(new MoveTo(x + l,  y + top));
 547                             result.add(new LineTo(x + r, y + top));
 548                             result.add(new LineTo(x + r, y + bottom));
 549                             result.add(new LineTo(x + l,  y + bottom));
 550                             result.add(new LineTo(x + l,  y + top));
 551                         }
 552                         left = runLeft;
 553                         right = runRight;
 554                     }
 555                     right = runRight;
 556                     if (count == 0) {
 557                         float l = left, r = right;
 558                         if (isMirrored()) {
 559                             float width = getMirroringWidth();
 560                             l = width - l;
 561                             r = width - r;
 562                         }
 563                         result.add(new MoveTo(x + l,  y + top));
 564                         result.add(new LineTo(x + r, y + top));
 565                         result.add(new LineTo(x + r, y + bottom));
 566                         result.add(new LineTo(x + l,  y + bottom));
 567                         result.add(new LineTo(x + l,  y + top));
 568                     }
 569                 }
 570                 lineX += runWidth;
 571                 runIndex++;
 572             }
 573             lineY += lineBounds.getHeight() + spacing;
 574         }
 575         return result.toArray(new PathElement[result.size()]);
 576     }
 577 
 578     public Shape getShape(int type, TextSpan filter) {
 579         ensureLayout();
 580         boolean text = (type & TYPE_TEXT) != 0;
 581         boolean underline = (type & TYPE_UNDERLINE) != 0;
 582         boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0;
 583         boolean baselineType = (type & TYPE_BASELINE) != 0;
 584         if (shape != null && text && !underline && !strikethrough && baselineType) {
 585             return shape;
 586         }
 587 
 588         Path2D outline = new Path2D();
 589         BaseTransform tx = new Translate2D(0, 0);
 590         /* Return a shape relative to the baseline of the first line so
 591          * it can be used for layout */
 592         float firstBaseline = 0;
 593         if (baselineType) {
 594             firstBaseline = -lines[0].getBounds().getMinY();
 595         }
 596         for (int i = 0; i < lines.length; i++) {
 597             TextLine line = lines[i];
 598             TextRun[] runs = line.getRuns();
 599             RectBounds bounds = line.getBounds();
 600             float baseline = -bounds.getMinY();
 601             for (int j = 0; j < runs.length; j++) {
 602                 TextRun run = runs[j];
 603                 FontStrike fontStrike = null;
 604                 if (spans != null) {
 605                     TextSpan span = run.getTextSpan();
 606                     if (filter != null && span != filter) continue;
 607                     PGFont font = (PGFont)span.getFont();
 608 
 609                     /* skip embedded runs */
 610                     if (font == null) continue;
 611                     fontStrike = font.getStrike(IDENTITY);
 612                 } else {
 613                     fontStrike = strike;
 614                 }
 615                 Point2D location = run.getLocation();
 616                 float runX = location.x;
 617                 float runY = location.y + baseline - firstBaseline;
 618                 Metrics metrics = null;
 619                 if (underline || strikethrough) {
 620                     metrics = fontStrike.getMetrics();
 621                 }
 622                 if (underline) {
 623                     RoundRectangle2D rect = new RoundRectangle2D();
 624                     rect.x = runX;
 625                     rect.y = runY + metrics.getUnderLineOffset();
 626                     rect.width = run.getWidth();
 627                     rect.height = metrics.getUnderLineThickness();
 628                     outline.append(rect, false);
 629                 }
 630                 if (strikethrough) {
 631                     RoundRectangle2D rect = new RoundRectangle2D();
 632                     rect.x = runX;
 633                     rect.y = runY + metrics.getStrikethroughOffset();
 634                     rect.width = run.getWidth();
 635                     rect.height = metrics.getStrikethroughThickness();
 636                     outline.append(rect, false);
 637                 }
 638                 if (text && run.getGlyphCount() > 0) {
 639                     tx.restoreTransform(1, 0, 0, 1, runX, runY);
 640                     Path2D path = (Path2D)fontStrike.getOutline(run, tx);
 641                     outline.append(path, false);
 642                 }
 643             }
 644         }
 645 
 646         if (text && !underline && !strikethrough) {
 647             shape = outline;
 648         }
 649         return outline;
 650     }
 651 
 652     @Override
 653     public boolean setTabSize(int spaces) {
 654         if (spaces < 1)
 655             spaces = 1;
 656         if (tabSize != spaces) {
 657             tabSize = spaces;
 658             relayout();
 659             return true;
 660         }
 661         return false;
 662     }
 663 
 664     /***************************************************************************
 665      *                                                                         *
 666      *                     Text Layout Implementation                          *
 667      *                                                                         *
 668      **************************************************************************/
 669 
 670     private int getLineIndex(float y) {
 671         int index = 0;
 672         float bottom = 0;
 673         int lineCount = getLineCount();
 674         while (index < lineCount) {
 675             bottom += lines[index].getBounds().getHeight() + spacing;
 676             if (index + 1 == lineCount) bottom -= lines[index].getLeading();
 677             if (bottom > y) break;
 678             index++;
 679         }
 680         return index;
 681     }
 682 
 683     private boolean copyCache() {
 684         int align = flags & ALIGN_MASK;
 685         int boundsType = flags & BOUNDS_MASK;
 686         /* Caching for boundsType == Center, bias towards  Modena */
 687         return wrapWidth != 0 || align != ALIGN_LEFT || boundsType == 0 || isMirrored();
 688     }
 689 
 690     private void initCache() {
 691         if (cacheKey != null) {
 692             if (layoutCache == null) {
 693                 LayoutCache cache = stringCache.get(cacheKey);
 694                 if (cache != null && cache.font.equals(font) && Arrays.equals(cache.text, text)) {
 695                     layoutCache = cache;
 696                     runs = cache.runs;
 697                     runCount = cache.runCount;
 698                     flags |= cache.analysis;
 699                 }
 700             }
 701             if (layoutCache != null) {
 702                 if (copyCache()) {
 703                     /* This instance has some property that requires it to
 704                      * build its own lines (i.e. wrapping width). Thus, only use
 705                      * the runs from the cache (and it needs to make a copy
 706                      * before using it as they will be modified).
 707                      * Note: the copy of the elements in the array happens in
 708                      * reuseRuns().
 709                      */
 710                     if (layoutCache.runs == runs) {
 711                         runs = new TextRun[runCount];
 712                         System.arraycopy(layoutCache.runs, 0, runs, 0, runCount);
 713                     }
 714                 } else {
 715                     if (layoutCache.lines != null) {
 716                         runs = layoutCache.runs;
 717                         runCount = layoutCache.runCount;
 718                         flags |= layoutCache.analysis;
 719                         lines = layoutCache.lines;
 720                         layoutWidth = layoutCache.layoutWidth;
 721                         layoutHeight = layoutCache.layoutHeight;
 722                         float ascent = lines[0].getBounds().getMinY();
 723                         logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0,
 724                                 layoutWidth, layoutHeight + ascent, 0);
 725                     }
 726                 }
 727             }
 728         }
 729     }
 730 
 731     private int getLineCount() {
 732         return lines.length;
 733     }
 734 
 735     private int getCharCount() {
 736         if (text != null) return text.length;
 737         int count = 0;
 738         for (int i = 0; i < lines.length; i++) {
 739             count += lines[i].getLength();
 740         }
 741         return count;
 742     }
 743 
 744     public TextSpan[] getTextSpans() {
 745         return spans;
 746     }
 747 
 748     public PGFont getFont() {
 749         return font;
 750     }
 751 
 752     public int getDirection() {
 753         if ((flags & DIRECTION_LTR) != 0) {
 754             return Bidi.DIRECTION_LEFT_TO_RIGHT;
 755         }
 756         if ((flags & DIRECTION_RTL) != 0) {
 757             return Bidi.DIRECTION_RIGHT_TO_LEFT;
 758         }
 759         if ((flags & DIRECTION_DEFAULT_LTR) != 0) {
 760             return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
 761         }
 762         if ((flags & DIRECTION_DEFAULT_RTL) != 0) {
 763             return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT;
 764         }
 765         return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
 766     }
 767 
 768     public void addTextRun(TextRun run) {
 769         if (runCount + 1 > runs.length) {
 770             TextRun[] newRuns = new TextRun[runs.length + 64];
 771             System.arraycopy(runs, 0, newRuns, 0, runs.length);
 772             runs = newRuns;
 773         }
 774         runs[runCount++] = run;
 775     }
 776 
 777     private void buildRuns(char[] chars) {
 778         runCount = 0;
 779         if (runs == null) {
 780             int count = Math.max(4, Math.min(chars.length / 16, 16));
 781             runs = new TextRun[count];
 782         }
 783         GlyphLayout layout = GlyphLayout.getInstance();
 784         flags = layout.breakRuns(this, chars, flags);
 785         layout.dispose();
 786         for (int j = runCount; j < runs.length; j++) {
 787             runs[j] = null;
 788         }
 789     }
 790 
 791     private void shape(TextRun run, char[] chars, GlyphLayout layout) {
 792         FontStrike strike;
 793         PGFont font;
 794         if (spans != null) {
 795             if (spans.length == 0) return;
 796             TextSpan span = run.getTextSpan();
 797             font = (PGFont)span.getFont();
 798             if (font == null) {
 799                 RectBounds bounds = span.getBounds();
 800                 run.setEmbedded(bounds, span.getText().length());
 801                 return;
 802             }
 803             strike = font.getStrike(IDENTITY);
 804         } else {
 805             font = this.font;
 806             strike = this.strike;
 807         }
 808 
 809         /* init metrics for line breaks for empty lines */
 810         if (run.getAscent() == 0) {
 811             Metrics m = strike.getMetrics();
 812 
 813             /* The implementation of the center layoutBounds mode is to assure the
 814              * layout has the same number of pixels above and bellow the cap
 815              * height.
 816              */
 817             if ((flags & BOUNDS_MASK) == BOUNDS_CENTER) {
 818                 float ascent = m.getAscent();
 819                 /* Segoe UI has a very large internal leading area, applying the
 820                  * center layoutBounds heuristics on it would result in several pixels
 821                  * being added to the descent. The final results would be
 822                  * overly large and visually unappealing. The fix is to reduce
 823                  * the ascent before applying the algorithm. */
 824                 if (font.getFamilyName().equals("Segoe UI")) {
 825                     ascent *= 0.80;
 826                 }
 827                 ascent = (int)(ascent-0.75);
 828                 float descent = (int)(m.getDescent()+0.75);
 829                 float leading = (int)(m.getLineGap()+0.75);
 830                 float capHeight = (int)(m.getCapHeight()+0.75);
 831                 float topPadding = -ascent - capHeight;
 832                 if (topPadding > descent) {
 833                     descent = topPadding;
 834                 } else {
 835                     ascent += (topPadding - descent);
 836                 }
 837                 run.setMetrics(ascent, descent, leading);
 838             } else {
 839                 run.setMetrics(m.getAscent(), m.getDescent(), m.getLineGap());
 840             }
 841         }
 842 
 843         if (run.isTab()) return;
 844         if (run.isLinebreak()) return;
 845         if (run.getGlyphCount() > 0) return;
 846         if (run.isComplex()) {
 847             /* Use GlyphLayout to shape complex text */
 848             layout.layout(run, font, strike, chars);
 849         } else {
 850             FontResource fr = strike.getFontResource();
 851             int start = run.getStart();
 852             int length = run.getLength();
 853 
 854             /* No glyph layout required */
 855             if (layoutCache == null) {
 856                 float fontSize = strike.getSize();
 857                 CharToGlyphMapper mapper  = fr.getGlyphMapper();
 858 
 859                 /* The text contains complex and non-complex runs */
 860                 int[] glyphs = new int[length];
 861                 mapper.charsToGlyphs(start, length, chars, glyphs);
 862                 float[] positions = new float[(length + 1) << 1];
 863                 float xadvance = 0;
 864                 for (int i = 0; i < length; i++) {
 865                     float width = fr.getAdvance(glyphs[i], fontSize);
 866                     positions[i<<1] = xadvance;
 867                     //yadvance always zero
 868                     xadvance += width;
 869                 }
 870                 positions[length<<1] = xadvance;
 871                 run.shape(length, glyphs, positions, null);
 872             } else {
 873 
 874                 /* The text only contains non-complex runs, all the glyphs and
 875                  * advances are stored in the shapeCache */
 876                 if (!layoutCache.valid) {
 877                     float fontSize = strike.getSize();
 878                     CharToGlyphMapper mapper  = fr.getGlyphMapper();
 879                     mapper.charsToGlyphs(start, length, chars, layoutCache.glyphs, start);
 880                     int end = start + length;
 881                     float width = 0;
 882                     for (int i = start; i < end; i++) {
 883                         float adv = fr.getAdvance(layoutCache.glyphs[i], fontSize);
 884                         layoutCache.advances[i] = adv;
 885                         width += adv;
 886                     }
 887                     run.setWidth(width);
 888                 }
 889                 run.shape(length, layoutCache.glyphs, layoutCache.advances);
 890             }
 891         }
 892     }
 893 
 894     private TextLine createLine(int start, int end, int startOffset) {
 895         int count = end - start + 1;
 896         TextRun[] lineRuns = new TextRun[count];
 897         if (start < runCount) {
 898             System.arraycopy(runs, start, lineRuns, 0, count);
 899         }
 900 
 901         /* Recompute line width, height, and length (wrapping) */
 902         float width = 0, ascent = 0, descent = 0, leading = 0;
 903         int length = 0;
 904         for (int i = 0; i < lineRuns.length; i++) {
 905             TextRun run = lineRuns[i];
 906             width += run.getWidth();
 907             ascent = Math.min(ascent, run.getAscent());
 908             descent = Math.max(descent, run.getDescent());
 909             leading = Math.max(leading, run.getLeading());
 910             length += run.getLength();
 911         }
 912         if (width > layoutWidth) layoutWidth = width;
 913         return new TextLine(startOffset, length, lineRuns,
 914                             width, ascent, descent, leading);
 915     }
 916 
 917     private void reorderLine(TextLine line) {
 918         TextRun[] runs = line.getRuns();
 919         int length = runs.length;
 920         if (length > 0 && runs[length - 1].isLinebreak()) {
 921             length--;
 922         }
 923         if (length < 2) return;
 924         byte[] levels = new byte[length];
 925         for (int i = 0; i < length; i++) {
 926             levels[i] = runs[i].getLevel();
 927         }
 928         Bidi.reorderVisually(levels, 0, runs, 0, length);
 929     }
 930 
 931     private char[] getText() {
 932         if (text == null) {
 933             int count = 0;
 934             for (int i = 0; i < spans.length; i++) {
 935                 count += spans[i].getText().length();
 936             }
 937             text = new char[count];
 938             int offset = 0;
 939             for (int i = 0; i < spans.length; i++) {
 940                 String string = spans[i].getText();
 941                 int length = string.length();
 942                 string.getChars(0, length, text, offset);
 943                 offset += length;
 944             }
 945         }
 946         return text;
 947     }
 948 
 949     private boolean isSimpleLayout() {
 950         int textAlignment = flags & ALIGN_MASK;
 951         boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY;
 952         int mask = FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX;
 953         return (flags & mask) == 0 && !justify;
 954     }
 955 
 956     private boolean isMirrored() {
 957         boolean mirrored = false;
 958         switch (flags & DIRECTION_MASK) {
 959         case DIRECTION_RTL: mirrored = true; break;
 960         case DIRECTION_LTR: mirrored = false; break;
 961         case DIRECTION_DEFAULT_LTR:
 962         case DIRECTION_DEFAULT_RTL:
 963             mirrored = (flags & FLAGS_RTL_BASE) != 0;
 964         }
 965         return mirrored;
 966     }
 967 
 968     private float getMirroringWidth() {
 969         /* The text node in the scene layer is mirrored based on
 970          * result of computeLayoutBounds. The coordinate translation
 971          * in text layout has to be based on the same width.
 972          */
 973         return wrapWidth != 0 ? wrapWidth : layoutWidth;
 974     }
 975 
 976     private void reuseRuns() {
 977         /* The runs list is always accessed by the same thread (as TextLayout
 978          * is not thread safe) thus it can be modified at any time, but the
 979          * elements inside of the list are shared among threads and cannot be
 980          * modified. Each reused element has to be cloned.*/
 981         runCount = 0;
 982         int index = 0;;
 983         while (index < runs.length) {
 984             TextRun run = runs[index];
 985             if (run == null) break;
 986             runs[index] = null;
 987             index++;
 988             runs[runCount++] = run = run.unwrap();
 989 
 990             if (run.isSplit()) {
 991                 run.merge(null); /* unmark split */
 992                 while (index < runs.length) {
 993                     TextRun nextRun = runs[index];
 994                     if (nextRun == null) break;
 995                     run.merge(nextRun);
 996                     runs[index] = null;
 997                     index++;
 998                     if (nextRun.isSplitLast()) break;
 999                 }
1000             }
1001         }
1002     }
1003 
1004     private float getTabAdvance() {
1005         float spaceAdvance = 0;
1006         if (spans != null) {
1007             /* Rich text case - use the first font (for now) */
1008             for (int i = 0; i < spans.length; i++) {
1009                 TextSpan span = spans[i];
1010                 PGFont font = (PGFont)span.getFont();
1011                 if (font != null) {
1012                     FontStrike strike = font.getStrike(IDENTITY);
1013                     spaceAdvance = strike.getCharAdvance(' ');
1014                     break;
1015                 }
1016             }
1017         } else {
1018             spaceAdvance = strike.getCharAdvance(' ');
1019         }
1020         return tabSize * spaceAdvance;
1021     }
1022 
1023     private void layout() {
1024         /* Try the cache */
1025         initCache();
1026 
1027         /* Whole layout retrieved from the cache */
1028         if (lines != null) return;
1029         char[] chars = getText();
1030 
1031         /* runs and runCount are set in reuseRuns or buildRuns */
1032         if ((flags & FLAGS_ANALYSIS_VALID) != 0 && isSimpleLayout()) {
1033             reuseRuns();
1034         } else {
1035             buildRuns(chars);
1036         }
1037 
1038         GlyphLayout layout = null;
1039         if ((flags & (FLAGS_HAS_COMPLEX)) != 0) {
1040             layout = GlyphLayout.getInstance();
1041         }
1042 
1043         float tabAdvance = 0;
1044         if ((flags & FLAGS_HAS_TABS) != 0) {
1045             tabAdvance = getTabAdvance();
1046         }
1047 
1048         BreakIterator boundary = null;
1049         if (wrapWidth > 0) {
1050             if ((flags & (FLAGS_HAS_COMPLEX | FLAGS_HAS_CJK)) != 0) {
1051                 boundary = BreakIterator.getLineInstance();
1052                 boundary.setText(new CharArrayIterator(chars));
1053             }
1054         }
1055         int textAlignment = flags & ALIGN_MASK;
1056 
1057         /* Optimize simple case: reuse the glyphs and advances as long as the
1058          * text and font are the same.
1059          * The simple case is no bidi, no complex, no justify, no features.
1060          */
1061 
1062         if (isSimpleLayout()) {
1063             if (layoutCache == null) {
1064                 layoutCache = new LayoutCache();
1065                 layoutCache.glyphs = new int[chars.length];
1066                 layoutCache.advances = new float[chars.length];
1067             }
1068         } else {
1069             layoutCache = null;
1070         }
1071 
1072         float lineWidth = 0;
1073         int startIndex = 0;
1074         int startOffset = 0;
1075         ArrayList<TextLine> linesList = new ArrayList<TextLine>();
1076         for (int i = 0; i < runCount; i++) {
1077             TextRun run = runs[i];
1078             shape(run, chars, layout);
1079             if (run.isTab()) {
1080                 float tabStop = ((int)(lineWidth / tabAdvance) +1) * tabAdvance;
1081                 run.setWidth(tabStop - lineWidth);
1082             }
1083 
1084             float runWidth = run.getWidth();
1085             if (wrapWidth > 0 && lineWidth + runWidth > wrapWidth && !run.isLinebreak()) {
1086 
1087                 /* Find offset of the first character that does not fit on the line */
1088                 int hitOffset = run.getStart() + run.getWrapIndex(wrapWidth - lineWidth);
1089 
1090                 /* Only keep whitespaces (not tabs) in the current run to avoid
1091                  * dealing with unshaped runs.
1092                  */
1093                 int offset = hitOffset;
1094                 int runEnd = run.getEnd();
1095                 while (offset + 1 < runEnd && chars[offset] == ' ') {
1096                     offset++;
1097                     /* Preserve behaviour: only keep one white space in the line
1098                      * before wrapping. Needed API to allow change.
1099                      */
1100                     break;
1101                 }
1102 
1103                 /* Find the break opportunity */
1104                 int breakOffset = offset;
1105                 if (boundary != null) {
1106                     /* Use Java BreakIterator when complex script are present */
1107                     breakOffset = boundary.isBoundary(offset) || chars[offset] == '\t' ? offset : boundary.preceding(offset);
1108                 } else {
1109                     /* Simple break strategy for latin text (Performance) */
1110                     boolean currentChar = Character.isWhitespace(chars[breakOffset]);
1111                     while (breakOffset > startOffset) {
1112                         boolean previousChar = Character.isWhitespace(chars[breakOffset - 1]);
1113                         if (!currentChar && previousChar) break;
1114                         currentChar = previousChar;
1115                         breakOffset--;
1116                     }
1117                 }
1118 
1119                 /* Never break before the line start offset */
1120                 if (breakOffset < startOffset) breakOffset = startOffset;
1121 
1122                 /* Find the run that contains the break offset */
1123                 int breakRunIndex = startIndex;
1124                 TextRun breakRun = null;
1125                 while (breakRunIndex < runCount) {
1126                     breakRun = runs[breakRunIndex];
1127                     if (breakRun.getEnd() > breakOffset) break;
1128                     breakRunIndex++;
1129                 }
1130 
1131                 /* No line breaks  between hit offset and line start offset.
1132                  * Try character wrapping mode at the hit offset.
1133                  */
1134                 if (breakOffset == startOffset) {
1135                     breakRun = run;
1136                     breakRunIndex = i;
1137                     breakOffset = hitOffset;
1138                 }
1139 
1140                 int breakOffsetInRun = breakOffset - breakRun.getStart();
1141                 /* Wrap the entire run to the next (only if it is not the first
1142                  * run of the line).
1143                  */
1144                 if (breakOffsetInRun == 0 && breakRunIndex != startIndex) {
1145                     i = breakRunIndex - 1;
1146                 } else {
1147                     i = breakRunIndex;
1148 
1149                     /* The break offset is at the first offset of the first run of the line.
1150                      * This happens when the wrap width is smaller than the width require
1151                      * to show the first character for the line.
1152                      */
1153                     if (breakOffsetInRun == 0) {
1154                         breakOffsetInRun++;
1155                     }
1156                     if (breakOffsetInRun < breakRun.getLength()) {
1157                         if (runCount >= runs.length) {
1158                             TextRun[] newRuns = new TextRun[runs.length + 64];
1159                             System.arraycopy(runs, 0, newRuns, 0, i + 1);
1160                             System.arraycopy(runs, i + 1, newRuns, i + 2, runs.length - i - 1);
1161                             runs = newRuns;
1162                         } else {
1163                             System.arraycopy(runs, i + 1, runs, i + 2, runCount - i - 1);
1164                         }
1165                         runs[i + 1] = breakRun.split(breakOffsetInRun);
1166                         if (breakRun.isComplex()) {
1167                             shape(breakRun, chars, layout);
1168                         }
1169                         runCount++;
1170                     }
1171                 }
1172 
1173                 /* No point marking the last run of a line a softbreak */
1174                 if (i + 1 < runCount && !runs[i + 1].isLinebreak()) {
1175                     run = runs[i];
1176                     run.setSoftbreak();
1177                     flags |= FLAGS_WRAPPED;
1178 
1179                     // Tabs should preserve width
1180 
1181                     /*
1182                      * Due to contextual forms (arabic) it is possible this line
1183                      * is still too big since the splitting of the arabic run
1184                      * changes the shape of boundary glyphs. For now the
1185                      * implementation has opted to have the appropriate
1186                      * initial/final shapes and allow those glyphs to
1187                      * potentially overlap the wrapping width, rather than use
1188                      * the medial form within the wrappingWidth. A better place
1189                      * to solve this would be TextRun#getWrapIndex - but its TBD
1190                      * there too.
1191                      */
1192                 }
1193             }
1194 
1195             lineWidth += runWidth;
1196             if (run.isBreak()) {
1197                 TextLine line = createLine(startIndex, i, startOffset);
1198                 linesList.add(line);
1199                 startIndex = i + 1;
1200                 startOffset += line.getLength();
1201                 lineWidth = 0;
1202             }
1203         }
1204         if (layout != null) layout.dispose();
1205 
1206         linesList.add(createLine(startIndex, runCount - 1, startOffset));
1207         lines = new TextLine[linesList.size()];
1208         linesList.toArray(lines);
1209 
1210         float fullWidth = Math.max(wrapWidth, layoutWidth);
1211         float lineY = 0;
1212         float align;
1213         if (isMirrored()) {
1214             align = 1; /* Left and Justify */
1215             if (textAlignment == ALIGN_RIGHT) align = 0;
1216         } else {
1217             align = 0; /* Left and Justify */
1218             if (textAlignment == ALIGN_RIGHT) align = 1;
1219         }
1220         if (textAlignment == ALIGN_CENTER) align = 0.5f;
1221         for (int i = 0; i < lines.length; i++) {
1222             TextLine line = lines[i];
1223             int lineStart = line.getStart();
1224             RectBounds bounds = line.getBounds();
1225 
1226             /* Center and right alignment */
1227             float lineX = (fullWidth - bounds.getWidth()) * align;
1228             line.setAlignment(lineX);
1229 
1230             /* Justify */
1231             boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY;
1232             if (justify) {
1233                 TextRun[] lineRuns = line.getRuns();
1234                 int lineRunCount = lineRuns.length;
1235                 if (lineRunCount > 0 && lineRuns[lineRunCount - 1].isSoftbreak()) {
1236                     /* count white spaces but skipping trailings whitespaces */
1237                     int lineEnd = lineStart + line.getLength();
1238                     int wsCount = 0;
1239                     boolean hitChar = false;
1240                     for (int j = lineEnd - 1; j >= lineStart; j--) {
1241                         if (!hitChar && chars[j] != ' ') hitChar = true;
1242                         if (hitChar && chars[j] == ' ') wsCount++;
1243                     }
1244                     if (wsCount != 0) {
1245                         float inc = (fullWidth - bounds.getWidth()) / wsCount;
1246                         done:
1247                         for (int j = 0; j < lineRunCount; j++) {
1248                             TextRun textRun = lineRuns[j];
1249                             int runStart = textRun.getStart();
1250                             int runEnd = textRun.getEnd();
1251                             for (int k = runStart; k < runEnd; k++) {
1252                                 // TODO kashidas
1253                                 if (chars[k] == ' ') {
1254                                     textRun.justify(k - runStart, inc);
1255                                     if (--wsCount == 0) break done;
1256                                 }
1257                             }
1258                         }
1259                         lineX = 0;
1260                         line.setAlignment(lineX);
1261                         line.setWidth(fullWidth);
1262                     }
1263                 }
1264             }
1265 
1266             if ((flags & FLAGS_HAS_BIDI) != 0) {
1267                 reorderLine(line);
1268             }
1269 
1270             computeSideBearings(line);
1271 
1272             /* Set run location */
1273             float runX = lineX;
1274             TextRun[] lineRuns = line.getRuns();
1275             for (int j = 0; j < lineRuns.length; j++) {
1276                 TextRun run = lineRuns[j];
1277                 run.setLocation(runX, lineY);
1278                 run.setLine(line);
1279                 runX += run.getWidth();
1280             }
1281             if (i + 1 < lines.length) {
1282                 lineY = Math.max(lineY, lineY + bounds.getHeight() + spacing);
1283             } else {
1284                 lineY += (bounds.getHeight() - line.getLeading());
1285             }
1286         }
1287         float ascent = lines[0].getBounds().getMinY();
1288         layoutHeight = lineY;
1289         logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, layoutWidth,
1290                                             layoutHeight + ascent, 0);
1291 
1292 
1293         if (layoutCache != null) {
1294             if (cacheKey != null && !layoutCache.valid && !copyCache()) {
1295                 /* After layoutCache is added to the stringCache it can be
1296                  * accessed by multiple threads. All the data in it must
1297                  * be immutable. See copyCache() for the cases where the entire
1298                  * layout is immutable.
1299                  */
1300                 layoutCache.font = font;
1301                 layoutCache.text = text;
1302                 layoutCache.runs = runs;
1303                 layoutCache.runCount = runCount;
1304                 layoutCache.lines = lines;
1305                 layoutCache.layoutWidth = layoutWidth;
1306                 layoutCache.layoutHeight = layoutHeight;
1307                 layoutCache.analysis = flags & ANALYSIS_MASK;
1308                 synchronized (CACHE_SIZE_LOCK) {
1309                     int charCount = chars.length;
1310                     if (cacheSize + charCount > MAX_CACHE_SIZE) {
1311                         stringCache.clear();
1312                         cacheSize = 0;
1313                     }
1314                     stringCache.put(cacheKey, layoutCache);
1315                     cacheSize += charCount;
1316                 }
1317             }
1318             layoutCache.valid = true;
1319         }
1320     }
1321 
1322     @Override
1323     public BaseBounds getVisualBounds(int type) {
1324         ensureLayout();
1325 
1326         /* Not defined for rich text */
1327         if (strike == null) {
1328             return null;
1329         }
1330 
1331         boolean underline = (type & TYPE_UNDERLINE) != 0;
1332         boolean hasUnderline = (flags & FLAGS_CACHED_UNDERLINE) != 0;
1333         boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0;
1334         boolean hasStrikethrough = (flags & FLAGS_CACHED_STRIKETHROUGH) != 0;
1335         if (visualBounds != null && underline == hasUnderline
1336                 && strikethrough == hasStrikethrough) {
1337             /* Return last cached value */
1338             return visualBounds;
1339         }
1340 
1341         flags &= ~(FLAGS_CACHED_STRIKETHROUGH | FLAGS_CACHED_UNDERLINE);
1342         if (underline) flags |= FLAGS_CACHED_UNDERLINE;
1343         if (strikethrough) flags |= FLAGS_CACHED_STRIKETHROUGH;
1344         visualBounds = new RectBounds();
1345 
1346         float xMin = Float.POSITIVE_INFINITY;
1347         float yMin = Float.POSITIVE_INFINITY;
1348         float xMax = Float.NEGATIVE_INFINITY;
1349         float yMax = Float.NEGATIVE_INFINITY;
1350         float bounds[] = new float[4];
1351         FontResource fr = strike.getFontResource();
1352         Metrics metrics = strike.getMetrics();
1353         float size = strike.getSize();
1354         for (int i = 0; i < lines.length; i++) {
1355             TextLine line = lines[i];
1356             TextRun[] runs = line.getRuns();
1357             for (int j = 0; j < runs.length; j++) {
1358                 TextRun run = runs[j];
1359                 Point2D pt = run.getLocation();
1360                 if (run.isLinebreak()) continue;
1361                 int glyphCount = run.getGlyphCount();
1362                 for (int gi = 0; gi < glyphCount; gi++) {
1363                     int gc = run.getGlyphCode(gi);
1364                     if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
1365                         fr.getGlyphBoundingBox(run.getGlyphCode(gi), size, bounds);
1366                         if (bounds[X_MIN_INDEX] != bounds[X_MAX_INDEX]) {
1367                             float glyphX = pt.x + run.getPosX(gi);
1368                             float glyphY = pt.y + run.getPosY(gi);
1369                             float glyphMinX = glyphX + bounds[X_MIN_INDEX];
1370                             float glyphMinY = glyphY - bounds[Y_MAX_INDEX];
1371                             float glyphMaxX = glyphX + bounds[X_MAX_INDEX];
1372                             float glyphMaxY = glyphY - bounds[Y_MIN_INDEX];
1373                             if (glyphMinX < xMin) xMin = glyphMinX;
1374                             if (glyphMinY < yMin) yMin = glyphMinY;
1375                             if (glyphMaxX > xMax) xMax = glyphMaxX;
1376                             if (glyphMaxY > yMax) yMax = glyphMaxY;
1377                         }
1378                     }
1379                 }
1380                 if (underline) {
1381                     float underlineMinX = pt.x;
1382                     float underlineMinY = pt.y + metrics.getUnderLineOffset();
1383                     float underlineMaxX = underlineMinX + run.getWidth();
1384                     float underlineMaxY = underlineMinY + metrics.getUnderLineThickness();
1385                     if (underlineMinX < xMin) xMin = underlineMinX;
1386                     if (underlineMinY < yMin) yMin = underlineMinY;
1387                     if (underlineMaxX > xMax) xMax = underlineMaxX;
1388                     if (underlineMaxY > yMax) yMax = underlineMaxY;
1389                 }
1390                 if (strikethrough) {
1391                     float strikethroughMinX = pt.x;
1392                     float strikethroughMinY = pt.y + metrics.getStrikethroughOffset();
1393                     float strikethroughMaxX = strikethroughMinX + run.getWidth();
1394                     float strikethroughMaxY = strikethroughMinY + metrics.getStrikethroughThickness();
1395                     if (strikethroughMinX < xMin) xMin = strikethroughMinX;
1396                     if (strikethroughMinY < yMin) yMin = strikethroughMinY;
1397                     if (strikethroughMaxX > xMax) xMax = strikethroughMaxX;
1398                     if (strikethroughMaxY > yMax) yMax = strikethroughMaxY;
1399                 }
1400             }
1401         }
1402 
1403         if (xMin < xMax && yMin < yMax) {
1404             visualBounds.setBounds(xMin, yMin, xMax, yMax);
1405         }
1406         return visualBounds;
1407     }
1408 
1409     private void computeSideBearings(TextLine line) {
1410         TextRun[] runs = line.getRuns();
1411         if (runs.length == 0) return;
1412         float bounds[] = new float[4];
1413         FontResource defaultFontResource = null;
1414         float size = 0;
1415         if (strike != null) {
1416             defaultFontResource = strike.getFontResource();
1417             size = strike.getSize();
1418         }
1419 
1420         /* The line lsb is the lsb of the first visual character in the line */
1421         float lsb = 0;
1422         float width = 0;
1423         lsbdone:
1424         for (int i = 0; i < runs.length; i++) {
1425             TextRun run = runs[i];
1426             int glyphCount = run.getGlyphCount();
1427             for (int gi = 0; gi < glyphCount; gi++) {
1428                 float advance = run.getAdvance(gi);
1429                 /* Skip any leading zero-width glyphs in the line */
1430                 if (advance != 0) {
1431                     int gc = run.getGlyphCode(gi);
1432                     /* Skip any leading invisible glyphs in the line */
1433                     if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
1434                         FontResource fr = defaultFontResource;
1435                         if (fr == null) {
1436                             TextSpan span = run.getTextSpan();
1437                             PGFont font = (PGFont)span.getFont();
1438                             /* No need to check font != null (run.glyphCount > 0)  */
1439                             size = font.getSize();
1440                             fr = font.getFontResource();
1441                         }
1442                         fr.getGlyphBoundingBox(gc, size, bounds);
1443                         float glyphLsb = bounds[X_MIN_INDEX];
1444                         lsb = Math.min(0, glyphLsb + width);
1445                         run.setLeftBearing();
1446                         break lsbdone;
1447                     }
1448                 }
1449                 width += advance;
1450             }
1451             // tabs
1452             if (glyphCount == 0) {
1453                 width += run.getWidth();
1454             }
1455         }
1456 
1457         /* The line rsb is the rsb of the last visual character in the line */
1458         float rsb = 0;
1459         width = 0;
1460         rsbdone:
1461         for (int i = runs.length - 1; i >= 0 ; i--) {
1462             TextRun run = runs[i];
1463             int glyphCount = run.getGlyphCount();
1464             for (int gi = glyphCount - 1; gi >= 0; gi--) {
1465                 float advance = run.getAdvance(gi);
1466                 /* Skip any trailing zero-width glyphs in the line */
1467                 if (advance != 0) {
1468                     int gc = run.getGlyphCode(gi);
1469                     /* Skip any trailing invisible glyphs in the line */
1470                     if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
1471                         FontResource fr = defaultFontResource;
1472                         if (fr == null) {
1473                             TextSpan span = run.getTextSpan();
1474                             PGFont font = (PGFont)span.getFont();
1475                             /* No need to check font != null (run.glyphCount > 0)  */
1476                             size = font.getSize();
1477                             fr = font.getFontResource();
1478                         }
1479                         fr.getGlyphBoundingBox(gc, size, bounds);
1480                         float glyphRsb = bounds[X_MAX_INDEX] - advance;
1481                         rsb = Math.max(0, glyphRsb - width);
1482                         run.setRightBearing();
1483                         break rsbdone;
1484                     }
1485                 }
1486                 width += advance;
1487             }
1488             // tabs
1489             if (glyphCount == 0) {
1490                 width += run.getWidth();
1491             }
1492         }
1493         line.setSideBearings(lsb, rsb);
1494     }
1495 }