2022-12-06 16:49:19 +01:00
using System ;
2020-05-17 00:10:29 +02:00
using System.Collections.Generic ;
2022-02-13 22:43:51 +01:00
using System.Collections.ObjectModel ;
2020-05-15 00:34:04 +02:00
using System.Linq ;
2020-05-15 13:16:03 +02:00
using System.Text ;
2020-05-15 00:34:04 +02:00
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
2022-12-07 13:35:57 +01:00
using MLEM.Extensions ;
2020-05-15 00:34:04 +02:00
using MLEM.Font ;
2020-05-15 14:22:33 +02:00
using MLEM.Formatting.Codes ;
using MLEM.Misc ;
2020-05-15 00:34:04 +02:00
namespace MLEM.Formatting {
2020-05-21 12:53:42 +02:00
/// <summary>
/// A tokenized string that was created using a <see cref="TextFormatter"/>
/// </summary>
2020-05-15 14:22:33 +02:00
public class TokenizedString : GenericDataHolder {
2020-05-15 00:34:04 +02:00
2020-05-21 12:53:42 +02:00
/// <summary>
/// The raw string that was used to create this tokenized string.
/// </summary>
2020-05-15 00:34:04 +02:00
public readonly string RawString ;
2020-05-21 12:53:42 +02:00
/// <summary>
/// The <see cref="RawString"/>, but with formatting codes stripped out.
/// </summary>
2020-05-17 00:10:29 +02:00
public readonly string String ;
2020-05-21 12:53:42 +02:00
/// <summary>
/// The string that is actually displayed by this tokenized string.
2021-05-18 16:47:38 +02:00
/// If this string has been <see cref="Split"/> or <see cref="Truncate"/> has been used, this string will contain the newline characters.
2020-05-21 12:53:42 +02:00
/// </summary>
2021-05-18 16:47:38 +02:00
public string DisplayString = > this . modifiedString ? ? this . String ;
2020-05-21 12:53:42 +02:00
/// <summary>
/// The tokens that this tokenized string contains.
/// </summary>
2020-05-15 00:34:04 +02:00
public readonly Token [ ] Tokens ;
2020-05-21 12:53:42 +02:00
/// <summary>
/// All of the formatting codes that are applied over this tokenized string.
/// Note that, to get a formatting code for a certain token, use <see cref="Token.AppliedCodes"/>
/// </summary>
2020-05-15 14:22:33 +02:00
public readonly Code [ ] AllCodes ;
2021-05-18 16:47:38 +02:00
private string modifiedString ;
2021-10-04 23:57:58 +02:00
private float initialInnerOffset ;
2022-12-07 13:35:57 +01:00
private RectangleF area ;
2020-05-15 00:34:04 +02:00
2021-06-25 15:23:30 +02:00
internal TokenizedString ( GenericFont font , TextAlignment alignment , string rawString , string strg , Token [ ] tokens ) {
2020-05-15 00:34:04 +02:00
this . RawString = rawString ;
this . String = strg ;
this . Tokens = tokens ;
2022-02-13 22:43:51 +01:00
2020-05-15 14:22:33 +02:00
// since a code can be present in multiple tokens, we use Distinct here
this . AllCodes = tokens . SelectMany ( t = > t . AppliedCodes ) . Distinct ( ) . ToArray ( ) ;
2022-02-13 22:43:51 +01:00
// TODO this can probably be optimized by keeping track of a code's tokens while tokenizing
foreach ( var code in this . AllCodes )
code . Tokens = new ReadOnlyCollection < Token > ( this . Tokens . Where ( t = > t . AppliedCodes . Contains ( code ) ) . ToList ( ) ) ;
2022-09-02 13:42:21 +02:00
this . Realign ( font , alignment ) ;
2020-05-15 00:34:04 +02:00
}
2020-05-21 12:53:42 +02:00
/// <summary>
/// Splits this tokenized string, inserting newline characters if the width of the string is bigger than the maximum width.
2021-05-18 16:47:38 +02:00
/// Note that a tokenized string can be re-split without losing any of its actual data, as this operation merely modifies the <see cref="DisplayString"/>.
2020-05-21 12:53:42 +02:00
/// </summary>
/// <param name="font">The font to use for width calculations</param>
2021-05-18 16:47:38 +02:00
/// <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>
2021-06-25 15:23:30 +02:00
/// <param name="alignment">The text alignment that should be used for width calculations</param>
public void Split ( GenericFont font , float width , float scale , TextAlignment alignment = TextAlignment . Left ) {
2022-12-07 13:35:57 +01:00
var index = 0 ;
var modified = new StringBuilder ( ) ;
foreach ( var part in GenericFont . SplitStringSeparate ( this . AsDecoratedSources ( font ) , width , scale ) ) {
var joined = string . Join ( "\n" , part ) ;
this . Tokens [ index ] . ModifiedSubstring = joined ;
modified . Append ( joined ) ;
index + + ;
}
this . modifiedString = modified . ToString ( ) ;
this . Realign ( font , alignment ) ;
2021-05-18 16:47:38 +02:00
}
2021-04-22 01:14:48 +02:00
2021-05-18 16:47:38 +02:00
/// <summary>
/// Truncates this tokenized string, removing any additional characters that exceed the length from the displayed string.
/// Note that a tokenized string can be re-truncated without losing any of its actual data, as this operation merely modifies the <see cref="DisplayString"/>.
2021-11-28 00:28:17 +01:00
/// <seealso cref="GenericFont.TruncateString(string,float,float,bool,string)"/>
2021-05-18 16:47:38 +02:00
/// </summary>
/// <param name="font">The font to use for width calculations</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="ellipsis">The characters to add to the end of the string if it is too long</param>
2021-06-25 15:23:30 +02:00
/// <param name="alignment">The text alignment that should be used for width calculations</param>
public void Truncate ( GenericFont font , float width , float scale , string ellipsis = "" , TextAlignment alignment = TextAlignment . Left ) {
2022-12-07 13:35:57 +01:00
var index = 0 ;
var modified = new StringBuilder ( ) ;
foreach ( var part in GenericFont . TruncateString ( this . AsDecoratedSources ( font ) , width , scale , false , ellipsis ) ) {
this . Tokens [ index ] . ModifiedSubstring = part . ToString ( ) ;
modified . Append ( part ) ;
index + + ;
}
this . modifiedString = modified . ToString ( ) ;
this . Realign ( font , alignment ) ;
2020-05-15 00:34:04 +02:00
}
2022-09-02 13:42:21 +02:00
/// <summary>
/// Realigns this tokenized string using the given <see cref="TextAlignment"/>.
/// If the <paramref name="alignment"/> is <see cref="TextAlignment.Right"/>, trailing space characters (but not <see cref="GenericFont.Nbsp"/>) will be removed.
/// </summary>
/// <param name="font">The font to use for width calculations.</param>
/// <param name="alignment">The text alignment that should be used for width calculations.</param>
public void Realign ( GenericFont font , TextAlignment alignment ) {
// split display strings
foreach ( var token in this . Tokens )
token . SplitDisplayString = token . DisplayString . Split ( '\n' ) ;
// token areas and inner offsets
2022-12-07 13:35:57 +01:00
this . area = RectangleF . Empty ;
2022-09-02 13:42:21 +02:00
this . initialInnerOffset = this . GetInnerOffsetX ( font , 0 , 0 , alignment ) ;
var innerOffset = new Vector2 ( this . initialInnerOffset , 0 ) ;
for ( var t = 0 ; t < this . Tokens . Length ; t + + ) {
var token = this . Tokens [ t ] ;
var tokenFont = token . GetFont ( font ) ;
token . InnerOffsets = new float [ token . SplitDisplayString . Length - 1 ] ;
2022-12-07 13:35:57 +01:00
var tokenArea = new List < RectangleF > ( ) ;
var selfRect = new RectangleF ( innerOffset , new Vector2 ( token . GetSelfWidth ( tokenFont ) , tokenFont . LineHeight ) ) ;
if ( ! selfRect . IsEmpty ) {
tokenArea . Add ( selfRect ) ;
this . area = RectangleF . Union ( this . area , selfRect ) ;
innerOffset . X + = selfRect . Width ;
}
2022-09-02 13:42:21 +02:00
for ( var l = 0 ; l < token . SplitDisplayString . Length ; l + + ) {
2022-12-06 16:49:19 +01:00
var size = tokenFont . MeasureString ( token . SplitDisplayString [ l ] , ! this . EndsLater ( t , l ) ) ;
2022-09-02 13:42:21 +02:00
var rect = new RectangleF ( innerOffset , size ) ;
2022-12-07 13:35:57 +01:00
if ( ! rect . IsEmpty ) {
tokenArea . Add ( rect ) ;
this . area = RectangleF . Union ( this . area , rect ) ;
}
2022-09-02 13:42:21 +02:00
if ( l < token . SplitDisplayString . Length - 1 ) {
innerOffset . X = token . InnerOffsets [ l ] = this . GetInnerOffsetX ( font , t , l + 1 , alignment ) ;
2022-12-07 13:35:57 +01:00
innerOffset . Y + = tokenFont . LineHeight ;
2022-09-02 13:42:21 +02:00
} else {
innerOffset . X + = size . X ;
}
}
2022-12-07 13:35:57 +01:00
token . Area = tokenArea . ToArray ( ) ;
2022-09-02 13:42:21 +02:00
}
}
2021-06-25 15:23:30 +02:00
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
2022-12-07 13:35:57 +01:00
[Obsolete("Measure is deprecated. Use GetArea, which returns the string's total size measurement, instead.")]
2020-05-15 19:55:59 +02:00
public Vector2 Measure ( GenericFont font ) {
2022-12-07 13:35:57 +01:00
return this . GetArea ( Vector2 . Zero , 1 ) . Size ;
}
/// <summary>
/// Measures the area that this entire tokenized string and all of its <see cref="Tokens"/> take up and returns it as a <see cref="RectangleF"/>.
/// </summary>
/// <param name="stringPos">The position that this string is being rendered at, which will offset the resulting <see cref="RectangleF"/>.</param>
/// <param name="scale">The scale that this string is being rendered with, which will scale the resulting <see cref="RectangleF"/>.</param>
/// <returns>The area that this tokenized string takes up.</returns>
public RectangleF GetArea ( Vector2 stringPos , float scale ) {
return new RectangleF ( stringPos + this . area . Location * scale , this . area . Size * scale ) ;
2020-05-15 19:55:59 +02:00
}
2020-05-21 12:53:42 +02:00
/// <summary>
/// Updates the formatting codes in this formatted string, causing animations to animate etc.
/// </summary>
/// <param name="time">The game's time</param>
2020-05-15 14:22:33 +02:00
public void Update ( GameTime time ) {
foreach ( var code in this . AllCodes )
code . Update ( time ) ;
}
2020-05-21 12:53:42 +02:00
/// <summary>
/// Returns the token under the given position.
/// This can be used for hovering effects when the mouse is over a token, etc.
/// </summary>
/// <param name="stringPos">The position that the string is drawn at</param>
/// <param name="target">The position to use for checking the token</param>
/// <param name="scale">The scale that the string is drawn at</param>
/// <returns>The token under the target position</returns>
2020-05-17 00:10:29 +02:00
public Token GetTokenUnderPos ( Vector2 stringPos , Vector2 target , float scale ) {
2021-07-18 22:18:46 +02:00
foreach ( var token in this . Tokens ) {
foreach ( var rect in token . GetArea ( stringPos , scale ) ) {
if ( rect . Contains ( target ) )
return token ;
}
}
return null ;
2020-05-15 22:15:24 +02:00
}
2020-05-21 12:53:42 +02:00
/// <inheritdoc cref="GenericFont.DrawString(SpriteBatch,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
2023-05-18 21:41:36 +02:00
public void Draw ( GameTime time , SpriteBatch batch , Vector2 pos , GenericFont font , Color color , float scale , float depth , int? startIndex = null , int? endIndex = null ) {
2021-10-04 23:57:58 +02:00
var innerOffset = new Vector2 ( this . initialInnerOffset * scale , 0 ) ;
2021-06-25 15:23:30 +02:00
for ( var t = 0 ; t < this . Tokens . Length ; t + + ) {
var token = this . Tokens [ t ] ;
2023-05-18 21:41:36 +02:00
if ( endIndex ! = null & & token . Index > = endIndex )
return ;
2021-11-27 23:35:37 +01:00
var drawFont = token . GetFont ( font ) ;
var drawColor = token . GetColor ( color ) ;
2022-06-15 11:38:11 +02:00
2023-05-18 21:41:36 +02:00
if ( startIndex = = null | | token . Index > = startIndex )
token . DrawSelf ( time , batch , pos + innerOffset , drawFont , drawColor , scale , depth ) ;
2022-12-06 17:49:38 +01:00
innerOffset . X + = token . GetSelfWidth ( drawFont ) * scale ;
2022-02-14 00:24:31 +01:00
var indexInToken = 0 ;
2021-06-25 15:23:30 +02:00
for ( var l = 0 ; l < token . SplitDisplayString . Length ; l + + ) {
2023-05-18 21:41:36 +02:00
var cpsIndex = 0 ;
2022-10-15 13:48:45 +02:00
var line = new CodePointSource ( token . SplitDisplayString [ l ] ) ;
2023-05-18 21:41:36 +02:00
while ( cpsIndex < line . Length ) {
if ( endIndex ! = null & & token . Index + indexInToken > = endIndex )
return ;
var ( codePoint , length ) = line . GetCodePoint ( cpsIndex ) ;
2022-10-23 21:23:16 +02:00
var character = CodePointSource . ToString ( codePoint ) ;
2022-06-15 11:38:11 +02:00
2023-05-18 21:41:36 +02:00
if ( startIndex = = null | | token . Index + indexInToken > = startIndex )
token . DrawCharacter ( time , batch , codePoint , character , indexInToken , pos + innerOffset , drawFont , drawColor , scale , depth ) ;
2021-06-25 15:23:30 +02:00
2022-10-15 13:48:45 +02:00
innerOffset . X + = drawFont . MeasureString ( character ) . X * scale ;
2023-05-18 21:41:36 +02:00
indexInToken + = length ;
cpsIndex + = length ;
2021-06-25 15:23:30 +02:00
}
2022-06-15 11:38:11 +02:00
2021-06-25 15:23:30 +02:00
// only split at a new line, not between tokens!
if ( l < token . SplitDisplayString . Length - 1 ) {
2021-10-04 23:57:58 +02:00
innerOffset . X = token . InnerOffsets [ l ] * scale ;
2022-12-07 13:35:57 +01:00
innerOffset . Y + = drawFont . LineHeight * scale ;
2021-06-25 15:23:30 +02:00
}
}
2021-05-18 16:47:38 +02:00
}
2020-05-17 00:10:29 +02:00
}
2021-11-27 22:45:37 +01:00
private float GetInnerOffsetX ( GenericFont defaultFont , int tokenIndex , int lineIndex , TextAlignment alignment ) {
2021-10-04 23:57:58 +02:00
if ( alignment > TextAlignment . Left ) {
var token = this . Tokens [ tokenIndex ] ;
2021-11-27 23:35:37 +01:00
var tokenFont = token . GetFont ( defaultFont ) ;
2022-12-06 16:49:19 +01:00
var tokenWidth = lineIndex < = 0 ? token . GetSelfWidth ( tokenFont ) : 0 ;
var endsLater = this . EndsLater ( tokenIndex , lineIndex ) ;
2021-11-22 18:52:52 +01:00
// if the line ends in our token, we should ignore trailing white space
2022-12-06 16:49:19 +01:00
var restOfLine = tokenFont . MeasureString ( token . SplitDisplayString [ lineIndex ] , ! endsLater ) . X + tokenWidth ;
2021-11-22 18:52:52 +01:00
if ( endsLater ) {
2021-10-04 23:57:58 +02:00
for ( var i = tokenIndex + 1 ; i < this . Tokens . Length ; i + + ) {
var other = this . Tokens [ i ] ;
2022-12-06 17:49:38 +01:00
var otherFont = other . GetFont ( defaultFont ) ;
restOfLine + = otherFont . MeasureString ( other . SplitDisplayString [ 0 ] , ! this . EndsLater ( i , 0 ) ) . X + other . GetSelfWidth ( otherFont ) ;
2022-09-02 14:07:23 +02:00
// if the token's split display string has multiple lines, then the line ends in it, which means we can stop
if ( other . SplitDisplayString . Length > 1 )
2021-10-04 23:57:58 +02:00
break ;
}
}
if ( alignment = = TextAlignment . Center )
restOfLine / = 2 ;
return - restOfLine ;
}
return 0 ;
}
2022-12-06 16:49:19 +01:00
private bool EndsLater ( int tokenIndex , int lineIndex ) {
// if we're the last line in our line array, then we don't contain a line split, so the line ends in a later token
return lineIndex > = this . Tokens [ tokenIndex ] . SplitDisplayString . Length - 1 & & tokenIndex < this . Tokens . Length - 1 ;
}
2022-12-07 13:35:57 +01:00
private IEnumerable < GenericFont . DecoratedCodePointSource > AsDecoratedSources ( GenericFont font ) {
return this . Tokens . Select ( t = > {
var tokenFont = t . GetFont ( font ) ;
return new GenericFont . DecoratedCodePointSource ( new CodePointSource ( t . Substring ) , tokenFont , t . GetSelfWidth ( tokenFont ) ) ;
} ) ;
}
2020-05-15 00:34:04 +02:00
}
2022-06-17 18:23:47 +02:00
}