//#define TMP_DEBUG_MODE using System; using System.Collections; using System.Collections.Generic; using System.Threading; 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. /// protected RectTransform m_RectTransform; [SerializeField] protected RectTransform m_TextViewport; protected RectMask2D m_TextComponentRectMask; protected RectMask2D m_TextViewportRectMask; private Rect m_CachedViewportRect; [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; [SerializeField] private LayoutGroup m_LayoutGroup; private IScrollHandler m_IScrollHandlerParent; /// /// 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_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; } } private bool m_IsCompositionActive = false; private bool m_ShouldUpdateIMEWindowPosition = false; private int m_PreviousIMEInsertionLine = 0; private int compositionLength { get { if (m_ReadOnly) return 0; return compositionString.Length; } } 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: #if UNITY_XR_VISIONOS_SUPPORTED case RuntimePlatform.VisionOS: #endif case RuntimePlatform.WSAPlayerX86: case RuntimePlatform.WSAPlayerX64: case RuntimePlatform.WSAPlayerARM: case RuntimePlatform.Stadia: #if UNITY_2020_2_OR_NEWER case RuntimePlatform.PS4: #if !(UNITY_2020_2_1 || UNITY_2020_2_2) case RuntimePlatform.PS5: #endif #endif case RuntimePlatform.Switch: return m_HideSoftKeyboard; default: return true; } } set { switch (Application.platform) { case RuntimePlatform.Android: case RuntimePlatform.IPhonePlayer: case RuntimePlatform.tvOS: #if UNITY_XR_VISIONOS_SUPPORTED case RuntimePlatform.VisionOS: #endif case RuntimePlatform.WSAPlayerX86: case RuntimePlatform.WSAPlayerX64: case RuntimePlatform.WSAPlayerARM: case RuntimePlatform.Stadia: #if UNITY_2020_2_OR_NEWER case RuntimePlatform.PS4: #if !(UNITY_2020_2_1 || UNITY_2020_2_2) case RuntimePlatform.PS5: #endif #endif case RuntimePlatform.Switch: 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: #if UNITY_XR_VISIONOS_SUPPORTED case RuntimePlatform.VisionOS: #endif #if UNITY_2020_2_OR_NEWER case RuntimePlatform.PS4: #if !(UNITY_2020_2_1 || UNITY_2020_2_2) case RuntimePlatform.PS5: #endif #endif case RuntimePlatform.Switch: 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; 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_PreviouslySelectedObject; /// /// 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 + compositionLength; } set { m_CaretPosition = value; ClampCaretPos(ref m_CaretPosition); } } protected int stringPositionInternal { get { return m_StringPosition + compositionLength; } set { m_StringPosition = value; ClampStringPos(ref m_StringPosition); } } protected int caretSelectPositionInternal { get { return m_CaretSelectPosition + compositionLength; } set { m_CaretSelectPosition = value; ClampCaretPos(ref m_CaretSelectPosition); } } protected int stringSelectPositionInternal { get { return m_StringSelectPosition + compositionLength; } 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 (compositionLength != 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 (compositionLength != 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 (compositionLength != 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 (compositionLength != 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 // if UNITY_EDITOR protected override void OnEnable() { //Debug.Log("*** OnEnable() *** - " + this.name); base.OnEnable(); if (m_Text == null) m_Text = string.Empty; // Check if Input Field is driven by any layout components ILayoutController layoutController = GetComponent(); if (layoutController != null) { m_IsDrivenByLayoutComponents = true; m_LayoutGroup = GetComponent(); } else m_IsDrivenByLayoutComponents = false; if (Application.isPlaying) { if (m_CachedInputRenderer == null && m_TextComponent != null) { GameObject go = new GameObject("Caret", typeof(TMP_SelectionCaret)); 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(); } } m_RectTransform = GetComponent(); // Check if parent component has IScrollHandler IScrollHandler[] scrollHandlers = GetComponentsInParent(); if (scrollHandlers.Length > 1) m_IScrollHandlerParent = scrollHandlers[1] as ScrollRect; // Get a reference to the RectMask 2D on the Viewport Text Area object. if (m_TextViewport != null) { m_TextViewportRectMask = m_TextViewport.GetComponent(); UpdateMaskRegions(); } // 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); // Cache reference to Vertical Scrollbar RectTransform and add listener. if (m_VerticalScrollbar != null) { 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) { bool isThisObject = obj == m_TextComponent; if (isThisObject) { if (Application.isPlaying && compositionLength == 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 } if (m_VerticalScrollbar) UpdateScrollbar(); } } 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 (Application.platform == RuntimePlatform.WSAPlayerX86 || Application.platform == RuntimePlatform.WSAPlayerX64 || Application.platform == RuntimePlatform.WSAPlayerARM) return !TouchScreenKeyboard.isSupported || m_TouchKeyboardAllowsInPlaceEditing; 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; } // 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 && m_ResetOnDeActivation) { ReleaseSelection(); return; } if (selectedObject != null && selectedObject != this.gameObject) { if (selectedObject == m_PreviouslySelectedObject) return; m_PreviouslySelectedObject = selectedObject; // Special handling for Vertical Scrollbar if (m_VerticalScrollbar && selectedObject == m_VerticalScrollbar.gameObject) { // Do not release selection return; } // Release selection for all objects when ResetOnDeActivation is true if (m_ResetOnDeActivation) { ReleaseSelection(); return; } // Release current selection of selected object is another Input Field if (selectedObject.GetComponent() != null) ReleaseSelection(); return; } #if ENABLE_INPUT_SYSTEM if (m_ProcessingEvent != null && m_ProcessingEvent.rawType == EventType.MouseDown && m_ProcessingEvent.button == 0) { // 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; ReleaseSelection(); return; } } #else 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; ReleaseSelection(); return; } } #endif } UpdateMaskRegions(); 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' || 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 != null && 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; CaretPosition insertionSide; int insertionIndex = TMP_TextUtilities.GetCursorIndexFromPosition(m_TextComponent, eventData.position, eventData.pressEventCamera, out 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) { Vector2 localMousePos; RectTransformUtility.ScreenPointToLocalPointInRectangle(textViewport, eventData.position, eventData.pressEventCamera, out 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; } } #if ENABLE_INPUT_SYSTEM Event.PopEvent(m_ProcessingEvent); bool shift = m_ProcessingEvent != null && (m_ProcessingEvent.modifiers & EventModifiers.Shift) != 0; #else bool shift = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); #endif // 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) { CaretPosition insertionSide; int insertionIndex = TMP_TextUtilities.GetCursorIndexFromPosition(m_TextComponent, eventData.position, eventData.pressEventCamera, out 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); } m_isSelectAll = false; } 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'; // Convert Shift Enter to Vertical tab if (shift && c == '\n') c = '\v'; if (IsValidChar(c)) { Append(c); } if (c == 0) { if (compositionLength > 0) { UpdateLabel(); } } return EditState.Continue; } protected virtual bool IsValidChar(char c) { // Null character if (c == 0) return false; // Delete key on mac if (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; EditState shouldContinue; while (Event.PopEvent(m_ProcessingEvent)) { //Debug.Log("Event: " + m_ProcessingEvent.ToString() + " IsCompositionActive= " + m_IsCompositionActive + " Composition Length: " + compositionLength); switch (m_ProcessingEvent.rawType) { case EventType.KeyUp: // TODO: Figure out way to handle navigation during IME Composition. break; case EventType.KeyDown: consumedEvent = true; // Special handling on OSX which produces more events which need to be suppressed. if (m_IsCompositionActive && compositionLength == 0) { //if (m_ProcessingEvent.keyCode == KeyCode.Backspace && m_ProcessingEvent.modifiers == EventModifiers.None) //{ // int eventCount = Event.GetEventCount(); // // Suppress all subsequent events // for (int i = 0; i < eventCount; i++) // Event.PopEvent(m_ProcessingEvent); // break; //} // Suppress other events related to navigation or termination of composition sequence. if (m_ProcessingEvent.character == 0 && m_ProcessingEvent.modifiers == EventModifiers.None) break; } shouldContinue = KeyPressed(m_ProcessingEvent); if (shouldContinue == EditState.Finish) { if (!m_WasCanceled) SendOnSubmit(); DeactivateInputField(); break; } m_IsTextComponentUpdateRequired = true; UpdateLabel(); break; 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) { // Return if Single Line if (m_LineType == LineType.SingleLine) { if (m_IScrollHandlerParent != null) m_IScrollHandlerParent.OnScroll(eventData); return; } if (m_TextComponent.preferredHeight < m_TextViewport.rect.height) return; float scrollDirection = -eventData.scrollDelta.y; // Determine the current scroll position of the text within the viewport m_ScrollPosition = GetScrollPositionRelativeToViewport(); m_ScrollPosition += (1f / m_TextComponent.textInfo.lineCount) * scrollDirection * m_ScrollSensitivity; m_ScrollPosition = Mathf.Clamp01(m_ScrollPosition); AdjustTextPositionRelativeToViewport(m_ScrollPosition); if (m_VerticalScrollbar) { m_VerticalScrollbar.value = m_ScrollPosition; } //Debug.Log(GetInstanceID() + "- Scroll Position:" + m_ScrollPosition); } float GetScrollPositionRelativeToViewport() { // Determine the current scroll position of the text within the viewport Rect viewportRect = m_TextViewport.rect; float scrollPosition = (m_TextComponent.textInfo.lineInfo[0].ascender - viewportRect.yMax + m_TextComponent.rectTransform.anchoredPosition.y) / ( m_TextComponent.preferredHeight - viewportRect.height); scrollPosition = (int)((scrollPosition * 1000) + 0.5f) / 1000.0f; return 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 = caretSelectPositionInternal < 1 ? m_TextComponent.textInfo.characterInfo[0].index : m_TextComponent.textInfo.characterInfo[caretSelectPositionInternal - 1].index; } } 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(); } #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(); } #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 (m_StringPosition == m_StringSelectPosition) return; if (m_isRichTextEditingAllowed || m_isSelectAll) { // Handling of Delete when Rich Text is allowed. if (m_StringPosition < m_StringSelectPosition) { m_Text = text.Remove(m_StringPosition, m_StringSelectPosition - m_StringPosition); m_StringSelectPosition = m_StringPosition; } else { m_Text = text.Remove(m_StringSelectPosition, m_StringPosition - m_StringSelectPosition); m_StringPosition = m_StringSelectPosition; } if (m_isSelectAll) { m_CaretPosition = m_CaretSelectPosition = 0; m_isSelectAll = false; } } else { if (m_CaretPosition < m_CaretSelectPosition) { m_StringPosition = m_TextComponent.textInfo.characterInfo[m_CaretPosition].index; m_StringSelectPosition = m_TextComponent.textInfo.characterInfo[m_CaretSelectPosition - 1].index + m_TextComponent.textInfo.characterInfo[m_CaretSelectPosition - 1].stringLength; m_Text = text.Remove(m_StringPosition, m_StringSelectPosition - m_StringPosition); m_StringSelectPosition = m_StringPosition; m_CaretSelectPosition = m_CaretPosition; } else { m_StringPosition = m_TextComponent.textInfo.characterInfo[m_CaretPosition - 1].index + m_TextComponent.textInfo.characterInfo[m_CaretPosition - 1].stringLength; m_StringSelectPosition = m_TextComponent.textInfo.characterInfo[m_CaretSelectPosition].index; m_Text = text.Remove(m_StringSelectPosition, m_StringPosition - m_StringSelectPosition); m_StringPosition = m_StringSelectPosition; m_CaretPosition = m_CaretSelectPosition; } } #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) { m_isLastKeyBackspace = true; 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); m_isLastKeyBackspace = true; 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); m_isLastKeyBackspace = true; 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) { m_isLastKeyBackspace = true; 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 < 1 ? m_TextComponent.textInfo.characterInfo[0].index : m_TextComponent.textInfo.characterInfo[caretPositionInternal - 1].index; 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 int insertionPosition = Mathf.Min(stringPositionInternal, stringSelectPositionInternal); //Get the text based on selection for validation instead of whole text(case 1253193). var validateText = text; if (selectionFocusPosition != selectionAnchorPosition) { if (m_isRichTextEditingAllowed || m_isSelectAll) { // Handling of Delete when Rich Text is allowed. if (m_StringPosition < m_StringSelectPosition) validateText = text.Remove(m_StringPosition, m_StringSelectPosition - m_StringPosition); else validateText = text.Remove(m_StringSelectPosition, m_StringPosition - m_StringSelectPosition); } else { if (m_CaretPosition < m_CaretSelectPosition) { m_StringPosition = m_TextComponent.textInfo.characterInfo[m_CaretPosition].index; m_StringSelectPosition = m_TextComponent.textInfo.characterInfo[m_CaretSelectPosition - 1].index + m_TextComponent.textInfo.characterInfo[m_CaretSelectPosition - 1].stringLength; validateText = text.Remove(m_StringPosition, m_StringSelectPosition - m_StringPosition); } else { m_StringPosition = m_TextComponent.textInfo.characterInfo[m_CaretPosition - 1].index + m_TextComponent.textInfo.characterInfo[m_CaretPosition - 1].stringLength; m_StringSelectPosition = m_TextComponent.textInfo.characterInfo[m_CaretSelectPosition].index; validateText = text.Remove(m_StringSelectPosition, m_StringPosition - m_StringSelectPosition); } } } if (onValidateInput != null) { input = onValidateInput(validateText, insertionPosition, input); } else if (characterValidation == CharacterValidation.CustomValidator) { input = Validate(validateText, insertionPosition, input); if (input == 0) return; SendOnValueChanged(); UpdateLabel(); return; } else if (characterValidation != CharacterValidation.None) { input = Validate(validateText, insertionPosition, 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; //Debug.Log("Inserting character " + m_IsCompositionActive); 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)) m_CaretSelectPosition = m_CaretPosition += 1; m_StringSelectPosition = m_StringPosition += 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 (compositionLength > 0 && m_ReadOnly == false) { //Input.imeCompositionMode = IMECompositionMode.On; // Handle selections Delete(); if (m_RichText) fullText = text.Substring(0, m_StringPosition) + "" + compositionString + "" + text.Substring(m_StringPosition); else fullText = text.Substring(0, m_StringPosition) + compositionString + text.Substring(m_StringPosition); m_IsCompositionActive = true; //Debug.Log("[" + Time.frameCount + "] Handling IME Input"); } else { fullText = text; m_IsCompositionActive = false; m_ShouldUpdateIMEWindowPosition = true; } //Debug.Log("Handling IME Input... [" + compositionString + "] of length [" + compositionLength + "] at StringPosition [" + m_StringPosition + "] IsActive [" + m_IsCompositionActive + "]"); 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 && m_ReadOnly == false) { SetCaretVisible(); } m_TextComponent.text = processed + "\u200B"; // Extra space is added for Caret tracking. // Rebuild layout if using Layout components. if (m_IsDrivenByLayoutComponents) LayoutRebuilder.MarkLayoutForRebuild(m_RectTransform); // Special handling to limit the number of lines of text in the Input Field. if (m_LineLimit > 0) { m_TextComponent.ForceMeshUpdate(); TMP_TextInfo textInfo = m_TextComponent.textInfo; // Check if text exceeds maximum number of lines. if (textInfo != null && textInfo.lineCount > m_LineLimit) { int lastValidCharacterIndex = textInfo.lineInfo[m_LineLimit - 1].lastCharacterIndex; int characterStringIndex = textInfo.characterInfo[lastValidCharacterIndex].index + textInfo.characterInfo[lastValidCharacterIndex].stringLength; text = processed.Remove(characterStringIndex, processed.Length - characterStringIndex); m_TextComponent.text = text + "\u200B"; } } if (m_IsTextComponentUpdateRequired || m_VerticalScrollbar) { m_IsTextComponentUpdateRequired = false; m_TextComponent.ForceMeshUpdate(); } MarkGeometryAsDirty(); m_PreventCallback = false; } } void UpdateScrollbar() { // Update Scrollbar if (m_VerticalScrollbar) { Rect viewportRect = m_TextViewport.rect; float size = viewportRect.height / m_TextComponent.preferredHeight; m_VerticalScrollbar.size = size; m_VerticalScrollbar.value = GetScrollPositionRelativeToViewport(); //Debug.Log(GetInstanceID() + "- UpdateScrollbar() - 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(GetInstanceID() + "- OnScrollbarValueChange() - Scrollbar value is: " + value + " Transform POS: " + m_TextComponent.rectTransform.anchoredPosition); } void UpdateMaskRegions() { // TODO: Figure out a better way to handle adding an offset to the masking region // This region is defined by the RectTransform of the GameObject that contains the RectMask2D component. /* // Update Masking Region if (m_TextViewportRectMask != null) { Rect viewportRect = m_TextViewportRectMask.canvasRect; if (viewportRect != m_CachedViewportRect) { m_CachedViewportRect = viewportRect; viewportRect.min -= m_TextViewport.offsetMin * 0.5f; viewportRect.max -= m_TextViewport.offsetMax * 0.5f; if (m_CachedInputRenderer != null) m_CachedInputRenderer.EnableRectClipping(viewportRect); if (m_TextComponent.canvasRenderer != null) m_TextComponent.canvasRenderer.EnableRectClipping(viewportRect); if (m_Placeholder != null && m_Placeholder.enabled) m_Placeholder.canvasRenderer.EnableRectClipping(viewportRect); } } */ } /// /// Adjusts the relative position of the body of the text relative to the viewport. /// /// void AdjustTextPositionRelativeToViewport (float 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; float verticalAlignmentOffset = 0; float textHeight = m_TextComponent.preferredHeight; switch (m_TextComponent.verticalAlignment) { case VerticalAlignmentOptions.Top: verticalAlignmentOffset = 0; break; case VerticalAlignmentOptions.Middle: verticalAlignmentOffset = 0.5f; break; case VerticalAlignmentOptions.Bottom: verticalAlignmentOffset = 1.0f; break; case VerticalAlignmentOptions.Baseline: break; case VerticalAlignmentOptions.Geometry: verticalAlignmentOffset = 0.5f; textHeight = m_TextComponent.bounds.size.y; break; case VerticalAlignmentOptions.Capline: verticalAlignmentOffset = 0.5f; break; } m_TextComponent.rectTransform.anchoredPosition = new Vector2(m_TextComponent.rectTransform.anchoredPosition.x, (textHeight - m_TextViewport.rect.height) * (relativePosition - verticalAlignmentOffset)); 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 (!Application.isPlaying || UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this)) return; #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; } } 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) { GenerateCaret(helper, Vector2.zero); SendOnEndTextSelection(); } else { GenerateHightlight(helper, Vector2.zero); SendOnTextSelection(); } helper.FillMesh(vbo); } } private void GenerateCaret(VertexHelper vbo, Vector2 roundingOffset) { if (m_CaretVisible == false || m_TextComponent.canvas == null || m_ReadOnly) 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; // Make sure caret position does not exceed characterInfo array size. if (caretPositionInternal >= m_TextComponent.textInfo.characterInfo.Length) return; 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]; height = currentCharacter.ascender - currentCharacter.descender; if (m_TextComponent.verticalAlignment == VerticalAlignmentOptions.Geometry) startPosition = new Vector2(currentCharacter.origin, 0 - height / 2); else startPosition = new Vector2(currentCharacter.origin, currentCharacter.descender); } else { currentCharacter = m_TextComponent.textInfo.characterInfo[caretPositionInternal - 1]; height = currentCharacter.ascender - currentCharacter.descender; if (m_TextComponent.verticalAlignment == VerticalAlignmentOptions.Geometry) startPosition = new Vector2(currentCharacter.xAdvance, 0 - height / 2); else startPosition = new Vector2(currentCharacter.xAdvance, currentCharacter.descender); } if (m_SoftKeyboard != null) { int selectionStart = m_StringPosition; int softKeyboardStringLength = m_SoftKeyboard.text == null ? 0 : m_SoftKeyboard.text.Length; if (selectionStart < 0) selectionStart = 0; if (selectionStart > softKeyboardStringLength) selectionStart = softKeyboardStringLength; m_SoftKeyboard.selection = new RangeInt(selectionStart, 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 || m_isLastKeyBackspace) 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, top, 0.0f); m_CursorVerts[3].position = new Vector3(startPosition.x + width, 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); // Update position of IME window when necessary. if (m_ShouldUpdateIMEWindowPosition || currentLine != m_PreviousIMEInsertionLine) { m_ShouldUpdateIMEWindowPosition = false; m_PreviousIMEInsertionLine = currentLine; // Calculate position of IME Window in screen space. Camera cameraRef; if (m_TextComponent.canvas.renderMode == RenderMode.ScreenSpaceOverlay) cameraRef = null; else { cameraRef = m_TextComponent.canvas.worldCamera; if (cameraRef == null) cameraRef = Camera.current; } Vector3 cursorPosition = m_CachedInputRenderer.gameObject.transform.TransformPoint(m_CursorVerts[0].position); Vector2 screenPosition = RectTransformUtility.WorldToScreenPoint(cameraRef, cursorPosition); screenPosition.y = Screen.height - screenPosition.y; if (inputSystem != null) inputSystem.compositionCursorPos = screenPosition; //Debug.Log("[" + Time.frameCount + "] Updating IME Window position Cursor Pos: (" + cursorPosition + ") Screen Pos: (" + screenPosition + ") with Composition Length: " + compositionLength); } //#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) { // Update Masking Region UpdateMaskRegions(); // Make sure caret position does not exceed characterInfo array size. //if (caretSelectPositionInternal >= m_TextComponent.textInfo.characterInfo.Length) // return; TMP_TextInfo textInfo = m_TextComponent.textInfo; m_CaretPosition = GetCaretPositionFromStringIndex(stringPositionInternal); m_CaretSelectPosition = GetCaretPositionFromStringIndex(stringSelectPositionInternal); if (m_SoftKeyboard != null) { int stringPosition = m_CaretPosition < m_CaretSelectPosition ? textInfo.characterInfo[m_CaretPosition].index : textInfo.characterInfo[m_CaretSelectPosition].index; int length = m_CaretPosition < m_CaretSelectPosition ? 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 (m_CaretSelectPosition < textInfo.characterCount) { caretPosition = new Vector2(textInfo.characterInfo[m_CaretSelectPosition].origin, textInfo.characterInfo[m_CaretSelectPosition].descender); height = textInfo.characterInfo[m_CaretSelectPosition].ascender - textInfo.characterInfo[m_CaretSelectPosition].descender; } else { caretPosition = new Vector2(textInfo.characterInfo[m_CaretSelectPosition - 1].xAdvance, textInfo.characterInfo[m_CaretSelectPosition - 1].descender); height = textInfo.characterInfo[m_CaretSelectPosition - 1].ascender - textInfo.characterInfo[m_CaretSelectPosition - 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, m_CaretPosition); int endChar = Mathf.Max(0, m_CaretSelectPosition); // 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++; } //#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) return; Vector3 localPosition = transform.localPosition; Vector3 textComponentLocalPosition = m_TextComponent.rectTransform.localPosition; Vector3 textViewportLocalPosition = m_TextViewport.localPosition; Rect textViewportRect = m_TextViewport.rect; Vector2 caretPosition = new Vector2(startPosition.x + textComponentLocalPosition.x + textViewportLocalPosition.x + localPosition.x, startPosition.y + textComponentLocalPosition.y + textViewportLocalPosition.y + localPosition.y); Rect viewportWSRect = new Rect(localPosition.x + textViewportLocalPosition.x + textViewportRect.x, localPosition.y + textViewportLocalPosition.y + textViewportRect.y, textViewportRect.width, textViewportRect.height); // Adjust the position of the RectTransform based on the caret position in the viewport. float rightOffset = viewportWSRect.xMax - (caretPosition.x + m_TextComponent.margin.z + m_CaretWidth); if (rightOffset < 0f) { if (!multiLine || (multiLine && isCharVisible)) { //Debug.Log("Shifting text to the LEFT by " + rightOffset.ToString("f3")); m_TextComponent.rectTransform.anchoredPosition += new Vector2(rightOffset, 0); AssignPositioningIfNeeded(); } } float leftOffset = (caretPosition.x - m_TextComponent.margin.x) - viewportWSRect.xMin; if (leftOffset < 0f) { //Debug.Log("Shifting text to the RIGHT 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 = viewportWSRect.yMax - (caretPosition.y + height); if (topOffset < -0.0001f) { //Debug.Log("Shifting text to Up " + topOffset.ToString("f3")); m_TextComponent.rectTransform.anchoredPosition += new Vector2(0, topOffset); AssignPositioningIfNeeded(); } float bottomOffset = caretPosition.y - viewportWSRect.yMin; if (bottomOffset < 0f) { //Debug.Log("Shifting text to Down " + bottomOffset.ToString("f3")); m_TextComponent.rectTransform.anchoredPosition -= new Vector2(0, bottomOffset); AssignPositioningIfNeeded(); } } // Special handling of backspace if (m_isLastKeyBackspace) { float anchoredPositionX = m_TextComponent.rectTransform.anchoredPosition.x; float firstCharPosition = localPosition.x + textViewportLocalPosition.x + textComponentLocalPosition.x + m_TextComponent.textInfo.characterInfo[0].origin - m_TextComponent.margin.x; float lastCharPosition = localPosition.x + textViewportLocalPosition.x + textComponentLocalPosition.x + m_TextComponent.textInfo.characterInfo[m_TextComponent.textInfo.characterCount - 1].origin + m_TextComponent.margin.z + m_CaretWidth; if (anchoredPositionX > 0.0001f && firstCharPosition > viewportWSRect.xMin) { float offset = viewportWSRect.xMin - firstCharPosition; if (anchoredPositionX < -offset) offset = -anchoredPositionX; m_TextComponent.rectTransform.anchoredPosition += new Vector2(offset, 0); AssignPositioningIfNeeded(); } else if (anchoredPositionX < -0.0001f && lastCharPosition < viewportWSRect.xMax) { float offset = viewportWSRect.xMax - lastCharPosition; if (-anchoredPositionX < offset) offset = -anchoredPositionX; 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; var separator = Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator; if (ch == Convert.ToChar(separator) && characterValidation == CharacterValidation.Decimal && !text.Contains(separator)) 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 prevChar = (text.Length > 0) ? text[Mathf.Clamp(pos - 1, 0, text.Length - 1)] : ' '; 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)) { // First letter is always capitalized if (char.IsLower(ch) && pos == 0) return char.ToUpper(ch); // Letter following a space or hyphen is always capitalized if (char.IsLower(ch) && (prevChar == ' ' || prevChar == '-')) return char.ToUpper(ch); // Uppercase letters are only allowed after spaces, apostrophes, hyphens or lowercase letter if (char.IsUpper(ch) && pos > 0 && prevChar != ' ' && prevChar != '\'' && prevChar != '-' && !char.IsLower(prevChar)) return char.ToLower(ch); // Do not allow uppercase characters to be inserted before another uppercase character if (char.IsUpper(ch) && char.IsUpper(lastChar)) return (char)0; // 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; } // Allow inserting a hyphen after a character if (char.IsLetter(prevChar) && ch == '-' && lastChar != '-') { return ch; } if ((ch == ' ' || ch == '-') && pos != 0) { // Don't allow more than one space in a row if (prevChar != ' ' && prevChar != '\'' && prevChar != '-' && lastChar != ' ' && lastChar != '\'' && lastChar != '-' && nextChar != ' ' && 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 != null && inputSystem.touchSupported) { TouchScreenKeyboard.hideInput = shouldHideMobileInput; } if (shouldHideSoftKeyboard == false && m_ReadOnly == false) { 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); 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 && m_ReadOnly == false && inputSystem != null) 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; m_ReleaseSelection = false; m_PreviouslySelectedObject = null; MarkGeometryAsDirty(); SendOnEndEdit(); SendOnEndTextSelection(); } 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 (m_VerticalScrollbar == null) ReleaseSelection(); } if (inputSystem != null) inputSystem.imeCompositionMode = IMECompositionMode.Auto; } MarkGeometryAsDirty(); } 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.Selected; 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; float horizontalPadding = 0; if (m_LayoutGroup != null) horizontalPadding = m_LayoutGroup.padding.horizontal; if (m_TextViewport != null) horizontalPadding += m_TextViewport.offsetMin.x - m_TextViewport.offsetMax.x; return m_TextComponent.preferredWidth + horizontalPadding; // Should add some extra padding for caret } } /// /// 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; float verticalPadding = 0; if (m_LayoutGroup != null) verticalPadding = m_LayoutGroup.padding.vertical; if (m_TextViewport != null) verticalPadding += m_TextViewport.offsetMin.y - m_TextViewport.offsetMax.y; return m_TextComponent.preferredHeight + verticalPadding; } } /// /// 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; } } }