diff --git a/Demos/UiDemo.cs b/Demos/UiDemo.cs index 3fe2687..299cdbc 100644 --- a/Demos/UiDemo.cs +++ b/Demos/UiDemo.cs @@ -8,6 +8,7 @@ using MLEM.Animations; using MLEM.Extensions; using MLEM.Font; using MLEM.Formatting; +using MLEM.Formatting.Codes; using MLEM.Input; using MLEM.Misc; using MLEM.Startup; @@ -85,21 +86,12 @@ 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 [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], [Bold]Bold[Regular], [Shadow]Drop Shadow'd[Regular] and [Shadow][Pink]mixed formatting[Regular][White]. \n[Italic]Even [CornflowerBlue]Cornflower Blue[White] works!")); + this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Paragraphs can also contain formatting codes, including colors and text styles. The names of all MonoGame Colors can be used, as well as the codes Italic, Bold, Drop Shadow'd and mixed formatting. \nEven inline custom colors work!")); // adding some custom image formatting codes - TextFormatting.FormattingCodes["Grass"] = new FormattingCode(image.Texture); - // formatting codes can also be sprite animations! - var atlas = new UniformTextureAtlas(LoadContent("Textures/Anim"), 4, 4); - TextFormatting.FormattingCodes["walk"] = new FormattingCode(new SpriteAnimation(0.2F, atlas[0, 0], atlas[0, 1], atlas[0, 2], atlas[0, 3])); - - this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Additionally, you can create custom formatting codes that contain [Grass] images or [Walk] sprite animations!")); - - var animatedPar = this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Defining text animations as formatting codes is also possible, including [Wobbly]wobbly text[Unanimated] as well as a [Typing]dialogue-esque typing effect by default. Of course, more animations can be added though.")); - this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 10), "Reset Typing Animation") { - // to reset any animation, simply change the paragraph's TimeIntoAnimation - OnPressed = e => animatedPar.TimeIntoAnimation = TimeSpan.Zero - }); + var p = this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Additionally, you can create custom formatting codes that contain images and more!")); + p.Formatter.AddImage("Grass", image.Texture); + this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Defining text animations as formatting codes is also possible, including wobbly text at different intensities. Of course, more animations can be added though.")); this.root.AddChild(new VerticalSpace(3)); this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Text input:", true)); diff --git a/MLEM.Ui/Elements/Paragraph.cs b/MLEM.Ui/Elements/Paragraph.cs index 73effca..92736f0 100644 --- a/MLEM.Ui/Elements/Paragraph.cs +++ b/MLEM.Ui/Elements/Paragraph.cs @@ -16,11 +16,15 @@ namespace MLEM.Ui.Elements { private string text; private string splitText; + [Obsolete("Use the new text formatting system in MLEM.Formatting instead")] public FormattingCodeCollection Formatting; public StyleProp RegularFont; public StyleProp BoldFont; public StyleProp ItalicFont; + [Obsolete("Use the new text formatting system in MLEM.Formatting instead")] public StyleProp FormatSettings; + public readonly TextFormatter Formatter; + public TokenizedString TokenizedText { get; private set; } public StyleProp TextColor; public StyleProp TextScale; @@ -36,6 +40,7 @@ namespace MLEM.Ui.Elements { } public bool AutoAdjustWidth; public TextCallback GetTextCallback; + [Obsolete("Use the new text formatting system in MLEM.Formatting instead")] public TextModifier RenderedTextModifier = text => text; public TimeSpan TimeIntoAnimation; @@ -54,17 +59,25 @@ namespace MLEM.Ui.Elements { this.AutoAdjustWidth = centerText; this.CanBeSelected = false; this.CanBeMoused = false; + this.Formatter = new TextFormatter(() => this.BoldFont, () => this.ItalicFont); } protected override Vector2 CalcActualSize(RectangleF parentArea) { var size = base.CalcActualSize(parentArea); - var sc = this.TextScale * this.Scale; + + // old formatting stuff this.splitText = this.RegularFont.Value.SplitString(this.text.RemoveFormatting(this.RegularFont.Value), size.X - this.ScaledPadding.Width, sc); this.Formatting = this.text.GetFormattingCodes(this.RegularFont.Value); + if (this.Formatting.Count > 0) { + var textDims = this.RegularFont.Value.MeasureString(this.splitText) * sc; + return new Vector2(this.AutoAdjustWidth ? textDims.X + this.ScaledPadding.Width : size.X, textDims.Y + this.ScaledPadding.Height); + } - var textDims = this.RegularFont.Value.MeasureString(this.splitText) * sc; - return new Vector2(this.AutoAdjustWidth ? textDims.X + this.ScaledPadding.Width : size.X, textDims.Y + this.ScaledPadding.Height); + this.TokenizedText = this.Formatter.Tokenize(this.RegularFont, this.text); + this.TokenizedText.Split(this.RegularFont, size.X - this.ScaledPadding.Width, sc); + var dims = this.TokenizedText.Measure(this.RegularFont) * sc; + return new Vector2(this.AutoAdjustWidth ? dims.X + this.ScaledPadding.Width : size.X, dims.Y + this.ScaledPadding.Height); } public override void ForceUpdateArea() { @@ -78,20 +91,20 @@ namespace MLEM.Ui.Elements { if (this.GetTextCallback != null) this.Text = this.GetTextCallback(this); this.TimeIntoAnimation += time.ElapsedGameTime; + this.TokenizedText?.Update(time); } public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) { var pos = this.DisplayArea.Location; var sc = this.TextScale * this.Scale; - var toRender = this.RenderedTextModifier(this.splitText); var color = this.TextColor.OrDefault(Color.White) * alpha; - // if we don't have any formatting codes, then we don't need to do complex drawing - if (this.Formatting.Count <= 0) { - this.RegularFont.Value.DrawString(batch, toRender, pos, color, 0, Vector2.Zero, sc, SpriteEffects.None, 0); - } else { - // if we have formatting codes, we should do it + // legacy formatting stuff + if (this.Formatting.Count > 0) { + var toRender = this.RenderedTextModifier(this.splitText); this.RegularFont.Value.DrawFormattedString(batch, pos, toRender, this.Formatting, color, sc, this.BoldFont.Value, this.ItalicFont.Value, 0, this.TimeIntoAnimation, this.FormatSettings); + } else { + this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0); } base.Draw(time, batch, alpha, blendState, samplerState, matrix); } @@ -107,6 +120,7 @@ namespace MLEM.Ui.Elements { public delegate string TextCallback(Paragraph paragraph); + [Obsolete("Use the new text formatting system in MLEM.Formatting instead")] public delegate string TextModifier(string text); } diff --git a/MLEM.Ui/Style/UiStyle.cs b/MLEM.Ui/Style/UiStyle.cs index cad992c..7f6aba4 100644 --- a/MLEM.Ui/Style/UiStyle.cs +++ b/MLEM.Ui/Style/UiStyle.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using MLEM.Font; @@ -38,6 +39,7 @@ namespace MLEM.Ui.Style { public GenericFont Font; public GenericFont BoldFont; public GenericFont ItalicFont; + [Obsolete("Use the new text formatting system in MLEM.Formatting instead")] public FormatSettings FormatSettings; public float TextScale = 1; public SoundEffect ActionSound; diff --git a/MLEM/Formatting/Codes/Code.cs b/MLEM/Formatting/Codes/Code.cs index 549666a..4cf2db1 100644 --- a/MLEM/Formatting/Codes/Code.cs +++ b/MLEM/Formatting/Codes/Code.cs @@ -7,11 +7,13 @@ using MLEM.Misc; namespace MLEM.Formatting.Codes { public class Code : GenericDataHolder { + public readonly Regex Regex; public readonly Match Match; public Token Token { get; internal set; } - public Code(Match match) { + protected Code(Match match, Regex regex) { this.Match = match; + this.Regex = regex; } public virtual bool EndsHere(Code other) { @@ -29,11 +31,18 @@ namespace MLEM.Formatting.Codes { public virtual void Update(GameTime time) { } + public virtual string GetReplacementString(GenericFont font) { + return string.Empty; + } + public virtual bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) { return false; } - public delegate Code Constructor(TextFormatter formatter, Match match); + public virtual void DrawSelf(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) { + } + + public delegate Code Constructor(TextFormatter formatter, Match match, Regex regex); } } \ No newline at end of file diff --git a/MLEM/Formatting/Codes/ColorCode.cs b/MLEM/Formatting/Codes/ColorCode.cs index 6cb6796..216e6b4 100644 --- a/MLEM/Formatting/Codes/ColorCode.cs +++ b/MLEM/Formatting/Codes/ColorCode.cs @@ -6,7 +6,7 @@ namespace MLEM.Formatting.Codes { private readonly Color? color; - public ColorCode(Match match, Color? color) : base(match) { + public ColorCode(Match match, Regex regex, Color? color) : base(match, regex) { this.color = color; } diff --git a/MLEM/Formatting/Codes/FontCode.cs b/MLEM/Formatting/Codes/FontCode.cs index 3228763..163b1d9 100644 --- a/MLEM/Formatting/Codes/FontCode.cs +++ b/MLEM/Formatting/Codes/FontCode.cs @@ -6,7 +6,7 @@ namespace MLEM.Formatting.Codes { private readonly GenericFont font; - public FontCode(Match match, GenericFont font) : base(match) { + public FontCode(Match match, Regex regex, GenericFont font) : base(match, regex) { this.font = font; } diff --git a/MLEM/Formatting/Codes/ImageCode.cs b/MLEM/Formatting/Codes/ImageCode.cs new file mode 100644 index 0000000..5f1cb69 --- /dev/null +++ b/MLEM/Formatting/Codes/ImageCode.cs @@ -0,0 +1,58 @@ +using System; +using System.Text.RegularExpressions; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MLEM.Animations; +using MLEM.Extensions; +using MLEM.Font; +using MLEM.Misc; +using MLEM.Textures; + +namespace MLEM.Formatting.Codes { + public class ImageCode : Code { + + private readonly SpriteAnimation image; + private string replacement; + private float gapSize; + + public ImageCode(Match match, Regex regex, SpriteAnimation image) : base(match, regex) { + this.image = image; + } + + public override bool EndsHere(Code other) { + return true; + } + + public override string GetReplacementString(GenericFont font) { + if (this.replacement == null) { + // use non-breaking space so that the image won't be line-splitted + var strg = font.GetWidthString(font.LineHeight, '\u00A0'); + this.replacement = strg.Remove(strg.Length - 1) + ' '; + this.gapSize = font.MeasureString(this.replacement).X; + } + return this.replacement; + } + + public override void Update(GameTime time) { + this.image.Update(time); + } + + public override void DrawSelf(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) { + var position = pos + new Vector2(this.gapSize - font.LineHeight, 0) / 2 * scale; + batch.Draw(this.image.CurrentRegion, new RectangleF(position, new Vector2(font.LineHeight * scale)), Color.White.CopyAlpha(color)); + } + + } + + public static class ImageCodeExtensions { + + public static void AddImage(this TextFormatter formatter, string name, TextureRegion image) { + formatter.AddImage(name, new SpriteAnimation(1, image)); + } + + public static void AddImage(this TextFormatter formatter, string name, SpriteAnimation image) { + formatter.Codes.Add(new Regex($""), (f, m, r) => new ImageCode(m, r, image)); + } + + } +} \ No newline at end of file diff --git a/MLEM/Formatting/TextFormatter.cs b/MLEM/Formatting/TextFormatter.cs index e466703..eb1343d 100644 --- a/MLEM/Formatting/TextFormatter.cs +++ b/MLEM/Formatting/TextFormatter.cs @@ -16,43 +16,43 @@ namespace MLEM.Formatting { public TextFormatter(Func boldFont = null, Func italicFont = null) { // font codes - this.Codes.Add(new Regex(""), (f, m) => new FontCode(m, boldFont?.Invoke())); - this.Codes.Add(new Regex(""), (f, m) => new FontCode(m, italicFont?.Invoke())); - this.Codes.Add(new Regex(@""), (f, m) => new ShadowCode(m, m.Groups[1].Success ? ColorExtensions.FromHex(m.Groups[1].Value) : Color.Black, new Vector2(float.TryParse(m.Groups[2].Value, out var offset) ? offset : 2))); - this.Codes.Add(new Regex(""), (f, m) => new FontCode(m, null)); + this.Codes.Add(new Regex(""), (f, m, r) => new FontCode(m, r, boldFont?.Invoke())); + this.Codes.Add(new Regex(""), (f, m, r) => new FontCode(m, r, italicFont?.Invoke())); + this.Codes.Add(new Regex(@""), (f, m, r) => new ShadowCode(m, r, m.Groups[1].Success ? ColorExtensions.FromHex(m.Groups[1].Value) : Color.Black, new Vector2(float.TryParse(m.Groups[2].Value, out var offset) ? offset : 2))); + this.Codes.Add(new Regex(""), (f, m, r) => new FontCode(m, r, null)); // color codes foreach (var c in typeof(Color).GetProperties()) { if (c.GetGetMethod().IsStatic) { var value = (Color) c.GetValue(null); - this.Codes.Add(new Regex($""), (f, m) => new ColorCode(m, value)); + this.Codes.Add(new Regex($""), (f, m, r) => new ColorCode(m, r, value)); } } - this.Codes.Add(new Regex(@""), (f, m) => new ColorCode(m, ColorExtensions.FromHex(m.Groups[1].Value))); - this.Codes.Add(new Regex(""), (f, m) => new ColorCode(m, null)); + this.Codes.Add(new Regex(@""), (f, m, r) => new ColorCode(m, r, ColorExtensions.FromHex(m.Groups[1].Value))); + this.Codes.Add(new Regex(""), (f, m, r) => new ColorCode(m, r, null)); // animation codes - this.Codes.Add(new Regex(@""), (f, m) => new WobblyCode(m, float.TryParse(m.Groups[1].Value, out var mod) ? mod : 5, float.TryParse(m.Groups[2].Value, out var heightMod) ? heightMod : 1 / 8F)); - this.Codes.Add(new Regex(""), (f, m) => new AnimatedCode(m)); + this.Codes.Add(new Regex(@""), (f, m, r) => new WobblyCode(m, r, float.TryParse(m.Groups[1].Value, out var mod) ? mod : 5, float.TryParse(m.Groups[2].Value, out var heightMod) ? heightMod : 1 / 8F)); + this.Codes.Add(new Regex(""), (f, m, r) => new AnimatedCode(m, r)); } - public TokenizedString Tokenize(string s) { + public TokenizedString Tokenize(GenericFont font, string s) { var tokens = new List(); var codes = new List(); var rawIndex = 0; while (rawIndex < s.Length) { - var index = this.StripFormatting(s.Substring(0, rawIndex)).Length; + var index = StripFormatting(font, s.Substring(0, rawIndex), tokens.SelectMany(t => t.AppliedCodes)).Length; var next = this.GetNextCode(s, rawIndex + 1); // if we've reached the end of the string if (next == null) { var sub = s.Substring(rawIndex, s.Length - rawIndex); - tokens.Add(new Token(codes.ToArray(), index, rawIndex, this.StripFormatting(sub), sub)); + tokens.Add(new Token(codes.ToArray(), index, rawIndex, StripFormatting(font, sub, codes), sub)); break; } // create a new token for the content up to the next code var ret = s.Substring(rawIndex, next.Match.Index - rawIndex); - tokens.Add(new Token(codes.ToArray(), index, rawIndex, this.StripFormatting(ret), ret)); + tokens.Add(new Token(codes.ToArray(), index, rawIndex, StripFormatting(font, ret, codes), ret)); // move to the start of the next code rawIndex = next.Match.Index; @@ -61,22 +61,22 @@ namespace MLEM.Formatting { codes.RemoveAll(c => c.EndsHere(next)); codes.Add(next); } - return new TokenizedString(s, this.StripFormatting(s), tokens.ToArray()); - } - - public string StripFormatting(string s) { - foreach (var regex in this.Codes.Keys) - s = regex.Replace(s, string.Empty); - return s; + return new TokenizedString(s, StripFormatting(font, s, tokens.SelectMany(t => t.AppliedCodes)), tokens.ToArray()); } private Code GetNextCode(string s, int index) { - var (c, m) = this.Codes - .Select(kv => (c: kv.Value, m: kv.Key.Match(s, index))) + var (c, m, r) = this.Codes + .Select(kv => (c: kv.Value, m: kv.Key.Match(s, index), r: kv.Key)) .Where(kv => kv.m.Success) .OrderBy(kv => kv.m.Index) .FirstOrDefault(); - return c?.Invoke(this, m); + return c?.Invoke(this, m, r); + } + + private static string StripFormatting(GenericFont font, string s, IEnumerable codes) { + foreach (var code in codes) + s = code.Regex.Replace(s, code.GetReplacementString(font)); + return s; } } diff --git a/MLEM/Formatting/TokenizedString.cs b/MLEM/Formatting/TokenizedString.cs index 89a28f7..9bf864d 100644 --- a/MLEM/Formatting/TokenizedString.cs +++ b/MLEM/Formatting/TokenizedString.cs @@ -11,7 +11,7 @@ namespace MLEM.Formatting { public class TokenizedString : GenericDataHolder { public readonly string RawString; - public readonly string String; + public string String { get; private set; } public readonly Token[] Tokens; public readonly Code[] AllCodes; @@ -26,18 +26,23 @@ namespace MLEM.Formatting { public void Split(GenericFont font, float width, float scale) { // a split string has the same character count as the input string // but with newline characters added - var split = font.SplitString(this.String, width, scale); + this.String = font.SplitString(this.String, width, scale); + // skip splitting logic for unformatted text + if (this.Tokens.Length == 1) { + this.Tokens[0].Substring = this.String; + return; + } foreach (var token in this.Tokens) { var index = 0; var length = 0; var ret = new StringBuilder(); // this is basically a substring function that ignores newlines for indexing - for (var i = 0; i < split.Length; i++) { + for (var i = 0; i < this.String.Length; i++) { // if we're within the bounds of the token's substring, append to the new substring if (index >= token.Index && length < token.Substring.Length) - ret.Append(split[i]); + ret.Append(this.String[i]); // if the current char is not a newline, we simulate length increase - if (split[i] != '\n') { + if (this.String[i] != '\n') { if (index >= token.Index) length++; index++; @@ -47,6 +52,10 @@ namespace MLEM.Formatting { } } + public Vector2 Measure(GenericFont font) { + return font.MeasureString(this.String); + } + public void Update(GameTime time) { foreach (var code in this.AllCodes) code.Update(time); @@ -62,8 +71,9 @@ namespace MLEM.Formatting { if (c == '\n') { innerOffset.X = 0; innerOffset.Y += font.LineHeight * scale; - continue; } + if (i == 0) + token.DrawSelf(time, batch, pos + innerOffset, font, color, scale, depth); var cString = c.ToString(); token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth); diff --git a/Sandbox/GameImpl.cs b/Sandbox/GameImpl.cs index 3274fc3..f20b858 100644 --- a/Sandbox/GameImpl.cs +++ b/Sandbox/GameImpl.cs @@ -12,6 +12,8 @@ using MLEM.Extended.Tiled; using MLEM.Extensions; using MLEM.Font; using MLEM.Formatting; +using MLEM.Formatting.Codes; +using MLEM.Input; using MLEM.Misc; using MLEM.Startup; using MLEM.Textures; @@ -113,16 +115,26 @@ namespace Sandbox { };*/ var formatter = new TextFormatter(); - var strg = "This is a formatted string with two bits of formatting! It also includesaverylongwordthatisformattedaswell. Additionally, it wobbles and has a shadow or a weird shadow"; - this.tokenized = formatter.Tokenize(strg); + formatter.AddImage("Test", new TextureRegion(tex, 0, 8, 24, 24)); + //var strg = "This is a formatted string with two bits of formatting! It also includesaverylongwordthatisformattedaswell. Additionally, it wobbles and has a shadow or a weird shadow. We like icons too! "; + var strg = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; + //var strg = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; + this.tokenized = formatter.Tokenize(font, strg); this.tokenized.Split(font, 400, 1); this.OnDraw += (g, time) => { this.SpriteBatch.Begin(); + this.SpriteBatch.FillRectangle(new RectangleF(400, 20, 400, 1000), Color.Green); this.tokenized.Draw(time, this.SpriteBatch, new Vector2(400, 20), font, Color.White, 1, 0); this.SpriteBatch.End(); }; - this.OnUpdate += (g, time) => this.tokenized.Update(time); + this.OnUpdate += (g, time) => { + if (this.InputHandler.IsPressed(Keys.W)) { + this.tokenized = formatter.Tokenize(font, strg); + this.tokenized.Split(font, this.InputHandler.IsModifierKeyDown(ModifierKey.Shift) ? 400 : 500, 1); + } + this.tokenized.Update(time); + }; } protected override void DoUpdate(GameTime gameTime) {