/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.badlogic.gdx.graphics.g2d;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Colors;
import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
import com.badlogic.gdx.graphics.g2d.BitmapFont.Glyph;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.Pool;
import com.badlogic.gdx.utils.Pool.Poolable;
import com.badlogic.gdx.utils.Pools;
/** Stores {@link GlyphRun runs} of glyphs for a piece of text. The text may contain newlines and color markup tags.
* @author Nathan Sweet
* @author davebaol
* @author Alexander Dorokhov */
public class GlyphLayout implements Poolable {
public final Array<GlyphRun> runs = new Array();
public float width, height;
private final Array<Color> colorStack = new Array(4);
/** Creates an empty GlyphLayout. */
public GlyphLayout () {
}
/** @see #setText(BitmapFont, CharSequence) */
public GlyphLayout (BitmapFont font, CharSequence str) {
setText(font, str);
}
/** @see #setText(BitmapFont, CharSequence) */
public GlyphLayout (BitmapFont font, CharSequence str, Color color, float targetWidth, int halign, boolean wrap) {
setText(font, str, color, targetWidth, halign, wrap);
}
/** @see #setText(BitmapFont, CharSequence) */
public GlyphLayout (BitmapFont font, CharSequence str, int start, int end, Color color, float targetWidth, int halign,
boolean wrap, String truncate) {
setText(font, str, start, end, color, targetWidth, halign, wrap, truncate);
}
/** Calls {@link #setText(BitmapFont, CharSequence, int, int, Color, float, int, boolean, String) setText} with the whole
* string, the font's current color, and no alignment or wrapping. */
public void setText (BitmapFont font, CharSequence str) {
setText(font, str, 0, str.length(), font.getColor(), 0, Align.left, false, null);
}
/** Calls {@link #setText(BitmapFont, CharSequence, int, int, Color, float, int, boolean, String) setText} with the whole
* string and no truncation. */
public void setText (BitmapFont font, CharSequence str, Color color, float targetWidth, int halign, boolean wrap) {
setText(font, str, 0, str.length(), color, targetWidth, halign, wrap, null);
}
/** @param color The default color to use for the text (the BitmapFont {@link BitmapFont#getColor() color} is not used). If
* {@link BitmapFontData#markupEnabled} is true, color markup tags in the specified string may change the color for
* portions of the text.
* @param halign Horizontal alignment of the text, see {@link Align}.
* @param targetWidth The width used for alignment, line wrapping, and truncation. May be zero if those features are not used.
* @param truncate If not null and the width of the glyphs exceed targetWidth, the glyphs are truncated and the glyphs for the
* specified truncate string are placed at the end. Empty string can be used to truncate without adding glyphs.
* Truncate should not be used with text that contains multiple lines. Wrap is ignored if truncate is not null. */
public void setText (BitmapFont font, CharSequence str, int start, int end, Color color, float targetWidth, int halign,
boolean wrap, String truncate) {
if (truncate != null)
wrap = true; // Causes truncate code to run, doesn't actually cause wrapping.
else if (targetWidth <= font.data.spaceWidth) //
wrap = false; // Avoid one line per character, which is very inefficient.
BitmapFontData fontData = font.data;
boolean markupEnabled = fontData.markupEnabled;
Pool<GlyphRun> glyphRunPool = Pools.get(GlyphRun.class);
Array<GlyphRun> runs = this.runs;
glyphRunPool.freeAll(runs);
runs.clear();
float x = 0, y = 0, width = 0;
int lines = 0, blankLines = 0;
Array<Color> colorStack = this.colorStack;
Color nextColor = color;
colorStack.add(color);
Pool<Color> colorPool = Pools.get(Color.class);
int runStart = start;
outer:
while (true) {
// Each run is delimited by newline or left square bracket.
int runEnd = -1;
boolean newline = false, colorRun = false;
if (start == end) {
if (runStart == end) break; // End of string with no run to process, we're done.
runEnd = end; // End of string, process last run.
} else {
switch (str.charAt(start++)) {
case '\n':
// End of line.
runEnd = start - 1;
newline = true;
break;
case '[':
// Possible color tag.
if (markupEnabled) {
int length = parseColorMarkup(str, start, end, colorPool);
if (length >= 0) {
runEnd = start - 1;
start += length + 1;
nextColor = colorStack.peek();
colorRun = true;
} else if (length == -2) {
start++; // Skip first of "[[" escape sequence.
continue outer;
}
}
break;
}
}
if (runEnd != -1) {
if (runEnd != runStart) { // Can happen (eg) when a color tag is at text start or a line is "\n".
// Store the run that has ended.
GlyphRun run = glyphRunPool.obtain();
run.color.set(color);
run.x = x;
run.y = y;
fontData.getGlyphs(run, str, runStart, runEnd, colorRun);
if (run.glyphs.size == 0)
glyphRunPool.free(run);
else {
runs.add(run);
// Compute the run width, wrap if necessary, and position the run.
float[] xAdvances = run.xAdvances.items;
for (int i = 0, n = run.xAdvances.size; i < n; i++) {
float xAdvance = xAdvances[i];
x += xAdvance;
// Don't wrap if the glyph would fit with just its width (no xadvance or kerning).
if (wrap && x > targetWidth && i > 1
&& x - xAdvance + (run.glyphs.get(i - 1).xoffset + run.glyphs.get(i - 1).width) * fontData.scaleX
- 0.0001f > targetWidth) {
if (truncate != null) {
truncate(fontData, run, targetWidth, truncate, i, glyphRunPool);
x = run.x + run.width;
break outer;
}
int wrapIndex = fontData.getWrapIndex(run.glyphs, i);
if ((run.x == 0 && wrapIndex == 0) // Require at least one glyph per line.
|| wrapIndex >= run.glyphs.size) { // Wrap at least the glyph that didn't fit.
wrapIndex = i - 1;
}
GlyphRun next;
if (wrapIndex == 0)
next = run; // No wrap index, move entire run to next line.
else {
next = wrap(fontData, run, glyphRunPool, wrapIndex, i);
runs.add(next);
}
// Start the loop over with the new run on the next line.
width = Math.max(width, run.x + run.width);
x = 0;
y += fontData.down;
lines++;
next.x = 0;
next.y = y;
i = -1;
n = next.xAdvances.size;
xAdvances = next.xAdvances.items;
run = next;
} else
run.width += xAdvance;
}
}
}
if (newline) {
// Next run will be on the next line.
width = Math.max(width, x);
x = 0;
float down = fontData.down;
if (runEnd == runStart) { // Blank line.
down *= fontData.blankLineScale;
blankLines++;
} else
lines++;
y += down;
}
runStart = start;
color = nextColor;
}
}
width = Math.max(width, x);
for (int i = 1, n = colorStack.size; i < n; i++)
colorPool.free(colorStack.get(i));
colorStack.clear();
// Align runs to center or right of targetWidth.
if ((halign & Align.left) == 0) { // Not left aligned, so must be center or right aligned.
boolean center = (halign & Align.center) != 0;
float lineWidth = 0, lineY = Integer.MIN_VALUE;
int lineStart = 0, n = runs.size;
for (int i = 0; i < n; i++) {
GlyphRun run = runs.get(i);
if (run.y != lineY) {
lineY = run.y;
float shift = targetWidth - lineWidth;
if (center) shift /= 2;
while (lineStart < i)
runs.get(lineStart++).x += shift;
lineWidth = 0;
}
lineWidth += run.width;
}
float shift = targetWidth - lineWidth;
if (center) shift /= 2;
while (lineStart < n)
runs.get(lineStart++).x += shift;
}
this.width = width;
this.height = fontData.capHeight + lines * fontData.lineHeight + blankLines * fontData.lineHeight * fontData.blankLineScale;
}
private void truncate (BitmapFontData fontData, GlyphRun run, float targetWidth, String truncate, int widthIndex,
Pool<GlyphRun> glyphRunPool) {
// Determine truncate string size.
GlyphRun truncateRun = glyphRunPool.obtain();
fontData.getGlyphs(truncateRun, truncate, 0, truncate.length(), true);
float truncateWidth = 0;
for (int i = 1, n = truncateRun.xAdvances.size; i < n; i++)
truncateWidth += truncateRun.xAdvances.get(i);
targetWidth -= truncateWidth;
// Determine visible glyphs.
int count = 0;
float width = run.x;
while (count < run.xAdvances.size) {
float xAdvance = run.xAdvances.get(count);
width += xAdvance;
if (width > targetWidth) {
run.width = width - run.x - xAdvance;
break;
}
count++;
}
if (count > 1) {
// Some run glyphs fit, append truncate glyphs.
run.glyphs.truncate(count - 1);
run.xAdvances.truncate(count);
adjustLastGlyph(fontData, run);
if (truncateRun.xAdvances.size > 0) run.xAdvances.addAll(truncateRun.xAdvances, 1, truncateRun.xAdvances.size - 1);
} else {
// No run glyphs fit, use only truncate glyphs.
run.glyphs.clear();
run.xAdvances.clear();
run.xAdvances.addAll(truncateRun.xAdvances);
if (truncateRun.xAdvances.size > 0) run.width += truncateRun.xAdvances.get(0);
}
run.glyphs.addAll(truncateRun.glyphs);
run.width += truncateWidth;
glyphRunPool.free(truncateRun);
}
private GlyphRun wrap (BitmapFontData fontData, GlyphRun first, Pool<GlyphRun> glyphRunPool, int wrapIndex, int widthIndex) {
GlyphRun second = glyphRunPool.obtain();
second.color.set(first.color);
int glyphCount = first.glyphs.size;
// Increase first run width up to the end index.
while (widthIndex < wrapIndex)
first.width += first.xAdvances.get(widthIndex++);
// Reduce first run width by the wrapped glyphs that have contributed to the width.
while (widthIndex > wrapIndex + 1)
first.width -= first.xAdvances.get(--widthIndex);
// Copy wrapped glyphs and xAdvances to second run.
// The second run will contain the remaining glyph data, so swap instances rather than copying to reduce large allocations.
if (wrapIndex < glyphCount) {
Array<Glyph> glyphs1 = second.glyphs; // Starts empty.
Array<Glyph> glyphs2 = first.glyphs; // Starts with all the glyphs.
glyphs1.addAll(glyphs2, 0, wrapIndex);
glyphs2.removeRange(0, wrapIndex - 1);
first.glyphs = glyphs1;
second.glyphs = glyphs2;
// Equivalent to:
// second.glyphs.addAll(first.glyphs, wrapIndex, glyphCount - wrapIndex);
// first.glyphs.truncate(wrapIndex);
FloatArray xAdvances1 = second.xAdvances; // Starts empty.
FloatArray xAdvances2 = first.xAdvances; // Starts with all the xAdvances.
xAdvances1.addAll(xAdvances2, 0, wrapIndex + 1);
xAdvances2.removeRange(1, wrapIndex); // Leave first entry to be overwritten by next line.
xAdvances2.set(0, -glyphs2.first().xoffset * fontData.scaleX - fontData.padLeft);
first.xAdvances = xAdvances1;
second.xAdvances = xAdvances2;
// Equivalent to:
// second.xAdvances.add(-second.glyphs.first().xoffset * fontData.scaleX - fontData.padLeft);
// second.xAdvances.addAll(first.xAdvances, wrapIndex + 1, first.xAdvances.size - (wrapIndex + 1));
// first.xAdvances.truncate(wrapIndex + 1);
}
if (wrapIndex == 0) {
// If the first run is now empty, remove it.
glyphRunPool.free(first);
runs.pop();
} else
adjustLastGlyph(fontData, first);
return second;
}
/** Adjusts the xadvance of the last glyph to use its width instead of xadvance. */
private void adjustLastGlyph (BitmapFontData fontData, GlyphRun run) {
Glyph last = run.glyphs.peek();
if (fontData.isWhitespace((char)last.id)) return; // Can happen when doing truncate.
float width = (last.xoffset + last.width) * fontData.scaleX - fontData.padRight;
run.width += width - run.xAdvances.peek(); // Can cause the run width to be > targetWidth, but the problem is minimal.
run.xAdvances.set(run.xAdvances.size - 1, width);
}
private int parseColorMarkup (CharSequence str, int start, int end, Pool<Color> colorPool) {
if (start == end) return -1; // String ended with "[".
switch (str.charAt(start)) {
case '#':
// Parse hex color RRGGBBAA where AA is optional and defaults to 0xFF if less than 6 chars are used.
int colorInt = 0;
for (int i = start + 1; i < end; i++) {
char ch = str.charAt(i);
if (ch == ']') {
if (i < start + 2 || i > start + 9) break; // Illegal number of hex digits.
if (i - start <= 7) { // RRGGBB or fewer chars.
for (int ii = 0, nn = 9 - (i - start); ii < nn; ii++)
colorInt = colorInt << 4;
colorInt |= 0xff;
}
Color color = colorPool.obtain();
colorStack.add(color);
Color.rgba8888ToColor(color, colorInt);
return i - start;
}
if (ch >= '0' && ch <= '9')
colorInt = colorInt * 16 + (ch - '0');
else if (ch >= 'a' && ch <= 'f')
colorInt = colorInt * 16 + (ch - ('a' - 10));
else if (ch >= 'A' && ch <= 'F')
colorInt = colorInt * 16 + (ch - ('A' - 10));
else
break; // Unexpected character in hex color.
}
return -1;
case '[': // "[[" is an escaped left square bracket.
return -2;
case ']': // "[]" is a "pop" color tag.
if (colorStack.size > 1) colorPool.free(colorStack.pop());
return 0;
}
// Parse named color.
int colorStart = start;
for (int i = start + 1; i < end; i++) {
char ch = str.charAt(i);
if (ch != ']') continue;
Color namedColor = Colors.get(str.subSequence(colorStart, i).toString());
if (namedColor == null) return -1; // Unknown color name.
Color color = colorPool.obtain();
colorStack.add(color);
color.set(namedColor);
return i - start;
}
return -1; // Unclosed color tag.
}
public void reset () {
Pools.get(GlyphRun.class).freeAll(runs);
runs.clear();
width = 0;
height = 0;
}
public String toString () {
if (runs.size == 0) return "";
StringBuilder buffer = new StringBuilder(128);
buffer.append(width);
buffer.append('x');
buffer.append(height);
buffer.append('\n');
for (int i = 0, n = runs.size; i < n; i++) {
buffer.append(runs.get(i).toString());
buffer.append('\n');
}
buffer.setLength(buffer.length() - 1);
return buffer.toString();
}
/** Stores glyphs and positions for a piece of text which is a single color and does not span multiple lines.
* @author Nathan Sweet */
static public class GlyphRun implements Poolable {
public Array<Glyph> glyphs = new Array();
/** Contains glyphs.size+1 entries: First entry is X offset relative to the drawing position. Subsequent entries are the X
* advance relative to previous glyph position. Last entry is the width of the last glyph. */
public FloatArray xAdvances = new FloatArray();
public float x, y, width;
public final Color color = new Color();
public void reset () {
glyphs.clear();
xAdvances.clear();
width = 0;
}
public String toString () {
StringBuilder buffer = new StringBuilder(glyphs.size);
Array<Glyph> glyphs = this.glyphs;
for (int i = 0, n = glyphs.size; i < n; i++) {
Glyph g = glyphs.get(i);
buffer.append((char)g.id);
}
buffer.append(", #");
buffer.append(color);
buffer.append(", ");
buffer.append(x);
buffer.append(", ");
buffer.append(y);
buffer.append(", ");
buffer.append(width);
return buffer.toString();
}
}
}