//---------------------------------------------- // NGUI: Next-Gen UI kit // Copyright © 2011-2015 Tasharen Entertainment //---------------------------------------------- #if !UNITY_3_5 #define DYNAMIC_FONT #endif using UnityEngine; using System.Text; using System.Diagnostics; /// /// Helper class containing functionality related to using dynamic fonts. /// static public class NGUIText { public enum Alignment { Automatic, Left, Center, Right, Justified, } public enum SymbolStyle { None, Normal, Colored, } public class GlyphInfo { public Vector2 v0; public Vector2 v1; public Vector2 u0; public Vector2 u1; public Vector2 u2; public Vector2 u3; public float advance = 0f; public int channel = 0; } /// /// When printing text, a lot of additional data must be passed in. In order to save allocations, /// this data is not passed at all, but is rather set in a single place before calling the functions that use it. /// static public UIFont bitmapFont; #if DYNAMIC_FONT static public Font dynamicFont; #endif static public GlyphInfo glyph = new GlyphInfo(); static public int fontSize = 16; static public float fontScale = 1f; static public float pixelDensity = 1f; static public FontStyle fontStyle = FontStyle.Normal; static public Alignment alignment = Alignment.Left; static public Color tint = Color.white; static public int rectWidth = 1000000; static public int rectHeight = 1000000; static public int regionWidth = 1000000; static public int regionHeight = 1000000; static public int maxLines = 0; static public bool gradient = false; static public Color gradientBottom = Color.white; static public Color gradientTop = Color.white; static public bool encoding = false; static public float spacingX = 0f; static public float spacingY = 0f; static public bool premultiply = false; static public SymbolStyle symbolStyle; static public int finalSize = 0; static public float finalSpacingX = 0f; static public float finalLineHeight = 0f; static public float baseline = 0f; static public bool useSymbols = false; /// /// Recalculate the 'final' values. /// static public void Update () { Update(true); } /// /// Recalculate the 'final' values. /// static public void Update (bool request) { finalSize = Mathf.RoundToInt(fontSize / pixelDensity); finalSpacingX = spacingX * fontScale; finalLineHeight = (fontSize + spacingY) * fontScale; useSymbols = (bitmapFont != null && bitmapFont.hasSymbols) && encoding && symbolStyle != SymbolStyle.None; #if DYNAMIC_FONT if (dynamicFont != null && request) { dynamicFont.RequestCharactersInTexture(")_-", finalSize, fontStyle); #if UNITY_4_3 || UNITY_4_5 || UNITY_4_6 if (!dynamicFont.GetCharacterInfo(')', out mTempChar, finalSize, fontStyle) || mTempChar.vert.height == 0f) { dynamicFont.RequestCharactersInTexture("A", finalSize, fontStyle); { if (!dynamicFont.GetCharacterInfo('A', out mTempChar, finalSize, fontStyle)) { baseline = 0f; return; } } } float y0 = mTempChar.vert.yMax; float y1 = mTempChar.vert.yMin; #else if (!dynamicFont.GetCharacterInfo(')', out mTempChar, finalSize, fontStyle) || mTempChar.maxY == 0f) { dynamicFont.RequestCharactersInTexture("A", finalSize, fontStyle); { if (!dynamicFont.GetCharacterInfo('A', out mTempChar, finalSize, fontStyle)) { baseline = 0f; return; } } } float y0 = mTempChar.maxY; float y1 = mTempChar.minY; #endif baseline = Mathf.Round(y0 + (finalSize - y0 + y1) * 0.5f); } #endif } /// /// Prepare to use the specified text. /// static public void Prepare (string text) { #if DYNAMIC_FONT if (dynamicFont != null) dynamicFont.RequestCharactersInTexture(text, finalSize, fontStyle); #endif } /// /// Get the specified symbol. /// static public BMSymbol GetSymbol (string text, int index, int textLength) { return (bitmapFont != null) ? bitmapFont.MatchSymbol(text, index, textLength) : null; } /// /// Get the width of the specified glyph. Returns zero if the glyph could not be retrieved. /// static public float GetGlyphWidth (int ch, int prev) { if (bitmapFont != null) { bool thinSpace = false; if (ch == '\u2009') { thinSpace = true; ch = ' '; } BMGlyph bmg = bitmapFont.bmFont.GetGlyph(ch); if (bmg != null) { int adv = bmg.advance; if (thinSpace) adv >>= 1; return fontScale * ((prev != 0) ? adv + bmg.GetKerning(prev) : bmg.advance); } } #if DYNAMIC_FONT else if (dynamicFont != null) { if (dynamicFont.GetCharacterInfo((char)ch, out mTempChar, finalSize, fontStyle)) #if UNITY_4_3 || UNITY_4_5 || UNITY_4_6 return mTempChar.width * fontScale * pixelDensity; #else return mTempChar.advance * fontScale * pixelDensity; #endif } #endif return 0f; } /// /// Get the specified glyph. /// static public GlyphInfo GetGlyph (int ch, int prev) { if (bitmapFont != null) { bool thinSpace = false; if (ch == '\u2009') { thinSpace = true; ch = ' '; } BMGlyph bmg = bitmapFont.bmFont.GetGlyph(ch); if (bmg != null) { int kern = (prev != 0) ? bmg.GetKerning(prev) : 0; glyph.v0.x = (prev != 0) ? bmg.offsetX + kern : bmg.offsetX; glyph.v1.y = -bmg.offsetY; glyph.v1.x = glyph.v0.x + bmg.width; glyph.v0.y = glyph.v1.y - bmg.height; glyph.u0.x = bmg.x; glyph.u0.y = bmg.y + bmg.height; glyph.u2.x = bmg.x + bmg.width; glyph.u2.y = bmg.y; glyph.u1.x = glyph.u0.x; glyph.u1.y = glyph.u2.y; glyph.u3.x = glyph.u2.x; glyph.u3.y = glyph.u0.y; int adv = bmg.advance; if (thinSpace) adv >>= 1; glyph.advance = adv + kern; glyph.channel = bmg.channel; if (fontScale != 1f) { glyph.v0 *= fontScale; glyph.v1 *= fontScale; glyph.advance *= fontScale; } return glyph; } } #if DYNAMIC_FONT else if (dynamicFont != null) { if (dynamicFont.GetCharacterInfo((char)ch, out mTempChar, finalSize, fontStyle)) { #if UNITY_4_3 || UNITY_4_5 || UNITY_4_6 glyph.v0.x = mTempChar.vert.xMin; glyph.v1.x = glyph.v0.x + mTempChar.vert.width; glyph.v0.y = mTempChar.vert.yMax - baseline; glyph.v1.y = glyph.v0.y - mTempChar.vert.height; glyph.u0.x = mTempChar.uv.xMin; glyph.u0.y = mTempChar.uv.yMin; glyph.u2.x = mTempChar.uv.xMax; glyph.u2.y = mTempChar.uv.yMax; if (mTempChar.flipped) { glyph.u1 = new Vector2(glyph.u2.x, glyph.u0.y); glyph.u3 = new Vector2(glyph.u0.x, glyph.u2.y); } else { glyph.u1 = new Vector2(glyph.u0.x, glyph.u2.y); glyph.u3 = new Vector2(glyph.u2.x, glyph.u0.y); } glyph.advance = mTempChar.width; glyph.channel = 0; #else glyph.v0.x = mTempChar.minX; glyph.v1.x = mTempChar.maxX; glyph.v0.y = mTempChar.maxY - baseline; glyph.v1.y = mTempChar.minY - baseline; glyph.u0 = mTempChar.uvTopLeft; glyph.u1 = mTempChar.uvBottomLeft; glyph.u2 = mTempChar.uvBottomRight; glyph.u3 = mTempChar.uvTopRight; glyph.advance = mTempChar.advance; glyph.channel = 0; #endif glyph.v0.x = Mathf.Round(glyph.v0.x); glyph.v0.y = Mathf.Round(glyph.v0.y); glyph.v1.x = Mathf.Round(glyph.v1.x); glyph.v1.y = Mathf.Round(glyph.v1.y); float pd = fontScale * pixelDensity; if (pd != 1f) { glyph.v0 *= pd; glyph.v1 *= pd; glyph.advance *= pd; } return glyph; } } #endif return null; } static Color mInvisible = new Color(0f, 0f, 0f, 0f); static BetterList mColors = new BetterList(); static float mAlpha = 1f; #if DYNAMIC_FONT static CharacterInfo mTempChar; #endif /// /// Parse Aa syntax alpha encoded in the string. /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public float ParseAlpha (string text, int index) { int a = (NGUIMath.HexToDecimal(text[index + 1]) << 4) | NGUIMath.HexToDecimal(text[index + 2]); return Mathf.Clamp01(a / 255f); } /// /// Parse a RrGgBb color encoded in the string. /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public Color ParseColor (string text, int offset) { return ParseColor24(text, offset); } /// /// Parse a RrGgBb color encoded in the string. /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public Color ParseColor24 (string text, int offset) { int r = (NGUIMath.HexToDecimal(text[offset]) << 4) | NGUIMath.HexToDecimal(text[offset + 1]); int g = (NGUIMath.HexToDecimal(text[offset + 2]) << 4) | NGUIMath.HexToDecimal(text[offset + 3]); int b = (NGUIMath.HexToDecimal(text[offset + 4]) << 4) | NGUIMath.HexToDecimal(text[offset + 5]); float f = 1f / 255f; return new Color(f * r, f * g, f * b); } /// /// Parse a RrGgBbAa color encoded in the string. /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public Color ParseColor32 (string text, int offset) { int r = (NGUIMath.HexToDecimal(text[offset]) << 4) | NGUIMath.HexToDecimal(text[offset + 1]); int g = (NGUIMath.HexToDecimal(text[offset + 2]) << 4) | NGUIMath.HexToDecimal(text[offset + 3]); int b = (NGUIMath.HexToDecimal(text[offset + 4]) << 4) | NGUIMath.HexToDecimal(text[offset + 5]); int a = (NGUIMath.HexToDecimal(text[offset + 6]) << 4) | NGUIMath.HexToDecimal(text[offset + 7]); float f = 1f / 255f; return new Color(f * r, f * g, f * b, f * a); } /// /// The reverse of ParseColor -- encodes a color in RrGgBb format. /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public string EncodeColor (Color c) { return EncodeColor24(c); } /// /// Convenience function that wraps the specified text block in a color tag. /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public string EncodeColor (string text, Color c) { return "[c][" + EncodeColor24(c) + "]" + text + "[-][/c]"; } /// /// The reverse of ParseAlpha -- encodes a color in Aa format. /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public string EncodeAlpha (float a) { int i = Mathf.Clamp(Mathf.RoundToInt(a * 255f), 0, 255); return NGUIMath.DecimalToHex8(i); } /// /// The reverse of ParseColor24 -- encodes a color in RrGgBb format. /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public string EncodeColor24 (Color c) { int i = 0xFFFFFF & (NGUIMath.ColorToInt(c) >> 8); return NGUIMath.DecimalToHex24(i); } /// /// The reverse of ParseColor32 -- encodes a color in RrGgBb format. /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public string EncodeColor32 (Color c) { int i = NGUIMath.ColorToInt(c); return NGUIMath.DecimalToHex32(i); } /// /// Parse an embedded symbol, such as [FFAA00] (set color) or [-] (undo color change). Returns whether the index was adjusted. /// static public bool ParseSymbol (string text, ref int index) { int n = 1; bool bold = false; bool italic = false; bool underline = false; bool strikethrough = false; bool ignoreColor = false; return ParseSymbol(text, ref index, null, false, ref n, ref bold, ref italic, ref underline, ref strikethrough, ref ignoreColor); } /// /// Whether the specified character falls under the 'hex' character category (0-9, A-F). /// [System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerStepThrough] static public bool IsHex (char ch) { return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'); } /// /// Parse the symbol, if possible. Returns 'true' if the 'index' was adjusted. /// Advanced symbol support originally contributed by Rudy Pangestu. /// static public bool ParseSymbol (string text, ref int index, BetterList colors, bool premultiply, ref int sub, ref bool bold, ref bool italic, ref bool underline, ref bool strike, ref bool ignoreColor) { int length = text.Length; if (index + 3 > length || text[index] != '[') return false; if (text[index + 2] == ']') { if (text[index + 1] == '-') { if (colors != null && colors.size > 1) colors.RemoveAt(colors.size - 1); index += 3; return true; } string sub3 = text.Substring(index, 3); switch (sub3) { case "[b]": bold = true; index += 3; return true; case "[i]": italic = true; index += 3; return true; case "[u]": underline = true; index += 3; return true; case "[s]": strike = true; index += 3; return true; case "[c]": ignoreColor = true; index += 3; return true; } } if (index + 4 > length) return false; if (text[index + 3] == ']') { string sub4 = text.Substring(index, 4); switch (sub4) { case "[/b]": bold = false; index += 4; return true; case "[/i]": italic = false; index += 4; return true; case "[/u]": underline = false; index += 4; return true; case "[/s]": strike = false; index += 4; return true; case "[/c]": ignoreColor = false; index += 4; return true; default: { char ch0 = text[index + 1]; char ch1 = text[index + 2]; if (IsHex(ch0) && IsHex(ch1)) { int a = (NGUIMath.HexToDecimal(ch0) << 4) | NGUIMath.HexToDecimal(ch1); mAlpha = a / 255f; index += 4; return true; } } break; } } if (index + 5 > length) return false; if (text[index + 4] == ']') { string sub5 = text.Substring(index, 5); switch (sub5) { case "[sub]": sub = 1; index += 5; return true; case "[sup]": sub = 2; index += 5; return true; } } if (index + 6 > length) return false; if (text[index + 5] == ']') { string sub6 = text.Substring(index, 6); switch (sub6) { case "[/sub]": sub = 0; index += 6; return true; case "[/sup]": sub = 0; index += 6; return true; case "[/url]": index += 6; return true; } } if (text[index + 1] == 'u' && text[index + 2] == 'r' && text[index + 3] == 'l' && text[index + 4] == '=') { int closingBracket = text.IndexOf(']', index + 4); if (closingBracket != -1) { index = closingBracket + 1; return true; } else { index = text.Length; return true; } } if (index + 8 > length) return false; if (text[index + 7] == ']') { Color c = ParseColor24(text, index + 1); if (EncodeColor24(c) != text.Substring(index + 1, 6).ToUpper()) return false; if (colors != null) { c.a = colors[colors.size - 1].a; if (premultiply && c.a != 1f) c = Color.Lerp(mInvisible, c, c.a); colors.Add(c); } index += 8; return true; } if (index + 10 > length) return false; if (text[index + 9] == ']') { Color c = ParseColor32(text, index + 1); if (EncodeColor32(c) != text.Substring(index + 1, 8).ToUpper()) return false; if (colors != null) { if (premultiply && c.a != 1f) c = Color.Lerp(mInvisible, c, c.a); colors.Add(c); } index += 10; return true; } return false; } /// /// Runs through the specified string and removes all color-encoding symbols. /// static public string StripSymbols (string text) { if (text != null) { for (int i = 0, imax = text.Length; i < imax; ) { char c = text[i]; if (c == '[') { int sub = 0; bool bold = false; bool italic = false; bool underline = false; bool strikethrough = false; bool ignoreColor = false; int retVal = i; if (ParseSymbol(text, ref retVal, null, false, ref sub, ref bold, ref italic, ref underline, ref strikethrough, ref ignoreColor)) { text = text.Remove(i, retVal - i); imax = text.Length; continue; } } ++i; } } return text; } /// /// Align the vertices to be right or center-aligned given the line width specified by NGUIText.lineWidth. /// static public void Align (BetterList verts, int indexOffset, float printedWidth) { switch (alignment) { case Alignment.Right: { float padding = rectWidth - printedWidth; if (padding < 0f) return; #if UNITY_FLASH for (int i = indexOffset; i < verts.size; ++i) verts.buffer[i] = verts.buffer[i] + new Vector3(padding, 0f); #else for (int i = indexOffset; i < verts.size; ++i) verts.buffer[i].x += padding; #endif break; } case Alignment.Center: { float padding = (rectWidth - printedWidth) * 0.5f; if (padding < 0f) return; // Keep it pixel-perfect int diff = Mathf.RoundToInt(rectWidth - printedWidth); int intWidth = Mathf.RoundToInt(rectWidth); bool oddDiff = (diff & 1) == 1; bool oddWidth = (intWidth & 1) == 1; if ((oddDiff && !oddWidth) || (!oddDiff && oddWidth)) padding += 0.5f * fontScale; #if UNITY_FLASH for (int i = indexOffset; i < verts.size; ++i) verts.buffer[i] = verts.buffer[i] + new Vector3(padding, 0f); #else for (int i = indexOffset; i < verts.size; ++i) verts.buffer[i].x += padding; #endif break; } case Alignment.Justified: { // Printed text needs to reach at least 65% of the width in order to be justified if (printedWidth < rectWidth * 0.65f) return; // There must be some padding involved float padding = (rectWidth - printedWidth) * 0.5f; if (padding < 1f) return; // There must be at least two characters int chars = (verts.size - indexOffset) / 4; if (chars < 1) return; float progressPerChar = 1f / (chars - 1); float scale = rectWidth / printedWidth; for (int i = indexOffset + 4, charIndex = 1; i < verts.size; ++charIndex) { float x0 = verts.buffer[i].x; float x1 = verts.buffer[i + 2].x; float w = x1 - x0; float x0a = x0 * scale; float x1a = x0a + w; float x1b = x1 * scale; float x0b = x1b - w; float progress = charIndex * progressPerChar; x0 = Mathf.Lerp(x0a, x0b, progress); x1 = Mathf.Lerp(x1a, x1b, progress); x0 = Mathf.Round(x0); x1 = Mathf.Round(x1); #if UNITY_FLASH verts.buffer[i] = verts.buffer[i] + new Vector3(x0, 0f); verts.buffer[i+1] = verts.buffer[i+1] + new Vector3(x0, 0f); verts.buffer[i+2] = verts.buffer[i+2] + new Vector3(x1, 0f); verts.buffer[i+3] = verts.buffer[i+3] + new Vector3(x1, 0f); i += 4; #else verts.buffer[i++].x = x0; verts.buffer[i++].x = x0; verts.buffer[i++].x = x1; verts.buffer[i++].x = x1; #endif } break; } } } /// /// Get the index of the closest character within the provided list of values. /// Meant to be used with the arrays created by PrintExactCharacterPositions(). /// static public int GetExactCharacterIndex (BetterList verts, BetterList indices, Vector2 pos) { for (int i = 0; i < indices.size; ++i) { int i0 = (i << 1); int i1 = i0 + 1; float x0 = verts[i0].x; if (pos.x < x0) continue; float x1 = verts[i1].x; if (pos.x > x1) continue; float y0 = verts[i0].y; if (pos.y < y0) continue; float y1 = verts[i1].y; if (pos.y > y1) continue; return indices[i]; } return 0; } /// /// Get the index of the closest vertex within the provided list of values. /// This function first sorts by Y, and only then by X. /// Meant to be used with the arrays created by PrintApproximateCharacterPositions(). /// static public int GetApproximateCharacterIndex (BetterList verts, BetterList indices, Vector2 pos) { // First sort by Y, and only then by X float bestX = float.MaxValue; float bestY = float.MaxValue; int bestIndex = 0; for (int i = 0; i < verts.size; ++i) { float diffY = Mathf.Abs(pos.y - verts[i].y); if (diffY > bestY) continue; float diffX = Mathf.Abs(pos.x - verts[i].x); if (diffY < bestY) { bestY = diffY; bestX = diffX; bestIndex = i; } else if (diffX < bestX) { bestX = diffX; bestIndex = i; } } return indices[bestIndex]; } /// /// Whether the specified character is a space. /// [DebuggerHidden] [DebuggerStepThrough] static bool IsSpace (int ch) { return (ch == ' ' || ch == 0x200a || ch == 0x200b || ch == '\u2009'); } /// /// Convenience function that ends the line by either appending a new line character or replacing a space with one. /// [DebuggerHidden] [DebuggerStepThrough] static public void EndLine (ref StringBuilder s) { int i = s.Length - 1; if (i > 0 && IsSpace(s[i])) s[i] = '\n'; else s.Append('\n'); } /// /// Convenience function that ends the line by replacing a space with a newline character. /// [DebuggerHidden] [DebuggerStepThrough] static void ReplaceSpaceWithNewline (ref StringBuilder s) { int i = s.Length - 1; if (i > 0 && IsSpace(s[i])) s[i] = '\n'; } /// /// Get the printed size of the specified string. The returned value is in pixels. /// static public Vector2 CalculatePrintedSize (string text) { Vector2 v = Vector2.zero; if (!string.IsNullOrEmpty(text)) { // When calculating printed size, get rid of all symbols first since they are invisible anyway if (encoding) text = StripSymbols(text); // Ensure we have characters to work with Prepare(text); float x = 0f, y = 0f, maxX = 0f; int textLength = text.Length, ch = 0, prev = 0; for (int i = 0; i < textLength; ++i) { ch = text[i]; // Start a new line if (ch == '\n') { if (x > maxX) maxX = x; x = 0f; y += finalLineHeight; continue; } // Skip invalid characters if (ch < ' ') continue; // See if there is a symbol matching this text BMSymbol symbol = useSymbols ? GetSymbol(text, i, textLength) : null; if (symbol == null) { float w = GetGlyphWidth(ch, prev); if (w != 0f) { w += finalSpacingX; if (Mathf.RoundToInt(x + w) > regionWidth) { if (x > maxX) maxX = x - finalSpacingX; x = w; y += finalLineHeight; } else x += w; prev = ch; } } else { float w = finalSpacingX + symbol.advance * fontScale; if (Mathf.RoundToInt(x + w) > regionWidth) { if (x > maxX) maxX = x - finalSpacingX; x = w; y += finalLineHeight; } else x += w; i += symbol.sequence.Length - 1; prev = 0; } } v.x = ((x > maxX) ? x - finalSpacingX : maxX); v.y = (y + finalLineHeight); } return v; } static BetterList mSizes = new BetterList(); /// /// Calculate the character index offset required to print the end of the specified text. /// static public int CalculateOffsetToFit (string text) { if (string.IsNullOrEmpty(text) || regionWidth < 1) return 0; Prepare(text); int textLength = text.Length, ch = 0, prev = 0; for (int i = 0, imax = text.Length; i < imax; ++i) { // See if there is a symbol matching this text BMSymbol symbol = useSymbols ? GetSymbol(text, i, textLength) : null; if (symbol == null) { ch = text[i]; float w = GetGlyphWidth(ch, prev); if (w != 0f) mSizes.Add(finalSpacingX + w); prev = ch; } else { mSizes.Add(finalSpacingX + symbol.advance * fontScale); for (int b = 0, bmax = symbol.sequence.Length - 1; b < bmax; ++b) mSizes.Add(0); i += symbol.sequence.Length - 1; prev = 0; } } float remainingWidth = regionWidth; int currentCharacterIndex = mSizes.size; while (currentCharacterIndex > 0 && remainingWidth > 0) remainingWidth -= mSizes[--currentCharacterIndex]; mSizes.Clear(); if (remainingWidth < 0) ++currentCharacterIndex; return currentCharacterIndex; } /// /// Get the end of line that would fit into a field of given width. /// static public string GetEndOfLineThatFits (string text) { int textLength = text.Length; int offset = CalculateOffsetToFit(text); return text.Substring(offset, textLength - offset); } /// /// Text wrapping functionality. The 'width' and 'height' should be in pixels. /// static public bool WrapText (string text, out string finalText) { return WrapText(text, out finalText, false); } /// /// Text wrapping functionality. The 'width' and 'height' should be in pixels. /// static public bool WrapText (string text, out string finalText, bool keepCharCount) { if (regionWidth < 1 || regionHeight < 1 || finalLineHeight < 1f) { finalText = ""; return false; } float height = (maxLines > 0) ? Mathf.Min(regionHeight, finalLineHeight * maxLines) : regionHeight; int maxLineCount = (maxLines > 0) ? maxLines : 1000000; maxLineCount = Mathf.FloorToInt(Mathf.Min(maxLineCount, height / finalLineHeight) + 0.01f); if (maxLineCount == 0) { finalText = ""; return false; } if (string.IsNullOrEmpty(text)) text = " "; Prepare(text); StringBuilder sb = new StringBuilder(); int textLength = text.Length; float remainingWidth = regionWidth; int start = 0, offset = 0, lineCount = 1, prev = 0; int oldOffset = 0; // add by chenbin string symbolStr = ""; // add by chenbin bool lineIsEmpty = true; bool fits = true; bool eastern = false; // Run through all characters for (; offset < textLength; ++offset) { char ch = text[offset]; if (ch > 12287) eastern = true; // New line character -- start a new line if (ch == '\n') { if (lineCount == maxLineCount) break; remainingWidth = regionWidth; // Add the previous word to the final string if (start < offset) sb.Append(text.Substring(start, offset - start + 1)); else sb.Append(ch); lineIsEmpty = true; ++lineCount; start = offset + 1; prev = 0; continue; } // When encoded symbols such as [RrGgBb] or [-] are encountered, skip past them oldOffset = offset; // add by chenbin if (encoding && ParseSymbol(text, ref offset)) { symbolStr = text.Substring(oldOffset, offset-oldOffset); // add by chenbin --offset; continue; } // See if there is a symbol matching this text BMSymbol symbol = useSymbols ? GetSymbol(text, offset, textLength) : null; // Calculate how wide this symbol or character is going to be float glyphWidth; if (symbol == null) { // Find the glyph for this character float w = GetGlyphWidth(ch, prev); if (w == 0f) continue; glyphWidth = finalSpacingX + w; } else glyphWidth = finalSpacingX + symbol.advance * fontScale; // Reduce the width remainingWidth -= glyphWidth; // If this marks the end of a word, add it to the final string. if (IsSpace(ch) && !eastern && start < offset) { int end = offset - start + 1; // Last word on the last line should not include an invisible character if (lineCount == maxLineCount && remainingWidth <= 0f && offset < textLength) { char cho = text[offset]; if (cho < ' ' || IsSpace(cho)) --end; } sb.Append(text.Substring(start, end)); lineIsEmpty = false; start = offset + 1; prev = ch; } // Doesn't fit? if (Mathf.RoundToInt(remainingWidth) < 0) { // Can't start a new line if (lineIsEmpty || lineCount == maxLineCount) { // This is the first word on the line -- add it up to the character that fits sb.Append(text.Substring(start, Mathf.Max(0, offset - start))); bool space = IsSpace(ch); if (!space && !eastern) fits = false; if (lineCount++ == maxLineCount) { start = offset; break; } #region add by chenbin 目的是为了多行时可以split出每行的内容 procSymbol4MultLines(ref sb, symbolStr); #endregion if (keepCharCount) ReplaceSpaceWithNewline(ref sb); else EndLine(ref sb); // Start a brand-new line lineIsEmpty = true; if (space) { start = offset + 1; remainingWidth = regionWidth; } else { start = offset; remainingWidth = regionWidth - glyphWidth; } prev = 0; } else { // Revert the position to the beginning of the word and reset the line lineIsEmpty = true; remainingWidth = regionWidth; offset = start - 1; prev = 0; if (lineCount++ == maxLineCount) break; #region add by chenbin 目的是为了多行时可以split出每行的内容 procSymbol4MultLines(ref sb, symbolStr); #endregion if (keepCharCount) ReplaceSpaceWithNewline(ref sb); else EndLine(ref sb); continue; } } else prev = ch; // Advance the offset past the symbol if (symbol != null) { offset += symbol.length - 1; prev = 0; } } if (start < offset) sb.Append(text.Substring(start, offset - start)); finalText = sb.ToString(); return fits && ((offset == textLength) || (lineCount <= Mathf.Min(maxLines, maxLineCount))); } #region add by chenbin static void procSymbol4MultLines(ref StringBuilder sb, string symbolStr) { if(!string.IsNullOrEmpty(symbolStr)) { // only proc [RrGgBb] if(symbolStr.Length == 8) { sb.Append ("[-]"); sb.Append ('\n'); sb.Append (symbolStr); } else { sb.Append ('\n'); } } else { sb.Append ('\n'); } } #endregion static Color32 s_c0, s_c1; /// /// Print the specified text into the buffers. /// static public void Print (string text, BetterList verts, BetterList uvs, BetterList cols) { if (string.IsNullOrEmpty(text)) return; int indexOffset = verts.size; Prepare(text); // Start with the white tint mColors.Add(Color.white); mAlpha = 1f; int ch = 0, prev = 0; float x = 0f, y = 0f, maxX = 0f; float sizeF = finalSize; Color gb = tint * gradientBottom; Color gt = tint * gradientTop; Color32 uc = tint; int textLength = text.Length; Rect uvRect = new Rect(); float invX = 0f, invY = 0f; float sizePD = sizeF * pixelDensity; // Advanced symbol support contributed by Rudy Pangestu. bool subscript = false; int subscriptMode = 0; // 0 = normal, 1 = subscript, 2 = superscript bool bold = false; bool italic = false; bool underline = false; bool strikethrough = false; bool ignoreColor = false; const float sizeShrinkage = 0.75f; float v0x; float v1x; float v1y; float v0y; float prevX = 0; if (bitmapFont != null) { uvRect = bitmapFont.uvRect; invX = uvRect.width / bitmapFont.texWidth; invY = uvRect.height / bitmapFont.texHeight; } for (int i = 0; i < textLength; ++i) { ch = text[i]; prevX = x; // New line character -- skip to the next line if (ch == '\n') { if (x > maxX) maxX = x; if (alignment != Alignment.Left) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } x = 0; y += finalLineHeight; prev = 0; continue; } // Invalid character -- skip it if (ch < ' ') { prev = ch; continue; } // Color changing symbol if (encoding && ParseSymbol(text, ref i, mColors, premultiply, ref subscriptMode, ref bold, ref italic, ref underline, ref strikethrough, ref ignoreColor)) { Color fc; if (ignoreColor) { fc = mColors[mColors.size - 1]; fc.a *= mAlpha * tint.a; } else { fc = tint * mColors[mColors.size - 1]; fc.a *= mAlpha; } uc = fc; for (int b = 0, bmax = mColors.size - 2; b < bmax; ++b) fc.a *= mColors[b].a; if (gradient) { gb = gradientBottom * fc; gt = gradientTop * fc; } --i; continue; } // See if there is a symbol matching this text BMSymbol symbol = useSymbols ? GetSymbol(text, i, textLength) : null; if (symbol != null) { v0x = x + symbol.offsetX * fontScale; v1x = v0x + symbol.width * fontScale; v1y = -(y + symbol.offsetY * fontScale); v0y = v1y - symbol.height * fontScale; // Doesn't fit? Move down to the next line if (Mathf.RoundToInt(x + symbol.advance * fontScale) > regionWidth) { if (x == 0f) return; if (alignment != Alignment.Left && indexOffset < verts.size) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } v0x -= x; v1x -= x; v0y -= finalLineHeight; v1y -= finalLineHeight; x = 0; y += finalLineHeight; prevX = 0; } verts.Add(new Vector3(v0x, v0y)); verts.Add(new Vector3(v0x, v1y)); verts.Add(new Vector3(v1x, v1y)); verts.Add(new Vector3(v1x, v0y)); x += finalSpacingX + symbol.advance * fontScale; i += symbol.length - 1; prev = 0; if (uvs != null) { Rect uv = symbol.uvRect; float u0x = uv.xMin; float u0y = uv.yMin; float u1x = uv.xMax; float u1y = uv.yMax; uvs.Add(new Vector2(u0x, u0y)); uvs.Add(new Vector2(u0x, u1y)); uvs.Add(new Vector2(u1x, u1y)); uvs.Add(new Vector2(u1x, u0y)); } if (cols != null) { if (symbolStyle == SymbolStyle.Colored) { for (int b = 0; b < 4; ++b) cols.Add(uc); } else { Color32 col = Color.white; col.a = uc.a; for (int b = 0; b < 4; ++b) cols.Add(col); } } } else // No symbol present { GlyphInfo glyph = GetGlyph(ch, prev); if (glyph == null) continue; prev = ch; if (subscriptMode != 0) { glyph.v0.x *= sizeShrinkage; glyph.v0.y *= sizeShrinkage; glyph.v1.x *= sizeShrinkage; glyph.v1.y *= sizeShrinkage; if (subscriptMode == 1) { glyph.v0.y -= fontScale * fontSize * 0.4f; glyph.v1.y -= fontScale * fontSize * 0.4f; } else { glyph.v0.y += fontScale * fontSize * 0.05f; glyph.v1.y += fontScale * fontSize * 0.05f; } } v0x = glyph.v0.x + x; v0y = glyph.v0.y - y; v1x = glyph.v1.x + x; v1y = glyph.v1.y - y; float w = glyph.advance; if (finalSpacingX < 0f) w += finalSpacingX; // Doesn't fit? Move down to the next line if (Mathf.RoundToInt(x + w) > regionWidth) { if (x == 0f) return; if (alignment != Alignment.Left && indexOffset < verts.size) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } v0x -= x; v1x -= x; v0y -= finalLineHeight; v1y -= finalLineHeight; x = 0; y += finalLineHeight; prevX = 0; } if (IsSpace(ch)) { if (underline) { ch = '_'; } else if (strikethrough) { ch = '-'; } } // Advance the position x += (subscriptMode == 0) ? finalSpacingX + glyph.advance : (finalSpacingX + glyph.advance) * sizeShrinkage; // No need to continue if this is a space character if (IsSpace(ch)) continue; // Texture coordinates if (uvs != null) { if (bitmapFont != null) { glyph.u0.x = uvRect.xMin + invX * glyph.u0.x; glyph.u2.x = uvRect.xMin + invX * glyph.u2.x; glyph.u0.y = uvRect.yMax - invY * glyph.u0.y; glyph.u2.y = uvRect.yMax - invY * glyph.u2.y; glyph.u1.x = glyph.u0.x; glyph.u1.y = glyph.u2.y; glyph.u3.x = glyph.u2.x; glyph.u3.y = glyph.u0.y; } for (int j = 0, jmax = (bold ? 4 : 1); j < jmax; ++j) { uvs.Add(glyph.u0); uvs.Add(glyph.u1); uvs.Add(glyph.u2); uvs.Add(glyph.u3); } } // Vertex colors if (cols != null) { if (glyph.channel == 0 || glyph.channel == 15) { if (gradient) { float min = sizePD + glyph.v0.y / fontScale; float max = sizePD + glyph.v1.y / fontScale; min /= sizePD; max /= sizePD; s_c0 = Color.Lerp(gb, gt, min); s_c1 = Color.Lerp(gb, gt, max); for (int j = 0, jmax = (bold ? 4 : 1); j < jmax; ++j) { cols.Add(s_c0); cols.Add(s_c1); cols.Add(s_c1); cols.Add(s_c0); } } else { for (int j = 0, jmax = (bold ? 16 : 4); j < jmax; ++j) cols.Add(uc); } } else { // Packed fonts come as alpha masks in each of the RGBA channels. // In order to use it we need to use a special shader. // // Limitations: // - Effects (drop shadow, outline) will not work. // - Should not be a part of the atlas (eastern fonts rarely are anyway). // - Lower color precision Color col = uc; col *= 0.49f; switch (glyph.channel) { case 1: col.b += 0.51f; break; case 2: col.g += 0.51f; break; case 4: col.r += 0.51f; break; case 8: col.a += 0.51f; break; } Color32 c = col; for (int j = 0, jmax = (bold ? 16 : 4); j < jmax; ++j) cols.Add(c); } } // Bold and italic contributed by Rudy Pangestu. if (!bold) { if (!italic) { verts.Add(new Vector3(v0x, v0y)); verts.Add(new Vector3(v0x, v1y)); verts.Add(new Vector3(v1x, v1y)); verts.Add(new Vector3(v1x, v0y)); } else // Italic { float slant = fontSize * 0.1f * ((v1y - v0y) / fontSize); verts.Add(new Vector3(v0x - slant, v0y)); verts.Add(new Vector3(v0x + slant, v1y)); verts.Add(new Vector3(v1x + slant, v1y)); verts.Add(new Vector3(v1x - slant, v0y)); } } else // Bold { for (int j = 0; j < 4; ++j) { float a = mBoldOffset[j * 2]; float b = mBoldOffset[j * 2 + 1]; float slant = (italic ? fontSize * 0.1f * ((v1y - v0y) / fontSize) : 0f); verts.Add(new Vector3(v0x + a - slant, v0y + b)); verts.Add(new Vector3(v0x + a + slant, v1y + b)); verts.Add(new Vector3(v1x + a + slant, v1y + b)); verts.Add(new Vector3(v1x + a - slant, v0y + b)); } } // Underline and strike-through contributed by Rudy Pangestu. if (underline || strikethrough) { GlyphInfo dash = GetGlyph(strikethrough ? '-' : '_', prev); if (dash == null) continue; if (uvs != null) { if (bitmapFont != null) { dash.u0.x = uvRect.xMin + invX * dash.u0.x; dash.u2.x = uvRect.xMin + invX * dash.u2.x; dash.u0.y = uvRect.yMax - invY * dash.u0.y; dash.u2.y = uvRect.yMax - invY * dash.u2.y; } float cx = (dash.u0.x + dash.u2.x) * 0.5f; for (int j = 0, jmax = (bold ? 4 : 1); j < jmax; ++j) { uvs.Add(new Vector2(cx, dash.u0.y)); uvs.Add(new Vector2(cx, dash.u2.y)); uvs.Add(new Vector2(cx, dash.u2.y)); uvs.Add(new Vector2(cx, dash.u0.y)); } } if (subscript && strikethrough) { v0y = (-y + dash.v0.y) * sizeShrinkage; v1y = (-y + dash.v1.y) * sizeShrinkage; } else { v0y = (-y + dash.v0.y); v1y = (-y + dash.v1.y); } if (bold) { for (int j = 0; j < 4; ++j) { float a = mBoldOffset[j * 2]; float b = mBoldOffset[j * 2 + 1]; verts.Add(new Vector3(prevX + a, v0y + b)); verts.Add(new Vector3(prevX + a, v1y + b)); verts.Add(new Vector3(x + a, v1y + b)); verts.Add(new Vector3(x + a, v0y + b)); } } else { verts.Add(new Vector3(prevX, v0y)); verts.Add(new Vector3(prevX, v1y)); verts.Add(new Vector3(x, v1y)); verts.Add(new Vector3(x, v0y)); } if (gradient) { float min = sizePD + dash.v0.y / fontScale; float max = sizePD + dash.v1.y / fontScale; min /= sizePD; max /= sizePD; s_c0 = Color.Lerp(gb, gt, min); s_c1 = Color.Lerp(gb, gt, max); for (int j = 0, jmax = (bold ? 4 : 1); j < jmax; ++j) { cols.Add(s_c0); cols.Add(s_c1); cols.Add(s_c1); cols.Add(s_c0); } } else { for (int j = 0, jmax = (bold ? 16 : 4); j < jmax; ++j) cols.Add(uc); } } } } if (alignment != Alignment.Left && indexOffset < verts.size) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } mColors.Clear(); } static float[] mBoldOffset = new float[] { -0.25f, 0f, 0.25f, 0f, 0f, -0.25f, 0f, 0.25f }; /// /// Print character positions and indices into the specified buffer. Meant to be used with the "find closest vertex" calculations. /// static public void PrintApproximateCharacterPositions (string text, BetterList verts, BetterList indices) { if (string.IsNullOrEmpty(text)) text = " "; Prepare(text); float x = 0f, y = 0f, maxX = 0f, halfSize = fontSize * fontScale * 0.5f; int textLength = text.Length, indexOffset = verts.size, ch = 0, prev = 0; for (int i = 0; i < textLength; ++i) { ch = text[i]; verts.Add(new Vector3(x, -y - halfSize)); indices.Add(i); if (ch == '\n') { if (x > maxX) maxX = x; if (alignment != Alignment.Left) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } x = 0; y += finalLineHeight; prev = 0; continue; } else if (ch < ' ') { prev = 0; continue; } if (encoding && ParseSymbol(text, ref i)) { --i; continue; } // See if there is a symbol matching this text BMSymbol symbol = useSymbols ? GetSymbol(text, i, textLength) : null; if (symbol == null) { float w = GetGlyphWidth(ch, prev); if (w != 0f) { w += finalSpacingX; if (Mathf.RoundToInt(x + w) > regionWidth) { if (x == 0f) return; if (alignment != Alignment.Left && indexOffset < verts.size) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } x = w; y += finalLineHeight; } else x += w; verts.Add(new Vector3(x, -y - halfSize)); indices.Add(i + 1); prev = ch; } } else { float w = symbol.advance * fontScale + finalSpacingX; if (Mathf.RoundToInt(x + w) > regionWidth) { if (x == 0f) return; if (alignment != Alignment.Left && indexOffset < verts.size) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } x = w; y += finalLineHeight; } else x += w; verts.Add(new Vector3(x, -y - halfSize)); indices.Add(i + 1); i += symbol.sequence.Length - 1; prev = 0; } } if (alignment != Alignment.Left && indexOffset < verts.size) Align(verts, indexOffset, x - finalSpacingX); } /// /// Print character positions and indices into the specified buffer. /// This function's data is meant to be used for precise character selection, such as clicking on a link. /// There are 2 vertices for every index: Bottom Left + Top Right. /// static public void PrintExactCharacterPositions (string text, BetterList verts, BetterList indices) { if (string.IsNullOrEmpty(text)) text = " "; Prepare(text); float fullSize = fontSize * fontScale; float x = 0f, y = 0f, maxX = 0f; int textLength = text.Length, indexOffset = verts.size, ch = 0, prev = 0; for (int i = 0; i < textLength; ++i) { ch = text[i]; if (ch == '\n') { if (x > maxX) maxX = x; if (alignment != Alignment.Left) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } x = 0; y += finalLineHeight; prev = 0; continue; } else if (ch < ' ') { prev = 0; continue; } if (encoding && ParseSymbol(text, ref i)) { --i; continue; } // See if there is a symbol matching this text BMSymbol symbol = useSymbols ? GetSymbol(text, i, textLength) : null; if (symbol == null) { float gw = GetGlyphWidth(ch, prev); if (gw != 0f) { float w = gw + finalSpacingX; if (Mathf.RoundToInt(x + w) > regionWidth) { if (x == 0f) return; if (alignment != Alignment.Left && indexOffset < verts.size) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } x = 0f; y += finalLineHeight; prev = 0; --i; continue; } indices.Add(i); verts.Add(new Vector3(x, -y - fullSize)); verts.Add(new Vector3(x + w, -y)); prev = ch; x += w; } } else { float w = symbol.advance * fontScale + finalSpacingX; if (Mathf.RoundToInt(x + w) > regionWidth) { if (x == 0f) return; if (alignment != Alignment.Left && indexOffset < verts.size) { Align(verts, indexOffset, x - finalSpacingX); indexOffset = verts.size; } x = 0f; y += finalLineHeight; prev = 0; --i; continue; } indices.Add(i); verts.Add(new Vector3(x, -y - fullSize)); verts.Add(new Vector3(x + w, -y)); i += symbol.sequence.Length - 1; x += w; prev = 0; } } if (alignment != Alignment.Left && indexOffset < verts.size) Align(verts, indexOffset, x - finalSpacingX); } /// /// Print the caret and selection vertices. Note that it's expected that 'text' has been stripped clean of symbols. /// static public void PrintCaretAndSelection (string text, int start, int end, BetterList caret, BetterList highlight) { if (string.IsNullOrEmpty(text)) text = " "; Prepare(text); int caretPos = end; if (start > end) { end = start; start = caretPos; } float x = 0f, y = 0f, maxX = 0f, fs = fontSize * fontScale; int caretOffset = (caret != null) ? caret.size : 0; int highlightOffset = (highlight != null) ? highlight.size : 0; int textLength = text.Length, index = 0, ch = 0, prev = 0; bool highlighting = false, caretSet = false; Vector2 last0 = Vector2.zero; Vector2 last1 = Vector2.zero; for (; index < textLength; ++index) { // Print the caret if (caret != null && !caretSet && caretPos <= index) { caretSet = true; caret.Add(new Vector3(x - 1f, -y - fs)); caret.Add(new Vector3(x - 1f, -y)); caret.Add(new Vector3(x + 1f, -y)); caret.Add(new Vector3(x + 1f, -y - fs)); } ch = text[index]; if (ch == '\n') { // Used for alignment purposes if (x > maxX) maxX = x; // Align the caret if (caret != null && caretSet) { if (alignment != Alignment.Left) Align(caret, caretOffset, x - finalSpacingX); caret = null; } if (highlight != null) { if (highlighting) { // Close the selection on this line highlighting = false; highlight.Add(last1); highlight.Add(last0); } else if (start <= index && end > index) { // This must be an empty line. Add a narrow vertical highlight. highlight.Add(new Vector3(x, -y - fs)); highlight.Add(new Vector3(x, -y)); highlight.Add(new Vector3(x + 2f, -y)); highlight.Add(new Vector3(x + 2f, -y - fs)); } // Align the highlight if (alignment != Alignment.Left && highlightOffset < highlight.size) { Align(highlight, highlightOffset, x - finalSpacingX); highlightOffset = highlight.size; } } x = 0; y += finalLineHeight; prev = 0; continue; } else if (ch < ' ') { prev = 0; continue; } if (encoding && ParseSymbol(text, ref index)) { --index; continue; } // See if there is a symbol matching this text BMSymbol symbol = useSymbols ? GetSymbol(text, index, textLength) : null; float w = (symbol != null) ? symbol.advance * fontScale : GetGlyphWidth(ch, prev); if (w != 0f) { float v0x = x; float v1x = x + w; float v0y = -y - fs; float v1y = -y; if (Mathf.RoundToInt(v1x + finalSpacingX) > regionWidth) { if (x == 0f) return; // Used for alignment purposes if (x > maxX) maxX = x; // Align the caret if (caret != null && caretSet) { if (alignment != Alignment.Left) Align(caret, caretOffset, x - finalSpacingX); caret = null; } if (highlight != null) { if (highlighting) { // Close the selection on this line highlighting = false; highlight.Add(last1); highlight.Add(last0); } else if (start <= index && end > index) { // This must be an empty line. Add a narrow vertical highlight. highlight.Add(new Vector3(x, -y - fs)); highlight.Add(new Vector3(x, -y)); highlight.Add(new Vector3(x + 2f, -y)); highlight.Add(new Vector3(x + 2f, -y - fs)); } // Align the highlight if (alignment != Alignment.Left && highlightOffset < highlight.size) { Align(highlight, highlightOffset, x - finalSpacingX); highlightOffset = highlight.size; } } v0x -= x; v1x -= x; v0y -= finalLineHeight; v1y -= finalLineHeight; x = 0; y += finalLineHeight; } x += w + finalSpacingX; // Print the highlight if (highlight != null) { if (start > index || end <= index) { if (highlighting) { // Finish the highlight highlighting = false; highlight.Add(last1); highlight.Add(last0); } } else if (!highlighting) { // Start the highlight highlighting = true; highlight.Add(new Vector3(v0x, v0y)); highlight.Add(new Vector3(v0x, v1y)); } } // Save what the character ended with last0 = new Vector2(v1x, v0y); last1 = new Vector2(v1x, v1y); prev = ch; } } // Ensure we always have a caret if (caret != null) { if (!caretSet) { caret.Add(new Vector3(x - 1f, -y - fs)); caret.Add(new Vector3(x - 1f, -y)); caret.Add(new Vector3(x + 1f, -y)); caret.Add(new Vector3(x + 1f, -y - fs)); } if (alignment != Alignment.Left) Align(caret, caretOffset, x - finalSpacingX); } // Close the selection if (highlight != null) { if (highlighting) { // Finish the highlight highlight.Add(last1); highlight.Add(last0); } else if (start < index && end == index) { // Happens when highlight ends on an empty line. Highlight it with a thin line. highlight.Add(new Vector3(x, -y - fs)); highlight.Add(new Vector3(x, -y)); highlight.Add(new Vector3(x + 2f, -y)); highlight.Add(new Vector3(x + 2f, -y - fs)); } // Align the highlight if (alignment != Alignment.Left && highlightOffset < highlight.size) Align(highlight, highlightOffset, x - finalSpacingX); } } }