2022-12-22 11:39:07 +01:00
using System ;
2019-08-10 19:23:08 +02:00
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
2023-06-21 00:10:52 +02:00
using Microsoft.Xna.Framework.Input ;
2019-08-10 19:23:08 +02:00
using MLEM.Font ;
2022-04-25 15:25:58 +02:00
using MLEM.Graphics ;
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 ;
2022-09-14 21:17:43 +02:00
#if NETSTANDARD2_0_OR_GREATER | | NET6_0_OR_GREATER
2020-02-27 17:51:44 +01:00
using TextCopy ;
2022-09-14 21:17:43 +02:00
#endif
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"/>.
2022-06-19 18:17:46 +02:00
/// This class interally uses MLEM's <see cref="TextInput"/>.
2020-05-22 17:02:24 +02:00
/// </summary>
2019-08-10 19:23:08 +02:00
public class TextField : Element {
2022-06-19 18:17:46 +02:00
/// <inheritdoc cref="TextInput.DefaultRule"/>
public static readonly Rule DefaultRule = ( field , add ) = > TextInput . DefaultRule ( field . textInput , add ) ;
/// <inheritdoc cref="TextInput.OnlyLetters"/>
public static readonly Rule OnlyLetters = ( field , add ) = > TextInput . OnlyLetters ( field . textInput , add ) ;
/// <inheritdoc cref="TextInput.OnlyNumbers"/>
public static readonly Rule OnlyNumbers = ( field , add ) = > TextInput . OnlyNumbers ( field . textInput , add ) ;
/// <inheritdoc cref="TextInput.LettersNumbers"/>
public static readonly Rule LettersNumbers = ( field , add ) = > TextInput . LettersNumbers ( field . textInput , add ) ;
/// <inheritdoc cref="TextInput.PathNames"/>
public static readonly Rule PathNames = ( field , add ) = > TextInput . PathNames ( field . textInput , add ) ;
/// <inheritdoc cref="TextInput.FileNames"/>
public static readonly Rule FileNames = ( field , add ) = > TextInput . FileNames ( field . textInput , add ) ;
2019-08-18 17:59:14 +02:00
2022-12-22 11:39:07 +01:00
#if NETSTANDARD2_0_OR_GREATER | | NET6_0_OR_GREATER
/// <summary>
/// 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.
/// </summary>
public static event Action < Exception > OnCopyPasteException ;
#endif
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>
2022-06-19 18:17:46 +02:00
public StyleProp < float > TextScale {
get = > this . textScale ;
set {
this . textScale = value ;
this . textInput . TextScale = value ;
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The font that this text field should display text with
/// </summary>
2022-06-19 18:17:46 +02:00
public StyleProp < GenericFont > Font {
get = > this . font ;
set {
this . font = value ;
this . textInput . Font = value ;
}
}
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>
2021-10-29 23:33:15 +02:00
public StyleProp < float > TextOffsetX ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-10-29 23:33:15 +02:00
/// The width that the caret should render with, in pixels
2020-05-22 17:02:24 +02:00
/// </summary>
2021-10-29 23:33:15 +02:00
public StyleProp < float > CaretWidth ;
2022-06-19 18:17:46 +02:00
/// <inheritdoc cref="TextInput.Text"/>
public string Text = > this . textInput . Text ;
/// <inheritdoc cref="TextInput.OnTextChange"/>
public TextChanged OnTextChange ;
/// <inheritdoc cref="TextInput.InputRule"/>
public Rule InputRule ;
/// <inheritdoc cref="TextInput.CaretPos"/>
public int CaretPos {
get = > this . textInput . CaretPos ;
set = > this . textInput . CaretPos = value ;
}
/// <inheritdoc cref="TextInput.CaretLine"/>
public int CaretLine = > this . textInput . CaretLine ;
/// <inheritdoc cref="TextInput.CaretPosInLine"/>
public int CaretPosInLine = > this . textInput . CaretPosInLine ;
/// <inheritdoc cref="TextInput.MaskingCharacter"/>
public char? MaskingCharacter {
get = > this . textInput . MaskingCharacter ;
set = > this . textInput . MaskingCharacter = value ;
}
/// <inheritdoc cref="TextInput.MaximumCharacters"/>
public int? MaximumCharacters {
get = > this . textInput . MaximumCharacters ;
set = > this . textInput . MaximumCharacters = value ;
}
/// <inheritdoc cref="TextInput.Multiline"/>
public bool Multiline {
get = > this . textInput . Multiline ;
set = > this . textInput . Multiline = value ;
}
2022-12-13 13:11:36 +01:00
#if FNA
2022-06-24 14:10:24 +02:00
/// <inheritdoc />
// 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 ;
2022-12-13 13:11:36 +01:00
#endif
2022-06-24 14:10:24 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
2022-06-19 18:17:46 +02:00
/// The text that displays in this text field if <see cref="Text"/> is empty
2020-05-22 17:02:24 +02:00
/// </summary>
2022-06-19 18:17:46 +02:00
public string PlaceholderText ;
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 ;
2023-06-21 00:10:52 +02:00
/// <summary>
/// An element that should be pressed (using <see cref="UiControls.PressElement"/>) if <see cref="Keys.Enter"/> is pressed while this text field is active.
/// Note that, for text fields that are <see cref="Multiline"/>, this is ignored.
/// This also occurs once the text input window is successfully closed on a mobile device.
/// </summary>
public Element EnterReceiver ;
2021-07-08 18:17:39 +02:00
2022-06-19 18:17:46 +02:00
private readonly TextInput textInput ;
private StyleProp < GenericFont > font ;
private StyleProp < float > textScale ;
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>
2021-10-12 02:16:09 +02:00
/// <param name="multiline">Whether the text field should support multi-line editing</param>
public TextField ( Anchor anchor , Vector2 size , Rule rule = null , GenericFont font = null , string text = null , bool multiline = false ) : base ( anchor , size ) {
2022-09-14 21:17:43 +02:00
this . textInput = new TextInput ( null , Vector2 . Zero , 1
2022-12-13 13:11:36 +01:00
#if NETSTANDARD2_0_OR_GREATER | | NET6_0_OR_GREATER
2022-12-22 11:39:07 +01:00
, 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 ;
}
}
2022-12-13 13:11:36 +01:00
#endif
2022-09-14 21:17:43 +02:00
) {
2022-06-19 18:17:46 +02:00
OnTextChange = ( i , s ) = > this . OnTextChange ? . Invoke ( this , s ) ,
InputRule = ( i , s ) = > this . InputRule . Invoke ( this , s )
} ;
2022-06-15 11:38:11 +02:00
this . InputRule = rule ? ? TextField . DefaultRule ;
2021-10-12 02:16:09 +02:00
this . Multiline = multiline ;
2019-10-14 21:28:12 +02:00
if ( font ! = null )
2021-12-21 00:01:57 +01:00
this . Font = 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-07-19 23:49:16 +02:00
2022-06-19 18:17:46 +02:00
this . OnPressed + = async e = > {
2021-07-19 23:49:16 +02:00
var title = this . MobileTitle ? ? this . PlaceholderText ;
var result = await MlemPlatform . Current . OpenOnScreenKeyboard ( title , this . MobileDescription , this . Text , false ) ;
2023-06-21 00:10:52 +02:00
if ( result ! = null ) {
2021-10-12 02:16:09 +02:00
this . SetText ( this . Multiline ? result : result . Replace ( '\n' , ' ' ) , true ) ;
2023-06-21 00:10:52 +02:00
this . EnterReceiver ? . Controls ? . PressElement ( this . EnterReceiver ) ;
}
2022-06-19 18:17:46 +02:00
} ;
this . OnTextInput + = ( element , key , character ) = > {
2023-06-21 00:10:52 +02:00
if ( this . IsSelectedActive & & ! this . IsHidden & & ! this . textInput . OnTextInput ( key , character ) & & key = = Keys . Enter & & ! this . Multiline )
this . EnterReceiver ? . Controls ? . PressElement ( this . EnterReceiver ) ;
2022-06-19 18:17:46 +02:00
} ;
this . OnDeselected + = e = > this . CaretPos = 0 ;
this . OnSelected + = e = > this . CaretPos = this . textInput . Length ;
}
/// <inheritdoc />
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 ;
2019-08-24 12:40:20 +02:00
}
2019-08-11 00:39:40 +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 ) ;
2023-06-21 00:10:52 +02:00
if ( this . IsSelectedActive & & ! this . IsHidden ) {
2022-06-19 18:17:46 +02:00
this . textInput . Update ( time , this . Input ) ;
2023-06-21 00:10:52 +02:00
#if FNA
// this occurs in OnTextInput outside FNA, where special keys are also counted as text input
if ( this . EnterReceiver ! = null & & ! this . Multiline & & this . Input . TryConsumePressed ( Keys . Enter ) )
this . EnterReceiver . Controls ? . PressElement ( this . EnterReceiver ) ;
#endif
}
2019-08-10 19:23:08 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2022-04-25 15:25:58 +02:00
public override void Draw ( GameTime time , SpriteBatch batch , float alpha , SpriteBatchContext context ) {
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
2022-06-19 18:17:46 +02:00
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 ) ;
2019-08-23 18:56:39 +02:00
}
2022-04-25 15:25:58 +02:00
base . Draw ( time , batch , alpha , context ) ;
2019-08-10 19:23:08 +02:00
}
2022-06-19 18:17:46 +02:00
/// <inheritdoc cref="TextInput.SetText"/>
2019-09-01 19:50:17 +02:00
public void SetText ( object text , bool removeMismatching = false ) {
2022-06-19 18:17:46 +02:00
this . textInput . SetText ( text , removeMismatching ) ;
2019-08-24 12:40:20 +02:00
}
2022-06-19 18:17:46 +02:00
/// <inheritdoc cref="TextInput.InsertText"/>
2021-10-12 19:58:31 +02:00
public void InsertText ( object text , bool removeMismatching = false ) {
2022-06-19 18:17:46 +02:00
this . textInput . InsertText ( text , removeMismatching ) ;
2019-08-24 12:40:20 +02:00
}
2022-06-19 18:17:46 +02:00
/// <inheritdoc cref="TextInput.RemoveText"/>
2019-08-24 12:40:20 +02:00
public void RemoveText ( int index , int length ) {
2022-06-19 18:17:46 +02:00
this . textInput . RemoveText ( index , length ) ;
2021-10-13 17:13:56 +02:00
}
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 ) ;
2021-12-21 11:54:32 +01:00
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 ) ;
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
}
2022-06-17 18:23:47 +02:00
}