using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Font; using MLEM.Formatting.Codes; using MLEM.Misc; namespace MLEM.Formatting { /// /// A tokenized string that was created using a /// public class TokenizedString : GenericDataHolder { /// /// The raw string that was used to create this tokenized string. /// public readonly string RawString; /// /// The , but with formatting codes stripped out. /// public readonly string String; /// /// The string that is actually displayed by this tokenized string. /// If this string has been or has been used, this string will contain the newline characters. /// public string DisplayString => this.modifiedString ?? this.String; /// /// The tokens that this tokenized string contains. /// public readonly Token[] Tokens; /// /// All of the formatting codes that are applied over this tokenized string. /// Note that, to get a formatting code for a certain token, use /// 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, Code[] allCodes) { this.RawString = rawString; this.String = strg; this.Tokens = tokens; this.AllCodes = allCodes; this.Realign(font, alignment); } /// /// 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 . /// /// The font to use for width calculations /// The maximum width, in display pixels based on the font and scale /// The scale to use for width measurements /// The text alignment that should be used for width calculations 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); } /// /// 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 . /// /// /// The font to use for width calculations /// The maximum width, in display pixels based on the font and scale /// The scale to use for width measurements /// The characters to add to the end of the string if it is too long /// The text alignment that should be used for width calculations 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); } /// /// Realigns this tokenized string using the given . /// If the is , trailing space characters (but not ) will be removed. /// /// The font to use for width calculations. /// The text alignment that should be used for width calculations. 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(); 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(); } } /// [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; } /// /// Measures the area that this entire tokenized string and all of its take up and returns it as a . /// /// The position that this string is being rendered at, which will offset the resulting . /// The scale that this string is being rendered with, which will scale the resulting . /// The area that this tokenized string takes up. public RectangleF GetArea(Vector2 stringPos, float scale) { return new RectangleF(stringPos + this.area.Location * scale, this.area.Size * scale); } /// /// Updates the formatting codes in this formatted string, causing animations to animate etc. /// /// The game's time public void Update(GameTime time) { foreach (var code in this.AllCodes) code.Update(time); } /// /// Returns the token under the given position. /// This can be used for hovering effects when the mouse is over a token, etc. /// /// The position that the string is drawn at /// The position to use for checking the token /// The scale that the string is drawn at /// The token under the target position 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; } /// 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 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)); }); } } }