2020-05-17 00:10:29 +02:00
using System.Collections.Generic ;
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 ;
2020-06-21 23:23:52 +02: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 ;
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 ;
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 ( ) ;
2021-06-25 15:23:30 +02:00
this . RecalculateTokenData ( 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
/// <seealso cref="GenericFont.SplitString"/>
/// </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 ) {
2021-05-18 16:47:38 +02:00
// a split string has the same character count as the input string but with newline characters added
this . modifiedString = font . SplitString ( this . String , width , scale ) ;
2021-06-25 15:23:30 +02:00
this . StoreModifiedSubstrings ( 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"/>.
/// <seealso cref="GenericFont.TruncateString"/>
/// </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 ) {
2021-05-18 16:47:38 +02:00
this . modifiedString = font . TruncateString ( this . String , width , scale , false , ellipsis ) ;
2021-06-25 15:23:30 +02:00
this . StoreModifiedSubstrings ( font , alignment ) ;
2020-05-15 00:34:04 +02:00
}
2021-06-25 15:23:30 +02:00
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
2020-05-15 19:55:59 +02:00
public Vector2 Measure ( GenericFont font ) {
2020-05-17 00:10:29 +02:00
return font . MeasureString ( this . DisplayString ) ;
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)"/>
2021-06-25 15:23:30 +02:00
public void Draw ( GameTime time , SpriteBatch batch , Vector2 pos , GenericFont font , Color color , float scale , float depth , TextAlignment alignment = TextAlignment . Left ) {
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 ] ;
2020-05-17 00:10:29 +02:00
var drawFont = token . GetFont ( font ) ? ? font ;
var drawColor = token . GetColor ( color ) ? ? color ;
2021-06-25 15:23:30 +02:00
for ( var l = 0 ; l < token . SplitDisplayString . Length ; l + + ) {
var line = token . SplitDisplayString [ l ] ;
for ( var i = 0 ; i < line . Length ; i + + ) {
var c = line [ i ] ;
2021-06-25 16:40:09 +02:00
if ( l = = 0 & & i = = 0 )
2021-06-25 15:23:30 +02:00
token . DrawSelf ( time , batch , pos + innerOffset , font , color , scale , depth ) ;
var cString = c . ToCachedString ( ) ;
token . DrawCharacter ( time , batch , c , cString , i , pos + innerOffset , drawFont , drawColor , scale , depth ) ;
innerOffset . X + = font . MeasureString ( cString ) . X * scale ;
}
// 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 ;
2020-05-15 00:34:04 +02:00
innerOffset . Y + = font . LineHeight * scale ;
}
}
}
}
2021-06-25 15:23:30 +02:00
private void StoreModifiedSubstrings ( GenericFont font , TextAlignment alignment ) {
2021-05-18 16:47:38 +02:00
if ( this . Tokens . Length = = 1 ) {
2021-06-25 15:23:30 +02:00
// skip substring logic for unformatted text
2021-05-18 16:47:38 +02:00
this . Tokens [ 0 ] . ModifiedSubstring = this . modifiedString ;
2021-06-25 15:23:30 +02:00
} else {
// this is basically a substring function that ignores added newlines for indexing
var index = 0 ;
var currToken = 0 ;
var splitIndex = 0 ;
var ret = new StringBuilder ( ) ;
while ( splitIndex < this . modifiedString . Length & & currToken < this . Tokens . Length ) {
var token = this . Tokens [ currToken ] ;
if ( token . Substring . Length > 0 ) {
ret . Append ( this . modifiedString [ splitIndex ] ) ;
// if the current char is not an added newline, we simulate length increase
if ( this . modifiedString [ splitIndex ] ! = '\n' | | this . String [ index ] = = '\n' )
index + + ;
splitIndex + + ;
}
// move on to the next token if we reached its end
if ( index > = token . Index + token . Substring . Length ) {
token . ModifiedSubstring = ret . ToString ( ) ;
ret . Clear ( ) ;
currToken + + ;
}
}
// set additional token contents beyond our string in case we truncated
if ( ret . Length > 0 )
this . Tokens [ currToken + + ] . ModifiedSubstring = ret . ToString ( ) ;
while ( currToken < this . Tokens . Length )
this . Tokens [ currToken + + ] . ModifiedSubstring = string . Empty ;
2021-05-18 16:47:38 +02:00
}
2021-06-25 15:23:30 +02:00
this . RecalculateTokenData ( font , alignment ) ;
}
private void RecalculateTokenData ( GenericFont font , TextAlignment alignment ) {
// split display strings
foreach ( var token in this . Tokens )
token . SplitDisplayString = token . DisplayString . Split ( '\n' ) ;
// token areas
2021-10-04 23:57:58 +02:00
this . initialInnerOffset = this . GetInnerOffsetX ( font , 0 , 0 , alignment ) ;
var innerOffset = new Vector2 ( this . initialInnerOffset , 0 ) ;
2021-06-25 15:23:30 +02:00
for ( var t = 0 ; t < this . Tokens . Length ; t + + ) {
var token = this . Tokens [ t ] ;
2021-10-04 23:57:58 +02:00
token . InnerOffsets = new float [ token . SplitDisplayString . Length - 1 ] ;
2020-05-17 00:10:29 +02:00
var area = new List < RectangleF > ( ) ;
2021-06-25 15:23:30 +02:00
for ( var l = 0 ; l < token . SplitDisplayString . Length ; l + + ) {
var size = font . MeasureString ( token . SplitDisplayString [ l ] ) ;
2020-05-19 21:52:29 +02:00
var rect = new RectangleF ( innerOffset , size ) ;
if ( ! rect . IsEmpty )
area . Add ( rect ) ;
2020-05-17 00:10:29 +02:00
2021-06-25 15:23:30 +02:00
if ( l < token . SplitDisplayString . Length - 1 ) {
2021-10-04 23:57:58 +02:00
innerOffset . X = token . InnerOffsets [ l ] = this . GetInnerOffsetX ( font , t , l + 1 , alignment ) ;
2020-05-17 00:10:29 +02:00
innerOffset . Y + = font . LineHeight ;
} else {
innerOffset . X + = size . X ;
}
}
token . Area = area . ToArray ( ) ;
}
}
2021-10-04 23:57:58 +02:00
private float GetInnerOffsetX ( GenericFont font , int tokenIndex , int lineIndex , TextAlignment alignment ) {
if ( alignment > TextAlignment . Left ) {
var token = this . Tokens [ tokenIndex ] ;
var restOfLine = font . MeasureString ( token . SplitDisplayString [ lineIndex ] , true ) . X ;
if ( lineIndex > = token . SplitDisplayString . Length - 1 ) {
// the line ends somewhere in or after the next token
for ( var i = tokenIndex + 1 ; i < this . Tokens . Length ; i + + ) {
var other = this . Tokens [ i ] ;
if ( other . SplitDisplayString . Length > 1 ) {
// the line ends in this token
restOfLine + = font . MeasureString ( other . SplitDisplayString [ 0 ] ) . X ;
break ;
} else {
// the line doesn't end in this token, so add it fully
restOfLine + = font . MeasureString ( other . DisplayString ) . X ;
}
}
}
if ( alignment = = TextAlignment . Center )
restOfLine / = 2 ;
return - restOfLine ;
}
return 0 ;
}
2020-05-15 00:34:04 +02:00
}
}