mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-22 12:58:33 +01: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:
parent
294af052ae
commit
764b29e120
3 changed files with 74 additions and 39 deletions
|
@ -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
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
Loading…
Reference in a new issue