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"/>.
2020-12-08 01:43:52 +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"/>.
2020-06-20 01:18:27 +02:00
/// </summary>
2020-12-08 01:43:52 +01:00
public const char OneEmSpace = ' \ u2003 ' ;
2020-07-01 14:30:47 +02:00
/// <summary>
/// This field holds the unicode representation of a non-breaking space.
/// 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"/>.
/// </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.
/// 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"/> and <see cref="SplitString"/>.
/// </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>
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString"/>.
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="OneEmSpace"/> for most generic fonts, which is why <see cref="MeasureString"/> should be used even for single characters.
/// </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
2020-05-20 23:59:40 +02:00
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
2020-03-28 22:25:06 +01:00
public abstract void DrawString ( SpriteBatch batch , string text , Vector2 position , Color color , float rotation , Vector2 origin , Vector2 scale , SpriteEffects effects , float layerDepth ) ;
2020-05-20 23:59:40 +02:00
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
2021-04-19 14:02:28 +02:00
public abstract void DrawString ( SpriteBatch batch , StringBuilder text , Vector2 position , Color color , float rotation , Vector2 origin , Vector2 scale , SpriteEffects effects , float 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-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.
/// 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="OneEmSpace"/>.
/// 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 ) {
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 + + ) {
switch ( text [ i ] ) {
2020-06-20 12:12:34 +02:00
case '\n' :
xOffset = 0 ;
size . Y + = this . LineHeight ;
break ;
case OneEmSpace :
xOffset + = this . LineHeight ;
break ;
2020-07-01 14:30:47 +02:00
case Nbsp :
2021-07-28 17:22:47 +02:00
xOffset + = this . MeasureChar ( ' ' ) ;
2020-07-01 14:30:47 +02:00
break ;
2021-04-14 23:13:19 +02:00
case Zwsp :
// don't add width for a zero-width space
break ;
2021-06-25 15:23:30 +02:00
case ' ' :
if ( ignoreTrailingSpaces & & IsTrailingSpace ( text , i ) ) {
// if this is a trailing space, we can skip remaining spaces too
i = text . Length - 1 ;
break ;
}
2021-07-28 17:22:47 +02:00
xOffset + = this . MeasureChar ( ' ' ) ;
2021-06-25 15:23:30 +02:00
break ;
2020-06-20 12:12:34 +02:00
default :
2021-07-28 17:22:47 +02:00
xOffset + = this . 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!
size . Y + = this . LineHeight ;
return size ;
}
2020-05-20 23:59:40 +02: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.
2020-10-06 20:14:57 +02:00
/// Optionally, the string can be cut off a bit sooner, adding the <paramref name="ellipsis"/> at the end instead.
2020-05-20 23:59:40 +02:00
/// </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>
2020-04-11 15:44:55 +02:00
public string TruncateString ( string text , float width , float scale , bool fromBack = false , string ellipsis = "" ) {
2019-09-05 18:15:51 +02:00
var total = new StringBuilder ( ) ;
2020-04-11 15:44:55 +02:00
var ellipsisWidth = this . MeasureString ( ellipsis ) . X * scale ;
2019-09-05 18:15:51 +02:00
for ( var i = 0 ; i < text . Length ; i + + ) {
if ( fromBack ) {
total . Insert ( 0 , text [ text . Length - 1 - i ] ) ;
} else {
total . Append ( text [ i ] ) ;
}
2020-06-20 01:18:27 +02:00
if ( this . MeasureString ( total . ToString ( ) ) . X * scale + ellipsisWidth > = width ) {
2020-04-11 15:47:33 +02:00
if ( fromBack ) {
return total . Remove ( 0 , 1 ) . Insert ( 0 , ellipsis ) . ToString ( ) ;
} else {
return total . Remove ( total . Length - 1 , 1 ) . Append ( ellipsis ) . ToString ( ) ;
}
}
2019-09-05 18:15:51 +02:00
}
return total . ToString ( ) ;
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// Splits a string to a given maximum width, adding newline characters between each line.
2021-10-12 03:23:35 +02:00
/// Also splits long words and supports zero-width spaces and takes into account existing newline characters in the passed <paramref name="text"/>.
/// See <see cref="SplitStringSeparate"/> for a method that differentiates between existing newline characters and splits due to maximum width.
2020-05-20 23:59:40 +02: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>
2020-03-28 22:25:06 +01:00
public string SplitString ( string text , float width , float scale ) {
2021-10-12 03:23:35 +02:00
return string . Join ( "\n" , this . SplitStringSeparate ( text , width , scale ) ) ;
}
/// <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.
/// This method differs from <see cref="SplitString"/> in that it differentiates between pre-existing newline characters and splits due to maximum width.
/// </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-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-07-28 17:22:47 +02:00
var cWidth = this . MeasureString ( c . ToCachedString ( ) ) . X * scale ;
2021-04-14 23:13:19 +02:00
if ( c = = ' ' | | c = = OneEmSpace | | c = = 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-06-25 15:23:30 +02:00
private static bool IsTrailingSpace ( string s , int index ) {
for ( var i = index + 1 ; i < s . Length ; i + + ) {
if ( s [ i ] ! = ' ' )
return false ;
}
return true ;
}
2019-08-06 14:20:11 +02:00
}
}