2019-08-10 19:23:08 +02:00
using System ;
2020-09-13 18:05:55 +02:00
using System.IO ;
2021-10-12 18:28:06 +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>
2021-07-18 22:18:46 +02:00
public static readonly Rule DefaultRule = ( field , add ) = > {
foreach ( var c in add ) {
2021-10-12 02:16:09 +02:00
if ( char . IsControl ( c ) & & ( ! field . Multiline | | c ! = '\n' ) )
2021-07-18 22:18:46 +02:00
return false ;
}
return true ;
} ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A <see cref="Rule"/> that only allows letters
/// </summary>
2021-07-18 22:18:46 +02:00
public static readonly Rule OnlyLetters = ( field , add ) = > {
foreach ( var c in add ) {
if ( ! char . IsLetter ( c ) )
return false ;
}
return true ;
} ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A <see cref="Rule"/> that only allows numerals
/// </summary>
2021-07-18 22:18:46 +02:00
public static readonly Rule OnlyNumbers = ( field , add ) = > {
foreach ( var c in add ) {
if ( ! char . IsNumber ( c ) )
return false ;
}
return true ;
} ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A <see cref="Rule"/> that only allows letters and numerals
/// </summary>
2021-07-18 22:18:46 +02:00
public static readonly Rule LettersNumbers = ( field , add ) = > {
foreach ( var c in add ) {
if ( ! char . IsLetter ( c ) | | ! char . IsNumber ( c ) )
return false ;
}
return true ;
} ;
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>
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 ;
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 {
2021-10-12 02:16:09 +02:00
get = > this . caretPos ;
2019-09-05 12:51:40 +02:00
set {
2021-10-12 02:16:09 +02:00
var val = MathHelper . Clamp ( value , 0 , this . text . Length ) ;
if ( this . caretPos ! = val ) {
this . caretPos = val ;
this . caretBlinkTimer = 0 ;
2019-09-05 12:51:40 +02:00
this . HandleTextChange ( false ) ;
}
}
}
2021-07-08 18:17:39 +02:00
/// <summary>
2021-10-12 18:28:06 +02:00
/// The line of text that the caret is currently on.
/// This can only be only non-0 if <see cref="Multiline"/> is true.
/// </summary>
public int CaretLine { get ; private set ; }
/// <summary>
/// The position in the current <see cref="CaretLine"/> that the caret is currently on.
/// If <see cref="Multiline"/> is false, this value is always equal to <see cref="CaretPos"/>.
/// </summary>
public int CaretPosInLine { get ; private set ; }
/// <summary>
2021-07-08 18:17:39 +02:00
/// 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 ) ;
}
}
2021-10-11 23:35:50 +02:00
/// <summary>
/// The maximum amount of characters that can be input into this text field.
/// If this is set, the length of <see cref="Text"/> will never exceed this value.
/// </summary>
public int? MaximumCharacters ;
2021-10-12 02:16:09 +02:00
/// <summary>
/// Whether this text field should support multi-line editing.
/// If this is true, pressing <see cref="Keys.Enter"/> will insert a new line into the <see cref="Text"/> if the <see cref="InputRule"/> allows it.
/// Additionally, text will be rendered with horizontal soft wraps, and lines that are outside of the text field's bounds will be hidden.
/// </summary>
public bool Multiline {
get = > this . multiline ;
set {
this . multiline = value ;
this . HandleTextChange ( false ) ;
}
}
2021-07-08 18:17:39 +02:00
private readonly StringBuilder text = new StringBuilder ( ) ;
private char? maskingCharacter ;
private double caretBlinkTimer ;
private string displayedText ;
2021-10-12 18:28:06 +02:00
private string [ ] splitText ;
2021-07-08 18:17:39 +02:00
private int textOffset ;
2021-10-12 02:16:09 +02:00
private int lineOffset ;
2021-07-08 18:17:39 +02:00
private int caretPos ;
2021-10-12 18:28:06 +02:00
private float caretDrawOffset ;
2021-10-12 02:16:09 +02:00
private bool multiline ;
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 ) {
2019-08-18 17:59:14 +02:00
this . InputRule = rule ? ? DefaultRule ;
2021-10-12 02:16:09 +02:00
this . Multiline = multiline ;
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-07-19 23:49:16 +02:00
this . OnPressed + = OnPressed ;
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 ) ;
2021-10-12 02:16:09 +02:00
} else if ( this . Multiline & & key = = Keys . Enter ) {
this . InsertText ( '\n' ) ;
2020-02-24 14:03:53 +01:00
} 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 ;
2021-07-19 23:49:16 +02:00
async void OnPressed ( Element e ) {
var title = this . MobileTitle ? ? this . PlaceholderText ;
var result = await MlemPlatform . Current . OpenOnScreenKeyboard ( title , this . MobileDescription , this . Text , false ) ;
if ( result ! = null )
2021-10-12 02:16:09 +02:00
this . SetText ( this . Multiline ? result : result . Replace ( '\n' , ' ' ) , true ) ;
2021-07-19 23:49:16 +02:00
}
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 ) ;
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 + + ;
2021-10-13 17:13:56 +02:00
} else if ( this . Multiline & & this . Input . IsKeyPressed ( Keys . Up ) ) {
this . MoveCaretToLine ( this . CaretLine - 1 ) ;
} else if ( this . Multiline & & this . Input . IsKeyPressed ( Keys . Down ) ) {
this . MoveCaretToLine ( this . CaretLine + 1 ) ;
2019-09-05 12:51:40 +02:00
} 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 )
2021-10-12 19:58:31 +02:00
this . InsertText ( clip , true ) ;
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 ;
2021-10-12 02:16:09 +02:00
var offset = new Vector2 (
this . TextOffsetX * this . Scale ,
this . Multiline ? this . TextOffsetX * this . Scale : this . DisplayArea . Height / 2 - lineHeight / 2 ) ;
var textPos = this . DisplayArea . Location + offset ;
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 ) ;
2021-10-12 02:16:09 +02:00
if ( this . IsSelected & & this . caretBlinkTimer < 0.5F ) {
2021-10-12 18:28:06 +02:00
var caretDrawPos = textPos + new Vector2 ( this . caretDrawOffset * this . TextScale * this . Scale , 0 ) ;
if ( this . Multiline )
caretDrawPos . Y + = this . Font . Value . LineHeight * ( this . CaretLine - this . lineOffset ) * this . TextScale * this . Scale ;
2021-10-12 02:16:09 +02:00
batch . Draw ( batch . GetBlankTexture ( ) , new RectangleF ( caretDrawPos , new Vector2 ( 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.
2021-10-11 23:35:50 +02:00
/// If the resulting <see cref="Text"/> exceeds <see cref="MaximumCharacters"/>, the end will be cropped to fit.
2020-05-22 17:02:24 +02:00
/// </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 ) {
2021-10-11 23:35:50 +02:00
var strg = text ? . ToString ( ) ? ? string . Empty ;
2021-10-12 19:58:31 +02:00
if ( ! this . FilterText ( ref strg , removeMismatching ) )
2019-08-24 12:40:20 +02:00
return ;
2021-10-11 23:35:50 +02:00
if ( this . MaximumCharacters ! = null & & strg . Length > this . MaximumCharacters )
strg = strg . Substring ( 0 , this . MaximumCharacters . Value ) ;
2019-08-24 12:40:20 +02:00
this . text . Clear ( ) ;
2021-10-11 23:35:50 +02:00
this . text . Append ( strg ) ;
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>
2021-10-11 23:35:50 +02:00
/// Inserts the given text at the <see cref="CaretPos"/>.
/// If the resulting <see cref="Text"/> exceeds <see cref="MaximumCharacters"/>, the end will be cropped to fit.
2020-05-22 17:02:24 +02:00
/// </summary>
/// <param name="text">The text to insert</param>
2021-10-12 19:58:31 +02:00
/// <param name="removeMismatching">If any characters that don't match the <see cref="InputRule"/> should be left out</param>
public void InsertText ( object text , bool removeMismatching = false ) {
var strg = text ? . ToString ( ) ? ? string . Empty ;
if ( ! this . FilterText ( ref strg , removeMismatching ) )
2019-08-24 12:40:20 +02:00
return ;
2021-10-11 23:35:50 +02:00
if ( this . MaximumCharacters ! = null & & this . text . Length + strg . Length > this . MaximumCharacters )
strg = strg . Substring ( 0 , this . MaximumCharacters . Value - this . text . Length ) ;
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 ) ;
2021-10-12 02:46:24 +02:00
// ensure that caret pos is still in bounds
this . CaretPos = this . CaretPos ;
2019-08-24 12:40:20 +02:00
this . HandleTextChange ( ) ;
}
2021-10-13 17:13:56 +02:00
/// <summary>
/// Moves the <see cref="CaretPos"/> to the given line, if it exists.
/// Additionally maintains the <see cref="CaretPosInLine"/> roughly based on the visual distance that the caret has from the left border of the current <see cref="CaretLine"/>.
/// </summary>
/// <param name="line">The line to move the caret to</param>
/// <returns>True if the caret was moved, false if it was not (which indicates that the line with the given <paramref name="line"/> index does not exist)</returns>
public bool MoveCaretToLine ( int line ) {
var ( destStart , destEnd ) = this . GetLineBounds ( line ) ;
if ( destEnd > 0 ) {
// find the position whose distance from the start is closest to the current distance from the start
var destAccum = "" ;
while ( destAccum . Length < destEnd - destStart ) {
if ( this . Font . Value . MeasureString ( destAccum ) . X > = this . caretDrawOffset ) {
this . CaretPos = destStart + destAccum . Length ;
return true ;
}
destAccum + = this . text [ destStart + destAccum . Length ] ;
}
// if we don't find a proper position, just move to the end of the destination line
this . CaretPos = destEnd ;
return true ;
}
return false ;
}
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 ) ;
2021-10-29 23:33:15 +02:00
this . TextOffsetX . SetFromStyle ( style . TextFieldTextOffsetX ) ;
this . CaretWidth . SetFromStyle ( style . TextFieldCaretWidth ) ;
2019-08-10 21:37:10 +02:00
}
2021-10-12 19:58:31 +02:00
private bool FilterText ( ref string text , bool removeMismatching ) {
if ( removeMismatching ) {
var result = new StringBuilder ( ) ;
foreach ( var c in text ) {
if ( this . InputRule ( this , c . ToCachedString ( ) ) )
result . Append ( c ) ;
}
text = result . ToString ( ) ;
} else if ( ! this . InputRule ( this , text ) )
return false ;
return true ;
}
private void HandleTextChange ( bool textChanged = true ) {
// not initialized yet
if ( ! this . Font . HasValue ( ) )
return ;
var maxWidth = this . DisplayArea . Width / this . Scale - this . TextOffsetX * 2 ;
if ( this . Multiline ) {
// soft wrap if we're multiline
this . splitText = this . Font . Value . SplitStringSeparate ( this . text . ToString ( ) , maxWidth , this . TextScale ) . ToArray ( ) ;
this . displayedText = string . Join ( "\n" , this . splitText ) ;
this . UpdateCaretData ( ) ;
var maxHeight = this . DisplayArea . Height / this . Scale - this . TextOffsetX * 2 ;
if ( this . Font . Value . MeasureString ( this . displayedText ) . Y * this . TextScale > maxHeight ) {
var maxLines = ( maxHeight / ( this . Font . Value . LineHeight * this . TextScale ) ) . Floor ( ) ;
if ( this . lineOffset > this . CaretLine ) {
// if we're moving up
this . lineOffset = this . CaretLine ;
} else if ( this . CaretLine > = maxLines ) {
// if we're moving down
var limit = this . CaretLine - ( maxLines - 1 ) ;
if ( limit > this . lineOffset )
this . lineOffset = limit ;
}
// calculate resulting string
var ret = new StringBuilder ( ) ;
var lines = 0 ;
var originalIndex = 0 ;
for ( var i = 0 ; i < this . displayedText . Length ; i + + ) {
if ( lines > = this . lineOffset ) {
if ( ret . Length < = 0 )
this . textOffset = originalIndex ;
ret . Append ( this . displayedText [ i ] ) ;
}
if ( this . displayedText [ i ] = = '\n' ) {
lines + + ;
if ( this . text [ originalIndex ] = = '\n' )
originalIndex + + ;
} else {
originalIndex + + ;
}
if ( lines - this . lineOffset > = maxLines )
break ;
}
this . displayedText = ret . ToString ( ) ;
} else {
this . lineOffset = 0 ;
this . textOffset = 0 ;
}
} else {
// not multiline, so scroll horizontally based on caret position
if ( this . Font . Value . MeasureString ( this . text . ToString ( ) ) . X * this . TextScale > maxWidth ) {
if ( this . textOffset > this . CaretPos ) {
// if we're moving the caret to the left
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 ;
}
this . UpdateCaretData ( ) ;
}
if ( this . MaskingCharacter ! = null )
this . displayedText = new string ( this . MaskingCharacter . Value , this . displayedText . Length ) ;
if ( textChanged )
this . OnTextChange ? . Invoke ( this , this . Text ) ;
}
private void UpdateCaretData ( ) {
if ( this . splitText ! = null ) {
var line = 0 ;
var index = 0 ;
for ( var d = 0 ; d < this . splitText . Length ; d + + ) {
var startOfLine = 0 ;
var split = this . splitText [ d ] ;
for ( var i = 0 ; i < = split . Length ; i + + ) {
if ( index = = this . CaretPos ) {
this . CaretLine = line ;
this . CaretPosInLine = i - startOfLine ;
this . caretDrawOffset = this . Font . Value . MeasureString ( split . Substring ( startOfLine , this . CaretPosInLine ) ) . X ;
return ;
}
if ( i < split . Length ) {
// manual splits
if ( split [ i ] = = '\n' ) {
startOfLine = i + 1 ;
line + + ;
}
index + + ;
}
}
// max width splits
line + + ;
}
} else if ( this . displayedText ! = null ) {
this . CaretLine = 0 ;
this . CaretPosInLine = this . CaretPos ;
this . caretDrawOffset = this . Font . Value . MeasureString ( this . displayedText . Substring ( 0 , this . CaretPos - this . textOffset ) ) . X ;
}
}
2021-10-13 17:13:56 +02:00
private ( int , int ) GetLineBounds ( int boundLine ) {
if ( this . splitText ! = null ) {
var line = 0 ;
var index = 0 ;
var startOfLineIndex = 0 ;
for ( var d = 0 ; d < this . splitText . Length ; d + + ) {
var split = this . splitText [ d ] ;
for ( var i = 0 ; i < split . Length ; i + + ) {
index + + ;
if ( split [ i ] = = '\n' ) {
if ( boundLine = = line )
return ( startOfLineIndex , index - 1 ) ;
line + + ;
startOfLineIndex = index ;
}
}
if ( boundLine = = line )
return ( startOfLineIndex , index - 1 ) ;
line + + ;
startOfLineIndex = index ;
}
}
return default ;
}
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
}
}