using System;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Style;
using TextCopy;
namespace MLEM.Ui.Elements {
///
/// A text field element for use inside of a .
/// A text field is a selectable element that can be typed in, as well as copied and pasted from.
/// If an on-screen keyboard is required, then this text field will automatically open an on-screen keyboard using .
///
public class TextField : Element {
///
/// A that allows any visible character and spaces
///
public static readonly Rule DefaultRule = (field, add) => !add.Any(char.IsControl);
///
/// A that only allows letters
///
public static readonly Rule OnlyLetters = (field, add) => add.All(char.IsLetter);
///
/// A that only allows numerals
///
public static readonly Rule OnlyNumbers = (field, add) => add.All(char.IsNumber);
///
/// A that only allows letters and numerals
///
public static readonly Rule LettersNumbers = (field, add) => add.All(c => char.IsLetter(c) || char.IsNumber(c));
///
/// A that only allows characters not contained in
///
public static readonly Rule PathNames = (field, add) => add.IndexOfAny(Path.GetInvalidPathChars()) < 0;
///
/// A that only allows characters not contained in
///
public static readonly Rule FileNames = (field, add) => add.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
///
/// The color that this text field's text should display with
///
public StyleProp TextColor;
///
/// The color that the should display with
///
public StyleProp PlaceholderColor;
///
/// This text field's texture
///
public StyleProp Texture;
///
/// This text field's texture while it is hovered
///
public StyleProp HoveredTexture;
///
/// The color that this text field should display with while it is hovered
///
public StyleProp HoveredColor;
///
/// The scale that this text field should render text with
///
public StyleProp TextScale;
///
/// The font that this text field should display text with
///
public StyleProp Font;
private readonly StringBuilder text = new StringBuilder();
///
/// This text field's current text
///
public string Text => this.text.ToString();
///
/// The text that displays in this text field if is empty
///
public string PlaceholderText;
///
/// An event that gets called when changes, either through input, or through a manual change.
///
public TextChanged OnTextChange;
///
/// The x position that text should start rendering at, based on the x position of this text field.
///
public float TextOffsetX = 4;
///
/// The width that the caret should render with.
///
public float CaretWidth = 0.5F;
private double caretBlinkTimer;
private string displayedText;
private int textOffset;
///
/// The rule used for text input.
/// Rules allow only certain characters to be allowed inside of a text field.
///
public Rule InputRule;
///
/// The title of the KeyboardInput field on mobile devices and consoles
///
public string MobileTitle;
///
/// The description of the KeyboardInput field on mobile devices and consoles
///
public string MobileDescription;
private int caretPos;
///
/// The position of the caret within the text.
/// 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;
}
set {
if (this.caretPos != value) {
this.caretPos = value;
this.HandleTextChange(false);
}
}
}
///
/// Creates a new text field with the given settings
///
/// The text field's anchor
/// The text field's size
/// The text field's input rule
/// The font to use for drawing text
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null) : base(anchor, size) {
this.InputRule = rule ?? DefaultRule;
if (font != null)
this.Font.Set(font);
MlemPlatform.EnsureExists();
this.OnPressed += async e => {
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.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);
}
};
this.OnDeselected += e => this.CaretPos = 0;
this.OnSelected += e => this.CaretPos = this.text.Length;
}
private void HandleTextChange(bool textChanged = true) {
// 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;
}
}
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 (textChanged)
this.OnTextChange?.Invoke(this, this.Text);
}
///
public override void Update(GameTime time) {
base.Update(time);
// handle first initialization if not done
if (this.displayedText == null)
this.HandleTextChange(false);
if (!this.IsSelected || this.IsHidden)
return;
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;
} else if (this.Input.IsModifierKeyDown(ModifierKey.Control)) {
if (this.Input.IsKeyPressed(Keys.V)) {
var clip = ClipboardService.GetText();
if (clip != null)
this.InsertText(clip);
} else if (this.Input.IsKeyPressed(Keys.C)) {
// until there is text selection, just copy the whole content
ClipboardService.SetText(this.Text);
}
}
this.caretBlinkTimer += time.ElapsedGameTime.TotalSeconds;
if (this.caretBlinkTimer >= 1)
this.caretBlinkTimer = 0;
}
///
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
var tex = this.Texture;
var color = Color.White * alpha;
if (this.IsMouseOver) {
tex = this.HoveredTexture.OrDefault(tex);
color = (Color) this.HoveredColor * alpha;
}
batch.Draw(tex, this.DisplayArea, color, this.Scale);
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);
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);
}
} 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);
}
}
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
}
///
/// Replaces this text field's text with the given text.
///
/// The new text
/// If any characters that don't match the should be left out
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.ToCachedString()))
result.Append(c);
}
text = result.ToString();
} else if (!this.InputRule(this, text.ToString()))
return;
this.text.Clear();
this.text.Append(text);
this.CaretPos = this.text.Length;
this.HandleTextChange();
}
///
/// Inserts the given text at the
///
/// The text to insert
public void InsertText(object text) {
var strg = text.ToString();
if (!this.InputRule(this, strg))
return;
this.text.Insert(this.CaretPos, strg);
this.CaretPos += strg.Length;
this.HandleTextChange();
}
///
/// Removes the given amount of text at the given index
///
/// The index
/// The amount of text to remove
public void RemoveText(int index, int length) {
if (index < 0 || index >= this.text.Length)
return;
this.text.Remove(index, length);
this.HandleTextChange();
}
///
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
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);
}
///
/// A delegate method used for
///
/// The text field whose text changed
/// The new text
public delegate void TextChanged(TextField field, string text);
///
/// A delegate method used for .
/// It should return whether the given text can be added to the text field.
///
/// The text field
/// The text that is tried to be added
public delegate bool Rule(TextField field, string textToAdd);
}
}