1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-24 21:48:35 +01:00

Compare commits

..

4 commits

6 changed files with 185 additions and 50 deletions

View file

@ -10,8 +10,13 @@ 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

View file

@ -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, "Text input:", true)); this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Multiline text input:", true));
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10)) { this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 50), multiline: true) {
PositionOffset = new Vector2(0, 1), PositionOffset = new Vector2(0, 1),
PlaceholderText = "Click here to input text" PlaceholderText = "Click here to input text"
}); });

View file

@ -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)) if (char.IsControl(c) && (!field.Multiline || c != '\n'))
return false; return false;
} }
return true; return true;
@ -135,13 +135,12 @@ 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 { get => this.caretPos;
this.CaretPos = MathHelper.Clamp(this.caretPos, 0, this.text.Length);
return this.caretPos;
}
set { set {
if (this.caretPos != value) { var val = MathHelper.Clamp(value, 0, this.text.Length);
this.caretPos = value; if (this.caretPos != val) {
this.caretPos = val;
this.caretBlinkTimer = 0;
this.HandleTextChange(false); this.HandleTextChange(false);
} }
} }
@ -158,6 +157,23 @@ 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();
@ -165,7 +181,9 @@ 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
@ -175,8 +193,10 @@ 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>
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null) : base(anchor, size) { /// <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, 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)
@ -195,6 +215,8 @@ 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);
} }
@ -206,7 +228,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(result.Replace('\n', ' '), true); this.SetText(this.Multiline ? result : result.Replace('\n', ' '), true);
} }
} }
@ -214,26 +236,83 @@ 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 (length > maxWidth) { if (this.Multiline) {
// if we're moving the caret to the left // soft wrap if we're multiline
if (this.textOffset > this.CaretPos) { this.displayedText = this.Font.Value.SplitString(this.text.ToString(), maxWidth, this.TextScale);
this.textOffset = this.CaretPos; var maxHeight = this.DisplayArea.Height / this.Scale - this.TextOffsetX * 2;
} else { if (this.Font.Value.MeasureString(this.displayedText).Y * this.TextScale > maxHeight) {
// if we're moving the caret to the right var maxLines = (maxHeight / (this.Font.Value.LineHeight * this.TextScale)).Floor();
var importantArea = this.text.ToString(this.textOffset, Math.Min(this.CaretPos, this.text.Length) - this.textOffset); // calculate what line the caret is on
var bound = this.CaretPos - this.Font.Value.TruncateString(importantArea, maxWidth, this.TextScale, true).Length; var caretLine = 0;
if (this.textOffset < bound) { var originalIndex = 0;
this.textOffset = bound; 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;
} }
var visible = this.text.ToString(this.textOffset, this.text.Length - this.textOffset);
this.displayedText = this.Font.Value.TruncateString(visible, maxWidth, this.TextScale);
} else { } else {
this.displayedText = this.Text; // not multiline, so scroll horizontally based on caret position
this.textOffset = 0; 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
this.textOffset = this.CaretPos;
} else {
// 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 bound = this.CaretPos - this.Font.Value.TruncateString(importantArea, maxWidth, this.TextScale, true).Length;
if (this.textOffset < bound)
this.textOffset = bound;
}
var visible = this.text.ToString(this.textOffset, this.text.Length - this.textOffset);
this.displayedText = this.Font.Value.TruncateString(visible, maxWidth, this.TextScale);
} else {
this.displayedText = this.Text;
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);
@ -288,13 +367,38 @@ 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 textPos = this.DisplayArea.Location + new Vector2(this.TextOffsetX * this.Scale, this.DisplayArea.Height / 2 - lineHeight / 2); var offset = new Vector2(
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) {
var textSize = this.Font.Value.MeasureString(this.displayedText.Substring(0, this.CaretPos - this.textOffset)) * this.TextScale * this.Scale; if (this.IsSelected && this.caretBlinkTimer < 0.5F) {
batch.Draw(batch.GetBlankTexture(), new RectangleF(textPos.X + textSize.X, textPos.Y, this.CaretWidth * this.Scale, lineHeight), null, textColor * alpha); var caretDrawPos = textPos;
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);
@ -305,33 +409,40 @@ 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 text.ToString()) { foreach (var c in strg) {
if (this.InputRule(this, c.ToCachedString())) if (this.InputRule(this, c.ToCachedString()))
result.Append(c); result.Append(c);
} }
text = result.ToString(); strg = result.ToString();
} else if (!this.InputRule(this, text.ToString())) } else if (!this.InputRule(this, strg))
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(text); this.text.Append(strg);
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();
@ -346,6 +457,8 @@ 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();
} }

View file

@ -1,3 +1,4 @@
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;
@ -160,22 +161,36 @@ 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. /// Also splits long words and supports zero-width spaces and takes into account existing newline characters in the passed <paramref name="text"/>.
/// 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) {
var ret = new StringBuilder(); return string.Join("\n", this.SplitStringSeparate(text, width, scale));
}
/// <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') {
// split at pre-defined new lines // fake split at pre-defined new lines
ret.Append(c); curr.Append(c);
lastSpaceIndex = -1; lastSpaceIndex = -1;
widthSinceLastSpace = 0; widthSinceLastSpace = 0;
currWidth = 0; currWidth = 0;
@ -183,17 +198,19 @@ 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 = ret.Length; lastSpaceIndex = curr.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
ret.Append('\n'); yield return curr.ToString();
currWidth = 0; currWidth = 0;
curr.Clear();
} else { } else {
// split after the last space // split after the last space
ret.Insert(lastSpaceIndex + 1, '\n'); yield return curr.ToString().Substring(0, lastSpaceIndex + 1);
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;
} }
@ -204,10 +221,11 @@ namespace MLEM.Font {
// add current character // add current character
currWidth += cWidth; currWidth += cWidth;
widthSinceLastSpace += cWidth; widthSinceLastSpace += cWidth;
ret.Append(c); curr.Append(c);
} }
} }
return ret.ToString(); if (curr.Length > 0)
yield return curr.ToString();
} }
private static bool IsTrailingSpace(string s, int index) { private static bool IsTrailingSpace(string s, int index) {

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;

View file

@ -29,9 +29,9 @@ namespace Tests {
[Test] [Test]
public void TestRegularSplit() { public void TestRegularSplit() {
Assert.AreEqual(this.font.SplitString( Assert.AreEqual(this.font.SplitStringSeparate(
"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), "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."); 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."});
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() {
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"; 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"};
Assert.AreEqual(this.font.SplitString( Assert.AreEqual(this.font.SplitStringSeparate(
"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, expectedDisplay); Assert.AreEqual(formatted.DisplayString, string.Join('\n', 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_",