2019-08-10 19:23:08 +02:00
|
|
|
using System;
|
2019-08-18 17:59:14 +02:00
|
|
|
using System.Linq;
|
2019-08-10 19:23:08 +02:00
|
|
|
using System.Text;
|
|
|
|
using Microsoft.Xna.Framework;
|
|
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
|
|
using Microsoft.Xna.Framework.Input;
|
2019-08-12 19:44:16 +02:00
|
|
|
using MLEM.Extensions;
|
2019-08-10 19:23:08 +02:00
|
|
|
using MLEM.Font;
|
2019-08-30 19:05:27 +02:00
|
|
|
using MLEM.Input;
|
2020-02-24 14:03:53 +01:00
|
|
|
using MLEM.Misc;
|
2019-08-10 19:23:08 +02:00
|
|
|
using MLEM.Textures;
|
2019-08-10 21:37:10 +02:00
|
|
|
using MLEM.Ui.Style;
|
2020-02-27 17:51:44 +01:00
|
|
|
using TextCopy;
|
2019-08-10 19:23:08 +02:00
|
|
|
|
|
|
|
namespace MLEM.Ui.Elements {
|
|
|
|
public class TextField : Element {
|
|
|
|
|
2019-08-18 17:59:14 +02:00
|
|
|
public static readonly Rule DefaultRule = (field, add) => !add.Any(char.IsControl);
|
|
|
|
public static readonly Rule OnlyLetters = (field, add) => add.All(char.IsLetter);
|
|
|
|
public static readonly Rule OnlyNumbers = (field, add) => add.All(char.IsNumber);
|
|
|
|
public static readonly Rule LettersNumbers = (field, add) => add.All(c => char.IsLetter(c) || char.IsNumber(c));
|
|
|
|
|
2020-03-19 03:27:21 +01:00
|
|
|
public StyleProp<Color> TextColor;
|
|
|
|
public StyleProp<Color> PlaceholderColor;
|
2019-10-14 21:28:12 +02:00
|
|
|
public StyleProp<NinePatch> Texture;
|
|
|
|
public StyleProp<NinePatch> HoveredTexture;
|
|
|
|
public StyleProp<Color> HoveredColor;
|
|
|
|
public StyleProp<float> TextScale;
|
2020-03-28 22:25:06 +01:00
|
|
|
public StyleProp<GenericFont> Font;
|
2019-08-24 12:40:20 +02:00
|
|
|
private readonly StringBuilder text = new StringBuilder();
|
|
|
|
public string Text => this.text.ToString();
|
2019-08-23 18:56:39 +02:00
|
|
|
public string PlaceholderText;
|
2019-08-10 19:23:08 +02:00
|
|
|
public TextChanged OnTextChange;
|
|
|
|
public float TextOffsetX = 4;
|
2020-03-19 03:27:21 +01:00
|
|
|
public float CaretWidth = 0.5F;
|
2019-08-10 19:23:08 +02:00
|
|
|
private double caretBlinkTimer;
|
2019-09-05 18:15:51 +02:00
|
|
|
private string displayedText;
|
|
|
|
private int textOffset;
|
2019-08-18 17:59:14 +02:00
|
|
|
public Rule InputRule;
|
2019-08-30 19:05:27 +02:00
|
|
|
public string MobileTitle;
|
|
|
|
public string MobileDescription;
|
2019-09-05 12:51:40 +02:00
|
|
|
private int caretPos;
|
|
|
|
public int CaretPos {
|
|
|
|
get {
|
|
|
|
this.CaretPos = MathHelper.Clamp(this.caretPos, 0, this.text.Length);
|
|
|
|
return this.caretPos;
|
|
|
|
}
|
|
|
|
set {
|
|
|
|
if (this.caretPos != value) {
|
|
|
|
this.caretPos = value;
|
|
|
|
this.HandleTextChange(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-08-10 19:23:08 +02:00
|
|
|
|
2020-03-28 22:25:06 +01:00
|
|
|
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null) : base(anchor, size) {
|
2019-08-18 17:59:14 +02:00
|
|
|
this.InputRule = rule ?? DefaultRule;
|
2019-10-14 21:28:12 +02:00
|
|
|
if (font != null)
|
|
|
|
this.Font.Set(font);
|
2019-08-30 19:05:27 +02:00
|
|
|
|
2020-02-24 14:03:53 +01:00
|
|
|
if (TextInputWrapper.Current.RequiresOnScreenKeyboard()) {
|
2020-02-01 21:16:10 +01:00
|
|
|
this.OnPressed += async e => {
|
|
|
|
if (!KeyboardInput.IsVisible) {
|
|
|
|
var title = this.MobileTitle ?? this.PlaceholderText;
|
|
|
|
var result = await KeyboardInput.Show(title, this.MobileDescription, this.Text);
|
|
|
|
if (result != null)
|
|
|
|
this.SetText(result.Replace('\n', ' '), true);
|
|
|
|
}
|
|
|
|
};
|
2019-08-30 19:05:27 +02:00
|
|
|
}
|
2020-02-24 14:03:53 +01:00
|
|
|
this.OnTextInput += (element, key, character) => {
|
|
|
|
if (!this.IsSelected || this.IsHidden)
|
|
|
|
return;
|
|
|
|
if (key == Keys.Back) {
|
|
|
|
if (this.CaretPos > 0) {
|
|
|
|
this.CaretPos--;
|
|
|
|
this.RemoveText(this.CaretPos, 1);
|
|
|
|
}
|
|
|
|
} else if (key == Keys.Delete) {
|
|
|
|
this.RemoveText(this.CaretPos, 1);
|
|
|
|
} else {
|
|
|
|
this.InsertText(character);
|
|
|
|
}
|
|
|
|
};
|
2020-02-01 21:16:10 +01:00
|
|
|
this.OnDeselected += e => this.CaretPos = 0;
|
|
|
|
this.OnSelected += e => this.CaretPos = this.text.Length;
|
2019-08-24 12:40:20 +02:00
|
|
|
}
|
2019-08-11 00:39:40 +02:00
|
|
|
|
2019-09-05 12:51:40 +02:00
|
|
|
private void HandleTextChange(bool textChanged = true) {
|
2019-08-24 12:40:20 +02:00
|
|
|
// not initialized yet
|
2020-02-06 17:36:51 +01:00
|
|
|
if (!this.Font.HasValue())
|
2019-08-24 12:40:20 +02:00
|
|
|
return;
|
2019-10-14 21:28:12 +02:00
|
|
|
var length = this.Font.Value.MeasureString(this.text).X * this.TextScale;
|
2019-08-24 12:40:20 +02:00
|
|
|
var maxWidth = this.DisplayArea.Width / this.Scale - this.TextOffsetX * 2;
|
|
|
|
if (length > maxWidth) {
|
2019-09-05 18:15:51 +02:00
|
|
|
// 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);
|
2019-10-14 21:28:12 +02:00
|
|
|
var bound = this.CaretPos - this.Font.Value.TruncateString(importantArea, maxWidth, this.TextScale, true).Length;
|
2019-09-05 18:15:51 +02:00
|
|
|
if (this.textOffset < bound) {
|
|
|
|
this.textOffset = bound;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var visible = this.text.ToString(this.textOffset, this.text.Length - this.textOffset);
|
2019-10-14 21:28:12 +02:00
|
|
|
this.displayedText = this.Font.Value.TruncateString(visible, maxWidth, this.TextScale);
|
2019-08-24 12:40:20 +02:00
|
|
|
} else {
|
2019-09-05 18:15:51 +02:00
|
|
|
this.displayedText = this.Text;
|
|
|
|
this.textOffset = 0;
|
2019-08-24 12:40:20 +02:00
|
|
|
}
|
|
|
|
|
2019-09-05 12:51:40 +02:00
|
|
|
if (textChanged)
|
|
|
|
this.OnTextChange?.Invoke(this, this.Text);
|
2019-08-10 19:23:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public override void Update(GameTime time) {
|
|
|
|
base.Update(time);
|
|
|
|
|
2019-09-05 18:15:51 +02:00
|
|
|
// handle first initialization if not done
|
|
|
|
if (this.displayedText == null)
|
|
|
|
this.HandleTextChange(false);
|
2020-03-02 09:40:07 +01:00
|
|
|
|
2020-02-27 17:51:44 +01:00
|
|
|
if (!this.IsSelected || this.IsHidden)
|
|
|
|
return;
|
2020-03-02 09:40:07 +01:00
|
|
|
|
2019-09-05 12:51:40 +02:00
|
|
|
if (this.Input.IsKeyPressed(Keys.Left)) {
|
|
|
|
this.CaretPos--;
|
|
|
|
} else if (this.Input.IsKeyPressed(Keys.Right)) {
|
|
|
|
this.CaretPos++;
|
|
|
|
} else if (this.Input.IsKeyPressed(Keys.Home)) {
|
|
|
|
this.CaretPos = 0;
|
|
|
|
} else if (this.Input.IsKeyPressed(Keys.End)) {
|
|
|
|
this.CaretPos = this.text.Length;
|
2020-02-27 17:51:44 +01:00
|
|
|
} else if (this.Input.IsModifierKeyDown(ModifierKey.Control)) {
|
|
|
|
if (this.Input.IsKeyPressed(Keys.V)) {
|
2020-03-02 09:40:07 +01:00
|
|
|
var clip = Clipboard.GetText();
|
|
|
|
if (clip != null)
|
|
|
|
this.InsertText(clip);
|
2020-02-27 17:51:44 +01:00
|
|
|
} else if (this.Input.IsKeyPressed(Keys.C)) {
|
|
|
|
// until there is text selection, just copy the whole content
|
|
|
|
Clipboard.SetText(this.Text);
|
|
|
|
}
|
2019-09-05 12:51:40 +02:00
|
|
|
}
|
|
|
|
|
2019-08-10 19:23:08 +02:00
|
|
|
this.caretBlinkTimer += time.ElapsedGameTime.TotalSeconds;
|
|
|
|
if (this.caretBlinkTimer >= 1)
|
|
|
|
this.caretBlinkTimer = 0;
|
|
|
|
}
|
|
|
|
|
2019-09-20 13:22:05 +02:00
|
|
|
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
|
2019-08-10 19:23:08 +02:00
|
|
|
var tex = this.Texture;
|
|
|
|
var color = Color.White * alpha;
|
|
|
|
if (this.IsMouseOver) {
|
2019-11-05 13:28:41 +01:00
|
|
|
tex = this.HoveredTexture.OrDefault(tex);
|
2019-10-14 21:28:12 +02:00
|
|
|
color = (Color) this.HoveredColor * alpha;
|
2019-08-10 19:23:08 +02:00
|
|
|
}
|
2019-09-04 17:19:31 +02:00
|
|
|
batch.Draw(tex, this.DisplayArea, color, this.Scale);
|
2019-08-23 18:56:39 +02:00
|
|
|
|
2019-11-02 14:53:59 +01:00
|
|
|
if (this.displayedText != null) {
|
2020-03-28 22:25:06 +01:00
|
|
|
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);
|
2019-11-02 14:53:59 +01:00
|
|
|
if (this.text.Length > 0 || this.IsSelected) {
|
2020-03-19 03:27:21 +01:00
|
|
|
var textColor = this.TextColor.OrDefault(Color.White);
|
2020-03-28 22:25:06 +01:00
|
|
|
this.Font.Value.DrawString(batch, this.displayedText, textPos, textColor * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
|
2020-03-19 03:27:21 +01:00
|
|
|
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;
|
2020-03-28 22:25:06 +01:00
|
|
|
batch.Draw(batch.GetBlankTexture(), new RectangleF(textPos.X + textSize.X, textPos.Y, this.CaretWidth * this.Scale, lineHeight), null, textColor * alpha);
|
2020-03-19 03:27:21 +01:00
|
|
|
}
|
2019-11-02 14:53:59 +01:00
|
|
|
} else if (this.PlaceholderText != null) {
|
2020-03-28 22:25:06 +01:00
|
|
|
this.Font.Value.DrawString(batch, this.PlaceholderText, textPos, this.PlaceholderColor.OrDefault(Color.Gray) * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
|
2019-11-02 14:53:59 +01:00
|
|
|
}
|
2019-08-23 18:56:39 +02:00
|
|
|
}
|
2019-09-20 13:22:05 +02:00
|
|
|
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
|
2019-08-10 19:23:08 +02:00
|
|
|
}
|
|
|
|
|
2019-09-01 19:50:17 +02:00
|
|
|
public void SetText(object text, bool removeMismatching = false) {
|
|
|
|
if (removeMismatching) {
|
|
|
|
var result = new StringBuilder();
|
|
|
|
foreach (var c in text.ToString()) {
|
|
|
|
if (this.InputRule(this, c.ToString()))
|
|
|
|
result.Append(c);
|
|
|
|
}
|
|
|
|
text = result.ToString();
|
|
|
|
} else if (!this.InputRule(this, text.ToString()))
|
2019-08-24 12:40:20 +02:00
|
|
|
return;
|
|
|
|
this.text.Clear();
|
|
|
|
this.text.Append(text);
|
2019-09-05 12:51:40 +02:00
|
|
|
this.CaretPos = this.text.Length;
|
2019-08-24 12:40:20 +02:00
|
|
|
this.HandleTextChange();
|
|
|
|
}
|
|
|
|
|
2019-09-05 12:51:40 +02:00
|
|
|
public void InsertText(object text) {
|
|
|
|
var strg = text.ToString();
|
|
|
|
if (!this.InputRule(this, strg))
|
2019-08-24 12:40:20 +02:00
|
|
|
return;
|
2019-09-05 12:51:40 +02:00
|
|
|
this.text.Insert(this.CaretPos, strg);
|
|
|
|
this.CaretPos += strg.Length;
|
2019-08-24 12:40:20 +02:00
|
|
|
this.HandleTextChange();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void RemoveText(int index, int length) {
|
2019-09-05 12:51:40 +02:00
|
|
|
if (index < 0 || index >= this.text.Length)
|
|
|
|
return;
|
2019-08-24 12:40:20 +02:00
|
|
|
this.text.Remove(index, length);
|
|
|
|
this.HandleTextChange();
|
|
|
|
}
|
|
|
|
|
2019-08-10 21:37:10 +02:00
|
|
|
protected override void InitStyle(UiStyle style) {
|
|
|
|
base.InitStyle(style);
|
2019-10-14 21:28:12 +02:00
|
|
|
this.TextScale.SetFromStyle(style.TextScale);
|
|
|
|
this.Font.SetFromStyle(style.Font);
|
|
|
|
this.Texture.SetFromStyle(style.TextFieldTexture);
|
|
|
|
this.HoveredTexture.SetFromStyle(style.TextFieldHoveredTexture);
|
|
|
|
this.HoveredColor.SetFromStyle(style.TextFieldHoveredColor);
|
2019-08-10 21:37:10 +02:00
|
|
|
}
|
|
|
|
|
2019-08-10 19:23:08 +02:00
|
|
|
public delegate void TextChanged(TextField field, string text);
|
|
|
|
|
2019-08-18 17:59:14 +02:00
|
|
|
public delegate bool Rule(TextField field, string textToAdd);
|
|
|
|
|
2019-08-10 19:23:08 +02:00
|
|
|
}
|
|
|
|
}
|