All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.badlogic.gdx.graphics.g2d.GlyphLayout Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * 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.IntArray;
import com.badlogic.gdx.utils.Null;
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.
 * 

* Where wrapping occurs is determined by {@link BitmapFontData#getWrapIndex(Array, int)}. Additionally, when * {@link BitmapFontData#markupEnabled} is true wrapping can occur at color start or end tags. *

* When wrapping occurs, whitespace is removed before and after the wrap position. Whitespace is determined by * {@link BitmapFontData#isWhitespace(char)}. *

* Glyphs positions are determined by {@link BitmapFontData#getGlyphs(GlyphRun, CharSequence, int, int, Glyph)}. *

* This class is not thread safe, even if synchronized externally, and must only be used from the game thread. * @author Nathan Sweet * @author davebaol * @author Alexander Dorokhov * @author Thomas Creutzenberg */ public class GlyphLayout implements Poolable { static private final Pool glyphRunPool = Pools.get(GlyphRun.class); static private final IntArray colorStack = new IntArray(4); static private final float epsilon = 0.0001f; /** Each run has the glyphs for a line of text. *

* Runs are pooled, so references should not be kept past the next call to * {@link #setText(BitmapFont, CharSequence, int, int, Color, float, int, boolean, String)} or {@link #reset()}. */ public final Array runs = new Array(1); /** Determines the colors of the glpyhs in the {@link #runs}. Entries are pairs where the first is the glyph index (across all * runs) where the color starts and the second is the color encoded as ABGR8888. *

* For example: [0, WHITE, 4, GREEN, 5, WHITE]
* Glpyhs 0 to 3 are WHITE, 4 is GREEN and 5 to the end are WHITE. *

* The array is empty if there are no runs, otherwise it has at least two entries: [0, startColor] */ public final IntArray colors = new IntArray(2); /** Number of glyphs across all {@link #runs}. */ public int glyphCount; public float width, height; /** 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, @Null String truncate) { reset(); BitmapFontData fontData = font.data; if (start == end) { // Empty string. height = fontData.capHeight; return; } // Avoid wrapping one line per character, which is very inefficient. if (wrap) targetWidth = Math.max(targetWidth, fontData.spaceXadvance * 3); boolean wrapOrTruncate = wrap || truncate != null; int currentColor = color.toIntBits(), nextColor = currentColor; colors.add(0, currentColor); boolean markupEnabled = fontData.markupEnabled; if (markupEnabled) colorStack.add(currentColor); boolean isLastRun = false; float y = 0, down = fontData.down; GlyphRun lineRun = null; // Collects glyphs for the current line. Glyph lastGlyph = null; // Last glyph of the previous run on the same line, used for kerning between runs. int runStart = start; outer: while (true) { int runEnd; boolean newline = false; if (start == end) { // End of text. if (runStart == end) break; // No run to process, we're done. runEnd = end; // Process the final run. isLastRun = true; } else { // Each run is delimited by newline or left square bracket. 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); if (length >= 0) { runEnd = start - 1; start += length + 1; if (start == end) isLastRun = true; // Color tag is the last element in the string. else nextColor = colorStack.peek(); break; } if (length == -2) start++; // Skip first of "[[" escape sequence. } // Fall through. default: continue outer; } } runEnded: { // Store the run that has ended. GlyphRun run = glyphRunPool.obtain(); run.x = 0; run.y = y; fontData.getGlyphs(run, str, runStart, runEnd, lastGlyph); glyphCount += run.glyphs.size; if (nextColor != currentColor) { // Can only be different if markupEnabled. if (colors.get(colors.size - 2) == glyphCount) { // Consecutive color changes, or after an empty run, or at the beginning of the string. colors.set(colors.size - 1, nextColor); } else { colors.add(glyphCount); colors.add(nextColor); } currentColor = nextColor; } if (run.glyphs.size == 0) { glyphRunPool.free(run); if (lineRun == null) break runEnded; // Otherwise wrap and truncate must still be processed for lineRun. } else if (lineRun == null) { lineRun = run; runs.add(lineRun); } else { lineRun.appendRun(run); glyphRunPool.free(run); } if (newline || isLastRun) { setLastGlyphXAdvance(fontData, lineRun); lastGlyph = null; } else lastGlyph = lineRun.glyphs.peek(); if (!wrapOrTruncate || lineRun.glyphs.size == 0) break runEnded; // No wrap or truncate, or no glyphs. if (newline || isLastRun) { // Wrap or truncate. First xadvance is the first glyph's X offset relative to the drawing position. float runWidth = lineRun.xAdvances.first() + lineRun.xAdvances.get(1); // At least the first glyph will fit. for (int i = 2; i < lineRun.xAdvances.size; i++) { Glyph glyph = lineRun.glyphs.get(i - 1); float glyphWidth = getGlyphWidth(glyph, fontData); if (runWidth + glyphWidth - epsilon <= targetWidth) { // Glyph fits. runWidth += lineRun.xAdvances.items[i]; continue; } if (truncate != null) { // Truncate. truncate(fontData, lineRun, targetWidth, truncate); break outer; } // Wrap. int wrapIndex = fontData.getWrapIndex(lineRun.glyphs, i); if ((wrapIndex == 0 && lineRun.x == 0) // Require at least one glyph per line. || wrapIndex >= lineRun.glyphs.size) { // Wrap at least the glyph that didn't fit. wrapIndex = i - 1; } lineRun = wrap(fontData, lineRun, wrapIndex); if (lineRun == null) break runEnded; // All wrapped glyphs were whitespace. runs.add(lineRun); y += down; lineRun.x = 0; lineRun.y = y; // Start the wrap loop again, another wrap might be necessary. runWidth = lineRun.xAdvances.first() + lineRun.xAdvances.get(1); // At least the first glyph will fit. i = 1; } } } if (newline) { lineRun = null; lastGlyph = null; // Next run will be on the next line. if (runEnd == runStart) // Blank line. y += down * fontData.blankLineScale; else y += down; } runStart = start; } height = fontData.capHeight + Math.abs(y); calculateWidths(fontData); alignRuns(targetWidth, halign); // Clear the color stack. if (markupEnabled) colorStack.clear(); } /** Calculate run widths and the entire layout width. */ private void calculateWidths (BitmapFontData fontData) { float width = 0; Object[] runsItems = runs.items; for (int i = 0, n = runs.size; i < n; i++) { GlyphRun run = (GlyphRun)runsItems[i]; float[] xAdvances = run.xAdvances.items; float runWidth = run.x + xAdvances[0], max = 0; // run.x is needed to ensure floats are rounded same as above. Object[] glyphs = run.glyphs.items; for (int ii = 0, nn = run.glyphs.size; ii < nn;) { Glyph glyph = (Glyph)glyphs[ii]; float glyphWidth = getGlyphWidth(glyph, fontData); max = Math.max(max, runWidth + glyphWidth); // A glyph can extend past the right edge of subsequent glyphs. ii++; runWidth += xAdvances[ii]; } run.width = Math.max(runWidth, max) - run.x; width = Math.max(width, run.x + run.width); } this.width = width; } /** Align runs to center or right of targetWidth. Requires run.width of runs to be already set */ private void alignRuns (float targetWidth, int halign) { if ((halign & Align.left) == 0) { // Not left aligned, so must be center or right aligned. boolean center = (halign & Align.center) != 0; Object[] runsItems = runs.items; for (int i = 0, n = runs.size; i < n; i++) { GlyphRun run = (GlyphRun)runsItems[i]; run.x += center ? 0.5f * (targetWidth - run.width) : targetWidth - run.width; } } } /** @param truncate May be empty string. */ private void truncate (BitmapFontData fontData, GlyphRun run, float targetWidth, String truncate) { int glyphCount = run.glyphs.size; // Determine truncate string size. GlyphRun truncateRun = glyphRunPool.obtain(); fontData.getGlyphs(truncateRun, truncate, 0, truncate.length(), null); float truncateWidth = 0; if (truncateRun.xAdvances.size > 0) { setLastGlyphXAdvance(fontData, truncateRun); float[] xAdvances = truncateRun.xAdvances.items; for (int i = 1, n = truncateRun.xAdvances.size; i < n; i++) // Skip first for tight bounds. truncateWidth += xAdvances[i]; } targetWidth -= truncateWidth; // Determine visible glyphs. int count = 0; float width = run.x; float[] xAdvances = run.xAdvances.items; while (count < run.xAdvances.size) { float xAdvance = xAdvances[count]; width += xAdvance; if (width > targetWidth) break; count++; } if (count > 1) { // Some run glyphs fit, append truncate glyphs. run.glyphs.truncate(count - 1); run.xAdvances.truncate(count); setLastGlyphXAdvance(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); } int droppedGlyphCount = glyphCount - run.glyphs.size; if (droppedGlyphCount > 0) { this.glyphCount -= droppedGlyphCount; if (fontData.markupEnabled) { while (colors.size > 2 && colors.get(colors.size - 2) >= this.glyphCount) colors.size -= 2; } } run.glyphs.addAll(truncateRun.glyphs); this.glyphCount += truncate.length(); glyphRunPool.free(truncateRun); } /** Breaks a run into two runs at the specified wrapIndex. * @return May be null if second run is all whitespace. */ private GlyphRun wrap (BitmapFontData fontData, GlyphRun first, int wrapIndex) { Array glyphs2 = first.glyphs; // Starts with all the glyphs. int glyphCount = first.glyphs.size; FloatArray xAdvances2 = first.xAdvances; // Starts with all the xadvances. // Skip whitespace before the wrap index. int firstEnd = wrapIndex; for (; firstEnd > 0; firstEnd--) if (!fontData.isWhitespace((char)glyphs2.get(firstEnd - 1).id)) break; // Skip whitespace after the wrap index. int secondStart = wrapIndex; for (; secondStart < glyphCount; secondStart++) if (!fontData.isWhitespace((char)glyphs2.get(secondStart).id)) break; // Copy wrapped glyphs and xadvances to second run. // The second run will contain the remaining glyph data, so swap instances rather than copying. GlyphRun second = null; if (secondStart < glyphCount) { second = glyphRunPool.obtain(); Array glyphs1 = second.glyphs; // Starts empty. glyphs1.addAll(glyphs2, 0, firstEnd); glyphs2.removeRange(0, secondStart - 1); first.glyphs = glyphs1; second.glyphs = glyphs2; FloatArray xAdvances1 = second.xAdvances; // Starts empty. xAdvances1.addAll(xAdvances2, 0, firstEnd + 1); xAdvances2.removeRange(1, secondStart); // Leave first entry to be overwritten by next line. xAdvances2.items[0] = getLineOffset(glyphs2, fontData); first.xAdvances = xAdvances1; second.xAdvances = xAdvances2; int firstGlyphCount = first.glyphs.size; // After wrapping it. int secondGlyphCount = second.glyphs.size; int droppedGlyphCount = glyphCount - firstGlyphCount - secondGlyphCount; this.glyphCount -= droppedGlyphCount; if (fontData.markupEnabled && droppedGlyphCount > 0) { int reductionThreshold = this.glyphCount - secondGlyphCount; for (int i = colors.size - 2; i >= 2; i -= 2) { // i >= 1 because first 2 values always determine the base color. int colorChangeIndex = colors.get(i); if (colorChangeIndex <= reductionThreshold) break; colors.set(i, colorChangeIndex - droppedGlyphCount); } } } else { // Second run is empty, just trim whitespace glyphs from end of first run. glyphs2.truncate(firstEnd); xAdvances2.truncate(firstEnd + 1); int droppedGlyphCount = secondStart - firstEnd; if (droppedGlyphCount > 0) { this.glyphCount -= droppedGlyphCount; if (fontData.markupEnabled && colors.get(colors.size - 2) > this.glyphCount) { // Many color changes can be hidden in the dropped whitespace, so keep only the very last color entry. int lastColor = colors.peek(); while (colors.get(colors.size - 2) > this.glyphCount) colors.size -= 2; colors.set(colors.size - 2, this.glyphCount); // Update color change index. colors.set(colors.size - 1, lastColor); // Update color entry. } } } if (firstEnd == 0) { // If the first run is now empty, remove it. glyphRunPool.free(first); runs.pop(); } else setLastGlyphXAdvance(fontData, first); return second; } /** Sets the xadvance of the last glyph to use its width instead of xadvance. */ private void setLastGlyphXAdvance (BitmapFontData fontData, GlyphRun run) { Glyph last = run.glyphs.peek(); if (!last.fixedWidth) run.xAdvances.items[run.xAdvances.size - 1] = getGlyphWidth(last, fontData); } /** Returns the distance from the glyph's drawing position to the right edge of the glyph. */ private float getGlyphWidth (Glyph glyph, BitmapFontData fontData) { return (glyph.fixedWidth ? glyph.xadvance : glyph.width + glyph.xoffset) * fontData.scaleX - fontData.padRight; } /** Returns an X offset for the first glyph so when drawn, none of it is left of the line's drawing position. */ private float getLineOffset (Array glyphs, BitmapFontData fontData) { Glyph first = glyphs.first(); return (first.fixedWidth ? 0 : -first.xoffset * fontData.scaleX) - fontData.padLeft; } private int parseColorMarkup (CharSequence str, int start, int end) { if (start == end) return -1; // String ended with "[". switch (str.charAt(start)) { case '#': // Parse hex color RRGGBBAA to an ABGR int, where AA is optional and defaults to FF if omitted. int color = 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 < 8) color = color << (9 - (i - start) << 2) | 0xff; // RRGGBB or fewer chars. colorStack.add(Integer.reverseBytes(color)); return i - start; } color = (color << 4) + ch; if (ch >= '0' && ch <= '9') color -= '0'; else if (ch >= 'A' && ch <= 'F') color -= 'A' - 10; else if (ch >= 'a' && ch <= 'f') color -= '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) colorStack.pop(); return 0; } // Parse named color. for (int i = start + 1; i < end; i++) { char ch = str.charAt(i); if (ch != ']') continue; Color color = Colors.get(str.subSequence(start, i).toString()); if (color == null) return -1; // Unknown color name. colorStack.add(color.toIntBits()); return i - start; } return -1; // Unclosed color tag. } public void reset () { glyphRunPool.freeAll(runs); runs.clear(); colors.clear(); glyphCount = 0; 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 line of text. * @author Nathan Sweet */ static public class GlyphRun implements Poolable { public Array glyphs = new Array(); /** Contains glyphs.size+1 entries:
* The first entry is the X offset relative to the drawing position.
* Subsequent entries are the X advance relative to previous glyph position.
* The last entry is the width of the last glyph. */ public FloatArray xAdvances = new FloatArray(); public float x, y, width; void appendRun (GlyphRun run) { glyphs.addAll(run.glyphs); // Remove the width of the last glyph. The first xadvance of the appended run has kerning for the last glyph of this run. if (xAdvances.notEmpty()) xAdvances.size--; xAdvances.addAll(run.xAdvances); } public void reset () { glyphs.clear(); xAdvances.clear(); } public String toString () { StringBuilder buffer = new StringBuilder(glyphs.size + 32); Array 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(x); buffer.append(", "); buffer.append(y); buffer.append(", "); buffer.append(width); return buffer.toString(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy