2021-11-27 22:45:37 +01:00
using System ;
2021-10-12 03:23:35 +02:00
using System.Collections.Generic ;
2019-08-06 14:20:11 +02:00
using System.Text ;
2020-03-28 22:25:06 +01:00
using Microsoft.Xna.Framework ;
2019-08-06 14:20:11 +02:00
using Microsoft.Xna.Framework.Graphics ;
2021-07-28 17:22:47 +02:00
using MLEM.Extensions ;
2020-09-28 20:38:56 +02:00
using MLEM.Misc ;
2019-08-06 14:20:11 +02:00
2020-03-28 22:25:06 +01:00
namespace MLEM.Font {
2020-05-20 23:59:40 +02:00
/// <summary>
/// Represents a font with additional abilities.
/// <seealso cref="GenericSpriteFont"/>
/// </summary>
2020-09-28 20:38:56 +02:00
public abstract class GenericFont : GenericDataHolder {
2019-08-06 14:20:11 +02:00
2020-06-20 01:18:27 +02:00
/// <summary>
2020-12-08 01:43:52 +01:00
/// This field holds the unicode representation of a one em space.
2020-06-20 01:18:27 +02:00
/// This is a character that isn't drawn, but has the same width as <see cref="LineHeight"/>.
2021-11-28 00:28:17 +01:00
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/>.
2020-06-20 01:18:27 +02:00
/// </summary>
2022-01-24 11:16:23 +01:00
public const char Emsp = ' \ u2003 ' ;
/// <inheritdoc cref="Emsp"/>
[Obsolete("Use the Emsp field instead.")]
2022-06-15 11:38:11 +02:00
public const char OneEmSpace = GenericFont . Emsp ;
2020-07-01 14:30:47 +02:00
/// <summary>
/// This field holds the unicode representation of a non-breaking space.
2021-11-28 00:28:17 +01:00
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/>.
2020-07-01 14:30:47 +02:00
/// </summary>
public const char Nbsp = ' \ u00A0 ' ;
2021-04-14 23:13:19 +02:00
/// <summary>
/// This field holds the unicode representation of a zero-width space.
2021-12-24 12:10:04 +01:00
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations and string splitting, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/> and <see cref="SplitString(string,float,float)"/>.
2021-04-14 23:13:19 +02:00
/// </summary>
public const char Zwsp = ' \ u200B ' ;
2020-06-20 01:18:27 +02:00
2020-05-20 23:59:40 +02:00
/// <summary>
/// The bold version of this font.
/// </summary>
2020-05-17 00:10:29 +02:00
public abstract GenericFont Bold { get ; }
2020-05-20 23:59:40 +02:00
/// <summary>
/// The italic version of this font.
/// </summary>
2020-05-17 00:10:29 +02:00
public abstract GenericFont Italic { get ; }
2021-07-28 17:22:47 +02:00
/// <summary>
/// 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.
/// </summary>
2020-03-28 22:25:06 +01:00
public abstract float LineHeight { get ; }
2019-08-25 19:07:45 +02:00
2021-07-28 17:22:47 +02:00
/// <summary>
2021-11-28 00:28:17 +01:00
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString(string,bool)"/>.
2022-01-24 11:16:23 +01:00
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="Emsp"/> for most generic fonts, which is why <see cref="MeasureString(string,bool)"/> should be used even for single characters.
2021-07-28 17:22:47 +02:00
/// </summary>
/// <param name="c">The character whose width to calculate</param>
/// <returns>The width of the given character with the default scale</returns>
protected abstract float MeasureChar ( char c ) ;
2020-03-28 22:25:06 +01:00
2021-12-22 12:46:17 +01:00
/// <summary>
/// Draws the given character with the given data for use in <see cref="DrawString(Microsoft.Xna.Framework.Graphics.SpriteBatch,System.Text.StringBuilder,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color,float,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,float)"/>.
/// Note that this method is only called internally.
/// </summary>
/// <param name="batch">The sprite batch to draw with.</param>
/// <param name="cString">A string representation of the character which will be drawn.</param>
/// <param name="position">The drawing location on screen.</param>
/// <param name="color">A color mask.</param>
/// <param name="rotation">A rotation of this character.</param>
/// <param name="scale">A scaling of this character.</param>
/// <param name="effects">Modificators for drawing. Can be combined.</param>
/// <param name="layerDepth">A depth of the layer of this character.</param>
2021-12-22 13:00:41 +01:00
protected abstract void DrawChar ( SpriteBatch batch , string cString , Vector2 position , Color color , float rotation , Vector2 scale , SpriteEffects effects , float layerDepth ) ;
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
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 CharSource ( text ) , position , color , rotation , origin , scale , effects , layerDepth ) ;
}
2020-03-28 22:25:06 +01:00
2020-05-20 23:59:40 +02:00
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
2021-12-22 12:46:17 +01:00
public void DrawString ( SpriteBatch batch , StringBuilder text , Vector2 position , Color color , float rotation , Vector2 origin , Vector2 scale , SpriteEffects effects , float layerDepth ) {
2021-12-22 13:00:41 +01:00
this . DrawString ( batch , new CharSource ( text ) , position , color , rotation , origin , scale , effects , layerDepth ) ;
2021-12-22 12:46:17 +01:00
}
2020-03-28 22:25:06 +01:00
2020-05-20 23:59:40 +02:00
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
2021-07-28 17:22:47 +02:00
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 ) ;
2021-04-19 14:02:28 +02:00
}
2020-03-28 22:25:06 +01:00
2020-05-20 23:59:40 +02:00
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
2021-07-28 17:22:47 +02:00
public void DrawString ( SpriteBatch batch , StringBuilder text , Vector2 position , Color color , float rotation , Vector2 origin , float scale , SpriteEffects effects , float layerDepth ) {
2021-04-19 14:02:28 +02:00
this . DrawString ( batch , text , position , color , rotation , origin , new Vector2 ( scale ) , effects , layerDepth ) ;
2020-04-11 15:32:01 +02:00
}
2021-04-19 14:02:28 +02:00
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
2021-07-28 17:22:47 +02:00
public void DrawString ( SpriteBatch batch , string text , Vector2 position , Color color ) {
2021-04-19 14:02:28 +02:00
this . DrawString ( batch , text , position , color , 0 , Vector2 . Zero , Vector2 . One , SpriteEffects . None , 0 ) ;
2020-04-11 15:32:01 +02:00
}
2021-04-19 14:02:28 +02:00
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
2021-07-28 17:22:47 +02:00
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 ) ;
2020-04-11 15:32:01 +02:00
}
2021-07-28 17:22:47 +02:00
/// <summary>
/// Measures the width of the given string when drawn with this font's underlying font.
2022-01-24 11:16:23 +01:00
/// This method uses <see cref="MeasureChar"/> internally to calculate the size of known characters and calculates additional characters like <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="Emsp"/>.
2021-07-28 17:22:47 +02:00
/// 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.
/// </summary>
/// <param name="text">The text whose size to calculate</param>
/// <param name="ignoreTrailingSpaces">Whether trailing whitespace should be ignored in the returned size, causing the end of each line to be effectively trimmed</param>
/// <returns>The size of the string when drawn with this font</returns>
2021-06-25 15:23:30 +02:00
public Vector2 MeasureString ( string text , bool ignoreTrailingSpaces = false ) {
2021-12-22 13:00:41 +01:00
return this . MeasureString ( new CharSource ( text ) , ignoreTrailingSpaces , null ) ;
2021-12-22 13:03:40 +01:00
}
/// <inheritdoc cref="MeasureString(string,bool)"/>
public Vector2 MeasureString ( StringBuilder text , bool ignoreTrailingSpaces = false ) {
return this . MeasureString ( new CharSource ( text ) , ignoreTrailingSpaces , null ) ;
2021-11-27 22:45:37 +01:00
}
/// <summary>
/// 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 <paramref name="ellipsis"/> at the end instead.
/// </summary>
/// <param name="text">The text to truncate</param>
/// <param name="width">The maximum width, in display pixels based on the font and scale</param>
/// <param name="scale">The scale to use for width measurements</param>
/// <param name="fromBack">If the string should be truncated from the back rather than the front</param>
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
/// <returns>The truncated string, or the same string if it is shorter than the maximum width</returns>
public string TruncateString ( string text , float width , float scale , bool fromBack = false , string ellipsis = "" ) {
2021-12-22 13:00:41 +01:00
return this . TruncateString ( new CharSource ( text ) , width , scale , fromBack , ellipsis , null ) . ToString ( ) ;
}
/// <inheritdoc cref="TruncateString(string,float,float,bool,string)"/>
public StringBuilder TruncateString ( StringBuilder text , float width , float scale , bool fromBack = false , string ellipsis = "" ) {
return this . TruncateString ( new CharSource ( text ) , width , scale , fromBack , ellipsis , null ) ;
2021-11-27 22:45:37 +01:00
}
/// <summary>
/// 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 <paramref name="text"/>.
2021-11-28 00:28:17 +01:00
/// See <see cref="SplitStringSeparate(string,float,float)"/> for a method that differentiates between existing newline characters and splits due to maximum width.
2021-11-27 22:45:37 +01:00
/// </summary>
/// <param name="text">The text to split into multiple lines</param>
/// <param name="width">The maximum width that each line should have</param>
/// <param name="scale">The scale to use for width measurements</param>
/// <returns>The split string, containing newline characters at each new line</returns>
public string SplitString ( string text , float width , float scale ) {
return string . Join ( "\n" , this . SplitStringSeparate ( text , width , scale ) ) ;
}
2021-12-22 13:00:41 +01:00
/// <inheritdoc cref="SplitString(string,float,float)"/>
public string SplitString ( StringBuilder text , float width , float scale ) {
return string . Join ( "\n" , this . SplitStringSeparate ( text , width , scale ) ) ;
}
2021-11-27 22:45:37 +01:00
/// <summary>
/// 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.
2021-12-22 13:00:41 +01:00
/// This method differs from <see cref="SplitString(string,float,float)"/> in that it differentiates between pre-existing newline characters and splits due to maximum width.
2021-11-27 22:45:37 +01:00
/// </summary>
/// <param name="text">The text to split into multiple lines</param>
/// <param name="width">The maximum width that each line should have</param>
/// <param name="scale">The scale to use for width measurements</param>
/// <returns>The split string as an enumerable of split sections</returns>
public IEnumerable < string > SplitStringSeparate ( string text , float width , float scale ) {
2021-12-22 13:00:41 +01:00
return this . SplitStringSeparate ( new CharSource ( text ) , width , scale , null ) ;
2021-11-27 22:45:37 +01:00
}
2021-12-22 13:00:41 +01:00
/// <inheritdoc cref="SplitStringSeparate(string,float,float)"/>
public IEnumerable < string > SplitStringSeparate ( StringBuilder text , float width , float scale ) {
return this . SplitStringSeparate ( new CharSource ( text ) , width , scale , null ) ;
}
internal Vector2 MeasureString ( CharSource text , bool ignoreTrailingSpaces , Func < int , GenericFont > fontFunction ) {
2020-06-20 01:18:27 +02:00
var size = Vector2 . Zero ;
2020-06-20 15:42:36 +02:00
if ( text . Length < = 0 )
return size ;
2020-06-20 01:18:27 +02:00
var xOffset = 0F ;
2021-06-25 15:23:30 +02:00
for ( var i = 0 ; i < text . Length ; i + + ) {
2021-11-28 00:28:17 +01:00
var font = fontFunction ? . Invoke ( i ) ? ? this ;
2021-06-25 15:23:30 +02:00
switch ( text [ i ] ) {
2020-06-20 12:12:34 +02:00
case '\n' :
xOffset = 0 ;
2021-11-28 11:30:57 +01:00
size . Y + = this . LineHeight ;
2020-06-20 12:12:34 +02:00
break ;
2022-06-15 11:38:11 +02:00
case GenericFont . Emsp :
2021-11-28 11:30:57 +01:00
xOffset + = this . LineHeight ;
2020-06-20 12:12:34 +02:00
break ;
2022-06-15 11:38:11 +02:00
case GenericFont . Nbsp :
2021-11-27 22:45:37 +01:00
xOffset + = font . MeasureChar ( ' ' ) ;
2020-07-01 14:30:47 +02:00
break ;
2022-06-15 11:38:11 +02:00
case GenericFont . Zwsp :
2021-04-14 23:13:19 +02:00
// don't add width for a zero-width space
break ;
2021-06-25 15:23:30 +02:00
case ' ' :
2022-06-15 11:38:11 +02:00
if ( ignoreTrailingSpaces & & GenericFont . IsTrailingSpace ( text , i ) ) {
2021-06-25 15:23:30 +02:00
// if this is a trailing space, we can skip remaining spaces too
i = text . Length - 1 ;
break ;
}
2021-11-27 22:45:37 +01:00
xOffset + = font . MeasureChar ( ' ' ) ;
2021-06-25 15:23:30 +02:00
break ;
2020-06-20 12:12:34 +02:00
default :
2021-11-27 22:45:37 +01:00
xOffset + = font . MeasureChar ( text [ i ] ) ;
2020-06-20 12:12:34 +02:00
break ;
2020-06-20 01:18:27 +02:00
}
// increase x size if this line is the longest
if ( xOffset > size . X )
size . X = xOffset ;
}
// include the last line's height too!
2021-11-28 11:30:57 +01:00
size . Y + = this . LineHeight ;
2020-06-20 01:18:27 +02:00
return size ;
}
2021-12-22 13:00:41 +01:00
internal StringBuilder TruncateString ( CharSource text , float width , float scale , bool fromBack , string ellipsis , Func < int , GenericFont > fontFunction ) {
2019-09-05 18:15:51 +02:00
var total = new StringBuilder ( ) ;
for ( var i = 0 ; i < text . Length ; i + + ) {
if ( fromBack ) {
total . Insert ( 0 , text [ text . Length - 1 - i ] ) ;
} else {
total . Append ( text [ i ] ) ;
}
2021-11-28 00:28:17 +01:00
var font = fontFunction ? . Invoke ( i ) ? ? this ;
if ( font . MeasureString ( total + ellipsis ) . X * scale > = width ) {
2020-04-11 15:47:33 +02:00
if ( fromBack ) {
2021-12-22 13:00:41 +01:00
return total . Remove ( 0 , 1 ) . Insert ( 0 , ellipsis ) ;
2020-04-11 15:47:33 +02:00
} else {
2021-12-22 13:00:41 +01:00
return total . Remove ( total . Length - 1 , 1 ) . Append ( ellipsis ) ;
2020-04-11 15:47:33 +02:00
}
}
2019-09-05 18:15:51 +02:00
}
2021-12-22 13:00:41 +01:00
return total ;
2019-09-05 18:15:51 +02:00
}
2021-12-22 13:00:41 +01:00
internal IEnumerable < string > SplitStringSeparate ( CharSource text , float width , float scale , Func < int , GenericFont > fontFunction ) {
2021-04-14 23:13:19 +02:00
var currWidth = 0F ;
var lastSpaceIndex = - 1 ;
var widthSinceLastSpace = 0F ;
2021-10-12 03:23:35 +02:00
var curr = new StringBuilder ( ) ;
2021-04-14 23:13:19 +02:00
for ( var i = 0 ; i < text . Length ; i + + ) {
var c = text [ i ] ;
if ( c = = '\n' ) {
2021-10-12 03:23:35 +02:00
// fake split at pre-defined new lines
curr . Append ( c ) ;
2021-04-14 23:13:19 +02:00
lastSpaceIndex = - 1 ;
widthSinceLastSpace = 0 ;
currWidth = 0 ;
} else {
2021-11-28 00:28:17 +01:00
var font = fontFunction ? . Invoke ( i ) ? ? this ;
var cWidth = font . MeasureString ( c . ToCachedString ( ) ) . X * scale ;
2022-06-15 11:38:11 +02:00
if ( c = = ' ' | | c = = GenericFont . Emsp | | c = = GenericFont . Zwsp ) {
2021-07-28 17:22:47 +02:00
// remember the location of this (breaking!) space
2021-10-12 03:23:35 +02:00
lastSpaceIndex = curr . Length ;
2021-04-14 23:13:19 +02:00
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
2021-10-12 03:23:35 +02:00
yield return curr . ToString ( ) ;
2021-04-14 23:13:19 +02:00
currWidth = 0 ;
2021-10-12 03:23:35 +02:00
curr . Clear ( ) ;
2021-04-14 23:13:19 +02:00
} else {
// split after the last space
2021-10-12 03:23:35 +02:00
yield return curr . ToString ( ) . Substring ( 0 , lastSpaceIndex + 1 ) ;
curr . Remove ( 0 , lastSpaceIndex + 1 ) ;
2021-04-14 23:13:19 +02:00
// we need to restore the width accumulated since the last space for the new line
currWidth = widthSinceLastSpace ;
2019-09-06 11:25:31 +02:00
}
2021-04-14 23:13:19 +02:00
widthSinceLastSpace = 0 ;
lastSpaceIndex = - 1 ;
2019-08-24 00:07:54 +02:00
}
2021-04-14 23:13:19 +02:00
// add current character
currWidth + = cWidth ;
widthSinceLastSpace + = cWidth ;
2021-10-12 03:23:35 +02:00
curr . Append ( c ) ;
2019-08-06 14:20:11 +02:00
}
}
2021-10-12 03:23:35 +02:00
if ( curr . Length > 0 )
yield return curr . ToString ( ) ;
2019-08-06 14:20:11 +02:00
}
2021-12-22 13:00:41 +01:00
private void DrawString ( SpriteBatch batch , CharSource text , Vector2 position , Color color , float rotation , Vector2 origin , Vector2 scale , SpriteEffects effects , float layerDepth ) {
2022-06-24 14:01:26 +02:00
var ( flipX , flipY ) = ( 0F , 0F ) ;
2021-12-22 13:00:41 +01:00
var flippedV = ( effects & SpriteEffects . FlipVertically ) ! = 0 ;
var flippedH = ( effects & SpriteEffects . FlipHorizontally ) ! = 0 ;
if ( flippedV | | flippedH ) {
2022-06-24 14:01:26 +02:00
var size = this . MeasureString ( text , false , null ) ;
2021-12-22 13:00:41 +01:00
if ( flippedH ) {
origin . X * = - 1 ;
2022-06-24 14:01:26 +02:00
flipX = - size . X ;
2021-12-22 13:00:41 +01:00
}
if ( flippedV ) {
origin . Y * = - 1 ;
2022-06-24 14:01:26 +02:00
flipY = this . LineHeight - size . Y ;
2021-12-22 13:00:41 +01:00
}
}
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 ;
for ( var i = 0 ; i < text . Length ; i + + ) {
var c = text [ i ] ;
if ( c = = '\n' ) {
offset . X = 0 ;
offset . Y + = this . LineHeight ;
continue ;
}
var cString = c . ToCachedString ( ) ;
2022-06-24 14:01:26 +02:00
var cSize = this . MeasureString ( cString ) ;
2021-12-22 13:00:41 +01:00
var charPos = offset ;
if ( flippedH )
2022-06-24 14:01:26 +02:00
charPos . X + = cSize . X ;
2021-12-22 13:00:41 +01:00
if ( flippedV )
2022-06-24 14:01:26 +02:00
charPos . Y + = cSize . Y - this . LineHeight ;
2021-12-22 13:00:41 +01:00
Vector2 . Transform ( ref charPos , ref trans , out charPos ) ;
this . DrawChar ( batch , cString , charPos , color , rotation , scale , effects , layerDepth ) ;
2022-06-24 14:01:26 +02:00
offset . X + = cSize . X ;
2021-12-22 13:00:41 +01:00
}
}
private static bool IsTrailingSpace ( CharSource s , int index ) {
2021-06-25 15:23:30 +02:00
for ( var i = index + 1 ; i < s . Length ; i + + ) {
if ( s [ i ] ! = ' ' )
return false ;
}
return true ;
}
2021-12-22 13:00:41 +01:00
internal readonly struct CharSource {
private readonly string strg ;
private readonly StringBuilder builder ;
public int Length = > this . strg ? . Length ? ? this . builder . Length ;
public char this [ int index ] = > this . strg ? [ index ] ? ? this . builder [ index ] ;
public CharSource ( string strg ) {
this . strg = strg ;
this . builder = null ;
}
public CharSource ( StringBuilder builder ) {
this . strg = null ;
this . builder = builder ;
}
}
2019-08-06 14:20:11 +02:00
}
2022-06-17 18:23:47 +02:00
}