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;
}
}
}
}