1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-15 05:38:46 +02:00

Text input improvements:

- Allow using control and arrow keys to move the visible area of a text input
- Don't reset the caret position of a text field when selecting or deselecting it
This commit is contained in:
Ell 2023-12-02 19:28:59 +01:00
parent 294af052ae
commit 764b29e120
3 changed files with 74 additions and 39 deletions

View file

@ -24,6 +24,7 @@ Additions
Improvements Improvements
- Stopped the text formatter throwing if a color can't be parsed - Stopped the text formatter throwing if a color can't be parsed
- Improved text formatter tokenization performance - Improved text formatter tokenization performance
- Allow using control and arrow keys to move the visible area of a text input
Fixes Fixes
- Fixed TextInput not working correctly when using surrogate pairs - Fixed TextInput not working correctly when using surrogate pairs
@ -37,6 +38,7 @@ Improvements
- Allow scrolling panels to contain other scrolling panels - Allow scrolling panels to contain other scrolling panels
- Allow dropdowns to have scrolling panels - Allow dropdowns to have scrolling panels
- Improved Panel performance when adding and removing a lot of children - Improved Panel performance when adding and removing a lot of children
- Don't reset the caret position of a text field when selecting or deselecting it
Fixes Fixes
- Fixed panels updating their relevant children too much when the scroll bar is hidden - Fixed panels updating their relevant children too much when the scroll bar is hidden

View file

@ -204,8 +204,6 @@ namespace MLEM.Ui.Elements {
if (this.IsSelectedActive && !this.IsHidden && !this.textInput.OnTextInput(key, character) && key == Keys.Enter && !this.Multiline) if (this.IsSelectedActive && !this.IsHidden && !this.textInput.OnTextInput(key, character) && key == Keys.Enter && !this.Multiline)
this.EnterReceiver?.Controls?.PressElement(this.EnterReceiver); this.EnterReceiver?.Controls?.PressElement(this.EnterReceiver);
}; };
this.OnDeselected += e => this.CaretPos = 0;
this.OnSelected += e => this.CaretPos = this.textInput.Length;
} }
/// <inheritdoc /> /// <inheritdoc />

View file

@ -204,6 +204,21 @@ namespace MLEM.Input {
} }
} }
/// <summary> /// <summary>
/// The maximum amount of lines that can be visible in this text input, based on its <see cref="Size"/>, the used <see cref="Font"/> and its <see cref="TextScale"/>.
/// Note that this may return a number higher than 1 even if this is not a <see cref="Multiline"/> text input.
/// </summary>
public int MaxDisplayedLines => (this.Size.Y / (this.Font.LineHeight * this.TextScale)).Floor();
/// <summary>
/// The index of the first line that is currently visible.
/// This value can be changed using <see cref="ShowLine"/>.
/// </summary>
public int FirstVisibleLine { get; private set; }
/// <summary>
/// The total amount of lines of text that this text input currently has, including additional lines added by automatic wrapping.
/// If this is not a <see cref="Multiline"/> text input, this value is always 1.
/// </summary>
public int Lines { get; private set; }
/// <summary>
/// A function that is invoked when a string of text should be copied to the clipboard. /// A function that is invoked when a string of text should be copied to the clipboard.
/// MLEM.Ui uses the TextCopy package for this, but other options are available. /// MLEM.Ui uses the TextCopy package for this, but other options are available.
/// </summary> /// </summary>
@ -218,10 +233,9 @@ namespace MLEM.Input {
private char? maskingCharacter; private char? maskingCharacter;
private double caretBlinkTimer; private double caretBlinkTimer;
private string displayedText; private string visibleText;
private string[] splitText; private string[] multilineSplitText;
private int textOffset; private int textOffset;
private int lineOffset;
private int caretPos; private int caretPos;
private int caretLine; private int caretLine;
private int caretPosInLine; private int caretPosInLine;
@ -302,9 +316,9 @@ namespace MLEM.Input {
this.CaretPos--; this.CaretPos--;
} else if (this.CaretPos < this.text.Length && input.TryConsumePressed(Keys.Right)) { } else if (this.CaretPos < this.text.Length && input.TryConsumePressed(Keys.Right)) {
this.CaretPos++; this.CaretPos++;
} else if (this.Multiline && input.IsPressedAvailable(Keys.Up) && this.MoveCaretToLine(this.CaretLine - 1)) { } else if (this.Multiline && input.IsPressedAvailable(Keys.Up) && (input.IsModifierKeyDown(ModifierKey.Control) ? this.ShowLine(this.FirstVisibleLine - 1) : this.MoveCaretToLine(this.CaretLine - 1))) {
input.TryConsumePressed(Keys.Up); input.TryConsumePressed(Keys.Up);
} else if (this.Multiline && input.IsPressedAvailable(Keys.Down) && this.MoveCaretToLine(this.CaretLine + 1)) { } else if (this.Multiline && input.IsPressedAvailable(Keys.Down) && (input.IsModifierKeyDown(ModifierKey.Control) ? this.ShowLine(this.FirstVisibleLine + 1) : this.MoveCaretToLine(this.CaretLine + 1))) {
input.TryConsumePressed(Keys.Down); input.TryConsumePressed(Keys.Down);
} else if (this.CaretPos != 0 && input.TryConsumePressed(Keys.Home)) { } else if (this.CaretPos != 0 && input.TryConsumePressed(Keys.Home)) {
this.CaretPos = 0; this.CaretPos = 0;
@ -340,12 +354,12 @@ namespace MLEM.Input {
this.UpdateTextDataIfDirty(); this.UpdateTextDataIfDirty();
var scale = this.TextScale * drawScale; var scale = this.TextScale * drawScale;
this.Font.DrawString(batch, this.displayedText, textPos, textColor, 0, Vector2.Zero, scale, SpriteEffects.None, 0); this.Font.DrawString(batch, this.visibleText, textPos, textColor, 0, Vector2.Zero, scale, SpriteEffects.None, 0);
if (caretWidth > 0 && this.caretBlinkTimer < 0.5F) { if (caretWidth > 0 && this.caretBlinkTimer < 0.5F) {
var caretDrawPos = textPos + new Vector2(this.caretDrawOffset * scale, 0); var caretDrawPos = textPos + new Vector2(this.caretDrawOffset * scale, 0);
if (this.Multiline) if (this.Multiline)
caretDrawPos.Y += this.Font.LineHeight * (this.CaretLine - this.lineOffset) * scale; caretDrawPos.Y += this.Font.LineHeight * (this.CaretLine - this.FirstVisibleLine) * scale;
batch.Draw(batch.GetBlankTexture(), new RectangleF(caretDrawPos, new Vector2(caretWidth * drawScale, this.Font.LineHeight * scale)), null, textColor); batch.Draw(batch.GetBlankTexture(), new RectangleF(caretDrawPos, new Vector2(caretWidth * drawScale, this.Font.LineHeight * scale)), null, textColor);
} }
} }
@ -428,6 +442,26 @@ namespace MLEM.Input {
return false; return false;
} }
/// <summary>
/// Moves visual focus into such bounds that the given line will be the first visible line of this text input.
/// </summary>
/// <param name="line">The first line that should be visible.</param>
/// <returns>Whether the line can be the fist visible line, and wasn't already the first visible line.</returns>
public bool ShowLine(int line) {
if (this.FirstVisibleLine != line && line >= 0 && line < this.Lines - (this.MaxDisplayedLines - 1)) {
this.FirstVisibleLine = line;
// move the caret into visible bounds if necessary
var clampedCaretLine = (int) MathHelper.Clamp(this.CaretLine, line, line + this.MaxDisplayedLines - 1F);
if (clampedCaretLine != this.CaretLine)
this.MoveCaretToLine(clampedCaretLine);
this.SetTextDataDirty(false);
return true;
}
return false;
}
private bool FilterText(ref string text, bool removeMismatching) { private bool FilterText(ref string text, bool removeMismatching) {
var result = new StringBuilder(); var result = new StringBuilder();
foreach (var codePoint in new CodePointSource(text)) { foreach (var codePoint in new CodePointSource(text)) {
@ -460,49 +494,50 @@ namespace MLEM.Input {
if (this.Multiline) { if (this.Multiline) {
// soft wrap if we're multiline // soft wrap if we're multiline
this.splitText = this.Font.SplitStringSeparate(visualText, this.Size.X, this.TextScale).ToArray(); this.multilineSplitText = this.Font.SplitStringSeparate(visualText, this.Size.X, this.TextScale).ToArray();
this.displayedText = string.Join("\n", this.splitText); this.visibleText = string.Join("\n", this.multilineSplitText);
this.Lines = this.visibleText.Count(c => c == '\n') + 1;
this.UpdateCaretData(); this.UpdateCaretData();
if (this.Font.MeasureString(this.displayedText).Y * this.TextScale > this.Size.Y) { if (this.Font.MeasureString(this.visibleText).Y * this.TextScale > this.Size.Y) {
var maxLines = (this.Size.Y / (this.Font.LineHeight * this.TextScale)).Floor(); if (this.FirstVisibleLine > this.CaretLine) {
if (this.lineOffset > this.CaretLine) {
// if we're moving up // if we're moving up
this.lineOffset = this.CaretLine; this.FirstVisibleLine = this.CaretLine;
} else if (this.CaretLine >= maxLines) { } else if (this.CaretLine >= this.MaxDisplayedLines) {
// if we're moving down // if we're moving down
var limit = this.CaretLine - (maxLines - 1); var limit = this.CaretLine - (this.MaxDisplayedLines - 1);
if (limit > this.lineOffset) if (limit > this.FirstVisibleLine)
this.lineOffset = limit; this.FirstVisibleLine = limit;
} }
// calculate resulting string // calculate resulting string
var ret = new StringBuilder(); var ret = new StringBuilder();
var lines = 0; var lines = 0;
var originalIndex = 0; var originalIndex = 0;
for (var i = 0; i < this.displayedText.Length; i++) { for (var i = 0; i < this.visibleText.Length; i++) {
if (lines >= this.lineOffset) { if (lines >= this.FirstVisibleLine) {
if (ret.Length <= 0) if (ret.Length <= 0)
this.textOffset = originalIndex; this.textOffset = originalIndex;
ret.Append(this.displayedText[i]); ret.Append(this.visibleText[i]);
} }
if (this.displayedText[i] == '\n') { if (this.visibleText[i] == '\n') {
lines++; lines++;
if (visualText[originalIndex] == '\n') if (visualText[originalIndex] == '\n')
originalIndex++; originalIndex++;
} else { } else {
originalIndex++; originalIndex++;
} }
if (lines - this.lineOffset >= maxLines) if (lines - this.FirstVisibleLine >= this.MaxDisplayedLines)
break; break;
} }
this.displayedText = ret.ToString(); this.visibleText = ret.ToString();
} else { } else {
this.lineOffset = 0; this.FirstVisibleLine = 0;
this.textOffset = 0; this.textOffset = 0;
} }
} else { } else {
this.splitText = null; this.multilineSplitText = null;
this.lineOffset = 0; this.FirstVisibleLine = 0;
this.Lines = 1;
// not multiline, so scroll horizontally based on caret position // not multiline, so scroll horizontally based on caret position
if (this.Font.MeasureString(visualText).X * this.TextScale > this.Size.X) { if (this.Font.MeasureString(visualText).X * this.TextScale > this.Size.X) {
if (this.textOffset > this.CaretPos) { if (this.textOffset > this.CaretPos) {
@ -516,9 +551,9 @@ namespace MLEM.Input {
this.textOffset = bound; this.textOffset = bound;
} }
var visible = visualText.ToString(this.textOffset, visualText.Length - this.textOffset); var visible = visualText.ToString(this.textOffset, visualText.Length - this.textOffset);
this.displayedText = this.Font.TruncateString(visible, this.Size.X, this.TextScale); this.visibleText = this.Font.TruncateString(visible, this.Size.X, this.TextScale);
} else { } else {
this.displayedText = visualText.ToString(); this.visibleText = visualText.ToString();
this.textOffset = 0; this.textOffset = 0;
} }
this.UpdateCaretData(); this.UpdateCaretData();
@ -526,9 +561,9 @@ namespace MLEM.Input {
} }
private void UpdateCaretData() { private void UpdateCaretData() {
if (this.splitText != null) { if (this.multilineSplitText != null) {
// the code below will never execute if our text is empty, so reset our caret position fully // the code below will never execute if our text is empty, so reset our caret position fully
if (this.splitText.Length <= 0) { if (this.multilineSplitText.Length <= 0) {
this.caretLine = 0; this.caretLine = 0;
this.caretPosInLine = 0; this.caretPosInLine = 0;
this.caretDrawOffset = 0; this.caretDrawOffset = 0;
@ -537,9 +572,9 @@ namespace MLEM.Input {
var line = 0; var line = 0;
var index = 0; var index = 0;
for (var d = 0; d < this.splitText.Length; d++) { for (var d = 0; d < this.multilineSplitText.Length; d++) {
var startOfLine = 0; var startOfLine = 0;
var split = this.splitText[d]; var split = this.multilineSplitText[d];
for (var i = 0; i <= split.Length; i++) { for (var i = 0; i <= split.Length; i++) {
if (index == this.CaretPos) { if (index == this.CaretPos) {
this.caretLine = line; this.caretLine = line;
@ -559,20 +594,20 @@ namespace MLEM.Input {
// max width splits // max width splits
line++; line++;
} }
} else if (this.displayedText != null) { } else if (this.visibleText != null) {
this.caretLine = 0; this.caretLine = 0;
this.caretPosInLine = this.CaretPos; this.caretPosInLine = this.CaretPos;
this.caretDrawOffset = this.Font.MeasureString(this.displayedText.Substring(0, this.CaretPos - this.textOffset)).X; this.caretDrawOffset = this.Font.MeasureString(this.visibleText.Substring(0, this.CaretPos - this.textOffset)).X;
} }
} }
private (int, int) GetLineBounds(int boundLine) { private (int, int) GetLineBounds(int boundLine) {
if (this.splitText != null) { if (this.multilineSplitText != null) {
var line = 0; var line = 0;
var index = 0; var index = 0;
var startOfLineIndex = 0; var startOfLineIndex = 0;
for (var d = 0; d < this.splitText.Length; d++) { for (var d = 0; d < this.multilineSplitText.Length; d++) {
var split = this.splitText[d]; var split = this.multilineSplitText[d];
for (var i = 0; i < split.Length; i++) { for (var i = 0; i < split.Length; i++) {
index++; index++;
if (split[i] == '\n') { if (split[i] == '\n') {