1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-10 03:28:43 +02:00

Made TextFormatter string size based on the currently active font and added a formatting code to allow for inline font changes

This commit is contained in:
Ell 2021-11-27 22:45:37 +01:00
parent ad1d6a864e
commit 53b93a34f8
11 changed files with 179 additions and 72 deletions

View file

@ -23,6 +23,7 @@ Improvements
- Added Padding.Empty
- Throw an exception when text formatter macros resolve recursively too many times
- Allow using StaticSpriteBatch for AutoTiling
- Made TextFormatter string size based on the currently active font rather than the default one
Fixes
- Fixed some end-of-line inconsistencies when using the Right text alignment
@ -31,6 +32,7 @@ Fixes
Additions
- Allow specifying a maximum amount of characters for a TextField
- Added a multiline editing mode to TextField
- Added a formatting code to allow for inline font changes
Improvements
- *Made Image ScaleToImage take ui scale into account*

View file

@ -34,6 +34,13 @@
/processorParam:TextureFormat=Compressed
/build:Fonts/TestFontItalic.spritefont
#begin Fonts/MonospacedFont.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
/processorParam:PremultiplyAlpha=True
/processorParam:TextureFormat=Compressed
/build:Fonts/MonospacedFont.spritefont
#begin Textures/Anim.png
/importer:TextureImporter
/processor:TextureProcessor

Binary file not shown.

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file contains an xml description of a font, and will be read by the XNA
Framework Content Pipeline. Follow the comments to customize the appearance
of the font in your game, and to change the characters which are available to draw
with.
-->
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<!--
Modify this string to change the font that will be imported.
-->
<FontName>JetBrainsMono-Regular.ttf</FontName>
<!--
Size is a float value, measured in points. Modify this value to change
the size of the font.
-->
<Size>32</Size>
<!--
Spacing is a float value, measured in pixels. Modify this value to change
the amount of spacing in between characters.
-->
<Spacing>0</Spacing>
<!--
UseKerning controls the layout of the font. If this value is true, kerning information
will be used when placing characters.
-->
<UseKerning>true</UseKerning>
<!--
Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
and "Bold, Italic", and are case sensitive.
-->
<Style>Regular</Style>
<!--
If you uncomment this line, the default character will be substituted if you draw
or measure text that contains characters which were not included in the font.
-->
<DefaultCharacter>*</DefaultCharacter>
<!--
CharacterRegions control what letters are available in the font. Every
character from Start to End will be built and made available for drawing. The
default range is from 32, (ASCII space), to 126, ('~'), covering the basic Latin
character set. The characters are ordered according to the Unicode standard.
See the documentation for more information.
-->
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#591;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

View file

@ -47,7 +47,8 @@ namespace Demos {
CheckboxTexture = new NinePatch(new TextureRegion(this.testTexture, 24, 8, 16, 16), 4),
CheckboxCheckmark = new TextureRegion(this.testTexture, 24, 0, 8, 8),
RadioTexture = new NinePatch(new TextureRegion(this.testTexture, 16, 0, 8, 8), 3),
RadioCheckmark = new TextureRegion(this.testTexture, 32, 0, 8, 8)
RadioCheckmark = new TextureRegion(this.testTexture, 32, 0, 8, 8),
AdditionalFonts = {{"Monospaced", new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/MonospacedFont"))}}
};
var untexturedStyle = new UntexturedStyle(this.SpriteBatch) {
TextScale = style.TextScale,
@ -87,7 +88,7 @@ namespace Demos {
this.root.AddChild(new VerticalSpace(3));
// a paragraph with formatting codes. To see them all or to add more, check the TextFormatting class
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Paragraphs can also contain <c Blue>formatting codes</c>, including colors and <i>text styles</i>. The names of all <c Orange>MonoGame Colors</c> can be used, as well as the codes <i>Italic</i>, <b>Bold</b>, <s>Drop Shadow'd</s> and <s><c Pink>mixed formatting</s></c>. \n<i>Even <c #ff611f82>inline custom colors</c> work!</i>"));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Paragraphs can also contain <c Blue>formatting codes</c>, including colors and <i>text styles</i>. The names of all <c Orange>MonoGame Colors</c> can be used, as well as the codes <i>Italic</i>, <b>Bold</b>, <s>Drop Shadow'd</s> and <s><c Pink>mixed formatting</s></c>. You can also add additional fonts for things like\n<f Monospaced>void Code() {\n // Code\n}</f>\n<i>Even <c #ff611f82>inline custom colors</c> work!</i>"));
// adding some custom image formatting codes
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Additionally, you can create custom formatting codes that contain <i Grass> images and more!"));

View file

@ -4,7 +4,7 @@ The **MLEM** package contains a simple text formatting system that supports colo
Text formatting makes use of [generic fonts](font_extensions.md).
It should also be noted that [MLEM.Ui](ui.md)'s `Paragraph`s support text formatting out of the box.
It should also be noted that [MLEM.Ui](ui.md)'s `Paragraph` supports text formatting out of the box.
## Formatting codes
To format your text, you can insert *formatting codes* into it. Almost all of these codes are single letters surrounded by `<>`, and some formatting codes can accept additional parameters after their letter representation.
@ -16,6 +16,10 @@ By default, the following formatting options are available:
- Underlined and strikethrough text using `<u>` and `<st>`, respectively. Reset using `</u>` and `</st>`.
- A wobbly sine wave animation using `<a wobbly>`. Optional parameters for the wobble's intensity and height are accepted: `<a wobbly 10 0.25>`. Reset using `</a>`.
When using [MLEM.Ui](ui.md)'s `Paragraph`, these additional formatting options are available by default:
- Hoverable and clickable links using `<l Url>`. Note that this code does not automatically change the color of the text. Reset using `</l>`.
- Inline font changes using `<f FontName>`, with custom fonts gathered from `UiStyle.AdditionalFonts`. Reset using `</f>`.
## Getting your text ready
To get your text ready for rendering with formatting codes, it has to be tokenized. For that, you need to create a new text formatter first. Additionally, you need to have a [generic font](font_extensions.md) ready:
```cs

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using MLEM.Font;
using MLEM.Formatting;
@ -194,6 +195,10 @@ namespace MLEM.Ui.Style {
/// Note that this sound is only played if the callbacks have any subscribers.
/// </summary>
public SoundEffectInfo ActionSound;
/// <summary>
/// A set of additional fonts that can be used for the <c>&lt;f FontName&gt;</c> formatting code
/// </summary>
public Dictionary<string, GenericFont> AdditionalFonts = new Dictionary<string, GenericFont>();
}
}

View file

@ -223,6 +223,8 @@ namespace MLEM.Ui {
this.TextFormatter = new TextFormatter();
this.TextFormatter.Codes.Add(new Regex("<l(?: ([^>]+))?>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F,
t => this.Controls.MousedElement is Paragraph.Link l1 && l1.Token == t || this.Controls.TouchedElement is Paragraph.Link l2 && l2.Token == t));
this.TextFormatter.Codes.Add(new Regex("<f ([^>]+)>"), (_, m, r) => new FontCode(m, r,
f => this.Style.AdditionalFonts != null && this.Style.AdditionalFonts.TryGetValue(m.Groups[1].Value, out var c) ? c : f));
}
/// <summary>

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
@ -88,44 +89,7 @@ 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) {
var size = Vector2.Zero;
if (text.Length <= 0)
return size;
var xOffset = 0F;
for (var i = 0; i < text.Length; i++) {
switch (text[i]) {
case '\n':
xOffset = 0;
size.Y += this.LineHeight;
break;
case OneEmSpace:
xOffset += this.LineHeight;
break;
case Nbsp:
xOffset += this.MeasureChar(' ');
break;
case Zwsp:
// don't add width for a zero-width space
break;
case ' ':
if (ignoreTrailingSpaces && IsTrailingSpace(text, i)) {
// if this is a trailing space, we can skip remaining spaces too
i = text.Length - 1;
break;
}
xOffset += this.MeasureChar(' ');
break;
default:
xOffset += this.MeasureChar(text[i]);
break;
}
// increase x size if this line is the longest
if (xOffset > size.X)
size.X = xOffset;
}
// include the last line's height too!
size.Y += this.LineHeight;
return size;
return MeasureString(i => this, text, ignoreTrailingSpaces);
}
/// <summary>
@ -139,24 +103,7 @@ 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 = "") {
var total = new StringBuilder();
var ellipsisWidth = this.MeasureString(ellipsis).X * scale;
for (var i = 0; i < text.Length; i++) {
if (fromBack) {
total.Insert(0, text[text.Length - 1 - i]);
} else {
total.Append(text[i]);
}
if (this.MeasureString(total.ToString()).X * scale + ellipsisWidth >= width) {
if (fromBack) {
return total.Remove(0, 1).Insert(0, ellipsis).ToString();
} else {
return total.Remove(total.Length - 1, 1).Append(ellipsis).ToString();
}
}
}
return total.ToString();
return TruncateString(i => this, text, width, scale, fromBack, ellipsis);
}
/// <summary>
@ -182,6 +129,72 @@ 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 SplitStringSeparate(i => this, text, width, scale);
}
internal static Vector2 MeasureString(Func<int, GenericFont> fontFunction, string text, bool ignoreTrailingSpaces) {
var size = Vector2.Zero;
if (text.Length <= 0)
return size;
var xOffset = 0F;
for (var i = 0; i < text.Length; i++) {
var font = fontFunction(i);
switch (text[i]) {
case '\n':
xOffset = 0;
size.Y += font.LineHeight;
break;
case OneEmSpace:
xOffset += font.LineHeight;
break;
case Nbsp:
xOffset += font.MeasureChar(' ');
break;
case Zwsp:
// don't add width for a zero-width space
break;
case ' ':
if (ignoreTrailingSpaces && IsTrailingSpace(text, i)) {
// if this is a trailing space, we can skip remaining spaces too
i = text.Length - 1;
break;
}
xOffset += font.MeasureChar(' ');
break;
default:
xOffset += font.MeasureChar(text[i]);
break;
}
// increase x size if this line is the longest
if (xOffset > size.X)
size.X = xOffset;
}
// include the last line's height too!
size.Y += fontFunction(text.Length - 1).LineHeight;
return size;
}
internal static string TruncateString(Func<int, GenericFont> fontFunction, string text, float width, float scale, bool fromBack, string ellipsis) {
var total = new StringBuilder();
for (var i = 0; i < text.Length; i++) {
if (fromBack) {
total.Insert(0, text[text.Length - 1 - i]);
} else {
total.Append(text[i]);
}
if (fontFunction(i).MeasureString(total + ellipsis).X * scale >= width) {
if (fromBack) {
return total.Remove(0, 1).Insert(0, ellipsis).ToString();
} else {
return total.Remove(total.Length - 1, 1).Append(ellipsis).ToString();
}
}
}
return total.ToString();
}
internal static IEnumerable<string> SplitStringSeparate(Func<int, GenericFont> fontFunction, string text, float width, float scale) {
var currWidth = 0F;
var lastSpaceIndex = -1;
var widthSinceLastSpace = 0F;
@ -195,7 +208,7 @@ namespace MLEM.Font {
widthSinceLastSpace = 0;
currWidth = 0;
} else {
var cWidth = this.MeasureString(c.ToCachedString()).X * scale;
var cWidth = fontFunction(i).MeasureString(c.ToCachedString()).X * scale;
if (c == ' ' || c == OneEmSpace || c == Zwsp) {
// remember the location of this (breaking!) space
lastSpaceIndex = curr.Length;

View file

@ -41,7 +41,7 @@ namespace MLEM.Formatting {
this.Codes.Add(new Regex("<u>"), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.85F));
this.Codes.Add(new Regex("<st>"), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.55F));
this.Codes.Add(new Regex("</(s|u|st|l)>"), (f, m, r) => new ResetFormattingCode(m, r));
this.Codes.Add(new Regex("</(b|i)>"), (f, m, r) => new FontCode(m, r, null));
this.Codes.Add(new Regex("</(b|i|f)>"), (f, m, r) => new FontCode(m, r, null));
// color codes
foreach (var c in typeof(Color).GetProperties()) {

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -59,7 +60,7 @@ 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 = font.SplitString(this.String, width, scale);
this.modifiedString = string.Join("\n", GenericFont.SplitStringSeparate(i => this.GetFontForIndex(font, i), this.String, width, scale));
this.StoreModifiedSubstrings(font, alignment);
}
@ -74,13 +75,13 @@ 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(this.String, width, scale, false, ellipsis);
this.modifiedString = GenericFont.TruncateString(i => this.GetFontForIndex(font, i), this.String, width, scale, false, ellipsis);
this.StoreModifiedSubstrings(font, alignment);
}
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
public Vector2 Measure(GenericFont font) {
return font.MeasureString(this.DisplayString);
return GenericFont.MeasureString(i => this.GetFontForIndex(font, i), this.DisplayString, false);
}
/// <summary>
@ -122,11 +123,11 @@ namespace MLEM.Formatting {
for (var i = 0; i < line.Length; i++) {
var c = line[i];
if (l == 0 && i == 0)
token.DrawSelf(time, batch, pos + innerOffset, font, color, scale, depth);
token.DrawSelf(time, batch, pos + innerOffset, drawFont, color, scale, depth);
var cString = c.ToCachedString();
token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth);
innerOffset.X += font.MeasureString(cString).X * scale;
innerOffset.X += drawFont.MeasureString(cString).X * scale;
}
// only split at a new line, not between tokens!
if (l < token.SplitDisplayString.Length - 1) {
@ -183,17 +184,18 @@ namespace MLEM.Formatting {
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) ?? font;
token.InnerOffsets = new float[token.SplitDisplayString.Length - 1];
var area = new List<RectangleF>();
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
var size = font.MeasureString(token.SplitDisplayString[l]);
var size = tokenFont.MeasureString(token.SplitDisplayString[l]);
var rect = new RectangleF(innerOffset, size);
if (!rect.IsEmpty)
area.Add(rect);
if (l < token.SplitDisplayString.Length - 1) {
innerOffset.X = token.InnerOffsets[l] = this.GetInnerOffsetX(font, t, l + 1, alignment);
innerOffset.Y += font.LineHeight;
innerOffset.Y += tokenFont.LineHeight;
} else {
innerOffset.X += size.X;
}
@ -202,24 +204,26 @@ namespace MLEM.Formatting {
}
}
private float GetInnerOffsetX(GenericFont font, int tokenIndex, int lineIndex, TextAlignment alignment) {
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) ?? 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;
// if the line ends in our token, we should ignore trailing white space
var restOfLine = font.MeasureString(token.SplitDisplayString[lineIndex], !endsLater).X;
var restOfLine = tokenFont.MeasureString(token.SplitDisplayString[lineIndex], !endsLater).X;
if (endsLater) {
for (var i = tokenIndex + 1; i < this.Tokens.Length; i++) {
var other = this.Tokens[i];
var otherFont = other.GetFont(defaultFont) ?? defaultFont;
if (other.SplitDisplayString.Length > 1) {
// the line ends in this token (so we also ignore trailing whitespaces)
restOfLine += font.MeasureString(other.SplitDisplayString[0], true).X;
restOfLine += otherFont.MeasureString(other.SplitDisplayString[0], true).X;
break;
} else {
// the line doesn't end in this token (or it's the last token), so add it fully
var lastToken = i >= this.Tokens.Length - 1;
restOfLine += font.MeasureString(other.DisplayString, lastToken).X;
restOfLine += otherFont.MeasureString(other.DisplayString, lastToken).X;
}
}
}
@ -230,5 +234,14 @@ namespace MLEM.Formatting {
return 0;
}
private GenericFont GetFontForIndex(GenericFont font, int index) {
foreach (var token in this.Tokens) {
index -= token.Substring.Length;
if (index <= 0)
return token.GetFont(font) ?? font;
}
return font;
}
}
}