1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-12-24 01:09:23 +01:00

streamline ui text formatting and paragraph links

This commit is contained in:
Ellpeck 2020-05-17 00:10:29 +02:00
parent fad06f28be
commit 037ed43410
16 changed files with 138 additions and 78 deletions

View file

@ -37,9 +37,8 @@ namespace Demos {
var style = new UntexturedStyle(this.SpriteBatch) {
// 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")),
// Supplying a bold and an italic version is optional
Font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont"), LoadContent<SpriteFont>("Fonts/TestFontBold"), LoadContent<SpriteFont>("Fonts/TestFontItalic")),
TextScale = 0.1F,
PanelTexture = this.testPatch,
ButtonTexture = new NinePatch(new TextureRegion(this.testTexture, 24, 8, 16, 16), 4),
@ -89,8 +88,8 @@ namespace Demos {
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
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, "Additionally, you can create custom formatting codes that contain <i Grass> images and more!"));
this.UiSystem.TextFormatter.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));

View file

@ -10,10 +10,14 @@ namespace MLEM.Extended.Font {
public class GenericBitmapFont : GenericFont {
public readonly BitmapFont Font;
public override GenericFont Bold { get; }
public override GenericFont Italic { get; }
public override float LineHeight => this.Font.LineHeight;
public GenericBitmapFont(BitmapFont font) {
public GenericBitmapFont(BitmapFont font, BitmapFont bold = null, BitmapFont italic = null) {
this.Font = font;
this.Bold = bold != null ? new GenericBitmapFont(bold) : this;
this.Italic = italic != null ? new GenericBitmapFont(italic) : this;
}
public override Vector2 MeasureString(string text) {

View file

@ -22,13 +22,13 @@ namespace MLEM.Ui.Elements {
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public FormattingCodeCollection Formatting;
public StyleProp<GenericFont> RegularFont;
[Obsolete("Use the new GenericFont.Bold and GenericFont.Italic instead")]
public StyleProp<GenericFont> BoldFont;
[Obsolete("Use the new GenericFont.Bold and GenericFont.Italic instead")]
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 Token HoveredToken { get; private set; }
public StyleProp<Color> TextColor;
public StyleProp<float> TextScale;
@ -39,6 +39,8 @@ namespace MLEM.Ui.Elements {
this.text = value;
this.IsHidden = string.IsNullOrWhiteSpace(this.text);
this.SetAreaDirty();
// cause text to be re-tokenized
this.TokenizedText = null;
}
}
}
@ -64,20 +66,6 @@ namespace MLEM.Ui.Elements {
this.AutoAdjustWidth = centerText;
this.CanBeSelected = false;
this.CanBeMoused = false;
this.Formatter = new TextFormatter(() => this.BoldFont, () => this.ItalicFont);
this.Formatter.Codes.Add(new Regex("<l ([^>]+)>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F, t => t == this.HoveredToken));
this.OnPressed += e => {
if (this.HoveredToken == null)
return;
foreach (var code in this.HoveredToken.AppliedCodes.OfType<LinkCode>()) {
try {
Process.Start(code.Match.Groups[1].Value);
} catch (Exception) {
// ignored
}
}
};
}
protected override Vector2 CalcActualSize(RectangleF parentArea) {
@ -92,9 +80,23 @@ namespace MLEM.Ui.Elements {
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);
this.CanBeMoused = this.TokenizedText.AllCodes.OfType<LinkCode>().Any();
var linkTokens = this.TokenizedText.Tokens.Where(t => t.AppliedCodes.Any(c => c is LinkCode)).ToArray();
// this basically checks if there are any tokens that have an area that doesn't have a link element associated with it
if (linkTokens.Any(t => !t.GetArea(Vector2.Zero, this.TextScale).All(a => this.GetChildren<Link>(c => c.PositionOffset == a.Location && c.Size == a.Size).Any()))) {
this.RemoveChildren(c => c is Link);
foreach (var link in linkTokens) {
var areas = link.GetArea(Vector2.Zero, this.TextScale).ToArray();
for (var i = 0; i < areas.Length; i++) {
var area = areas[i];
this.AddChild(new Link(Anchor.TopLeft, link, area.Size) {
PositionOffset = area.Location,
// only allow selecting the first part of a link
CanBeSelected = i == 0
});
}
}
}
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);
@ -103,6 +105,9 @@ namespace MLEM.Ui.Elements {
public override void ForceUpdateArea() {
if (this.GetTextCallback != null)
this.Text = this.GetTextCallback(this);
if (this.TokenizedText == null)
this.TokenizedText = this.System.TextFormatter.Tokenize(this.RegularFont, this.text);
base.ForceUpdateArea();
}
@ -112,10 +117,8 @@ namespace MLEM.Ui.Elements {
this.Text = this.GetTextCallback(this);
this.TimeIntoAnimation += time.ElapsedGameTime;
if (this.TokenizedText != null) {
if (this.TokenizedText != null)
this.TokenizedText.Update(time);
this.HoveredToken = this.TokenizedText.GetTokenUnderPos(this.RegularFont, this.DisplayArea.Location, this.Input.MousePosition.ToVector2(), this.TextScale * this.Scale);
}
}
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
@ -147,5 +150,24 @@ namespace MLEM.Ui.Elements {
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public delegate string TextModifier(string text);
public class Link : Element {
public readonly Token Token;
public Link(Anchor anchor, Token token, Vector2 size) : base(anchor, size) {
this.Token = token;
this.OnPressed += e => {
foreach (var code in token.AppliedCodes.OfType<LinkCode>()) {
try {
Process.Start(code.Match.Groups[1].Value);
} catch (Exception) {
// ignored
}
}
};
}
}
}
}

View file

@ -37,7 +37,9 @@ namespace MLEM.Ui.Style {
public NinePatch ProgressBarProgressTexture;
public Color ProgressBarProgressColor;
public GenericFont Font;
[Obsolete("Use the new GenericFont.Bold and GenericFont.Italic instead")]
public GenericFont BoldFont;
[Obsolete("Use the new GenericFont.Bold and GenericFont.Italic instead")]
public GenericFont ItalicFont;
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public FormatSettings FormatSettings;

View file

@ -36,6 +36,8 @@ namespace MLEM.Ui.Style {
private class EmptyFont : GenericFont {
public override GenericFont Bold => this;
public override GenericFont Italic => this;
public override float LineHeight => 1;
public override Vector2 MeasureString(string text) {

View file

@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
@ -50,6 +53,7 @@ namespace MLEM.Ui {
public float DrawAlpha = 1;
public BlendState BlendState;
public SamplerState SamplerState = SamplerState.PointClamp;
public TextFormatter TextFormatter;
public UiControls Controls;
public Element.DrawCallback OnElementDrawn = (e, time, batch, alpha) => e.OnDrawn?.Invoke(e, time, batch, alpha);
@ -97,6 +101,9 @@ namespace MLEM.Ui {
if (e.OnSecondaryPressed != null)
e.SecondActionSound.Value?.Replay();
};
this.TextFormatter = new TextFormatter();
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 link && link.Token == t));
}
public override void Update(GameTime time) {

View file

@ -7,6 +7,10 @@ using Microsoft.Xna.Framework.Graphics;
namespace MLEM.Font {
public abstract class GenericFont {
public abstract GenericFont Bold { get; }
public abstract GenericFont Italic { get; }
public abstract float LineHeight { get; }
public abstract Vector2 MeasureString(string text);

View file

@ -8,10 +8,14 @@ namespace MLEM.Font {
public class GenericSpriteFont : GenericFont {
public readonly SpriteFont Font;
public override GenericFont Bold { get; }
public override GenericFont Italic { get; }
public override float LineHeight => this.Font.LineSpacing;
public GenericSpriteFont(SpriteFont font) {
public GenericSpriteFont(SpriteFont font, SpriteFont bold = null, SpriteFont italic = null) {
this.Font = font;
this.Bold = bold != null ? new GenericSpriteFont(bold) : this;
this.Italic = italic != null ? new GenericSpriteFont(italic) : this;
}
public override Vector2 MeasureString(string text) {

View file

@ -20,11 +20,11 @@ namespace MLEM.Formatting.Codes {
return other.GetType() == this.GetType();
}
public virtual Color? GetColor() {
public virtual Color? GetColor(Color defaultPick) {
return null;
}
public virtual GenericFont GetFont() {
public virtual GenericFont GetFont(GenericFont defaultPick) {
return null;
}

View file

@ -10,7 +10,7 @@ namespace MLEM.Formatting.Codes {
this.color = color;
}
public override Color? GetColor() {
public override Color? GetColor(Color defaultPick) {
return this.color;
}

View file

@ -1,17 +1,18 @@
using System;
using System.Text.RegularExpressions;
using MLEM.Font;
namespace MLEM.Formatting.Codes {
public class FontCode : Code {
private readonly GenericFont font;
private readonly Func<GenericFont, GenericFont> font;
public FontCode(Match match, Regex regex, GenericFont font) : base(match, regex) {
public FontCode(Match match, Regex regex, Func<GenericFont, GenericFont> font) : base(match, regex) {
this.font = font;
}
public override GenericFont GetFont() {
return this.font;
public override GenericFont GetFont(GenericFont defaultPick) {
return this.font?.Invoke(defaultPick);
}
public override bool EndsHere(Code other) {

View file

@ -13,7 +13,7 @@ namespace MLEM.Formatting.Codes {
this.isSelected = isSelected;
}
public bool IsSelected() {
public virtual bool IsSelected() {
return this.isSelected(this.Token);
}

View file

@ -18,7 +18,7 @@ namespace MLEM.Formatting.Codes {
public override 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) {
// don't underline spaces at the end of lines
if (c == ' ' && this.Token.Substring.Length > indexInToken + 1 && this.Token.Substring[indexInToken + 1] == '\n')
if (c == ' ' && this.Token.DisplayString.Length > indexInToken + 1 && this.Token.DisplayString[indexInToken + 1] == '\n')
return false;
var size = font.MeasureString(cString) * scale;
var thicc = size.Y * this.thickness;

View file

@ -14,10 +14,10 @@ namespace MLEM.Formatting {
public readonly Dictionary<Regex, Code.Constructor> Codes = new Dictionary<Regex, Code.Constructor>();
public TextFormatter(Func<GenericFont> boldFont = null, Func<GenericFont> italicFont = null) {
public TextFormatter() {
// font codes
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("<b>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Bold));
this.Codes.Add(new Regex("<i>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Italic));
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("<u>"), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.85F));
this.Codes.Add(new Regex("</(b|i|s|u|l)>"), (f, m, r) => new FontCode(m, r, null));
@ -62,7 +62,7 @@ namespace MLEM.Formatting {
codes.RemoveAll(c => c.EndsHere(next));
codes.Add(next);
}
return new TokenizedString(s, StripFormatting(font, s, tokens.SelectMany(t => t.AppliedCodes)), tokens.ToArray());
return new TokenizedString(font, s, StripFormatting(font, s, tokens.SelectMany(t => t.AppliedCodes)), tokens.ToArray());
}
private Code GetNextCode(string s, int index) {

View file

@ -13,8 +13,11 @@ namespace MLEM.Formatting {
public readonly Code[] AppliedCodes;
public readonly int Index;
public readonly int RawIndex;
public string Substring { get; internal set; }
public readonly string Substring;
public string DisplayString => this.SplitSubstring ?? this.Substring;
public readonly string RawSubstring;
internal RectangleF[] Area;
internal string SplitSubstring;
public Token(Code[] appliedCodes, int index, int rawIndex, string substring, string rawSubstring) {
this.AppliedCodes = appliedCodes;
@ -27,12 +30,12 @@ namespace MLEM.Formatting {
code.Token = this;
}
public Color? GetColor() {
return this.AppliedCodes.Select(c => c.GetColor()).FirstOrDefault(c => c.HasValue);
public Color? GetColor(Color defaultPick) {
return this.AppliedCodes.Select(c => c.GetColor(defaultPick)).FirstOrDefault(c => c.HasValue);
}
public GenericFont GetFont() {
return this.AppliedCodes.Select(c => c.GetFont()).FirstOrDefault();
public GenericFont GetFont(GenericFont defaultPick) {
return this.AppliedCodes.Select(c => c.GetFont(defaultPick)).FirstOrDefault();
}
public void DrawSelf(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
@ -50,5 +53,9 @@ namespace MLEM.Formatting {
font.DrawString(batch, cString, pos, color, 0, Vector2.Zero, scale, SpriteEffects.None, depth);
}
public IEnumerable<RectangleF> GetArea(Vector2 stringPos, float scale) {
return this.Area.Select(a => new RectangleF(stringPos + a.Location * scale, a.Size * scale));
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
@ -11,25 +12,28 @@ namespace MLEM.Formatting {
public class TokenizedString : GenericDataHolder {
public readonly string RawString;
public string String { get; private set; }
public readonly string String;
public string DisplayString => this.splitString ?? this.String;
public readonly Token[] Tokens;
public readonly Code[] AllCodes;
private string splitString;
public TokenizedString(string rawString, string strg, Token[] tokens) {
public TokenizedString(GenericFont font, string rawString, string strg, Token[] tokens) {
this.RawString = rawString;
this.String = strg;
this.Tokens = tokens;
// since a code can be present in multiple tokens, we use Distinct here
this.AllCodes = tokens.SelectMany(t => t.AppliedCodes).Distinct().ToArray();
this.CalculateTokenAreas(font);
}
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
this.String = font.SplitString(this.String, width, scale);
this.splitString = font.SplitString(this.String, width, scale);
// skip splitting logic for unformatted text
if (this.Tokens.Length == 1) {
this.Tokens[0].Substring = this.String;
this.Tokens[0].SplitSubstring = this.splitString;
return;
}
foreach (var token in this.Tokens) {
@ -37,23 +41,24 @@ namespace MLEM.Formatting {
var length = 0;
var ret = new StringBuilder();
// this is basically a substring function that ignores newlines for indexing
for (var i = 0; i < this.String.Length; i++) {
for (var i = 0; i < this.splitString.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(this.String[i]);
ret.Append(this.splitString[i]);
// if the current char is not a newline, we simulate length increase
if (this.String[i] != '\n') {
if (this.splitString[i] != '\n') {
if (index >= token.Index)
length++;
index++;
}
}
token.Substring = ret.ToString();
token.SplitSubstring = ret.ToString();
}
this.CalculateTokenAreas(font);
}
public Vector2 Measure(GenericFont font) {
return font.MeasureString(this.String);
return font.MeasureString(this.DisplayString);
}
public void Update(GameTime time) {
@ -61,34 +66,17 @@ namespace MLEM.Formatting {
code.Update(time);
}
public Token GetTokenUnderPos(GenericFont font, Vector2 stringPos, Vector2 target, float scale) {
var innerOffset = new Vector2();
foreach (var token in this.Tokens) {
var split = token.Substring.Split('\n');
for (var i = 0; i < split.Length; i++) {
var size = font.MeasureString(split[i]) * scale;
var lineArea = new RectangleF(stringPos + innerOffset, size);
if (lineArea.Contains(target))
return token;
if (i < split.Length - 1) {
innerOffset.X = 0;
innerOffset.Y += font.LineHeight * scale;
} else {
innerOffset.X += size.X;
}
}
}
return null;
public Token GetTokenUnderPos(Vector2 stringPos, Vector2 target, float scale) {
return this.Tokens.FirstOrDefault(t => t.GetArea(stringPos, scale).Any(r => r.Contains(target)));
}
public void Draw(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
var innerOffset = new Vector2();
foreach (var token in this.Tokens) {
var drawFont = token.GetFont() ?? font;
var drawColor = token.GetColor() ?? color;
for (var i = 0; i < token.Substring.Length; i++) {
var c = token.Substring[i];
var drawFont = token.GetFont(font) ?? font;
var drawColor = token.GetColor(color) ?? color;
for (var i = 0; i < token.DisplayString.Length; i++) {
var c = token.DisplayString[i];
if (c == '\n') {
innerOffset.X = 0;
innerOffset.Y += font.LineHeight * scale;
@ -103,5 +91,25 @@ namespace MLEM.Formatting {
}
}
private void CalculateTokenAreas(GenericFont font) {
var innerOffset = new Vector2();
foreach (var token in this.Tokens) {
var area = new List<RectangleF>();
var split = token.DisplayString.Split('\n');
for (var i = 0; i < split.Length; i++) {
var size = font.MeasureString(split[i]);
area.Add(new RectangleF(innerOffset, size));
if (i < split.Length - 1) {
innerOffset.X = 0;
innerOffset.Y += font.LineHeight;
} else {
innerOffset.X += size.X;
}
}
token.Area = area.ToArray();
}
}
}
}