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

View file

@ -34,6 +34,13 @@
/processorParam:TextureFormat=Compressed /processorParam:TextureFormat=Compressed
/build:Fonts/TestFontItalic.spritefont /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 #begin Textures/Anim.png
/importer:TextureImporter /importer:TextureImporter
/processor:TextureProcessor /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), CheckboxTexture = new NinePatch(new TextureRegion(this.testTexture, 24, 8, 16, 16), 4),
CheckboxCheckmark = new TextureRegion(this.testTexture, 24, 0, 8, 8), CheckboxCheckmark = new TextureRegion(this.testTexture, 24, 0, 8, 8),
RadioTexture = new NinePatch(new TextureRegion(this.testTexture, 16, 0, 8, 8), 3), 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) { var untexturedStyle = new UntexturedStyle(this.SpriteBatch) {
TextScale = style.TextScale, TextScale = style.TextScale,
@ -87,7 +88,7 @@ namespace Demos {
this.root.AddChild(new VerticalSpace(3)); this.root.AddChild(new VerticalSpace(3));
// a paragraph with formatting codes. To see them all or to add more, check the TextFormatting class // 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 // 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!")); 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). 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 ## 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. 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>`. - 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>`. - 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 ## 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: 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 ```cs

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MLEM.Font; using MLEM.Font;
using MLEM.Formatting; using MLEM.Formatting;
@ -194,6 +195,10 @@ namespace MLEM.Ui.Style {
/// Note that this sound is only played if the callbacks have any subscribers. /// Note that this sound is only played if the callbacks have any subscribers.
/// </summary> /// </summary>
public SoundEffectInfo ActionSound; 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 = new TextFormatter();
this.TextFormatter.Codes.Add(new Regex("<l(?: ([^>]+))?>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F, 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)); 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> /// <summary>

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
@ -15,17 +16,17 @@ namespace MLEM.Font {
/// <summary> /// <summary>
/// This field holds the unicode representation of a one em space. /// 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"/>. /// 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> /// </summary>
public const char OneEmSpace = '\u2003'; public const char OneEmSpace = '\u2003';
/// <summary> /// <summary>
/// This field holds the unicode representation of a non-breaking space. /// 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> /// </summary>
public const char Nbsp = '\u00A0'; public const char Nbsp = '\u00A0';
/// <summary> /// <summary>
/// This field holds the unicode representation of a zero-width space. /// 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> /// </summary>
public const char Zwsp = '\u200B'; public const char Zwsp = '\u200B';
@ -46,8 +47,8 @@ namespace MLEM.Font {
public abstract float LineHeight { get; } public abstract float LineHeight { get; }
/// <summary> /// <summary>
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString"/>. /// 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"/> should be used even for single characters. /// 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> /// </summary>
/// <param name="c">The character whose width to calculate</param> /// <param name="c">The character whose width to calculate</param>
/// <returns>The width of the given character with the default scale</returns> /// <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> /// <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> /// <returns>The size of the string when drawn with this font</returns>
public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) { public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) {
var size = Vector2.Zero; return this.MeasureString(text, ignoreTrailingSpaces, null);
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;
} }
/// <summary> /// <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> /// <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> /// <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 = "") { public string TruncateString(string text, float width, float scale, bool fromBack = false, string ellipsis = "") {
var total = new StringBuilder(); return this.TruncateString(text, width, scale, fromBack, ellipsis, null);
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();
} }
/// <summary> /// <summary>
/// Splits a string to a given maximum width, adding newline characters between each line. /// 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"/>. /// 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> /// </summary>
/// <param name="text">The text to split into multiple lines</param> /// <param name="text">The text to split into multiple lines</param>
/// <param name="width">The maximum width that each line should have</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> /// <param name="scale">The scale to use for width measurements</param>
/// <returns>The split string as an enumerable of split sections</returns> /// <returns>The split string as an enumerable of split sections</returns>
public IEnumerable<string> SplitStringSeparate(string text, float width, float scale) { 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 currWidth = 0F;
var lastSpaceIndex = -1; var lastSpaceIndex = -1;
var widthSinceLastSpace = 0F; var widthSinceLastSpace = 0F;
@ -195,7 +209,8 @@ namespace MLEM.Font {
widthSinceLastSpace = 0; widthSinceLastSpace = 0;
currWidth = 0; currWidth = 0;
} else { } 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) { if (c == ' ' || c == OneEmSpace || c == Zwsp) {
// remember the location of this (breaking!) space // remember the location of this (breaking!) space
lastSpaceIndex = curr.Length; 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("<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("<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("</(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 // color codes
foreach (var c in typeof(Color).GetProperties()) { foreach (var c in typeof(Color).GetProperties()) {

View file

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

View file

@ -59,14 +59,14 @@ namespace MLEM.Formatting {
/// <param name="alignment">The text alignment that should be used for width calculations</param> /// <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) { 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 // 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); this.StoreModifiedSubstrings(font, alignment);
} }
/// <summary> /// <summary>
/// Truncates this tokenized string, removing any additional characters that exceed the length from the displayed string. /// 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"/>. /// 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> /// </summary>
/// <param name="font">The font to use for width calculations</param> /// <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> /// <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="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> /// <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) { 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); this.StoreModifiedSubstrings(font, alignment);
} }
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/> /// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
public Vector2 Measure(GenericFont font) { public Vector2 Measure(GenericFont font) {
return font.MeasureString(this.DisplayString); return font.MeasureString(this.DisplayString, false, i => this.GetFontForIndex(font, i));
} }
/// <summary> /// <summary>
@ -115,18 +115,18 @@ namespace MLEM.Formatting {
var innerOffset = new Vector2(this.initialInnerOffset * scale, 0); var innerOffset = new Vector2(this.initialInnerOffset * scale, 0);
for (var t = 0; t < this.Tokens.Length; t++) { for (var t = 0; t < this.Tokens.Length; t++) {
var token = this.Tokens[t]; var token = this.Tokens[t];
var drawFont = token.GetFont(font) ?? font; var drawFont = token.GetFont(font);
var drawColor = token.GetColor(color) ?? color; var drawColor = token.GetColor(color);
for (var l = 0; l < token.SplitDisplayString.Length; l++) { for (var l = 0; l < token.SplitDisplayString.Length; l++) {
var line = token.SplitDisplayString[l]; var line = token.SplitDisplayString[l];
for (var i = 0; i < line.Length; i++) { for (var i = 0; i < line.Length; i++) {
var c = line[i]; var c = line[i];
if (l == 0 && i == 0) 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(); var cString = c.ToCachedString();
token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth); 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! // only split at a new line, not between tokens!
if (l < token.SplitDisplayString.Length - 1) { if (l < token.SplitDisplayString.Length - 1) {
@ -183,17 +183,18 @@ namespace MLEM.Formatting {
var innerOffset = new Vector2(this.initialInnerOffset, 0); var innerOffset = new Vector2(this.initialInnerOffset, 0);
for (var t = 0; t < this.Tokens.Length; t++) { for (var t = 0; t < this.Tokens.Length; t++) {
var token = this.Tokens[t]; var token = this.Tokens[t];
var tokenFont = token.GetFont(font);
token.InnerOffsets = new float[token.SplitDisplayString.Length - 1]; token.InnerOffsets = new float[token.SplitDisplayString.Length - 1];
var area = new List<RectangleF>(); var area = new List<RectangleF>();
for (var l = 0; l < token.SplitDisplayString.Length; l++) { 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); var rect = new RectangleF(innerOffset, size);
if (!rect.IsEmpty) if (!rect.IsEmpty)
area.Add(rect); area.Add(rect);
if (l < token.SplitDisplayString.Length - 1) { if (l < token.SplitDisplayString.Length - 1) {
innerOffset.X = token.InnerOffsets[l] = this.GetInnerOffsetX(font, t, l + 1, alignment); innerOffset.X = token.InnerOffsets[l] = this.GetInnerOffsetX(font, t, l + 1, alignment);
innerOffset.Y += font.LineHeight; innerOffset.Y += tokenFont.LineHeight;
} else { } else {
innerOffset.X += size.X; 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) { if (alignment > TextAlignment.Left) {
var token = this.Tokens[tokenIndex]; 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 // 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; var endsLater = lineIndex >= token.SplitDisplayString.Length - 1;
// if the line ends in our token, we should ignore trailing white space // 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) { if (endsLater) {
for (var i = tokenIndex + 1; i < this.Tokens.Length; i++) { for (var i = tokenIndex + 1; i < this.Tokens.Length; i++) {
var other = this.Tokens[i]; var other = this.Tokens[i];
var otherFont = other.GetFont(defaultFont);
if (other.SplitDisplayString.Length > 1) { if (other.SplitDisplayString.Length > 1) {
// the line ends in this token (so we also ignore trailing whitespaces) // 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; break;
} else { } else {
// the line doesn't end in this token (or it's the last token), so add it fully // 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; 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; 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;
}
} }
} }