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 ;
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 ; }
2020-05-20 23:59:40 +02:00
///<inheritdoc cref="SpriteFont.LineSpacing"/>
2020-03-28 22:25:06 +01:00
public abstract float LineHeight { get ; }
2019-08-25 19:07:45 +02:00
2021-04-19 14:02:28 +02:00
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
protected abstract Vector2 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-04-19 14:02:28 +02:00
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 ) ;
}
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-04-19 14:02:28 +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 ) ;
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)"/>
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-04-19 14:02:28 +02:00
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
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 ) ;
2020-04-11 15:32:01 +02:00
}
2020-06-20 01:18:27 +02:00
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
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 :
xOffset + = this . MeasureChar ( ' ' ) . X ;
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 ;
}
xOffset + = this . MeasureChar ( ' ' ) . X ;
break ;
2020-06-20 12:12:34 +02:00
default :
2021-06-25 15:23:30 +02:00
xOffset + = this . MeasureChar ( text [ i ] ) . X ;
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.
2020-12-31 17:22:51 +01:00
/// Also splits long words and supports zero-width spaces.
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-04-14 23:13:19 +02:00
var ret = new StringBuilder ( ) ;
var currWidth = 0F ;
var lastSpaceIndex = - 1 ;
var widthSinceLastSpace = 0F ;
for ( var i = 0 ; i < text . Length ; i + + ) {
var c = text [ i ] ;
if ( c = = '\n' ) {
// split at pre-defined new lines
ret . Append ( c ) ;
lastSpaceIndex = - 1 ;
widthSinceLastSpace = 0 ;
currWidth = 0 ;
} else {
var cWidth = this . MeasureChar ( c ) . X * scale ;
if ( c = = ' ' | | c = = OneEmSpace | | c = = Zwsp ) {
// remember the location of this space
lastSpaceIndex = ret . 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
ret . Append ( '\n' ) ;
currWidth = 0 ;
} else {
// split after the last space
ret . Insert ( lastSpaceIndex + 1 , '\n' ) ;
// 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 ;
ret . Append ( c ) ;
2019-08-06 14:20:11 +02:00
}
}
2021-04-14 23:13:19 +02:00
return ret . 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
}
}