mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-24 21:48:35 +01:00
Compare commits
No commits in common. "9aef994c510ba0c59845442f5210cfe43842945f" and "fd5b83eaa0c9ec8824d38e632d96dbd74c9ff7e4" have entirely different histories.
9aef994c51
...
fd5b83eaa0
6 changed files with 50 additions and 185 deletions
|
@ -10,13 +10,8 @@ Jump to version:
|
||||||
### MLEM
|
### MLEM
|
||||||
Additions
|
Additions
|
||||||
- Added a strikethrough formatting code
|
- Added a strikethrough formatting code
|
||||||
- Added GenericFont SplitStringSeparate which differentiates between existing newline characters and splits due to maximum width
|
|
||||||
|
|
||||||
### MLEM.Ui
|
### MLEM.Ui
|
||||||
Additions
|
|
||||||
- Allow specifying a maximum amount of characters for a TextField
|
|
||||||
- Added a multiline editing mode to TextField
|
|
||||||
|
|
||||||
Improvements
|
Improvements
|
||||||
- Cache TokenizedString inner offsets for non-Left text alignments to improve performance
|
- Cache TokenizedString inner offsets for non-Left text alignments to improve performance
|
||||||
|
|
||||||
|
|
|
@ -95,8 +95,8 @@ namespace Demos {
|
||||||
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 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 VerticalSpace(3));
|
||||||
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Multiline text input:", true));
|
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Text input:", true));
|
||||||
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 50), multiline: true) {
|
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10)) {
|
||||||
PositionOffset = new Vector2(0, 1),
|
PositionOffset = new Vector2(0, 1),
|
||||||
PlaceholderText = "Click here to input text"
|
PlaceholderText = "Click here to input text"
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,7 @@ namespace MLEM.Ui.Elements {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly Rule DefaultRule = (field, add) => {
|
public static readonly Rule DefaultRule = (field, add) => {
|
||||||
foreach (var c in add) {
|
foreach (var c in add) {
|
||||||
if (char.IsControl(c) && (!field.Multiline || c != '\n'))
|
if (char.IsControl(c))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -135,12 +135,13 @@ namespace MLEM.Ui.Elements {
|
||||||
/// This is always between 0 and the <see cref="string.Length"/> of <see cref="Text"/>
|
/// This is always between 0 and the <see cref="string.Length"/> of <see cref="Text"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int CaretPos {
|
public int CaretPos {
|
||||||
get => this.caretPos;
|
get {
|
||||||
|
this.CaretPos = MathHelper.Clamp(this.caretPos, 0, this.text.Length);
|
||||||
|
return this.caretPos;
|
||||||
|
}
|
||||||
set {
|
set {
|
||||||
var val = MathHelper.Clamp(value, 0, this.text.Length);
|
if (this.caretPos != value) {
|
||||||
if (this.caretPos != val) {
|
this.caretPos = value;
|
||||||
this.caretPos = val;
|
|
||||||
this.caretBlinkTimer = 0;
|
|
||||||
this.HandleTextChange(false);
|
this.HandleTextChange(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,23 +158,6 @@ namespace MLEM.Ui.Elements {
|
||||||
this.HandleTextChange(false);
|
this.HandleTextChange(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// <summary>
|
|
||||||
/// The maximum amount of characters that can be input into this text field.
|
|
||||||
/// If this is set, the length of <see cref="Text"/> will never exceed this value.
|
|
||||||
/// </summary>
|
|
||||||
public int? MaximumCharacters;
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this text field should support multi-line editing.
|
|
||||||
/// If this is true, pressing <see cref="Keys.Enter"/> will insert a new line into the <see cref="Text"/> if the <see cref="InputRule"/> allows it.
|
|
||||||
/// Additionally, text will be rendered with horizontal soft wraps, and lines that are outside of the text field's bounds will be hidden.
|
|
||||||
/// </summary>
|
|
||||||
public bool Multiline {
|
|
||||||
get => this.multiline;
|
|
||||||
set {
|
|
||||||
this.multiline = value;
|
|
||||||
this.HandleTextChange(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly StringBuilder text = new StringBuilder();
|
private readonly StringBuilder text = new StringBuilder();
|
||||||
|
|
||||||
|
@ -181,9 +165,7 @@ namespace MLEM.Ui.Elements {
|
||||||
private double caretBlinkTimer;
|
private double caretBlinkTimer;
|
||||||
private string displayedText;
|
private string displayedText;
|
||||||
private int textOffset;
|
private int textOffset;
|
||||||
private int lineOffset;
|
|
||||||
private int caretPos;
|
private int caretPos;
|
||||||
private bool multiline;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new text field with the given settings
|
/// Creates a new text field with the given settings
|
||||||
|
@ -193,10 +175,8 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <param name="rule">The text field's input rule</param>
|
/// <param name="rule">The text field's input rule</param>
|
||||||
/// <param name="font">The font to use for drawing text</param>
|
/// <param name="font">The font to use for drawing text</param>
|
||||||
/// <param name="text">The text that the text field should contain by default</param>
|
/// <param name="text">The text that the text field should contain by default</param>
|
||||||
/// <param name="multiline">Whether the text field should support multi-line editing</param>
|
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null) : base(anchor, size) {
|
||||||
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null, bool multiline = false) : base(anchor, size) {
|
|
||||||
this.InputRule = rule ?? DefaultRule;
|
this.InputRule = rule ?? DefaultRule;
|
||||||
this.Multiline = multiline;
|
|
||||||
if (font != null)
|
if (font != null)
|
||||||
this.Font.Set(font);
|
this.Font.Set(font);
|
||||||
if (text != null)
|
if (text != null)
|
||||||
|
@ -215,8 +195,6 @@ namespace MLEM.Ui.Elements {
|
||||||
}
|
}
|
||||||
} else if (key == Keys.Delete) {
|
} else if (key == Keys.Delete) {
|
||||||
this.RemoveText(this.CaretPos, 1);
|
this.RemoveText(this.CaretPos, 1);
|
||||||
} else if (this.Multiline && key == Keys.Enter) {
|
|
||||||
this.InsertText('\n');
|
|
||||||
} else {
|
} else {
|
||||||
this.InsertText(character);
|
this.InsertText(character);
|
||||||
}
|
}
|
||||||
|
@ -228,7 +206,7 @@ namespace MLEM.Ui.Elements {
|
||||||
var title = this.MobileTitle ?? this.PlaceholderText;
|
var title = this.MobileTitle ?? this.PlaceholderText;
|
||||||
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
|
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
|
||||||
if (result != null)
|
if (result != null)
|
||||||
this.SetText(this.Multiline ? result : result.Replace('\n', ' '), true);
|
this.SetText(result.Replace('\n', ' '), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,83 +214,26 @@ namespace MLEM.Ui.Elements {
|
||||||
// not initialized yet
|
// not initialized yet
|
||||||
if (!this.Font.HasValue())
|
if (!this.Font.HasValue())
|
||||||
return;
|
return;
|
||||||
|
var length = this.Font.Value.MeasureString(this.text.ToString()).X * this.TextScale;
|
||||||
var maxWidth = this.DisplayArea.Width / this.Scale - this.TextOffsetX * 2;
|
var maxWidth = this.DisplayArea.Width / this.Scale - this.TextOffsetX * 2;
|
||||||
if (this.Multiline) {
|
if (length > maxWidth) {
|
||||||
// soft wrap if we're multiline
|
|
||||||
this.displayedText = this.Font.Value.SplitString(this.text.ToString(), maxWidth, this.TextScale);
|
|
||||||
var maxHeight = this.DisplayArea.Height / this.Scale - this.TextOffsetX * 2;
|
|
||||||
if (this.Font.Value.MeasureString(this.displayedText).Y * this.TextScale > maxHeight) {
|
|
||||||
var maxLines = (maxHeight / (this.Font.Value.LineHeight * this.TextScale)).Floor();
|
|
||||||
// calculate what line the caret is on
|
|
||||||
var caretLine = 0;
|
|
||||||
var originalIndex = 0;
|
|
||||||
var addedLineBreaks = 0;
|
|
||||||
for (var i = 0; i <= this.CaretPos + addedLineBreaks - 1 && i < this.displayedText.Length; i++) {
|
|
||||||
if (this.displayedText[i] == '\n') {
|
|
||||||
caretLine++;
|
|
||||||
if (this.text[originalIndex] != '\n') {
|
|
||||||
addedLineBreaks++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
originalIndex++;
|
|
||||||
}
|
|
||||||
// when we're multiline, the text offset is measured in lines
|
|
||||||
if (this.lineOffset > caretLine) {
|
|
||||||
// if we're moving up
|
|
||||||
this.lineOffset = caretLine;
|
|
||||||
} else if (caretLine >= maxLines) {
|
|
||||||
// if we're moving down
|
|
||||||
var limit = caretLine - (maxLines - 1);
|
|
||||||
if (limit > this.lineOffset)
|
|
||||||
this.lineOffset = limit;
|
|
||||||
}
|
|
||||||
// calculate resulting string
|
|
||||||
var ret = new StringBuilder();
|
|
||||||
var lines = 0;
|
|
||||||
originalIndex = 0;
|
|
||||||
for (var i = 0; i < this.displayedText.Length; i++) {
|
|
||||||
if (lines >= this.lineOffset) {
|
|
||||||
if (ret.Length <= 0)
|
|
||||||
this.textOffset = originalIndex;
|
|
||||||
ret.Append(this.displayedText[i]);
|
|
||||||
}
|
|
||||||
if (this.displayedText[i] == '\n') {
|
|
||||||
lines++;
|
|
||||||
if (this.text[originalIndex] == '\n')
|
|
||||||
originalIndex++;
|
|
||||||
} else {
|
|
||||||
originalIndex++;
|
|
||||||
}
|
|
||||||
if (lines - this.lineOffset >= maxLines)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.displayedText = ret.ToString();
|
|
||||||
} else {
|
|
||||||
this.lineOffset = 0;
|
|
||||||
this.textOffset = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// not multiline, so scroll horizontally based on caret position
|
|
||||||
if (this.Font.Value.MeasureString(this.text.ToString()).X * this.TextScale > maxWidth) {
|
|
||||||
if (this.textOffset > this.CaretPos) {
|
|
||||||
// if we're moving the caret to the left
|
// if we're moving the caret to the left
|
||||||
|
if (this.textOffset > this.CaretPos) {
|
||||||
this.textOffset = this.CaretPos;
|
this.textOffset = this.CaretPos;
|
||||||
} else {
|
} else {
|
||||||
// if we're moving the caret to the right
|
// if we're moving the caret to the right
|
||||||
var importantArea = this.text.ToString(this.textOffset, Math.Min(this.CaretPos, this.text.Length) - this.textOffset);
|
var importantArea = this.text.ToString(this.textOffset, Math.Min(this.CaretPos, this.text.Length) - this.textOffset);
|
||||||
var bound = this.CaretPos - this.Font.Value.TruncateString(importantArea, maxWidth, this.TextScale, true).Length;
|
var bound = this.CaretPos - this.Font.Value.TruncateString(importantArea, maxWidth, this.TextScale, true).Length;
|
||||||
if (this.textOffset < bound)
|
if (this.textOffset < bound) {
|
||||||
this.textOffset = bound;
|
this.textOffset = bound;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
var visible = this.text.ToString(this.textOffset, this.text.Length - this.textOffset);
|
var visible = this.text.ToString(this.textOffset, this.text.Length - this.textOffset);
|
||||||
this.displayedText = this.Font.Value.TruncateString(visible, maxWidth, this.TextScale);
|
this.displayedText = this.Font.Value.TruncateString(visible, maxWidth, this.TextScale);
|
||||||
} else {
|
} else {
|
||||||
this.displayedText = this.Text;
|
this.displayedText = this.Text;
|
||||||
this.textOffset = 0;
|
this.textOffset = 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (this.MaskingCharacter != null)
|
if (this.MaskingCharacter != null)
|
||||||
this.displayedText = new string(this.MaskingCharacter.Value, this.displayedText.Length);
|
this.displayedText = new string(this.MaskingCharacter.Value, this.displayedText.Length);
|
||||||
|
|
||||||
|
@ -367,38 +288,13 @@ namespace MLEM.Ui.Elements {
|
||||||
|
|
||||||
if (this.displayedText != null) {
|
if (this.displayedText != null) {
|
||||||
var lineHeight = this.Font.Value.LineHeight * this.TextScale * this.Scale;
|
var lineHeight = this.Font.Value.LineHeight * this.TextScale * this.Scale;
|
||||||
var offset = new Vector2(
|
var textPos = this.DisplayArea.Location + new Vector2(this.TextOffsetX * this.Scale, this.DisplayArea.Height / 2 - lineHeight / 2);
|
||||||
this.TextOffsetX * this.Scale,
|
|
||||||
this.Multiline ? this.TextOffsetX * this.Scale : this.DisplayArea.Height / 2 - lineHeight / 2);
|
|
||||||
var textPos = this.DisplayArea.Location + offset;
|
|
||||||
if (this.text.Length > 0 || this.IsSelected) {
|
if (this.text.Length > 0 || this.IsSelected) {
|
||||||
var textColor = this.TextColor.OrDefault(Color.White);
|
var textColor = this.TextColor.OrDefault(Color.White);
|
||||||
this.Font.Value.DrawString(batch, this.displayedText, textPos, textColor * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
|
this.Font.Value.DrawString(batch, this.displayedText, textPos, textColor * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
|
||||||
|
if (this.IsSelected && this.caretBlinkTimer >= 0.5F) {
|
||||||
if (this.IsSelected && this.caretBlinkTimer < 0.5F) {
|
var textSize = this.Font.Value.MeasureString(this.displayedText.Substring(0, this.CaretPos - this.textOffset)) * this.TextScale * this.Scale;
|
||||||
var caretDrawPos = textPos;
|
batch.Draw(batch.GetBlankTexture(), new RectangleF(textPos.X + textSize.X, textPos.Y, this.CaretWidth * this.Scale, lineHeight), null, textColor * alpha);
|
||||||
if (this.Multiline) {
|
|
||||||
var lines = 0;
|
|
||||||
var lastLineBreak = 0;
|
|
||||||
var originalIndex = 0;
|
|
||||||
var addedLineBreaks = 0;
|
|
||||||
for (var i = 0; i <= this.CaretPos - this.textOffset + addedLineBreaks - 1 && i < this.displayedText.Length; i++) {
|
|
||||||
if (this.displayedText[i] == '\n') {
|
|
||||||
lines++;
|
|
||||||
lastLineBreak = i;
|
|
||||||
if (this.text[originalIndex] != '\n') {
|
|
||||||
addedLineBreaks++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
originalIndex++;
|
|
||||||
}
|
|
||||||
var sub = this.displayedText.Substring(lastLineBreak, this.CaretPos - this.textOffset + addedLineBreaks - lastLineBreak);
|
|
||||||
caretDrawPos += new Vector2(this.Font.Value.MeasureString(sub).X * this.TextScale * this.Scale, lineHeight * lines);
|
|
||||||
} else {
|
|
||||||
caretDrawPos.X += this.Font.Value.MeasureString(this.displayedText.Substring(0, this.CaretPos - this.textOffset)).X * this.TextScale * this.Scale;
|
|
||||||
}
|
|
||||||
batch.Draw(batch.GetBlankTexture(), new RectangleF(caretDrawPos, new Vector2(this.CaretWidth * this.Scale, lineHeight)), null, textColor * alpha);
|
|
||||||
}
|
}
|
||||||
} else if (this.PlaceholderText != null) {
|
} else if (this.PlaceholderText != null) {
|
||||||
this.Font.Value.DrawString(batch, this.PlaceholderText, textPos, this.PlaceholderColor.OrDefault(Color.Gray) * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
|
this.Font.Value.DrawString(batch, this.PlaceholderText, textPos, this.PlaceholderColor.OrDefault(Color.Gray) * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
|
||||||
|
@ -409,40 +305,33 @@ namespace MLEM.Ui.Elements {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replaces this text field's text with the given text.
|
/// Replaces this text field's text with the given text.
|
||||||
/// If the resulting <see cref="Text"/> exceeds <see cref="MaximumCharacters"/>, the end will be cropped to fit.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="text">The new text</param>
|
/// <param name="text">The new text</param>
|
||||||
/// <param name="removeMismatching">If any characters that don't match the <see cref="InputRule"/> should be left out</param>
|
/// <param name="removeMismatching">If any characters that don't match the <see cref="InputRule"/> should be left out</param>
|
||||||
public void SetText(object text, bool removeMismatching = false) {
|
public void SetText(object text, bool removeMismatching = false) {
|
||||||
var strg = text?.ToString() ?? string.Empty;
|
|
||||||
if (removeMismatching) {
|
if (removeMismatching) {
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
foreach (var c in strg) {
|
foreach (var c in text.ToString()) {
|
||||||
if (this.InputRule(this, c.ToCachedString()))
|
if (this.InputRule(this, c.ToCachedString()))
|
||||||
result.Append(c);
|
result.Append(c);
|
||||||
}
|
}
|
||||||
strg = result.ToString();
|
text = result.ToString();
|
||||||
} else if (!this.InputRule(this, strg))
|
} else if (!this.InputRule(this, text.ToString()))
|
||||||
return;
|
return;
|
||||||
if (this.MaximumCharacters != null && strg.Length > this.MaximumCharacters)
|
|
||||||
strg = strg.Substring(0, this.MaximumCharacters.Value);
|
|
||||||
this.text.Clear();
|
this.text.Clear();
|
||||||
this.text.Append(strg);
|
this.text.Append(text);
|
||||||
this.CaretPos = this.text.Length;
|
this.CaretPos = this.text.Length;
|
||||||
this.HandleTextChange();
|
this.HandleTextChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Inserts the given text at the <see cref="CaretPos"/>.
|
/// Inserts the given text at the <see cref="CaretPos"/>
|
||||||
/// If the resulting <see cref="Text"/> exceeds <see cref="MaximumCharacters"/>, the end will be cropped to fit.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="text">The text to insert</param>
|
/// <param name="text">The text to insert</param>
|
||||||
public void InsertText(object text) {
|
public void InsertText(object text) {
|
||||||
var strg = text.ToString();
|
var strg = text.ToString();
|
||||||
if (!this.InputRule(this, strg))
|
if (!this.InputRule(this, strg))
|
||||||
return;
|
return;
|
||||||
if (this.MaximumCharacters != null && this.text.Length + strg.Length > this.MaximumCharacters)
|
|
||||||
strg = strg.Substring(0, this.MaximumCharacters.Value - this.text.Length);
|
|
||||||
this.text.Insert(this.CaretPos, strg);
|
this.text.Insert(this.CaretPos, strg);
|
||||||
this.CaretPos += strg.Length;
|
this.CaretPos += strg.Length;
|
||||||
this.HandleTextChange();
|
this.HandleTextChange();
|
||||||
|
@ -457,8 +346,6 @@ namespace MLEM.Ui.Elements {
|
||||||
if (index < 0 || index >= this.text.Length)
|
if (index < 0 || index >= this.text.Length)
|
||||||
return;
|
return;
|
||||||
this.text.Remove(index, length);
|
this.text.Remove(index, length);
|
||||||
// ensure that caret pos is still in bounds
|
|
||||||
this.CaretPos = this.CaretPos;
|
|
||||||
this.HandleTextChange();
|
this.HandleTextChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
@ -161,36 +160,22 @@ namespace MLEM.Font {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Splits a string to a given maximum width, adding newline characters between each line.
|
/// Splits a string to a given maximum width, adding newline characters between each line.
|
||||||
/// Also splits long words and supports zero-width spaces and takes into account existing newline characters in the passed <paramref name="text"/>.
|
/// Also splits long words and supports zero-width spaces.
|
||||||
/// See <see cref="SplitStringSeparate"/> for a method that differentiates between existing newline characters and splits due to maximum width.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="text">The text to split into multiple lines</param>
|
/// <param name="text">The text to split into multiple lines</param>
|
||||||
/// <param name="width">The maximum width that each line should have</param>
|
/// <param name="width">The maximum width that each line should have</param>
|
||||||
/// <param name="scale">The scale to use for width measurements</param>
|
/// <param name="scale">The scale to use for width measurements</param>
|
||||||
/// <returns>The split string, containing newline characters at each new line</returns>
|
/// <returns>The split string, containing newline characters at each new line</returns>
|
||||||
public string SplitString(string text, float width, float scale) {
|
public string SplitString(string text, float width, float scale) {
|
||||||
return string.Join("\n", this.SplitStringSeparate(text, width, scale));
|
var ret = new StringBuilder();
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Splits a string to a given maximum width and returns each split section as a separate string.
|
|
||||||
/// Note that existing new lines are taken into account for line length, but not split in the resulting strings.
|
|
||||||
/// This method differs from <see cref="SplitString"/> in that it differentiates between pre-existing newline characters and splits due to maximum width.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">The text to split into multiple lines</param>
|
|
||||||
/// <param name="width">The maximum width that each line should have</param>
|
|
||||||
/// <param name="scale">The scale to use for width measurements</param>
|
|
||||||
/// <returns>The split string as an enumerable of split sections</returns>
|
|
||||||
public IEnumerable<string> SplitStringSeparate(string text, float width, float scale) {
|
|
||||||
var currWidth = 0F;
|
var currWidth = 0F;
|
||||||
var lastSpaceIndex = -1;
|
var lastSpaceIndex = -1;
|
||||||
var widthSinceLastSpace = 0F;
|
var widthSinceLastSpace = 0F;
|
||||||
var curr = new StringBuilder();
|
|
||||||
for (var i = 0; i < text.Length; i++) {
|
for (var i = 0; i < text.Length; i++) {
|
||||||
var c = text[i];
|
var c = text[i];
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
// fake split at pre-defined new lines
|
// split at pre-defined new lines
|
||||||
curr.Append(c);
|
ret.Append(c);
|
||||||
lastSpaceIndex = -1;
|
lastSpaceIndex = -1;
|
||||||
widthSinceLastSpace = 0;
|
widthSinceLastSpace = 0;
|
||||||
currWidth = 0;
|
currWidth = 0;
|
||||||
|
@ -198,19 +183,17 @@ namespace MLEM.Font {
|
||||||
var cWidth = this.MeasureString(c.ToCachedString()).X * scale;
|
var cWidth = this.MeasureString(c.ToCachedString()).X * scale;
|
||||||
if (c == ' ' || c == OneEmSpace || c == Zwsp) {
|
if (c == ' ' || c == OneEmSpace || c == Zwsp) {
|
||||||
// remember the location of this (breaking!) space
|
// remember the location of this (breaking!) space
|
||||||
lastSpaceIndex = curr.Length;
|
lastSpaceIndex = ret.Length;
|
||||||
widthSinceLastSpace = 0;
|
widthSinceLastSpace = 0;
|
||||||
} else if (currWidth + cWidth >= width) {
|
} else if (currWidth + cWidth >= width) {
|
||||||
// check if this line contains a space
|
// check if this line contains a space
|
||||||
if (lastSpaceIndex < 0) {
|
if (lastSpaceIndex < 0) {
|
||||||
// if there is no last space, the word is longer than a line so we split here
|
// if there is no last space, the word is longer than a line so we split here
|
||||||
yield return curr.ToString();
|
ret.Append('\n');
|
||||||
currWidth = 0;
|
currWidth = 0;
|
||||||
curr.Clear();
|
|
||||||
} else {
|
} else {
|
||||||
// split after the last space
|
// split after the last space
|
||||||
yield return curr.ToString().Substring(0, lastSpaceIndex + 1);
|
ret.Insert(lastSpaceIndex + 1, '\n');
|
||||||
curr.Remove(0, lastSpaceIndex + 1);
|
|
||||||
// we need to restore the width accumulated since the last space for the new line
|
// we need to restore the width accumulated since the last space for the new line
|
||||||
currWidth = widthSinceLastSpace;
|
currWidth = widthSinceLastSpace;
|
||||||
}
|
}
|
||||||
|
@ -221,11 +204,10 @@ namespace MLEM.Font {
|
||||||
// add current character
|
// add current character
|
||||||
currWidth += cWidth;
|
currWidth += cWidth;
|
||||||
widthSinceLastSpace += cWidth;
|
widthSinceLastSpace += cWidth;
|
||||||
curr.Append(c);
|
ret.Append(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (curr.Length > 0)
|
return ret.ToString();
|
||||||
yield return curr.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsTrailingSpace(string s, int index) {
|
private static bool IsTrailingSpace(string s, int index) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
|
@ -29,9 +29,9 @@ namespace Tests {
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRegularSplit() {
|
public void TestRegularSplit() {
|
||||||
Assert.AreEqual(this.font.SplitStringSeparate(
|
Assert.AreEqual(this.font.SplitString(
|
||||||
"Note that the default style does not contain any textures or font files and, as such, is quite bland. However, the default style is quite easy to override, as can be seen in the code for this demo.",
|
"Note that the default style does not contain any textures or font files and, as such, is quite bland. However, the default style is quite easy to override, as can be seen in the code for this demo.",
|
||||||
65, 0.1F), new[] {"Note that the default style does ", "not contain any textures or font ", "files and, as such, is quite ", "bland. However, the default ", "style is quite easy to override, ", "as can be seen in the code for ", "this demo."});
|
65, 0.1F), "Note that the default style does \nnot contain any textures or font \nfiles and, as such, is quite \nbland. However, the default \nstyle is quite easy to override, \nas can be seen in the code for \nthis demo.");
|
||||||
|
|
||||||
var formatted = this.formatter.Tokenize(this.font,
|
var formatted = this.formatter.Tokenize(this.font,
|
||||||
"Select the demo you want to see below using your mouse, touch input, your keyboard or a controller. Check the demos' <c CornflowerBlue><l https://github.com/Ellpeck/MLEM/tree/main/Demos>source code</l></c> for more in-depth explanations of their functionality or the <c CornflowerBlue><l https://mlem.ellpeck.de/>website</l></c> for tutorials and API documentation.");
|
"Select the demo you want to see below using your mouse, touch input, your keyboard or a controller. Check the demos' <c CornflowerBlue><l https://github.com/Ellpeck/MLEM/tree/main/Demos>source code</l></c> for more in-depth explanations of their functionality or the <c CornflowerBlue><l https://mlem.ellpeck.de/>website</l></c> for tutorials and API documentation.");
|
||||||
|
@ -55,16 +55,16 @@ namespace Tests {
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestLongLineSplit() {
|
public void TestLongLineSplit() {
|
||||||
var expectedDisplay = new[] {"This_is_a_really_long_line_to_s", "ee_if_splitting_without_spaces_", "works_properly._I_also_want_to_", "see_if_it_works_across_multiple", "_lines_or_just_on_the_first_one. ", "But after this, I want the text to ", "continue normally before ", "changing_back_to_being_really_", "long_oh_yes"};
|
const string expectedDisplay = "This_is_a_really_long_line_to_s\nee_if_splitting_without_spaces_\nworks_properly._I_also_want_to_\nsee_if_it_works_across_multiple\n_lines_or_just_on_the_first_one. \nBut after this, I want the text to \ncontinue normally before \nchanging_back_to_being_really_\nlong_oh_yes";
|
||||||
|
|
||||||
Assert.AreEqual(this.font.SplitStringSeparate(
|
Assert.AreEqual(this.font.SplitString(
|
||||||
"This_is_a_really_long_line_to_see_if_splitting_without_spaces_works_properly._I_also_want_to_see_if_it_works_across_multiple_lines_or_just_on_the_first_one. But after this, I want the text to continue normally before changing_back_to_being_really_long_oh_yes",
|
"This_is_a_really_long_line_to_see_if_splitting_without_spaces_works_properly._I_also_want_to_see_if_it_works_across_multiple_lines_or_just_on_the_first_one. But after this, I want the text to continue normally before changing_back_to_being_really_long_oh_yes",
|
||||||
65, 0.1F), expectedDisplay);
|
65, 0.1F), expectedDisplay);
|
||||||
|
|
||||||
var formatted = this.formatter.Tokenize(this.font,
|
var formatted = this.formatter.Tokenize(this.font,
|
||||||
"This_is_a_really_long_line_to_see_if_<c Blue>splitting</c>_without_spaces_works_properly._I_also_want_to_see_if_it_works_across_multiple_<c Yellow>lines</c>_or_just_on_the_first_one. But after this, I want the <b>text</b> to continue normally before changing_back_<i>to</i>_being_really_long_oh_yes");
|
"This_is_a_really_long_line_to_see_if_<c Blue>splitting</c>_without_spaces_works_properly._I_also_want_to_see_if_it_works_across_multiple_<c Yellow>lines</c>_or_just_on_the_first_one. But after this, I want the <b>text</b> to continue normally before changing_back_<i>to</i>_being_really_long_oh_yes");
|
||||||
formatted.Split(this.font, 65, 0.1F);
|
formatted.Split(this.font, 65, 0.1F);
|
||||||
Assert.AreEqual(formatted.DisplayString, string.Join('\n', expectedDisplay));
|
Assert.AreEqual(formatted.DisplayString, expectedDisplay);
|
||||||
|
|
||||||
var tokens = new[] {
|
var tokens = new[] {
|
||||||
"This_is_a_really_long_line_to_s\nee_if_",
|
"This_is_a_really_long_line_to_s\nee_if_",
|
||||||
|
|
Loading…
Reference in a new issue