1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-24 21:48:35 +01:00

Compare commits

...

4 commits

12 changed files with 202 additions and 84 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;
@ -15,17 +16,17 @@ namespace MLEM.Font {
/// <summary>
/// This field holds the unicode representation of a one em space.
/// This is a character that isn't drawn, but has the same width as <see cref="LineHeight"/>.
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations, generic fonts implicitly support it in <see cref="MeasureString"/>.
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/>.
/// </summary>
public const char OneEmSpace = '\u2003';
/// <summary>
/// This field holds the unicode representation of a non-breaking space.
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations, generic fonts implicitly support it in <see cref="MeasureString"/>.
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/>.
/// </summary>
public const char Nbsp = '\u00A0';
/// <summary>
/// This field holds the unicode representation of a zero-width space.
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations and string splitting, generic fonts implicitly support it in <see cref="MeasureString"/> and <see cref="SplitString"/>.
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations and string splitting, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/> and <see cref="SplitString"/>.
/// </summary>
public const char Zwsp = '\u200B';
@ -46,8 +47,8 @@ namespace MLEM.Font {
public abstract float LineHeight { get; }
/// <summary>
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString"/>.
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="OneEmSpace"/> for most generic fonts, which is why <see cref="MeasureString"/> should be used even for single characters.
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString(string,bool)"/>.
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="OneEmSpace"/> for most generic fonts, which is why <see cref="MeasureString(string,bool)"/> should be used even for single characters.
/// </summary>
/// <param name="c">The character whose width to calculate</param>
/// <returns>The width of the given character with the default scale</returns>
@ -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 this.MeasureString(text, ignoreTrailingSpaces, null);
}
/// <summary>
@ -139,30 +103,13 @@ 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 this.TruncateString(text, width, scale, fromBack, ellipsis, null);
}
/// <summary>
/// Splits a string to a given maximum width, adding newline characters between each line.
/// Also splits long words and supports zero-width spaces and takes into account existing newline characters in the passed <paramref name="text"/>.
/// See <see cref="SplitStringSeparate"/> for a method that differentiates between existing newline characters and splits due to maximum width.
/// See <see cref="SplitStringSeparate(string,float,float)"/> for a method that differentiates between existing newline characters and splits due to maximum width.
/// </summary>
/// <param name="text">The text to split into multiple lines</param>
/// <param name="width">The maximum width that each line should have</param>
@ -182,6 +129,73 @@ 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(text, width, scale, null);
}
internal Vector2 MeasureString(string text, bool ignoreTrailingSpaces, Func<int, GenericFont> fontFunction) {
var size = Vector2.Zero;
if (text.Length <= 0)
return size;
var xOffset = 0F;
for (var i = 0; i < text.Length; i++) {
var font = fontFunction?.Invoke(i) ?? this;
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?.Invoke(text.Length - 1) ?? this).LineHeight;
return size;
}
internal string TruncateString(string text, float width, float scale, bool fromBack, string ellipsis, Func<int, GenericFont> fontFunction) {
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]);
}
var font = fontFunction?.Invoke(i) ?? this;
if (font.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 IEnumerable<string> SplitStringSeparate(string text, float width, float scale, Func<int, GenericFont> fontFunction) {
var currWidth = 0F;
var lastSpaceIndex = -1;
var widthSinceLastSpace = 0F;
@ -195,7 +209,8 @@ namespace MLEM.Font {
widthSinceLastSpace = 0;
currWidth = 0;
} else {
var cWidth = this.MeasureString(c.ToCachedString()).X * scale;
var font = fontFunction?.Invoke(i) ?? this;
var cWidth = font.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

@ -60,8 +60,13 @@ namespace MLEM.Formatting {
/// </summary>
/// <param name="defaultPick">The default color, if none is specified</param>
/// <returns>The color to render with</returns>
public Color? GetColor(Color defaultPick) {
return this.AppliedCodes.Select(c => c.GetColor(defaultPick)).FirstOrDefault(c => c.HasValue);
public Color GetColor(Color defaultPick) {
foreach (var code in this.AppliedCodes) {
var color = code.GetColor(defaultPick);
if (color.HasValue)
return color.Value;
}
return defaultPick;
}
/// <summary>
@ -70,7 +75,12 @@ namespace MLEM.Formatting {
/// <param name="defaultPick">The default font, if none is specified</param>
/// <returns>The font to render with</returns>
public GenericFont GetFont(GenericFont defaultPick) {
return this.AppliedCodes.Select(c => c.GetFont(defaultPick)).FirstOrDefault(f => f != null);
foreach (var code in this.AppliedCodes) {
var font = code.GetFont(defaultPick);
if (font != null)
return font;
}
return defaultPick;
}
/// <summary>

View file

@ -59,14 +59,14 @@ 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", font.SplitStringSeparate(this.String, width, scale, i => this.GetFontForIndex(font, i)));
this.StoreModifiedSubstrings(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"/>
/// <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>
@ -74,13 +74,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 = font.TruncateString(this.String, width, scale, false, ellipsis, i => this.GetFontForIndex(font, i));
this.StoreModifiedSubstrings(font, alignment);
}
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
public Vector2 Measure(GenericFont font) {
return font.MeasureString(this.DisplayString);
return font.MeasureString(this.DisplayString, false, i => this.GetFontForIndex(font, i));
}
/// <summary>
@ -115,18 +115,18 @@ namespace MLEM.Formatting {
var innerOffset = new Vector2(this.initialInnerOffset * scale, 0);
for (var t = 0; t < this.Tokens.Length; t++) {
var token = this.Tokens[t];
var drawFont = token.GetFont(font) ?? font;
var drawColor = token.GetColor(color) ?? color;
var drawFont = token.GetFont(font);
var drawColor = token.GetColor(color);
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
var line = token.SplitDisplayString[l];
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 +183,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);
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 +203,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);
// 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);
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 +233,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);
}
return font;
}
}
}