1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-29 03:23:37 +02:00

added text formatting

This commit is contained in:
Ellpeck 2019-08-24 00:07:54 +02:00
parent b6d3496987
commit e1baacdb0d
11 changed files with 316 additions and 29 deletions

View file

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

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>Arial Bold</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

@ -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>Arial Italic</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

@ -42,6 +42,8 @@ namespace Demos {
// when using a SpriteFont, use GenericSpriteFont. When using a MonoGame.Extended BitmapFont, use GenericBitmapFont.
// Wrapping fonts like this allows for both types to be usable within MLEM.Ui easily
Font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont")),
BoldFont = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFontBold")),
ItalicFont = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFontItalic")),
TextScale = 0.1F,
PanelTexture = this.testPatch,
ButtonTexture = new NinePatch(new TextureRegion(this.testTexture, 24, 8, 16, 16), 4),
@ -77,6 +79,7 @@ namespace Demos {
image.IsHidden = !image.IsHidden;
}
});
root.AddChild(new VerticalSpace(3));
root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Note that the default style does not contain any textures or font files and, as such, is quite bland. However, the default style is quite easy to override."));
root.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 10), "Change Style") {
@ -92,6 +95,10 @@ namespace Demos {
HoveredColor = Color.LightGray
});
root.AddChild(new VerticalSpace(3));
// a paragraph with formatting codes. To see them all or to add more, check the TextFormatting class
root.AddChild(new Paragraph(Anchor.AutoLeft, 1,"Paragraphs can also contain [Blue]formatting codes[White], including colors and [Italic]text styles[Regular]. The names of all [Orange]MonoGame Colors[White] can be used, as well as the codes [Italic]Italic[Regular] and [Bold]Bold[Regular]. \n[Italic]Even [CornflowerBlue]Cornflower Blue[White] works!"));
root.AddChild(new VerticalSpace(3));
root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Text input:", true));
root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10)) {

View file

@ -13,6 +13,7 @@ namespace MLEM.Ui.Elements {
public Color HoveredColor;
public TextureRegion Checkmark;
public Paragraph Label;
public float TextOffsetX = 2;
private bool checced;
public bool Checked {
@ -35,15 +36,17 @@ namespace MLEM.Ui.Elements {
};
if (label != null) {
this.Label = new Paragraph(Anchor.CenterRight, 0, label, true);
this.Label = new Paragraph(Anchor.CenterLeft, 0, label);
this.AddChild(this.Label);
}
}
protected override Point CalcActualSize(Rectangle parentArea) {
var size = base.CalcActualSize(parentArea);
if (this.Label != null)
this.Label.Size = new Vector2((size.X - size.Y) / this.Scale, 1);
if (this.Label != null) {
this.Label.Size = new Vector2((size.X - size.Y) / this.Scale - this.TextOffsetX, 1);
this.Label.PositionOffset = new Vector2(size.Y / this.Scale + this.TextOffsetX, 0);
}
return size;
}

View file

@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Textures;
using MLEM.Ui.Format;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
@ -16,8 +17,10 @@ namespace MLEM.Ui.Elements {
private float lineHeight;
private float longestLineLength;
private string[] splitText;
private IGenericFont font;
private readonly bool centerText;
private Dictionary<int, FormattingCode> codeLocations;
private IGenericFont regularFont;
private IGenericFont boldFont;
private IGenericFont italicFont;
public NinePatch Background;
public Color BackgroundColor;
@ -36,27 +39,28 @@ namespace MLEM.Ui.Elements {
public TextCallback GetTextCallback;
public float LineSpace = 1;
public Paragraph(Anchor anchor, float width, TextCallback textCallback, bool centerText = false, IGenericFont font = null)
: this(anchor, width, "", centerText, font) {
public Paragraph(Anchor anchor, float width, TextCallback textCallback, bool centerText = false)
: this(anchor, width, "", centerText) {
this.GetTextCallback = textCallback;
this.Text = textCallback(this);
}
public Paragraph(Anchor anchor, float width, string text, bool centerText = false, IGenericFont font = null) : base(anchor, new Vector2(width, 0)) {
public Paragraph(Anchor anchor, float width, string text, bool centerText = false) : base(anchor, new Vector2(width, 0)) {
this.text = text;
this.font = font;
this.centerText = centerText;
this.AutoAdjustWidth = centerText;
this.IgnoresMouse = true;
}
protected override Point CalcActualSize(Rectangle parentArea) {
var size = base.CalcActualSize(parentArea);
this.splitText = this.font.SplitString(this.text, size.X - this.ScaledPadding.X * 2, this.TextScale * this.Scale).ToArray();
var sc = this.TextScale * this.Scale;
this.splitText = this.regularFont.SplitString(this.text.RemoveFormatting(), size.X - this.ScaledPadding.X * 2, sc).ToArray();
this.codeLocations = this.text.GetFormattingCodes();
this.lineHeight = 0;
this.longestLineLength = 0;
foreach (var strg in this.splitText) {
var strgScale = this.font.MeasureString(strg) * this.TextScale * this.Scale;
var strgScale = this.regularFont.MeasureString(strg) * sc;
if (strgScale.Y + 1 > this.lineHeight)
this.lineHeight = strgScale.Y + 1;
if (strgScale.X > this.longestLineLength)
@ -78,13 +82,56 @@ namespace MLEM.Ui.Elements {
var pos = this.DisplayArea.Location.ToVector2();
var off = offset.ToVector2();
foreach (var line in this.splitText) {
if (this.centerText) {
this.font.DrawCenteredString(batch, line, pos + off + new Vector2(this.DisplayArea.Width / 2, 0), this.TextScale * this.Scale, this.TextColor * alpha);
} else {
this.font.DrawString(batch, line, pos + off, this.TextColor * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
var sc = this.TextScale * this.Scale;
// if we don't have any formatting codes, then we don't need to do complex drawing
if (this.codeLocations.Count <= 0) {
foreach (var line in this.splitText) {
this.regularFont.DrawString(batch, line, pos + off, this.TextColor * alpha, 0, Vector2.Zero, sc, SpriteEffects.None, 0);
off.Y += this.lineHeight;
}
} else {
// if we have formatting codes, we need to go through each index and see how it should be drawn
var characterCounter = 0;
var currColor = this.TextColor;
var currFont = this.regularFont;
foreach (var line in this.splitText) {
var lineOffset = new Vector2();
foreach (var c in line) {
// check if the current character's index has a formatting code
this.codeLocations.TryGetValue(characterCounter, out var code);
if (code != null) {
// if so, apply it
if (code.IsColorCode) {
currColor = code.Color;
} else {
switch (code.Style) {
case TextStyle.Regular:
currFont = this.regularFont;
break;
case TextStyle.Bold:
currFont = this.boldFont;
break;
case TextStyle.Italic:
currFont = this.italicFont;
break;
}
}
}
var cSt = c.ToString();
currFont.DrawString(batch, cSt, pos + off + lineOffset, currColor * alpha, 0, Vector2.Zero, sc, SpriteEffects.None, 0);
// get the width based on the regular font so that the split text doesn't overshoot the borders
// this is a bit of a hack, but bold fonts shouldn't be that much thicker so it won't look bad
lineOffset.X += this.regularFont.MeasureString(cSt).X * sc;
characterCounter++;
}
// spaces are replaced by newline characters, account for that
characterCounter++;
off.Y += this.lineHeight;
}
off.Y += this.lineHeight;
}
base.Draw(time, batch, alpha, offset);
}
@ -92,7 +139,9 @@ namespace MLEM.Ui.Elements {
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.TextScale = style.TextScale;
this.font = style.Font;
this.regularFont = style.Font;
this.boldFont = style.BoldFont ?? style.Font;
this.italicFont = style.ItalicFont ?? style.Font;
}
public delegate string TextCallback(Paragraph paragraph);

View file

@ -9,8 +9,8 @@ namespace MLEM.Ui.Elements {
public Vector2 MouseOffset = new Vector2(2, 3);
public Tooltip(float width, string text, IGenericFont font = null) :
base(Anchor.TopLeft, width, text, false, font) {
public Tooltip(float width, string text) :
base(Anchor.TopLeft, width, text) {
this.AutoAdjustWidth = true;
this.Padding = new Point(2);
}

View file

@ -0,0 +1,29 @@
using Microsoft.Xna.Framework;
namespace MLEM.Ui.Format {
public class FormattingCode {
public readonly Color Color;
public readonly TextStyle Style;
public readonly bool IsColorCode;
public FormattingCode(Color color) {
this.Color = color;
this.IsColorCode = true;
}
public FormattingCode(TextStyle style) {
this.Style = style;
this.IsColorCode = false;
}
}
public enum TextStyle {
Regular,
Bold,
Italic
}
}

View file

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
namespace MLEM.Ui.Format {
public static class TextFormatting {
private static Regex formatRegex;
public static readonly Dictionary<string, FormattingCode> FormattingCodes = new Dictionary<string, FormattingCode>();
static TextFormatting() {
SetFormatIndicators('[', ']');
FormattingCodes["regular"] = new FormattingCode(TextStyle.Regular);
FormattingCodes["italic"] = new FormattingCode(TextStyle.Italic);
FormattingCodes["bold"] = new FormattingCode(TextStyle.Bold);
var colors = typeof(Color).GetProperties();
foreach (var color in colors) {
if (color.GetGetMethod().IsStatic)
FormattingCodes[color.Name.ToLowerInvariant()] = new FormattingCode((Color) color.GetValue(null));
}
}
public static void SetFormatIndicators(char opener, char closer) {
// escape the opener and closer so that any character can be used
var op = "\\" + opener;
var cl = "\\" + closer;
// find any text that is surrounded by the opener and closer
formatRegex = new Regex($"{op}[^{op}{cl}]*{cl}");
}
public static string RemoveFormatting(this string s) {
return formatRegex.Replace(s, string.Empty);
}
public static Dictionary<int, FormattingCode> GetFormattingCodes(this string s, bool indicesIgnoreCode = true) {
var codes = new Dictionary<int, FormattingCode>();
var codeLengths = 0;
foreach (Match match in formatRegex.Matches(s)) {
var rawCode = match.Value.Substring(1, match.Value.Length - 2).ToLowerInvariant();
codes[match.Index - codeLengths] = FormattingCodes[rawCode];
// if indices of formatting codes should ignore the codes themselves, then the lengths of all
// of the codes we have sound so far needs to be subtracted from the found code's index
if (indicesIgnoreCode)
codeLengths += match.Length;
}
return codes;
}
}
}

View file

@ -25,7 +25,8 @@ namespace MLEM.Ui.Style {
public NinePatch TooltipBackground;
public Color TooltipBackgroundColor;
public IGenericFont Font;
public IGenericFont BoldFont;
public IGenericFont ItalicFont;
public float TextScale = 1;
}
}

View file

@ -7,15 +7,18 @@ namespace MLEM.Extensions {
public static IEnumerable<string> SplitString(this SpriteFont font, string text, float width, float scale) {
var builder = new StringBuilder();
foreach (var word in text.Split(' ')) {
builder.Append(word).Append(' ');
if (font.MeasureString(builder).X * scale >= width) {
var len = builder.Length - word.Length - 1;
yield return builder.ToString(0, len - 1);
builder.Remove(0, len);
foreach (var line in text.Split('\n')) {
foreach (var word in line.Split(' ')) {
builder.Append(word).Append(' ');
if (font.MeasureString(builder).X * scale >= width) {
var len = builder.Length - word.Length - 1;
yield return builder.ToString(0, len - 1);
builder.Remove(0, len);
}
}
yield return builder.ToString(0, builder.Length - 1);
builder.Clear();
}
yield return builder.ToString(0, builder.Length - 1);
}
}