2019-08-10 19:23:08 +02:00
using System ;
2020-09-13 18:05:55 +02:00
using System.IO ;
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 {
2020-05-22 17:02:24 +02:00
/// <summary>
/// A text field element for use inside of a <see cref="UiSystem"/>.
/// A text field is a selectable element that can be typed in, as well as copied and pasted from.
2021-04-23 00:17:46 +02:00
/// If an on-screen keyboard is required, then this text field will automatically open an on-screen keyboard using <see cref="MlemPlatform.OpenOnScreenKeyboard"/>.
2020-05-22 17:02:24 +02:00
/// </summary>
2019-08-10 19:23:08 +02:00
public class TextField : Element {
2020-05-22 17:02:24 +02:00
/// <summary>
/// A <see cref="Rule"/> that allows any visible character and spaces
/// </summary>
2019-08-18 17:59:14 +02:00
public static readonly Rule DefaultRule = ( field , add ) = > ! add . Any ( char . IsControl ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A <see cref="Rule"/> that only allows letters
/// </summary>
2019-08-18 17:59:14 +02:00
public static readonly Rule OnlyLetters = ( field , add ) = > add . All ( char . IsLetter ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A <see cref="Rule"/> that only allows numerals
/// </summary>
2019-08-18 17:59:14 +02:00
public static readonly Rule OnlyNumbers = ( field , add ) = > add . All ( char . IsNumber ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A <see cref="Rule"/> that only allows letters and numerals
/// </summary>
2019-08-18 17:59:14 +02:00
public static readonly Rule LettersNumbers = ( field , add ) = > add . All ( c = > char . IsLetter ( c ) | | char . IsNumber ( c ) ) ;
2020-09-13 18:05:55 +02:00
/// <summary>
/// A <see cref="Rule"/> that only allows characters not contained in <see cref="Path.GetInvalidPathChars"/>
/// </summary>
public static readonly Rule PathNames = ( field , add ) = > add . IndexOfAny ( Path . GetInvalidPathChars ( ) ) < 0 ;
/// <summary>
/// A <see cref="Rule"/> that only allows characters not contained in <see cref="Path.GetInvalidFileNameChars"/>
/// </summary>
public static readonly Rule FileNames = ( field , add ) = > add . IndexOfAny ( Path . GetInvalidFileNameChars ( ) ) < 0 ;
2019-08-18 17:59:14 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// The color that this text field's text should display with
/// </summary>
2020-03-19 03:27:21 +01:00
public StyleProp < Color > TextColor ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The color that the <see cref="PlaceholderText"/> should display with
/// </summary>
2020-03-19 03:27:21 +01:00
public StyleProp < Color > PlaceholderColor ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// This text field's texture
/// </summary>
2019-10-14 21:28:12 +02:00
public StyleProp < NinePatch > Texture ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// This text field's texture while it is hovered
/// </summary>
2019-10-14 21:28:12 +02:00
public StyleProp < NinePatch > HoveredTexture ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The color that this text field should display with while it is hovered
/// </summary>
2019-10-14 21:28:12 +02:00
public StyleProp < Color > HoveredColor ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The scale that this text field should render text with
/// </summary>
2019-10-14 21:28:12 +02:00
public StyleProp < float > TextScale ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The font that this text field should display text with
/// </summary>
2020-03-28 22:25:06 +01:00
public StyleProp < GenericFont > Font ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// This text field's current text
/// </summary>
2019-08-24 12:40:20 +02:00
public string Text = > this . text . ToString ( ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The text that displays in this text field if <see cref="Text"/> is empty
/// </summary>
2019-08-23 18:56:39 +02:00
public string PlaceholderText ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// An event that gets called when <see cref="Text"/> changes, either through input, or through a manual change.
/// </summary>
2019-08-10 19:23:08 +02:00
public TextChanged OnTextChange ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The x position that text should start rendering at, based on the x position of this text field.
/// </summary>
2019-08-10 19:23:08 +02:00
public float TextOffsetX = 4 ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The width that the caret should render with.
/// </summary>
2020-03-19 03:27:21 +01:00
public float CaretWidth = 0.5F ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The rule used for text input.
/// Rules allow only certain characters to be allowed inside of a text field.
/// </summary>
2019-08-18 17:59:14 +02:00
public Rule InputRule ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-04-22 19:26:07 +02:00
/// The title of the <c>KeyboardInput</c> field on mobile devices and consoles
2020-05-22 17:02:24 +02:00
/// </summary>
2019-08-30 19:05:27 +02:00
public string MobileTitle ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-04-22 19:26:07 +02:00
/// The description of the <c>KeyboardInput</c> field on mobile devices and consoles
2020-05-22 17:02:24 +02:00
/// </summary>
2019-08-30 19:05:27 +02:00
public string MobileDescription ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The position of the caret within the text.
/// This is always between 0 and the <see cref="string.Length"/> of <see cref="Text"/>
/// </summary>
2019-09-05 12:51:40 +02:00
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 ) ;
}
}
}
2021-07-08 18:17:39 +02:00
/// <summary>
/// A character that should be displayed instead of this text field's <see cref="Text"/> content.
/// The amount of masking characters displayed will be equal to the <see cref="Text"/>'s length.
/// This behavior is useful for password fields or similar.
/// </summary>
public char? MaskingCharacter {
get = > this . maskingCharacter ;
set {
this . maskingCharacter = value ;
this . HandleTextChange ( false ) ;
}
}
private readonly StringBuilder text = new StringBuilder ( ) ;
private char? maskingCharacter ;
private double caretBlinkTimer ;
private string displayedText ;
private int textOffset ;
private int caretPos ;
2019-08-10 19:23:08 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a new text field with the given settings
/// </summary>
/// <param name="anchor">The text field's anchor</param>
/// <param name="size">The text field's size</param>
/// <param name="rule">The text field's input rule</param>
/// <param name="font">The font to use for drawing text</param>
2021-07-08 18:17:39 +02:00
/// <param name="text">The text that the text field should contain by default</param>
public TextField ( Anchor anchor , Vector2 size , Rule rule = null , GenericFont font = null , string text = 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 ) ;
2021-07-08 18:17:39 +02:00
if ( text ! = null )
this . SetText ( text , true ) ;
2019-08-30 19:05:27 +02:00
2021-04-23 00:17:46 +02:00
MlemPlatform . EnsureExists ( ) ;
2021-04-22 19:40:14 +02:00
this . OnPressed + = async e = > {
var title = this . MobileTitle ? ? this . PlaceholderText ;
2021-04-23 00:17:46 +02:00
var result = await MlemPlatform . Current . OpenOnScreenKeyboard ( title , this . MobileDescription , this . Text , false ) ;
2021-04-22 19:40:14 +02:00
if ( result ! = null )
this . SetText ( result . Replace ( '\n' , ' ' ) , true ) ;
} ;
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 ;
2020-06-20 01:18:27 +02:00
var length = this . Font . Value . MeasureString ( this . text . ToString ( ) ) . 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
}
2021-07-08 18:17:39 +02:00
if ( this . MaskingCharacter ! = null )
this . displayedText = new string ( this . MaskingCharacter . Value , this . displayedText . Length ) ;
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
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
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 ) ) {
2021-03-14 01:03:17 +01:00
var clip = ClipboardService . GetText ( ) ;
2020-03-02 09:40:07 +01:00
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
2021-03-14 01:03:17 +01:00
ClipboardService . SetText ( this . Text ) ;
2020-02-27 17:51:44 +01:00
}
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 ;
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
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
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Replaces this text field's text with the given text.
/// </summary>
/// <param name="text">The new text</param>
/// <param name="removeMismatching">If any characters that don't match the <see cref="InputRule"/> should be left out</param>
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 ( ) ) {
2020-09-28 20:43:37 +02:00
if ( this . InputRule ( this , c . ToCachedString ( ) ) )
2019-09-01 19:50:17 +02:00
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 ( ) ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Inserts the given text at the <see cref="CaretPos"/>
/// </summary>
/// <param name="text">The text to insert</param>
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 ( ) ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Removes the given amount of text at the given index
/// </summary>
/// <param name="index">The index</param>
/// <param name="length">The amount of text to remove</param>
2019-08-24 12:40:20 +02:00
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 ( ) ;
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
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
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// A delegate method used for <see cref="TextField.OnTextChange"/>
/// </summary>
/// <param name="field">The text field whose text changed</param>
/// <param name="text">The new text</param>
2019-08-10 19:23:08 +02:00
public delegate void TextChanged ( TextField field , string text ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A delegate method used for <see cref="InputRule"/>.
/// It should return whether the given text can be added to the text field.
/// </summary>
/// <param name="field">The text field</param>
/// <param name="textToAdd">The text that is tried to be added</param>
2019-08-18 17:59:14 +02:00
public delegate bool Rule ( TextField field , string textToAdd ) ;
2019-08-10 19:23:08 +02:00
}
}