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 com.onkiup.minedroid.gui.betterfonts; import java.lang.ref.WeakReference; import java.util.*; import java.awt.font.GlyphVector; import java.awt.Font; import java.awt.Point; import java.text.Bidi; import com.onkiup.minedroid.gui.GuiManager; import com.onkiup.minedroid.gui.views.View; import net.minecraft.client.renderer.GlStateManager; 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; private Font currentFont; /** * Reference to the unicode.FontRenderer class. Needed for creating GlyphVectors and retrieving glyph texture coordinates. */ private 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 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 WeakHashMap<Key<String>, WeakHashMap<Font, Entry>> stringCache = new WeakHashMap<Key<String>, WeakHashMap<Font, Entry>>(); /** * 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 WeakHashMap<String, Key<String>> 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 Key stringKey = new Key<String>(); private Key fontKey = new Key<Font>(); /** * 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 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; protected String currentFontName = ""; /** * 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 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<Type> { /** * 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 Type key; /** * 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; if (key instanceof String) { String str = (String) key; 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'); } } else { code = key.hashCode(); } 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(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; } if (!(key instanceof String)) return key.equals(o); /* Calling toString on a String object simply returns itself so no new object allocation is performed */ String str = (String) key; String other = o.toString(); 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++) { char c1 = str.charAt(index); 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 key.toString(); } } /** * This entry holds the layed out glyph positions for the cached string along with some relevant metadata. */ static public 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; public double height = 0; public double baseline = 0; public int getScaledAdvance() { int f = GuiManager.getScale().getScaleFactor(); return (int) Math.round((advance) / (float) f); } public double getScaledHeight() { int f = GuiManager.getScale().getScaleFactor(); return Math.ceil((height + baseline) / (float) f); } public double getScaledBase() { return Math.ceil(baseline / (float) GuiManager.getScale().getScaleFactor()); } } /** * 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(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 public 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(Glyph o) { return (stringIndex == o.stringIndex) ? 0 : (stringIndex < o.stringIndex) ? -1 : 1; } public int getScaledAdvance() { return (int) Math.ceil(advance / (float) GuiManager.getScale().getScaleFactor()); } public int getScaledWidth() { return (int) Math.ceil(texture.width / (float) GuiManager.getScale().getScaleFactor()); } } public StringCache() { colorTable = new int[32]; for (int var5 = 0; var5 < 32; ++var5) { int var6 = (var5 >> 3 & 1) * 85; int var7 = (var5 >> 2 & 1) * 170 + var6; int var8 = (var5 >> 1 & 1) * 170 + var6; int var9 = (var5 >> 0 & 1) * 170 + var6; if (var5 == 6) { var7 += 85; } if (var5 >= 16) { var7 /= 4; var8 /= 4; var9 /= 4; } colorTable[var5] = (var7 & 255) << 16 | (var8 & 255) << 8 | var9 & 255; } /* StringCache is created by the main game thread; remember it for later thread safety checks */ mainThread = Thread.currentThread(); glyphCache = new GlyphCache(); /* Pre-cache the ASCII digits to allow for fast glyph substitution */ // cacheDightGlyphs(); } /** * A single StringCache object is allocated by Minecraft's FontRenderer which forwards all string drawing and requests for * string width to this class. * * @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(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 Font setDefaultFont(String name, int size, boolean antiAlias) { // do not change the font if it's already set if (currentFont != null && currentFontName.equals(name) && currentFont.getSize() == size && antiAlias == antiAliasEnabled) return currentFont; /* Change the font in the glyph cache and clear the string cache so all strings have to be re-layed out and re-rendered */ currentFont = glyphCache.setDefaultFont(name, size, antiAlias); antiAliasEnabled = antiAlias; // weakRefCache.clear(); // stringCache.clear(); /* Pre-cache the ASCII digits to allow for fast glyph substitution */ cacheDightGlyphs(); return currentFont; } public void setDefaultFont(Font f, boolean antiAlias) { if (f.equals(currentFont)) return; glyphCache.setDefaultFont(f, true); antiAliasEnabled = antiAlias; 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("lo0123456789").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 Entry renderString(String str, int startX, int startY, int initialColor, boolean shadowFlag) { /* Check for invalid arguments */ if (str == null || str.isEmpty()) { return null; } View.resetBlending(); GlStateManager.enableTexture2D(); float scale = GuiManager.getDisplayScale(); float f = GuiManager.getScale().getScaleFactor(); /* Make sure the entire string is cached before rendering and return its glyph representation */ Entry entry = cacheString(str); /* Adjust the baseline of the string because the startY coordinate in Minecraft is for the top of the string */ startY += entry.getScaledBase(); /* 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 */ Tessellator tessellator = Tessellator.getInstance(); tessellator.getWorldRenderer().startDrawingQuads(); tessellator.getWorldRenderer().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 */ 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. */ char c = str.charAt(glyph.stringIndex); if (c >= '0' && c <= '9') { int oldWidth = texture.width; texture = digitGlyphs[fontStyle][c - '0'].texture; 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.getWorldRenderer().startDrawingQuads(); tessellator.getWorldRenderer().setColorRGBA(color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, color >> 24 & 0xff); GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture.textureName); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); boundTextureName = texture.textureName; } /* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */ float x1 = startX + (glyphX) / f; float x2 = startX + (glyphX + texture.width) / f; float y1 = startY + (glyph.y) / f; float y2 = startY + (glyph.y + texture.height) / f; tessellator.getWorldRenderer().addVertexWithUV(x1, y1, 0, texture.u1, texture.v1); tessellator.getWorldRenderer().addVertexWithUV(x1, y2, 0, texture.u1, texture.v2); tessellator.getWorldRenderer().addVertexWithUV(x2, y2, 0, texture.u2, texture.v2); tessellator.getWorldRenderer().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.getWorldRenderer().startDrawingQuads(); tessellator.getWorldRenderer().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 */ Glyph glyph = entry.glyphs[glyphIndex]; /* The strike/underlines are drawn beyond the glyph's width to include the extra space between glyphs */ 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 */ float x1 = startX + (glyph.x - glyphSpace) / f; float x2 = startX + (glyph.x + glyph.advance) / f; float y1 = startY + (UNDERLINE_OFFSET) / f; float y2 = startY + (UNDERLINE_OFFSET + UNDERLINE_THICKNESS) / f; tessellator.getWorldRenderer().addVertex(x1, y1, 0); tessellator.getWorldRenderer().addVertex(x1, y2, 0); tessellator.getWorldRenderer().addVertex(x2, y2, 0); tessellator.getWorldRenderer().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 */ float x1 = startX + (glyph.x - glyphSpace) / f; float x2 = startX + (glyph.x + glyph.advance) / f; float y1 = startY + (STRIKETHROUGH_OFFSET) / f; float y2 = startY + (STRIKETHROUGH_OFFSET + STRIKETHROUGH_THICKNESS) / f; tessellator.getWorldRenderer().addVertex(x1, y1, 0); tessellator.getWorldRenderer().addVertex(x1, y2, 0); tessellator.getWorldRenderer().addVertex(x2, y2, 0); tessellator.getWorldRenderer().addVertex(x2, y1, 0); } } /* Finish drawing the last strikethrough/underline segments */ tessellator.draw(); GL11.glEnable(GL11.GL_TEXTURE_2D); } /* Return total horizontal advance (slightly wider than the bounding box, but close enough for centering strings) */ return entry; } /** * 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(String str, int width, 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 */ 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) { 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(String str, 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, int width, boolean reverse) { 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, 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.getInstance().getWorldRenderer().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 */ public Entry cacheString(String str) { /* * New Key object allocated only if the string was not found in the StringCache using stringKey. 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()) { /* If this string is already in the cache, simply return the cached Entry object */ entry = getEntry(str); } /* 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 */ char text[] = str.toCharArray(); /* Strip all color codes from the string */ entry = new Entry(); int length = stripColorCodes(entry, str, text); /* Layout the entire string, splitting it up by color codes and the Unicode bidirectional algorithm */ List<Glyph> glyphList = new ArrayList(); entry.advance = (int) 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 (int glyphIndex = 0; glyphIndex < entry.glyphs.length; glyphIndex++) { Glyph glyph = entry.glyphs[glyphIndex]; /* * 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; if (Math.abs(glyph.y) > entry.baseline) entry.baseline = Math.abs(glyph.y); if (glyph.texture.height + glyph.y > entry.height) { entry.height = glyph.texture.height + glyph.y; } } putEntry(str, entry); } /* 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). * * @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(Entry cacheEntry, String str, char text[]) { 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 */ 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 */ 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(List<Glyph> glyphList, char text[], int start, int limit, 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 */ 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 { int runCount = bidi.getRunCount(); byte levels[] = new byte[runCount]; 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++) { int logicalIndex = ranges[visualIndex]; /* An odd numbered level indicates right-to-left ordering */ 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(List<Glyph> glyphList, char text[], int start, int limit, int layoutFlags, int advance, 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(List<Glyph> glyphList, char text[], int start, int limit, int layoutFlags, int advance, 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) { Font font = currentFont.deriveFont(style); // 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(List<Glyph> glyphList, char text[], int start, int limit, int layoutFlags, int advance, 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 */ GlyphVector vector = glyphCache.layoutGlyphVector(font, text, start, limit, layoutFlags); float scale = glyphCache.getScale(); /* * 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; int numGlyphs = vector.getNumGlyphs(); for (int index = 0; index < numGlyphs; index++) { 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; } protected Entry getEntry(String s) { stringKey.key = s; WeakHashMap<Font, Entry> entries = stringCache.get(stringKey); if (entries == null) return null; return entries.get(currentFont); } protected void putEntry(String s, Entry entry) { if (mainThread == Thread.currentThread()) { Key k = new Key(); k.key = new String(s); entry.keyRef = new WeakReference<Key>(k); if (!stringCache.containsKey(stringKey)) { weakRefCache.put(s, k); stringCache.put(k, new WeakHashMap<Font, Entry>()); } stringCache.get(k).put(currentFont, entry); } } }