Search Unity

  1. Get the latest news, tutorials and offers directly to your inbox with our newsletters. Sign up now.
    Dismiss Notice

TextMesh Pro TextMeshPro hyperlinks

Discussion in 'UGUI & TextMesh Pro' started by Epsilon_Delta, Apr 11, 2021.

  1. Epsilon_Delta

    Epsilon_Delta

    Joined:
    Mar 14, 2018
    Posts:
    44
    Here's a little script I wrote to support clickable hyperlinks for TextMeshProUGUI. You can attach it to gameobject with TextMeshProUGUI component and it should make <link="url"></link> tags in the text clickable color changing hyperlinks. It supports underline and 6 color states = startColor, hovered, pressed, used, usedHovered, usedPressed. It supports mouse and touch input.

    Note that this is not all-possibilities-encompassing script. It's just something I made for myself and made public. Maybe someone finds it useful as starting point since TMP does not have this kind of component included and some things are not documented that well yet (like underline vertices). I created this script with the help of some TMP Examples & Extras scripts and Stephan_B's videos on this topic. I tested it many times over, but there might be bugs or unoptimized code. Use at your own risk.

    https://github.com/EpsilonD3lta/UnityUtilities/blob/master/Scripts/Runtime/TMProUGUIHyperlinks.cs
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using TMPro;
    3. using UnityEngine;
    4. using UnityEngine.EventSystems;
    5.  
    6. /// <summary>
    7. /// This class handles basic link color behavior, supports also underline
    8. /// Does not support strike-through, but can be easily implemented in the same way as the underline
    9. /// </summary>
    10. [DisallowMultipleComponent()]
    11. [RequireComponent(typeof(TextMeshProUGUI))]
    12. public class TMProUGUIHyperlinks : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
    13. {
    14.     [SerializeField]
    15.     private Color32 hoveredColor = new Color32(0x00, 0x59, 0xFF, 0xFF);
    16.     [SerializeField]
    17.     private Color32 pressedColor = new Color32(0x00, 0x00, 0xB7, 0xFF);
    18.     [SerializeField]
    19.     private Color32 usedColor = new Color32(0xFF, 0x00, 0xFF, 0xFF);
    20.     [SerializeField]
    21.     private Color32 usedHoveredColor = new Color32(0xFD, 0x5E, 0xFD, 0xFF);
    22.     [SerializeField]
    23.     private Color32 usedPressedColor = new Color32(0xCF, 0x00, 0xCF, 0xFF);
    24.  
    25.     private List<Color32[]> startColors = new List<Color32[]>();
    26.     private TextMeshProUGUI textMeshPro;
    27.     private Dictionary<int, bool> usedLinks = new Dictionary<int, bool>();
    28.     private int hoveredLinkIndex = -1;
    29.     private int pressedLinkIndex = -1;
    30.     private Camera mainCamera;
    31.  
    32.     void Awake()
    33.     {
    34.         textMeshPro = GetComponent<TextMeshProUGUI>();
    35.  
    36.         mainCamera = Camera.main;
    37.         if (textMeshPro.canvas.renderMode == RenderMode.ScreenSpaceOverlay) mainCamera = null;
    38.         else if (textMeshPro.canvas.worldCamera != null) mainCamera = textMeshPro.canvas.worldCamera;
    39.     }
    40.  
    41.     public void OnPointerDown(PointerEventData eventData)
    42.     {
    43.         int linkIndex = GetLinkIndex();
    44.         if (linkIndex != -1) // Was pointer intersecting a link?
    45.         {
    46.             pressedLinkIndex = linkIndex;
    47.             if (usedLinks.TryGetValue(linkIndex, out bool isUsed) && isUsed) // Has the link been already used?
    48.             {
    49.                 // Have we hovered before we pressed? Touch input will first press, then hover
    50.                 if (pressedLinkIndex != hoveredLinkIndex) startColors = SetLinkColor(linkIndex, usedPressedColor);
    51.                 else SetLinkColor(linkIndex, usedPressedColor);
    52.             }
    53.             else
    54.             {
    55.                 // Have we hovered before we pressed? Touch input will first press, then hover
    56.                 if (pressedLinkIndex != hoveredLinkIndex) startColors = SetLinkColor(linkIndex, pressedColor);
    57.                 else SetLinkColor(linkIndex, pressedColor);
    58.             }
    59.             hoveredLinkIndex = pressedLinkIndex; // Changes flow in LateUpdate
    60.         }
    61.         else pressedLinkIndex = -1;
    62.     }
    63.  
    64.     public void OnPointerUp(PointerEventData eventData)
    65.     {
    66.         int linkIndex = GetLinkIndex();
    67.         if (linkIndex != -1 && linkIndex == pressedLinkIndex) // Was pointer intersecting the same link as OnPointerDown?
    68.         {
    69.             TMP_LinkInfo linkInfo = textMeshPro.textInfo.linkInfo[linkIndex];
    70.             SetLinkColor(linkIndex, usedHoveredColor);
    71.             startColors.ForEach(c => c[0] = c[1] = c[2] = c[3] = usedColor);
    72.             usedLinks[linkIndex] = true;
    73.             Application.OpenURL(linkInfo.GetLinkID());
    74.         }
    75.         pressedLinkIndex = -1;
    76.     }
    77.  
    78.     private void LateUpdate()
    79.     {
    80.         int linkIndex = GetLinkIndex();
    81.         if (linkIndex != -1) // Was pointer intersecting a link?
    82.         {
    83.             if (linkIndex != hoveredLinkIndex) // We started hovering above link (hover can be set from OnPointerDown!)
    84.             {
    85.                 if (hoveredLinkIndex != -1) ResetLinkColor(hoveredLinkIndex, startColors); // If we hovered above other link before
    86.                 hoveredLinkIndex = linkIndex;
    87.                 if (usedLinks.TryGetValue(linkIndex, out bool isUsed) && isUsed) // Has the link been already used?
    88.                 {
    89.                     // If we have pressed on link, wandered away and came back, set the pressed color
    90.                     if (pressedLinkIndex == linkIndex) startColors = SetLinkColor(hoveredLinkIndex, usedPressedColor);
    91.                     else startColors = SetLinkColor(hoveredLinkIndex, usedHoveredColor);
    92.                 }
    93.                 else
    94.                 {
    95.                     // If we have pressed on link, wandered away and came back, set the pressed color
    96.                     if (pressedLinkIndex == linkIndex) startColors = SetLinkColor(hoveredLinkIndex, pressedColor);
    97.                     else startColors = SetLinkColor(hoveredLinkIndex, hoveredColor);
    98.                 }
    99.             }
    100.         }
    101.         else if (hoveredLinkIndex != -1) // If we hovered above other link before
    102.         {
    103.             ResetLinkColor(hoveredLinkIndex, startColors);
    104.             hoveredLinkIndex = -1;
    105.         }
    106.     }
    107.  
    108.     private int GetLinkIndex()
    109.     {
    110.         return TMP_TextUtilities.FindIntersectingLink(textMeshPro, Input.mousePosition, mainCamera);
    111.     }
    112.  
    113.     private List<Color32[]> SetLinkColor(int linkIndex, Color32 color)
    114.     {
    115.         TMP_LinkInfo linkInfo = textMeshPro.textInfo.linkInfo[linkIndex];
    116.  
    117.         var oldVertexColors = new List<Color32[]>(); // Store the old character colors
    118.         int underlineIndex = -1;
    119.         for (int i = 0; i < linkInfo.linkTextLength; i++)
    120.         {
    121.             // For each character in the link string
    122.             int characterIndex = linkInfo.linkTextfirstCharacterIndex + i; // The current character index
    123.             var charInfo = textMeshPro.textInfo.characterInfo[characterIndex];
    124.             int meshIndex = charInfo.materialReferenceIndex; // Get the index of the material/subtext object used by this character.
    125.             int vertexIndex = charInfo.vertexIndex; // Get the index of the first vertex of this character.
    126.  
    127.             // This array contains colors for all vertices of the mesh (might be multiple chars)
    128.             Color32[] vertexColors = textMeshPro.textInfo.meshInfo[meshIndex].colors32;
    129.             oldVertexColors.Add(new Color32[] { vertexColors[vertexIndex + 0], vertexColors[vertexIndex + 1], vertexColors[vertexIndex + 2], vertexColors[vertexIndex + 3] });
    130.             if (charInfo.isVisible)
    131.             {
    132.                 vertexColors[vertexIndex + 0] = color;
    133.                 vertexColors[vertexIndex + 1] = color;
    134.                 vertexColors[vertexIndex + 2] = color;
    135.                 vertexColors[vertexIndex + 3] = color;
    136.             }
    137.             // Each line will have its own underline mesh with different index, index == 0 means there is no underline
    138.             if (charInfo.isVisible && charInfo.underlineVertexIndex > 0 && charInfo.underlineVertexIndex != underlineIndex && charInfo.underlineVertexIndex < vertexColors.Length)
    139.             {
    140.                 underlineIndex = charInfo.underlineVertexIndex;
    141.                 for (int j = 0; j < 12; j++) // Underline seems to be always 3 quads = 12 vertices
    142.                 {
    143.                     vertexColors[underlineIndex + j] = color;
    144.                 }
    145.             }
    146.         }
    147.  
    148.         textMeshPro.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
    149.         return oldVertexColors;
    150.     }
    151.  
    152.     private void ResetLinkColor(int linkIndex, List<Color32[]> startColors)
    153.     {
    154.         TMP_LinkInfo linkInfo = textMeshPro.textInfo.linkInfo[linkIndex];
    155.         int underlineIndex = -1;
    156.         for (int i = 0; i < linkInfo.linkTextLength; i++)
    157.         {
    158.             int characterIndex = linkInfo.linkTextfirstCharacterIndex + i;
    159.             var charInfo = textMeshPro.textInfo.characterInfo[characterIndex];
    160.             int meshIndex = charInfo.materialReferenceIndex;
    161.             int vertexIndex = charInfo.vertexIndex;
    162.  
    163.             Color32[] vertexColors = textMeshPro.textInfo.meshInfo[meshIndex].colors32;
    164.             if (charInfo.isVisible)
    165.             {
    166.                 vertexColors[vertexIndex + 0] = startColors[i][0];
    167.                 vertexColors[vertexIndex + 1] = startColors[i][1];
    168.                 vertexColors[vertexIndex + 2] = startColors[i][2];
    169.                 vertexColors[vertexIndex + 3] = startColors[i][3];
    170.             }
    171.             if (charInfo.isVisible && charInfo.underlineVertexIndex > 0 && charInfo.underlineVertexIndex != underlineIndex && charInfo.underlineVertexIndex < vertexColors.Length)
    172.             {
    173.                 underlineIndex = charInfo.underlineVertexIndex;
    174.                 for (int j = 0; j < 12; j++)
    175.                 {
    176.                     vertexColors[underlineIndex + j] = startColors[i][0];
    177.                 }
    178.             }
    179.         }
    180.  
    181.         textMeshPro.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
    182.     }
    183. }
     
unityunity