using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; 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 Emsp = '\u2003'; /// [Obsolete("Use the Emsp field instead.")] public const char OneEmSpace = GenericFont.Emsp; /// /// 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 code point 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 code point whose width to calculate /// The width of the given character with the default scale protected abstract float MeasureCharacter(int codePoint); /// /// Draws the given code point with the given data for use in . /// Note that this method is only called internally. /// /// The sprite batch to draw with. /// The code point which will be drawn. /// A string representation of the character which will be drawn. /// The drawing location on screen. /// A color mask. /// A rotation of this character. /// A scaling of this character. /// Modificators for drawing. Can be combined. /// A depth of the layer of this character. protected abstract void DrawCharacter(SpriteBatch batch, int codePoint, string character, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth); /// public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) { this.DrawString(batch, new CodePointSource(text), position, color, rotation, origin, scale, effects, layerDepth); } /// public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) { this.DrawString(batch, new CodePointSource(text), position, color, rotation, origin, scale, effects, 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) { return this.MeasureString(new CodePointSource(text), ignoreTrailingSpaces); } /// public Vector2 MeasureString(StringBuilder text, bool ignoreTrailingSpaces = false) { return this.MeasureString(new CodePointSource(text), ignoreTrailingSpaces); } /// /// 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 = "") { return GenericFont.TruncateString(Enumerable.Repeat(new DecoratedCodePointSource(new CodePointSource(text), this, 0), 1), width, scale, fromBack, ellipsis).First().ToString(); } /// public StringBuilder TruncateString(StringBuilder text, float width, float scale, bool fromBack = false, string ellipsis = "") { return GenericFont.TruncateString(Enumerable.Repeat(new DecoratedCodePointSource(new CodePointSource(text), this, 0), 1), width, scale, fromBack, ellipsis).First(); } /// /// 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)); } /// public string SplitString(StringBuilder 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) { return GenericFont.SplitStringSeparate(Enumerable.Repeat(new DecoratedCodePointSource(new CodePointSource(text), this, 0), 1), width, scale).First(); } /// public IEnumerable SplitStringSeparate(StringBuilder text, float width, float scale) { return GenericFont.SplitStringSeparate(Enumerable.Repeat(new DecoratedCodePointSource(new CodePointSource(text), this, 0), 1), width, scale).First(); } private Vector2 MeasureString(CodePointSource text, bool ignoreTrailingSpaces) { var size = Vector2.Zero; if (text.Length <= 0) return size; var xOffset = 0F; var index = 0; while (index < text.Length) { var (codePoint, length) = text.GetCodePoint(index); switch (codePoint) { case '\n': xOffset = 0; size.Y += this.LineHeight; break; case GenericFont.Emsp: xOffset += this.LineHeight; break; case GenericFont.Nbsp: xOffset += this.MeasureCharacter(' '); break; case GenericFont.Zwsp: // don't add width for a zero-width space break; case ' ': if (ignoreTrailingSpaces && GenericFont.IsTrailingSpace(text, index)) { // if this is a trailing space, we can skip remaining spaces too index = text.Length - 1; break; } xOffset += this.MeasureCharacter(' '); break; default: xOffset += this.MeasureCharacter(codePoint); break; } // increase x size if this line is the longest if (xOffset > size.X) size.X = xOffset; index += length; } // include the last line's height too! size.Y += this.LineHeight; return size; } private void DrawString(SpriteBatch batch, CodePointSource text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) { var (flipX, flipY) = (0F, 0F); var flippedV = (effects & SpriteEffects.FlipVertically) != 0; var flippedH = (effects & SpriteEffects.FlipHorizontally) != 0; if (flippedV || flippedH) { var size = this.MeasureString(text, false); if (flippedH) { origin.X *= -1; flipX = -size.X; } if (flippedV) { origin.Y *= -1; flipY = this.LineHeight - size.Y; } } var trans = Matrix.Identity; if (rotation == 0) { trans.M11 = flippedH ? -scale.X : scale.X; trans.M22 = flippedV ? -scale.Y : scale.Y; trans.M41 = (flipX - origin.X) * trans.M11 + position.X; trans.M42 = (flipY - origin.Y) * trans.M22 + position.Y; } else { var sin = (float) Math.Sin(rotation); var cos = (float) Math.Cos(rotation); trans.M11 = (flippedH ? -scale.X : scale.X) * cos; trans.M12 = (flippedH ? -scale.X : scale.X) * sin; trans.M21 = (flippedV ? -scale.Y : scale.Y) * -sin; trans.M22 = (flippedV ? -scale.Y : scale.Y) * cos; trans.M41 = (flipX - origin.X) * trans.M11 + (flipY - origin.Y) * trans.M21 + position.X; trans.M42 = (flipX - origin.X) * trans.M12 + (flipY - origin.Y) * trans.M22 + position.Y; } var offset = Vector2.Zero; var index = 0; while (index < text.Length) { var (codePoint, length) = text.GetCodePoint(index); if (codePoint == '\n') { offset.X = 0; offset.Y += this.LineHeight; } else { var character = CodePointSource.ToString(codePoint); var charSize = this.MeasureString(character); var charPos = offset; if (flippedH) charPos.X += charSize.X; if (flippedV) charPos.Y += charSize.Y - this.LineHeight; Vector2.Transform(ref charPos, ref trans, out charPos); this.DrawCharacter(batch, codePoint, character, charPos, color, rotation, scale, effects, layerDepth); offset.X += charSize.X; } index += length; } } internal static IEnumerable> SplitStringSeparate(IEnumerable text, float maxWidth, float scale) { var currWidth = 0F; var lastSpacePart = -1; var lastSpaceIndex = -1; var widthSinceLastSpace = 0F; var curr = new StringBuilder(); var fullSplit = new List>(); foreach (var part in text) { var partSplit = new List(); AddWidth(partSplit, part.ExtraWidth * scale, true); var index = 0; while (index < part.Source.Length) { var (codePoint, length) = part.Source.GetCodePoint(index); if (codePoint == '\n') { // fake split at pre-defined new lines curr.Append('\n'); lastSpacePart = -1; lastSpaceIndex = -1; widthSinceLastSpace = 0; currWidth = 0; } else { var character = CodePointSource.ToString(codePoint); var charWidth = part.Font.MeasureString(character).X * scale; if (codePoint == ' ' || codePoint == GenericFont.Emsp || codePoint == GenericFont.Zwsp) { // remember the location of this (breaking!) space lastSpacePart = fullSplit.Count; lastSpaceIndex = curr.Length; widthSinceLastSpace = 0; // we never want to insert a line break before a space! AddWidth(partSplit, charWidth, false); } else { AddWidth(partSplit, charWidth, true); } curr.Append(character); } index += length; } if (curr.Length > 0) { partSplit.Add(curr.ToString()); curr.Clear(); } fullSplit.Add(partSplit); } return fullSplit; void AddWidth(ICollection partSplit, float width, bool canBreakHere) { if (canBreakHere && currWidth + width >= maxWidth) { // 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 partSplit.Add(curr.ToString()); curr.Clear(); currWidth = 0; } else { if (lastSpacePart < fullSplit.Count) { // the last space exists, but isn't a part of curr, so we have to backtrack and split the previous token var prevPart = fullSplit[lastSpacePart]; var prevCurr = prevPart[prevPart.Count - 1]; prevPart[prevPart.Count - 1] = prevCurr.Substring(0, lastSpaceIndex + 1); prevPart.Add(prevCurr.Substring(lastSpaceIndex + 1)); } else { // split after the last space partSplit.Add(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; lastSpacePart = -1; lastSpaceIndex = -1; } currWidth += width; widthSinceLastSpace += width; } } internal static IEnumerable TruncateString(IEnumerable text, float maxWidth, float scale, bool fromBack, string ellipsis) { var total = new StringBuilder(); var extraWidth = 0F; var endReached = false; foreach (var part in fromBack ? text.Reverse() : text) { var curr = new StringBuilder(); // if we reached the end previously, all the other parts should just be empty if (!endReached) { extraWidth += part.ExtraWidth * scale; var index = 0; while (index < part.Source.Length) { var innerIndex = fromBack ? part.Source.Length - 1 - index : index; var (codePoint, length) = part.Source.GetCodePoint(innerIndex, fromBack); var character = CodePointSource.ToString(codePoint); if (fromBack) { curr.Insert(0, character); total.Insert(0, character); } else { curr.Append(character); total.Append(character); } if (part.Font.MeasureString(new CodePointSource(total + ellipsis), false).X * scale + extraWidth >= maxWidth) { if (fromBack) { curr.Remove(0, length).Insert(0, ellipsis); total.Remove(0, length).Insert(0, ellipsis); } else { curr.Remove(curr.Length - length, length).Append(ellipsis); total.Remove(total.Length - length, length).Append(ellipsis); } endReached = true; break; } index += length; } } yield return curr; } } private static bool IsTrailingSpace(CodePointSource s, int index) { while (index < s.Length) { var (codePoint, length) = s.GetCodePoint(index); if (codePoint != ' ') return false; index += length; } return true; } internal readonly struct DecoratedCodePointSource { public readonly CodePointSource Source; public readonly GenericFont Font; public readonly float ExtraWidth; public DecoratedCodePointSource(CodePointSource source, GenericFont font, float extraWidth) { this.Source = source; this.Font = font; this.ExtraWidth = extraWidth; } } } }