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