mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-12-25 17:59:24 +01:00
Allow formatting codes to have an arbitrary custom width
This commit is contained in:
parent
cf3d0e8e0c
commit
b374d50815
9 changed files with 94 additions and 25 deletions
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -110,12 +110,12 @@ namespace MLEM.Font {
|
|||
/// <param name="ignoreTrailingSpaces">Whether trailing whitespace should be ignored in the returned size, causing the end of each line to be effectively trimmed</param>
|
||||
/// <returns>The size of the string when drawn with this font</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="MeasureString(string,bool)"/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -129,12 +129,12 @@ namespace MLEM.Font {
|
|||
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
|
||||
/// <returns>The truncated string, or the same string if it is shorter than the maximum width</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TruncateString(string,float,float,bool,string)"/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -165,21 +165,22 @@ namespace MLEM.Font {
|
|||
/// <param name="scale">The scale to use for width measurements</param>
|
||||
/// <returns>The split string as an enumerable of split sections</returns>
|
||||
public IEnumerable<string> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SplitStringSeparate(string,float,float)"/>
|
||||
public IEnumerable<string> 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<int, GenericFont> fontFunction) {
|
||||
internal Vector2 MeasureString(CodePointSource text, bool ignoreTrailingSpaces, Func<int, GenericFont> fontFunction, Func<int, float> 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<int, GenericFont> fontFunction) {
|
||||
internal StringBuilder TruncateString(CodePointSource text, float width, float scale, bool fromBack, string ellipsis, Func<int, GenericFont> fontFunction, Func<int, float> 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<string> SplitStringSeparate(CodePointSource text, float width, float scale, Func<int, GenericFont> fontFunction) {
|
||||
internal IEnumerable<string> SplitStringSeparate(CodePointSource text, float width, float scale, Func<int, GenericFont> fontFunction, Func<int, float> 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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Token.GetSelfWidth"/>
|
||||
public virtual float GetSelfWidth(GenericFont font) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update this formatting code's animations etc.
|
||||
/// </summary>
|
||||
|
@ -80,6 +86,7 @@ namespace MLEM.Formatting.Codes {
|
|||
/// </summary>
|
||||
/// <param name="font">The font that is used</param>
|
||||
/// <returns>The replacement string for this formatting code</returns>
|
||||
[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;
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ namespace MLEM.Formatting.Codes {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetReplacementString(GenericFont font) {
|
||||
return GenericFont.Emsp.ToString();
|
||||
public override float GetSelfWidth(GenericFont font) {
|
||||
return font.LineHeight;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -142,8 +142,11 @@ namespace MLEM.Formatting {
|
|||
}
|
||||
|
||||
private static string StripFormatting(GenericFont font, string s, IEnumerable<Code> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -80,6 +80,19 @@ namespace MLEM.Formatting {
|
|||
return defaultPick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the width of the token itself, including all of the <see cref="Code"/> instances that this token contains.
|
||||
/// Note that this method does not return the width of this token's <see cref="DisplayString"/>, but only the width that the codes themselves take up.
|
||||
/// </summary>
|
||||
/// <param name="font">The font to use for calculating the width.</param>
|
||||
/// <returns>The width of this token itself.</returns>
|
||||
public float GetSelfWidth(GenericFont font) {
|
||||
var ret = 0F;
|
||||
foreach (var code in this.AppliedCodes)
|
||||
ret += code.GetSelfWidth(font);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the token itself, including all of the <see cref="Code"/> instances that this token contains.
|
||||
/// Note that, to draw the token's actual string, <see cref="DrawCharacter"/> is used.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
@ -63,7 +64,8 @@ namespace MLEM.Formatting {
|
|||
/// <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) {
|
||||
// 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 {
|
|||
/// <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) {
|
||||
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<RectangleF>();
|
||||
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 {
|
|||
|
||||
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <i Test> is simply dummy text of the <i Test> printing and typesetting <i Test> industry. Lorem Ipsum has been the industry's standard dummy text <i Test> ever since the <i Test> 1500s, when <i Test><i Test><i Test><i Test><i Test><i Test><i Test> 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]
|
||||
|
|
Loading…
Reference in a new issue