Java tutorial
/* * Minecraft OpenType Font Support Mod * * Copyright (C) 2012 Wojciech Stryjewski <thvortex@gmail.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This program 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>.. */ package cheatingessentials.mod.internal.ttf; import java.awt.Font; import java.awt.Point; import java.awt.font.GlyphVector; import java.lang.ref.WeakReference; import java.text.Bidi; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.WeakHashMap; import net.minecraft.client.renderer.Tessellator; import org.lwjgl.opengl.GL11; /** * The StringCache is the public interface for rendering of all Unicode strings * using OpenType fonts. It caches the glyph layout of individual strings, and * it uses a GlyphCache instance to cache the pre-rendered images for individual * glyphs. Once a string and its glyph images are cached, the critical path in * renderString() will draw the glyphs as fast as if using a bitmap font. * Strings are cached using weak references through a two layer string cache. * Strings that are no longer in use by Minecraft will be evicted from the * cache, while the pre-rendered images of individual glyphs remains cached * forever. The following diagram illustrates how this works: * <p/> * * <pre> * String passed to Key object considers Entry object holds Each Glyph object GlyphCache * .Entry stores * renderString(); all ASCII digits equal an array of Glyph belongs to only one the texture * ID, image * mapped by weak to zero ('0'); objects which may Entry object; it has width/height * and * weakRefCache mapped by weak not directly the glyph x/y pos normalized * texture * to Key object stringCahe to Entry correspond to Unicode within the string coordinates. * chars in string * String("Fi1") ------------\ ---> Glyph("F") ----------> GlyphCache * .Entry("F") * N:1 \ 1:1 1:N / N:1 * String("Fi4") ------------> Key("Fi0") -------------> Entry("Fi0") -----+----> Glyph("i") ----------> GlyphCache * .Entry("i") * \ N:1 * ---> Glyph("0") -----\ * ----> GlyphCache * .Entry("0") * ---> Glyph("0") -----/ * N:1 1:1 1:N / N:1 * String("Be1") ------------> Key("Be0") -------------> Entry("Be0") -----+----> Glyph("e") ----------> GlyphCache * .Entry("e") * \ N:1 * ---> Glyph("B") ----------> GlyphCache * .Entry("B") * </pre> */ public class StringCache { /** * Vertical adjustment (in pixels * 2) to string position because Minecraft * uses top of string instead of baseline */ private static final int BASELINE_OFFSET = 7; /** * Offset from the string's baseline as which to draw the underline (in * pixels) */ private static final int UNDERLINE_OFFSET = 1; /** * Thickness of the underline (in pixels) */ private static final int UNDERLINE_THICKNESS = 2; /** * Offset from the string's baseline as which to draw the strikethrough line * (in pixels) */ private static final int STRIKETHROUGH_OFFSET = -6; /** * Thickness of the strikethrough line (in pixels) */ private static final int STRIKETHROUGH_THICKNESS = 2; /** * Reference to the unicode.FontRenderer class. Needed for creating * GlyphVectors and retrieving glyph texture coordinates. */ private final GlyphCache glyphCache; /** * Color codes from original FontRender class. First 16 entries are the * primary chat colors; second 16 are darker versions used for drop shadows. */ private final int colorTable[]; /** * A cache of recently seen strings to their fully layed-out state, complete * with color changes and texture coordinates of all pre-rendered glyph * images needed to display this string. The weakRefCache holds strong * references to the Key objects used in this map. */ private final WeakHashMap<Key, Entry> stringCache = new WeakHashMap(); /** * Every String passed to the public renderString() function is added to * this WeakHashMap. As long as As long as Minecraft continues to hold a * strong reference to the String object (i.e. from TileEntitySign and * ChatLine) passed here, the weakRefCache map will continue to hold a * strong reference to the Key object that said strings all map to (multiple * strings in weakRefCache can map to a single Key if those strings only * differ by their ASCII digits). */ private final WeakHashMap<String, Key> weakRefCache = new WeakHashMap(); /** * Temporary Key object re-used for lookups with stringCache.get(). Using a * temporary object like this avoids the overhead of allocating new objects * in the critical rendering path. Of course, new Key objects are always * created when adding a mapping to stringCache. */ private final Key lookupKey = new Key(); /** * Pre-cached glyphs for the ASCII digits 0-9 (in that order). Used by * renderString() to substiture digit glyphs on the fly as a performance * boost. The speed up is most noticable on the F3 screen which rapidly * displays lots of changing numbers. The 4 element array is index by the * font style (combination of Font.PLAIN, Font.BOLD, and Font.ITALIC), and * each of the nested elements is index by the digit value 0-9. */ private final Glyph[][] digitGlyphs = new Glyph[4][]; /** * True if digitGlyphs[] has been assigned and cacheString() can begin * replacing all digits with '0' in the string. */ private boolean digitGlyphsReady = false; /** * If true, then enble GL_BLEND in renderString() so anti-aliasing font * glyphs show up properly. */ private boolean antiAliasEnabled = false; /** * Reference to the main Minecraft thread that created this GlyphCache * object. Starting with Minecraft 1.3.1, it is possible for * GlyphCache.cacheGlyphs() to be invoked from the TcpReaderThread while * processing a chat packet and computing the width of the incoming chat * text. Unfortunately, if cacheGlyphs() makes any OpenGL calls from any * thread except the main one, it will crash LWJGL with a * NullPointerException. By remembering the initial thread and comparing it * later against Thread.currentThread(), the StringCache code can avoid * calling cacheGlyphs() when it's not safe to do so. */ private final Thread mainThread; /** * Wraps a String and acts as the key into stringCache. The hashCode() and * equals() methods consider all ASCII digits to be equal when hashing and * comparing Key objects together. Therefore, Strings which only differ in * their digits will be all hashed together into the same entry. The * renderString() method will then substitute the correct digit glyph on the * fly. This special digit handling gives a significant speedup on the F3 * debug screen. */ static private class Key { /** * A copy of the String which this Key is indexing. A copy is used to * avoid creating a strong reference to the original passed into * renderString(). When the original String is no longer needed by * Minecraft, it will be garbage collected and the WeakHashMaps in * StringCache will allow this Key object and its associated Entry * object to be garbage collected as well. */ public String str; /** * Computes a hash code on str in the same manner as the String class, * except all ASCII digits hash as '0' * * @return the augmented hash code on str */ @Override public int hashCode() { int code = 0; final int length = str.length(); /* * True if a section mark character was last seen. In this case, if * the next character is a digit, it must not be considered equal to * any other digit. This forces any string that differs in color * codes only to have a separate entry in the StringCache. */ boolean colorCode = false; for (int index = 0; index < length; index++) { char c = str.charAt(index); if ((c >= '0') && (c <= '9') && !colorCode) { c = '0'; } code = (code * 31) + c; colorCode = (c == '\u00A7'); } return code; } /** * Compare str against another object (specifically, the object's string * representation as returned by toString). All ASCII digits are * considered equal by this method, as long as they are at the same * index within the string. * * @return true if the strings are the identical, or only differ in * their ASCII digits */ @Override public boolean equals(final Object o) { /* * There seems to be a timing window inside WeakHashMap itself where * a null object can be passed to this equals() method. Presumably * it happens between computing a hash code for the weakly * referenced Key object while it still exists and calling its * equals() method after it was garbage collected. */ if (o == null) { return false; } /* * Calling toString on a String object simply returns itself so no * new object allocation is performed */ final String other = o.toString(); final int length = str.length(); if (length != other.length()) { return false; } /* * True if a section mark character was last seen. In this case, if * the next character is a digit, it must not be considered equal to * any other digit. This forces any string that differs in color * codes only to have a separate entry in the StringCache. */ boolean colorCode = false; for (int index = 0; index < length; index++) { final char c1 = str.charAt(index); final char c2 = other.charAt(index); if ((c1 != c2) && ((c1 < '0') || (c1 > '9') || (c2 < '0') || (c2 > '9') || colorCode)) { return false; } colorCode = (c1 == '\u00A7'); } return true; } /** * Returns the contained String object within this Key. * * @return the str object */ @Override public String toString() { return str; } } /** * This entry holds the layed out glyph positions for the cached string * along with some relevant metadata. */ static private class Entry { /** * A weak reference back to the Key object in stringCache that maps to * this Entry. */ public WeakReference<Key> keyRef; /** * The total horizontal advance (i.e. width) for this string in pixels. */ public int advance; /** * Array of fully layed out glyphs for the string. Sorted by logical * order of characters (i.e. glyph .stringIndex) */ public Glyph glyphs[]; /** * Array of color code locations from the original string */ public ColorCode colors[]; /** * True if the string uses strikethrough or underlines anywhere and * needs an extra pass in renderString() */ public boolean specialRender; } /** * Identifies the location and value of a single color code in the original * string */ static private class ColorCode implements Comparable<Integer> { /** * Bit flag used with renderStyle to request the underline style */ public static final byte UNDERLINE = 1; /** * Bit flag used with renderStyle to request the strikethrough style */ public static final byte STRIKETHROUGH = 2; /** * The index into the original string (i.e. with color codes) for the * location of this color code. */ public int stringIndex; /** * The index into the stripped string (i.e. with no color codes) of * where this color code would have appeared */ public int stripIndex; /** * The numeric color code (i.e. index into the colorCode[] array); -1 to * reset default color */ public byte colorCode; /** * Combination of Font.PLAIN, Font.BOLD, and Font.ITALIC specifying font * specific syles */ public byte fontStyle; /** * Combination of UNDERLINE and STRIKETHROUGH flags specifying effects * performed by renderString() */ public byte renderStyle; /** * Performs numeric comparison on stripIndex. Allows binary search on * ColorCode arrays in layoutStyle. * * @param i * the Integer object being compared * @return either -1, 0, or 1 if this < other, this == other, or this > * other */ @Override public int compareTo(final Integer i) { return (stringIndex == i.intValue()) ? 0 : (stringIndex < i.intValue()) ? -1 : 1; } } /** * Identifies a single glyph in the layed-out string. Includes a reference * to a GlyphCache.Entry with the OpenGL texture ID and position of the * pre-rendered glyph image, and includes the x/y pixel coordinates of where * this glyph occurs within the string to which this Glyph object belongs. */ static private class Glyph implements Comparable<Glyph> { /** * The index into the original string (i.e. with color codes) for the * character that generated this glyph. */ public int stringIndex; /** * Texture ID and position/size of the glyph's pre-rendered image within * the cache texture. */ public GlyphCache.Entry texture; /** * Glyph's horizontal position (in pixels) relative to the entire * string's baseline */ public int x; /** * Glyph's vertical position (in pixels) relative to the entire string's * baseline */ public int y; /** * Glyph's horizontal advance (in pixels) used for strikethrough and * underline effects */ public int advance; /** * Allows arrays of Glyph objects to be sorted. Performs numeric * comparison on stringIndex. * * @param o * the other Glyph object being compared with this one * @return either -1, 0, or 1 if this < other, this == other, or this > * other */ @Override public int compareTo(final Glyph o) { return (stringIndex == o.stringIndex) ? 0 : (stringIndex < o.stringIndex) ? -1 : 1; } } /** * A single StringCache object is allocated by Minecraft's FontRenderer * which forwards all string drawing and requests for string width to this * class. * <p/> * renderEngine needed for allocating new OpenGL textures * * @param colors * 32 element array of RGBA colors corresponding to the 16 text * color codes followed by 16 darker version of the color codes * for use as drop shadows */ public StringCache(final int colors[]) { /* * StringCache is created by the main game thread; remember it for later * thread safety checks */ mainThread = Thread.currentThread(); glyphCache = new GlyphCache(); colorTable = colors; /* Pre-cache the ASCII digits to allow for fast glyph substitution */ cacheDightGlyphs(); } /** * Change the default font used to pre-render glyph images. If this method * is called at runtime, the string cache is flushed so that all visible * strings will be immediately re-layed out using the new font selection. * * @param name * the new font name * @param size * the new point size */ public void setDefaultFont(final String name, final int size, final boolean antiAlias) { /* * Change the font in the glyph cache and clear the string cache so all * strings have to be re-layed out and re-rendered */ glyphCache.setDefaultFont(name, size, antiAlias); antiAliasEnabled = antiAlias; weakRefCache.clear(); stringCache.clear(); /* Pre-cache the ASCII digits to allow for fast glyph substitution */ cacheDightGlyphs(); } /** * Pre-cache the ASCII digits to allow for fast glyph substitution. Called * once from the constructor and called any time the font selection changes * at runtime via setDefaultFont(). */ private void cacheDightGlyphs() { /* * Need to cache each font style combination; the digitGlyphsReady = * false disabled the normal glyph substitution mechanism */ digitGlyphsReady = false; digitGlyphs[Font.PLAIN] = cacheString("0123456789").glyphs; digitGlyphs[Font.BOLD] = cacheString("???l0123456789").glyphs; digitGlyphs[Font.ITALIC] = cacheString("???o0123456789").glyphs; digitGlyphs[Font.BOLD | Font.ITALIC] = cacheString("???l???o0123456789").glyphs; digitGlyphsReady = true; } /** * Render a single-line string to the screen using the current OpenGL color. * The (x, y) coordinates are of the uppet-left corner of the string's * bounding box, rather than the baseline position as is typical with fonts. * This function will also add the string to the cache so the next * renderString() call with the same string is faster. * * @param str * the string being rendered; it can contain color codes * @param startX * the x coordinate to draw at * @param startY * the y coordinate to draw at * @param initialColor * the initial RGBA color to use when drawing the string; * embedded color codes can override the RGB component * @param shadowFlag * if true, color codes are replaces by a darker version used for * drop shadows * @return the total advance (horizontal distance) of this string * @todo Add optional NumericShaper to replace ASCII digits with locale * specific ones * @todo Add support for the "k" code which randomly replaces letters on * each render (used only by splash screen) * @todo Pre-sort by texture to minimize binds; can store colors per glyph * in string cache * @todo Optimize the underline/strikethrough drawing to draw a single line * for each run */ public int renderString(final String str, final float startX, float startY, final int initialColor, final boolean shadowFlag) { /* Check for invalid arguments */ if ((str == null) || str.isEmpty()) { return 0; } /* * Make sure the entire string is cached before rendering and return its * glyph representation */ final Entry entry = cacheString(str); /* * Adjust the baseline of the string because the startY coordinate in * Minecraft is for the top of the string */ startY += BASELINE_OFFSET; /* * Color currently selected by color code; reapplied to Tessellator * instance after glBindTexture() */ int color = initialColor; /* * Track which texture is currently bound to minimize the number of * glBindTexture() and Tessellator.draw() calls needed */ int boundTextureName = 0; /* * This color change will have no effect on the actual text (since * colors are included in the Tessellator vertex array), however * GuiEditSign of all things depends on having the current color set to * white when it renders its "Edit sign message:" text. Otherwise, the * sign which is rendered underneath would look too dark. */ GL11.glColor3f((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff); /* * Enable GL_BLEND in case the font is drawn anti-aliased because * Minecraft itself only enables blending for chat text (so it can fade * out), but not GUI text or signs. Minecraft uses multiple blend * functions so it has to be specified here as well for consistent * blending. To reduce the overhead of OpenGL state changes and making * native LWJGL calls, this function doesn't try to save/restore the * blending state. Hopefully everything else that depends on blending in * Minecraft will set its own state as needed. */ if (antiAliasEnabled) { GL11.glEnable(GL11.GL_BLEND); GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); } /* * Using the Tessellator to queue up data in a vertex array and then * draw all at once should be faster than immediate mode */ final Tessellator tessellator = Tessellator.instance; tessellator.startDrawingQuads(); tessellator.setColorRGBA((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, (color >> 24) & 0xff); /* * The currently active font syle is needed to select the proper ASCII * digit style for fast replacement */ int fontStyle = Font.PLAIN; for (int glyphIndex = 0, colorIndex = 0; glyphIndex < entry.glyphs.length; glyphIndex++) { /* * If the original string had a color code at this glyph's position, * then change the current GL color that gets added to the vertex * array. Note that only the RGB component of the color is replaced * by a color code; the alpha component of the original color passed * into this function will remain. The while loop handles multiple * consecutive color codes, in which case only the last such color * code takes effect. */ while ((colorIndex < entry.colors.length) && (entry.glyphs[glyphIndex].stringIndex >= entry.colors[colorIndex].stringIndex)) { color = applyColorCode(entry.colors[colorIndex].colorCode, initialColor, shadowFlag); fontStyle = entry.colors[colorIndex].fontStyle; colorIndex++; } /* * Select the current glyph's texture information and horizontal * layout position within this string */ final Glyph glyph = entry.glyphs[glyphIndex]; GlyphCache.Entry texture = glyph.texture; int glyphX = glyph.x; /* * Replace ASCII digits in the string with their respective glyphs; * strings differing by digits are only cached once. If the new * replacement glyph has a different width than the original * placeholder glyph (e.g. the '1' glyph is often narrower than * other digits), re-center the new glyph over the placeholder's * position to minimize the visual impact of the width mismatch. */ final char c = str.charAt(glyph.stringIndex); if ((c >= '0') && (c <= '9')) { final int oldWidth = texture.width; texture = digitGlyphs[fontStyle][c - '0'].texture; final int newWidth = texture.width; glyphX += (oldWidth - newWidth) >> 1; } /* * Make sure the OpenGL texture storing this glyph's image is bound * (if not already bound). All pending glyphs in the Tessellator's * vertex array must be drawn before switching textures, otherwise * they would erroneously use the new texture as well. */ if (boundTextureName != texture.textureName) { tessellator.draw(); tessellator.startDrawingQuads(); tessellator.setColorRGBA((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, (color >> 24) & 0xff); GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture.textureName); boundTextureName = texture.textureName; } /* * The divide by 2.0F is needed to align with the scaled GUI * coordinate system; startX/startY are already scaled */ final float x1 = startX + ((glyphX) / 2.0F); final float x2 = startX + ((glyphX + texture.width) / 2.0F); final float y1 = startY + ((glyph.y) / 2.0F); final float y2 = startY + ((glyph.y + texture.height) / 2.0F); tessellator.addVertexWithUV(x1, y1, 0, texture.u1, texture.v1); tessellator.addVertexWithUV(x1, y2, 0, texture.u1, texture.v2); tessellator.addVertexWithUV(x2, y2, 0, texture.u2, texture.v2); tessellator.addVertexWithUV(x2, y1, 0, texture.u2, texture.v1); } /* * Draw any remaining glyphs in the Tessellator's vertex array (there * should be at least one glyph pending) */ tessellator.draw(); /* Draw strikethrough and underlines if the string uses them anywhere */ if (entry.specialRender) { int renderStyle = 0; /* * Use initial color passed to renderString(); disable texturing to * draw solid color lines */ color = initialColor; GL11.glDisable(GL11.GL_TEXTURE_2D); tessellator.startDrawingQuads(); tessellator.setColorRGBA((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, (color >> 24) & 0xff); for (int glyphIndex = 0, colorIndex = 0; glyphIndex < entry.glyphs.length; glyphIndex++) { /* * If the original string had a color code at this glyph's * position, then change the current GL color that gets added to * the vertex array. The while loop handles multiple consecutive * color codes, in which case only the last such color code * takes effect. */ while ((colorIndex < entry.colors.length) && (entry.glyphs[glyphIndex].stringIndex >= entry.colors[colorIndex].stringIndex)) { color = applyColorCode(entry.colors[colorIndex].colorCode, initialColor, shadowFlag); renderStyle = entry.colors[colorIndex].renderStyle; colorIndex++; } /* * Select the current glyph within this string for its layout * position */ final Glyph glyph = entry.glyphs[glyphIndex]; /* * The strike/underlines are drawn beyond the glyph's width to * include the extra space between glyphs */ final int glyphSpace = glyph.advance - glyph.texture.width; /* Draw underline under glyph if the style is enabled */ if ((renderStyle & ColorCode.UNDERLINE) != 0) { /* * The divide by 2.0F is needed to align with the scaled GUI * coordinate system; startX/startY are already scaled */ final float x1 = startX + ((glyph.x - glyphSpace) / 2.0F); final float x2 = startX + ((glyph.x + glyph.advance) / 2.0F); final float y1 = startY + ((UNDERLINE_OFFSET) / 2.0F); final float y2 = startY + ((UNDERLINE_OFFSET + UNDERLINE_THICKNESS) / 2.0F); tessellator.addVertex(x1, y1, 0); tessellator.addVertex(x1, y2, 0); tessellator.addVertex(x2, y2, 0); tessellator.addVertex(x2, y1, 0); } /* * Draw strikethrough in the middle of glyph if the style is * enabled */ if ((renderStyle & ColorCode.STRIKETHROUGH) != 0) { /* * The divide by 2.0F is needed to align with the scaled GUI * coordinate system; startX/startY are already scaled */ final float x1 = startX + ((glyph.x - glyphSpace) / 2.0F); final float x2 = startX + ((glyph.x + glyph.advance) / 2.0F); final float y1 = startY + ((STRIKETHROUGH_OFFSET) / 2.0F); final float y2 = startY + ((STRIKETHROUGH_OFFSET + STRIKETHROUGH_THICKNESS) / 2.0F); tessellator.addVertex(x1, y1, 0); tessellator.addVertex(x1, y2, 0); tessellator.addVertex(x2, y2, 0); tessellator.addVertex(x2, y1, 0); } } /* Finish drawing the last strikethrough/underline segments */ tessellator.draw(); GL11.glEnable(GL11.GL_TEXTURE_2D); } if (antiAliasEnabled) { GL11.glDisable(GL11.GL_BLEND); } /* * Return total horizontal advance (slightly wider than the bounding * box, but close enough for centering strings) */ return entry.advance / 2; } /** * Return the width of a string in pixels. Used for centering strings inside * GUI buttons. * * @param str * compute the width of this string * @return the width in pixels (divided by 2; this matches the scaled * coordinate system used by GUIs in Minecraft) */ public int getStringWidth(final String str) { /* Check for invalid arguments */ if ((str == null) || str.isEmpty()) { return 0; } /* * Make sure the entire string is cached and rendered since it will * probably be used again in a renderString() call */ final Entry entry = cacheString(str); /* * Return total horizontal advance (slightly wider than the bounding * box, but close enough for centering strings) */ return entry.advance / 2; } public int getStringHeight() { final Entry entry = cacheString("|"); final Glyph glyph = entry.glyphs[0]; return (glyph.texture.height / 2) - 1; } /** * Return the number of characters in a string that will completly fit * inside the specified width when rendered, with or without prefering to * break the line at whitespace instead of breaking in the middle of a word. * This private provides the real implementation of both sizeStringToWidth() * and trimStringToWidth(). * * @param str * the String to analyze * @param width * the desired string width (in GUI coordinate system) * @param breakAtSpaces * set to prefer breaking line at spaces than in the middle of a * word * @return the number of characters from str that will fit inside width */ private int sizeString(final String str, int width, final boolean breakAtSpaces) { /* Check for invalid arguments */ if ((str == null) || str.isEmpty()) { return 0; } /* Convert the width from GUI coordinate system to pixels */ width += width; /* * The glyph array for a string is sorted by the string's logical * character position */ final Glyph glyphs[] = cacheString(str).glyphs; /* * Index of the last whitespace found in the string; used if * breakAtSpaces is true */ int wsIndex = -1; /* * Add up the individual advance of each glyph until it exceeds the * specified width */ int advance = 0, index = 0; while ((index < glyphs.length) && (advance <= width)) { /* Keep track of spaces if breakAtSpaces it set */ if (breakAtSpaces) { final char c = str.charAt(glyphs[index].stringIndex); if (c == ' ') { wsIndex = index; } else if (c == '\n') { wsIndex = index; break; } } advance += glyphs[index].advance; index++; } /* * Avoid splitting individual words if breakAtSpaces set; same test * condition as in Minecraft's FontRenderer */ if ((index < glyphs.length) && (wsIndex != -1) && (wsIndex < index)) { index = wsIndex; } /* * The string index of the last glyph that wouldn't fit gives the total * desired length of the string in characters */ return index < glyphs.length ? glyphs[index].stringIndex : str.length(); } /** * Return the number of characters in a string that will completly fit * inside the specified width when rendered. * * @param str * the String to analyze * @param width * the desired string width (in GUI coordinate system) * @return the number of characters from str that will fit inside width */ public int sizeStringToWidth(final String str, final int width) { return sizeString(str, width, true); } /** * Trim a string so that it fits in the specified width when rendered, * optionally reversing the string * * @param str * the String to trim * @param width * the desired string width (in GUI coordinate system) * @param reverse * if true, the returned string will also be reversed * @return the trimmed and optionally reversed string */ public String trimStringToWidth(String str, final int width, final boolean reverse) { final int length = sizeString(str, width, false); str = str.substring(0, length); if (reverse) { str = (new StringBuilder(str)).reverse().toString(); } return str; } /** * Apply a new vertex color to the Tessellator instance based on the numeric * chat color code. Only the RGB component of the color is replaced by a * color code; the alpha component of the original default color will * remain. * * @param colorCode * the chat color code as a number 0-15 or -1 to reset the * default color * @param color * the default color used when the colorCode is -1 * @param shadowFlag * ir true, the color code will select a darker version of the * color suitable for drop shadows * @return the new RGBA color set by this function */ private int applyColorCode(int colorCode, int color, final boolean shadowFlag) { /* * A -1 color code indicates a reset to the initial color passed into * renderString() */ if (colorCode != -1) { colorCode = shadowFlag ? colorCode + 16 : colorCode; color = (colorTable[colorCode] & 0xffffff) | (color & 0xff000000); } Tessellator.instance.setColorRGBA((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, (color >> 24) & 0xff); return color; } /** * Add a string to the string cache by perform full layout on it, * remembering its glyph positions, and making sure that every font glyph * used by the string is pre-rendering. If this string has already been * cached, then simply return its existing Entry from the cahe. Note that * for caching purposes, this method considers two strings to be identical * if they only differ in their ASCII digits; the renderString() method * performs fast glyph substitution based on the actual digits in the string * at the time. * * @param str * this String will be layed out and added to the cache (or * looked up, if alraedy cached) * @return the string's cache entry containing all the glyph positions */ private Entry cacheString(final String str) { /* * New Key object allocated only if the string was not found in the * StringCache using lookupKey. This variable must be outside the (entry * == null) code block to have a temporary strong reference between the * time when the Key is added to stringCache and added to weakRefCache. */ Key key; /* * Either a newly created Entry object for the string, or the cached * Entry if the string is already in the cache */ Entry entry = null; /* * Don't perform a cache lookup from other threads because the * stringCache is not synchronized */ if (mainThread == Thread.currentThread()) { /* * Re-use existing lookupKey to avoid allocation overhead on the * critical rendering path */ lookupKey.str = str; /* * If this string is already in the cache, simply return the cached * Entry object */ entry = stringCache.get(lookupKey); } /* * If string is not cached (or not on main thread) then layout the * string */ if (entry == null) { /* * layoutGlyphVector() requires a char[] so create it here and pass * it around to avoid duplication later on */ final char text[] = str.toCharArray(); /* Strip all color codes from the string */ entry = new Entry(); final int length = stripColorCodes(entry, str, text); /* * Layout the entire string, splitting it up by color codes and the * Unicode bidirectional algorithm */ final List<Glyph> glyphList = new ArrayList(); entry.advance = layoutBidiString(glyphList, text, 0, length, entry.colors); /* * Convert the accumulated Glyph list to an array for efficient * storage */ entry.glyphs = new Glyph[glyphList.size()]; entry.glyphs = glyphList.toArray(entry.glyphs); /* * Sort Glyph array by stringIndex so it can be compared during * rendering to the already sorted ColorCode array. This will apply * color codes in the string's logical character order and not the * visual order on screen. */ Arrays.sort(entry.glyphs); /* Do some post-processing on each Glyph object */ int colorIndex = 0, shift = 0; for (final Glyph glyph2 : entry.glyphs) { final Glyph glyph = glyph2; /* * Adjust the string index for each glyph to point into the * original string with unstripped color codes. The while loop * is necessary to handle multiple consecutive color codes with * no visible glyphs between them. These new adjusted * stringIndex can now be compared against the color stringIndex * during rendering. It also allows lookups of ASCII digits in * the original string for fast glyph replacement during * rendering. */ while ((colorIndex < entry.colors.length) && ((glyph.stringIndex + shift) >= entry.colors[colorIndex].stringIndex)) { shift += 2; colorIndex++; } glyph.stringIndex += shift; } /* * Do not actually cache the string when called from other threads * because GlyphCache.cacheGlyphs() will not have been called and * the cache entry does not contain any texture data needed for * rendering. */ if (mainThread == Thread.currentThread()) { /* * Wrap the string in a Key object (to change how ASCII digits * are compared) and cache it along with the newly generated * Entry */ key = new Key(); /* * Make a copy of the original String to avoid creating a strong * reference to it */ key.str = new String(str); entry.keyRef = new WeakReference(key); stringCache.put(key, entry); } } /* * Do not access weakRefCache from other threads since it is * unsynchronized, and for a newly created entry, the keyRef is null */ if (mainThread == Thread.currentThread()) { /* * Add the String passed into this method to the stringWeakMap so it * keeps the Key reference live as long as the String is in use. If * an existing Entry was already found in the stringCache, it's * possible that its Key has already been garbage collected. The * code below checks for this to avoid adding (str, null) entries * into weakRefCache. Note that if a new Key object was created, it * will still be live because of the strong reference created by the * "key" variable. */ final Key oldKey = entry.keyRef.get(); if (oldKey != null) { weakRefCache.put(str, oldKey); } lookupKey.str = null; } /* * Return either the existing or the newly created entry so it can be * accessed immediately */ return entry; } /** * Remove all color codes from the string by shifting data in the text[] * array over so it overwrites them. The value of each color code and its * position (relative to the new stripped text[]) is also recorded in a * separate array. The color codes must be removed for a font's context * sensitive glyph substitution to work (like Arabic letter middle form). * <p/> * colorList each color change in the string will add a new ColorCode object * to this list * * @param str * the string from which color codes will be stripped * @param text * on input it should be an identical copy of str; on output it * will be string with all color codes removed * @return the length of the new stripped string in text[]; actual * text.length will not change because the array is not reallocated */ private int stripColorCodes(final Entry cacheEntry, final String str, final char text[]) { final List<ColorCode> colorList = new ArrayList(); int start = 0, shift = 0, next; byte fontStyle = Font.PLAIN; byte renderStyle = 0; byte colorCode = -1; /* * Search for section mark characters indicating the start of a color * code (but only if followed by at least one character) */ while (((next = str.indexOf('\u00A7', start)) != -1) && ((next + 1) < str.length())) { /* * Remove the two char color code from text[] by shifting the * remaining data in the array over on top of it. The "start" and * "next" variables all contain offsets into the original unmodified * "str" string. The "shift" variable keeps track of how many * characters have been sripped so far, and it's used to compute * offsets into the text[] array based on the start/next offsets in * the original string. */ System.arraycopy(text, (next - shift) + 2, text, next - shift, text.length - next - 2); /* * Decode escape code used in the string and change current font * style / color based on it */ final int code = "0123456789abcdefklmnor".indexOf(Character.toLowerCase(str.charAt(next + 1))); switch (code) { /* Random style; TODO: NOT IMPLEMENTED YET */ case 16: break; /* Bold style */ case 17: fontStyle |= Font.BOLD; break; /* Strikethrough style */ case 18: renderStyle |= ColorCode.STRIKETHROUGH; cacheEntry.specialRender = true; break; /* Underline style */ case 19: renderStyle |= ColorCode.UNDERLINE; cacheEntry.specialRender = true; break; /* Italic style */ case 20: fontStyle |= Font.ITALIC; break; /* Plain style */ case 21: fontStyle = Font.PLAIN; renderStyle = 0; colorCode = -1; // This may be a bug in Minecraft's original // FontRenderer break; /* Otherwise, must be a color code or some other unsupported code */ default: if ((code >= 0) && (code <= 15)) { colorCode = (byte) code; fontStyle = Font.PLAIN; // This may be a bug in Minecraft's // original FontRenderer renderStyle = 0; // This may be a bug in Minecraft's // original FontRenderer } break; } /* * Create a new ColorCode object that tracks the position of the * code in the original string */ final ColorCode entry = new ColorCode(); entry.stringIndex = next; entry.stripIndex = next - shift; entry.colorCode = colorCode; entry.fontStyle = fontStyle; entry.renderStyle = renderStyle; colorList.add(entry); /* Resume search for section marks after skipping this one */ start = next + 2; shift += 2; } /* * Convert the accumulated ColorCode list to an array for efficient * storage */ cacheEntry.colors = new ColorCode[colorList.size()]; cacheEntry.colors = colorList.toArray(cacheEntry.colors); /* * Return the new length of the string after all color codes were * removed */ return text.length - shift; } /** * Split a string into contiguous LTR or RTL sections by applying the * Unicode Bidirectional Algorithm. Calls layoutString() for each contiguous * run to perform further analysis. * * @param glyphList * will hold all new Glyph objects allocated by layoutFont() * @param text * the string to lay out * @param start * the offset into text at which to start the layout * @param limit * the (offset + length) at which to stop performing the layout * @return the total advance (horizontal distance) of this string */ private int layoutBidiString(final List<Glyph> glyphList, final char text[], final int start, final int limit, final ColorCode colors[]) { int advance = 0; /* * Avoid performing full bidirectional analysis if text has no "strong" * right-to-left characters */ if (Bidi.requiresBidi(text, start, limit)) { /* * Note that while requiresBidi() uses start/limit the Bidi * constructor uses start/length */ final Bidi bidi = new Bidi(text, start, null, 0, limit - start, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT); /* * If text is entirely right-to-left, then insert an EntryText node * for the entire string */ if (bidi.isRightToLeft()) { return layoutStyle(glyphList, text, start, limit, Font.LAYOUT_RIGHT_TO_LEFT, advance, colors); } /* * Otherwise text has a mixture of LTR and RLT, and it requires full * bidirectional analysis */ else { final int runCount = bidi.getRunCount(); final byte levels[] = new byte[runCount]; final Integer ranges[] = new Integer[runCount]; /* * Reorder contiguous runs of text into their display order from * left to right */ for (int index = 0; index < runCount; index++) { levels[index] = (byte) bidi.getRunLevel(index); ranges[index] = new Integer(index); } Bidi.reorderVisually(levels, 0, ranges, 0, runCount); /* * Every GlyphVector must be created on a contiguous run of * left-to-right or right-to-left text. Keep track of the * horizontal advance between each run of text, so that the * glyphs in each run can be assigned a position relative to the * start of the entire string and not just relative to that run. */ for (int visualIndex = 0; visualIndex < runCount; visualIndex++) { final int logicalIndex = ranges[visualIndex]; /* An odd numbered level indicates right-to-left ordering */ final int layoutFlag = (bidi.getRunLevel(logicalIndex) & 1) == 1 ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT; advance = layoutStyle(glyphList, text, start + bidi.getRunStart(logicalIndex), start + bidi.getRunLimit(logicalIndex), layoutFlag, advance, colors); } } return advance; } /* * If text is entirely left-to-right, then insert an EntryText node for * the entire string */ else { return layoutStyle(glyphList, text, start, limit, Font.LAYOUT_LEFT_TO_RIGHT, advance, colors); } } private int layoutStyle(final List<Glyph> glyphList, final char text[], int start, final int limit, final int layoutFlags, int advance, final ColorCode colors[]) { int currentFontStyle = Font.PLAIN; /* * Find ColorCode object with stripIndex <= start; that will have the * font style in effect at the beginning of this text run */ int colorIndex = Arrays.binarySearch(colors, start); /* * If no exact match is found, Arrays.binarySearch() returns * (-(insertion point) - 1) where the insertion point is the index of * the first ColorCode with a stripIndex > start. In that case, * colorIndex is adjusted to select the immediately preceding ColorCode * whose stripIndex < start. */ if (colorIndex < 0) { colorIndex = -colorIndex - 2; } /* * Break up the string into segments, where each segment has the same * font style in use */ while (start < limit) { int next = limit; /* * In case of multiple consecutive color codes with the same * stripIndex, select the last one which will have active font style */ while ((colorIndex >= 0) && (colorIndex < (colors.length - 1)) && (colors[colorIndex].stripIndex == colors[colorIndex + 1].stripIndex)) { colorIndex++; } /* * If an actual ColorCode object was found (colorIndex within the * array), use its fontStyle for layout and render */ if ((colorIndex >= 0) && (colorIndex < colors.length)) { currentFontStyle = colors[colorIndex].fontStyle; } /* * Search for the next ColorCode that uses a different fontStyle * than the current one. If found, the stripIndex of that new code * is the split point where the string must be split into a * separately styled segment. */ while (++colorIndex < colors.length) { if (colors[colorIndex].fontStyle != currentFontStyle) { next = colors[colorIndex].stripIndex; break; } } /* * Layout the string segment with the style currently selected by * the last color code */ advance = layoutString(glyphList, text, start, next, layoutFlags, advance, currentFontStyle); start = next; } return advance; } /** * Given a string that runs contiguously LTR or RTL, break it up into * individual segments based on which fonts can render which characters in * the string. Calls layoutFont() for each portion of the string that can be * layed out with a single font. * * @param glyphList * will hold all new Glyph objects allocated by layoutFont() * @param text * the string to lay out * @param start * the offset into text at which to start the layout * @param limit * the (offset + length) at which to stop performing the layout * @param layoutFlags * either Font.LAYOUT_RIGHT_TO_LEFT or Font.LAYOUT_LEFT_TO_RIGHT * @param advance * the horizontal advance (i.e. X position) returned by previous * call to layoutString() * @param style * combination of Font.PLAIN, Font.BOLD, and Font.ITALIC to * select a fonts with some specific style * @return the advance (horizontal distance) of this string plus the advance * passed in as an argument * @todo Correctly handling RTL font selection requires scanning the sctring * from RTL as well. * @todo Use bitmap fonts as a fallback if no OpenType font could be found */ private int layoutString(final List<Glyph> glyphList, final char text[], int start, final int limit, final int layoutFlags, int advance, final int style) { /* * Convert all digits in the string to a '0' before layout to ensure * that any glyphs replaced on the fly will all have the same positions. * Under Windows, Java's "SansSerif" logical font uses the "Arial" font * for digits, in which the "1" digit is slightly narrower than all * other digits. Checking the digitGlyphsReady flag prevents a * chicken-and-egg problem where the digit glyphs have to be initially * cached and the digitGlyphs[] array initialized without replacing * every digit with '0'. */ if (digitGlyphsReady) { for (int index = start; index < limit; index++) { if ((text[index] >= '0') && (text[index] <= '9')) { text[index] = '0'; } } } /* * Break the string up into segments, where each segment can be * displayed using a single font */ while (start < limit) { final Font font = glyphCache.lookupFont(text, start, limit, style); int next = font.canDisplayUpTo(text, start, limit); /* * canDisplayUpTo returns -1 if the entire string range is supported * by this font */ if (next == -1) { next = limit; } /* * canDisplayUpTo() returns start if the starting character is not * supported at all. In that case, draw just the one unsupported * character (which will use the font's "missing glyph code"), then * retry the lookup again at the next character after that. */ if (next == start) { next++; } advance = layoutFont(glyphList, text, start, next, layoutFlags, advance, font); start = next; } return advance; } /** * Allocate new Glyph objects and add them to the glyph list. This sequence * of Glyphs represents a portion of the string where all glyphs run * contiguously in either LTR or RTL and come from the same physical/logical * font. * * @param glyphList * all newly created Glyph objects are added to this list * @param text * the string to layout * @param start * the offset into text at which to start the layout * @param limit * the (offset + length) at which to stop performing the layout * @param layoutFlags * either Font.LAYOUT_RIGHT_TO_LEFT or Font.LAYOUT_LEFT_TO_RIGHT * @param advance * the horizontal advance (i.e. X position) returned by previous * call to layoutString() * @param font * the Font used to layout a GlyphVector for the string * @return the advance (horizontal distance) of this string plus the advance * passed in as an argument * @todo need to ajust position of all glyphs if digits are present, by * assuming every digit should be 0 in length */ private int layoutFont(final List<Glyph> glyphList, final char text[], final int start, final int limit, final int layoutFlags, int advance, final Font font) { /* * Ensure that all glyphs used by the string are pre-rendered and cached * in the texture. Only safe to do so from the main thread because * cacheGlyphs() can crash LWJGL if it makes OpenGL calls from any other * thread. In this case, cacheString() will also not insert the entry * into the stringCache since it may be incomplete if lookupGlyph() * returns null for any glyphs not yet stored in the glyph cache. */ if (mainThread == Thread.currentThread()) { glyphCache.cacheGlyphs(font, text, start, limit, layoutFlags); } /* * Creating a GlyphVector takes care of all language specific OpenType * glyph substitutions and positionings */ final GlyphVector vector = glyphCache.layoutGlyphVector(font, text, start, limit, layoutFlags); /* * Extract all needed information for each glyph from the GlyphVector so * it won't be needed for actual rendering. Note that initially, * glyph.start holds the character index into the stripped text array. * But after the entire string is layed out, this field will be adjusted * on every Glyph object to correctly index the original unstripped * string. */ Glyph glyph = null; final int numGlyphs = vector.getNumGlyphs(); for (int index = 0; index < numGlyphs; index++) { final Point position = vector.getGlyphPixelBounds(index, null, advance, 0).getLocation(); /* * Compute horizontal advance for the previous glyph based on this * glyph's position */ if (glyph != null) { glyph.advance = position.x - glyph.x; } /* * Allocate a new glyph object and add to the glyphList. The * glyph.stringIndex here is really like stripIndex but it will be * corrected later to account for the color codes that have been * stripped out. */ glyph = new Glyph(); glyph.stringIndex = start + vector.getGlyphCharIndex(index); glyph.texture = glyphCache.lookupGlyph(font, vector.getGlyphCode(index)); glyph.x = position.x; glyph.y = position.y; glyphList.add(glyph); } /* * Compute the advance position of the last glyph (or only glyph) since * it can't be done by the above loop */ advance += (int) vector.getGlyphPosition(numGlyphs).getX(); if (glyph != null) { glyph.advance = advance - glyph.x; } /* * Return the overall horizontal advance in pixels from the start of * string */ return advance; } }