mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-12-24 17:29:23 +01:00
added text alignment options to tokenized strings and paragraphs
This commit is contained in:
parent
d1ce9412a2
commit
14940d39c5
7 changed files with 188 additions and 72 deletions
|
@ -6,6 +6,7 @@ using Microsoft.Xna.Framework;
|
|||
using Microsoft.Xna.Framework.Graphics;
|
||||
using MLEM.Extensions;
|
||||
using MLEM.Font;
|
||||
using MLEM.Formatting;
|
||||
using MLEM.Formatting.Codes;
|
||||
using MLEM.Misc;
|
||||
using MLEM.Startup;
|
||||
|
@ -191,6 +192,14 @@ namespace Demos {
|
|||
this.root.AddChild(new VerticalSpace(3));
|
||||
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Disabled button", "This button can't be clicked or moved to using automatic navigation") {IsDisabled = true}).PositionOffset = new Vector2(0, 1);
|
||||
|
||||
const string alignText = "Paragraphs can have <c CornflowerBlue><l Left>left</l></c> aligned text, <c CornflowerBlue><l Right>right</l></c> aligned text and <c CornflowerBlue><l Center>center</l></c> aligned text.";
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
var alignPar = this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, alignText));
|
||||
alignPar.LinkAction = (l, c) => {
|
||||
if (Enum.TryParse<TextAlignment>(c.Match.Groups[1].Value, out var alignment))
|
||||
alignPar.Alignment = alignment;
|
||||
};
|
||||
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "The code for this demo contains some examples for how to query element data. This is the output of that:"));
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
public class Paragraph : Element {
|
||||
|
||||
private string text;
|
||||
/// <summary>
|
||||
/// The font that this paragraph draws text with.
|
||||
/// To set its bold and italic font, use <see cref="GenericFont.Bold"/> and <see cref="GenericFont.Italic"/>.
|
||||
|
@ -84,6 +83,20 @@ namespace MLEM.Ui.Elements {
|
|||
/// By default, <see cref="MlemPlatform.OpenLinkOrFile"/> is executed.
|
||||
/// </summary>
|
||||
public Action<Link, LinkCode> LinkAction;
|
||||
/// <summary>
|
||||
/// The <see cref="TextAlignment"/> that this paragraph's text should be rendered with
|
||||
/// </summary>
|
||||
public TextAlignment Alignment {
|
||||
get => this.alignment;
|
||||
set {
|
||||
this.alignment = value;
|
||||
this.SetAreaDirty();
|
||||
this.TokenizedText = null;
|
||||
}
|
||||
}
|
||||
|
||||
private string text;
|
||||
private TextAlignment alignment;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new paragraph with the given settings.
|
||||
|
@ -128,10 +141,10 @@ namespace MLEM.Ui.Elements {
|
|||
|
||||
/// <inheritdoc />
|
||||
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
|
||||
var pos = this.DisplayArea.Location;
|
||||
var pos = this.DisplayArea.Location + new Vector2(GetAlignmentOffset(), 0);
|
||||
var sc = this.TextScale * this.TextScaleMultiplier * this.Scale;
|
||||
var color = this.TextColor.OrDefault(Color.White) * alpha;
|
||||
this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0);
|
||||
this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0, this.Alignment);
|
||||
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
|
||||
}
|
||||
|
||||
|
@ -151,7 +164,7 @@ namespace MLEM.Ui.Elements {
|
|||
protected virtual void ParseText(Vector2 size) {
|
||||
if (this.TokenizedText == null) {
|
||||
// tokenize the text
|
||||
this.TokenizedText = this.System.TextFormatter.Tokenize(this.RegularFont, this.Text);
|
||||
this.TokenizedText = this.System.TextFormatter.Tokenize(this.RegularFont, this.Text, this.Alignment);
|
||||
|
||||
// add links to the paragraph
|
||||
this.RemoveChildren(c => c is Link);
|
||||
|
@ -162,9 +175,9 @@ namespace MLEM.Ui.Elements {
|
|||
var width = size.X - this.ScaledPadding.Width;
|
||||
var scale = this.TextScale * this.TextScaleMultiplier * this.Scale;
|
||||
if (this.TruncateIfLong) {
|
||||
this.TokenizedText.Truncate(this.RegularFont, width, scale, this.Ellipsis);
|
||||
this.TokenizedText.Truncate(this.RegularFont, width, scale, this.Ellipsis, this.Alignment);
|
||||
} else {
|
||||
this.TokenizedText.Split(this.RegularFont, width, scale);
|
||||
this.TokenizedText.Split(this.RegularFont, width, scale, this.Alignment);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,6 +186,16 @@ namespace MLEM.Ui.Elements {
|
|||
this.Text = this.GetTextCallback(this);
|
||||
}
|
||||
|
||||
private float GetAlignmentOffset() {
|
||||
switch (this.Alignment) {
|
||||
case TextAlignment.Center:
|
||||
return this.DisplayArea.Width / 2;
|
||||
case TextAlignment.Right:
|
||||
return this.DisplayArea.Width;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A delegate method used for <see cref="Paragraph.GetTextCallback"/>
|
||||
/// </summary>
|
||||
|
@ -214,7 +237,7 @@ namespace MLEM.Ui.Elements {
|
|||
public override void ForceUpdateArea() {
|
||||
// set the position offset and size to the token's first area
|
||||
var area = this.Token.GetArea(Vector2.Zero, this.textScale).First();
|
||||
this.PositionOffset = area.Location;
|
||||
this.PositionOffset = area.Location + new Vector2(((Paragraph) this.Parent).GetAlignmentOffset() / this.Parent.Scale, 0);
|
||||
this.Size = area.Size;
|
||||
base.ForceUpdateArea();
|
||||
}
|
||||
|
|
|
@ -70,13 +70,13 @@ namespace MLEM.Font {
|
|||
}
|
||||
|
||||
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
|
||||
public Vector2 MeasureString(string text) {
|
||||
public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) {
|
||||
var size = Vector2.Zero;
|
||||
if (text.Length <= 0)
|
||||
return size;
|
||||
var xOffset = 0F;
|
||||
foreach (var c in text) {
|
||||
switch (c) {
|
||||
for (var i = 0; i < text.Length; i++) {
|
||||
switch (text[i]) {
|
||||
case '\n':
|
||||
xOffset = 0;
|
||||
size.Y += this.LineHeight;
|
||||
|
@ -90,8 +90,16 @@ namespace MLEM.Font {
|
|||
case Zwsp:
|
||||
// don't add width for a zero-width space
|
||||
break;
|
||||
case ' ':
|
||||
if (ignoreTrailingSpaces && IsTrailingSpace(text, i)) {
|
||||
// if this is a trailing space, we can skip remaining spaces too
|
||||
i = text.Length - 1;
|
||||
break;
|
||||
}
|
||||
xOffset += this.MeasureChar(' ').X;
|
||||
break;
|
||||
default:
|
||||
xOffset += this.MeasureChar(c).X;
|
||||
xOffset += this.MeasureChar(text[i]).X;
|
||||
break;
|
||||
}
|
||||
// increase x size if this line is the longest
|
||||
|
@ -186,5 +194,13 @@ namespace MLEM.Font {
|
|||
return ret.ToString();
|
||||
}
|
||||
|
||||
private static bool IsTrailingSpace(string s, int index) {
|
||||
for (var i = index + 1; i < s.Length; i++) {
|
||||
if (s[i] != ' ')
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
22
MLEM/Formatting/TextAlignment.cs
Normal file
22
MLEM/Formatting/TextAlignment.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
namespace MLEM.Formatting {
|
||||
/// <summary>
|
||||
/// An enumeration that represents a set of alignment options for <see cref="TokenizedString"/> objects and MLEM.Ui paragraphs.
|
||||
/// </summary>
|
||||
public enum TextAlignment {
|
||||
|
||||
/// <summary>
|
||||
/// Left alignment, which is also the default value
|
||||
/// </summary>
|
||||
Left,
|
||||
/// <summary>
|
||||
/// Center alignment
|
||||
/// </summary>
|
||||
Center,
|
||||
/// <summary>
|
||||
/// Right alignment.
|
||||
/// In this alignment option, trailing spaces are ignored to ensure that visual alignment is consistent.
|
||||
/// </summary>
|
||||
Right
|
||||
|
||||
}
|
||||
}
|
|
@ -67,8 +67,9 @@ namespace MLEM.Formatting {
|
|||
/// </summary>
|
||||
/// <param name="font">The font to use for tokenization. Note that this font needs to be the same that will later be used for splitting, measuring and/or drawing.</param>
|
||||
/// <param name="s">The string to tokenize</param>
|
||||
/// <param name="alignment">The text alignment that should be used. Note that this alignment needs to be the same that will later be used for splitting, measuring and/or drawing.</param>
|
||||
/// <returns></returns>
|
||||
public TokenizedString Tokenize(GenericFont font, string s) {
|
||||
public TokenizedString Tokenize(GenericFont font, string s, TextAlignment alignment = TextAlignment.Left) {
|
||||
// resolve macros
|
||||
s = this.ResolveMacros(s);
|
||||
var tokens = new List<Token>();
|
||||
|
@ -101,7 +102,7 @@ namespace MLEM.Formatting {
|
|||
codes.RemoveAll(c => c.EndsHere(next));
|
||||
codes.Add(next);
|
||||
}
|
||||
return new TokenizedString(font, s, StripFormatting(font, s, tokens.SelectMany(t => t.AppliedCodes)), tokens.ToArray());
|
||||
return new TokenizedString(font, alignment, s, StripFormatting(font, s, tokens.SelectMany(t => t.AppliedCodes)), tokens.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -33,6 +33,10 @@ namespace MLEM.Formatting {
|
|||
/// </summary>
|
||||
public string DisplayString => this.ModifiedSubstring ?? this.Substring;
|
||||
/// <summary>
|
||||
/// The <see cref="DisplayString"/>, but split at newline characters
|
||||
/// </summary>
|
||||
public string[] SplitDisplayString { get; internal set; }
|
||||
/// <summary>
|
||||
/// The substring that this token contains, without the formatting codes removed.
|
||||
/// </summary>
|
||||
public readonly string RawSubstring;
|
||||
|
|
|
@ -38,13 +38,13 @@ namespace MLEM.Formatting {
|
|||
public readonly Code[] AllCodes;
|
||||
private string modifiedString;
|
||||
|
||||
internal TokenizedString(GenericFont font, string rawString, string strg, Token[] tokens) {
|
||||
internal TokenizedString(GenericFont font, TextAlignment alignment, 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);
|
||||
this.RecalculateTokenData(font, alignment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -55,10 +55,11 @@ namespace MLEM.Formatting {
|
|||
/// <param name="font">The font to use for width calculations</param>
|
||||
/// <param name="width">The maximum width, in display pixels based on the font and scale</param>
|
||||
/// <param name="scale">The scale to use for width measurements</param>
|
||||
public void Split(GenericFont font, float width, float scale) {
|
||||
/// <param name="alignment">The text alignment that should be used for width calculations</param>
|
||||
public void Split(GenericFont font, float width, float scale, TextAlignment alignment = TextAlignment.Left) {
|
||||
// a split string has the same character count as the input string but with newline characters added
|
||||
this.modifiedString = font.SplitString(this.String, width, scale);
|
||||
this.StoreModifiedSubstrings(font);
|
||||
this.StoreModifiedSubstrings(font, alignment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -70,12 +71,13 @@ namespace MLEM.Formatting {
|
|||
/// <param name="width">The maximum width, in display pixels based on the font and scale</param>
|
||||
/// <param name="scale">The scale to use for width measurements</param>
|
||||
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
|
||||
public void Truncate(GenericFont font, float width, float scale, string ellipsis = "") {
|
||||
/// <param name="alignment">The text alignment that should be used for width calculations</param>
|
||||
public void Truncate(GenericFont font, float width, float scale, string ellipsis = "", TextAlignment alignment = TextAlignment.Left) {
|
||||
this.modifiedString = font.TruncateString(this.String, width, scale, false, ellipsis);
|
||||
this.StoreModifiedSubstrings(font);
|
||||
this.StoreModifiedSubstrings(font, alignment);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GenericFont.MeasureString(string)"/>
|
||||
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
|
||||
public Vector2 Measure(GenericFont font) {
|
||||
return font.MeasureString(this.DisplayString);
|
||||
}
|
||||
|
@ -102,77 +104,116 @@ namespace MLEM.Formatting {
|
|||
}
|
||||
|
||||
/// <inheritdoc cref="GenericFont.DrawString(SpriteBatch,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
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) {
|
||||
public void Draw(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth, TextAlignment alignment = TextAlignment.Left) {
|
||||
var innerOffset = new Vector2(this.GetInnerOffsetX(font, 0, 0, scale, alignment), 0);
|
||||
for (var t = 0; t < this.Tokens.Length; t++) {
|
||||
var token = this.Tokens[t];
|
||||
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;
|
||||
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
||||
var line = token.SplitDisplayString[l];
|
||||
for (var i = 0; i < line.Length; i++) {
|
||||
var c = line[i];
|
||||
if (i == 0)
|
||||
token.DrawSelf(time, batch, pos + innerOffset, font, color, scale, depth);
|
||||
|
||||
var cString = c.ToCachedString();
|
||||
token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth);
|
||||
innerOffset.X += font.MeasureString(cString).X * scale;
|
||||
}
|
||||
// only split at a new line, not between tokens!
|
||||
if (l < token.SplitDisplayString.Length - 1) {
|
||||
innerOffset.X = this.GetInnerOffsetX(font, t, l + 1, scale, alignment);
|
||||
innerOffset.Y += font.LineHeight * scale;
|
||||
}
|
||||
if (i == 0)
|
||||
token.DrawSelf(time, batch, pos + innerOffset, font, color, scale, depth);
|
||||
|
||||
var cString = c.ToCachedString();
|
||||
token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth);
|
||||
innerOffset.X += font.MeasureString(cString).X * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StoreModifiedSubstrings(GenericFont font) {
|
||||
// skip substring logic for unformatted text
|
||||
private void StoreModifiedSubstrings(GenericFont font, TextAlignment alignment) {
|
||||
if (this.Tokens.Length == 1) {
|
||||
// skip substring logic for unformatted text
|
||||
this.Tokens[0].ModifiedSubstring = this.modifiedString;
|
||||
return;
|
||||
} else {
|
||||
// this is basically a substring function that ignores added newlines for indexing
|
||||
var index = 0;
|
||||
var currToken = 0;
|
||||
var splitIndex = 0;
|
||||
var ret = new StringBuilder();
|
||||
while (splitIndex < this.modifiedString.Length && currToken < this.Tokens.Length) {
|
||||
var token = this.Tokens[currToken];
|
||||
if (token.Substring.Length > 0) {
|
||||
ret.Append(this.modifiedString[splitIndex]);
|
||||
// if the current char is not an added newline, we simulate length increase
|
||||
if (this.modifiedString[splitIndex] != '\n' || this.String[index] == '\n')
|
||||
index++;
|
||||
splitIndex++;
|
||||
}
|
||||
// move on to the next token if we reached its end
|
||||
if (index >= token.Index + token.Substring.Length) {
|
||||
token.ModifiedSubstring = ret.ToString();
|
||||
ret.Clear();
|
||||
currToken++;
|
||||
}
|
||||
}
|
||||
// set additional token contents beyond our string in case we truncated
|
||||
if (ret.Length > 0)
|
||||
this.Tokens[currToken++].ModifiedSubstring = ret.ToString();
|
||||
while (currToken < this.Tokens.Length)
|
||||
this.Tokens[currToken++].ModifiedSubstring = string.Empty;
|
||||
}
|
||||
|
||||
// this is basically a substring function that ignores added newlines for indexing
|
||||
var index = 0;
|
||||
var currToken = 0;
|
||||
var splitIndex = 0;
|
||||
var ret = new StringBuilder();
|
||||
while (splitIndex < this.modifiedString.Length && currToken < this.Tokens.Length) {
|
||||
var token = this.Tokens[currToken];
|
||||
if (token.Substring.Length > 0) {
|
||||
ret.Append(this.modifiedString[splitIndex]);
|
||||
// if the current char is not an added newline, we simulate length increase
|
||||
if (this.modifiedString[splitIndex] != '\n' || this.String[index] == '\n')
|
||||
index++;
|
||||
splitIndex++;
|
||||
}
|
||||
// move on to the next token if we reached its end
|
||||
if (index >= token.Index + token.Substring.Length) {
|
||||
token.ModifiedSubstring = ret.ToString();
|
||||
ret.Clear();
|
||||
currToken++;
|
||||
}
|
||||
}
|
||||
// set additional token contents beyond our string in case we truncated
|
||||
if (ret.Length > 0)
|
||||
this.Tokens[currToken++].ModifiedSubstring = ret.ToString();
|
||||
while (currToken < this.Tokens.Length)
|
||||
this.Tokens[currToken++].ModifiedSubstring = string.Empty;
|
||||
|
||||
this.CalculateTokenAreas(font);
|
||||
this.RecalculateTokenData(font, alignment);
|
||||
}
|
||||
|
||||
private void CalculateTokenAreas(GenericFont font) {
|
||||
var innerOffset = new Vector2();
|
||||
foreach (var token in this.Tokens) {
|
||||
private float GetInnerOffsetX(GenericFont font, int tokenIndex, int lineIndex, float scale, TextAlignment alignment) {
|
||||
if (alignment > TextAlignment.Left) {
|
||||
var rest = this.GetRestOfLineLength(font, tokenIndex, lineIndex) * scale;
|
||||
if (alignment == TextAlignment.Center)
|
||||
rest /= 2;
|
||||
return -rest;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private float GetRestOfLineLength(GenericFont font, int tokenIndex, int lineIndex) {
|
||||
var token = this.Tokens[tokenIndex];
|
||||
var ret = font.MeasureString(token.SplitDisplayString[lineIndex], true).X;
|
||||
if (lineIndex >= token.SplitDisplayString.Length - 1) {
|
||||
// the line ends somewhere in or after the next token
|
||||
for (var i = tokenIndex + 1; i < this.Tokens.Length; i++) {
|
||||
var other = this.Tokens[i];
|
||||
if (other.SplitDisplayString.Length > 1) {
|
||||
// the line ends in this token
|
||||
ret += font.MeasureString(other.SplitDisplayString[0]).X;
|
||||
break;
|
||||
} else {
|
||||
// the line doesn't end in this token, so add it fully
|
||||
ret += font.MeasureString(other.DisplayString).X;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void RecalculateTokenData(GenericFont font, TextAlignment alignment) {
|
||||
// split display strings
|
||||
foreach (var token in this.Tokens)
|
||||
token.SplitDisplayString = token.DisplayString.Split('\n');
|
||||
|
||||
// token areas
|
||||
var innerOffset = new Vector2(this.GetInnerOffsetX(font, 0, 0, 1, alignment), 0);
|
||||
for (var t = 0; t < this.Tokens.Length; t++) {
|
||||
var token = this.Tokens[t];
|
||||
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]);
|
||||
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
||||
var size = font.MeasureString(token.SplitDisplayString[l]);
|
||||
var rect = new RectangleF(innerOffset, size);
|
||||
if (!rect.IsEmpty)
|
||||
area.Add(rect);
|
||||
|
||||
if (i < split.Length - 1) {
|
||||
innerOffset.X = 0;
|
||||
if (l < token.SplitDisplayString.Length - 1) {
|
||||
innerOffset.X = this.GetInnerOffsetX(font, t, l + 1, 1, alignment);
|
||||
innerOffset.Y += font.LineHeight;
|
||||
} else {
|
||||
innerOffset.X += size.X;
|
||||
|
|
Loading…
Reference in a new issue