mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-09-27 08:51:04 +02:00
272 lines
14 KiB
C#
272 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Microsoft.Xna.Framework;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using MLEM.Extensions;
|
|
using MLEM.Font;
|
|
using MLEM.Formatting.Codes;
|
|
using MLEM.Misc;
|
|
|
|
namespace MLEM.Formatting {
|
|
/// <summary>
|
|
/// A tokenized string that was created using a <see cref="TextFormatter"/>
|
|
/// </summary>
|
|
public class TokenizedString : GenericDataHolder {
|
|
|
|
/// <summary>
|
|
/// The raw string that was used to create this tokenized string.
|
|
/// </summary>
|
|
public readonly string RawString;
|
|
/// <summary>
|
|
/// The <see cref="RawString"/>, but with formatting codes stripped out.
|
|
/// </summary>
|
|
public readonly string String;
|
|
/// <summary>
|
|
/// The string that is actually displayed by this tokenized string.
|
|
/// If this string has been <see cref="Split"/> or <see cref="Truncate"/> has been used, this string will contain the newline characters.
|
|
/// </summary>
|
|
public string DisplayString => this.modifiedString ?? this.String;
|
|
/// <summary>
|
|
/// The tokens that this tokenized string contains.
|
|
/// </summary>
|
|
public readonly Token[] Tokens;
|
|
/// <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>
|
|
public readonly Code[] AllCodes;
|
|
private string modifiedString;
|
|
private float initialInnerOffset;
|
|
private RectangleF area;
|
|
|
|
internal TokenizedString(GenericFont font, TextAlignment alignment, string rawString, string strg, Token[] tokens) {
|
|
this.RawString = rawString;
|
|
this.String = strg;
|
|
this.Tokens = tokens;
|
|
|
|
// since a code can be present in multiple tokens, we use Distinct here
|
|
this.AllCodes = tokens.SelectMany(t => t.AppliedCodes).Distinct().ToArray();
|
|
// 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());
|
|
|
|
this.Realign(font, alignment);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Splits this tokenized string, inserting newline characters if the width of the string is bigger than the maximum width.
|
|
/// 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"/>.
|
|
/// </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="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) {
|
|
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);
|
|
}
|
|
|
|
/// <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(string,float,float,bool,string)"/>
|
|
/// </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>
|
|
/// <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) {
|
|
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);
|
|
}
|
|
|
|
/// <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
|
|
this.area = RectangleF.Empty;
|
|
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];
|
|
|
|
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;
|
|
}
|
|
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
|
var size = tokenFont.MeasureString(token.SplitDisplayString[l], !this.EndsLater(t, l));
|
|
var rect = new RectangleF(innerOffset, size);
|
|
if (!rect.IsEmpty) {
|
|
tokenArea.Add(rect);
|
|
this.area = RectangleF.Union(this.area, rect);
|
|
}
|
|
|
|
if (l < token.SplitDisplayString.Length - 1) {
|
|
innerOffset.X = token.InnerOffsets[l] = this.GetInnerOffsetX(font, t, l + 1, alignment);
|
|
innerOffset.Y += tokenFont.LineHeight;
|
|
} else {
|
|
innerOffset.X += size.X;
|
|
}
|
|
}
|
|
token.Area = tokenArea.ToArray();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
|
|
[Obsolete("Measure is deprecated. Use GetArea, which returns the string's total size measurement, instead.")]
|
|
public Vector2 Measure(GenericFont font) {
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the formatting codes in this formatted string, causing animations to animate etc.
|
|
/// </summary>
|
|
/// <param name="time">The game's time</param>
|
|
public void Update(GameTime time) {
|
|
foreach (var code in this.AllCodes)
|
|
code.Update(time);
|
|
}
|
|
|
|
/// <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>
|
|
public Token GetTokenUnderPos(Vector2 stringPos, Vector2 target, float scale) {
|
|
foreach (var token in this.Tokens) {
|
|
foreach (var rect in token.GetArea(stringPos, scale)) {
|
|
if (rect.Contains(target))
|
|
return token;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <inheritdoc cref="GenericFont.DrawString(SpriteBatch,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
|
public void Draw(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth, int? startIndex = null, int? endIndex = null) {
|
|
var innerOffset = new Vector2(this.initialInnerOffset * scale, 0);
|
|
for (var t = 0; t < this.Tokens.Length; t++) {
|
|
var token = this.Tokens[t];
|
|
if (endIndex != null && token.Index >= endIndex)
|
|
return;
|
|
|
|
var drawFont = token.GetFont(font);
|
|
var drawColor = token.GetColor(color);
|
|
|
|
if (startIndex == null || token.Index >= startIndex)
|
|
token.DrawSelf(time, batch, pos + innerOffset, drawFont, drawColor, scale, depth);
|
|
innerOffset.X += token.GetSelfWidth(drawFont) * scale;
|
|
|
|
var indexInToken = 0;
|
|
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
|
var cpsIndex = 0;
|
|
var line = new CodePointSource(token.SplitDisplayString[l]);
|
|
while (cpsIndex < line.Length) {
|
|
if (endIndex != null && token.Index + indexInToken >= endIndex)
|
|
return;
|
|
|
|
var (codePoint, length) = line.GetCodePoint(cpsIndex);
|
|
var character = CodePointSource.ToString(codePoint);
|
|
|
|
if (startIndex == null || token.Index + indexInToken >= startIndex)
|
|
token.DrawCharacter(time, batch, codePoint, character, indexInToken, pos + innerOffset, drawFont, drawColor, scale, depth);
|
|
|
|
innerOffset.X += drawFont.MeasureString(character).X * scale;
|
|
indexInToken += length;
|
|
cpsIndex += length;
|
|
}
|
|
|
|
// only split at a new line, not between tokens!
|
|
if (l < token.SplitDisplayString.Length - 1) {
|
|
innerOffset.X = token.InnerOffsets[l] * scale;
|
|
innerOffset.Y += drawFont.LineHeight * scale;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private float GetInnerOffsetX(GenericFont defaultFont, int tokenIndex, int lineIndex, TextAlignment alignment) {
|
|
if (alignment > TextAlignment.Left) {
|
|
var token = this.Tokens[tokenIndex];
|
|
var tokenFont = token.GetFont(defaultFont);
|
|
var tokenWidth = lineIndex <= 0 ? token.GetSelfWidth(tokenFont) : 0;
|
|
var endsLater = this.EndsLater(tokenIndex, lineIndex);
|
|
// if the line ends in our token, we should ignore trailing white space
|
|
var restOfLine = tokenFont.MeasureString(token.SplitDisplayString[lineIndex], !endsLater).X + tokenWidth;
|
|
if (endsLater) {
|
|
for (var i = tokenIndex + 1; i < this.Tokens.Length; i++) {
|
|
var other = this.Tokens[i];
|
|
var otherFont = other.GetFont(defaultFont);
|
|
restOfLine += otherFont.MeasureString(other.SplitDisplayString[0], !this.EndsLater(i, 0)).X + other.GetSelfWidth(otherFont);
|
|
// 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)
|
|
break;
|
|
}
|
|
}
|
|
if (alignment == TextAlignment.Center)
|
|
restOfLine /= 2;
|
|
return -restOfLine;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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));
|
|
});
|
|
}
|
|
|
|
}
|
|
}
|