using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Extensions; using MLEM.Misc; namespace MLEM.Font { /// /// Represents a font with additional abilities. /// /// public abstract class GenericFont : GenericDataHolder { /// /// This field holds the unicode representation of a one em space. /// This is a character that isn't drawn, but has the same width as . /// Whereas a regular would have to explicitly support this character for width calculations, generic fonts implicitly support it in . /// public const char OneEmSpace = '\u2003'; /// /// This field holds the unicode representation of a non-breaking space. /// Whereas a regular would have to explicitly support this character for width calculations, generic fonts implicitly support it in . /// public const char Nbsp = '\u00A0'; /// /// This field holds the unicode representation of a zero-width space. /// Whereas a regular would have to explicitly support this character for width calculations and string splitting, generic fonts implicitly support it in and . /// public const char Zwsp = '\u200B'; /// /// The bold version of this font. /// public abstract GenericFont Bold { get; } /// /// The italic version of this font. /// public abstract GenericFont Italic { get; } /// /// The height of each line of text of this font. /// This is the value that the text's draw position is offset by every time a newline character is reached. /// public abstract float LineHeight { get; } /// /// Measures the width of the given character with the default scale for use in . /// Note that this method does not support , and for most generic fonts, which is why should be used even for single characters. /// /// The character whose width to calculate /// The width of the given character with the default scale protected abstract float MeasureChar(char c); /// public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth); /// public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth); /// public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) { this.DrawString(batch, text, position, color, rotation, origin, new Vector2(scale), effects, layerDepth); } /// public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) { this.DrawString(batch, text, position, color, rotation, origin, new Vector2(scale), effects, layerDepth); } /// public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) { this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0); } /// public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) { this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0); } /// /// Measures the width of the given string when drawn with this font's underlying font. /// This method uses internally to calculate the size of known characters and calculates additional characters like , and . /// If the text contains newline characters (\n), the size returned will represent a rectangle that encompasses the width of the longest line and the string's full height. /// /// The text whose size to calculate /// Whether trailing whitespace should be ignored in the returned size, causing the end of each line to be effectively trimmed /// The size of the string when drawn with this font public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) { var size = Vector2.Zero; if (text.Length <= 0) return size; var xOffset = 0F; for (var i = 0; i < text.Length; i++) { switch (text[i]) { case '\n': xOffset = 0; size.Y += this.LineHeight; break; case OneEmSpace: xOffset += this.LineHeight; break; case Nbsp: xOffset += this.MeasureChar(' '); break; case Zwsp: // don't add width for a zero-width space break; case ' ': if (ignoreTrailingSpaces && IsTrailingSpace(text, i)) { // if this is a trailing space, we can skip remaining spaces too i = text.Length - 1; break; } xOffset += this.MeasureChar(' '); break; default: xOffset += this.MeasureChar(text[i]); break; } // increase x size if this line is the longest if (xOffset > size.X) size.X = xOffset; } // include the last line's height too! size.Y += this.LineHeight; return size; } /// /// Truncates a string to a given width. If the string's displayed area is larger than the maximum width, the string is cut off. /// Optionally, the string can be cut off a bit sooner, adding the at the end instead. /// /// The text to truncate /// The maximum width, in display pixels based on the font and scale /// The scale to use for width measurements /// If the string should be truncated from the back rather than the front /// The characters to add to the end of the string if it is too long /// The truncated string, or the same string if it is shorter than the maximum width public string TruncateString(string text, float width, float scale, bool fromBack = false, string ellipsis = "") { var total = new StringBuilder(); var ellipsisWidth = this.MeasureString(ellipsis).X * scale; for (var i = 0; i < text.Length; i++) { if (fromBack) { total.Insert(0, text[text.Length - 1 - i]); } else { total.Append(text[i]); } if (this.MeasureString(total.ToString()).X * scale + ellipsisWidth >= width) { if (fromBack) { return total.Remove(0, 1).Insert(0, ellipsis).ToString(); } else { return total.Remove(total.Length - 1, 1).Append(ellipsis).ToString(); } } } return total.ToString(); } /// /// Splits a string to a given maximum width, adding newline characters between each line. /// Also splits long words and supports zero-width spaces and takes into account existing newline characters in the passed . /// See for a method that differentiates between existing newline characters and splits due to maximum width. /// /// The text to split into multiple lines /// The maximum width that each line should have /// The scale to use for width measurements /// The split string, containing newline characters at each new line public string SplitString(string text, float width, float scale) { return string.Join("\n", this.SplitStringSeparate(text, width, scale)); } /// /// Splits a string to a given maximum width and returns each split section as a separate string. /// Note that existing new lines are taken into account for line length, but not split in the resulting strings. /// This method differs from in that it differentiates between pre-existing newline characters and splits due to maximum width. /// /// The text to split into multiple lines /// The maximum width that each line should have /// The scale to use for width measurements /// The split string as an enumerable of split sections public IEnumerable SplitStringSeparate(string text, float width, float scale) { var currWidth = 0F; var lastSpaceIndex = -1; var widthSinceLastSpace = 0F; var curr = new StringBuilder(); for (var i = 0; i < text.Length; i++) { var c = text[i]; if (c == '\n') { // fake split at pre-defined new lines curr.Append(c); lastSpaceIndex = -1; widthSinceLastSpace = 0; currWidth = 0; } else { var cWidth = this.MeasureString(c.ToCachedString()).X * scale; if (c == ' ' || c == OneEmSpace || c == Zwsp) { // remember the location of this (breaking!) space lastSpaceIndex = curr.Length; widthSinceLastSpace = 0; } else if (currWidth + cWidth >= width) { // check if this line contains a space if (lastSpaceIndex < 0) { // if there is no last space, the word is longer than a line so we split here yield return curr.ToString(); currWidth = 0; curr.Clear(); } else { // split after the last space yield return curr.ToString().Substring(0, lastSpaceIndex + 1); curr.Remove(0, lastSpaceIndex + 1); // we need to restore the width accumulated since the last space for the new line currWidth = widthSinceLastSpace; } widthSinceLastSpace = 0; lastSpaceIndex = -1; } // add current character currWidth += cWidth; widthSinceLastSpace += cWidth; curr.Append(c); } } if (curr.Length > 0) yield return curr.ToString(); } private static bool IsTrailingSpace(string s, int index) { for (var i = index + 1; i < s.Length; i++) { if (s[i] != ' ') return false; } return true; } } }