From e1baacdb0dd437def656bc794ee4755e44be75b1 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Sat, 24 Aug 2019 00:07:54 +0200 Subject: [PATCH] added text formatting --- Demos/Content/Content.mgcb | 21 +++++ Demos/Content/Fonts/TestFontBold.spritefont | 60 ++++++++++++++ Demos/Content/Fonts/TestFontItalic.spritefont | 60 ++++++++++++++ Demos/UiDemo.cs | 7 ++ MLEM.Ui/Elements/Checkbox.cs | 9 ++- MLEM.Ui/Elements/Paragraph.cs | 81 +++++++++++++++---- MLEM.Ui/Elements/Tooltip.cs | 4 +- MLEM.Ui/Format/FormattingCode.cs | 29 +++++++ MLEM.Ui/Format/TextFormatting.cs | 54 +++++++++++++ MLEM.Ui/Style/UiStyle.cs | 3 +- MLEM/Extensions/SpriteFontExtensions.cs | 17 ++-- 11 files changed, 316 insertions(+), 29 deletions(-) create mode 100644 Demos/Content/Fonts/TestFontBold.spritefont create mode 100644 Demos/Content/Fonts/TestFontItalic.spritefont create mode 100644 MLEM.Ui/Format/FormattingCode.cs create mode 100644 MLEM.Ui/Format/TextFormatting.cs diff --git a/Demos/Content/Content.mgcb b/Demos/Content/Content.mgcb index ba5c6f7..80f2e3d 100644 --- a/Demos/Content/Content.mgcb +++ b/Demos/Content/Content.mgcb @@ -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 diff --git a/Demos/Content/Fonts/TestFontBold.spritefont b/Demos/Content/Fonts/TestFontBold.spritefont new file mode 100644 index 0000000..60cca08 --- /dev/null +++ b/Demos/Content/Fonts/TestFontBold.spritefont @@ -0,0 +1,60 @@ + + + + + + + Arial Bold + + + 32 + + + 0 + + + true + + + + + + * + + + + + + ɏ + + + + diff --git a/Demos/Content/Fonts/TestFontItalic.spritefont b/Demos/Content/Fonts/TestFontItalic.spritefont new file mode 100644 index 0000000..7a87b09 --- /dev/null +++ b/Demos/Content/Fonts/TestFontItalic.spritefont @@ -0,0 +1,60 @@ + + + + + + + Arial Italic + + + 32 + + + 0 + + + true + + + + + + * + + + + + + ɏ + + + + diff --git a/Demos/UiDemo.cs b/Demos/UiDemo.cs index f5fe287..53ad0e1 100644 --- a/Demos/UiDemo.cs +++ b/Demos/UiDemo.cs @@ -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("Fonts/TestFont")), + BoldFont = new GenericSpriteFont(LoadContent("Fonts/TestFontBold")), + ItalicFont = new GenericSpriteFont(LoadContent("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)) { diff --git a/MLEM.Ui/Elements/Checkbox.cs b/MLEM.Ui/Elements/Checkbox.cs index 506f66f..b33a7df 100644 --- a/MLEM.Ui/Elements/Checkbox.cs +++ b/MLEM.Ui/Elements/Checkbox.cs @@ -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; } diff --git a/MLEM.Ui/Elements/Paragraph.cs b/MLEM.Ui/Elements/Paragraph.cs index c855120..3a9d0ff 100644 --- a/MLEM.Ui/Elements/Paragraph.cs +++ b/MLEM.Ui/Elements/Paragraph.cs @@ -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 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); diff --git a/MLEM.Ui/Elements/Tooltip.cs b/MLEM.Ui/Elements/Tooltip.cs index 2918669..141272c 100644 --- a/MLEM.Ui/Elements/Tooltip.cs +++ b/MLEM.Ui/Elements/Tooltip.cs @@ -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); } diff --git a/MLEM.Ui/Format/FormattingCode.cs b/MLEM.Ui/Format/FormattingCode.cs new file mode 100644 index 0000000..840b957 --- /dev/null +++ b/MLEM.Ui/Format/FormattingCode.cs @@ -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 + + } +} \ No newline at end of file diff --git a/MLEM.Ui/Format/TextFormatting.cs b/MLEM.Ui/Format/TextFormatting.cs new file mode 100644 index 0000000..57ad55c --- /dev/null +++ b/MLEM.Ui/Format/TextFormatting.cs @@ -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 FormattingCodes = new Dictionary(); + + 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 GetFormattingCodes(this string s, bool indicesIgnoreCode = true) { + var codes = new Dictionary(); + 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; + } + + } +} \ No newline at end of file diff --git a/MLEM.Ui/Style/UiStyle.cs b/MLEM.Ui/Style/UiStyle.cs index 0c0cf7f..1ea536c 100644 --- a/MLEM.Ui/Style/UiStyle.cs +++ b/MLEM.Ui/Style/UiStyle.cs @@ -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; - } } \ No newline at end of file diff --git a/MLEM/Extensions/SpriteFontExtensions.cs b/MLEM/Extensions/SpriteFontExtensions.cs index fb4f0ef..e6be953 100644 --- a/MLEM/Extensions/SpriteFontExtensions.cs +++ b/MLEM/Extensions/SpriteFontExtensions.cs @@ -7,15 +7,18 @@ namespace MLEM.Extensions { public static IEnumerable 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); } }