1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-24 13:38:34 +01:00

Compare commits

..

No commits in common. "1377941f1ac4c13a4b7e0baf4a3958ebe450355a" and "d1ce9412a25affda8c92578810c24a70e30e8819" have entirely different histories.

8 changed files with 74 additions and 199 deletions

View file

@ -6,7 +6,6 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions; using MLEM.Extensions;
using MLEM.Font; using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes; using MLEM.Formatting.Codes;
using MLEM.Misc; using MLEM.Misc;
using MLEM.Startup; using MLEM.Startup;
@ -192,14 +191,6 @@ namespace Demos {
this.root.AddChild(new VerticalSpace(3)); 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); 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 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:")); 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:"));

View file

@ -257,12 +257,12 @@ namespace MLEM.Ui.Elements {
public bool CanAutoAnchorsAttach = true; public bool CanAutoAnchorsAttach = true;
/// <summary> /// <summary>
/// Set this field to true to cause this element's width to be automatically calculated based on the area that its <see cref="Children"/> take up. /// Set this field to true to cause this element's width to be automatically calculated based on the area that its <see cref="Children"/> take up.
/// To use this element's <see cref="Size"/>'s X coordinate as a minimum or maximum width rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> or <see cref="TreatSizeAsMaximum"/> to true. /// To use this element's <see cref="Size"/>'s X coordinate as a minimum width rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> to true.
/// </summary> /// </summary>
public bool SetWidthBasedOnChildren; public bool SetWidthBasedOnChildren;
/// <summary> /// <summary>
/// Set this field to true to cause this element's height to be automatically calculated based on the area that its <see cref="Children"/> take up. /// Set this field to true to cause this element's height to be automatically calculated based on the area that its <see cref="Children"/> take up.
/// To use this element's <see cref="Size"/>'s Y coordinate as a minimum or maximum height rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> or <see cref="TreatSizeAsMaximum"/> to true. /// To use this element's <see cref="Size"/>'s Y coordinate as a minimum height rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> to true.
/// </summary> /// </summary>
public bool SetHeightBasedOnChildren; public bool SetHeightBasedOnChildren;
/// <summary> /// <summary>
@ -272,11 +272,6 @@ namespace MLEM.Ui.Elements {
/// </summary> /// </summary>
public bool TreatSizeAsMinimum; public bool TreatSizeAsMinimum;
/// <summary> /// <summary>
/// If this field is set to true, and <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/>are enabled, the resulting width or height weill always be less than or equal to this element's <see cref="Size"/>.
/// Note that this value only has an effect if <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled.
/// </summary>
public bool TreatSizeAsMaximum;
/// <summary>
/// Set this field to true to cause this element's final display area to never exceed that of its <see cref="Parent"/>. /// Set this field to true to cause this element's final display area to never exceed that of its <see cref="Parent"/>.
/// If the resulting area is too large, the size of this element is shrunk to fit the target area. /// If the resulting area is too large, the size of this element is shrunk to fit the target area.
/// This can be useful if an element should fill the remaining area of a parent exactly. /// This can be useful if an element should fill the remaining area of a parent exactly.
@ -666,12 +661,8 @@ namespace MLEM.Ui.Elements {
} }
} }
if (this.TreatSizeAsMinimum) { if (this.TreatSizeAsMinimum)
autoSize = Vector2.Max(autoSize, actualSize); autoSize = Vector2.Max(autoSize, actualSize);
} else if (this.TreatSizeAsMaximum) {
autoSize = Vector2.Min(autoSize, actualSize);
}
if (!autoSize.Equals(this.UnscrolledArea.Size, 0.01F)) { if (!autoSize.Equals(this.UnscrolledArea.Size, 0.01F)) {
recursion++; recursion++;
if (recursion >= 16) { if (recursion >= 16) {

View file

@ -16,6 +16,7 @@ namespace MLEM.Ui.Elements {
/// </summary> /// </summary>
public class Paragraph : Element { public class Paragraph : Element {
private string text;
/// <summary> /// <summary>
/// The font that this paragraph draws text with. /// 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"/>. /// To set its bold and italic font, use <see cref="GenericFont.Bold"/> and <see cref="GenericFont.Italic"/>.
@ -83,20 +84,6 @@ namespace MLEM.Ui.Elements {
/// By default, <see cref="MlemPlatform.OpenLinkOrFile"/> is executed. /// By default, <see cref="MlemPlatform.OpenLinkOrFile"/> is executed.
/// </summary> /// </summary>
public Action<Link, LinkCode> LinkAction; 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> /// <summary>
/// Creates a new paragraph with the given settings. /// Creates a new paragraph with the given settings.
@ -141,10 +128,10 @@ namespace MLEM.Ui.Elements {
/// <inheritdoc /> /// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) { public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
var pos = this.DisplayArea.Location + new Vector2(GetAlignmentOffset(), 0); var pos = this.DisplayArea.Location;
var sc = this.TextScale * this.TextScaleMultiplier * this.Scale; var sc = this.TextScale * this.TextScaleMultiplier * this.Scale;
var color = this.TextColor.OrDefault(Color.White) * alpha; var color = this.TextColor.OrDefault(Color.White) * alpha;
this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0, this.Alignment); this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0);
base.Draw(time, batch, alpha, blendState, samplerState, matrix); base.Draw(time, batch, alpha, blendState, samplerState, matrix);
} }
@ -164,7 +151,7 @@ namespace MLEM.Ui.Elements {
protected virtual void ParseText(Vector2 size) { protected virtual void ParseText(Vector2 size) {
if (this.TokenizedText == null) { if (this.TokenizedText == null) {
// tokenize the text // tokenize the text
this.TokenizedText = this.System.TextFormatter.Tokenize(this.RegularFont, this.Text, this.Alignment); this.TokenizedText = this.System.TextFormatter.Tokenize(this.RegularFont, this.Text);
// add links to the paragraph // add links to the paragraph
this.RemoveChildren(c => c is Link); this.RemoveChildren(c => c is Link);
@ -175,9 +162,9 @@ namespace MLEM.Ui.Elements {
var width = size.X - this.ScaledPadding.Width; var width = size.X - this.ScaledPadding.Width;
var scale = this.TextScale * this.TextScaleMultiplier * this.Scale; var scale = this.TextScale * this.TextScaleMultiplier * this.Scale;
if (this.TruncateIfLong) { if (this.TruncateIfLong) {
this.TokenizedText.Truncate(this.RegularFont, width, scale, this.Ellipsis, this.Alignment); this.TokenizedText.Truncate(this.RegularFont, width, scale, this.Ellipsis);
} else { } else {
this.TokenizedText.Split(this.RegularFont, width, scale, this.Alignment); this.TokenizedText.Split(this.RegularFont, width, scale);
} }
} }
@ -186,16 +173,6 @@ namespace MLEM.Ui.Elements {
this.Text = this.GetTextCallback(this); 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> /// <summary>
/// A delegate method used for <see cref="Paragraph.GetTextCallback"/> /// A delegate method used for <see cref="Paragraph.GetTextCallback"/>
/// </summary> /// </summary>
@ -237,7 +214,7 @@ namespace MLEM.Ui.Elements {
public override void ForceUpdateArea() { public override void ForceUpdateArea() {
// set the position offset and size to the token's first area // set the position offset and size to the token's first area
var area = this.Token.GetArea(Vector2.Zero, this.textScale).First(); var area = this.Token.GetArea(Vector2.Zero, this.textScale).First();
this.PositionOffset = area.Location + new Vector2(((Paragraph) this.Parent).GetAlignmentOffset() / this.Parent.Scale, 0); this.PositionOffset = area.Location;
this.Size = area.Size; this.Size = area.Size;
base.ForceUpdateArea(); base.ForceUpdateArea();
} }

View file

@ -70,13 +70,13 @@ namespace MLEM.Font {
} }
///<inheritdoc cref="SpriteFont.MeasureString(string)"/> ///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) { public Vector2 MeasureString(string text) {
var size = Vector2.Zero; var size = Vector2.Zero;
if (text.Length <= 0) if (text.Length <= 0)
return size; return size;
var xOffset = 0F; var xOffset = 0F;
for (var i = 0; i < text.Length; i++) { foreach (var c in text) {
switch (text[i]) { switch (c) {
case '\n': case '\n':
xOffset = 0; xOffset = 0;
size.Y += this.LineHeight; size.Y += this.LineHeight;
@ -90,16 +90,8 @@ namespace MLEM.Font {
case Zwsp: case Zwsp:
// don't add width for a zero-width space // don't add width for a zero-width space
break; 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: default:
xOffset += this.MeasureChar(text[i]).X; xOffset += this.MeasureChar(c).X;
break; break;
} }
// increase x size if this line is the longest // increase x size if this line is the longest
@ -194,13 +186,5 @@ namespace MLEM.Font {
return ret.ToString(); 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;
}
} }
} }

View file

@ -1,22 +0,0 @@
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
}
}

View file

@ -67,9 +67,8 @@ namespace MLEM.Formatting {
/// </summary> /// </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="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="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> /// <returns></returns>
public TokenizedString Tokenize(GenericFont font, string s, TextAlignment alignment = TextAlignment.Left) { public TokenizedString Tokenize(GenericFont font, string s) {
// resolve macros // resolve macros
s = this.ResolveMacros(s); s = this.ResolveMacros(s);
var tokens = new List<Token>(); var tokens = new List<Token>();
@ -102,7 +101,7 @@ namespace MLEM.Formatting {
codes.RemoveAll(c => c.EndsHere(next)); codes.RemoveAll(c => c.EndsHere(next));
codes.Add(next); codes.Add(next);
} }
return new TokenizedString(font, alignment, 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());
} }
/// <summary> /// <summary>

View file

@ -33,10 +33,6 @@ namespace MLEM.Formatting {
/// </summary> /// </summary>
public string DisplayString => this.ModifiedSubstring ?? this.Substring; public string DisplayString => this.ModifiedSubstring ?? this.Substring;
/// <summary> /// <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. /// The substring that this token contains, without the formatting codes removed.
/// </summary> /// </summary>
public readonly string RawSubstring; public readonly string RawSubstring;

View file

@ -38,13 +38,13 @@ namespace MLEM.Formatting {
public readonly Code[] AllCodes; public readonly Code[] AllCodes;
private string modifiedString; private string modifiedString;
internal TokenizedString(GenericFont font, TextAlignment alignment, string rawString, string strg, Token[] tokens) { internal TokenizedString(GenericFont font, string rawString, string strg, Token[] tokens) {
this.RawString = rawString; this.RawString = rawString;
this.String = strg; this.String = strg;
this.Tokens = tokens; this.Tokens = tokens;
// since a code can be present in multiple tokens, we use Distinct here // since a code can be present in multiple tokens, we use Distinct here
this.AllCodes = tokens.SelectMany(t => t.AppliedCodes).Distinct().ToArray(); this.AllCodes = tokens.SelectMany(t => t.AppliedCodes).Distinct().ToArray();
this.RecalculateTokenData(font, alignment); this.CalculateTokenAreas(font);
} }
/// <summary> /// <summary>
@ -55,11 +55,10 @@ namespace MLEM.Formatting {
/// <param name="font">The font to use for width calculations</param> /// <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="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="scale">The scale to use for width measurements</param>
/// <param name="alignment">The text alignment that should be used for width calculations</param> public void Split(GenericFont font, float width, float scale) {
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 // 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.modifiedString = font.SplitString(this.String, width, scale);
this.StoreModifiedSubstrings(font, alignment); this.StoreModifiedSubstrings(font);
} }
/// <summary> /// <summary>
@ -71,13 +70,12 @@ namespace MLEM.Formatting {
/// <param name="width">The maximum width, in display pixels based on the font and scale</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> /// <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> /// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
/// <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 = "") {
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.modifiedString = font.TruncateString(this.String, width, scale, false, ellipsis);
this.StoreModifiedSubstrings(font, alignment); this.StoreModifiedSubstrings(font);
} }
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/> /// <inheritdoc cref="GenericFont.MeasureString(string)"/>
public Vector2 Measure(GenericFont font) { public Vector2 Measure(GenericFont font) {
return font.MeasureString(this.DisplayString); return font.MeasureString(this.DisplayString);
} }
@ -104,37 +102,34 @@ namespace MLEM.Formatting {
} }
/// <inheritdoc cref="GenericFont.DrawString(SpriteBatch,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/> /// <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, TextAlignment alignment = TextAlignment.Left) { public void Draw(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
var innerOffset = new Vector2(this.GetInnerOffsetX(font, 0, 0, scale, alignment), 0); var innerOffset = new Vector2();
for (var t = 0; t < this.Tokens.Length; t++) { foreach (var token in this.Tokens) {
var token = this.Tokens[t];
var drawFont = token.GetFont(font) ?? font; var drawFont = token.GetFont(font) ?? font;
var drawColor = token.GetColor(color) ?? color; var drawColor = token.GetColor(color) ?? color;
for (var l = 0; l < token.SplitDisplayString.Length; l++) { for (var i = 0; i < token.DisplayString.Length; i++) {
var line = token.SplitDisplayString[l]; var c = token.DisplayString[i];
for (var i = 0; i < line.Length; i++) { if (c == '\n') {
var c = line[i]; innerOffset.X = 0;
if (l == 0 && i == 0) innerOffset.Y += font.LineHeight * scale;
}
if (i == 0)
token.DrawSelf(time, batch, pos + innerOffset, font, color, scale, depth); token.DrawSelf(time, batch, pos + innerOffset, font, color, scale, depth);
var cString = c.ToCachedString(); var cString = c.ToCachedString();
token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth); token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth);
innerOffset.X += font.MeasureString(cString).X * scale; 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;
}
}
} }
} }
private void StoreModifiedSubstrings(GenericFont font, TextAlignment alignment) { private void StoreModifiedSubstrings(GenericFont font) {
if (this.Tokens.Length == 1) {
// skip substring logic for unformatted text // skip substring logic for unformatted text
if (this.Tokens.Length == 1) {
this.Tokens[0].ModifiedSubstring = this.modifiedString; this.Tokens[0].ModifiedSubstring = this.modifiedString;
} else { return;
}
// this is basically a substring function that ignores added newlines for indexing // this is basically a substring function that ignores added newlines for indexing
var index = 0; var index = 0;
var currToken = 0; var currToken = 0;
@ -161,59 +156,23 @@ namespace MLEM.Formatting {
this.Tokens[currToken++].ModifiedSubstring = ret.ToString(); this.Tokens[currToken++].ModifiedSubstring = ret.ToString();
while (currToken < this.Tokens.Length) while (currToken < this.Tokens.Length)
this.Tokens[currToken++].ModifiedSubstring = string.Empty; 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 area = new List<RectangleF>();
for (var l = 0; l < token.SplitDisplayString.Length; l++) { var split = token.DisplayString.Split('\n');
var size = font.MeasureString(token.SplitDisplayString[l]); for (var i = 0; i < split.Length; i++) {
var size = font.MeasureString(split[i]);
var rect = new RectangleF(innerOffset, size); var rect = new RectangleF(innerOffset, size);
if (!rect.IsEmpty) if (!rect.IsEmpty)
area.Add(rect); area.Add(rect);
if (l < token.SplitDisplayString.Length - 1) { if (i < split.Length - 1) {
innerOffset.X = this.GetInnerOffsetX(font, t, l + 1, 1, alignment); innerOffset.X = 0;
innerOffset.Y += font.LineHeight; innerOffset.Y += font.LineHeight;
} else { } else {
innerOffset.X += size.X; innerOffset.X += size.X;