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 }