2021-11-27 22:45:37 +01:00
using System ;
2021-10-12 03:23:35 +02:00
using System.Collections.Generic ;
2022-12-07 13:35:57 +01:00
using System.Linq ;
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 ;
2024-04-10 20:27:00 +02:00
using MLEM.Formatting ;
using MLEM.Formatting.Codes ;
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>
2022-10-15 13:48:45 +02:00
/// Measures the width of the given code point 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>
2022-10-15 13:48:45 +02:00
/// <param name="codePoint">The code point whose width to calculate</param>
2021-07-28 17:22:47 +02:00
/// <returns>The width of the given character with the default scale</returns>
2022-10-15 13:48:45 +02:00
protected abstract float MeasureCharacter ( int codePoint ) ;
2020-03-28 22:25:06 +01:00
2021-12-22 12:46:17 +01:00
/// <summary>
2022-10-15 13:48:45 +02:00
/// Draws the given code point 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)"/>.
2024-04-10 20:27:00 +02:00
/// Note that this method should only be called internally for rendering of more complex strings, like in <see cref="TextFormatter"/> <see cref="Code"/> implementations.
2021-12-22 12:46:17 +01:00
/// </summary>
/// <param name="batch">The sprite batch to draw with.</param>
2022-10-15 13:48:45 +02:00
/// <param name="codePoint">The code point which will be drawn.</param>
/// <param name="character">A string representation of the character which will be drawn.</param>
2021-12-22 12:46:17 +01:00
/// <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>
2024-04-10 20:27:00 +02:00
public abstract void DrawCharacter ( SpriteBatch batch , int codePoint , string character , Vector2 position , Color color , float rotation , Vector2 scale , SpriteEffects effects , float layerDepth ) ;
2021-12-22 13:00:41 +01:00
///<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 ) {
2022-10-15 13:48:45 +02:00
this . DrawString ( batch , new CodePointSource ( text ) , position , color , rotation , origin , scale , effects , layerDepth ) ;
2021-12-22 13:00:41 +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-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 ) {
2022-10-15 13:48:45 +02:00
this . DrawString ( batch , new CodePointSource ( 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-10-15 13:48:45 +02:00
/// This method uses <see cref="MeasureCharacter"/> 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 ) {
2022-12-07 13:35:57 +01:00
return this . MeasureString ( new CodePointSource ( text ) , ignoreTrailingSpaces ) ;
2021-12-22 13:03:40 +01:00
}
/// <inheritdoc cref="MeasureString(string,bool)"/>
public Vector2 MeasureString ( StringBuilder text , bool ignoreTrailingSpaces = false ) {
2022-12-07 13:35:57 +01:00
return this . MeasureString ( new CodePointSource ( text ) , ignoreTrailingSpaces ) ;
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 = "" ) {
2022-12-07 13:35:57 +01:00
return GenericFont . TruncateString ( Enumerable . Repeat ( new DecoratedCodePointSource ( new CodePointSource ( text ) , this , 0 ) , 1 ) , width , scale , fromBack , ellipsis ) . First ( ) . ToString ( ) ;
2021-12-22 13:00:41 +01:00
}
/// <inheritdoc cref="TruncateString(string,float,float,bool,string)"/>
public StringBuilder TruncateString ( StringBuilder text , float width , float scale , bool fromBack = false , string ellipsis = "" ) {
2022-12-07 13:35:57 +01:00
return GenericFont . TruncateString ( Enumerable . Repeat ( new DecoratedCodePointSource ( new CodePointSource ( text ) , this , 0 ) , 1 ) , width , scale , fromBack , ellipsis ) . First ( ) ;
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 ) {
2022-12-07 13:35:57 +01:00
return GenericFont . SplitStringSeparate ( Enumerable . Repeat ( new DecoratedCodePointSource ( new CodePointSource ( text ) , this , 0 ) , 1 ) , width , scale ) . First ( ) ;
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 ) {
2022-12-07 13:35:57 +01:00
return GenericFont . SplitStringSeparate ( Enumerable . Repeat ( new DecoratedCodePointSource ( new CodePointSource ( text ) , this , 0 ) , 1 ) , width , scale ) . First ( ) ;
2021-12-22 13:00:41 +01:00
}
2024-04-10 20:45:16 +02:00
/// <summary>
/// Calculates a transformation matrix for drawing a string with the given data.
/// </summary>
/// <param name="position">The position to draw at.</param>
/// <param name="rotation">The rotation to draw with.</param>
/// <param name="origin">The origin to subtract from the position.</param>
/// <param name="scale">The scale to draw with.</param>
/// <param name="effects">The flipping to draw with.</param>
/// <param name="flipSize">The size of the string, which is only used when <paramref name="effects"/> is not <see cref="SpriteEffects.None"/>.</param>
/// <returns>A transformation matrix.</returns>
2024-04-10 20:27:00 +02:00
public Matrix CalculateStringTransform ( Vector2 position , float rotation , Vector2 origin , Vector2 scale , SpriteEffects effects , Vector2 flipSize ) {
var ( flipX , flipY ) = ( 0F , 0F ) ;
var flippedV = ( effects & SpriteEffects . FlipVertically ) ! = 0 ;
var flippedH = ( effects & SpriteEffects . FlipHorizontally ) ! = 0 ;
if ( flippedV | | flippedH ) {
if ( flippedH ) {
origin . X * = - 1 ;
flipX = - flipSize . X ;
}
if ( flippedV ) {
origin . Y * = - 1 ;
flipY = this . LineHeight - flipSize . 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 ;
}
return trans ;
}
2024-04-10 20:45:16 +02:00
/// <summary>
/// Moves the passed <paramref name="charPos"/> based on the given flipping data.
/// </summary>
/// <param name="charPos">The position to move.</param>
/// <param name="effects">The flipping to move based on.</param>
/// <param name="charSize">The size of the object to move, which is only used when <paramref name="effects"/> is not <see cref="SpriteEffects.None"/>.</param>
/// <returns>The moved position.</returns>
2024-04-10 20:27:00 +02:00
public Vector2 MoveFlipped ( Vector2 charPos , SpriteEffects effects , Vector2 charSize ) {
if ( ( effects & SpriteEffects . FlipHorizontally ) ! = 0 )
charPos . X + = charSize . X ;
if ( ( effects & SpriteEffects . FlipVertically ) ! = 0 )
charPos . Y + = charSize . Y - this . LineHeight ;
return charPos ;
}
2024-04-10 20:45:16 +02:00
/// <summary>
/// Transforms the position of a single character to draw.
/// In general, it is efficient to calculate the transformation matrix once at the start (using <see cref="CalculateStringTransform"/>) and to then apply flipping data for each character individually (using <see cref="MoveFlipped"/>).
/// </summary>
/// <param name="stringPos">The position that the string is drawn at.</param>
/// <param name="charPosOffset">The offset from the <paramref name="stringPos"/> that the current character is drawn at.</param>
/// <param name="rotation">The rotation to draw with.</param>
/// <param name="origin">The origin to subtract from the position.</param>
/// <param name="scale">The scale to draw with.</param>
/// <param name="effects">The flipping to draw with.</param>
/// <param name="stringSize">The size of the string, which is only used when <paramref name="effects"/> is not <see cref="SpriteEffects.None"/>.</param>
/// <param name="charSize">The size of the current character, which is only used when <paramref name="effects"/> is not <see cref="SpriteEffects.None"/>.</param>
/// <returns>The transformed final draw position.</returns>
2024-04-10 20:27:00 +02:00
public Vector2 TransformSingleCharacter ( Vector2 stringPos , Vector2 charPosOffset , float rotation , Vector2 origin , Vector2 scale , SpriteEffects effects , Vector2 stringSize , Vector2 charSize ) {
return Vector2 . Transform ( this . MoveFlipped ( charPosOffset , effects , charSize ) , this . CalculateStringTransform ( stringPos , rotation , origin , scale , effects , stringSize ) ) ;
}
2022-12-07 13:35:57 +01:00
private Vector2 MeasureString ( CodePointSource text , bool ignoreTrailingSpaces ) {
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 ;
2022-10-15 13:48:45 +02:00
var index = 0 ;
while ( index < text . Length ) {
var ( codePoint , length ) = text . GetCodePoint ( index ) ;
switch ( codePoint ) {
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 :
2022-12-07 13:35:57 +01:00
xOffset + = this . MeasureCharacter ( ' ' ) ;
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-10-15 13:48:45 +02:00
if ( ignoreTrailingSpaces & & GenericFont . IsTrailingSpace ( text , index ) ) {
2021-06-25 15:23:30 +02:00
// if this is a trailing space, we can skip remaining spaces too
2022-10-15 13:48:45 +02:00
index = text . Length - 1 ;
2021-06-25 15:23:30 +02:00
break ;
}
2022-12-07 13:35:57 +01:00
xOffset + = this . MeasureCharacter ( ' ' ) ;
2021-06-25 15:23:30 +02:00
break ;
2020-06-20 12:12:34 +02:00
default :
2022-12-07 13:35:57 +01:00
xOffset + = this . MeasureCharacter ( codePoint ) ;
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 ;
2022-10-15 13:48:45 +02:00
index + = length ;
2020-06-20 01:18:27 +02:00
}
// 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 ;
}
2022-10-15 13:48:45 +02:00
private void DrawString ( SpriteBatch batch , CodePointSource text , Vector2 position , Color color , float rotation , Vector2 origin , Vector2 scale , SpriteEffects effects , float layerDepth ) {
2024-04-10 18:37:14 +02:00
var flipSize = effects ! = SpriteEffects . None ? this . MeasureString ( text , false ) : Vector2 . Zero ;
2024-04-10 20:27:00 +02:00
var trans = this . CalculateStringTransform ( position , rotation , origin , scale , effects , flipSize ) ;
2021-12-22 13:00:41 +01:00
var offset = Vector2 . Zero ;
2022-10-15 13:48:45 +02:00
var index = 0 ;
while ( index < text . Length ) {
var ( codePoint , length ) = text . GetCodePoint ( index ) ;
if ( codePoint = = '\n' ) {
2021-12-22 13:00:41 +01:00
offset . X = 0 ;
offset . Y + = this . LineHeight ;
2022-10-15 13:48:45 +02:00
} else {
2022-10-23 21:23:16 +02:00
var character = CodePointSource . ToString ( codePoint ) ;
2022-10-15 13:48:45 +02:00
var charSize = this . MeasureString ( character ) ;
2024-04-10 20:27:00 +02:00
var charPos = Vector2 . Transform ( this . MoveFlipped ( offset , effects , charSize ) , trans ) ;
2022-10-15 13:48:45 +02:00
this . DrawCharacter ( batch , codePoint , character , charPos , color , rotation , scale , effects , layerDepth ) ;
offset . X + = charSize . X ;
2021-12-22 13:00:41 +01:00
}
2022-10-15 13:48:45 +02:00
index + = length ;
2021-12-22 13:00:41 +01:00
}
}
2022-12-07 13:35:57 +01:00
internal static IEnumerable < IEnumerable < string > > SplitStringSeparate ( IEnumerable < DecoratedCodePointSource > 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 < List < string > > ( ) ;
foreach ( var part in text ) {
var partSplit = new List < string > ( ) ;
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 < string > 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 < StringBuilder > TruncateString ( IEnumerable < DecoratedCodePointSource > text , float maxWidth , float scale , bool fromBack , string ellipsis ) {
var total = new StringBuilder ( ) ;
var extraWidth = 0F ;
var endReached = false ;
2022-12-13 13:11:36 +01:00
foreach ( var part in fromBack ? text . Reverse ( ) : text ) {
2022-12-07 13:35:57 +01:00
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 ;
}
}
2022-10-15 13:48:45 +02:00
private static bool IsTrailingSpace ( CodePointSource s , int index ) {
while ( index < s . Length ) {
var ( codePoint , length ) = s . GetCodePoint ( index ) ;
if ( codePoint ! = ' ' )
2021-06-25 15:23:30 +02:00
return false ;
2022-10-15 13:48:45 +02:00
index + = length ;
2021-06-25 15:23:30 +02:00
}
return true ;
}
2022-12-07 13:35:57 +01:00
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 ;
}
}
2019-08-06 14:20:11 +02:00
}
2022-06-17 18:23:47 +02:00
}