diff --git a/CHANGELOG.md b/CHANGELOG.md index 0427396..e40dd8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Improvements - Improved the way InputHandler down time calculation works - Allow explicitly specifying each region for extended auto tiles - Added a generic version of IGenericDataHolder.SetData +- Allow formatting codes to have an arbitrary custom width - **Drastically improved StaticSpriteBatch batching performance** - **Made GenericFont and TokenizedString support UTF-32 characters like emoji** @@ -44,6 +45,7 @@ Fixes Removals - Removed DataContract attribute from GenericDataHolder - Marked EnumHelper as obsolete due to its reimplementation in [DynamicEnums](https://www.nuget.org/packages/DynamicEnums) +- Marked Code.GetReplacementString as obsolete ### MLEM.Ui Additions diff --git a/Demos/TextFormattingDemo.cs b/Demos/TextFormattingDemo.cs index 2473ff0..3ba66d3 100644 --- a/Demos/TextFormattingDemo.cs +++ b/Demos/TextFormattingDemo.cs @@ -1,6 +1,8 @@ using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MLEM.Extensions; using MLEM.Font; using MLEM.Formatting; using MLEM.Formatting.Codes; @@ -23,6 +25,7 @@ namespace Demos { private TextFormatter formatter; private TokenizedString tokenizedText; private GenericFont font; + private bool drawBounds; public TextFormattingDemo(MlemGame game) : base(game) {} @@ -56,6 +59,16 @@ namespace Demos { // since the text is already center-aligned, we only need to align it on the y axis here var size = this.tokenizedText.Measure(this.font) * TextFormattingDemo.Scale; var pos = new Vector2(this.GraphicsDevice.Viewport.Width / 2, (this.GraphicsDevice.Viewport.Height - size.Y) / 2); + + // draw bounds, which can be toggled with B in this demo + if (this.drawBounds) { + foreach (var token in this.tokenizedText.Tokens) { + foreach (var area in token.GetArea(pos, TextFormattingDemo.Scale)) + this.SpriteBatch.Draw(this.SpriteBatch.GetBlankTexture(), area, Color.Black * 0.25F); + } + } + + // draw the text itself this.tokenizedText.Draw(time, this.SpriteBatch, pos, this.font, Color.White, TextFormattingDemo.Scale, 0); this.SpriteBatch.End(); @@ -64,6 +77,8 @@ namespace Demos { public override void Update(GameTime time) { // update our tokenized string to animate the animation codes this.tokenizedText.Update(time); + if (this.InputHandler.IsPressed(Keys.B)) + this.drawBounds = !this.drawBounds; } public override void Clear() { diff --git a/MLEM/Font/GenericFont.cs b/MLEM/Font/GenericFont.cs index 2895a99..f849972 100644 --- a/MLEM/Font/GenericFont.cs +++ b/MLEM/Font/GenericFont.cs @@ -110,12 +110,12 @@ namespace MLEM.Font { /// Whether trailing whitespace should be ignored in the returned size, causing the end of each line to be effectively trimmed /// The size of the string when drawn with this font public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) { - return this.MeasureString(new CodePointSource(text), ignoreTrailingSpaces, null); + return this.MeasureString(new CodePointSource(text), ignoreTrailingSpaces, null, null); } /// public Vector2 MeasureString(StringBuilder text, bool ignoreTrailingSpaces = false) { - return this.MeasureString(new CodePointSource(text), ignoreTrailingSpaces, null); + return this.MeasureString(new CodePointSource(text), ignoreTrailingSpaces, null, null); } /// @@ -129,12 +129,12 @@ namespace MLEM.Font { /// The characters to add to the end of the string if it is too long /// The truncated string, or the same string if it is shorter than the maximum width public string TruncateString(string text, float width, float scale, bool fromBack = false, string ellipsis = "") { - return this.TruncateString(new CodePointSource(text), width, scale, fromBack, ellipsis, null).ToString(); + return this.TruncateString(new CodePointSource(text), width, scale, fromBack, ellipsis, null, null).ToString(); } /// public StringBuilder TruncateString(StringBuilder text, float width, float scale, bool fromBack = false, string ellipsis = "") { - return this.TruncateString(new CodePointSource(text), width, scale, fromBack, ellipsis, null); + return this.TruncateString(new CodePointSource(text), width, scale, fromBack, ellipsis, null, null); } /// @@ -165,21 +165,22 @@ namespace MLEM.Font { /// The scale to use for width measurements /// The split string as an enumerable of split sections public IEnumerable SplitStringSeparate(string text, float width, float scale) { - return this.SplitStringSeparate(new CodePointSource(text), width, scale, null); + return this.SplitStringSeparate(new CodePointSource(text), width, scale, null, null); } /// public IEnumerable SplitStringSeparate(StringBuilder text, float width, float scale) { - return this.SplitStringSeparate(new CodePointSource(text), width, scale, null); + return this.SplitStringSeparate(new CodePointSource(text), width, scale, null, null); } - internal Vector2 MeasureString(CodePointSource text, bool ignoreTrailingSpaces, Func fontFunction) { + internal Vector2 MeasureString(CodePointSource text, bool ignoreTrailingSpaces, Func fontFunction, Func extraWidthFunction) { var size = Vector2.Zero; if (text.Length <= 0) return size; var xOffset = 0F; var index = 0; while (index < text.Length) { + xOffset += extraWidthFunction?.Invoke(index) ?? 0; var font = fontFunction?.Invoke(index) ?? this; var (codePoint, length) = text.GetCodePoint(index); switch (codePoint) { @@ -218,7 +219,7 @@ namespace MLEM.Font { return size; } - internal StringBuilder TruncateString(CodePointSource text, float width, float scale, bool fromBack, string ellipsis, Func fontFunction) { + internal StringBuilder TruncateString(CodePointSource text, float width, float scale, bool fromBack, string ellipsis, Func fontFunction, Func extraWidthFunction) { var total = new StringBuilder(); var index = 0; while (index < text.Length) { @@ -230,7 +231,7 @@ namespace MLEM.Font { total.Append(CodePointSource.ToString(codePoint)); } - if (this.MeasureString(new CodePointSource(total + ellipsis), false, fontFunction).X * scale >= width) { + if (this.MeasureString(new CodePointSource(total + ellipsis), false, fontFunction, extraWidthFunction).X * scale >= width) { if (fromBack) { return total.Remove(0, length).Insert(0, ellipsis); } else { @@ -242,7 +243,7 @@ namespace MLEM.Font { return total; } - internal IEnumerable SplitStringSeparate(CodePointSource text, float width, float scale, Func fontFunction) { + internal IEnumerable SplitStringSeparate(CodePointSource text, float width, float scale, Func fontFunction, Func extraWidthFunction) { var currWidth = 0F; var lastSpaceIndex = -1; var widthSinceLastSpace = 0F; @@ -259,7 +260,7 @@ namespace MLEM.Font { } else { var font = fontFunction?.Invoke(index) ?? this; var character = CodePointSource.ToString(codePoint); - var charWidth = font.MeasureString(character).X * scale; + var charWidth = (font.MeasureString(character).X + (extraWidthFunction?.Invoke(index) ?? 0)) * scale; if (codePoint == ' ' || codePoint == GenericFont.Emsp || codePoint == GenericFont.Zwsp) { // remember the location of this (breaking!) space lastSpaceIndex = curr.Length; @@ -298,7 +299,7 @@ namespace MLEM.Font { var flippedV = (effects & SpriteEffects.FlipVertically) != 0; var flippedH = (effects & SpriteEffects.FlipHorizontally) != 0; if (flippedV || flippedH) { - var size = this.MeasureString(text, false, null); + var size = this.MeasureString(text, false, null, null); if (flippedH) { origin.X *= -1; flipX = -size.X; diff --git a/MLEM/Formatting/Codes/Code.cs b/MLEM/Formatting/Codes/Code.cs index 261c703..8b0f75e 100644 --- a/MLEM/Formatting/Codes/Code.cs +++ b/MLEM/Formatting/Codes/Code.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Text.RegularExpressions; using Microsoft.Xna.Framework; @@ -68,6 +69,11 @@ namespace MLEM.Formatting.Codes { return null; } + /// + public virtual float GetSelfWidth(GenericFont font) { + return 0; + } + /// /// Update this formatting code's animations etc. /// @@ -80,6 +86,7 @@ namespace MLEM.Formatting.Codes { /// /// The font that is used /// The replacement string for this formatting code + [Obsolete("This method is deprecated. Use GetSelfWidth to add additional width to this code and DrawSelf or DrawCharacter to draw additional items.")] public virtual string GetReplacementString(GenericFont font) { return string.Empty; } diff --git a/MLEM/Formatting/Codes/ImageCode.cs b/MLEM/Formatting/Codes/ImageCode.cs index 6e8dffb..4a6b6fd 100644 --- a/MLEM/Formatting/Codes/ImageCode.cs +++ b/MLEM/Formatting/Codes/ImageCode.cs @@ -26,8 +26,8 @@ namespace MLEM.Formatting.Codes { } /// - public override string GetReplacementString(GenericFont font) { - return GenericFont.Emsp.ToString(); + public override float GetSelfWidth(GenericFont font) { + return font.LineHeight; } /// diff --git a/MLEM/Formatting/TextFormatter.cs b/MLEM/Formatting/TextFormatter.cs index cc1e9c6..9190e8a 100644 --- a/MLEM/Formatting/TextFormatter.cs +++ b/MLEM/Formatting/TextFormatter.cs @@ -142,8 +142,11 @@ namespace MLEM.Formatting { } private static string StripFormatting(GenericFont font, string s, IEnumerable codes) { - foreach (var code in codes) + foreach (var code in codes) { + #pragma warning disable CS0618 s = code.Regex.Replace(s, code.GetReplacementString(font)); + #pragma warning restore CS0618 + } return s; } diff --git a/MLEM/Formatting/Token.cs b/MLEM/Formatting/Token.cs index ca2409a..f5a6467 100644 --- a/MLEM/Formatting/Token.cs +++ b/MLEM/Formatting/Token.cs @@ -80,6 +80,19 @@ namespace MLEM.Formatting { return defaultPick; } + /// + /// Returns the width of the token itself, including all of the instances that this token contains. + /// Note that this method does not return the width of this token's , but only the width that the codes themselves take up. + /// + /// The font to use for calculating the width. + /// The width of this token itself. + public float GetSelfWidth(GenericFont font) { + var ret = 0F; + foreach (var code in this.AppliedCodes) + ret += code.GetSelfWidth(font); + return ret; + } + /// /// Draws the token itself, including all of the instances that this token contains. /// Note that, to draw the token's actual string, is used. diff --git a/MLEM/Formatting/TokenizedString.cs b/MLEM/Formatting/TokenizedString.cs index 7dc3825..0f90d99 100644 --- a/MLEM/Formatting/TokenizedString.cs +++ b/MLEM/Formatting/TokenizedString.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -63,7 +64,8 @@ namespace MLEM.Formatting { /// The text alignment that should be used for width calculations public void Split(GenericFont font, float width, float scale, TextAlignment alignment = TextAlignment.Left) { // a split string has the same character count as the input string but with newline characters added - this.modifiedString = string.Join("\n", font.SplitStringSeparate(new CodePointSource(this.String), width, scale, i => this.GetFontForIndex(font, i))); + this.modifiedString = string.Join("\n", font.SplitStringSeparate(new CodePointSource(this.String), width, scale, + i => this.GetFontForIndex(font, i), i => this.GetSelfWidthForIndex(font, i))); this.StoreModifiedSubstrings(font, alignment); } @@ -78,7 +80,8 @@ namespace MLEM.Formatting { /// 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) { - this.modifiedString = font.TruncateString(new CodePointSource(this.String), width, scale, false, ellipsis, i => this.GetFontForIndex(font, i)).ToString(); + this.modifiedString = font.TruncateString(new CodePointSource(this.String), width, scale, false, ellipsis, + i => this.GetFontForIndex(font, i), i => this.GetSelfWidthForIndex(font, i)).ToString(); this.StoreModifiedSubstrings(font, alignment); } @@ -102,7 +105,10 @@ namespace MLEM.Formatting { token.InnerOffsets = new float[token.SplitDisplayString.Length - 1]; var area = new List(); for (var l = 0; l < token.SplitDisplayString.Length; l++) { - var size = tokenFont.MeasureString(token.SplitDisplayString[l]); + var size = tokenFont.MeasureString(token.SplitDisplayString[l], !this.EndsLater(t, l)); + if (l == 0) + size.X += token.GetSelfWidth(tokenFont); + var rect = new RectangleF(innerOffset, size); if (!rect.IsEmpty) area.Add(rect); @@ -120,7 +126,7 @@ namespace MLEM.Formatting { /// public Vector2 Measure(GenericFont font) { - return font.MeasureString(new CodePointSource(this.DisplayString), false, i => this.GetFontForIndex(font, i)); + return font.MeasureString(new CodePointSource(this.DisplayString), false, i => this.GetFontForIndex(font, i), i => this.GetSelfWidthForIndex(font, i)); } /// @@ -156,6 +162,7 @@ namespace MLEM.Formatting { for (var t = 0; t < this.Tokens.Length; t++) { var token = this.Tokens[t]; var drawFont = token.GetFont(font); + var selfWidth = token.GetSelfWidth(drawFont); var drawColor = token.GetColor(color); var indexInToken = 0; @@ -171,6 +178,8 @@ namespace MLEM.Formatting { token.DrawCharacter(time, batch, codePoint, character, indexInToken, pos + innerOffset, drawFont, drawColor, scale, depth); innerOffset.X += drawFont.MeasureString(character).X * scale; + if (indexInToken == 0) + innerOffset.X += selfWidth * scale; charIndex += length; indexInToken++; } @@ -224,14 +233,15 @@ namespace MLEM.Formatting { if (alignment > TextAlignment.Left) { var token = this.Tokens[tokenIndex]; var tokenFont = token.GetFont(defaultFont); - // 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 - var endsLater = lineIndex >= token.SplitDisplayString.Length - 1 && tokenIndex < this.Tokens.Length - 1; + 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; + 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]; - restOfLine += other.GetFont(defaultFont).MeasureString(other.SplitDisplayString[0], true).X; + var otherWidth = other.GetSelfWidth(defaultFont); + restOfLine += other.GetFont(defaultFont).MeasureString(other.SplitDisplayString[0], !this.EndsLater(i, 0)).X + otherWidth; // 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; @@ -253,5 +263,21 @@ namespace MLEM.Formatting { return null; } + private float GetSelfWidthForIndex(GenericFont font, int index) { + foreach (var token in this.Tokens) { + if (index == token.Index) + return token.GetSelfWidth(font); + // if we're already beyond this token, any later tokens definitely won't match + if (token.Index > index) + break; + } + 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; + } + } } diff --git a/Tests/FontTests.cs b/Tests/FontTests.cs index 6da3a13..7511d03 100644 --- a/Tests/FontTests.cs +++ b/Tests/FontTests.cs @@ -8,7 +8,7 @@ using MLEM.Formatting.Codes; using MLEM.Textures; using NUnit.Framework; -namespace Tests; +namespace Tests; public class FontTests { @@ -121,7 +121,7 @@ public class FontTests { const string strg = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."; var ret = this.formatter.Tokenize(this.font, strg); Assert.AreEqual(ret.Tokens.Length, 13); - Assert.AreEqual(ret.DisplayString, "Lorem Ipsum \u2003 is simply dummy text of the \u2003 printing and typesetting \u2003 industry. Lorem Ipsum has been the industry's standard dummy text \u2003 ever since the \u2003 1500s, when \u2003\u2003\u2003\u2003\u2003\u2003\u2003 an unknown printer took a galley of type and scrambled it to make a type specimen book."); + Assert.AreEqual(ret.DisplayString, "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."); Assert.AreEqual(ret.AllCodes.Length, 12); } @@ -137,6 +137,8 @@ public class FontTests { CompareSizes("\nThis is a very simple test string"); CompareSizes("This is a very simple test string\n"); CompareSizes("This is a very simple test string\n\n\n\n\n"); + CompareSizes("This is a very simple test string"); + CompareSizes("This is a very simple \n test string"); } [Test]