using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using MLEM.Font; using MLEM.Graphics; using MLEM.Input; using MLEM.Maths; using MLEM.Misc; using MLEM.Textures; using MLEM.Ui.Style; #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER using TextCopy; #endif 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 . /// This class interally uses MLEM's . /// public class TextField : Element { /// public static readonly Rule DefaultRule = (field, add) => TextInput.DefaultRule(field.textInput, add); /// public static readonly Rule OnlyLetters = (field, add) => TextInput.OnlyLetters(field.textInput, add); /// public static readonly Rule OnlyNumbers = (field, add) => TextInput.OnlyNumbers(field.textInput, add); /// public static readonly Rule LettersNumbers = (field, add) => TextInput.LettersNumbers(field.textInput, add); /// public static readonly Rule PathNames = (field, add) => TextInput.PathNames(field.textInput, add); /// public static readonly Rule FileNames = (field, add) => TextInput.FileNames(field.textInput, add); #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER /// /// An event that is raised when an exception is thrown while trying to copy or paste clipboard contents using TextCopy. /// If no event handlers are added, the exception is ignored. /// public static event Action OnCopyPasteException; #endif /// /// 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 { get => this.textScale; set { this.textScale = value; this.textInput.TextScale = value; } } /// /// The font that this text field should display text with /// public StyleProp Font { get => this.font; set { this.font = value; this.textInput.Font = value; } } /// /// The x position that text should start rendering at, based on the x position of this text field. /// public StyleProp TextOffsetX; /// /// The width that the caret should render with, in pixels /// public StyleProp CaretWidth; /// public string Text => this.textInput.Text; /// public TextChanged OnTextChange; /// public Rule InputRule; /// public int CaretPos { get => this.textInput.CaretPos; set => this.textInput.CaretPos = value; } /// public int CaretLine => this.textInput.CaretLine; /// public int CaretPosInLine => this.textInput.CaretPosInLine; /// public char? MaskingCharacter { get => this.textInput.MaskingCharacter; set => this.textInput.MaskingCharacter = value; } /// public int? MaximumCharacters { get => this.textInput.MaximumCharacters; set => this.textInput.MaximumCharacters = value; } /// public bool Multiline { get => this.textInput.Multiline; set => this.textInput.Multiline = value; } #if FNA /// // we need to make sure that the enter press doesn't get consumed by our press function so that it still works in TextInput public override bool CanBePressed => base.CanBePressed && !this.IsSelected; #endif /// /// The text that displays in this text field if is empty /// public string PlaceholderText; /// /// 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; /// /// An event that is invoked if is pressed while this text field is active. /// Note that, for text fields that are , this is ignored. /// This also occurs once the text input window is successfully closed on a mobile device. /// If another 's press behavior should be invoked when enter is pressed, can be used instead. /// public GenericCallback OnEnterPressed; /// /// An element that should be pressed (using ) if is pressed while this text field is active. /// Note that, for text fields that are , this is ignored. /// This also occurs once the text input window is successfully closed on a mobile device. /// public Element EnterReceiver; private readonly TextInput textInput; private StyleProp font; private StyleProp textScale; /// /// 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 /// The text that the text field should contain by default /// 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.textInput = new TextInput(null, Vector2.Zero, 1 #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER , null, s => { try { ClipboardService.SetText(s); } catch (Exception e) { TextField.OnCopyPasteException?.Invoke(e); } }, () => { try { return ClipboardService.GetText(); } catch (Exception e) { TextField.OnCopyPasteException?.Invoke(e); return null; } } #endif ) { OnTextChange = (i, s) => this.OnTextChange?.Invoke(this, s), InputRule = (i, s) => this.InputRule.Invoke(this, s) }; this.InputRule = rule ?? TextField.DefaultRule; this.Multiline = multiline; if (font != null) this.Font = font; if (text != null) this.SetText(text, true); 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(this.Multiline ? result : result.Replace('\n', ' '), true); this.InvokeOnEnter(); } }; this.OnTextInput += (element, key, character) => { if (this.IsSelectedActive && !this.IsHidden && !this.textInput.OnTextInput(key, character) && key == Keys.Enter && !this.Multiline) this.InvokeOnEnter(); }; } /// public override void SetAreaAndUpdateChildren(RectangleF area) { base.SetAreaAndUpdateChildren(area); this.textInput.Size = this.DisplayArea.Size / this.Scale - new Vector2(2 * this.TextOffsetX); this.textInput.TextScale = this.TextScale; } /// public override void Update(GameTime time) { base.Update(time); if (this.IsSelectedActive && !this.IsHidden) { this.textInput.Update(time, this.Input); #if FNA // this occurs in OnTextInput outside FNA, where special keys are also counted as text input if ((this.OnEnterPressed != null || this.EnterReceiver != null) && !this.Multiline && this.Input.TryConsumePressed(Keys.Enter)) this.InvokeOnEnter(); #endif } } /// public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) { 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); var lineHeight = this.Font.Value.LineHeight * this.TextScale * this.Scale; var textPos = this.DisplayArea.Location + new Vector2( this.TextOffsetX * this.Scale, this.Multiline ? this.TextOffsetX * this.Scale : this.DisplayArea.Height / 2 - lineHeight / 2); if (this.textInput.Length > 0 || this.IsSelected) { this.textInput.Draw(batch, textPos, this.Scale, this.IsSelected ? this.CaretWidth : 0, this.TextColor.OrDefault(Color.White) * 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, context); } /// public void SetText(object text, bool removeMismatching = false) { this.textInput.SetText(text, removeMismatching); } /// public void InsertText(object text, bool removeMismatching = false) { this.textInput.InsertText(text, removeMismatching); } /// public void RemoveText(int index, int length) { this.textInput.RemoveText(index, length); } /// protected override void InitStyle(UiStyle style) { base.InitStyle(style); this.TextScale = this.TextScale.OrStyle(style.TextScale); this.Font = this.Font.OrStyle(style.Font); this.Texture = this.Texture.OrStyle(style.TextFieldTexture); this.HoveredTexture = this.HoveredTexture.OrStyle(style.TextFieldHoveredTexture); this.HoveredColor = this.HoveredColor.OrStyle(style.TextFieldHoveredColor); this.TextOffsetX = this.TextOffsetX.OrStyle(style.TextFieldTextOffsetX); this.CaretWidth = this.CaretWidth.OrStyle(style.TextFieldCaretWidth); } private void InvokeOnEnter() { this.OnEnterPressed?.Invoke(this); this.EnterReceiver?.Controls?.PressElement(this.EnterReceiver); } /// /// 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); } }