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 ;
2020-05-15 00:34:04 +02:00
2020-05-21 12:53:42 +02:00
internal TokenizedString ( GenericFont font , 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 ( ) ;
2020-05-17 00:10:29 +02:00
this . CalculateTokenAreas ( font ) ;
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>
2020-05-15 00:34:04 +02:00
public void Split ( GenericFont font , float width , float scale ) {
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 ) ;
this . StoreModifiedSubstrings ( font ) ;
}
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>
public void Truncate ( GenericFont font , float width , float scale , string ellipsis = "" ) {
this . modifiedString = font . TruncateString ( this . String , width , scale , false , ellipsis ) ;
this . StoreModifiedSubstrings ( font ) ;
2020-05-15 00:34:04 +02:00
}
2020-05-21 12:53:42 +02:00
/// <inheritdoc cref="GenericFont.MeasureString(string)"/>
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 ) {
return this . Tokens . FirstOrDefault ( t = > t . GetArea ( stringPos , scale ) . Any ( r = > r . Contains ( target ) ) ) ;
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)"/>
2020-05-15 00:34:04 +02:00
public void Draw ( GameTime time , SpriteBatch batch , Vector2 pos , GenericFont font , Color color , float scale , float depth ) {
var innerOffset = new Vector2 ( ) ;
foreach ( var token in this . Tokens ) {
2020-05-17 00:10:29 +02:00
var drawFont = token . GetFont ( font ) ? ? font ;
var drawColor = token . GetColor ( color ) ? ? color ;
for ( var i = 0 ; i < token . DisplayString . Length ; i + + ) {
var c = token . DisplayString [ i ] ;
2020-05-15 00:34:04 +02:00
if ( c = = '\n' ) {
innerOffset . X = 0 ;
innerOffset . Y + = font . LineHeight * scale ;
}
2020-05-15 19:55:59 +02:00
if ( i = = 0 )
token . DrawSelf ( time , batch , pos + innerOffset , font , color , scale , depth ) ;
2020-05-15 00:34:04 +02:00
2020-06-21 23:23:52 +02:00
var cString = c . ToCachedString ( ) ;
2020-06-20 12:12:34 +02:00
token . DrawCharacter ( time , batch , c , cString , i , pos + innerOffset , drawFont , drawColor , scale , depth ) ;
innerOffset . X + = font . MeasureString ( cString ) . X * scale ;
2020-05-15 00:34:04 +02:00
}
}
}
2021-05-18 16:47:38 +02:00
private void StoreModifiedSubstrings ( GenericFont font ) {
// skip substring logic for unformatted text
if ( this . Tokens . Length = = 1 ) {
this . Tokens [ 0 ] . ModifiedSubstring = this . modifiedString ;
return ;
}
// 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 ( ) ;
2021-05-20 19:59:37 +02:00
while ( splitIndex < this . modifiedString . Length & & currToken < this . Tokens . Length ) {
2021-05-18 16:47:38 +02:00
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 )
2021-05-20 19:59:37 +02:00
this . Tokens [ currToken + + ] . ModifiedSubstring = ret . ToString ( ) ;
2021-05-18 16:47:38 +02:00
while ( currToken < this . Tokens . Length )
this . Tokens [ currToken + + ] . ModifiedSubstring = string . Empty ;
this . CalculateTokenAreas ( font ) ;
}
2020-05-17 00:10:29 +02:00
private void CalculateTokenAreas ( GenericFont font ) {
var innerOffset = new Vector2 ( ) ;
foreach ( var token in this . Tokens ) {
var area = new List < RectangleF > ( ) ;
var split = token . DisplayString . Split ( '\n' ) ;
for ( var i = 0 ; i < split . Length ; i + + ) {
var size = font . MeasureString ( split [ i ] ) ;
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
if ( i < split . Length - 1 ) {
innerOffset . X = 0 ;
innerOffset . Y + = font . LineHeight ;
} else {
innerOffset . X + = size . X ;
}
}
token . Area = area . ToArray ( ) ;
}
}
2020-05-15 00:34:04 +02:00
}
}