com.sun.javafx.text.PrismTextLayout Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openjfx-78-backport Show documentation
Show all versions of openjfx-78-backport Show documentation
This is a backport of OpenJFX 8 to run on Java 7.
The newest version!
/*
* Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
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.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.HitInfo;
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;
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 X_MAX_INDEX = 2;
private char[] text;
private TextSpan[] spans; /* Rich text (null for single font text) */
private PGFont font; /* Single font text (null for rich text) */
private FontStrike strike; /* cached strike of font (identity) */
private TextLine[] lines;
private TextRun[] runs;
private int runCount;
private BaseBounds bounds;
private float layoutWidth, layoutHeight;
private float wrapWidth, spacing;
private LayoutCache layoutCache;
private Shape shape;
private int flags;
public PrismTextLayout() {
bounds = new RectBounds();
}
private void reset() {
layoutCache = null;
runs = null;
flags &= ~ANALYSIS_MASK;
relayout();
}
private void relayout() {
bounds.makeEmpty();
layoutWidth = layoutHeight = 0;
flags &= ~FLAGS_WRAPPED;
lines = null;
shape = null;
}
/***************************************************************************
* *
* TextLayout API *
* *
**************************************************************************/
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; /* Initialized in getText() */
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();
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(); /* Reset to force run metrics to be recomputed */
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) {
float oldWidth = this.wrapWidth;
this.wrapWidth = newWidth;
boolean needsLayout = true;
if (lines != null && oldWidth != 0 && newWidth != 0) {
if ((flags & ALIGN_LEFT) != 0) {
if (newWidth > oldWidth) {
/* If wrapping width is increasing and there is no
* wrapped lines then the text remains valid.
*/
if ((flags & FLAGS_WRAPPED) == 0) {
needsLayout = false;
}
} else {
/* If wrapping width is decreasing but it is still
* greater than the max line width then the text
* remains valid.
*/
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 bounds;
}
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 {
/* end of line (line break or offset>=charCount) */
int maxOffset = 0;
/* set run index to zero to handle empty line case (only break line) */
runIndex = 0;
for (int i = 0; i < runCount; i++) {
TextRun run = runs[i];
/*use the trailing edge of the last logical run*/
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 HitInfo getHitInfo(float x, float y) {
ensureLayout();
HitInfo info = new HitInfo();
int lineIndex = getLineIndex(y);
if (lineIndex >= getLineCount()) {
info.setCharIndex(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();
//TODO binary search
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];
info.setCharIndex(run.getStart() + run.getOffsetAtX(x, trailing));
info.setLeading(trailing[0] == 0);
} else {
//empty line, set to line break leading
info.setCharIndex(line.getStart());
info.setLeading(true);
}
}
return info;
}
public PathElement[] getRange(int start, int end, int type,
float x, float y) {
ensureLayout();
int lineCount = getLineCount();
ArrayList result = new ArrayList();
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;
}
/* The list of runs in the line is visually ordered.
* Thus, finding the run that includes the selection end offset
* does not mean that all selected runs have being visited.
* Instead, this implementation first computes the number of selected
* characters in the current line, then iterates over the runs consuming
* selected characters till all of them are found.
*/
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;
}
/* Merge continuous rectangles */
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);
/* Return a shape relative to the baseline of the first line so
* it can be used for layout */
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();
/* skip embedded runs */
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;
}
/***************************************************************************
* *
* Text Layout Implementation *
* *
**************************************************************************/
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 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;
}
/* init metrics for line breaks for empty lines */
if (run.getAscent() == 0) {
Metrics m = strike.getMetrics();
/* The implementation of the center bounds mode is to assure the
* layout has the same number of pixels above and bellow the cap
* height.
*/
if ((flags & BOUNDS_MASK) == BOUNDS_CENTER) {
float ascent = m.getAscent();
/* Segoe UI has a very large internal leading area, applying the
* center bounds heuristics on it would result in several pixels
* being added to the descent. The final results would be
* overly large and visually unappealing. The fix is to reduce
* the ascent before applying the algorithm. */
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()) {
/* Use GlyphLayout to shape complex text */
layout.layout(run, font, strike, chars);
} else {
FontResource fr = strike.getFontResource();
int start = run.getStart();
int length = run.getLength();
/* No glyph layout required */
if (layoutCache == null) {
float fontSize = strike.getSize();
CharToGlyphMapper mapper = fr.getGlyphMapper();
/* The text contains complex and non-complex runs */
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;
//yadvance always zero
xadvance += width;
}
positions[length<<1] = xadvance;
run.shape(length, glyphs, positions, null);
} else {
/* The text only contains non-complex runs, all the glyphs and
* advances are stored in the shapeCache */
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);
}
/* Recompute line width, height, and length (wrapping) */
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() {
/* The text node in the scene layer is mirrored based on
* result of impl_computeLayoutBounds. The coordinate translation
* in text layout has to be based on the same width.
*/
return wrapWidth != 0 ? wrapWidth : layoutWidth;
}
private void reuseRuns() {
/* The runs list is always accessed by the same thread (as TextLayout
* is not thread safe) thus it can be modified at any time, but the
* elements inside of the list are shared among threads and cannot be
* modified. Each reused element has to be cloned.*/
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); /* unmark split */
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) {
/* Rich text case - use the first font (for now) */
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() {
char[] chars = getText();
/* runs and runCount are set in reuseRuns or buildRuns */
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)) != 0) {
boundary = BreakIterator.getLineInstance();
boundary.setText(new CharArrayIterator(chars));
}
}
int textAlignment = flags & ALIGN_MASK;
/* Optimize simple case: reuse the glyphs and advances as long as the
* text and font are the same.
* The simple case is no bidi, no complex, no justify, no features.
*/
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 linesList = new ArrayList();
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()) {
/* Find offset of the first character that does not fit on the line */
int hitOffset = run.getStart() + run.getWrapIndex(wrapWidth - lineWidth);
/* Only keep whitespaces (not tabs) in the current run to avoid
* dealing with unshaped runs.
*/
int offset = hitOffset;
int runEnd = run.getEnd();
while (offset + 1 < runEnd && chars[offset] == ' ') {
offset++;
/* Preserve behaviour: only keep one white space in the line
* before wrapping. Needed API to allow change.
*/
break;
}
/* Find the break opportunity */
int breakOffset = offset;
if (boundary != null) {
/* Use Java BreakIterator when complex script are present */
breakOffset = boundary.isBoundary(offset) || chars[offset] == '\t' ? offset : boundary.preceding(offset);
} else {
/* Simple break strategy for latin text (Performance) */
boolean currentChar = Character.isWhitespace(chars[breakOffset]);
while (breakOffset > startOffset) {
boolean previousChar = Character.isWhitespace(chars[breakOffset - 1]);
if (!currentChar && previousChar) break;
currentChar = previousChar;
breakOffset--;
}
}
/* Never break before the line start offset */
if (breakOffset < startOffset) breakOffset = startOffset;
/* Find the run that contains the break offset */
int breakRunIndex = startIndex;
TextRun breakRun = null;
while (breakRunIndex < runCount) {
breakRun = runs[breakRunIndex];
if (breakRun.getEnd() > breakOffset) break;
breakRunIndex++;
}
/* No line breaks between hit offset and line start offset.
* Try character wrapping mode at the hit offset.
*/
if (breakOffset == startOffset) {
breakRun = run;
breakRunIndex = i;
breakOffset = hitOffset;
}
int breakOffsetInRun = breakOffset - breakRun.getStart();
/* Wrap the entire run to the next (only if it is not the first
* run of the line).
*/
if (breakOffsetInRun == 0 && breakRunIndex != startIndex) {
i = breakRunIndex - 1;
} else {
i = breakRunIndex;
/* The break offset is at the first offset of the first run of the line.
* This happens when the wrap width is smaller than the width require
* to show the first character for the line.
*/
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++;
}
}
/* No point marking the last run of a line a softbreak */
if (i + 1 < runCount && !runs[i + 1].isLinebreak()) {
run = runs[i];
run.setSoftbreak();
flags |= FLAGS_WRAPPED;
// Tabs should preserve width
/*
* Due to contextual forms (arabic) it is possible this line
* is still too big since the splitting of the arabic run
* changes the shape of boundary glyphs. For now the
* implementation has opted to have the appropriate
* initial/final shapes and allow those glyphs to
* potentially overlap the wrapping width, rather than use
* the medial form within the wrappingWidth. A better place
* to solve this would be TextRun#getWrapIndex - but its TBD
* there too.
*/
}
}
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();
if (layoutCache != null) layoutCache.valid = true;
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; /* Left and Justify */
if (textAlignment == ALIGN_RIGHT) align = 0;
} else {
align = 0; /* Left and Justify */
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();
/* Center and right alignment */
float lineX = (fullWidth - bounds.getWidth()) * align;
line.setAlignment(lineX);
/* Justify */
boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY;
if (justify) {
TextRun[] lineRuns = line.getRuns();
int lineRunCount = lineRuns.length;
if (lineRunCount > 0 && lineRuns[lineRunCount - 1].isSoftbreak()) {
/* count white spaces but skipping trailings whitespaces */
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++) {
// TODO kashidas
if (chars[k] == ' ') {
textRun.justify(k - runStart, inc);
if (--wsCount == 0) break done;
}
}
}
line.setWidth(fullWidth);
}
}
}
if ((flags & FLAGS_HAS_BIDI) != 0) {
reorderLine(line);
}
computeSideBearings(line);
/* Set run location */
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;
bounds = bounds.deriveWithNewBounds(0, ascent, 0, layoutWidth,
layoutHeight + ascent, 0);
}
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();
}
/* The line lsb is the lsb of the first visual character in the line */
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);
/* Skip any leading zero-width glyphs in the line */
if (advance != 0) {
int gc = run.getGlyphCode(gi);
/* Skip any leading invisible glyphs in the line */
if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
FontResource fr = defaultFontResource;
if (fr == null) {
TextSpan span = run.getTextSpan();
PGFont font = (PGFont)span.getFont();
/* No need to check font != null (run.glyphCount > 0) */
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;
}
// tabs
if (glyphCount == 0) {
width += run.getWidth();
}
}
/* The line rsb is the rsb of the last visual character in the line */
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);
/* Skip any trailing zero-width glyphs in the line */
if (advance != 0) {
int gc = run.getGlyphCode(gi);
/* Skip any trailing invisible glyphs in the line */
if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
FontResource fr = defaultFontResource;
if (fr == null) {
TextSpan span = run.getTextSpan();
PGFont font = (PGFont)span.getFont();
/* No need to check font != null (run.glyphCount > 0) */
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;
}
// tabs
if (glyphCount == 0) {
width += run.getWidth();
}
}
line.setSideBearings(lsb, rsb);
}
}