1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-23 17:13:38 +02:00

finished the new formatting, finally!

This commit is contained in:
Ellpeck 2020-05-15 19:55:59 +02:00
parent 011f9dd4f1
commit fcd898e16b
10 changed files with 155 additions and 58 deletions

View file

@ -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 <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>"));
// 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<Texture2D>("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 <i Grass> 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 <a wobbly>wobbly text</a> at <a wobbly 8 0.25>different intensities</a>. 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));

View file

@ -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<GenericFont> RegularFont;
public StyleProp<GenericFont> BoldFont;
public StyleProp<GenericFont> ItalicFont;
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public StyleProp<FormatSettings> FormatSettings;
public readonly TextFormatter Formatter;
public TokenizedString TokenizedText { get; private set; }
public StyleProp<Color> TextColor;
public StyleProp<float> 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);
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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($"<i {name}>"), (f, m, r) => new ImageCode(m, r, image));
}
}
}

View file

@ -16,43 +16,43 @@ namespace MLEM.Formatting {
public TextFormatter(Func<GenericFont> boldFont = null, Func<GenericFont> italicFont = null) {
// font codes
this.Codes.Add(new Regex("<b>"), (f, m) => new FontCode(m, boldFont?.Invoke()));
this.Codes.Add(new Regex("<i>"), (f, m) => new FontCode(m, italicFont?.Invoke()));
this.Codes.Add(new Regex(@"<s(?: #([0-9\w]{6,8}) (([+-.0-9]*)))?>"), (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("</(b|i|s)>"), (f, m) => new FontCode(m, null));
this.Codes.Add(new Regex("<b>"), (f, m, r) => new FontCode(m, r, boldFont?.Invoke()));
this.Codes.Add(new Regex("<i>"), (f, m, r) => new FontCode(m, r, italicFont?.Invoke()));
this.Codes.Add(new Regex(@"<s(?: #([0-9\w]{6,8}) (([+-.0-9]*)))?>"), (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("</(b|i|s)>"), (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($"<c {c.Name}>"), (f, m) => new ColorCode(m, value));
this.Codes.Add(new Regex($"<c {c.Name}>"), (f, m, r) => new ColorCode(m, r, value));
}
}
this.Codes.Add(new Regex(@"<c #([0-9\w]{6,8})>"), (f, m) => new ColorCode(m, ColorExtensions.FromHex(m.Groups[1].Value)));
this.Codes.Add(new Regex("</c>"), (f, m) => new ColorCode(m, null));
this.Codes.Add(new Regex(@"<c #([0-9\w]{6,8})>"), (f, m, r) => new ColorCode(m, r, ColorExtensions.FromHex(m.Groups[1].Value)));
this.Codes.Add(new Regex("</c>"), (f, m, r) => new ColorCode(m, r, null));
// animation codes
this.Codes.Add(new Regex(@"<a wobbly(?: ([+-.0-9]*) ([+-.0-9]*))?>"), (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("</a>"), (f, m) => new AnimatedCode(m));
this.Codes.Add(new Regex(@"<a wobbly(?: ([+-.0-9]*) ([+-.0-9]*))?>"), (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("</a>"), (f, m, r) => new AnimatedCode(m, r));
}
public TokenizedString Tokenize(string s) {
public TokenizedString Tokenize(GenericFont font, string s) {
var tokens = new List<Token>();
var codes = new List<Code>();
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<Code> codes) {
foreach (var code in codes)
s = code.Regex.Replace(s, code.GetReplacementString(font));
return s;
}
}

View file

@ -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);

View file

@ -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 <c CornflowerBlue>is a formatted string</c> with <c #ff0000>two bits of formatting</c>! It also includesavery<c Pink>long</c>wordthatis<c Blue>formatted</c>aswell. Additionally, it <a wobbly>wobbles</a> and has a <s>shadow</s> or a <s #ff0000 4>weird shadow</s>";
this.tokenized = formatter.Tokenize(strg);
formatter.AddImage("Test", new TextureRegion(tex, 0, 8, 24, 24));
//var strg = "This <c CornflowerBlue>is a formatted string</c> with <c #ffff0000>two bits of formatting</c>! It also includesavery<c Pink>long</c>wordthatis<c Blue>formatted</c>aswell. Additionally, it <a wobbly>wobbles</a> and has a <s>shadow</s> or a <s #ffff0000 4>weird shadow</s>. We like icons too! <i Test> <i Test> <i Test> <i Test> <i Test> <i Test> <i Test> <i Test> <i Test> <i Test> <i Test> <i Test> <i Test><i Test><i Test><i Test><i Test><i Test><i Test><i Test><i Test><i Test><i Test><i Test><i Test>";
var strg = "Lorem Ipsum <i Test> is simply dummy text of the <i Test> printing and typesetting <i Test> industry. Lorem Ipsum has been the industry's standard dummy text <i Test> ever since the <i Test> 1500s, when <i Test><i Test><i Test><i Test><i Test><i Test><i Test> 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) {