diff --git a/Demos/UiDemo.cs b/Demos/UiDemo.cs
index c2e7735..7379a03 100644
--- a/Demos/UiDemo.cs
+++ b/Demos/UiDemo.cs
@@ -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 left aligned text, right aligned text and center 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(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:"));
diff --git a/MLEM.Ui/Elements/Paragraph.cs b/MLEM.Ui/Elements/Paragraph.cs
index e6adf4d..3f5276a 100644
--- a/MLEM.Ui/Elements/Paragraph.cs
+++ b/MLEM.Ui/Elements/Paragraph.cs
@@ -16,7 +16,6 @@ namespace MLEM.Ui.Elements {
///
public class Paragraph : Element {
- private string text;
///
/// The font that this paragraph draws text with.
/// To set its bold and italic font, use and .
@@ -84,6 +83,20 @@ namespace MLEM.Ui.Elements {
/// By default, is executed.
///
public Action LinkAction;
+ ///
+ /// The that this paragraph's text should be rendered with
+ ///
+ public TextAlignment Alignment {
+ get => this.alignment;
+ set {
+ this.alignment = value;
+ this.SetAreaDirty();
+ this.TokenizedText = null;
+ }
+ }
+
+ private string text;
+ private TextAlignment alignment;
///
/// Creates a new paragraph with the given settings.
@@ -128,10 +141,10 @@ namespace MLEM.Ui.Elements {
///
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;
+ }
+
///
/// A delegate method used for
///
@@ -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();
}
diff --git a/MLEM/Font/GenericFont.cs b/MLEM/Font/GenericFont.cs
index 14fe4e7..17f0c6c 100644
--- a/MLEM/Font/GenericFont.cs
+++ b/MLEM/Font/GenericFont.cs
@@ -70,13 +70,13 @@ namespace MLEM.Font {
}
///
- 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;
+ }
+
}
}
\ No newline at end of file
diff --git a/MLEM/Formatting/TextAlignment.cs b/MLEM/Formatting/TextAlignment.cs
new file mode 100644
index 0000000..3f7876a
--- /dev/null
+++ b/MLEM/Formatting/TextAlignment.cs
@@ -0,0 +1,22 @@
+namespace MLEM.Formatting {
+ ///
+ /// An enumeration that represents a set of alignment options for objects and MLEM.Ui paragraphs.
+ ///
+ public enum TextAlignment {
+
+ ///
+ /// Left alignment, which is also the default value
+ ///
+ Left,
+ ///
+ /// Center alignment
+ ///
+ Center,
+ ///
+ /// Right alignment.
+ /// In this alignment option, trailing spaces are ignored to ensure that visual alignment is consistent.
+ ///
+ Right
+
+ }
+}
\ No newline at end of file
diff --git a/MLEM/Formatting/TextFormatter.cs b/MLEM/Formatting/TextFormatter.cs
index 2a24fdb..6248ef0 100644
--- a/MLEM/Formatting/TextFormatter.cs
+++ b/MLEM/Formatting/TextFormatter.cs
@@ -67,8 +67,9 @@ namespace MLEM.Formatting {
///
/// 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.
/// The string to tokenize
+ /// 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.
///
- 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();
@@ -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());
}
///
diff --git a/MLEM/Formatting/Token.cs b/MLEM/Formatting/Token.cs
index c51d956..460e05f 100644
--- a/MLEM/Formatting/Token.cs
+++ b/MLEM/Formatting/Token.cs
@@ -33,6 +33,10 @@ namespace MLEM.Formatting {
///
public string DisplayString => this.ModifiedSubstring ?? this.Substring;
///
+ /// The , but split at newline characters
+ ///
+ public string[] SplitDisplayString { get; internal set; }
+ ///
/// The substring that this token contains, without the formatting codes removed.
///
public readonly string RawSubstring;
diff --git a/MLEM/Formatting/TokenizedString.cs b/MLEM/Formatting/TokenizedString.cs
index 153ccf7..a990ba2 100644
--- a/MLEM/Formatting/TokenizedString.cs
+++ b/MLEM/Formatting/TokenizedString.cs
@@ -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);
}
///
@@ -55,10 +55,11 @@ namespace MLEM.Formatting {
/// The font to use for width calculations
/// The maximum width, in display pixels based on the font and scale
/// The scale to use for width measurements
- public void Split(GenericFont font, float width, float scale) {
+ /// The text alignment that should be used for width calculations
+ 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);
}
///
@@ -70,12 +71,13 @@ namespace MLEM.Formatting {
/// The maximum width, in display pixels based on the font and scale
/// The scale to use for width measurements
/// The characters to add to the end of the string if it is too long
- public void Truncate(GenericFont font, float width, float scale, string ellipsis = "") {
+ /// The text alignment that should be used for width calculations
+ 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);
}
- ///
+ ///
public Vector2 Measure(GenericFont font) {
return font.MeasureString(this.DisplayString);
}
@@ -102,77 +104,116 @@ namespace MLEM.Formatting {
}
///
- 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();
- 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;