From c55fba8ab2a1c9d3df65eda4a5a1e957f4aa1f78 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Sun, 19 Apr 2020 17:19:32 -0400 Subject: Inital commit --- .../Scripts/Runtime/TMP_InputField.cs | 4149 ++++++++++++++++++++ 1 file changed, 4149 insertions(+) create mode 100644 Library/PackageCache/com.unity.textmeshpro@2.0.1/Scripts/Runtime/TMP_InputField.cs (limited to 'Library/PackageCache/com.unity.textmeshpro@2.0.1/Scripts/Runtime/TMP_InputField.cs') diff --git a/Library/PackageCache/com.unity.textmeshpro@2.0.1/Scripts/Runtime/TMP_InputField.cs b/Library/PackageCache/com.unity.textmeshpro@2.0.1/Scripts/Runtime/TMP_InputField.cs new file mode 100644 index 0000000..33ec8be --- /dev/null +++ b/Library/PackageCache/com.unity.textmeshpro@2.0.1/Scripts/Runtime/TMP_InputField.cs @@ -0,0 +1,4149 @@ +//#define TMP_DEBUG_MODE + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.Events; +using UnityEngine.EventSystems; +using UnityEngine.Serialization; + + +namespace TMPro +{ + /// + /// Editable text input field. + /// + [AddComponentMenu("UI/TextMeshPro - Input Field", 11)] + public class TMP_InputField : Selectable, + IUpdateSelectedHandler, + IBeginDragHandler, + IDragHandler, + IEndDragHandler, + IPointerClickHandler, + ISubmitHandler, + ICanvasElement, + ILayoutElement, + IScrollHandler + { + + // Setting the content type acts as a shortcut for setting a combination of InputType, CharacterValidation, LineType, and TouchScreenKeyboardType + public enum ContentType + { + Standard, + Autocorrected, + IntegerNumber, + DecimalNumber, + Alphanumeric, + Name, + EmailAddress, + Password, + Pin, + Custom + } + + public enum InputType + { + Standard, + AutoCorrect, + Password, + } + + public enum CharacterValidation + { + None, + Digit, + Integer, + Decimal, + Alphanumeric, + Name, + Regex, + EmailAddress, + CustomValidator + } + + public enum LineType + { + SingleLine, + MultiLineSubmit, + MultiLineNewline + } + + public delegate char OnValidateInput(string text, int charIndex, char addedChar); + + [Serializable] + public class SubmitEvent : UnityEvent { } + + [Serializable] + public class OnChangeEvent : UnityEvent { } + + [Serializable] + public class SelectionEvent : UnityEvent { } + + [Serializable] + public class TextSelectionEvent : UnityEvent { } + + [Serializable] + public class TouchScreenKeyboardEvent : UnityEvent { } + + protected TouchScreenKeyboard m_SoftKeyboard; + static private readonly char[] kSeparators = { ' ', '.', ',', '\t', '\r', '\n' }; + + #region Exposed properties + /// + /// Text Text used to display the input's value. + /// + + [SerializeField] + protected RectTransform m_TextViewport; + + //Vector3[] m_ViewportCorners = new Vector3[4]; + + [SerializeField] + protected TMP_Text m_TextComponent; + + protected RectTransform m_TextComponentRectTransform; + + [SerializeField] + protected Graphic m_Placeholder; + + [SerializeField] + protected Scrollbar m_VerticalScrollbar; + + [SerializeField] + protected TMP_ScrollbarEventHandler m_VerticalScrollbarEventHandler; + //private bool m_ForceDeactivation; + + private bool m_IsDrivenByLayoutComponents = false; + + /// + /// Used to keep track of scroll position + /// + private float m_ScrollPosition; + + /// + /// + /// + [SerializeField] + protected float m_ScrollSensitivity = 1.0f; + + //[SerializeField] + //protected TMP_Text m_PlaceholderTextComponent; + + [SerializeField] + private ContentType m_ContentType = ContentType.Standard; + + /// + /// Type of data expected by the input field. + /// + [SerializeField] + private InputType m_InputType = InputType.Standard; + + /// + /// The character used to hide text in password field. + /// + [SerializeField] + private char m_AsteriskChar = '*'; + + /// + /// Keyboard type applies to mobile keyboards that get shown. + /// + [SerializeField] + private TouchScreenKeyboardType m_KeyboardType = TouchScreenKeyboardType.Default; + + [SerializeField] + private LineType m_LineType = LineType.SingleLine; + + /// + /// Should hide mobile input field part of the virtual keyboard. + /// + [SerializeField] + private bool m_HideMobileInput = false; + + /// + /// Should hide soft / virtual keyboard. + /// + [SerializeField] + private bool m_HideSoftKeyboard = false; + + /// + /// What kind of validation to use with the input field's data. + /// + [SerializeField] + private CharacterValidation m_CharacterValidation = CharacterValidation.None; + + /// + /// The Regex expression used for validating the text input. + /// + [SerializeField] + private string m_RegexValue = string.Empty; + + /// + /// The point sized used by the placeholder and input text object. + /// + [SerializeField] + private float m_GlobalPointSize = 14; + + /// + /// Maximum number of characters allowed before input no longer works. + /// + [SerializeField] + private int m_CharacterLimit = 0; + + /// + /// Event delegates triggered when the input field submits its data. + /// + [SerializeField] + private SubmitEvent m_OnEndEdit = new SubmitEvent(); + + /// + /// Event delegates triggered when the input field submits its data. + /// + [SerializeField] + private SubmitEvent m_OnSubmit = new SubmitEvent(); + + /// + /// Event delegates triggered when the input field is focused. + /// + [SerializeField] + private SelectionEvent m_OnSelect = new SelectionEvent(); + + /// + /// Event delegates triggered when the input field focus is lost. + /// + [SerializeField] + private SelectionEvent m_OnDeselect = new SelectionEvent(); + + /// + /// Event delegates triggered when the text is selected / highlighted. + /// + [SerializeField] + private TextSelectionEvent m_OnTextSelection = new TextSelectionEvent(); + + /// + /// Event delegates triggered when text is no longer select / highlighted. + /// + [SerializeField] + private TextSelectionEvent m_OnEndTextSelection = new TextSelectionEvent(); + + /// + /// Event delegates triggered when the input field changes its data. + /// + [SerializeField] + private OnChangeEvent m_OnValueChanged = new OnChangeEvent(); + + /// + /// Event delegates triggered when the status of the TouchScreenKeyboard changes. + /// + [SerializeField] + private TouchScreenKeyboardEvent m_OnTouchScreenKeyboardStatusChanged = new TouchScreenKeyboardEvent(); + + /// + /// Custom validation callback. + /// + [SerializeField] + private OnValidateInput m_OnValidateInput; + + [SerializeField] + private Color m_CaretColor = new Color(50f / 255f, 50f / 255f, 50f / 255f, 1f); + + [SerializeField] + private bool m_CustomCaretColor = false; + + [SerializeField] + private Color m_SelectionColor = new Color(168f / 255f, 206f / 255f, 255f / 255f, 192f / 255f); + + /// + /// Input field's value. + /// + + [SerializeField] + [TextArea(5, 10)] + protected string m_Text = string.Empty; + + [SerializeField] + [Range(0f, 4f)] + private float m_CaretBlinkRate = 0.85f; + + [SerializeField] + [Range(1, 5)] + private int m_CaretWidth = 1; + + [SerializeField] + private bool m_ReadOnly = false; + + [SerializeField] + private bool m_RichText = true; + + #endregion + + protected int m_StringPosition = 0; + protected int m_StringSelectPosition = 0; + protected int m_CaretPosition = 0; + protected int m_CaretSelectPosition = 0; + + private RectTransform caretRectTrans = null; + protected UIVertex[] m_CursorVerts = null; + private CanvasRenderer m_CachedInputRenderer; + private Vector2 m_LastPosition; + + [NonSerialized] + protected Mesh m_Mesh; + private bool m_AllowInput = false; + //bool m_HasLostFocus = false; + private bool m_ShouldActivateNextUpdate = false; + private bool m_UpdateDrag = false; + private bool m_DragPositionOutOfBounds = false; + private const float kHScrollSpeed = 0.05f; + private const float kVScrollSpeed = 0.10f; + protected bool m_CaretVisible; + private Coroutine m_BlinkCoroutine = null; + private float m_BlinkStartTime = 0.0f; + private Coroutine m_DragCoroutine = null; + private string m_OriginalText = ""; + private bool m_WasCanceled = false; + private bool m_HasDoneFocusTransition = false; + private WaitForSecondsRealtime m_WaitForSecondsRealtime; + private bool m_PreventCallback = false; + + private bool m_TouchKeyboardAllowsInPlaceEditing = false; + + private bool m_IsTextComponentUpdateRequired = false; + private bool m_IsScrollbarUpdateRequired = false; + private bool m_IsUpdatingScrollbarValues = false; + + private bool m_isLastKeyBackspace = false; + private float m_PointerDownClickStartTime; + private float m_KeyDownStartTime; + private float m_DoubleClickDelay = 0.5f; + + // Doesn't include dot and @ on purpose! See usage for details. + const string kEmailSpecialCharacters = "!#$%&'*+-/=?^_`{|}~"; + + private BaseInput inputSystem + { + get + { + if (EventSystem.current && EventSystem.current.currentInputModule) + return EventSystem.current.currentInputModule.input; + return null; + } + } + + private string compositionString + { + get { return inputSystem != null ? inputSystem.compositionString : Input.compositionString; } + } + + + + protected TMP_InputField() + { + SetTextComponentWrapMode(); + } + + protected Mesh mesh + { + get + { + if (m_Mesh == null) + m_Mesh = new Mesh(); + return m_Mesh; + } + } + + /// + /// Should the mobile keyboard input be hidden. + /// + public bool shouldHideMobileInput + { + get + { + switch (Application.platform) + { + case RuntimePlatform.Android: + case RuntimePlatform.IPhonePlayer: + case RuntimePlatform.tvOS: + return m_HideMobileInput; + default: + return true; + } + } + + set + { + switch(Application.platform) + { + case RuntimePlatform.Android: + case RuntimePlatform.IPhonePlayer: + case RuntimePlatform.tvOS: + SetPropertyUtility.SetStruct(ref m_HideMobileInput, value); + break; + default: + m_HideMobileInput = true; + break; + } + } + } + + public bool shouldHideSoftKeyboard + { + get + { + switch (Application.platform) + { + case RuntimePlatform.Android: + case RuntimePlatform.IPhonePlayer: + case RuntimePlatform.tvOS: + case RuntimePlatform.WSAPlayerX86: + case RuntimePlatform.WSAPlayerX64: + case RuntimePlatform.WSAPlayerARM: + return m_HideSoftKeyboard; + default: + return true; + } + } + + set + { + switch (Application.platform) + { + case RuntimePlatform.Android: + case RuntimePlatform.IPhonePlayer: + case RuntimePlatform.tvOS: + case RuntimePlatform.WSAPlayerX86: + case RuntimePlatform.WSAPlayerX64: + case RuntimePlatform.WSAPlayerARM: + SetPropertyUtility.SetStruct(ref m_HideSoftKeyboard, value); + break; + default: + m_HideSoftKeyboard = true; + break; + } + + if (m_HideSoftKeyboard == true && m_SoftKeyboard != null && TouchScreenKeyboard.isSupported && m_SoftKeyboard.active) + { + m_SoftKeyboard.active = false; + m_SoftKeyboard = null; + } + } + } + + private bool isKeyboardUsingEvents() + { + switch (Application.platform) + { + case RuntimePlatform.Android: + case RuntimePlatform.IPhonePlayer: + case RuntimePlatform.tvOS: + return false; + default: + return true; + } + } + + /// + /// Input field's current text value. This is not necessarily the same as what is visible on screen. + /// + /// + /// Note that null is invalid value for InputField.text. + /// + /// + /// + /// using UnityEngine; + /// using System.Collections; + /// using UnityEngine.UI; // Required when Using UI elements. + /// + /// public class Example : MonoBehaviour + /// { + /// public InputField mainInputField; + /// + /// public void Start() + /// { + /// mainInputField.text = "Enter Text Here..."; + /// } + /// } + /// + /// + public string text + { + get + { + return m_Text; + } + set + { + SetText(value); + } + } + + /// + /// Set Input field's current text value without invoke onValueChanged. This is not necessarily the same as what is visible on screen. + /// + public void SetTextWithoutNotify(string input) + { + SetText(input, false); + } + + void SetText(string value, bool sendCallback = true) + { + if (this.text == value) + return; + + if (value == null) + value = ""; + + value = value.Replace("\0", string.Empty); // remove embedded nulls + + m_Text = value; + + /* + if (m_LineType == LineType.SingleLine) + value = value.Replace("\n", "").Replace("\t", ""); + + // If we have an input validator, validate the input and apply the character limit at the same time. + if (onValidateInput != null || characterValidation != CharacterValidation.None) + { + m_Text = ""; + OnValidateInput validatorMethod = onValidateInput ?? Validate; + m_CaretPosition = m_CaretSelectPosition = value.Length; + int charactersToCheck = characterLimit > 0 ? Math.Min(characterLimit, value.Length) : value.Length; + for (int i = 0; i < charactersToCheck; ++i) + { + char c = validatorMethod(m_Text, m_Text.Length, value[i]); + if (c != 0) + m_Text += c; + } + } + else + { + m_Text = characterLimit > 0 && value.Length > characterLimit ? value.Substring(0, characterLimit) : value; + } + */ + + #if UNITY_EDITOR + if (!Application.isPlaying) + { + SendOnValueChangedAndUpdateLabel(); + return; + } + #endif + + if (m_SoftKeyboard != null) + m_SoftKeyboard.text = m_Text; + + if (m_StringPosition > m_Text.Length) + m_StringPosition = m_StringSelectPosition = m_Text.Length; + else if (m_StringSelectPosition > m_Text.Length) + m_StringSelectPosition = m_Text.Length; + + // Set RectTransform relative position to top of viewport. + AdjustTextPositionRelativeToViewport(0); + + m_forceRectTransformAdjustment = true; + + m_IsTextComponentUpdateRequired = true; + UpdateLabel(); + + if (sendCallback) + SendOnValueChanged(); + } + + + public bool isFocused + { + get { return m_AllowInput; } + } + + public float caretBlinkRate + { + get { return m_CaretBlinkRate; } + set + { + if (SetPropertyUtility.SetStruct(ref m_CaretBlinkRate, value)) + { + if (m_AllowInput) + SetCaretActive(); + } + } + } + + public int caretWidth { get { return m_CaretWidth; } set { if (SetPropertyUtility.SetStruct(ref m_CaretWidth, value)) MarkGeometryAsDirty(); } } + + public RectTransform textViewport { get { return m_TextViewport; } set { SetPropertyUtility.SetClass(ref m_TextViewport, value); } } + + public TMP_Text textComponent + { + get { return m_TextComponent; } + set + { + if (SetPropertyUtility.SetClass(ref m_TextComponent, value)) + { + SetTextComponentWrapMode(); + } + } + } + + //public TMP_Text placeholderTextComponent { get { return m_PlaceholderTextComponent; } set { SetPropertyUtility.SetClass(ref m_PlaceholderTextComponent, value); } } + + public Graphic placeholder { get { return m_Placeholder; } set { SetPropertyUtility.SetClass(ref m_Placeholder, value); } } + + public Scrollbar verticalScrollbar + { + get { return m_VerticalScrollbar; } + set + { + if (m_VerticalScrollbar != null) + m_VerticalScrollbar.onValueChanged.RemoveListener(OnScrollbarValueChange); + + SetPropertyUtility.SetClass(ref m_VerticalScrollbar, value); + + if (m_VerticalScrollbar) + { + m_VerticalScrollbar.onValueChanged.AddListener(OnScrollbarValueChange); + + } + } + } + + public float scrollSensitivity { get { return m_ScrollSensitivity; } set { if (SetPropertyUtility.SetStruct(ref m_ScrollSensitivity, value)) MarkGeometryAsDirty(); } } + + public Color caretColor { get { return customCaretColor ? m_CaretColor : textComponent.color; } set { if (SetPropertyUtility.SetColor(ref m_CaretColor, value)) MarkGeometryAsDirty(); } } + + public bool customCaretColor { get { return m_CustomCaretColor; } set { if (m_CustomCaretColor != value) { m_CustomCaretColor = value; MarkGeometryAsDirty(); } } } + + public Color selectionColor { get { return m_SelectionColor; } set { if (SetPropertyUtility.SetColor(ref m_SelectionColor, value)) MarkGeometryAsDirty(); } } + + public SubmitEvent onEndEdit { get { return m_OnEndEdit; } set { SetPropertyUtility.SetClass(ref m_OnEndEdit, value); } } + + public SubmitEvent onSubmit { get { return m_OnSubmit; } set { SetPropertyUtility.SetClass(ref m_OnSubmit, value); } } + + public SelectionEvent onSelect { get { return m_OnSelect; } set { SetPropertyUtility.SetClass(ref m_OnSelect, value); } } + + public SelectionEvent onDeselect { get { return m_OnDeselect; } set { SetPropertyUtility.SetClass(ref m_OnDeselect, value); } } + + public TextSelectionEvent onTextSelection { get { return m_OnTextSelection; } set { SetPropertyUtility.SetClass(ref m_OnTextSelection, value); } } + + public TextSelectionEvent onEndTextSelection { get { return m_OnEndTextSelection; } set { SetPropertyUtility.SetClass(ref m_OnEndTextSelection, value); } } + + public OnChangeEvent onValueChanged { get { return m_OnValueChanged; } set { SetPropertyUtility.SetClass(ref m_OnValueChanged, value); } } + + public TouchScreenKeyboardEvent onTouchScreenKeyboardStatusChanged { get { return m_OnTouchScreenKeyboardStatusChanged; } set { SetPropertyUtility.SetClass(ref m_OnTouchScreenKeyboardStatusChanged, value); } } + + public OnValidateInput onValidateInput { get { return m_OnValidateInput; } set { SetPropertyUtility.SetClass(ref m_OnValidateInput, value); } } + + public int characterLimit + { + get { return m_CharacterLimit; } + set + { + if (SetPropertyUtility.SetStruct(ref m_CharacterLimit, Math.Max(0, value))) + { + UpdateLabel(); + if (m_SoftKeyboard != null) + m_SoftKeyboard.characterLimit = value; + } + } + } + + //public bool isInteractableControl { set { if ( } } + + /// + /// Set the point size on both Placeholder and Input text object. + /// + public float pointSize + { + get { return m_GlobalPointSize; } + set { + if (SetPropertyUtility.SetStruct(ref m_GlobalPointSize, Math.Max(0, value))) + { + SetGlobalPointSize(m_GlobalPointSize); + UpdateLabel(); + } + } + } + + /// + /// Sets the Font Asset on both Placeholder and Input child objects. + /// + public TMP_FontAsset fontAsset + { + get { return m_GlobalFontAsset; } + set + { + if (SetPropertyUtility.SetClass(ref m_GlobalFontAsset, value)) + { + SetGlobalFontAsset(m_GlobalFontAsset); + UpdateLabel(); + } + } + } + [SerializeField] + protected TMP_FontAsset m_GlobalFontAsset; + + /// + /// Determines if the whole text will be selected when focused. + /// + public bool onFocusSelectAll + { + get { return m_OnFocusSelectAll; } + set { m_OnFocusSelectAll = value; } + } + [SerializeField] + protected bool m_OnFocusSelectAll = true; + protected bool m_isSelectAll; + + /// + /// Determines if the text and caret position as well as selection will be reset when the input field is deactivated. + /// + public bool resetOnDeActivation + { + get { return m_ResetOnDeActivation; } + set { m_ResetOnDeActivation = value; } + } + [SerializeField] + protected bool m_ResetOnDeActivation = true; + private bool m_SelectionStillActive = false; + private bool m_ReleaseSelection = false; + + private GameObject m_SelectedObject; + + /// + /// Controls whether the original text is restored when pressing "ESC". + /// + public bool restoreOriginalTextOnEscape + { + get { return m_RestoreOriginalTextOnEscape; } + set { m_RestoreOriginalTextOnEscape = value; } + } + [SerializeField] + private bool m_RestoreOriginalTextOnEscape = true; + + /// + /// Is Rich Text editing allowed? + /// + public bool isRichTextEditingAllowed + { + get { return m_isRichTextEditingAllowed; } + set { m_isRichTextEditingAllowed = value; } + } + [SerializeField] + protected bool m_isRichTextEditingAllowed = false; + + + // Content Type related + public ContentType contentType { get { return m_ContentType; } set { if (SetPropertyUtility.SetStruct(ref m_ContentType, value)) EnforceContentType(); } } + + public LineType lineType + { + get { return m_LineType; } + set + { + if (SetPropertyUtility.SetStruct(ref m_LineType, value)) + { + SetToCustomIfContentTypeIsNot(ContentType.Standard, ContentType.Autocorrected); + SetTextComponentWrapMode(); + } + } + } + + /// + /// Limits the number of lines of text in the Input Field. + /// + public int lineLimit + { + get { return m_LineLimit; } + set + { + if (m_LineType == LineType.SingleLine) + m_LineLimit = 1; + else + SetPropertyUtility.SetStruct(ref m_LineLimit, value); + + } + } + [SerializeField] + protected int m_LineLimit = 0; + + public InputType inputType { get { return m_InputType; } set { if (SetPropertyUtility.SetStruct(ref m_InputType, value)) SetToCustom(); } } + + public TouchScreenKeyboardType keyboardType + { + get { return m_KeyboardType; } + set + { + if (SetPropertyUtility.SetStruct(ref m_KeyboardType, value)) + SetToCustom(); + } + } + + public CharacterValidation characterValidation { get { return m_CharacterValidation; } set { if (SetPropertyUtility.SetStruct(ref m_CharacterValidation, value)) SetToCustom(); } } + + /// + /// Sets the Input Validation to use a Custom Input Validation script. + /// + public TMP_InputValidator inputValidator + { + get { return m_InputValidator; } + set { if (SetPropertyUtility.SetClass(ref m_InputValidator, value)) SetToCustom(CharacterValidation.CustomValidator); } + } + [SerializeField] + protected TMP_InputValidator m_InputValidator = null; + + public bool readOnly { get { return m_ReadOnly; } set { m_ReadOnly = value; } } + + public bool richText { get { return m_RichText; } set { m_RichText = value; SetTextComponentRichTextMode(); } } + + // Derived property + public bool multiLine { get { return m_LineType == LineType.MultiLineNewline || lineType == LineType.MultiLineSubmit; } } + // Not shown in Inspector. + public char asteriskChar { get { return m_AsteriskChar; } set { if (SetPropertyUtility.SetStruct(ref m_AsteriskChar, value)) UpdateLabel(); } } + public bool wasCanceled { get { return m_WasCanceled; } } + + + protected void ClampStringPos(ref int pos) + { + if (pos < 0) + pos = 0; + else if (pos > text.Length) + pos = text.Length; + } + + protected void ClampCaretPos(ref int pos) + { + if (pos < 0) + pos = 0; + else if (pos > m_TextComponent.textInfo.characterCount - 1) + pos = m_TextComponent.textInfo.characterCount - 1; + } + + /// + /// Current position of the cursor. + /// Getters are public Setters are protected + /// + + protected int caretPositionInternal { get { return m_CaretPosition + compositionString.Length; } set { m_CaretPosition = value; ClampCaretPos(ref m_CaretPosition); } } + protected int stringPositionInternal { get { return m_StringPosition + compositionString.Length; } set { m_StringPosition = value; ClampStringPos(ref m_StringPosition); } } + + protected int caretSelectPositionInternal { get { return m_CaretSelectPosition + compositionString.Length; } set { m_CaretSelectPosition = value; ClampCaretPos(ref m_CaretSelectPosition); } } + protected int stringSelectPositionInternal { get { return m_StringSelectPosition + compositionString.Length; } set { m_StringSelectPosition = value; ClampStringPos(ref m_StringSelectPosition); } } + + private bool hasSelection { get { return stringPositionInternal != stringSelectPositionInternal; } } + private bool m_isSelected; + private bool m_IsStringPositionDirty; + private bool m_IsCaretPositionDirty; + private bool m_forceRectTransformAdjustment; + + /// + /// Get: Returns the focus position as thats the position that moves around even during selection. + /// Set: Set both the anchor and focus position such that a selection doesn't happen + /// + public int caretPosition + { + get { return caretSelectPositionInternal; } + set { selectionAnchorPosition = value; selectionFocusPosition = value; m_IsStringPositionDirty = true; } + } + + /// + /// Get: Returns the fixed position of selection + /// Set: If compositionString is 0 set the fixed position + /// + public int selectionAnchorPosition + { + get + { + return caretPositionInternal; + } + + set + { + if (compositionString.Length != 0) + return; + + caretPositionInternal = value; + m_IsStringPositionDirty = true; + } + } + + /// + /// Get: Returns the variable position of selection + /// Set: If compositionString is 0 set the variable position + /// + public int selectionFocusPosition + { + get + { + return caretSelectPositionInternal; + } + set + { + if (compositionString.Length != 0) + return; + + caretSelectPositionInternal = value; + m_IsStringPositionDirty = true; + } + } + + + /// + /// + /// + public int stringPosition + { + get { return stringSelectPositionInternal; } + set { selectionStringAnchorPosition = value; selectionStringFocusPosition = value; m_IsCaretPositionDirty = true; } + } + + + /// + /// The fixed position of the selection in the raw string which may contains rich text. + /// + public int selectionStringAnchorPosition + { + get + { + return stringPositionInternal; + } + + set + { + if (compositionString.Length != 0) + return; + + stringPositionInternal = value; + m_IsCaretPositionDirty = true; + } + } + + + /// + /// The variable position of the selection in the raw string which may contains rich text. + /// + public int selectionStringFocusPosition + { + get + { + return stringSelectPositionInternal; + } + set + { + if (compositionString.Length != 0) + return; + + stringSelectPositionInternal = value; + m_IsCaretPositionDirty = true; + } + } + + + #if UNITY_EDITOR + // Remember: This is NOT related to text validation! + // This is Unity's own OnValidate method which is invoked when changing values in the Inspector. + protected override void OnValidate() + { + base.OnValidate(); + EnforceContentType(); + + m_CharacterLimit = Math.Max(0, m_CharacterLimit); + + //This can be invoked before OnEnabled is called. So we shouldn't be accessing other objects, before OnEnable is called. + if (!IsActive()) + return; + + SetTextComponentRichTextMode(); + + UpdateLabel(); + if (m_AllowInput) + SetCaretActive(); + } + #endif + + protected override void OnEnable() + { + //Debug.Log("*** OnEnable() *** - " + this.name); + + base.OnEnable(); + + if (m_Text == null) + m_Text = string.Empty; + + if (Application.isPlaying) + { + if (m_CachedInputRenderer == null && m_TextComponent != null) + { + // Check if Input Field is driven by any layout components + m_IsDrivenByLayoutComponents = GetComponent() != null ? true : false; + + GameObject go = new GameObject(transform.name + " Input Caret", typeof(RectTransform)); + + // Add MaskableGraphic Component + TMP_SelectionCaret caret = go.AddComponent(); + caret.raycastTarget = false; + caret.color = Color.clear; + + go.hideFlags = HideFlags.DontSave; + go.transform.SetParent(m_TextComponent.transform.parent); + go.transform.SetAsFirstSibling(); + go.layer = gameObject.layer; + + caretRectTrans = go.GetComponent(); + m_CachedInputRenderer = go.GetComponent(); + m_CachedInputRenderer.SetMaterial(Graphic.defaultGraphicMaterial, Texture2D.whiteTexture); + + // Needed as if any layout is present we want the caret to always be the same as the text area. + go.AddComponent().ignoreLayout = true; + + AssignPositioningIfNeeded(); + } + } + + // If we have a cached renderer then we had OnDisable called so just restore the material. + if (m_CachedInputRenderer != null) + m_CachedInputRenderer.SetMaterial(Graphic.defaultGraphicMaterial, Texture2D.whiteTexture); + + if (m_TextComponent != null) + { + m_TextComponent.RegisterDirtyVerticesCallback(MarkGeometryAsDirty); + m_TextComponent.RegisterDirtyVerticesCallback(UpdateLabel); + //m_TextComponent.ignoreRectMaskCulling = multiLine; + + //m_DefaultTransformPosition = m_TextComponent.rectTransform.localPosition; + + // Cache reference to Vertical Scrollbar RectTransform and add listener. + if (m_VerticalScrollbar != null) + { + m_TextComponent.ignoreRectMaskCulling = true; + m_VerticalScrollbar.onValueChanged.AddListener(OnScrollbarValueChange); + } + + UpdateLabel(); + } + + // Subscribe to event fired when text object has been regenerated. + TMPro_EventManager.TEXT_CHANGED_EVENT.Add(ON_TEXT_CHANGED); + } + + protected override void OnDisable() + { + // the coroutine will be terminated, so this will ensure it restarts when we are next activated + m_BlinkCoroutine = null; + + DeactivateInputField(); + if (m_TextComponent != null) + { + m_TextComponent.UnregisterDirtyVerticesCallback(MarkGeometryAsDirty); + m_TextComponent.UnregisterDirtyVerticesCallback(UpdateLabel); + + if (m_VerticalScrollbar != null) + m_VerticalScrollbar.onValueChanged.RemoveListener(OnScrollbarValueChange); + + } + CanvasUpdateRegistry.UnRegisterCanvasElementForRebuild(this); + + // Clear needs to be called otherwise sync never happens as the object is disabled. + if (m_CachedInputRenderer != null) + m_CachedInputRenderer.Clear(); + + if (m_Mesh != null) + DestroyImmediate(m_Mesh); + m_Mesh = null; + + // Unsubscribe to event triggered when text object has been regenerated + TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(ON_TEXT_CHANGED); + + base.OnDisable(); + } + + + /// + /// Method used to update the tracking of the caret position when the text object has been regenerated. + /// + /// + private void ON_TEXT_CHANGED(UnityEngine.Object obj) + { + if (obj == m_TextComponent && Application.isPlaying && compositionString.Length == 0) + { + caretPositionInternal = GetCaretPositionFromStringIndex(stringPositionInternal); + caretSelectPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + } + + + IEnumerator CaretBlink() + { + // Always ensure caret is initially visible since it can otherwise be confusing for a moment. + m_CaretVisible = true; + yield return null; + + while ((isFocused || m_SelectionStillActive) && m_CaretBlinkRate > 0) + { + // the blink rate is expressed as a frequency + float blinkPeriod = 1f / m_CaretBlinkRate; + + // the caret should be ON if we are in the first half of the blink period + bool blinkState = (Time.unscaledTime - m_BlinkStartTime) % blinkPeriod < blinkPeriod / 2; + if (m_CaretVisible != blinkState) + { + m_CaretVisible = blinkState; + if (!hasSelection) + MarkGeometryAsDirty(); + } + + // Then wait again. + yield return null; + } + m_BlinkCoroutine = null; + } + + void SetCaretVisible() + { + if (!m_AllowInput) + return; + + m_CaretVisible = true; + m_BlinkStartTime = Time.unscaledTime; + SetCaretActive(); + } + + // SetCaretActive will not set the caret immediately visible - it will wait for the next time to blink. + // However, it will handle things correctly if the blink speed changed from zero to non-zero or non-zero to zero. + void SetCaretActive() + { + if (!m_AllowInput) + return; + + if (m_CaretBlinkRate > 0.0f) + { + if (m_BlinkCoroutine == null) + m_BlinkCoroutine = StartCoroutine(CaretBlink()); + } + else + { + m_CaretVisible = true; + } + } + + protected void OnFocus() + { + if (m_OnFocusSelectAll) + SelectAll(); + } + + protected void SelectAll() + { + m_isSelectAll = true; + stringPositionInternal = text.Length; + stringSelectPositionInternal = 0; + } + + /// + /// Move to the end of the text. + /// + /// + public void MoveTextEnd(bool shift) + { + if (m_isRichTextEditingAllowed) + { + int position = text.Length; + + if (shift) + { + stringSelectPositionInternal = position; + } + else + { + stringPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal; + } + } + else + { + int position = m_TextComponent.textInfo.characterCount - 1; + + if (shift) + { + caretSelectPositionInternal = position; + stringSelectPositionInternal = GetStringIndexFromCaretPosition(position); + } + else + { + caretPositionInternal = caretSelectPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal = GetStringIndexFromCaretPosition(position); + } + } + + UpdateLabel(); + } + + /// + /// Move to the start of the text. + /// + /// + public void MoveTextStart(bool shift) + { + if (m_isRichTextEditingAllowed) + { + int position = 0; + + if (shift) + { + stringSelectPositionInternal = position; + } + else + { + stringPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal; + } + } + else + { + int position = 0; + + if (shift) + { + caretSelectPositionInternal = position; + stringSelectPositionInternal = GetStringIndexFromCaretPosition(position); + } + else + { + caretPositionInternal = caretSelectPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal = GetStringIndexFromCaretPosition(position); + } + } + + UpdateLabel(); + } + + + /// + /// Move to the end of the current line of text. + /// + /// + public void MoveToEndOfLine(bool shift, bool ctrl) + { + // Get the line the caret is currently located on. + int currentLine = m_TextComponent.textInfo.characterInfo[caretPositionInternal].lineNumber; + + // Get the last character of the given line. + int characterIndex = ctrl == true ? m_TextComponent.textInfo.characterCount - 1 : m_TextComponent.textInfo.lineInfo[currentLine].lastCharacterIndex; + + int position = m_TextComponent.textInfo.characterInfo[characterIndex].index; + + if (shift) + { + stringSelectPositionInternal = position; + + caretSelectPositionInternal = characterIndex; + } + else + { + stringPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal; + + caretSelectPositionInternal = caretPositionInternal = characterIndex; + } + + UpdateLabel(); + } + + /// + /// Move to the start of the current line of text. + /// + /// + public void MoveToStartOfLine(bool shift, bool ctrl) + { + // Get the line the caret is currently located on. + int currentLine = m_TextComponent.textInfo.characterInfo[caretPositionInternal].lineNumber; + + // Get the first character of the given line. + int characterIndex = ctrl == true ? 0 : m_TextComponent.textInfo.lineInfo[currentLine].firstCharacterIndex; + + int position = 0; + if (characterIndex > 0) + position = m_TextComponent.textInfo.characterInfo[characterIndex - 1].index + m_TextComponent.textInfo.characterInfo[characterIndex - 1].stringLength; + + if (shift) + { + stringSelectPositionInternal = position; + + caretSelectPositionInternal = characterIndex; + } + else + { + stringPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal; + + caretSelectPositionInternal = caretPositionInternal = characterIndex; + } + + UpdateLabel(); + } + + + static string clipboard + { + get + { + return GUIUtility.systemCopyBuffer; + } + set + { + GUIUtility.systemCopyBuffer = value; + } + } + + private bool InPlaceEditing() + { + if (m_TouchKeyboardAllowsInPlaceEditing || (TouchScreenKeyboard.isSupported && (Application.platform == RuntimePlatform.WSAPlayerX86 || Application.platform == RuntimePlatform.WSAPlayerX64 || Application.platform == RuntimePlatform.WSAPlayerARM))) + return true; + + if (TouchScreenKeyboard.isSupported && shouldHideSoftKeyboard) + return true; + + if (TouchScreenKeyboard.isSupported && shouldHideSoftKeyboard == false && shouldHideMobileInput == false) + return false; + + return true; + } + + void UpdateStringPositionFromKeyboard() + { + // TODO: Might want to add null check here. + var selectionRange = m_SoftKeyboard.selection; + + if (selectionRange.start == 0 && selectionRange.length == 0) + return; + + var selectionStart = selectionRange.start; + var selectionEnd = selectionRange.end; + + var stringPositionChanged = false; + + if (stringPositionInternal != selectionStart) + { + stringPositionChanged = true; + stringPositionInternal = selectionStart; + + caretPositionInternal = GetCaretPositionFromStringIndex(stringPositionInternal); + } + + if (stringSelectPositionInternal != selectionEnd) + { + stringSelectPositionInternal = selectionEnd; + stringPositionChanged = true; + + caretSelectPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + } + + if (stringPositionChanged) + { + m_BlinkStartTime = Time.unscaledTime; + + UpdateLabel(); + } + } + + /// + /// Update the text based on input. + /// + // TODO: Make LateUpdate a coroutine instead. Allows us to control the update to only be when the field is active. + protected virtual void LateUpdate() + { + // Only activate if we are not already activated. + if (m_ShouldActivateNextUpdate) + { + if (!isFocused) + { + ActivateInputFieldInternal(); + m_ShouldActivateNextUpdate = false; + return; + } + + // Reset as we are already activated. + m_ShouldActivateNextUpdate = false; + } + + // Update Scrollbar if needed + if (m_IsScrollbarUpdateRequired) + { + UpdateScrollbar(); + m_IsScrollbarUpdateRequired = false; + } + + // Handle double click to reset / deselect Input Field when ResetOnActivation is false. + if (!isFocused && m_SelectionStillActive) + { + GameObject selectedObject = EventSystem.current != null ? EventSystem.current.currentSelectedGameObject : null; + + if (selectedObject != null && selectedObject != this.gameObject) + { + if (selectedObject != m_SelectedObject) + { + m_SelectedObject = selectedObject; + + // Check if object has a TMP Input Field + if (selectedObject.GetComponent() != null) + { + // Release selection + m_SelectionStillActive = false; + MarkGeometryAsDirty(); + m_SelectedObject = null; + } + } + + return; + } + + if (Input.GetKeyDown(KeyCode.Mouse0)) + { + // Check for Double Click + bool isDoubleClick = false; + float timeStamp = Time.unscaledTime; + + if (m_KeyDownStartTime + m_DoubleClickDelay > timeStamp) + isDoubleClick = true; + + m_KeyDownStartTime = timeStamp; + + if (isDoubleClick) + { + //m_StringPosition = m_StringSelectPosition = 0; + //m_CaretPosition = m_CaretSelectPosition = 0; + //m_TextComponent.rectTransform.localPosition = m_DefaultTransformPosition; + + //if (caretRectTrans != null) + // caretRectTrans.localPosition = Vector3.zero; + + m_SelectionStillActive = false; + + MarkGeometryAsDirty(); + + return; + } + } + } + + if (InPlaceEditing() && isKeyboardUsingEvents() || !isFocused) + { + return; + } + + AssignPositioningIfNeeded(); + + if (m_SoftKeyboard == null || m_SoftKeyboard.status != TouchScreenKeyboard.Status.Visible) + { + if (m_SoftKeyboard != null) + { + if (!m_ReadOnly) + text = m_SoftKeyboard.text; + + if (m_SoftKeyboard.status == TouchScreenKeyboard.Status.LostFocus) + SendTouchScreenKeyboardStatusChanged(); + + if (m_SoftKeyboard.status == TouchScreenKeyboard.Status.Canceled) + { + m_ReleaseSelection = true; + m_WasCanceled = true; + SendTouchScreenKeyboardStatusChanged(); + } + + if (m_SoftKeyboard.status == TouchScreenKeyboard.Status.Done) + { + m_ReleaseSelection = true; + OnSubmit(null); + SendTouchScreenKeyboardStatusChanged(); + } + } + + OnDeselect(null); + return; + } + + string val = m_SoftKeyboard.text; + + if (m_Text != val) + { + if (m_ReadOnly) + { + m_SoftKeyboard.text = m_Text; + } + else + { + m_Text = ""; + + for (int i = 0; i < val.Length; ++i) + { + char c = val[i]; + + if (c == '\r' || (int)c == 3) + c = '\n'; + + if (onValidateInput != null) + c = onValidateInput(m_Text, m_Text.Length, c); + else if (characterValidation != CharacterValidation.None) + c = Validate(m_Text, m_Text.Length, c); + + if (lineType == LineType.MultiLineSubmit && c == '\n') + { + m_SoftKeyboard.text = m_Text; + + OnSubmit(null); + OnDeselect(null); + return; + } + + if (c != 0) + m_Text += c; + } + + if (characterLimit > 0 && m_Text.Length > characterLimit) + m_Text = m_Text.Substring(0, characterLimit); + + UpdateStringPositionFromKeyboard(); + + // Set keyboard text before updating label, as we might have changed it with validation + // and update label will take the old value from keyboard if we don't change it here + if (m_Text != val) + m_SoftKeyboard.text = m_Text; + + SendOnValueChangedAndUpdateLabel(); + } + } + else if (m_HideMobileInput && Application.platform == RuntimePlatform.Android) + { + UpdateStringPositionFromKeyboard(); + } + + //else if (m_HideMobileInput) // m_Keyboard.canSetSelection + //{ + // int length = stringPositionInternal < stringSelectPositionInternal ? stringSelectPositionInternal - stringPositionInternal : stringPositionInternal - stringSelectPositionInternal; + // m_SoftKeyboard.selection = new RangeInt(stringPositionInternal < stringSelectPositionInternal ? stringPositionInternal : stringSelectPositionInternal, length); + //} + //else if (!m_HideMobileInput) // m_Keyboard.canGetSelection) + //{ + // UpdateStringPositionFromKeyboard(); + //} + + if (m_SoftKeyboard.status != TouchScreenKeyboard.Status.Visible) + { + if (m_SoftKeyboard.status == TouchScreenKeyboard.Status.Canceled) + m_WasCanceled = true; + + OnDeselect(null); + } + } + + + private bool MayDrag(PointerEventData eventData) + { + return IsActive() && + IsInteractable() && + eventData.button == PointerEventData.InputButton.Left && + m_TextComponent != null && + (m_SoftKeyboard == null || shouldHideSoftKeyboard || shouldHideMobileInput); + } + + public virtual void OnBeginDrag(PointerEventData eventData) + { + if (!MayDrag(eventData)) + return; + + m_UpdateDrag = true; + } + + public virtual void OnDrag(PointerEventData eventData) + { + if (!MayDrag(eventData)) + return; + + int insertionIndex = TMP_TextUtilities.GetCursorIndexFromPosition(m_TextComponent, eventData.position, eventData.pressEventCamera, out CaretPosition insertionSide); + + if (m_isRichTextEditingAllowed) + { + if (insertionSide == CaretPosition.Left) + { + stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index; + } + else if (insertionSide == CaretPosition.Right) + { + stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index + m_TextComponent.textInfo.characterInfo[insertionIndex].stringLength; + } + } + else + { + if (insertionSide == CaretPosition.Left) + { + stringSelectPositionInternal = insertionIndex == 0 + ? m_TextComponent.textInfo.characterInfo[0].index + : m_TextComponent.textInfo.characterInfo[insertionIndex - 1].index + m_TextComponent.textInfo.characterInfo[insertionIndex - 1].stringLength; + } + else if (insertionSide == CaretPosition.Right) + { + stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index + m_TextComponent.textInfo.characterInfo[insertionIndex].stringLength; + } + } + + caretSelectPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + + MarkGeometryAsDirty(); + + m_DragPositionOutOfBounds = !RectTransformUtility.RectangleContainsScreenPoint(textViewport, eventData.position, eventData.pressEventCamera); + if (m_DragPositionOutOfBounds && m_DragCoroutine == null) + m_DragCoroutine = StartCoroutine(MouseDragOutsideRect(eventData)); + + eventData.Use(); + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + IEnumerator MouseDragOutsideRect(PointerEventData eventData) + { + while (m_UpdateDrag && m_DragPositionOutOfBounds) + { + RectTransformUtility.ScreenPointToLocalPointInRectangle(textViewport, eventData.position, eventData.pressEventCamera, out Vector2 localMousePos); + + Rect rect = textViewport.rect; + + if (multiLine) + { + if (localMousePos.y > rect.yMax) + MoveUp(true, true); + else if (localMousePos.y < rect.yMin) + MoveDown(true, true); + } + else + { + if (localMousePos.x < rect.xMin) + MoveLeft(true, false); + else if (localMousePos.x > rect.xMax) + MoveRight(true, false); + } + + UpdateLabel(); + + float delay = multiLine ? kVScrollSpeed : kHScrollSpeed; + + if (m_WaitForSecondsRealtime == null) + m_WaitForSecondsRealtime = new WaitForSecondsRealtime(delay); + else + m_WaitForSecondsRealtime.waitTime = delay; + + yield return m_WaitForSecondsRealtime; + } + m_DragCoroutine = null; + } + + public virtual void OnEndDrag(PointerEventData eventData) + { + if (!MayDrag(eventData)) + return; + + m_UpdateDrag = false; + } + + public override void OnPointerDown(PointerEventData eventData) + { + if (!MayDrag(eventData)) + return; + + EventSystem.current.SetSelectedGameObject(gameObject, eventData); + + bool hadFocusBefore = m_AllowInput; + base.OnPointerDown(eventData); + + if (InPlaceEditing() == false) + { + if (m_SoftKeyboard == null || !m_SoftKeyboard.active) + { + OnSelect(eventData); + return; + } + } + + bool shift = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); + + // Check for Double Click + bool isDoubleClick = false; + float timeStamp = Time.unscaledTime; + + if (m_PointerDownClickStartTime + m_DoubleClickDelay > timeStamp) + isDoubleClick = true; + + m_PointerDownClickStartTime = timeStamp; + + // Only set caret position if we didn't just get focus now. + // Otherwise it will overwrite the select all on focus. + if (hadFocusBefore || !m_OnFocusSelectAll) + { + int insertionIndex = TMP_TextUtilities.GetCursorIndexFromPosition(m_TextComponent, eventData.position, eventData.pressEventCamera, out CaretPosition insertionSide); + + if (shift) + { + if (m_isRichTextEditingAllowed) + { + if (insertionSide == CaretPosition.Left) + { + stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index; + } + else if (insertionSide == CaretPosition.Right) + { + stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index + m_TextComponent.textInfo.characterInfo[insertionIndex].stringLength; + } + } + else + { + if (insertionSide == CaretPosition.Left) + { + stringSelectPositionInternal = insertionIndex == 0 + ? m_TextComponent.textInfo.characterInfo[0].index + : m_TextComponent.textInfo.characterInfo[insertionIndex - 1].index + m_TextComponent.textInfo.characterInfo[insertionIndex - 1].stringLength; + } + else if (insertionSide == CaretPosition.Right) + { + stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index + m_TextComponent.textInfo.characterInfo[insertionIndex].stringLength; + } + } + } + else + { + if (m_isRichTextEditingAllowed) + { + if (insertionSide == CaretPosition.Left) + { + stringPositionInternal = stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index; + } + else if (insertionSide == CaretPosition.Right) + { + stringPositionInternal = stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index + m_TextComponent.textInfo.characterInfo[insertionIndex].stringLength; + } + } + else + { + if (insertionSide == CaretPosition.Left) + { + stringPositionInternal = stringSelectPositionInternal = insertionIndex == 0 + ? m_TextComponent.textInfo.characterInfo[0].index + : m_TextComponent.textInfo.characterInfo[insertionIndex - 1].index + m_TextComponent.textInfo.characterInfo[insertionIndex - 1].stringLength; + } + else if (insertionSide == CaretPosition.Right) + { + stringPositionInternal = stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index + m_TextComponent.textInfo.characterInfo[insertionIndex].stringLength; + } + } + } + + + if (isDoubleClick) + { + int wordIndex = TMP_TextUtilities.FindIntersectingWord(m_TextComponent, eventData.position, eventData.pressEventCamera); + + if (wordIndex != -1) + { + // TODO: Should behavior be different if rich text editing is enabled or not? + + // Select current word + caretPositionInternal = m_TextComponent.textInfo.wordInfo[wordIndex].firstCharacterIndex; + caretSelectPositionInternal = m_TextComponent.textInfo.wordInfo[wordIndex].lastCharacterIndex + 1; + + stringPositionInternal = m_TextComponent.textInfo.characterInfo[caretPositionInternal].index; + stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal - 1].index + m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal - 1].stringLength; + } + else + { + // Select current character + caretPositionInternal = insertionIndex; + caretSelectPositionInternal = caretPositionInternal + 1; + + stringPositionInternal = m_TextComponent.textInfo.characterInfo[insertionIndex].index; + stringSelectPositionInternal = stringPositionInternal + m_TextComponent.textInfo.characterInfo[insertionIndex].stringLength; + } + } + else + { + caretPositionInternal = caretSelectPositionInternal = GetCaretPositionFromStringIndex(stringPositionInternal); + } + } + + UpdateLabel(); + eventData.Use(); + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + protected enum EditState + { + Continue, + Finish + } + + protected EditState KeyPressed(Event evt) + { + var currentEventModifiers = evt.modifiers; + bool ctrl = SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX ? (currentEventModifiers & EventModifiers.Command) != 0 : (currentEventModifiers & EventModifiers.Control) != 0; + bool shift = (currentEventModifiers & EventModifiers.Shift) != 0; + bool alt = (currentEventModifiers & EventModifiers.Alt) != 0; + bool ctrlOnly = ctrl && !alt && !shift; + + switch (evt.keyCode) + { + case KeyCode.Backspace: + { + Backspace(); + return EditState.Continue; + } + + case KeyCode.Delete: + { + DeleteKey(); + return EditState.Continue; + } + + case KeyCode.Home: + { + MoveToStartOfLine(shift, ctrl); + return EditState.Continue; + } + + case KeyCode.End: + { + MoveToEndOfLine(shift, ctrl); + return EditState.Continue; + } + + // Select All + case KeyCode.A: + { + if (ctrlOnly) + { + SelectAll(); + return EditState.Continue; + } + break; + } + + // Copy + case KeyCode.C: + { + if (ctrlOnly) + { + if (inputType != InputType.Password) + clipboard = GetSelectedString(); + else + clipboard = ""; + return EditState.Continue; + } + break; + } + + // Paste + case KeyCode.V: + { + if (ctrlOnly) + { + Append(clipboard); + return EditState.Continue; + } + break; + } + + // Cut + case KeyCode.X: + { + if (ctrlOnly) + { + if (inputType != InputType.Password) + clipboard = GetSelectedString(); + else + clipboard = ""; + Delete(); + UpdateTouchKeyboardFromEditChanges(); + SendOnValueChangedAndUpdateLabel(); + return EditState.Continue; + } + break; + } + + case KeyCode.LeftArrow: + { + MoveLeft(shift, ctrl); + return EditState.Continue; + } + + case KeyCode.RightArrow: + { + MoveRight(shift, ctrl); + return EditState.Continue; + } + + case KeyCode.UpArrow: + { + MoveUp(shift); + return EditState.Continue; + } + + case KeyCode.DownArrow: + { + MoveDown(shift); + return EditState.Continue; + } + + case KeyCode.PageUp: + { + MovePageUp(shift); + return EditState.Continue; + } + + case KeyCode.PageDown: + { + MovePageDown(shift); + return EditState.Continue; + } + + // Submit + case KeyCode.Return: + case KeyCode.KeypadEnter: + { + if (lineType != LineType.MultiLineNewline) + { + m_ReleaseSelection = true; + return EditState.Finish; + } + break; + } + + case KeyCode.Escape: + { + m_ReleaseSelection = true; + m_WasCanceled = true; + return EditState.Finish; + } + } + + char c = evt.character; + + // Don't allow return chars or tabulator key to be entered into single line fields. + if (!multiLine && (c == '\t' || c == '\r' || c == 10)) + return EditState.Continue; + + // Convert carriage return and end-of-text characters to newline. + if (c == '\r' || (int)c == 3) + c = '\n'; + + if (IsValidChar(c)) + { + Append(c); + } + + if (c == 0) + { + if (compositionString.Length > 0) + { + UpdateLabel(); + } + } + return EditState.Continue; + } + + protected virtual bool IsValidChar(char c) + { + // Delete key on mac + if ((int)c == 127) + return false; + // Accept newline and tab + if (c == '\t' || c == '\n') + return true; + + return true; + + // With the addition of Dynamic support, I think this will best be handled by the text component. + //return m_TextComponent.font.HasCharacter(c, true); + } + + /// + /// Handle the specified event. + /// + private Event m_ProcessingEvent = new Event(); + + public void ProcessEvent(Event e) + { + KeyPressed(e); + } + + + /// + /// + /// + /// + public virtual void OnUpdateSelected(BaseEventData eventData) + { + if (!isFocused) + return; + + bool consumedEvent = false; + while (Event.PopEvent(m_ProcessingEvent)) + { + if (m_ProcessingEvent.rawType == EventType.KeyDown) + { + //Debug.Log("Event: " + m_ProcessingEvent.ToString()); + + consumedEvent = true; + var shouldContinue = KeyPressed(m_ProcessingEvent); + if (shouldContinue == EditState.Finish) + { + SendOnSubmit(); + DeactivateInputField(); + break; + } + } + + switch (m_ProcessingEvent.type) + { + case EventType.ValidateCommand: + case EventType.ExecuteCommand: + switch (m_ProcessingEvent.commandName) + { + case "SelectAll": + SelectAll(); + consumedEvent = true; + break; + } + break; + } + } + + if (consumedEvent) + UpdateLabel(); + + eventData.Use(); + } + + + /// + /// + /// + /// + public virtual void OnScroll(PointerEventData eventData) + { + if (m_TextComponent.preferredHeight < m_TextViewport.rect.height) return; + + float scrollDirection = -eventData.scrollDelta.y; + + m_ScrollPosition = m_ScrollPosition + (1f / m_TextComponent.textInfo.lineCount) * scrollDirection * m_ScrollSensitivity; + + m_ScrollPosition = Mathf.Clamp01(m_ScrollPosition); + + AdjustTextPositionRelativeToViewport(m_ScrollPosition); + + // Disable focus until user re-selected the input field. + m_AllowInput = false; + + if (m_VerticalScrollbar) + { + m_IsUpdatingScrollbarValues = true; + m_VerticalScrollbar.value = m_ScrollPosition; + } + + //Debug.Log("Scroll Position:" + m_ScrollPosition); + } + + + private string GetSelectedString() + { + if (!hasSelection) + return ""; + + int startPos = stringPositionInternal; + int endPos = stringSelectPositionInternal; + + // Ensure pos is always less then selPos to make the code simpler + if (startPos > endPos) + { + int temp = startPos; + startPos = endPos; + endPos = temp; + } + + //for (int i = m_CaretPosition; i < m_CaretSelectPosition; i++) + //{ + // Debug.Log("Character [" + m_TextComponent.textInfo.characterInfo[i].character + "] using Style [" + m_TextComponent.textInfo.characterInfo[i].style + "] has been selected."); + //} + + + return text.Substring(startPos, endPos - startPos); + } + + private int FindNextWordBegin() + { + if (stringSelectPositionInternal + 1 >= text.Length) + return text.Length; + + int spaceLoc = text.IndexOfAny(kSeparators, stringSelectPositionInternal + 1); + + if (spaceLoc == -1) + spaceLoc = text.Length; + else + spaceLoc++; + + return spaceLoc; + } + + private void MoveRight(bool shift, bool ctrl) + { + if (hasSelection && !shift) + { + // By convention, if we have a selection and move right without holding shift, + // we just place the cursor at the end. + stringPositionInternal = stringSelectPositionInternal = Mathf.Max(stringPositionInternal, stringSelectPositionInternal); + caretPositionInternal = caretSelectPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + return; + } + + int position; + if (ctrl) + position = FindNextWordBegin(); + else + { + if (m_isRichTextEditingAllowed) + { + // Special handling for Surrogate pairs and Diacritical marks. + if (stringSelectPositionInternal < text.Length && char.IsHighSurrogate(text[stringSelectPositionInternal])) + position = stringSelectPositionInternal + 2; + else + position = stringSelectPositionInternal + 1; + } + else + { + position = m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal].index + m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal].stringLength; + } + + } + + if (shift) + { + stringSelectPositionInternal = position; + caretSelectPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + } + else + { + stringSelectPositionInternal = stringPositionInternal = position; + + // Only increase caret position as we cross character boundary. + if (stringPositionInternal >= m_TextComponent.textInfo.characterInfo[caretPositionInternal].index + m_TextComponent.textInfo.characterInfo[caretPositionInternal].stringLength) + caretSelectPositionInternal = caretPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + } + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + private int FindPrevWordBegin() + { + if (stringSelectPositionInternal - 2 < 0) + return 0; + + int spaceLoc = text.LastIndexOfAny(kSeparators, stringSelectPositionInternal - 2); + + if (spaceLoc == -1) + spaceLoc = 0; + else + spaceLoc++; + + return spaceLoc; + } + + private void MoveLeft(bool shift, bool ctrl) + { + if (hasSelection && !shift) + { + // By convention, if we have a selection and move left without holding shift, + // we just place the cursor at the start. + stringPositionInternal = stringSelectPositionInternal = Mathf.Min(stringPositionInternal, stringSelectPositionInternal); + caretPositionInternal = caretSelectPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + return; + } + + int position; + if (ctrl) + position = FindPrevWordBegin(); + else + { + if (m_isRichTextEditingAllowed) + { + // Special handling for Surrogate pairs and Diacritical marks. + if (stringSelectPositionInternal > 0 && char.IsLowSurrogate(text[stringSelectPositionInternal - 1])) + position = stringSelectPositionInternal - 2; + else + position = stringSelectPositionInternal - 1; + } + else + { + //position = GetStringIndexFromCaretPosition(caretSelectPositionInternal - 1); + position = caretSelectPositionInternal < 2 + ? m_TextComponent.textInfo.characterInfo[0].index + : m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal - 2].index + m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal - 2].stringLength; + } + } + + if (shift) + { + stringSelectPositionInternal = position; + caretSelectPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + } + else + { + stringSelectPositionInternal = stringPositionInternal = position; + + // Only decrease caret position as we cross character boundary. + if (caretPositionInternal > 0 && stringPositionInternal <= m_TextComponent.textInfo.characterInfo[caretPositionInternal - 1].index) + caretSelectPositionInternal = caretPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + } + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + + private int LineUpCharacterPosition(int originalPos, bool goToFirstChar) + { + if (originalPos >= m_TextComponent.textInfo.characterCount) + originalPos -= 1; + + TMP_CharacterInfo originChar = m_TextComponent.textInfo.characterInfo[originalPos]; + int originLine = originChar.lineNumber; + + // We are on the first line return first character + if (originLine - 1 < 0) + return goToFirstChar ? 0 : originalPos; + + int endCharIdx = m_TextComponent.textInfo.lineInfo[originLine].firstCharacterIndex - 1; + + int closest = -1; + float distance = TMP_Math.FLOAT_MAX; + float range = 0; + + for (int i = m_TextComponent.textInfo.lineInfo[originLine - 1].firstCharacterIndex; i < endCharIdx; ++i) + { + TMP_CharacterInfo currentChar = m_TextComponent.textInfo.characterInfo[i]; + + float d = originChar.origin - currentChar.origin; + float r = d / (currentChar.xAdvance - currentChar.origin); + + if (r >= 0 && r <= 1) + { + if (r < 0.5f) + return i; + else + return i + 1; + } + + d = Mathf.Abs(d); + + if (d < distance) + { + closest = i; + distance = d; + range = r; + } + } + + if (closest == -1) return endCharIdx; + + //Debug.Log("Returning nearest character with Range = " + range); + + if (range < 0.5f) + return closest; + else + return closest + 1; + } + + + private int LineDownCharacterPosition(int originalPos, bool goToLastChar) + { + if (originalPos >= m_TextComponent.textInfo.characterCount) + return m_TextComponent.textInfo.characterCount - 1; // text.Length; + + TMP_CharacterInfo originChar = m_TextComponent.textInfo.characterInfo[originalPos]; + int originLine = originChar.lineNumber; + + //// We are on the last line return last character + if (originLine + 1 >= m_TextComponent.textInfo.lineCount) + return goToLastChar ? m_TextComponent.textInfo.characterCount - 1 : originalPos; + + // Need to determine end line for next line. + int endCharIdx = m_TextComponent.textInfo.lineInfo[originLine + 1].lastCharacterIndex; + + int closest = -1; + float distance = TMP_Math.FLOAT_MAX; + float range = 0; + + for (int i = m_TextComponent.textInfo.lineInfo[originLine + 1].firstCharacterIndex; i < endCharIdx; ++i) + { + TMP_CharacterInfo currentChar = m_TextComponent.textInfo.characterInfo[i]; + + float d = originChar.origin - currentChar.origin; + float r = d / (currentChar.xAdvance - currentChar.origin); + + if (r >= 0 && r <= 1) + { + if (r < 0.5f) + return i; + else + return i + 1; + } + + d = Mathf.Abs(d); + + if (d < distance) + { + closest = i; + distance = d; + range = r; + } + } + + if (closest == -1) return endCharIdx; + + //Debug.Log("Returning nearest character with Range = " + range); + + if (range < 0.5f) + return closest; + else + return closest + 1; + } + + + private int PageUpCharacterPosition(int originalPos, bool goToFirstChar) + { + if (originalPos >= m_TextComponent.textInfo.characterCount) + originalPos -= 1; + + TMP_CharacterInfo originChar = m_TextComponent.textInfo.characterInfo[originalPos]; + int originLine = originChar.lineNumber; + + // We are on the first line return first character + if (originLine - 1 < 0) + return goToFirstChar ? 0 : originalPos; + + float viewportHeight = m_TextViewport.rect.height; + + int newLine = originLine - 1; + // Iterate through each subsequent line to find the first baseline that is not visible in the viewport. + for (; newLine > 0; newLine--) + { + if (m_TextComponent.textInfo.lineInfo[newLine].baseline > m_TextComponent.textInfo.lineInfo[originLine].baseline + viewportHeight) + break; + } + + int endCharIdx = m_TextComponent.textInfo.lineInfo[newLine].lastCharacterIndex; + + int closest = -1; + float distance = TMP_Math.FLOAT_MAX; + float range = 0; + + for (int i = m_TextComponent.textInfo.lineInfo[newLine].firstCharacterIndex; i < endCharIdx; ++i) + { + TMP_CharacterInfo currentChar = m_TextComponent.textInfo.characterInfo[i]; + + float d = originChar.origin - currentChar.origin; + float r = d / (currentChar.xAdvance - currentChar.origin); + + if (r >= 0 && r <= 1) + { + if (r < 0.5f) + return i; + else + return i + 1; + } + + d = Mathf.Abs(d); + + if (d < distance) + { + closest = i; + distance = d; + range = r; + } + } + + if (closest == -1) return endCharIdx; + + //Debug.Log("Returning nearest character with Range = " + range); + + if (range < 0.5f) + return closest; + else + return closest + 1; + } + + + private int PageDownCharacterPosition(int originalPos, bool goToLastChar) + { + if (originalPos >= m_TextComponent.textInfo.characterCount) + return m_TextComponent.textInfo.characterCount - 1; + + TMP_CharacterInfo originChar = m_TextComponent.textInfo.characterInfo[originalPos]; + int originLine = originChar.lineNumber; + + // We are on the last line return last character + if (originLine + 1 >= m_TextComponent.textInfo.lineCount) + return goToLastChar ? m_TextComponent.textInfo.characterCount - 1 : originalPos; + + float viewportHeight = m_TextViewport.rect.height; + + int newLine = originLine + 1; + // Iterate through each subsequent line to find the first baseline that is not visible in the viewport. + for (; newLine < m_TextComponent.textInfo.lineCount - 1; newLine++) + { + if (m_TextComponent.textInfo.lineInfo[newLine].baseline < m_TextComponent.textInfo.lineInfo[originLine].baseline - viewportHeight) + break; + } + + // Need to determine end line for next line. + int endCharIdx = m_TextComponent.textInfo.lineInfo[newLine].lastCharacterIndex; + + int closest = -1; + float distance = TMP_Math.FLOAT_MAX; + float range = 0; + + for (int i = m_TextComponent.textInfo.lineInfo[newLine].firstCharacterIndex; i < endCharIdx; ++i) + { + TMP_CharacterInfo currentChar = m_TextComponent.textInfo.characterInfo[i]; + + float d = originChar.origin - currentChar.origin; + float r = d / (currentChar.xAdvance - currentChar.origin); + + if (r >= 0 && r <= 1) + { + if (r < 0.5f) + return i; + else + return i + 1; + } + + d = Mathf.Abs(d); + + if (d < distance) + { + closest = i; + distance = d; + range = r; + } + } + + if (closest == -1) return endCharIdx; + + if (range < 0.5f) + return closest; + else + return closest + 1; + } + + + private void MoveDown(bool shift) + { + MoveDown(shift, true); + } + + + private void MoveDown(bool shift, bool goToLastChar) + { + if (hasSelection && !shift) + { + // If we have a selection and press down without shift, + // set caret to end of selection before we move it down. + caretPositionInternal = caretSelectPositionInternal = Mathf.Max(caretPositionInternal, caretSelectPositionInternal); + } + + int position = multiLine ? LineDownCharacterPosition(caretSelectPositionInternal, goToLastChar) : m_TextComponent.textInfo.characterCount - 1; // text.Length; + + if (shift) + { + caretSelectPositionInternal = position; + stringSelectPositionInternal = GetStringIndexFromCaretPosition(caretSelectPositionInternal); + } + else + { + caretSelectPositionInternal = caretPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal = GetStringIndexFromCaretPosition(caretSelectPositionInternal); + } + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + private void MoveUp(bool shift) + { + MoveUp(shift, true); + } + + + private void MoveUp(bool shift, bool goToFirstChar) + { + if (hasSelection && !shift) + { + // If we have a selection and press up without shift, + // set caret position to start of selection before we move it up. + caretPositionInternal = caretSelectPositionInternal = Mathf.Min(caretPositionInternal, caretSelectPositionInternal); + } + + int position = multiLine ? LineUpCharacterPosition(caretSelectPositionInternal, goToFirstChar) : 0; + + if (shift) + { + caretSelectPositionInternal = position; + stringSelectPositionInternal = GetStringIndexFromCaretPosition(caretSelectPositionInternal); + } + else + { + caretSelectPositionInternal = caretPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal = GetStringIndexFromCaretPosition(caretSelectPositionInternal); + } + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + + private void MovePageUp(bool shift) + { + MovePageUp(shift, true); + } + + private void MovePageUp(bool shift, bool goToFirstChar) + { + if (hasSelection && !shift) + { + // If we have a selection and press up without shift, + // set caret position to start of selection before we move it up. + caretPositionInternal = caretSelectPositionInternal = Mathf.Min(caretPositionInternal, caretSelectPositionInternal); + } + + int position = multiLine ? PageUpCharacterPosition(caretSelectPositionInternal, goToFirstChar) : 0; + + if (shift) + { + caretSelectPositionInternal = position; + stringSelectPositionInternal = GetStringIndexFromCaretPosition(caretSelectPositionInternal); + } + else + { + caretSelectPositionInternal = caretPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal = GetStringIndexFromCaretPosition(caretSelectPositionInternal); + } + + + // Scroll to top of viewport + //int currentLine = m_TextComponent.textInfo.characterInfo[position].lineNumber; + //float lineAscender = m_TextComponent.textInfo.lineInfo[currentLine].ascender; + + // Adjust text area up or down if not in single line mode. + if (m_LineType != LineType.SingleLine) + { + float offset = m_TextViewport.rect.height; // m_TextViewport.rect.yMax - (m_TextComponent.rectTransform.anchoredPosition.y + lineAscender); + + float topTextBounds = m_TextComponent.rectTransform.position.y + m_TextComponent.textBounds.max.y; + float topViewportBounds = m_TextViewport.position.y + m_TextViewport.rect.yMax; + + offset = topViewportBounds > topTextBounds + offset ? offset : topViewportBounds - topTextBounds; + + m_TextComponent.rectTransform.anchoredPosition += new Vector2(0, offset); + AssignPositioningIfNeeded(); + m_IsScrollbarUpdateRequired = true; + } + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + + } + + + private void MovePageDown(bool shift) + { + MovePageDown(shift, true); + } + + private void MovePageDown(bool shift, bool goToLastChar) + { + if (hasSelection && !shift) + { + // If we have a selection and press down without shift, + // set caret to end of selection before we move it down. + caretPositionInternal = caretSelectPositionInternal = Mathf.Max(caretPositionInternal, caretSelectPositionInternal); + } + + int position = multiLine ? PageDownCharacterPosition(caretSelectPositionInternal, goToLastChar) : m_TextComponent.textInfo.characterCount - 1; + + if (shift) + { + caretSelectPositionInternal = position; + stringSelectPositionInternal = GetStringIndexFromCaretPosition(caretSelectPositionInternal); + } + else + { + caretSelectPositionInternal = caretPositionInternal = position; + stringSelectPositionInternal = stringPositionInternal = GetStringIndexFromCaretPosition(caretSelectPositionInternal); + } + + // Scroll to top of viewport + //int currentLine = m_TextComponent.textInfo.characterInfo[position].lineNumber; + //float lineAscender = m_TextComponent.textInfo.lineInfo[currentLine].ascender; + + // Adjust text area up or down if not in single line mode. + if (m_LineType != LineType.SingleLine) + { + float offset = m_TextViewport.rect.height; // m_TextViewport.rect.yMax - (m_TextComponent.rectTransform.anchoredPosition.y + lineAscender); + + float bottomTextBounds = m_TextComponent.rectTransform.position.y + m_TextComponent.textBounds.min.y; + float bottomViewportBounds = m_TextViewport.position.y + m_TextViewport.rect.yMin; + + offset = bottomViewportBounds > bottomTextBounds + offset ? offset : bottomViewportBounds - bottomTextBounds; + + m_TextComponent.rectTransform.anchoredPosition += new Vector2(0, offset); + AssignPositioningIfNeeded(); + m_IsScrollbarUpdateRequired = true; + } + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + + } + + private void Delete() + { + if (m_ReadOnly) + return; + + if (stringPositionInternal == stringSelectPositionInternal) + return; + + if (m_isRichTextEditingAllowed || m_isSelectAll) + { + // Handling of Delete when Rich Text is allowed. + if (stringPositionInternal < stringSelectPositionInternal) + { + m_Text = text.Remove(stringPositionInternal, stringSelectPositionInternal - stringPositionInternal); + stringSelectPositionInternal = stringPositionInternal; + } + else + { + m_Text = text.Remove(stringSelectPositionInternal, stringPositionInternal - stringSelectPositionInternal); + stringPositionInternal = stringSelectPositionInternal; + } + + m_isSelectAll = false; + } + else + { + if (caretPositionInternal < caretSelectPositionInternal) + { + stringPositionInternal = m_TextComponent.textInfo.characterInfo[caretPositionInternal].index; + stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal - 1].index + m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal - 1].stringLength; + + m_Text = text.Remove(stringPositionInternal, stringSelectPositionInternal - stringPositionInternal); + + stringSelectPositionInternal = stringPositionInternal; + caretSelectPositionInternal = caretPositionInternal; + } + else + { + stringPositionInternal = m_TextComponent.textInfo.characterInfo[caretPositionInternal - 1].index + m_TextComponent.textInfo.characterInfo[caretPositionInternal - 1].stringLength; + stringSelectPositionInternal = m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal].index; + + m_Text = text.Remove(stringSelectPositionInternal, stringPositionInternal - stringSelectPositionInternal); + + stringPositionInternal = stringSelectPositionInternal; + caretPositionInternal = caretSelectPositionInternal; + } + } + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + /// + /// Handling of DEL key + /// + private void DeleteKey() + { + if (m_ReadOnly) + return; + + if (hasSelection) + { + Delete(); + UpdateTouchKeyboardFromEditChanges(); + SendOnValueChangedAndUpdateLabel(); + } + else + { + if (m_isRichTextEditingAllowed) + { + if (stringPositionInternal < text.Length) + { + // Special handling for Surrogate Pairs + if (char.IsHighSurrogate(text[stringPositionInternal])) + m_Text = text.Remove(stringPositionInternal, 2); + else + m_Text = text.Remove(stringPositionInternal, 1); + + UpdateTouchKeyboardFromEditChanges(); + SendOnValueChangedAndUpdateLabel(); + } + } + else + { + if (caretPositionInternal < m_TextComponent.textInfo.characterCount - 1) + { + int numberOfCharactersToRemove = m_TextComponent.textInfo.characterInfo[caretPositionInternal].stringLength; + + // Adjust string position to skip any potential rich text tags. + int nextCharacterStringPosition = m_TextComponent.textInfo.characterInfo[caretPositionInternal].index; + + m_Text = text.Remove(nextCharacterStringPosition, numberOfCharactersToRemove); + + SendOnValueChangedAndUpdateLabel(); + } + } + } + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + /// + /// Handling of Backspace key + /// + private void Backspace() + { + if (m_ReadOnly) + return; + + if (hasSelection) + { + Delete(); + UpdateTouchKeyboardFromEditChanges(); + SendOnValueChangedAndUpdateLabel(); + } + else + { + if (m_isRichTextEditingAllowed) + { + if (stringPositionInternal > 0) + { + int numberOfCharactersToRemove = 1; + + // Special handling for Surrogate pairs and Diacritical marks + if (char.IsLowSurrogate(text[stringPositionInternal - 1])) + numberOfCharactersToRemove = 2; + + stringSelectPositionInternal = stringPositionInternal = stringPositionInternal - numberOfCharactersToRemove; + + m_Text = text.Remove(stringPositionInternal, numberOfCharactersToRemove); + + caretSelectPositionInternal = caretPositionInternal = caretPositionInternal - 1; + + m_isLastKeyBackspace = true; + + UpdateTouchKeyboardFromEditChanges(); + SendOnValueChangedAndUpdateLabel(); + } + } + else + { + if (caretPositionInternal > 0) + { + int numberOfCharactersToRemove = m_TextComponent.textInfo.characterInfo[caretPositionInternal - 1].stringLength; + + // Delete the previous character + m_Text = text.Remove(m_TextComponent.textInfo.characterInfo[caretPositionInternal - 1].index, numberOfCharactersToRemove); + + // Get new adjusted string position + stringSelectPositionInternal = stringPositionInternal = caretPositionInternal < 2 + ? m_TextComponent.textInfo.characterInfo[0].index + : m_TextComponent.textInfo.characterInfo[caretPositionInternal - 2].index + m_TextComponent.textInfo.characterInfo[caretPositionInternal - 2].stringLength; + + caretSelectPositionInternal = caretPositionInternal = caretPositionInternal - 1; + } + + m_isLastKeyBackspace = true; + + UpdateTouchKeyboardFromEditChanges(); + SendOnValueChangedAndUpdateLabel(); + } + + } + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + + /// + /// Append the specified text to the end of the current. + /// + protected virtual void Append(string input) + { + if (m_ReadOnly) + return; + + if (InPlaceEditing() == false) + return; + + for (int i = 0, imax = input.Length; i < imax; ++i) + { + char c = input[i]; + + if (c >= ' ' || c == '\t' || c == '\r' || c == 10 || c == '\n') + { + Append(c); + } + } + } + + protected virtual void Append(char input) + { + if (m_ReadOnly) + return; + + if (InPlaceEditing() == false) + return; + + // If we have an input validator, validate the input first + if (onValidateInput != null) + input = onValidateInput(text, stringPositionInternal, input); + else if (characterValidation == CharacterValidation.CustomValidator) + { + input = Validate(text, stringPositionInternal, input); + + if (input == 0) return; + + SendOnValueChanged(); + UpdateLabel(); + + return; + } + else if (characterValidation != CharacterValidation.None) + input = Validate(text, stringPositionInternal, input); + + // If the input is invalid, skip it + if (input == 0) + return; + + // Append the character and update the label + Insert(input); + } + + + // Insert the character and update the label. + private void Insert(char c) + { + if (m_ReadOnly) + return; + + string replaceString = c.ToString(); + Delete(); + + // Can't go past the character limit + if (characterLimit > 0 && text.Length >= characterLimit) + return; + + m_Text = text.Insert(m_StringPosition, replaceString); + + if (!char.IsHighSurrogate(c)) + caretSelectPositionInternal = caretPositionInternal += 1; + + stringSelectPositionInternal = stringPositionInternal += 1; + + UpdateTouchKeyboardFromEditChanges(); + SendOnValueChanged(); + + #if TMP_DEBUG_MODE + Debug.Log("Caret Position: " + caretPositionInternal + " Selection Position: " + caretSelectPositionInternal + " String Position: " + stringPositionInternal + " String Select Position: " + stringSelectPositionInternal); + #endif + } + + private void UpdateTouchKeyboardFromEditChanges() + { + // Update the TouchKeyboard's text from edit changes + // if in-place editing is allowed + if (m_SoftKeyboard != null && InPlaceEditing()) + { + m_SoftKeyboard.text = m_Text; + } + } + + private void SendOnValueChangedAndUpdateLabel() + { + UpdateLabel(); + SendOnValueChanged(); + } + + private void SendOnValueChanged() + { + if (onValueChanged != null) + onValueChanged.Invoke(text); + } + + /// + /// Submit the input field's text. + /// + + protected void SendOnEndEdit() + { + if (onEndEdit != null) + onEndEdit.Invoke(m_Text); + } + + protected void SendOnSubmit() + { + if (onSubmit != null) + onSubmit.Invoke(m_Text); + } + + protected void SendOnFocus() + { + if (onSelect != null) + onSelect.Invoke(m_Text); + } + + protected void SendOnFocusLost() + { + if (onDeselect != null) + onDeselect.Invoke(m_Text); + } + + protected void SendOnTextSelection() + { + m_isSelected = true; + + if (onTextSelection != null) + onTextSelection.Invoke(m_Text, stringPositionInternal, stringSelectPositionInternal); + } + + protected void SendOnEndTextSelection() + { + if (!m_isSelected) return; + + if (onEndTextSelection != null) + onEndTextSelection.Invoke(m_Text, stringPositionInternal, stringSelectPositionInternal); + + m_isSelected = false; + } + + protected void SendTouchScreenKeyboardStatusChanged() + { + if (onTouchScreenKeyboardStatusChanged != null) + onTouchScreenKeyboardStatusChanged.Invoke(m_SoftKeyboard.status); + } + + + /// + /// Update the visual text Text. + /// + + protected void UpdateLabel() + { + if (m_TextComponent != null && m_TextComponent.font != null && m_PreventCallback == false) + { + // Prevent callback from the text component as we assign new text. This is to prevent a recursive call. + m_PreventCallback = true; + + string fullText; + if (compositionString.Length > 0) + { + fullText = text.Substring(0, m_StringPosition) + compositionString + text.Substring(m_StringPosition); + + // Should adjust caret position + //Debug.Log("Handling IME Input... [" + compositionString + "] of length [" + compositionString.Length + "] at StringPosition [" + m_StringPosition + "]"); + //for (int i = 0; i < compositionString.Length; i++) + // Debug.Log((uint)compositionString[i]); + } + else + { + fullText = text; + //Debug.Log("Handling Input... [" + text + "]"); + } + + string processed; + if (inputType == InputType.Password) + processed = new string(asteriskChar, fullText.Length); + else + processed = fullText; + + bool isEmpty = string.IsNullOrEmpty(fullText); + + if (m_Placeholder != null) + m_Placeholder.enabled = isEmpty; + + if (!isEmpty) + { + SetCaretVisible(); + } + + m_TextComponent.text = processed + "\u200B"; // Extra space is added for Caret tracking. + + // Special handling to limit the number of lines of text in the Input Field. + if (m_LineLimit > 0) + { + m_TextComponent.ForceMeshUpdate(); + + // Check if text exceeds maximum number of lines. + if (m_TextComponent.textInfo.lineCount > m_LineLimit) + { + int lastValidCharacterIndex = m_TextComponent.textInfo.lineInfo[m_LineLimit - 1].lastCharacterIndex; + int characterStringIndex = m_TextComponent.textInfo.characterInfo[lastValidCharacterIndex].index + m_TextComponent.textInfo.characterInfo[lastValidCharacterIndex].stringLength; + text = processed.Remove(characterStringIndex, processed.Length - characterStringIndex); + m_TextComponent.text = text + "\u200B"; + } + } + + if (m_IsTextComponentUpdateRequired) + { + m_IsTextComponentUpdateRequired = false; + m_TextComponent.ForceMeshUpdate(); + } + + MarkGeometryAsDirty(); + + // Scrollbar should be updated. + m_IsScrollbarUpdateRequired = true; + + m_PreventCallback = false; + } + } + + void UpdateScrollbar() + { + // Update Scrollbar + if (m_VerticalScrollbar) + { + float size = m_TextViewport.rect.height / m_TextComponent.preferredHeight; + + m_IsUpdatingScrollbarValues = true; + + m_VerticalScrollbar.size = size; + + m_ScrollPosition = m_VerticalScrollbar.value = m_TextComponent.rectTransform.anchoredPosition.y / (m_TextComponent.preferredHeight - m_TextViewport.rect.height); + + //m_VerticalScrollbar.numberOfSteps = (int)(m_TextComponent.textInfo.lineCount / 0.25f); // Replace by scroll sensitivity. + + //Debug.Log("Updating Scrollbar... Value: " + m_VerticalScrollbar.value); + } + } + + + /// + /// Function to update the vertical position of the text container when OnValueChanged event is received from the Scrollbar. + /// + /// + void OnScrollbarValueChange(float value) + { + if (m_IsUpdatingScrollbarValues) { m_IsUpdatingScrollbarValues = false; return; } + + if (value < 0 || value > 1) return; + + AdjustTextPositionRelativeToViewport(value); + + m_ScrollPosition = value; + + //Debug.Log("Scrollbar value is: " + value + " Transform POS: " + m_TextComponent.rectTransform.anchoredPosition); + } + + /// + /// Adjusts the relative position of the body of the text relative to the viewport. + /// + /// + void AdjustTextPositionRelativeToViewport (float relativePosition) + { + //Debug.Log("- Adjusting vertical text position to " + relativePosition); + if (m_TextViewport == null) + return; + + TMP_TextInfo textInfo = m_TextComponent.textInfo; + + // Check to make sure we have valid data and lines to query. + if (textInfo == null || textInfo.lineInfo == null || textInfo.lineCount == 0 || textInfo.lineCount > textInfo.lineInfo.Length) return; + + //m_TextComponent.rectTransform.anchoredPosition = new Vector2(m_TextComponent.rectTransform.anchoredPosition.x, (textHeight - viewportHeight) * relativePosition); + m_TextComponent.rectTransform.anchoredPosition = new Vector2(m_TextComponent.rectTransform.anchoredPosition.x, (m_TextComponent.preferredHeight - m_TextViewport.rect.height) * relativePosition); + + AssignPositioningIfNeeded(); + + //Debug.Log("Text height: " + m_TextComponent.preferredHeight + " Viewport height: " + m_TextViewport.rect.height + " Adjusted RectTransform anchordedPosition:" + m_TextComponent.rectTransform.anchoredPosition + " Text Bounds: " + m_TextComponent.bounds.ToString("f3")); + } + + + private int GetCaretPositionFromStringIndex(int stringIndex) + { + int count = m_TextComponent.textInfo.characterCount; + + for (int i = 0; i < count; i++) + { + if (m_TextComponent.textInfo.characterInfo[i].index >= stringIndex) + return i; + } + + return count; + } + + /// + /// Returns / places the caret before the given character at the string index. + /// + /// + /// + private int GetMinCaretPositionFromStringIndex(int stringIndex) + { + int count = m_TextComponent.textInfo.characterCount; + + for (int i = 0; i < count; i++) + { + if (stringIndex < m_TextComponent.textInfo.characterInfo[i].index + m_TextComponent.textInfo.characterInfo[i].stringLength) + return i; + } + + return count; + } + + /// + /// Returns / places the caret after the given character at the string index. + /// + /// + /// + private int GetMaxCaretPositionFromStringIndex(int stringIndex) + { + int count = m_TextComponent.textInfo.characterCount; + + for (int i = 0; i < count; i++) + { + if (m_TextComponent.textInfo.characterInfo[i].index >= stringIndex) + return i; + } + + return count; + } + + private int GetStringIndexFromCaretPosition(int caretPosition) + { + // Clamp values between 0 and character count. + ClampCaretPos(ref caretPosition); + + return m_TextComponent.textInfo.characterInfo[caretPosition].index; + } + + + public void ForceLabelUpdate() + { + UpdateLabel(); + } + + private void MarkGeometryAsDirty() + { + #if UNITY_EDITOR + #if UNITY_2018_3_OR_NEWER + if (!Application.isPlaying || UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this)) + return; + #else + if (!Application.isPlaying || UnityEditor.PrefabUtility.GetPrefabObject(gameObject) != null) + return; + #endif + #endif + + CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this); + } + + public virtual void Rebuild(CanvasUpdate update) + { + switch (update) + { + case CanvasUpdate.LatePreRender: + UpdateGeometry(); + break; + } + } + + public virtual void LayoutComplete() + { } + + public virtual void GraphicUpdateComplete() + { } + + private void UpdateGeometry() + { + #if UNITY_EDITOR + if (!Application.isPlaying) + return; + #endif + + // No need to draw a cursor on mobile as its handled by the devices keyboard. + if (InPlaceEditing() == false) + return; + + if (m_CachedInputRenderer == null) + return; + + OnFillVBO(mesh); + + m_CachedInputRenderer.SetMesh(mesh); + } + + + /// + /// Method to keep the Caret RectTransform properties in sync with the text object's RectTransform + /// + private void AssignPositioningIfNeeded() + { + if (m_TextComponent != null && caretRectTrans != null && + (caretRectTrans.localPosition != m_TextComponent.rectTransform.localPosition || + caretRectTrans.localRotation != m_TextComponent.rectTransform.localRotation || + caretRectTrans.localScale != m_TextComponent.rectTransform.localScale || + caretRectTrans.anchorMin != m_TextComponent.rectTransform.anchorMin || + caretRectTrans.anchorMax != m_TextComponent.rectTransform.anchorMax || + caretRectTrans.anchoredPosition != m_TextComponent.rectTransform.anchoredPosition || + caretRectTrans.sizeDelta != m_TextComponent.rectTransform.sizeDelta || + caretRectTrans.pivot != m_TextComponent.rectTransform.pivot)) + { + caretRectTrans.localPosition = m_TextComponent.rectTransform.localPosition; + caretRectTrans.localRotation = m_TextComponent.rectTransform.localRotation; + caretRectTrans.localScale = m_TextComponent.rectTransform.localScale; + caretRectTrans.anchorMin = m_TextComponent.rectTransform.anchorMin; + caretRectTrans.anchorMax = m_TextComponent.rectTransform.anchorMax; + caretRectTrans.anchoredPosition = m_TextComponent.rectTransform.anchoredPosition; + caretRectTrans.sizeDelta = m_TextComponent.rectTransform.sizeDelta; + caretRectTrans.pivot = m_TextComponent.rectTransform.pivot; + + // Get updated world corners of viewport. + //m_TextViewport.GetLocalCorners(m_ViewportCorners); + } + } + + + private void OnFillVBO(Mesh vbo) + { + using (var helper = new VertexHelper()) + { + if (!isFocused && !m_SelectionStillActive) + { + helper.FillMesh(vbo); + return; + } + + if (m_IsStringPositionDirty) + { + stringPositionInternal = GetStringIndexFromCaretPosition(m_CaretPosition); + stringSelectPositionInternal = GetStringIndexFromCaretPosition(m_CaretSelectPosition); + m_IsStringPositionDirty = false; + } + + if (m_IsCaretPositionDirty) + { + caretPositionInternal = GetCaretPositionFromStringIndex(stringPositionInternal); + caretSelectPositionInternal = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + m_IsCaretPositionDirty = false; + } + + if (!hasSelection && !m_ReadOnly) + { + GenerateCaret(helper, Vector2.zero); + SendOnEndTextSelection(); + } + else + { + GenerateHightlight(helper, Vector2.zero); + SendOnTextSelection(); + } + + helper.FillMesh(vbo); + } + } + + + private void GenerateCaret(VertexHelper vbo, Vector2 roundingOffset) + { + if (!m_CaretVisible) + return; + + if (m_CursorVerts == null) + { + CreateCursorVerts(); + } + + float width = m_CaretWidth; + + // TODO: Optimize to only update the caret position when needed. + + Vector2 startPosition = Vector2.zero; + float height = 0; + TMP_CharacterInfo currentCharacter; + + int currentLine = m_TextComponent.textInfo.characterInfo[caretPositionInternal].lineNumber; + + // Caret is positioned at the origin for the first character of each lines and at the advance for subsequent characters. + if (caretPositionInternal == m_TextComponent.textInfo.lineInfo[currentLine].firstCharacterIndex) + { + currentCharacter = m_TextComponent.textInfo.characterInfo[caretPositionInternal]; + startPosition = new Vector2(currentCharacter.origin, currentCharacter.descender); + height = currentCharacter.ascender - currentCharacter.descender; + } + else + { + currentCharacter = m_TextComponent.textInfo.characterInfo[caretPositionInternal - 1]; + startPosition = new Vector2(currentCharacter.xAdvance, currentCharacter.descender); + height = currentCharacter.ascender - currentCharacter.descender; + } + + if (m_SoftKeyboard != null) + m_SoftKeyboard.selection = new RangeInt(stringPositionInternal, 0); + + // Adjust the position of the RectTransform based on the caret position in the viewport (only if we have focus). + if (isFocused && startPosition != m_LastPosition || m_forceRectTransformAdjustment) + AdjustRectTransformRelativeToViewport(startPosition, height, currentCharacter.isVisible); + + m_LastPosition = startPosition; + + // Clamp Caret height + float top = startPosition.y + height; + float bottom = top - height; + + // Minor tweak to address caret potentially being too thin based on canvas scaler values. + float scale = m_TextComponent.canvas.scaleFactor; + + m_CursorVerts[0].position = new Vector3(startPosition.x, bottom, 0.0f); + m_CursorVerts[1].position = new Vector3(startPosition.x, top, 0.0f); + m_CursorVerts[2].position = new Vector3(startPosition.x + (width + 1) / scale, top, 0.0f); + m_CursorVerts[3].position = new Vector3(startPosition.x + (width + 1) / scale, bottom, 0.0f); + + // Set Vertex Color for the caret color. + m_CursorVerts[0].color = caretColor; + m_CursorVerts[1].color = caretColor; + m_CursorVerts[2].color = caretColor; + m_CursorVerts[3].color = caretColor; + + vbo.AddUIVertexQuad(m_CursorVerts); + + int screenHeight = Screen.height; + // Removed multiple display support until it supports none native resolutions(case 741751) + //int displayIndex = m_TextComponent.canvas.targetDisplay; + //if (Screen.fullScreen && displayIndex < Display.displays.Length) + // screenHeight = Display.displays[displayIndex].renderingHeight; + + startPosition.y = screenHeight - startPosition.y; + inputSystem.compositionCursorPos = startPosition; + + //#if TMP_DEBUG_MODE + //Debug.Log("Caret position updated at frame: " + Time.frameCount); + //#endif + } + + + private void CreateCursorVerts() + { + m_CursorVerts = new UIVertex[4]; + + for (int i = 0; i < m_CursorVerts.Length; i++) + { + m_CursorVerts[i] = UIVertex.simpleVert; + m_CursorVerts[i].uv0 = Vector2.zero; + } + } + + + private void GenerateHightlight(VertexHelper vbo, Vector2 roundingOffset) + { + TMP_TextInfo textInfo = m_TextComponent.textInfo; + + caretPositionInternal = m_CaretPosition = GetCaretPositionFromStringIndex(stringPositionInternal); + caretSelectPositionInternal = m_CaretSelectPosition = GetCaretPositionFromStringIndex(stringSelectPositionInternal); + + if (m_SoftKeyboard != null) + { + int stringPosition = caretPositionInternal < caretSelectPositionInternal ? textInfo.characterInfo[caretPositionInternal].index : textInfo.characterInfo[caretSelectPositionInternal].index; + int length = caretPositionInternal < caretSelectPositionInternal ? stringSelectPositionInternal - stringPosition : stringPositionInternal - stringPosition; + m_SoftKeyboard.selection = new RangeInt(stringPosition, length); + } + + // Adjust text RectTranform position to make sure it is visible in viewport. + Vector2 caretPosition; + float height = 0; + if (caretSelectPositionInternal < textInfo.characterCount) + { + caretPosition = new Vector2(textInfo.characterInfo[caretSelectPositionInternal].origin, textInfo.characterInfo[caretSelectPositionInternal].descender); + height = textInfo.characterInfo[caretSelectPositionInternal].ascender - textInfo.characterInfo[caretSelectPositionInternal].descender; + } + else + { + caretPosition = new Vector2(textInfo.characterInfo[caretSelectPositionInternal - 1].xAdvance, textInfo.characterInfo[caretSelectPositionInternal - 1].descender); + height = textInfo.characterInfo[caretSelectPositionInternal - 1].ascender - textInfo.characterInfo[caretSelectPositionInternal - 1].descender; + } + + // TODO: Don't adjust the position of the RectTransform if Reset On Deactivation is disabled + // and we just selected the Input Field again. + AdjustRectTransformRelativeToViewport(caretPosition, height, true); + + int startChar = Mathf.Max(0, caretPositionInternal); + int endChar = Mathf.Max(0, caretSelectPositionInternal); + + // Ensure pos is always less then selPos to make the code simpler + if (startChar > endChar) + { + int temp = startChar; + startChar = endChar; + endChar = temp; + } + + endChar -= 1; + + //Debug.Log("Updating Highlight... Caret Position: " + startChar + " Caret Select POS: " + endChar); + + + int currentLineIndex = textInfo.characterInfo[startChar].lineNumber; + int nextLineStartIdx = textInfo.lineInfo[currentLineIndex].lastCharacterIndex; + + UIVertex vert = UIVertex.simpleVert; + vert.uv0 = Vector2.zero; + vert.color = selectionColor; + + int currentChar = startChar; + while (currentChar <= endChar && currentChar < textInfo.characterCount) + { + if (currentChar == nextLineStartIdx || currentChar == endChar) + { + TMP_CharacterInfo startCharInfo = textInfo.characterInfo[startChar]; + TMP_CharacterInfo endCharInfo = textInfo.characterInfo[currentChar]; + + // Extra check to handle Carriage Return + if (currentChar > 0 && endCharInfo.character == 10 && textInfo.characterInfo[currentChar - 1].character == 13) + endCharInfo = textInfo.characterInfo[currentChar - 1]; + + Vector2 startPosition = new Vector2(startCharInfo.origin, textInfo.lineInfo[currentLineIndex].ascender); + Vector2 endPosition = new Vector2(endCharInfo.xAdvance, textInfo.lineInfo[currentLineIndex].descender); + + var startIndex = vbo.currentVertCount; + vert.position = new Vector3(startPosition.x, endPosition.y, 0.0f); + vbo.AddVert(vert); + + vert.position = new Vector3(endPosition.x, endPosition.y, 0.0f); + vbo.AddVert(vert); + + vert.position = new Vector3(endPosition.x, startPosition.y, 0.0f); + vbo.AddVert(vert); + + vert.position = new Vector3(startPosition.x, startPosition.y, 0.0f); + vbo.AddVert(vert); + + vbo.AddTriangle(startIndex, startIndex + 1, startIndex + 2); + vbo.AddTriangle(startIndex + 2, startIndex + 3, startIndex + 0); + + startChar = currentChar + 1; + currentLineIndex++; + + if (currentLineIndex < textInfo.lineCount) + nextLineStartIdx = textInfo.lineInfo[currentLineIndex].lastCharacterIndex; + } + currentChar++; + } + + // Scrollbar should be updated. + m_IsScrollbarUpdateRequired = true; + + //#if TMP_DEBUG_MODE + // Debug.Log("Text selection updated at frame: " + Time.frameCount); + //#endif + } + + + /// + /// + /// + /// + /// + /// + private void AdjustRectTransformRelativeToViewport(Vector2 startPosition, float height, bool isCharVisible) + { + //Debug.Log("Adjusting transform position relative to viewport."); + + if (m_TextViewport == null || m_IsDrivenByLayoutComponents) + return; + + float viewportMin = m_TextViewport.rect.xMin; + float viewportMax = m_TextViewport.rect.xMax; + + //Debug.Log("Viewport Rect: " + viewportMax + " Start Position: " + startPosition); + // Adjust the position of the RectTransform based on the caret position in the viewport. + float rightOffset = viewportMax - (m_TextComponent.rectTransform.anchoredPosition.x + startPosition.x + m_TextComponent.margin.z + m_CaretWidth); + if (rightOffset < 0f) + { + if (!multiLine || (multiLine && isCharVisible)) + { + //Debug.Log("Shifting text to the right by " + rightOffset.ToString("f3")); + m_TextComponent.rectTransform.anchoredPosition += new Vector2(rightOffset, 0); + + AssignPositioningIfNeeded(); + } + } + + float leftOffset = (m_TextComponent.rectTransform.anchoredPosition.x + startPosition.x - m_TextComponent.margin.x) - viewportMin; + if (leftOffset < 0f) + { + //Debug.Log("Shifting text to the left by " + leftOffset.ToString("f3")); + m_TextComponent.rectTransform.anchoredPosition += new Vector2(-leftOffset, 0); + AssignPositioningIfNeeded(); + } + + + // Adjust text area up or down if not in single line mode. + if (m_LineType != LineType.SingleLine) + { + float topOffset = m_TextViewport.rect.yMax - (m_TextComponent.rectTransform.anchoredPosition.y + startPosition.y + height); + if (topOffset < -0.0001f) + { + m_TextComponent.rectTransform.anchoredPosition += new Vector2(0, topOffset); + AssignPositioningIfNeeded(); + m_IsScrollbarUpdateRequired = true; + } + + float bottomOffset = (m_TextComponent.rectTransform.anchoredPosition.y + startPosition.y) - m_TextViewport.rect.yMin; + if (bottomOffset < 0f) + { + m_TextComponent.rectTransform.anchoredPosition -= new Vector2(0, bottomOffset); + AssignPositioningIfNeeded(); + m_IsScrollbarUpdateRequired = true; + } + } + + // Special handling of backspace + if (m_isLastKeyBackspace) + { + float firstCharPosition = m_TextComponent.rectTransform.anchoredPosition.x + m_TextComponent.textInfo.characterInfo[0].origin - m_TextComponent.margin.x; + float lastCharPosition = m_TextComponent.rectTransform.anchoredPosition.x + m_TextComponent.textInfo.characterInfo[m_TextComponent.textInfo.characterCount - 1].origin + m_TextComponent.margin.z; + + // Check if caret is at the left most position of the viewport + if (m_TextComponent.rectTransform.anchoredPosition.x + startPosition.x <= viewportMin + 0.0001f) + { + if (firstCharPosition < viewportMin) + { + float offset = Mathf.Min((viewportMax - viewportMin) / 2, viewportMin - firstCharPosition); + m_TextComponent.rectTransform.anchoredPosition += new Vector2(offset, 0); + AssignPositioningIfNeeded(); + } + } + else if (lastCharPosition < viewportMax && firstCharPosition < viewportMin) + { + float offset = Mathf.Min(viewportMax - lastCharPosition, viewportMin - firstCharPosition); + + m_TextComponent.rectTransform.anchoredPosition += new Vector2(offset, 0); + AssignPositioningIfNeeded(); + } + + m_isLastKeyBackspace = false; + } + + m_forceRectTransformAdjustment = false; + } + + /// + /// Validate the specified input. + /// + protected char Validate(string text, int pos, char ch) + { + // Validation is disabled + if (characterValidation == CharacterValidation.None || !enabled) + return ch; + + if (characterValidation == CharacterValidation.Integer || characterValidation == CharacterValidation.Decimal) + { + // Integer and decimal + bool cursorBeforeDash = (pos == 0 && text.Length > 0 && text[0] == '-'); + bool selectionAtStart = stringPositionInternal == 0 || stringSelectPositionInternal == 0; + if (!cursorBeforeDash) + { + if (ch >= '0' && ch <= '9') return ch; + if (ch == '-' && (pos == 0 || selectionAtStart)) return ch; + if (ch == '.' && characterValidation == CharacterValidation.Decimal && !text.Contains(".")) return ch; + } + } + else if (characterValidation == CharacterValidation.Digit) + { + if (ch >= '0' && ch <= '9') return ch; + } + else if (characterValidation == CharacterValidation.Alphanumeric) + { + // All alphanumeric characters + if (ch >= 'A' && ch <= 'Z') return ch; + if (ch >= 'a' && ch <= 'z') return ch; + if (ch >= '0' && ch <= '9') return ch; + } + else if (characterValidation == CharacterValidation.Name) + { + char lastChar = (text.Length > 0) ? text[Mathf.Clamp(pos, 0, text.Length - 1)] : ' '; + char nextChar = (text.Length > 0) ? text[Mathf.Clamp(pos + 1, 0, text.Length - 1)] : '\n'; + + if (char.IsLetter(ch)) + { + // Space followed by a letter -- make sure it's capitalized + if (char.IsLower(ch) && lastChar == ' ') + return char.ToUpper(ch); + + // Uppercase letters are only allowed after spaces (and apostrophes) + if (char.IsUpper(ch) && lastChar != ' ' && lastChar != '\'') + return char.ToLower(ch); + + // If character was already in correct case, return it as-is. + // Also, letters that are neither upper nor lower case are always allowed. + return ch; + } + else if (ch == '\'') + { + // Don't allow more than one apostrophe + if (lastChar != ' ' && lastChar != '\'' && nextChar != '\'' && !text.Contains("'")) + return ch; + } + else if (ch == ' ') + { + // Don't allow more than one space in a row + if (lastChar != ' ' && lastChar != '\'' && nextChar != ' ' && nextChar != '\'') + return ch; + } + } + else if (characterValidation == CharacterValidation.EmailAddress) + { + // From StackOverflow about allowed characters in email addresses: + // Uppercase and lowercase English letters (a-z, A-Z) + // Digits 0 to 9 + // Characters ! # $ % & ' * + - / = ? ^ _ ` { | } ~ + // Character . (dot, period, full stop) provided that it is not the first or last character, + // and provided also that it does not appear two or more times consecutively. + + if (ch >= 'A' && ch <= 'Z') return ch; + if (ch >= 'a' && ch <= 'z') return ch; + if (ch >= '0' && ch <= '9') return ch; + if (ch == '@' && text.IndexOf('@') == -1) return ch; + if (kEmailSpecialCharacters.IndexOf(ch) != -1) return ch; + if (ch == '.') + { + char lastChar = (text.Length > 0) ? text[Mathf.Clamp(pos, 0, text.Length - 1)] : ' '; + char nextChar = (text.Length > 0) ? text[Mathf.Clamp(pos + 1, 0, text.Length - 1)] : '\n'; + if (lastChar != '.' && nextChar != '.') + return ch; + } + } + else if (characterValidation == CharacterValidation.Regex) + { + // Regex expression + if (Regex.IsMatch(ch.ToString(), m_RegexValue)) + { + return ch; + } + } + else if (characterValidation == CharacterValidation.CustomValidator) + { + if (m_InputValidator != null) + { + char c = m_InputValidator.Validate(ref text, ref pos, ch); + m_Text = text; + stringSelectPositionInternal = stringPositionInternal = pos; + return c; + } + } + return (char)0; + } + + public void ActivateInputField() + { + if (m_TextComponent == null || m_TextComponent.font == null || !IsActive() || !IsInteractable()) + return; + + if (isFocused) + { + if (m_SoftKeyboard != null && !m_SoftKeyboard.active) + { + m_SoftKeyboard.active = true; + m_SoftKeyboard.text = m_Text; + } + } + + m_ShouldActivateNextUpdate = true; + } + + private void ActivateInputFieldInternal() + { + if (EventSystem.current == null) + return; + + if (EventSystem.current.currentSelectedGameObject != gameObject) + EventSystem.current.SetSelectedGameObject(gameObject); + + if (TouchScreenKeyboard.isSupported && shouldHideSoftKeyboard == false) + { + if (inputSystem.touchSupported) + { + TouchScreenKeyboard.hideInput = shouldHideMobileInput; + } + + if (shouldHideSoftKeyboard == false && m_ReadOnly == false && contentType != ContentType.Custom) + { + m_SoftKeyboard = (inputType == InputType.Password) ? + TouchScreenKeyboard.Open(m_Text, keyboardType, false, multiLine, true, false, "", characterLimit) : + TouchScreenKeyboard.Open(m_Text, keyboardType, inputType == InputType.AutoCorrect, multiLine, false, false, "", characterLimit); + + if (shouldHideMobileInput == false) + { + MoveTextEnd(false); + } + else + { + OnFocus(); + + // Opening the soft keyboard sets its selection to the end of the text. + // As such, we set the selection to match the Input Field's internal selection. + if (m_SoftKeyboard != null) + { + int length = stringPositionInternal < stringSelectPositionInternal ? stringSelectPositionInternal - stringPositionInternal : stringPositionInternal - stringSelectPositionInternal; + m_SoftKeyboard.selection = new RangeInt(stringPositionInternal < stringSelectPositionInternal ? stringPositionInternal : stringSelectPositionInternal, length); + } + } + } + + // Cache the value of isInPlaceEditingAllowed, because on UWP this involves calling into native code + // The value only needs to be updated once when the TouchKeyboard is opened. + #if UNITY_2019_1_OR_NEWER + m_TouchKeyboardAllowsInPlaceEditing = TouchScreenKeyboard.isInPlaceEditingAllowed; + #endif + } + else + { + if (!TouchScreenKeyboard.isSupported) + inputSystem.imeCompositionMode = IMECompositionMode.On; + + OnFocus(); + } + + m_AllowInput = true; + m_OriginalText = text; + m_WasCanceled = false; + SetCaretVisible(); + UpdateLabel(); + } + + public override void OnSelect(BaseEventData eventData) + { + //Debug.Log("OnSelect()"); + + base.OnSelect(eventData); + SendOnFocus(); + + ActivateInputField(); + } + + public virtual void OnPointerClick(PointerEventData eventData) + { + //Debug.Log("Pointer Click Event..."); + + if (eventData.button != PointerEventData.InputButton.Left) + return; + + ActivateInputField(); + } + + public void OnControlClick() + { + //Debug.Log("Input Field control click..."); + } + + public void ReleaseSelection() + { + m_SelectionStillActive = false; + MarkGeometryAsDirty(); + } + + public void DeactivateInputField(bool clearSelection = false) + { + //Debug.Log("Deactivate Input Field..."); + + // Not activated do nothing. + if (!m_AllowInput) + return; + + m_HasDoneFocusTransition = false; + m_AllowInput = false; + + if (m_Placeholder != null) + m_Placeholder.enabled = string.IsNullOrEmpty(m_Text); + + if (m_TextComponent != null && IsInteractable()) + { + if (m_WasCanceled && m_RestoreOriginalTextOnEscape) + text = m_OriginalText; + + if (m_SoftKeyboard != null) + { + m_SoftKeyboard.active = false; + m_SoftKeyboard = null; + } + + m_SelectionStillActive = true; + + if (m_ResetOnDeActivation || m_ReleaseSelection) + { + //m_StringPosition = m_StringSelectPosition = 0; + //m_CaretPosition = m_CaretSelectPosition = 0; + //m_TextComponent.rectTransform.localPosition = m_DefaultTransformPosition; + + //if (caretRectTrans != null) + // caretRectTrans.localPosition = Vector3.zero; + + m_SelectionStillActive = false; + m_ReleaseSelection = false; + m_SelectedObject = null; + } + + SendOnEndEdit(); + SendOnEndTextSelection(); + + inputSystem.imeCompositionMode = IMECompositionMode.Auto; + } + + MarkGeometryAsDirty(); + + // Scrollbar should be updated. + m_IsScrollbarUpdateRequired = true; + } + + public override void OnDeselect(BaseEventData eventData) + { + DeactivateInputField(); + + base.OnDeselect(eventData); + SendOnFocusLost(); + } + + public virtual void OnSubmit(BaseEventData eventData) + { + //Debug.Log("OnSubmit()"); + + if (!IsActive() || !IsInteractable()) + return; + + if (!isFocused) + m_ShouldActivateNextUpdate = true; + + SendOnSubmit(); + } + + //public virtual void OnLostFocus(BaseEventData eventData) + //{ + // if (!IsActive() || !IsInteractable()) + // return; + //} + + private void EnforceContentType() + { + switch (contentType) + { + case ContentType.Standard: + { + // Don't enforce line type for this content type. + m_InputType = InputType.Standard; + m_KeyboardType = TouchScreenKeyboardType.Default; + m_CharacterValidation = CharacterValidation.None; + break; + } + case ContentType.Autocorrected: + { + // Don't enforce line type for this content type. + m_InputType = InputType.AutoCorrect; + m_KeyboardType = TouchScreenKeyboardType.Default; + m_CharacterValidation = CharacterValidation.None; + break; + } + case ContentType.IntegerNumber: + { + m_LineType = LineType.SingleLine; + m_InputType = InputType.Standard; + m_KeyboardType = TouchScreenKeyboardType.NumberPad; + m_CharacterValidation = CharacterValidation.Integer; + break; + } + case ContentType.DecimalNumber: + { + m_LineType = LineType.SingleLine; + m_InputType = InputType.Standard; + m_KeyboardType = TouchScreenKeyboardType.NumbersAndPunctuation; + m_CharacterValidation = CharacterValidation.Decimal; + break; + } + case ContentType.Alphanumeric: + { + m_LineType = LineType.SingleLine; + m_InputType = InputType.Standard; + m_KeyboardType = TouchScreenKeyboardType.ASCIICapable; + m_CharacterValidation = CharacterValidation.Alphanumeric; + break; + } + case ContentType.Name: + { + m_LineType = LineType.SingleLine; + m_InputType = InputType.Standard; + m_KeyboardType = TouchScreenKeyboardType.Default; + m_CharacterValidation = CharacterValidation.Name; + break; + } + case ContentType.EmailAddress: + { + m_LineType = LineType.SingleLine; + m_InputType = InputType.Standard; + m_KeyboardType = TouchScreenKeyboardType.EmailAddress; + m_CharacterValidation = CharacterValidation.EmailAddress; + break; + } + case ContentType.Password: + { + m_LineType = LineType.SingleLine; + m_InputType = InputType.Password; + m_KeyboardType = TouchScreenKeyboardType.Default; + m_CharacterValidation = CharacterValidation.None; + break; + } + case ContentType.Pin: + { + m_LineType = LineType.SingleLine; + m_InputType = InputType.Password; + m_KeyboardType = TouchScreenKeyboardType.NumberPad; + m_CharacterValidation = CharacterValidation.Digit; + break; + } + default: + { + // Includes Custom type. Nothing should be enforced. + break; + } + } + + SetTextComponentWrapMode(); + } + + void SetTextComponentWrapMode() + { + if (m_TextComponent == null) + return; + + if (multiLine) + m_TextComponent.enableWordWrapping = true; + else + m_TextComponent.enableWordWrapping = false; + } + + // Control Rich Text option on the text component. + void SetTextComponentRichTextMode() + { + if (m_TextComponent == null) + return; + + m_TextComponent.richText = m_RichText; + } + + void SetToCustomIfContentTypeIsNot(params ContentType[] allowedContentTypes) + { + if (contentType == ContentType.Custom) + return; + + for (int i = 0; i < allowedContentTypes.Length; i++) + if (contentType == allowedContentTypes[i]) + return; + + contentType = ContentType.Custom; + } + + void SetToCustom() + { + if (contentType == ContentType.Custom) + return; + + contentType = ContentType.Custom; + } + + void SetToCustom(CharacterValidation characterValidation) + { + if (contentType == ContentType.Custom) + { + characterValidation = CharacterValidation.CustomValidator; + return; + } + + contentType = ContentType.Custom; + characterValidation = CharacterValidation.CustomValidator; + } + + + protected override void DoStateTransition(SelectionState state, bool instant) + { + if (m_HasDoneFocusTransition) + state = SelectionState.Highlighted; + else if (state == SelectionState.Pressed) + m_HasDoneFocusTransition = true; + + base.DoStateTransition(state, instant); + } + + + /// + /// See ILayoutElement.CalculateLayoutInputHorizontal. + /// + public virtual void CalculateLayoutInputHorizontal() + { } + + /// + /// See ILayoutElement.CalculateLayoutInputVertical. + /// + public virtual void CalculateLayoutInputVertical() + { } + + /// + /// See ILayoutElement.minWidth. + /// + public virtual float minWidth { get { return 0; } } + + /// + /// Get the displayed with of all input characters. + /// + public virtual float preferredWidth + { + get + { + if (textComponent == null) + return 0; + + return m_TextComponent.preferredWidth + 16 + m_CaretWidth + 1; + } + } + + /// + /// See ILayoutElement.flexibleWidth. + /// + public virtual float flexibleWidth { get { return -1; } } + + /// + /// See ILayoutElement.minHeight. + /// + public virtual float minHeight { get { return 0; } } + + /// + /// Get the height of all the text if constrained to the height of the RectTransform. + /// + public virtual float preferredHeight + { + get + { + if (textComponent == null) + return 0; + + return m_TextComponent.preferredHeight + 16; + } + } + + /// + /// See ILayoutElement.flexibleHeight. + /// + public virtual float flexibleHeight { get { return -1; } } + + /// + /// See ILayoutElement.layoutPriority. + /// + public virtual int layoutPriority { get { return 1; } } + + + /// + /// Function to conveniently set the point size of both Placeholder and Input Field text object. + /// + /// + public void SetGlobalPointSize(float pointSize) + { + TMP_Text placeholderTextComponent = m_Placeholder as TMP_Text; + + if (placeholderTextComponent != null) placeholderTextComponent.fontSize = pointSize; + textComponent.fontSize = pointSize; + } + + /// + /// Function to conveniently set the Font Asset of both Placeholder and Input Field text object. + /// + /// + public void SetGlobalFontAsset(TMP_FontAsset fontAsset) + { + TMP_Text placeholderTextComponent = m_Placeholder as TMP_Text; + + if (placeholderTextComponent != null) placeholderTextComponent.font = fontAsset; + textComponent.font = fontAsset; + + } + + } + + + static class SetPropertyUtility + { + public static bool SetColor(ref Color currentValue, Color newValue) + { + if (currentValue.r == newValue.r && currentValue.g == newValue.g && currentValue.b == newValue.b && currentValue.a == newValue.a) + return false; + + currentValue = newValue; + return true; + } + + public static bool SetEquatableStruct(ref T currentValue, T newValue) where T : IEquatable + { + if (currentValue.Equals(newValue)) + return false; + + currentValue = newValue; + return true; + } + + public static bool SetStruct(ref T currentValue, T newValue) where T : struct + { + if (currentValue.Equals(newValue)) + return false; + + currentValue = newValue; + return true; + } + + public static bool SetClass(ref T currentValue, T newValue) where T : class + { + if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue))) + return false; + + currentValue = newValue; + return true; + } + } +} \ No newline at end of file -- cgit v1.2.3