com.badlogic.gdx.graphics.g2d.BitmapFontCache.java Source code

Java tutorial

Introduction

Here is the source code for com.badlogic.gdx.graphics.g2d.BitmapFontCache.java

Source

/*******************************************************************************
 * 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.graphics.g2d.BitmapFont.HAlignment;
import com.badlogic.gdx.graphics.g2d.BitmapFont.TextBounds;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.NumberUtils;
import com.badlogic.gdx.utils.Pool;
import com.badlogic.gdx.utils.Pool.Poolable;
import com.badlogic.gdx.utils.StringBuilder;

/** Caches glyph geometry for a BitmapFont, providing a fast way to render static text. This saves needing to compute the location
 * of each glyph each frame.
 * @author Nathan Sweet
 * @author Matthias Mann
 * @author davebaol */
public class BitmapFontCache {

    private final BitmapFont font;

    private float[][] vertexData;

    private static final Pool<ColorChunk> colorChunkPool = new Pool<ColorChunk>(32) {
        protected ColorChunk newObject() {
            return new ColorChunk();
        }
    };

    private Array<ColorChunk> colorChunks;

    private int[] idx;
    /** Used internally to ensure a correct capacity for multi-page font vertex data. */
    private int[] tmpGlyphCount;

    private float x, y;
    private float color = Color.WHITE.toFloatBits();
    private final Color tempColor = new Color(1, 1, 1, 1);
    private final Color hexColor = new Color();
    private final StringBuilder colorBuffer = new StringBuilder();
    private final TextBounds textBounds = new TextBounds();
    private boolean integer = true;
    private int glyphCount = 0;

    /** An array for each page containing an entry for each glyph from that page, where the entry is the index of the character in
     * the full text being cached. */
    private IntArray[] glyphIndices;

    private boolean textChanged;
    private float oldTint = 0;
    private final Color currentChunkColor = new Color();
    private int currentChunkEndIndex = 0;

    public BitmapFontCache(BitmapFont font) {
        this(font, font.usesIntegerPositions());
    }

    /** Creates a new BitmapFontCache
     * @param font the font to use
     * @param integer whether to use integer positions and sizes. */
    public BitmapFontCache(BitmapFont font, boolean integer) {
        this.font = font;
        this.integer = integer;

        int regionsLength = font.regions.length;
        if (regionsLength == 0)
            throw new IllegalArgumentException("The specified font must contain at least 1 texture page");

        this.vertexData = new float[regionsLength][];
        this.colorChunks = new Array<ColorChunk>();

        this.idx = new int[regionsLength];
        int vertexDataLength = vertexData.length;
        if (vertexDataLength > 1) { // if we have multiple pages...
            // contains the indices of the glyph in the Cache as they are added
            glyphIndices = new IntArray[vertexDataLength];
            for (int i = 0, n = glyphIndices.length; i < n; i++) {
                glyphIndices[i] = new IntArray();
            }

            tmpGlyphCount = new int[vertexDataLength];
        }
    }

    /** Sets the position of the text, relative to the position when the cached text was created.
     * @param x The x coordinate
     * @param y The y coordinate */
    public void setPosition(float x, float y) {
        translate(x - this.x, y - this.y);
    }

    /** Sets the position of the text, relative to its current position.
     * @param xAmount The amount in x to move the text
     * @param yAmount The amount in y to move the text */
    public void translate(float xAmount, float yAmount) {
        if (xAmount == 0 && yAmount == 0)
            return;
        if (integer) {
            xAmount = Math.round(xAmount);
            yAmount = Math.round(yAmount);
        }
        x += xAmount;
        y += yAmount;

        for (int j = 0, length = vertexData.length; j < length; j++) {
            float[] vertices = vertexData[j];
            for (int i = 0, n = idx[j]; i < n; i += 5) {
                vertices[i] += xAmount;
                vertices[i + 1] += yAmount;
            }
        }
    }

    private Color setColor(Color color, float floatColor) {
        int intBits = NumberUtils.floatToIntColor(floatColor);
        color.r = (intBits & 0xff) / 255f;
        color.g = ((intBits >>> 8) & 0xff) / 255f;
        color.b = ((intBits >>> 16) & 0xff) / 255f;
        color.a = ((intBits >>> 24) & 0xff) / 255f;
        return color;
    }

    private int updateCurrentChunk(int lastChunkIndex, Color tint) {
        lastChunkIndex++;
        if (colorChunks.size <= lastChunkIndex) {
            if (colorChunks.size <= 0)
                currentChunkColor.set(tint);
            currentChunkEndIndex = Integer.MAX_VALUE;
        } else {
            ColorChunk cc = colorChunks.get(lastChunkIndex);
            if (currentChunkEndIndex == cc.endIndex)
                lastChunkIndex = updateCurrentChunk(lastChunkIndex, tint);
            else {
                setColor(currentChunkColor, cc.color).mul(tint);
                currentChunkEndIndex = cc.endIndex;
            }
        }
        return lastChunkIndex;
    }

    /** Tints all text currently in the cache. Does not affect subsequently added text. */
    public void tint(Color tint) {
        float floatTint = tint.toFloatBits();
        if (textChanged || oldTint != floatTint) {
            textChanged = false;
            oldTint = floatTint;
            if (font.markupEnabled) {
                int lastChunkIndex = updateCurrentChunk(-1, tint);
                float color = currentChunkColor.toFloatBits();
                int ci = 0; // character index
                for (int j = 0, length = vertexData.length; j < length; j++) {
                    float[] vertices = vertexData[j];
                    for (int i = 2, n = idx[j]; i < n; i += 5) {
                        if ((i % 20) == 2) {
                            if (++ci > currentChunkEndIndex) {
                                lastChunkIndex = updateCurrentChunk(lastChunkIndex, tint);
                                color = currentChunkColor.toFloatBits();
                            }
                        }
                        vertices[i] = color;
                    }
                }
            } else {
                for (int j = 0, length = vertexData.length; j < length; j++) {
                    float[] vertices = vertexData[j];
                    for (int i = 2, n = idx[j]; i < n; i += 5) {
                        vertices[i] = floatTint;
                    }
                }
            }
        }
    }

    /** Sets the color of all text currently in the cache. Does not affect subsequently added text. */
    public void setColors(float color) {
        for (int j = 0, length = vertexData.length; j < length; j++) {
            float[] vertices = vertexData[j];
            for (int i = 2, n = idx[j]; i < n; i += 5)
                vertices[i] = color;
        }
    }

    /** Sets the color of all text currently in the cache. Does not affect subsequently added text. */
    public void setColors(Color tint) {
        final float color = tint.toFloatBits();
        for (int j = 0, length = vertexData.length; j < length; j++) {
            float[] vertices = vertexData[j];
            for (int i = 2, n = idx[j]; i < n; i += 5)
                vertices[i] = color;
        }
    }

    /** Sets the color of all text currently in the cache. Does not affect subsequently added text. */
    public void setColors(float r, float g, float b, float a) {
        int intBits = ((int) (255 * a) << 24) | ((int) (255 * b) << 16) | ((int) (255 * g) << 8)
                | ((int) (255 * r));
        float color = NumberUtils.intToFloatColor(intBits);
        for (int j = 0, length = vertexData.length; j < length; j++) {
            float[] vertices = vertexData[j];
            for (int i = 2, n = idx[j]; i < n; i += 5)
                vertices[i] = color;
        }
    }

    /** Sets the color of the specified characters. This may only be called after {@link #setText(CharSequence, float, float)} and
     * is reset every time setText is called. */
    public void setColors(Color tint, int start, int end) {
        final float color = tint.toFloatBits();

        if (vertexData.length == 1) { // only one page...
            float[] vertices = vertexData[0];
            for (int i = start * 20 + 2, n = end * 20; i < n; i += 5)
                vertices[i] = color;
        } else {
            int pageCount = vertexData.length;

            // for each page...
            for (int i = 0; i < pageCount; i++) {
                float[] vertices = vertexData[i];

                // we need to loop through the indices and determine whether the glyph is inside begin/end
                for (int j = 0, n = glyphIndices[i].size; j < n; j++) {
                    int gInd = glyphIndices[i].items[j];

                    // break early if the glyph is outside our bounds
                    if (gInd >= end)
                        break;

                    // if the glyph is inside start and end, then change it's colour
                    if (gInd >= start) { // && gInd < end
                        // modify color index
                        for (int off = 0; off < 20; off += 5)
                            vertices[off + (j * 20 + 2)] = color;
                    }
                }
            }

        }
    }

    /** Sets the color of subsequently added text. Does not affect text currently in the cache. */
    public void setColor(Color tint) {
        color = tint.toFloatBits();
    }

    /** Sets the color of subsequently added text. Does not affect text currently in the cache. */
    public void setColor(float r, float g, float b, float a) {
        int intBits = (int) (255 * a) << 24 | (int) (255 * b) << 16 | (int) (255 * g) << 8 | (int) (255 * r);
        color = NumberUtils.intToFloatColor(intBits);
    }

    /** Sets the color of subsequently added text. Does not affect text currently in the cache. */
    public void setColor(float color) {
        this.color = color;
    }

    public Color getColor() {
        int intBits = NumberUtils.floatToIntColor(color);
        Color color = tempColor;
        color.r = (intBits & 0xff) / 255f;
        color.g = ((intBits >>> 8) & 0xff) / 255f;
        color.b = ((intBits >>> 16) & 0xff) / 255f;
        color.a = ((intBits >>> 24) & 0xff) / 255f;
        return color;
    }

    public void draw(Batch spriteBatch) {
        TextureRegion[] regions = font.getRegions();
        for (int j = 0, n = vertexData.length; j < n; j++) {
            if (idx[j] > 0) { // ignore if this texture has no glyphs
                float[] vertices = vertexData[j];
                spriteBatch.draw(regions[j].getTexture(), vertices, 0, idx[j]);
            }
        }
    }

    public void draw(Batch spriteBatch, int start, int end) {
        if (vertexData.length == 1) { // i.e. 1 page
            spriteBatch.draw(font.getRegion().getTexture(), vertexData[0], start * 20, (end - start) * 20);
        } else { // i.e. multiple pages
            // TODO: bounds check?

            // We basically need offset and len for each page
            // Different pages might have different offsets and lengths
            // Some pages might not need to be rendered at all..

            TextureRegion[] regions = font.getRegions();

            // for each page...
            for (int i = 0, pageCount = vertexData.length; i < pageCount; i++) {

                int offset = -1;
                int count = 0;

                // we need to loop through the indices and determine where we begin within the start/end bounds
                IntArray currentGlyphIndices = glyphIndices[i];
                for (int j = 0, n = currentGlyphIndices.size; j < n; j++) {
                    int glyphIndex = currentGlyphIndices.items[j];

                    // break early if the glyph is outside our bounds
                    if (glyphIndex >= end)
                        break;

                    // determine if this glyph is "inside" our start/end bounds
                    // if so; use the first match of that for the offset
                    if (offset == -1 && glyphIndex >= start)
                        offset = j;

                    // we also need to determine the length of our vertices array...
                    // we do so by counting the glyphs within our bounds
                    if (glyphIndex >= start) // && gInd < end
                        count++;
                }

                // this page isn't necessary to be rendered
                if (offset == -1 || count == 0)
                    continue;

                // render the page vertex data with our determined offset and length
                spriteBatch.draw(regions[i].getTexture(), vertexData[i], offset * 20, count * 20);
            }
        }
    }

    public void draw(Batch spriteBatch, float alphaModulation) {
        if (alphaModulation == 1) {
            draw(spriteBatch);
            return;
        }
        Color color = getColor();
        float oldAlpha = color.a;
        color.a *= alphaModulation;
        setColors(color);
        draw(spriteBatch);
        color.a = oldAlpha;
        setColors(color);
    }

    /** Removes all glyphs in the cache. */
    public void clear() {
        x = 0;
        y = 0;
        glyphCount = 0;
        for (int i = 0, n = idx.length; i < n; i++) {
            if (glyphIndices != null)
                glyphIndices[i].clear();
            idx[i] = 0;
        }

        // Remove all the color chunks from the list and releases them to the internal pool
        for (int i = 0; i < colorChunks.size; i++) {
            colorChunkPool.free(colorChunks.get(i));
            colorChunks.set(i, null);
        }
        colorChunks.size = 0;
    }

    /** Counts the actual glyphs excluding characters used to markup the text. */
    private int countGlyphs(CharSequence seq, int start, int end) {
        int count = end - start;
        while (start < end) {
            char ch = seq.charAt(start++);
            if (ch == '[') {
                count--;
                if (!(start < end && seq.charAt(start) == '[')) { // non escaped '['
                    while (start < end && seq.charAt(start) != ']') {
                        start++;
                        count--;
                    }
                    count--;
                }
                start++;
            }
        }
        return count;
    }

    private void requireSequence(CharSequence seq, int start, int end) {
        if (vertexData.length == 1) {
            // don't scan sequence if we just have one page and markup is disabled
            int newGlyphCount = font.markupEnabled ? countGlyphs(seq, start, end) : end - start;
            require(0, newGlyphCount);
        } else {
            for (int i = 0, n = tmpGlyphCount.length; i < n; i++)
                tmpGlyphCount[i] = 0;

            // determine # of glyphs in each page
            while (start < end) {
                char ch = seq.charAt(start++);
                if (ch == '[' && font.markupEnabled) {
                    if (!(start < end && seq.charAt(start) == '[')) { // non escaped '['
                        while (start < end && seq.charAt(start) != ']')
                            start++;
                        start++;
                        continue;
                    }
                    start++;
                }
                Glyph g = font.data.getGlyph(ch);
                if (g == null)
                    continue;
                tmpGlyphCount[g.page]++;
            }
            // require that many for each page
            for (int i = 0, n = tmpGlyphCount.length; i < n; i++)
                require(i, tmpGlyphCount[i]);
        }
    }

    private void require(int page, int glyphCount) {
        if (glyphIndices != null) {
            if (glyphCount > glyphIndices[page].items.length)
                glyphIndices[page].ensureCapacity(glyphCount - glyphIndices[page].items.length);
        }

        int vertexCount = idx[page] + glyphCount * 20;
        float[] vertices = vertexData[page];
        if (vertices == null) {
            vertexData[page] = new float[vertexCount];
        } else if (vertices.length < vertexCount) {
            float[] newVertices = new float[vertexCount];
            System.arraycopy(vertices, 0, newVertices, 0, idx[page]);
            vertexData[page] = newVertices;
        }
    }

    private int parseAndSetColor(CharSequence str, int start, int end) {
        if (start < end) {
            if (str.charAt(start) == '#') {
                // 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)
                            throw new GdxRuntimeException("Hex color cannot have " + (i - start - 1) + " digits");
                        if (i <= start + 7) { // RRGGBB
                            Color.rgb888ToColor(hexColor, colorInt);
                            hexColor.a = 1f;
                        } else { // RRGGBBAA
                            Color.rgba8888ToColor(hexColor, colorInt);
                        }
                        this.color = hexColor.toFloatBits();
                        addColorChunk(this.color, false);
                        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
                        throw new GdxRuntimeException("Unexpected '" + ch + "' in hex color");
                }
            } else {
                // Parse named color
                colorBuffer.setLength(0);
                for (int i = start; i < end; i++) {
                    char ch = str.charAt(i);
                    if (ch == ']') {
                        if (colorBuffer.length() == 0) { // end tag []
                            int popIndex = Math.max(0, colorChunks.peek().popIndex);
                            this.color = colorChunks.get(popIndex).color;
                            addColorChunk(this.color, true);
                        } else {
                            String colorString = colorBuffer.toString();
                            Color newColor = Colors.get(colorString);
                            if (newColor == null)
                                throw new GdxRuntimeException("Unknown color '" + colorString + "'");
                            this.color = newColor.toFloatBits();
                            addColorChunk(this.color, false);
                        }
                        return i - start;
                    } else {
                        colorBuffer.append(ch);
                    }
                }
            }
        }
        throw new GdxRuntimeException("Unclosed color tag");
    }

    private void addColorChunk(float color, boolean isPop) {
        int popIndex = -1;
        if (colorChunks.size > 0) {
            ColorChunk last = colorChunks.peek();
            last.endIndex = (glyphIndices != null ? glyphCount : this.idx[0] / 20);
            if (isPop)
                popIndex = colorChunks.get(Math.max(0, last.popIndex)).popIndex;
            else
                popIndex = colorChunks.size - 1;
        }
        colorChunks.add(obtainColorChunk(color, popIndex));
    }

    private ColorChunk obtainColorChunk(float color, int popIndex) {
        // Get a color chunk from the pool
        ColorChunk colorChunk = colorChunkPool.obtain();
        colorChunk.color = color;
        colorChunk.popIndex = popIndex;
        return colorChunk;
    }

    private float addToCache(CharSequence str, float x, float y, int start, int end) {
        float startX = x;
        BitmapFont font = this.font;
        Glyph lastGlyph = null;
        BitmapFontData data = font.data;
        textChanged = start < end;
        if (font.markupEnabled && colorChunks.size == 0)
            colorChunks.add(obtainColorChunk(this.color, -1));
        if (data.scaleX == 1 && data.scaleY == 1) {
            while (start < end) {
                char ch = str.charAt(start++);
                if (ch == '[' && font.markupEnabled) {
                    if (!(start < end && str.charAt(start) == '[')) { // non escaped '['
                        start += parseAndSetColor(str, start, end) + 1;
                        continue;
                    }
                    start++;
                }
                lastGlyph = data.getGlyph(ch);
                if (lastGlyph != null) {
                    addGlyph(lastGlyph, x + lastGlyph.xoffset, y + lastGlyph.yoffset, lastGlyph.width,
                            lastGlyph.height);
                    x += lastGlyph.xadvance;
                    break;
                }
            }
            while (start < end) {
                char ch = str.charAt(start++);
                if (ch == '[' && font.markupEnabled) {
                    if (!(start < end && str.charAt(start) == '[')) { // non escaped '['
                        start += parseAndSetColor(str, start, end) + 1;
                        continue;
                    }
                    start++;
                }
                Glyph g = data.getGlyph(ch);
                if (g != null) {
                    x += lastGlyph.getKerning(ch);
                    lastGlyph = g;
                    addGlyph(lastGlyph, x + g.xoffset, y + g.yoffset, g.width, g.height);
                    x += g.xadvance;
                }
            }
        } else {
            float scaleX = data.scaleX, scaleY = data.scaleY;
            while (start < end) {
                char ch = str.charAt(start++);
                if (ch == '[' && font.markupEnabled) {
                    if (!(start < end && str.charAt(start) == '[')) { // non escaped '['
                        start += parseAndSetColor(str, start, end) + 1;
                        continue;
                    }
                    start++;
                }
                lastGlyph = data.getGlyph(ch);
                if (lastGlyph != null) {
                    addGlyph(lastGlyph, //
                            x + lastGlyph.xoffset * scaleX, //
                            y + lastGlyph.yoffset * scaleY, //
                            lastGlyph.width * scaleX, //
                            lastGlyph.height * scaleY);
                    x += lastGlyph.xadvance * scaleX;
                    break;
                }
            }
            while (start < end) {
                char ch = str.charAt(start++);
                if (ch == '[' && font.markupEnabled) {
                    if (!(start < end && str.charAt(start) == '[')) { // non escaped '['
                        start += parseAndSetColor(str, start, end) + 1;
                        continue;
                    }
                    start++;
                }
                Glyph g = data.getGlyph(ch);
                if (g != null) {
                    x += lastGlyph.getKerning(ch) * scaleX;
                    lastGlyph = g;
                    addGlyph(lastGlyph, //
                            x + g.xoffset * scaleX, //
                            y + g.yoffset * scaleY, //
                            g.width * scaleX, //
                            g.height * scaleY);
                    x += g.xadvance * scaleX;
                }
            }
        }
        return x - startX;
    }

    private void addGlyph(Glyph glyph, float x, float y, float width, float height) {
        float x2 = x + width;
        float y2 = y + height;
        final float u = glyph.u;
        final float u2 = glyph.u2;
        final float v = glyph.v;
        final float v2 = glyph.v2;

        final int page = glyph.page;

        if (glyphIndices != null) {
            glyphIndices[page].add(glyphCount++);
        }

        final float[] vertices = vertexData[page];

        if (integer) {
            x = Math.round(x);
            y = Math.round(y);
            x2 = Math.round(x2);
            y2 = Math.round(y2);
        }

        int idx = this.idx[page];
        this.idx[page] += 20;

        vertices[idx++] = x;
        vertices[idx++] = y;
        vertices[idx++] = color;
        vertices[idx++] = u;
        vertices[idx++] = v;

        vertices[idx++] = x;
        vertices[idx++] = y2;
        vertices[idx++] = color;
        vertices[idx++] = u;
        vertices[idx++] = v2;

        vertices[idx++] = x2;
        vertices[idx++] = y2;
        vertices[idx++] = color;
        vertices[idx++] = u2;
        vertices[idx++] = v2;

        vertices[idx++] = x2;
        vertices[idx++] = y;
        vertices[idx++] = color;
        vertices[idx++] = u2;
        vertices[idx] = v;
    }

    /** Clears any cached glyphs and adds glyphs for the specified text.
     * @see #addText(CharSequence, float, float, int, int) */
    public TextBounds setText(CharSequence str, float x, float y) {
        clear();
        return addText(str, x, y, 0, str.length());
    }

    /** Clears any cached glyphs and adds glyphs for the specified text.
     * @see #addText(CharSequence, float, float, int, int) */
    public TextBounds setText(CharSequence str, float x, float y, int start, int end) {
        clear();
        return addText(str, x, y, start, end);
    }

    /** Adds glyphs for the specified text.
     * @see #addText(CharSequence, float, float, int, int) */
    public TextBounds addText(CharSequence str, float x, float y) {
        return addText(str, x, y, 0, str.length());
    }

    /** Adds glyphs for the the specified text.
     * @param x The x position for the left most character.
     * @param y The y position for the top of most capital letters in the font (the {@link BitmapFont#getCapHeight() cap height}).
     * @param start The first character of the string to draw.
     * @param end The last character of the string to draw (exclusive).
     * @return The bounds of the cached string (the height is the distance from y to the baseline). */
    public TextBounds addText(CharSequence str, float x, float y, int start, int end) {
        requireSequence(str, start, end);
        y += font.data.ascent;
        textBounds.width = addToCache(str, x, y, start, end);
        textBounds.height = font.data.capHeight;
        return textBounds;
    }

    /** Clears any cached glyphs and adds glyphs for the specified text, which may contain newlines (\n).
     * @see #addMultiLineText(CharSequence, float, float, float, HAlignment) */
    public TextBounds setMultiLineText(CharSequence str, float x, float y) {
        clear();
        return addMultiLineText(str, x, y, 0, HAlignment.LEFT);
    }

    /** Clears any cached glyphs and adds glyphs for the specified text, which may contain newlines (\n).
     * @see #addMultiLineText(CharSequence, float, float, float, HAlignment) */
    public TextBounds setMultiLineText(CharSequence str, float x, float y, float alignmentWidth,
            HAlignment alignment) {
        clear();
        return addMultiLineText(str, x, y, alignmentWidth, alignment);
    }

    /** Adds glyphs for the specified text, which may contain newlines (\n).
     * @see #addMultiLineText(CharSequence, float, float, float, HAlignment) */
    public TextBounds addMultiLineText(CharSequence str, float x, float y) {
        return addMultiLineText(str, x, y, 0, HAlignment.LEFT);
    }

    /** Adds glyphs for the specified text, which may contain newlines (\n). Each line is aligned horizontally within a rectangle of
     * the specified width.
     * @param x The x position for the left most character.
     * @param y The y position for the top of most capital letters in the font (the {@link BitmapFont#getCapHeight() cap height}).
     * @param alignment The horizontal alignment of wrapped line.
     * @return The bounds of the cached string (the height is the distance from y to the baseline of the last line). */
    public TextBounds addMultiLineText(CharSequence str, float x, float y, float alignmentWidth,
            HAlignment alignment) {
        BitmapFont font = this.font;

        int length = str.length();
        requireSequence(str, 0, length);

        y += font.data.ascent;
        float down = font.data.down;

        float maxWidth = 0;
        float startY = y;
        int start = 0;
        int numLines = 0;
        while (start < length) {
            int lineEnd = BitmapFont.indexOf(str, '\n', start);
            float xOffset = 0;
            if (alignment != HAlignment.LEFT) {
                float lineWidth = font.getBounds(str, start, lineEnd).width;
                xOffset = alignmentWidth - lineWidth;
                if (alignment == HAlignment.CENTER)
                    xOffset /= 2;
            }
            float lineWidth = addToCache(str, x + xOffset, y, start, lineEnd);
            maxWidth = Math.max(maxWidth, lineWidth);
            start = lineEnd + 1;
            y += down;
            numLines++;
        }
        textBounds.width = maxWidth;
        textBounds.height = font.data.capHeight + (numLines - 1) * font.data.lineHeight;
        return textBounds;
    }

    /** Clears any cached glyphs and adds glyphs for the specified text, which may contain newlines (\n) and is automatically
     * wrapped within the specified width.
     * @see #addWrappedText(CharSequence, float, float, float, HAlignment) */
    public TextBounds setWrappedText(CharSequence str, float x, float y, float wrapWidth) {
        clear();
        return addWrappedText(str, x, y, wrapWidth, HAlignment.LEFT);
    }

    /** Clears any cached glyphs and adds glyphs for the specified text, which may contain newlines (\n) and is automatically
     * wrapped within the specified width.
     * @see #addWrappedText(CharSequence, float, float, float, HAlignment) */
    public TextBounds setWrappedText(CharSequence str, float x, float y, float wrapWidth, HAlignment alignment) {
        clear();
        return addWrappedText(str, x, y, wrapWidth, alignment);
    }

    /** Adds glyphs for the specified text, which may contain newlines (\n) and is automatically wrapped within the specified width.
     * @see #addWrappedText(CharSequence, float, float, float, HAlignment) */
    public TextBounds addWrappedText(CharSequence str, float x, float y, float wrapWidth) {
        return addWrappedText(str, x, y, wrapWidth, HAlignment.LEFT);
    }

    /** Adds glyphs for the specified text, which may contain newlines (\n) and is automatically wrapped within the specified width.
     * @param x The x position for the left most character.
     * @param y The y position for the top of most capital letters in the font (the {@link BitmapFont#getCapHeight() cap height}).
     * @param alignment The horizontal alignment of wrapped line.
     * @return The bounds of the cached string (the height is the distance from y to the baseline of the last line). */
    public TextBounds addWrappedText(CharSequence str, float x, float y, float wrapWidth, HAlignment alignment) {
        BitmapFont font = this.font;

        int length = str.length();
        requireSequence(str, 0, length);

        y += font.data.ascent;
        float down = font.data.down;

        if (wrapWidth <= 0)
            wrapWidth = Integer.MAX_VALUE;
        float maxWidth = 0;
        int start = 0;
        int numLines = 0;
        while (start < length) {
            int newLine = BitmapFont.indexOf(str, '\n', start);
            // Eat whitespace at start of line.
            while (start < newLine) {
                if (!BitmapFont.isWhitespace(str.charAt(start)))
                    break;
                start++;
            }
            int lineEnd = start + font.computeVisibleGlyphs(str, start, newLine, wrapWidth);
            int nextStart = lineEnd + 1;
            if (lineEnd < newLine) {
                // Find char to break on.
                while (lineEnd > start) {
                    if (BitmapFont.isWhitespace(str.charAt(lineEnd)))
                        break;
                    lineEnd--;
                }
                if (lineEnd == start) {
                    if (nextStart > start + 1)
                        nextStart--;
                    lineEnd = nextStart; // If no characters to break, show all.
                } else {
                    nextStart = lineEnd;
                    // Eat whitespace at end of line.
                    while (lineEnd > start) {
                        if (!BitmapFont.isWhitespace(str.charAt(lineEnd - 1)))
                            break;
                        lineEnd--;
                    }
                }
            }
            if (lineEnd > start) {
                float xOffset = 0;
                if (alignment != HAlignment.LEFT) {
                    float lineWidth = font.getBounds(str, start, lineEnd).width;
                    xOffset = wrapWidth - lineWidth;
                    if (alignment == HAlignment.CENTER)
                        xOffset /= 2;
                }
                float lineWidth = addToCache(str, x + xOffset, y, start, lineEnd);
                maxWidth = Math.max(maxWidth, lineWidth);
            }
            start = nextStart;
            y += down;
            numLines++;
        }
        textBounds.width = maxWidth;
        textBounds.height = font.data.capHeight + (numLines - 1) * font.data.lineHeight;
        return textBounds;
    }

    /** Returns the size of the cached string. The height is the distance from the top of most capital letters in the font (the
     * {@link BitmapFont#getCapHeight() cap height}) to the baseline of the last line of text. */
    public TextBounds getBounds() {
        return textBounds;
    }

    /** Returns the x position of the cached string, relative to the position when the string was cached. */
    public float getX() {
        return x;
    }

    /** Returns the y position of the cached string, relative to the position when the string was cached. */
    public float getY() {
        return y;
    }

    public BitmapFont getFont() {
        return font;
    }

    /** Specifies whether to use integer positions or not. Default is to use them so filtering doesn't kick in as badly.
     * @param use */
    public void setUseIntegerPositions(boolean use) {
        this.integer = use;
    }

    /** @return whether this font uses integer positions for drawing. */
    public boolean usesIntegerPositions() {
        return integer;
    }

    public float[] getVertices() {
        return getVertices(0);
    }

    public float[] getVertices(int page) {
        return vertexData[page];
    }

    private static class ColorChunk implements Poolable {
        float color;
        int endIndex;
        int popIndex; // needed to emulate the color stack

        ColorChunk() {
            this.endIndex = Integer.MAX_VALUE;
        }

        ColorChunk(float color, int popIndex) {
            this.color = color;
            this.endIndex = Integer.MAX_VALUE;
            this.popIndex = popIndex;
        }

        @Override
        public void reset() {
            this.color = 0f;
            this.endIndex = Integer.MAX_VALUE;
            this.popIndex = 0;
        }
    }
}