diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e7d9c2..2f251d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ Additions
### MLEM.Ui
Additions
- Allow specifying a maximum amount of characters for a TextField
+- Added a multiline editing mode to TextField
Improvements
- Cache TokenizedString inner offsets for non-Left text alignments to improve performance
diff --git a/Demos/UiDemo.cs b/Demos/UiDemo.cs
index aee9d41..ff0373f 100644
--- a/Demos/UiDemo.cs
+++ b/Demos/UiDemo.cs
@@ -95,8 +95,8 @@ namespace Demos {
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Defining text animations as formatting codes is also possible, including wobbly text at different intensities. Of course, more animations can be added though."));
this.root.AddChild(new VerticalSpace(3));
- this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Text input:", true));
- this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10)) {
+ this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Multiline text input:", true));
+ this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 50), multiline: true) {
PositionOffset = new Vector2(0, 1),
PlaceholderText = "Click here to input text"
});
diff --git a/MLEM.Ui/Elements/TextField.cs b/MLEM.Ui/Elements/TextField.cs
index 3495d5a..e91d684 100644
--- a/MLEM.Ui/Elements/TextField.cs
+++ b/MLEM.Ui/Elements/TextField.cs
@@ -25,7 +25,7 @@ namespace MLEM.Ui.Elements {
///
public static readonly Rule DefaultRule = (field, add) => {
foreach (var c in add) {
- if (char.IsControl(c))
+ if (char.IsControl(c) && (!field.Multiline || c != '\n'))
return false;
}
return true;
@@ -135,13 +135,12 @@ namespace MLEM.Ui.Elements {
/// This is always between 0 and the of
///
public int CaretPos {
- get {
- this.CaretPos = MathHelper.Clamp(this.caretPos, 0, this.text.Length);
- return this.caretPos;
- }
+ get => this.caretPos;
set {
- if (this.caretPos != value) {
- this.caretPos = value;
+ var val = MathHelper.Clamp(value, 0, this.text.Length);
+ if (this.caretPos != val) {
+ this.caretPos = val;
+ this.caretBlinkTimer = 0;
this.HandleTextChange(false);
}
}
@@ -163,6 +162,21 @@ namespace MLEM.Ui.Elements {
/// If this is set, the length of will never exceed this value.
///
public int? MaximumCharacters;
+ ///
+ /// Whether this text field should support multi-line editing.
+ /// If this is true, pressing will insert a new line into the if the 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.
+ ///
+ ///
+ /// Moving up and down through the text field, and clicking on text to start editing at the mouse's position, are currently not supported.
+ ///
+ public bool Multiline {
+ get => this.multiline;
+ set {
+ this.multiline = value;
+ this.HandleTextChange(false);
+ }
+ }
private readonly StringBuilder text = new StringBuilder();
@@ -170,7 +184,9 @@ namespace MLEM.Ui.Elements {
private double caretBlinkTimer;
private string displayedText;
private int textOffset;
+ private int lineOffset;
private int caretPos;
+ private bool multiline;
///
/// Creates a new text field with the given settings
@@ -180,8 +196,10 @@ namespace MLEM.Ui.Elements {
/// The text field's input rule
/// The font to use for drawing text
/// The text that the text field should contain by default
- public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null) : base(anchor, size) {
+ /// Whether the text field should support multi-line editing
+ 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.Multiline = multiline;
if (font != null)
this.Font.Set(font);
if (text != null)
@@ -200,6 +218,8 @@ namespace MLEM.Ui.Elements {
}
} else if (key == Keys.Delete) {
this.RemoveText(this.CaretPos, 1);
+ } else if (this.Multiline && key == Keys.Enter) {
+ this.InsertText('\n');
} else {
this.InsertText(character);
}
@@ -211,7 +231,7 @@ namespace MLEM.Ui.Elements {
var title = this.MobileTitle ?? this.PlaceholderText;
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
if (result != null)
- this.SetText(result.Replace('\n', ' '), true);
+ this.SetText(this.Multiline ? result : result.Replace('\n', ' '), true);
}
}
@@ -219,26 +239,83 @@ namespace MLEM.Ui.Elements {
// not initialized yet
if (!this.Font.HasValue())
return;
- var length = this.Font.Value.MeasureString(this.text.ToString()).X * this.TextScale;
var maxWidth = this.DisplayArea.Width / this.Scale - this.TextOffsetX * 2;
- if (length > maxWidth) {
- // if we're moving the caret to the left
- if (this.textOffset > this.CaretPos) {
- 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;
+ if (this.Multiline) {
+ // 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 && 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 {
- this.displayedText = this.Text;
- this.textOffset = 0;
+ // 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
+ 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)
this.displayedText = new string(this.MaskingCharacter.Value, this.displayedText.Length);
@@ -293,13 +370,38 @@ namespace MLEM.Ui.Elements {
if (this.displayedText != null) {
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) {
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);
- 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;
- batch.Draw(batch.GetBlankTexture(), new RectangleF(textPos.X + textSize.X, textPos.Y, this.CaretWidth * this.Scale, lineHeight), null, textColor * alpha);
+
+ if (this.IsSelected && this.caretBlinkTimer < 0.5F) {
+ 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 && 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) {
this.Font.Value.DrawString(batch, this.PlaceholderText, textPos, this.PlaceholderColor.OrDefault(Color.Gray) * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
@@ -358,6 +460,7 @@ namespace MLEM.Ui.Elements {
if (index < 0 || index >= this.text.Length)
return;
this.text.Remove(index, length);
+ this.CaretPos = this.text.Length;
this.HandleTextChange();
}
diff --git a/MLEM/Formatting/TokenizedString.cs b/MLEM/Formatting/TokenizedString.cs
index 4ef054c..bdc2fa5 100644
--- a/MLEM/Formatting/TokenizedString.cs
+++ b/MLEM/Formatting/TokenizedString.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;