package com.sun.javafx.text;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.PathElement;
import com.sun.javafx.font.CharToGlyphMapper;
import com.sun.javafx.font.FontResource;
import com.sun.javafx.font.FontStrike;
import com.sun.javafx.font.Metrics;
import com.sun.javafx.font.PGFont;
import com.sun.javafx.font.PrismFontFactory;
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.Path2D;
import com.sun.javafx.geom.Point2D;
import com.sun.javafx.geom.RectBounds;
import com.sun.javafx.geom.RoundRectangle2D;
import com.sun.javafx.geom.Shape;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.geom.transform.Translate2D;
import com.sun.javafx.scene.text.GlyphList;
import com.sun.javafx.scene.text.TextLayout;
import com.sun.javafx.scene.text.TextSpan;
import java.text.Bidi;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
public class PrismTextLayout implements TextLayout {
private static final BaseTransform IDENTITY = BaseTransform.IDENTITY_TRANSFORM;
private static final int X_MIN_INDEX = 0;
private static final int Y_MIN_INDEX = 1;
private static final int X_MAX_INDEX = 2;
private static final int Y_MAX_INDEX = 3;
private static final Hashtable<Integer, LayoutCache> stringCache = new Hashtable<>();
private static final Object CACHE_SIZE_LOCK = new Object();
private static int cacheSize = 0;
private static final int MAX_STRING_SIZE = 256;
private static final int MAX_CACHE_SIZE = PrismFontFactory.cacheLayoutSize;
private char[] text;
private TextSpan[] spans;
private PGFont font;
private FontStrike strike;
private Integer cacheKey;
private TextLine[] lines;
private TextRun[] runs;
private int runCount;
private BaseBounds logicalBounds;
private RectBounds visualBounds;
private float layoutWidth, layoutHeight;
private float wrapWidth, spacing;
private LayoutCache layoutCache;
private Shape shape;
private int flags;
public PrismTextLayout() {
logicalBounds = new RectBounds();
flags = ALIGN_LEFT;
}
private void reset() {
layoutCache = null;
runs = null;
flags &= ~ANALYSIS_MASK;
relayout();
}
private void relayout() {
logicalBounds.makeEmpty();
visualBounds = null;
layoutWidth = layoutHeight = 0;
flags &= ~(FLAGS_WRAPPED | FLAGS_CACHED_UNDERLINE | FLAGS_CACHED_STRIKETHROUGH);
lines = null;
shape = null;
}
public boolean setContent(TextSpan[] spans) {
if (spans == null && this.spans == null) return false;
if (spans != null && this.spans != null) {
if (spans.length == this.spans.length) {
int i = 0;
while (i < spans.length) {
if (spans[i] != this.spans[i]) break;
i++;
}
if (i == spans.length) return false;
}
}
reset();
this.spans = spans;
this.font = null;
this.strike = null;
this.text = null;
this.cacheKey = null;
return true;
}
public boolean setContent(String text, Object font) {
reset();
this.spans = null;
this.font = (PGFont)font;
this.strike = ((PGFont)font).getStrike(IDENTITY);
this.text = text.toCharArray();
if (MAX_CACHE_SIZE > 0) {
int length = text.length();
if (0 < length && length <= MAX_STRING_SIZE) {
cacheKey = text.hashCode() * strike.hashCode();
}
}
return true;
}
public boolean setDirection(int direction) {
if ((flags & DIRECTION_MASK) == direction) return false;
flags &= ~DIRECTION_MASK;
flags |= (direction & DIRECTION_MASK);
reset();
return true;
}
public boolean setBoundsType(int type) {
if ((flags & BOUNDS_MASK) == type) return false;
flags &= ~BOUNDS_MASK;
flags |= (type & BOUNDS_MASK);
reset();
return true;
}
public boolean setAlignment(int alignment) {
int align = ALIGN_LEFT;
switch (alignment) {
case 0: align = ALIGN_LEFT; break;
case 1: align = ALIGN_CENTER; break;
case 2: align = ALIGN_RIGHT; break;
case 3: align = ALIGN_JUSTIFY; break;
}
if ((flags & ALIGN_MASK) == align) return false;
if (align == ALIGN_JUSTIFY || (flags & ALIGN_JUSTIFY) != 0) {
reset();
}
flags &= ~ALIGN_MASK;
flags |= align;
relayout();
return true;
}
public boolean setWrapWidth(float newWidth) {
if (Float.isInfinite(newWidth)) newWidth = 0;
if (Float.isNaN(newWidth)) newWidth = 0;
float oldWidth = this.wrapWidth;
this.wrapWidth = Math.max(0, newWidth);
boolean needsLayout = true;
if (lines != null && oldWidth != 0 && newWidth != 0) {
if ((flags & ALIGN_LEFT) != 0) {
if (newWidth > oldWidth) {
if ((flags & FLAGS_WRAPPED) == 0) {
needsLayout = false;
}
} else {
if (newWidth >= layoutWidth) {
needsLayout = false;
}
}
}
}
if (needsLayout) relayout();
return needsLayout;
}
public boolean setLineSpacing(float spacing) {
if (this.spacing == spacing) return false;
this.spacing = spacing;
relayout();
return true;
}
private void ensureLayout() {
if (lines == null) {
layout();
}
}
public com.sun.javafx.scene.text.TextLine[] getLines() {
ensureLayout();
return lines;
}
public GlyphList[] getRuns() {
ensureLayout();
GlyphList[] result = new GlyphList[runCount];
int count = 0;
for (int i = 0; i < lines.length; i++) {
GlyphList[] lineRuns = lines[i].getRuns();
int length = lineRuns.length;
System.arraycopy(lineRuns, 0, result, count, length);
count += length;
}
return result;
}
public BaseBounds getBounds() {
ensureLayout();
return logicalBounds;
}
public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) {
ensureLayout();
float left = Float.POSITIVE_INFINITY;
float top = Float.POSITIVE_INFINITY;
float right = Float.NEGATIVE_INFINITY;
float bottom = Float.NEGATIVE_INFINITY;
if (filter != null) {
for (int i = 0; i < lines.length; i++) {
TextLine line = lines[i];
TextRun[] lineRuns = line.getRuns();
for (int j = 0; j < lineRuns.length; j++) {
TextRun run = lineRuns[j];
TextSpan span = run.getTextSpan();
if (span != filter) continue;
Point2D location = run.getLocation();
float runLeft = location.x;
if (run.isLeftBearing()) {
runLeft += line.getLeftSideBearing();
}
float runRight = location.x + run.getWidth();
if (run.isRightBearing()) {
runRight += line.getRightSideBearing();
}
float runTop = location.y;
float runBottom = location.y + line.getBounds().getHeight() + spacing;
if (runLeft < left) left = runLeft;
if (runTop < top) top = runTop;
if (runRight > right) right = runRight;
if (runBottom > bottom) bottom = runBottom;
}
}
} else {
top = bottom = 0;
for (int i = 0; i < lines.length; i++) {
TextLine line = lines[i];
RectBounds lineBounds = line.getBounds();
float lineLeft = lineBounds.getMinX() + line.getLeftSideBearing();
if (lineLeft < left) left = lineLeft;
float lineRight = lineBounds.getMaxX() + line.getRightSideBearing();
if (lineRight > right) right = lineRight;
bottom += lineBounds.getHeight();
}
if (isMirrored()) {
float width = getMirroringWidth();
float bearing = left;
left = width - right;
right = width - bearing;
}
}
return bounds.deriveWithNewBounds(left, top, 0, right, bottom, 0);
}
public PathElement[] getCaretShape(int offset, boolean isLeading,
float x, float y) {
ensureLayout();
int lineIndex = 0;
int lineCount = getLineCount();
while (lineIndex < lineCount - 1) {
TextLine line = lines[lineIndex];
int lineEnd = line.getStart() + line.getLength();
if (lineEnd > offset) break;
lineIndex++;
}
int sliptCaretOffset = -1;
int level = 0;
float lineX = 0, lineY = 0, lineHeight = 0;
TextLine line = lines[lineIndex];
TextRun[] runs = line.getRuns();
int runCount = runs.length;
int runIndex = -1;
for (int i = 0; i < runCount; i++) {
TextRun run = runs[i];
int runStart = run.getStart();
int runEnd = run.getEnd();
if (runStart <= offset && offset < runEnd) {
if (!run.isLinebreak()) {
runIndex = i;
}
break;
}
}
if (runIndex != -1) {
TextRun run = runs[runIndex];
int runStart = run.getStart();
Point2D location = run.getLocation();
lineX = location.x + run.getXAtOffset(offset - runStart, isLeading);
lineY = location.y;
lineHeight = line.getBounds().getHeight();
if (isLeading) {
if (runIndex > 0 && offset == runStart) {
level = run.getLevel();
sliptCaretOffset = offset - 1;
}
} else {
int runEnd = run.getEnd();
if (runIndex + 1 < runs.length && offset + 1 == runEnd) {
level = run.getLevel();
sliptCaretOffset = offset + 1;
}
}
} else {
int maxOffset = 0;
runIndex = 0;
for (int i = 0; i < runCount; i++) {
TextRun run = runs[i];
if (run.getStart() >= maxOffset && !run.isLinebreak()) {
maxOffset = run.getStart();
runIndex = i;
}
}
TextRun run = runs[runIndex];
Point2D location = run.getLocation();
lineX = location.x + (run.isLeftToRight() ? run.getWidth() : 0);
lineY = location.y;
lineHeight = line.getBounds().getHeight();
}
if (isMirrored()) {
lineX = getMirroringWidth() - lineX;
}
lineX += x;
lineY += y;
if (sliptCaretOffset != -1) {
for (int i = 0; i < runs.length; i++) {
TextRun run = runs[i];
int runStart = run.getStart();
int runEnd = run.getEnd();
if (runStart <= sliptCaretOffset && sliptCaretOffset < runEnd) {
if ((run.getLevel() & 1) != (level & 1)) {
Point2D location = run.getLocation();
float lineX2 = location.x;
if (isLeading) {
if ((level & 1) != 0) lineX2 += run.getWidth();
} else {
if ((level & 1) == 0) lineX2 += run.getWidth();
}
if (isMirrored()) {
lineX2 = getMirroringWidth() - lineX2;
}
lineX2 += x;
PathElement[] result = new PathElement[4];
result[0] = new MoveTo(lineX, lineY);
result[1] = new LineTo(lineX, lineY + lineHeight / 2);
result[2] = new MoveTo(lineX2, lineY + lineHeight / 2);
result[3] = new LineTo(lineX2, lineY + lineHeight);
return result;
}
}
}
}
PathElement[] result = new PathElement[2];
result[0] = new MoveTo(lineX, lineY);
result[1] = new LineTo(lineX, lineY + lineHeight);
return result;
}
public Hit getHitInfo(float x, float y) {
int charIndex = -1;
boolean leading = false;
ensureLayout();
int lineIndex = getLineIndex(y);
if (lineIndex >= getLineCount()) {
charIndex = getCharCount();
} else {
if (isMirrored()) {
x = getMirroringWidth() - x;
}
TextLine line = lines[lineIndex];
TextRun[] runs = line.getRuns();
RectBounds bounds = line.getBounds();
TextRun run = null;
x -= bounds.getMinX();
for (int i = 0; i < runs.length; i++) {
run = runs[i];
if (x < run.getWidth()) break;
if (i + 1 < runs.length) {
if (runs[i + 1].isLinebreak()) break;
x -= run.getWidth();
}
}
if (run != null) {
int[] trailing = new int[1];
charIndex = run.getStart() + run.getOffsetAtX(x, trailing);
leading = (trailing[0] == 0);
} else {
charIndex = line.getStart();
leading = true;
}
}
return new Hit(charIndex, -1, leading);
}
public PathElement[] getRange(int start, int end, int type,
float x, float y) {
ensureLayout();
int lineCount = getLineCount();
ArrayList<PathElement> result = new ArrayList<PathElement>();
float lineY = 0;
for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) {
TextLine line = lines[lineIndex];
RectBounds lineBounds = line.getBounds();
int lineStart = line.getStart();
if (lineStart >= end) break;
int lineEnd = lineStart + line.getLength();
if (start > lineEnd) {
lineY += lineBounds.getHeight() + spacing;
continue;
}
TextRun[] runs = line.getRuns();
int count = Math.min(lineEnd, end) - Math.max(lineStart, start);
int runIndex = 0;
float left = -1;
float right = -1;
float lineX = lineBounds.getMinX();
while (count > 0 && runIndex < runs.length) {
TextRun run = runs[runIndex];
int runStart = run.getStart();
int runEnd = run.getEnd();
float runWidth = run.getWidth();
int clmapStart = Math.max(runStart, Math.min(start, runEnd));
int clampEnd = Math.max(runStart, Math.min(end, runEnd));
int runCount = clampEnd - clmapStart;
if (runCount != 0) {
boolean ltr = run.isLeftToRight();
float runLeft;
if (runStart > start) {
runLeft = ltr ? lineX : lineX + runWidth;
} else {
runLeft = lineX + run.getXAtOffset(start - runStart, true);
}
float runRight;
if (runEnd < end) {
runRight = ltr ? lineX + runWidth : lineX;
} else {
runRight = lineX + run.getXAtOffset(end - runStart, true);
}
if (runLeft > runRight) {
float tmp = runLeft;
runLeft = runRight;
runRight = tmp;
}
count -= runCount;
float top = 0, bottom = 0;
switch (type) {
case TYPE_TEXT:
top = lineY;
bottom = lineY + lineBounds.getHeight();
break;
case TYPE_UNDERLINE:
case TYPE_STRIKETHROUGH:
FontStrike fontStrike = null;
if (spans != null) {
TextSpan span = run.getTextSpan();
PGFont font = (PGFont)span.getFont();
if (font == null) break;
fontStrike = font.getStrike(IDENTITY);
} else {
fontStrike = strike;
}
top = lineY - run.getAscent();
Metrics metrics = fontStrike.getMetrics();
if (type == TYPE_UNDERLINE) {
top += metrics.getUnderLineOffset();
bottom = top + metrics.getUnderLineThickness();
} else {
top += metrics.getStrikethroughOffset();
bottom = top + metrics.getStrikethroughThickness();
}
break;
}
if (runLeft != right) {
if (left != -1 && right != -1) {
float l = left, r = right;
if (isMirrored()) {
float width = getMirroringWidth();
l = width - l;
r = width - r;
}
result.add(new MoveTo(x + l, y + top));
result.add(new LineTo(x + r, y + top));
result.add(new LineTo(x + r, y + bottom));
result.add(new LineTo(x + l, y + bottom));
result.add(new LineTo(x + l, y + top));
}
left = runLeft;
right = runRight;
}
right = runRight;
if (count == 0) {
float l = left, r = right;
if (isMirrored()) {
float width = getMirroringWidth();
l = width - l;
r = width - r;
}
result.add(new MoveTo(x + l, y + top));
result.add(new LineTo(x + r, y + top));
result.add(new LineTo(x + r, y + bottom));
result.add(new LineTo(x + l, y + bottom));
result.add(new LineTo(x + l, y + top));
}
}
lineX += runWidth;
runIndex++;
}
lineY += lineBounds.getHeight() + spacing;
}
return result.toArray(new PathElement[result.size()]);
}
public Shape getShape(int type, TextSpan filter) {
ensureLayout();
boolean text = (type & TYPE_TEXT) != 0;
boolean underline = (type & TYPE_UNDERLINE) != 0;
boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0;
boolean baselineType = (type & TYPE_BASELINE) != 0;
if (shape != null && text && !underline && !strikethrough && baselineType) {
return shape;
}
Path2D outline = new Path2D();
BaseTransform tx = new Translate2D(0, 0);
float firstBaseline = 0;
if (baselineType) {
firstBaseline = -lines[0].getBounds().getMinY();
}
for (int i = 0; i < lines.length; i++) {
TextLine line = lines[i];
TextRun[] runs = line.getRuns();
RectBounds bounds = line.getBounds();
float baseline = -bounds.getMinY();
for (int j = 0; j < runs.length; j++) {
TextRun run = runs[j];
FontStrike fontStrike = null;
if (spans != null) {
TextSpan span = run.getTextSpan();
if (filter != null && span != filter) continue;
PGFont font = (PGFont)span.getFont();
if (font == null) continue;
fontStrike = font.getStrike(IDENTITY);
} else {
fontStrike = strike;
}
Point2D location = run.getLocation();
float runX = location.x;
float runY = location.y + baseline - firstBaseline;
Metrics metrics = null;
if (underline || strikethrough) {
metrics = fontStrike.getMetrics();
}
if (underline) {
RoundRectangle2D rect = new RoundRectangle2D();
rect.x = runX;
rect.y = runY + metrics.getUnderLineOffset();
rect.width = run.getWidth();
rect.height = metrics.getUnderLineThickness();
outline.append(rect, false);
}
if (strikethrough) {
RoundRectangle2D rect = new RoundRectangle2D();
rect.x = runX;
rect.y = runY + metrics.getStrikethroughOffset();
rect.width = run.getWidth();
rect.height = metrics.getStrikethroughThickness();
outline.append(rect, false);
}
if (text && run.getGlyphCount() > 0) {
tx.restoreTransform(1, 0, 0, 1, runX, runY);
Path2D path = (Path2D)fontStrike.getOutline(run, tx);
outline.append(path, false);
}
}
}
if (text && !underline && !strikethrough) {
shape = outline;
}
return outline;
}
private int getLineIndex(float y) {
int index = 0;
float bottom = 0;
int lineCount = getLineCount();
while (index < lineCount) {
bottom += lines[index].getBounds().getHeight() + spacing;
if (index + 1 == lineCount) bottom -= lines[index].getLeading();
if (bottom > y) break;
index++;
}
return index;
}
private boolean copyCache() {
int align = flags & ALIGN_MASK;
int boundsType = flags & BOUNDS_MASK;
return wrapWidth != 0 || align != ALIGN_LEFT || boundsType == 0 || isMirrored();
}
private void initCache() {
if (cacheKey != null) {
if (layoutCache == null) {
LayoutCache cache = stringCache.get(cacheKey);
if (cache != null && cache.font.equals(font) && Arrays.equals(cache.text, text)) {
layoutCache = cache;
runs = cache.runs;
runCount = cache.runCount;
flags |= cache.analysis;
}
}
if (layoutCache != null) {
if (copyCache()) {
if (layoutCache.runs == runs) {
runs = new TextRun[runCount];
System.arraycopy(layoutCache.runs, 0, runs, 0, runCount);
}
} else {
if (layoutCache.lines != null) {
runs = layoutCache.runs;
runCount = layoutCache.runCount;
flags |= layoutCache.analysis;
lines = layoutCache.lines;
layoutWidth = layoutCache.layoutWidth;
layoutHeight = layoutCache.layoutHeight;
float ascent = lines[0].getBounds().getMinY();
logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0,
layoutWidth, layoutHeight + ascent, 0);
}
}
}
}
}
private int getLineCount() {
return lines.length;
}
private int getCharCount() {
if (text != null) return text.length;
int count = 0;
for (int i = 0; i < lines.length; i++) {
count += lines[i].getLength();
}
return count;
}
public TextSpan[] getTextSpans() {
return spans;
}
public PGFont getFont() {
return font;
}
public int getDirection() {
if ((flags & DIRECTION_LTR) != 0) {
return Bidi.DIRECTION_LEFT_TO_RIGHT;
}
if ((flags & DIRECTION_RTL) != 0) {
return Bidi.DIRECTION_RIGHT_TO_LEFT;
}
if ((flags & DIRECTION_DEFAULT_LTR) != 0) {
return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
}
if ((flags & DIRECTION_DEFAULT_RTL) != 0) {
return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT;
}
return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
}
public void addTextRun(TextRun run) {
if (runCount + 1 > runs.length) {
TextRun[] newRuns = new TextRun[runs.length + 64];
System.arraycopy(runs, 0, newRuns, 0, runs.length);
runs = newRuns;
}
runs[runCount++] = run;
}
private void buildRuns(char[] chars) {
runCount = 0;
if (runs == null) {
int count = Math.max(4, Math.min(chars.length / 16, 16));
runs = new TextRun[count];
}
GlyphLayout layout = GlyphLayout.getInstance();
flags = layout.breakRuns(this, chars, flags);
layout.dispose();
for (int j = runCount; j < runs.length; j++) {
runs[j] = null;
}
}
private void shape(TextRun run, char[] chars, GlyphLayout layout) {
FontStrike strike;
PGFont font;
if (spans != null) {
if (spans.length == 0) return;
TextSpan span = run.getTextSpan();
font = (PGFont)span.getFont();
if (font == null) {
RectBounds bounds = span.getBounds();
run.setEmbedded(bounds, span.getText().length());
return;
}
strike = font.getStrike(IDENTITY);
} else {
font = this.font;
strike = this.strike;
}
if (run.getAscent() == 0) {
Metrics m = strike.getMetrics();
if ((flags & BOUNDS_MASK) == BOUNDS_CENTER) {
float ascent = m.getAscent();
if (font.getFamilyName().equals("Segoe UI")) {
ascent *= 0.80;
}
ascent = (int)(ascent-0.75);
float descent = (int)(m.getDescent()+0.75);
float leading = (int)(m.getLineGap()+0.75);
float capHeight = (int)(m.getCapHeight()+0.75);
float topPadding = -ascent - capHeight;
if (topPadding > descent) {
descent = topPadding;
} else {
ascent += (topPadding - descent);
}
run.setMetrics(ascent, descent, leading);
} else {
run.setMetrics(m.getAscent(), m.getDescent(), m.getLineGap());
}
}
if (run.isTab()) return;
if (run.isLinebreak()) return;
if (run.getGlyphCount() > 0) return;
if (run.isComplex()) {
layout.layout(run, font, strike, chars);
} else {
FontResource fr = strike.getFontResource();
int start = run.getStart();
int length = run.getLength();
if (layoutCache == null) {
float fontSize = strike.getSize();
CharToGlyphMapper mapper = fr.getGlyphMapper();
int[] glyphs = new int[length];
mapper.charsToGlyphs(start, length, chars, glyphs);
float[] positions = new float[(length + 1) << 1];
float xadvance = 0;
for (int i = 0; i < length; i++) {
float width = fr.getAdvance(glyphs[i], fontSize);
positions[i<<1] = xadvance;
xadvance += width;
}
positions[length<<1] = xadvance;
run.shape(length, glyphs, positions, null);
} else {
if (!layoutCache.valid) {
float fontSize = strike.getSize();
CharToGlyphMapper mapper = fr.getGlyphMapper();
mapper.charsToGlyphs(start, length, chars, layoutCache.glyphs, start);
int end = start + length;
float width = 0;
for (int i = start; i < end; i++) {
float adv = fr.getAdvance(layoutCache.glyphs[i], fontSize);
layoutCache.advances[i] = adv;
width += adv;
}
run.setWidth(width);
}
run.shape(length, layoutCache.glyphs, layoutCache.advances);
}
}
}
private TextLine createLine(int start, int end, int startOffset) {
int count = end - start + 1;
TextRun[] lineRuns = new TextRun[count];
if (start < runCount) {
System.arraycopy(runs, start, lineRuns, 0, count);
}
float width = 0, ascent = 0, descent = 0, leading = 0;
int length = 0;
for (int i = 0; i < lineRuns.length; i++) {
TextRun run = lineRuns[i];
width += run.getWidth();
ascent = Math.min(ascent, run.getAscent());
descent = Math.max(descent, run.getDescent());
leading = Math.max(leading, run.getLeading());
length += run.getLength();
}
if (width > layoutWidth) layoutWidth = width;
return new TextLine(startOffset, length, lineRuns,
width, ascent, descent, leading);
}
private void reorderLine(TextLine line) {
TextRun[] runs = line.getRuns();
int length = runs.length;
if (length > 0 && runs[length - 1].isLinebreak()) {
length--;
}
if (length < 2) return;
byte[] levels = new byte[length];
for (int i = 0; i < length; i++) {
levels[i] = runs[i].getLevel();
}
Bidi.reorderVisually(levels, 0, runs, 0, length);
}
private char[] getText() {
if (text == null) {
int count = 0;
for (int i = 0; i < spans.length; i++) {
count += spans[i].getText().length();
}
text = new char[count];
int offset = 0;
for (int i = 0; i < spans.length; i++) {
String string = spans[i].getText();
int length = string.length();
string.getChars(0, length, text, offset);
offset += length;
}
}
return text;
}
private boolean isSimpleLayout() {
int textAlignment = flags & ALIGN_MASK;
boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY;
int mask = FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX;
return (flags & mask) == 0 && !justify;
}
private boolean isMirrored() {
boolean mirrored = false;
switch (flags & DIRECTION_MASK) {
case DIRECTION_RTL: mirrored = true; break;
case DIRECTION_LTR: mirrored = false; break;
case DIRECTION_DEFAULT_LTR:
case DIRECTION_DEFAULT_RTL:
mirrored = (flags & FLAGS_RTL_BASE) != 0;
}
return mirrored;
}
private float getMirroringWidth() {
return wrapWidth != 0 ? wrapWidth : layoutWidth;
}
private void reuseRuns() {
runCount = 0;
int index = 0;;
while (index < runs.length) {
TextRun run = runs[index];
if (run == null) break;
runs[index] = null;
index++;
runs[runCount++] = run = run.unwrap();
if (run.isSplit()) {
run.merge(null);
while (index < runs.length) {
TextRun nextRun = runs[index];
if (nextRun == null) break;
run.merge(nextRun);
runs[index] = null;
index++;
if (nextRun.isSplitLast()) break;
}
}
}
}
private float getTabAdvance() {
float spaceAdvance = 0;
if (spans != null) {
for (int i = 0; i < spans.length; i++) {
TextSpan span = spans[i];
PGFont font = (PGFont)span.getFont();
if (font != null) {
FontStrike strike = font.getStrike(IDENTITY);
spaceAdvance = strike.getCharAdvance(' ');
break;
}
}
} else {
spaceAdvance = strike.getCharAdvance(' ');
}
return 8 * spaceAdvance;
}
private void layout() {
initCache();
if (lines != null) return;
char[] chars = getText();
if ((flags & FLAGS_ANALYSIS_VALID) != 0 && isSimpleLayout()) {
reuseRuns();
} else {
buildRuns(chars);
}
GlyphLayout layout = null;
if ((flags & (FLAGS_HAS_COMPLEX)) != 0) {
layout = GlyphLayout.getInstance();
}
float tabAdvance = 0;
if ((flags & FLAGS_HAS_TABS) != 0) {
tabAdvance = getTabAdvance();
}
BreakIterator boundary = null;
if (wrapWidth > 0) {
if ((flags & (FLAGS_HAS_COMPLEX | FLAGS_HAS_CJK)) != 0) {
boundary = BreakIterator.getLineInstance();
boundary.setText(new CharArrayIterator(chars));
}
}
int textAlignment = flags & ALIGN_MASK;
if (isSimpleLayout()) {
if (layoutCache == null) {
layoutCache = new LayoutCache();
layoutCache.glyphs = new int[chars.length];
layoutCache.advances = new float[chars.length];
}
} else {
layoutCache = null;
}
float lineWidth = 0;
int startIndex = 0;
int startOffset = 0;
ArrayList<TextLine> linesList = new ArrayList<TextLine>();
for (int i = 0; i < runCount; i++) {
TextRun run = runs[i];
shape(run, chars, layout);
if (run.isTab()) {
float tabStop = ((int)(lineWidth / tabAdvance) +1) * tabAdvance;
run.setWidth(tabStop - lineWidth);
}
float runWidth = run.getWidth();
if (wrapWidth > 0 && lineWidth + runWidth > wrapWidth && !run.isLinebreak()) {
int hitOffset = run.getStart() + run.getWrapIndex(wrapWidth - lineWidth);
int offset = hitOffset;
int runEnd = run.getEnd();
while (offset + 1 < runEnd && chars[offset] == ' ') {
offset++;
break;
}
int breakOffset = offset;
if (boundary != null) {
breakOffset = boundary.isBoundary(offset) || chars[offset] == '\t' ? offset : boundary.preceding(offset);
} else {
boolean currentChar = Character.isWhitespace(chars[breakOffset]);
while (breakOffset > startOffset) {
boolean previousChar = Character.isWhitespace(chars[breakOffset - 1]);
if (!currentChar && previousChar) break;
currentChar = previousChar;
breakOffset--;
}
}
if (breakOffset < startOffset) breakOffset = startOffset;
int breakRunIndex = startIndex;
TextRun breakRun = null;
while (breakRunIndex < runCount) {
breakRun = runs[breakRunIndex];
if (breakRun.getEnd() > breakOffset) break;
breakRunIndex++;
}
if (breakOffset == startOffset) {
breakRun = run;
breakRunIndex = i;
breakOffset = hitOffset;
}
int breakOffsetInRun = breakOffset - breakRun.getStart();
if (breakOffsetInRun == 0 && breakRunIndex != startIndex) {
i = breakRunIndex - 1;
} else {
i = breakRunIndex;
if (breakOffsetInRun == 0) {
breakOffsetInRun++;
}
if (breakOffsetInRun < breakRun.getLength()) {
if (runCount >= runs.length) {
TextRun[] newRuns = new TextRun[runs.length + 64];
System.arraycopy(runs, 0, newRuns, 0, i + 1);
System.arraycopy(runs, i + 1, newRuns, i + 2, runs.length - i - 1);
runs = newRuns;
} else {
System.arraycopy(runs, i + 1, runs, i + 2, runCount - i - 1);
}
runs[i + 1] = breakRun.split(breakOffsetInRun);
if (breakRun.isComplex()) {
shape(breakRun, chars, layout);
}
runCount++;
}
}
if (i + 1 < runCount && !runs[i + 1].isLinebreak()) {
run = runs[i];
run.setSoftbreak();
flags |= FLAGS_WRAPPED;
}
}
lineWidth += runWidth;
if (run.isBreak()) {
TextLine line = createLine(startIndex, i, startOffset);
linesList.add(line);
startIndex = i + 1;
startOffset += line.getLength();
lineWidth = 0;
}
}
if (layout != null) layout.dispose();
linesList.add(createLine(startIndex, runCount - 1, startOffset));
lines = new TextLine[linesList.size()];
linesList.toArray(lines);
float fullWidth = Math.max(wrapWidth, layoutWidth);
float lineY = 0;
float align;
if (isMirrored()) {
align = 1;
if (textAlignment == ALIGN_RIGHT) align = 0;
} else {
align = 0;
if (textAlignment == ALIGN_RIGHT) align = 1;
}
if (textAlignment == ALIGN_CENTER) align = 0.5f;
for (int i = 0; i < lines.length; i++) {
TextLine line = lines[i];
int lineStart = line.getStart();
RectBounds bounds = line.getBounds();
float lineX = (fullWidth - bounds.getWidth()) * align;
line.setAlignment(lineX);
boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY;
if (justify) {
TextRun[] lineRuns = line.getRuns();
int lineRunCount = lineRuns.length;
if (lineRunCount > 0 && lineRuns[lineRunCount - 1].isSoftbreak()) {
int lineEnd = lineStart + line.getLength();
int wsCount = 0;
boolean hitChar = false;
for (int j = lineEnd - 1; j >= lineStart; j--) {
if (!hitChar && chars[j] != ' ') hitChar = true;
if (hitChar && chars[j] == ' ') wsCount++;
}
if (wsCount != 0) {
float inc = (fullWidth - bounds.getWidth()) / wsCount;
done:
for (int j = 0; j < lineRunCount; j++) {
TextRun textRun = lineRuns[j];
int runStart = textRun.getStart();
int runEnd = textRun.getEnd();
for (int k = runStart; k < runEnd; k++) {
if (chars[k] == ' ') {
textRun.justify(k - runStart, inc);
if (--wsCount == 0) break done;
}
}
}
lineX = 0;
line.setAlignment(lineX);
line.setWidth(fullWidth);
}
}
}
if ((flags & FLAGS_HAS_BIDI) != 0) {
reorderLine(line);
}
computeSideBearings(line);
float runX = lineX;
TextRun[] lineRuns = line.getRuns();
for (int j = 0; j < lineRuns.length; j++) {
TextRun run = lineRuns[j];
run.setLocation(runX, lineY);
run.setLine(line);
runX += run.getWidth();
}
if (i + 1 < lines.length) {
lineY = Math.max(lineY, lineY + bounds.getHeight() + spacing);
} else {
lineY += (bounds.getHeight() - line.getLeading());
}
}
float ascent = lines[0].getBounds().getMinY();
layoutHeight = lineY;
logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, layoutWidth,
layoutHeight + ascent, 0);
if (layoutCache != null) {
if (cacheKey != null && !layoutCache.valid && !copyCache()) {
layoutCache.font = font;
layoutCache.text = text;
layoutCache.runs = runs;
layoutCache.runCount = runCount;
layoutCache.lines = lines;
layoutCache.layoutWidth = layoutWidth;
layoutCache.layoutHeight = layoutHeight;
layoutCache.analysis = flags & ANALYSIS_MASK;
synchronized (CACHE_SIZE_LOCK) {
int charCount = chars.length;
if (cacheSize + charCount > MAX_CACHE_SIZE) {
stringCache.clear();
cacheSize = 0;
}
stringCache.put(cacheKey, layoutCache);
cacheSize += charCount;
}
}
layoutCache.valid = true;
}
}
@Override
public BaseBounds getVisualBounds(int type) {
ensureLayout();
if (strike == null) {
return null;
}
boolean underline = (type & TYPE_UNDERLINE) != 0;
boolean hasUnderline = (flags & FLAGS_CACHED_UNDERLINE) != 0;
boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0;
boolean hasStrikethrough = (flags & FLAGS_CACHED_STRIKETHROUGH) != 0;
if (visualBounds != null && underline == hasUnderline
&& strikethrough == hasStrikethrough) {
return visualBounds;
}
flags &= ~(FLAGS_CACHED_STRIKETHROUGH | FLAGS_CACHED_UNDERLINE);
if (underline) flags |= FLAGS_CACHED_UNDERLINE;
if (strikethrough) flags |= FLAGS_CACHED_STRIKETHROUGH;
visualBounds = new RectBounds();
float xMin = Float.POSITIVE_INFINITY;
float yMin = Float.POSITIVE_INFINITY;
float xMax = Float.NEGATIVE_INFINITY;
float yMax = Float.NEGATIVE_INFINITY;
float bounds[] = new float[4];
FontResource fr = strike.getFontResource();
Metrics metrics = strike.getMetrics();
float size = strike.getSize();
for (int i = 0; i < lines.length; i++) {
TextLine line = lines[i];
TextRun[] runs = line.getRuns();
for (int j = 0; j < runs.length; j++) {
TextRun run = runs[j];
Point2D pt = run.getLocation();
if (run.isLinebreak()) continue;
int glyphCount = run.getGlyphCount();
for (int gi = 0; gi < glyphCount; gi++) {
int gc = run.getGlyphCode(gi);
if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
fr.getGlyphBoundingBox(run.getGlyphCode(gi), size, bounds);
if (bounds[X_MIN_INDEX] != bounds[X_MAX_INDEX]) {
float glyphX = pt.x + run.getPosX(gi);
float glyphY = pt.y + run.getPosY(gi);
float glyphMinX = glyphX + bounds[X_MIN_INDEX];
float glyphMinY = glyphY - bounds[Y_MAX_INDEX];
float glyphMaxX = glyphX + bounds[X_MAX_INDEX];
float glyphMaxY = glyphY - bounds[Y_MIN_INDEX];
if (glyphMinX < xMin) xMin = glyphMinX;
if (glyphMinY < yMin) yMin = glyphMinY;
if (glyphMaxX > xMax) xMax = glyphMaxX;
if (glyphMaxY > yMax) yMax = glyphMaxY;
}
}
}
if (underline) {
float underlineMinX = pt.x;
float underlineMinY = pt.y + metrics.getUnderLineOffset();
float underlineMaxX = underlineMinX + run.getWidth();
float underlineMaxY = underlineMinY + metrics.getUnderLineThickness();
if (underlineMinX < xMin) xMin = underlineMinX;
if (underlineMinY < yMin) yMin = underlineMinY;
if (underlineMaxX > xMax) xMax = underlineMaxX;
if (underlineMaxY > yMax) yMax = underlineMaxY;
}
if (strikethrough) {
float strikethroughMinX = pt.x;
float strikethroughMinY = pt.y + metrics.getStrikethroughOffset();
float strikethroughMaxX = strikethroughMinX + run.getWidth();
float strikethroughMaxY = strikethroughMinY + metrics.getStrikethroughThickness();
if (strikethroughMinX < xMin) xMin = strikethroughMinX;
if (strikethroughMinY < yMin) yMin = strikethroughMinY;
if (strikethroughMaxX > xMax) xMax = strikethroughMaxX;
if (strikethroughMaxY > yMax) yMax = strikethroughMaxY;
}
}
}
if (xMin < xMax && yMin < yMax) {
visualBounds.setBounds(xMin, yMin, xMax, yMax);
}
return visualBounds;
}
private void computeSideBearings(TextLine line) {
TextRun[] runs = line.getRuns();
if (runs.length == 0) return;
float bounds[] = new float[4];
FontResource defaultFontResource = null;
float size = 0;
if (strike != null) {
defaultFontResource = strike.getFontResource();
size = strike.getSize();
}
float lsb = 0;
float width = 0;
lsbdone:
for (int i = 0; i < runs.length; i++) {
TextRun run = runs[i];
int glyphCount = run.getGlyphCount();
for (int gi = 0; gi < glyphCount; gi++) {
float advance = run.getAdvance(gi);
if (advance != 0) {
int gc = run.getGlyphCode(gi);
if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
FontResource fr = defaultFontResource;
if (fr == null) {
TextSpan span = run.getTextSpan();
PGFont font = (PGFont)span.getFont();
size = font.getSize();
fr = font.getFontResource();
}
fr.getGlyphBoundingBox(gc, size, bounds);
float glyphLsb = bounds[X_MIN_INDEX];
lsb = Math.min(0, glyphLsb + width);
run.setLeftBearing();
break lsbdone;
}
}
width += advance;
}
if (glyphCount == 0) {
width += run.getWidth();
}
}
float rsb = 0;
width = 0;
rsbdone:
for (int i = runs.length - 1; i >= 0 ; i--) {
TextRun run = runs[i];
int glyphCount = run.getGlyphCount();
for (int gi = glyphCount - 1; gi >= 0; gi--) {
float advance = run.getAdvance(gi);
if (advance != 0) {
int gc = run.getGlyphCode(gi);
if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
FontResource fr = defaultFontResource;
if (fr == null) {
TextSpan span = run.getTextSpan();
PGFont font = (PGFont)span.getFont();
size = font.getSize();
fr = font.getFontResource();
}
fr.getGlyphBoundingBox(gc, size, bounds);
float glyphRsb = bounds[X_MAX_INDEX] - advance;
rsb = Math.max(0, glyphRsb - width);
run.setRightBearing();
break rsbdone;
}
}
width += advance;
}
if (glyphCount == 0) {
width += run.getWidth();
}
}
line.setSideBearings(lsb, rsb);
}
}