Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

TextMesh Pro Clickable link within a text

Discussion in 'UGUI & TextMesh Pro' started by Necronomicron, Jun 11, 2020.

  1. Necronomicron

    Necronomicron

    Joined:
    Mar 4, 2015
    Posts:
    108
    Hello!
    I'm trying to make something like this:
    The docs explain literally nothing.
    This just doesn't work:
    Click <link="http://example.com/">this</link>to be happy.

    And I have no clue what are those IDs and where I have to put them.
     
  2. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Take a look at example "12 - Link Example" and "12a - Text Interactions" included in the TMP Examples & Extras and the scripts these examples use.

    The link tag format is <link="ID">visible text</link>.

    This ID should be unique to allow your code to implement whatever logic is desired when interacting with this link which in end simply define a region of text.

    Since the desired interaction can be anything. Ie. maybe you want something to happen on mouse over or maybe on click and the result of this interaction is to play a sound or open a window or change the color of the text or whatever else, the <link="ID">link description</link> is generic as to allow you to implement any type of logic.

    Again the link simply allows you to define a section of text and to tag it with a unique ID.
     
    DragonCoder, liby99 and Necronomicron like this.
  3. Necronomicron

    Necronomicron

    Joined:
    Mar 4, 2015
    Posts:
    108
    Does something have to happen when I click on the link in 12a? Because nothing happens except some messages in the console.
     
  4. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    In 12a, take a look at the console output as you hover over characters, words, lines, sprites, etc.

    Note: A message is output only when hover over new / different elements.
     
  5. Necronomicron

    Necronomicron

    Joined:
    Mar 4, 2015
    Posts:
    108
    Too much extra work for such a simple task if you ask me. Besides nothing works for me.
    OnPointerClick()
    in
    TMP_TextSelector_B
    seems to be deprecated.
    charIndex
    is always -1. Same for
    linkIndex
    .
     
    Last edited: Jun 11, 2020
  6. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Make sure you are using the latest release of the TMP package which is Preview 14.

    As per the following summary comment
    <param name="camera">The scene camera which may be assigned to a Canvas using ScreenSpace Camera or WorldSpace render mode. Set to null if using ScreenSpace Overlay.</param>

    In terms of too much extra work for such a simple task. I agree that it is a complex task which on the surface would appear like a simple task but you have to consider all the potential interaction a user might want.
    - What should we do when someone clicks a link or hover? Play a sound? Instantiate an object? Open a bowser? Does the platform you target support a bowser? Do we open a window? How big... There is no way to know what your expected behavior and requirements are when interacting with a link.

    As such, the <link=ID> simply allows you to define a region of text for which you can implement any potential handling based on your needs.

    Example 12a does show a simpler way to implement the behaviors you might seek using Events.
     
    Necronomicron likes this.
  7. Necronomicron

    Necronomicron

    Joined:
    Mar 4, 2015
    Posts:
    108
    I've already figured out it was because camera parameter isn't null, but where is it from? I don't have this description in IDE.
     
  8. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    The IDE / Intellisense should bring that up for the Camera parameter of the function.

    P.S. I certainly acknowledge the documentation on TMP is horrible and we have folks working it.

    In the mean time, always do a quick search on the forum as there have been many posts about most topics. If your search fails to produce the result you seek, always feel free to post on the forum where I'll be more than happy to provide assistance if other experience TMP users haven't already done so.
     
  9. Necronomicron

    Necronomicron

    Joined:
    Mar 4, 2015
    Posts:
    108
    Well, it didn't. At least for
    FindIntersectingLink()
    . I have the latest TMPro and I use Visual Studio.
     
    Last edited: Jun 11, 2020
  10. John-B

    John-B

    Joined:
    Nov 14, 2009
    Posts:
    1,259
    I don't understand how this is supposed to work. I just want to know when the user clicks certain words in a TMP canvas text field so I can pop up an image.

    In Example 12a, there are two TMP fields, one uses a mesh renderer, and the other uses a canvas renderer. Only the mesh renderer TMP text shows the console output, the canvas renderer TMP field does nothing, as far as I can tell. I don't see why one works and one doesn't, and I'm not sure how the one that works outputs anything to the console. The TMP field in the example has a script attached, TMP_TextEventHandler, but there are no scripts set for any of the selection events. The output comes from another script, TMP_TextEventCheck. Are both these scripts necessary? I've also noticed that these are NOT selection event as the output says, just rollovers. Nothing different seems to happen when a link is clicked.

    How do I detect a click on a link in a TMP canvas text field so that I can call a function when it's clicked? Is there any sample code that works and show how this is done?
     
  11. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    The Event Processor object in that scene has a reference to the text component that uses the mesh renderer. If you replace that reference by other text component (the one using the Canvas Renderer) then it will work and output in the console.

    The TMP_TextEventHandler.cs contains the logic related to what happens when the mouse intersects with the RectTransform of the text object and then with characters, sprites, words, lines, and links. This logic is contained in the LateUpdate function.

    If you look specifically at the "Example of Character or Sprite Selection", you will see the it uses the
    TMP_TextUtilities.FindIntersectingCharacter(). There are similar functions for words, etc.

    In this example, it is only concerned with the mouse intersecting with a character so you would need to add the part about mouse intersecting + clicking as an additional check.

    You would mirror the implementation of "Example of Link Handling" in LateUpdate() but add the additional check for clicking in addition to existing mouse position check.

    You could revise the conditional check as follows:
    Code (csharp):
    1.  
    2. // Handle new Link selection.
    3. if (linkIndex != -1 && Input.GetKeyDown(KeyCode.Mouse0))
    4. {
    5.     m_selectedLink = linkIndex;
    6.  
    7.     // Get information about the link.
    8.     TMP_LinkInfo linkInfo = m_TextComponent.textInfo.linkInfo[linkIndex];
    9.  
    10.     // Send the event to any listeners.
    11.     SendOnLinkSelection(linkInfo.GetLinkID(), linkInfo.GetLinkText(), linkIndex);
    12. }
    13.  
     
  12. John-B

    John-B

    Joined:
    Nov 14, 2009
    Posts:
    1,259
    I attached two scripts to my TMP text field: TMP_TextEventCheck and TMP_TextEventHandler, and set the correct reference. Now, when I move the mouse over the field, a few words, lines, and characters generate console output, apparently at random, but only a few, and it seems to depend on how the field is scrolled. Whatever text happens to be about a 1/3 of the way down the field at the time, as I scroll, different lines/words get flagged. And the ONE LINK I have in the text does nothing, no output when the mouse goes over the link.

    I could save a lot of my time by putting a transparent button over the link, as I've done in the past, and call it a day. But I'd really like to do it the right way and get this to work, and save the hassle of moving the button every time the text changes.
     
  13. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    In order to avoid spamming the console when the mouse position is over a character, word, line, link, etc. A check is made to only handle newer interactions.

    In this portion of code from the TMP_TextEventHandler.cs, you can see that it will only report new characters that it encounters. Removing the charIndex != m_lastCharIndex would result in constent events being raised while the mouse is over a character. Obviously, you can modify / fit this logic to your needs.

    Code (csharp):
    1.  
    2. // Example of Character or Sprite Selection
    3. int charIndex = TMP_TextUtilities.FindIntersectingCharacter(m_TextComponent, Input.mousePosition, m_Camera, true);
    4. if (charIndex != -1 && charIndex != m_lastCharIndex)
    5. {
    6.     m_lastCharIndex = charIndex;
    7.  
    8.     TMP_TextElementType elementType = m_TextComponent.textInfo.characterInfo[charIndex].elementType;
    9.  
    10.     // Send event to any event listeners depending on whether it is a character or sprite.
    11.     if (elementType == TMP_TextElementType.Character)
    12.         SendOnCharacterSelection(m_TextComponent.textInfo.characterInfo[charIndex].character, charIndex);
    13.     else if (elementType == TMP_TextElementType.Sprite)
    14.         SendOnSpriteSelection(m_TextComponent.textInfo.characterInfo[charIndex].character, charIndex);
    15. }
    16.  
    I would need a better understanding of how your have this setup. Is the text object in some scroll view or something?

    I would suggest first experimenting with a non scrolling text object to make sure everything works as expected. Then add the scrolling part to figure out why that might be an issue.

    If you can't figure it out, please consider providing me with some simple scene / project for me to look at which would enable me to (1) make sure everything is working as expected on the TMP side and then provider pointers as to why you are observing reported behavior.
     
    sohaib_qadri likes this.
  14. mrm83

    mrm83

    Joined:
    Nov 29, 2014
    Posts:
    345
    Did this change between Unity 2019 and 2020? My links are busted in Unity 2020 but works in 2019. Link index returning -1. Tried upgrading to latest version of TMP and the same thing happens.
     
    Last edited: Jun 10, 2021
  15. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Nothing changed on the TMP front.

    Did you switch to the new input system or to SRP? Looking for perhaps a change to some other system.

    See if the example scenes like 12 and 12a still work as expected on your end.
     
  16. mrm83

    mrm83

    Joined:
    Nov 29, 2014
    Posts:
    345
    Got it. was camera issue.
     
  17. d2clon

    d2clon

    Joined:
    Aug 19, 2017
    Posts:
    19
    This generic script worked for me:

    Code (CSharp):
    1. using TMPro;
    2. using UnityEngine;
    3. using UnityEngine.EventSystems;
    4.  
    5. [RequireComponent(typeof(TMP_Text))]
    6. public class LinkOpener : MonoBehaviour, IPointerClickHandler {
    7.  
    8.     public void OnPointerClick(PointerEventData eventData) {
    9.         TMP_Text pTextMeshPro = GetComponent<TMP_Text>();
    10.         int linkIndex = TMP_TextUtilities.FindIntersectingLink(pTextMeshPro, eventData.position, null);  // If you are not in a Canvas using Screen Overlay, put your camera instead of null
    11.         if (linkIndex != -1) { // was a link clicked?
    12.             TMP_LinkInfo linkInfo = pTextMeshPro.textInfo.linkInfo[linkIndex];
    13.             Application.OpenURL(linkInfo.GetLinkID());
    14.         }
    15.     }
    16.  
    17. }
    18.  
    Source: https://www.feelouttheform.net/unity3d-links-textmeshpro/
     
    mitaywalle, Prodigga, CyRaid and 9 others like this.
  18. Gulliver

    Gulliver

    Joined:
    Jan 8, 2013
    Posts:
    101
    doesn't work
     
    S0paTa likes this.
  19. JTSenneker

    JTSenneker

    Joined:
    Jan 18, 2019
    Posts:
    2
    This also worked for me, I just had to make sure the TMP_Text was the TMP component that had the link in it. Thank you for this solution!
     
  20. EmpirisoftNathan

    EmpirisoftNathan

    Joined:
    Sep 6, 2016
    Posts:
    12
    The documentation described in this thread no longer exists, and the old text mesh pro package that contains the scenes is deprecated and no longer usable.

    Where does one learn how to use TextMeshPro in 2021?
     
    sp-LeventeLajtai likes this.
  21. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    The latest TMP package for Unity 2020 or newer is version 3.2.0-pre.1.

    Since preview package discoverability has changed between Unity 2020, 2021 and 2022, you may have to manually reference this new package in the "ProjectRoot/Packages/manifest.json" file.

    The examples for the <link> tag and text interactions are still part of the TMP package. These examples are contained in the TMP Examples & Extras which can be manually imported in the project via the "Window - TextMeshPro - Import TMP Examples & Extras" menu option.

    The examples you should be looking at are 12 - Link Example and 12a - Text Interactions.
     
  22. fafase

    fafase

    Joined:
    Jul 3, 2012
    Posts:
    161
    I think there is a general misunderstanding that link would mean hyperlink that opens a browser.
    Link could be interpreted as meta data for the section of text, at least that how I understand it.

    But hyperlink content can be done with:

    <link="https://www.google.com">Link to Google.com</link>

    It seems that extra space in the tag will break, so no <link = "www.google.com">

    Using the script from previous posts:

    Code (CSharp):
    1.     [RequireComponent(typeof(TMP_Text))]
    2.     public class OpenHyperlink : MonoBehaviour, IPointerClickHandler
    3.     {
    4.         private TMP_Text m_textMeshPro;
    5.         void Start()
    6.         {
    7.             m_textMeshPro = GetComponent<TMP_Text>();
    8.         }
    9.  
    10.         public void OnPointerClick(PointerEventData eventData)
    11.         {
    12.             int linkIndex = TMP_TextUtilities.FindIntersectingLink(m_textMeshPro, Input.mousePosition, null);
    13.             if (linkIndex != -1)
    14.             {
    15.                 TMP_LinkInfo linkInfo = m_textMeshPro.textInfo.linkInfo[linkIndex];  
    16.                 Application.OpenURL(linkInfo.GetLinkID());
    17.             }
    18.         }
    19.     }
    It's the same as above with slight improvement that it finds the component on start. If you have multiple cameras in your scene, this can fail so for instance, I needed extra code:

    Code (CSharp):
    1.     [RequireComponent(typeof(TMP_Text))]
    2.     public class OpenHyperlink : MonoBehaviour, IPointerClickHandler
    3.     {
    4.         private TMP_Text m_textMeshPro;
    5.         private Camera m_uiCamera;
    6.         void Start()
    7.         {
    8.             m_textMeshPro = GetComponent<TMP_Text>();
    9.             Camera[] cameras = FindObjectsOfType<Camera>();
    10.             for(int i = 0; i< cameras.Length; i++)
    11.             {
    12.                 if (cameras[i].CompareTag("UICamera")) // this may be whatever for your case
    13.                 {
    14.                     m_uiCamera = cameras[i];
    15.                     break;
    16.                 }
    17.             }
    18.         }
    19.  
    20.         public void OnPointerClick(PointerEventData eventData)
    21.         {
    22.            // Same
    23.         }
    24.     }
     
  23. ashtorak

    ashtorak

    Joined:
    Feb 19, 2014
    Posts:
    53
    Thanks. What would we do without the forum :D
    My adaptation for new input system including using statements:

    Code (CSharp):
    1. using UnityEngine;
    2. using TMPro;
    3. using UnityEngine.EventSystems;
    4. using UnityEngine.InputSystem;
    5.  
    6.  [RequireComponent(typeof(TMP_Text))]
    7.     public class OpenHyperlink : MonoBehaviour, IPointerClickHandler
    8.     {
    9.         private TMP_Text m_textMeshPro;
    10.         void Start()
    11.         {
    12.             m_textMeshPro = GetComponent<TMP_Text>();
    13.         }
    14.         public void OnPointerClick(PointerEventData eventData)
    15.         {
    16.  
    17.             int linkIndex = TMP_TextUtilities.FindIntersectingLink(m_textMeshPro, new Vector3(Mouse.current.position.x.ReadValue(), Mouse.current.position.y.ReadValue(), 0), PROVIDEYOURCAM);
    18.             if (linkIndex != -1)
    19.             {
    20.                 TMP_LinkInfo linkInfo = m_textMeshPro.textInfo.linkInfo[linkIndex];
    21.                 Application.OpenURL(linkInfo.GetLinkID());
    22.             }
    23.         }
     
  24. masterton

    masterton

    Joined:
    Dec 11, 2012
    Posts:
    41
    If you can't get this to work - your canvas might be "Screen Space - Overlay" - in which case you should pass null as the camera component.

    An older post from Stephan B
     
  25. Nightmare34

    Nightmare34

    Joined:
    Oct 3, 2020
    Posts:
    7
    You can also use :
    eventData.position.x, eventData.position.y instead of Mouse.current.position.x.ReadValue(), Mouse.current.position.y.ReadValue(). This way your are not dependent of the New or old input system.
     
    Hypertectonic and ashtorak like this.
  26. Kilik1985

    Kilik1985

    Joined:
    Oct 10, 2022
    Posts:
    2
    I pass null for the camera and eventData.position but the app does nothing when I click on a link. The code works when linkIndex != -1 but it's like the script stops working when I do click on a link.

    The variable linkIndex doesn't even get a value when I click on the link. I set a breakpoint on the line and I can't get any value, it's as if the code just doesn't execute anymore.

    EDIT: nothing to do with the script, sorry. My link was at the bottom of a viewport and clicks aren't being registered there. That's another issue I have to figure it out now
     
    Last edited: Dec 2, 2022
  27. FilippoG

    FilippoG

    Joined:
    Nov 18, 2019
    Posts:
    16
    There is no way I can get links to work...
    Camera is found, mesh too... Still nothing happens when clicking on link.
    OnPointerClick is never triggered.
    I add the script to the Text component in the canvas, but nothing.
     
  28. FilippoG

    FilippoG

    Joined:
    Nov 18, 2019
    Posts:
    16
    Git it. Input events are not registered on dinamically sized TextMesh. It is only registered within the initial dimensions of mesh set in editor.
    If Text is resized by text content, nothing is registered on newly occupied space (no rollover, rollout or clicks, nothing).
    This is ridiculous.
    We could handle this sort of things in Flash in a breeze at the end of the '90s...
     
  29. Hobby-Game-Developer

    Hobby-Game-Developer

    Joined:
    Aug 27, 2017
    Posts:
    14
    Hey,

    i've created a method, to find any links in a dynamically created string inside a TextMeshProGUI Component. Not only https, also http, ftp, file, anything you search:

    Code (CSharp):
    1. using TMPro;
    2. using System.Linq;
    3. public string SearchLink(Notiz note)
    4.     {
    5.         string[] textSplitter = new string[0];
    6.         List<string> originalLinks = new();
    7.         List<string> links = new();
    8.         List<string> hyperlinkTypes = new() { "https://", "http://", "ftp://", "file://" };
    9.         string finalString = note.NOTE;
    10.         textSplitter = note.NOTE.Split(' ', '\n');
    11.         for (int i = 0; i < textSplitter.Length; i++)
    12.         {
    13.             int found = 0;
    14.             bool alreadyFound = false;
    15.             for (int ii = 0; ii < hyperlinkTypes.Count; ii++)
    16.             {
    17.                 found = 0;
    18.                 alreadyFound = false;
    19.                 for (int iii = 0; iii < hyperlinkTypes[ii].Length; iii++)
    20.                 {
    21.                     if (iii < textSplitter[i].Length)
    22.                     {
    23.                         if (hyperlinkTypes[ii][iii] == textSplitter[i][iii])
    24.                         {
    25.                             found++;
    26.                         }
    27.                         else
    28.                         {
    29.                             found = 0;
    30.                         }
    31.                     }
    32.                     if (found == hyperlinkTypes[ii].Length)
    33.                     {
    34.                         links.Add("<color=#0000ffff><link=" + '"' + textSplitter[i] + '"' + '>' + textSplitter[i] + "</link></color>");
    35.                         originalLinks.Add(textSplitter[i]);
    36.                         alreadyFound = true;
    37.                         break;
    38.                     }
    39.                     else if (found == 0)
    40.                     {
    41.                         break;
    42.                     }
    43.                 }
    44.                 if (alreadyFound)
    45.                 {
    46.                     break;
    47.                 }
    48.             }
    49.         }
    50.         for (int l = 0; l < links.Count; l++)
    51.         {
    52.             finalString = finalString.Replace(originalLinks[l], links[l]);
    53.         }
    54.         return finalString;
    55.     }
    56.  
    On another Noteobject with a script and the TextMeshProGUI Component, i use a codeline i found online. Most of you already know it. But it was only shown for static strings, not for any strings you enter:

    Code (CSharp):
    1.  
    2. using TMPro;
    3. using UnityEngine.EventSystems;
    4. public void OnPointerClick(PointerEventData eventData)
    5.     {
    6.         if(eventData.button == PointerEventData.InputButton.Left)
    7.         {
    8.             int index = TMP_TextUtilities.FindIntersectingLink(gameObject.GetComponent<TextMeshProUGUI>(),Input.mousePosition,null);
    9.             if(index > -1)
    10.             {
    11.                 Application.OpenURL(gameObject.GetComponent<TextMeshProUGUI>().textInfo.linkInfo[index].GetLinkID());
    12.             }
    13.         }
    14.     }
    If it doesn't work for you, it's probably because of missing namespaces or weird links or missing spaces!
    It works like on whatsapp for example. You have to have one space or new line between the link. Otherwise it will not detect the link.

    As for you: It does nothing, because your link has to be on quotes. Therefore you need to put those quotes on quotes, like this :
    Code (CSharp):
    1. string link = "<link=" + ' " ' + "http://example.com/" + ' " ' + ">this</link>to be happy."
    If nothing works (i encountered some problems), try to use ".com" or "/" at the end of the link. And if you try out your link system, try to use simple links, like https://google.com . If that doesn't work, you can be sure, that something's wrong.

    And here is an example of what i use with link-IDs and multiple links:

    Code (CSharp):
    1. //public class OpenLinks : MonoBehaviour, IPointerClickHandler {}
    2. //TMP_Text linkText = "<color=#00FFFF><link=\"ID0\">Homepage</link></color>\n\n" +
    3. //"<color=#00FFFF><link=\"ID1\">Contact</link></color>\n\n" +
    4. //"<color=#00FFFF><link=\"ID2\">Data protection</link></color>";
    5. //links = new string[] {"https://...com","https://...com","https://...com"};
    6. public void OnPointerClick(PointerEventData eventData)
    7. {
    8.     if (eventData.button == PointerEventData.InputButton.Left)
    9.     {
    10.         int index = TMP_TextUtilities.FindIntersectingLink(linkText, Input.mousePosition, null);
    11.         for(int i = 0; i < index+1; i++)
    12.         {
    13.             TMP_LinkInfo linkInfo = linkText.textInfo.linkInfo[i];
    14.             switch (linkInfo.GetLinkID())
    15.             {
    16.                 case "ID0":
    17.                     Application.OpenURL(links[0]);
    18.                     break;
    19.                 case "ID1":
    20.                     Application.OpenURL(links[1]);
    21.                     break;
    22.                 case "ID2":
    23.                     Application.OpenURL(links[2]);
    24.                     break;
    25.             }
    26.         }
    27.     }
    28. }
    Modify it as you please and have fun with it.

    Facebook: https://www.facebook.com/HobbyGameDeveloper/
    Google Play Store: https://play.google.com/store/apps/details?id=com.WSE.PersonalOrganizer&pcampaignid=web_share
    Homepage: https://wse-hobby-game-developer.jimdosite.com/
    Contact: https://wse-hobby-game-developer.jimdosite.com/kontakt/
     
    Last edited: Sep 12, 2023
  30. Yany

    Yany

    Joined:
    May 24, 2013
    Posts:
    96
  31. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,677
    In case someone needs it, I've written a small class that allows you to associate actions with the IDs.
    Just add this class to the same object as the TMP component and use the "Register" methods to add whatever shall happen upon (un)hover or click of any text between "<link=THE_ID_STRING>" and "</link>".

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using TMPro;
    4. using UnityEngine;
    5. using UnityEngine.EventSystems;
    6.  
    7. [RequireComponent(typeof(TextMeshProUGUI))]
    8. public class UniversalTMPclickableTextLinker : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
    9. {
    10.     private bool m_on_text = false;
    11.     private bool m_clicked = false;
    12.     private Camera m_optional_camera;
    13.  
    14.     private TextMeshProUGUI m_text;
    15.  
    16.     private Dictionary<int, Action> m_hover_actions = new Dictionary<int, Action>();
    17.     private Dictionary<int, Action> m_unhover_actions = new Dictionary<int, Action>();
    18.     private Dictionary<int, Action> m_click_actions = new Dictionary<int, Action>();
    19.  
    20.     int currently_hovered_id_hash = -1;
    21.  
    22.     public void RegisterHoverOnId(string id, Action hover_action, Action unhover_action)
    23.     {
    24.         m_hover_actions[TMP_TextUtilities.GetSimpleHashCode(id)] = hover_action;
    25.         m_unhover_actions[TMP_TextUtilities.GetSimpleHashCode(id)] = unhover_action;
    26.     }
    27.     public void RegisterClickOnId(string id, Action click_action) =>
    28.         m_click_actions[TMP_TextUtilities.GetSimpleHashCode(id)] = click_action;
    29.  
    30.     public void OnPointerEnter(PointerEventData eventData) => m_on_text = true;
    31.     public void OnPointerExit(PointerEventData eventData) => m_on_text = false;
    32.  
    33.     public void OnPointerClick(PointerEventData eventData) => m_clicked = true;
    34.  
    35.     void Start()
    36.     {
    37.         m_text = gameObject.GetComponent<TextMeshProUGUI>();
    38.         Canvas canvas = gameObject.GetComponentInParent<Canvas>();
    39.         m_optional_camera = (canvas.renderMode == RenderMode.ScreenSpaceOverlay) ? null : canvas.worldCamera;
    40.     }
    41.  
    42.     private void ExecuteId(Dictionary<int, Action> dict, int id)
    43.     {
    44.         if (dict.TryGetValue(id, out Action action))
    45.             action?.Invoke();
    46.     }
    47.  
    48.     void LateUpdate()
    49.     {
    50.         if (m_on_text)
    51.         {
    52.             int hovered_link_index = TMP_TextUtilities.FindIntersectingLink(m_text, Input.mousePosition, m_optional_camera);
    53.             if (hovered_link_index == -1)
    54.             {
    55.                 if (currently_hovered_id_hash != -1)
    56.                     ExecuteId(m_unhover_actions, currently_hovered_id_hash);
    57.                 currently_hovered_id_hash = -1;
    58.                 return;
    59.             }
    60.  
    61.             int hovered_link_hash = m_text.textInfo.linkInfo[hovered_link_index].hashCode;
    62.  
    63.             if (hovered_link_hash != currently_hovered_id_hash) // hovered hash has changed
    64.             {
    65.                 if (currently_hovered_id_hash != -1)
    66.                     ExecuteId(m_unhover_actions, currently_hovered_id_hash);
    67.                 ExecuteId(m_hover_actions, hovered_link_hash);
    68.                 currently_hovered_id_hash = hovered_link_hash;
    69.             }
    70.  
    71.             if (m_clicked)
    72.                 ExecuteId(m_click_actions, hovered_link_hash);
    73.             m_clicked = false;
    74.         }
    75.     }
    76.  
    77. }
    78.  
     
    Nit_Ram likes this.
  32. Nit_Ram

    Nit_Ram

    Joined:
    May 8, 2016
    Posts:
    27
    I overhauled @DragonCoder 's script (it had a bug where it didn't work when the link ID was an integer & added an option to deregister linkIDs). Was there a reason why you chose to work with hashes instead of the linkID strings @DragonCoder ? It seemed like an unneccessary extra-step.

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using TMPro;
    4. using UnityEngine;
    5. using UnityEngine.EventSystems;
    6.  
    7. //Text with link example: "Hello, <link=YOUR_ID>this</link> is a link."
    8. [RequireComponent(typeof(TextMeshProUGUI))]
    9. public class TMP_LinkHelper : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
    10. {
    11.     private bool _pointerIsOverText = false;
    12.     private bool _clicked = false;
    13.     private Camera _optionalCamera;
    14.  
    15.     private TextMeshProUGUI _text;
    16.  
    17.     private Dictionary<string/*linkID*/, Action> _hoverActions = new Dictionary<string, Action>();
    18.     private Dictionary<string/*linkID*/, Action> _unhoverActions = new Dictionary<string, Action>();
    19.     private Dictionary<string/*linkID*/, Action> _clickActions = new Dictionary<string, Action>();
    20.  
    21.     private string _currentlyHoveredLinkID = null;
    22.  
    23.     public void RegisterHoverOnId(string id, Action hoverAction, Action unhoverAction)
    24.     {
    25.         _hoverActions[id] = hoverAction;
    26.         _unhoverActions[id] = unhoverAction;
    27.     }
    28.  
    29.     public void RegisterClickOnId(string id, Action clickAction)
    30.     {
    31.         _clickActions[id] = clickAction;
    32.     }
    33.  
    34.     public void DeRegisterHoverOnId(string id)
    35.     {
    36.         _hoverActions.Remove(id);
    37.         _unhoverActions.Remove(id);
    38.     }
    39.  
    40.     public void DeRegisterClickOnId(string id)
    41.     {
    42.         _clickActions.Remove(id);
    43.     }
    44.  
    45.     public void OnPointerEnter(PointerEventData eventData) => _pointerIsOverText = true;
    46.     public void OnPointerExit(PointerEventData eventData) => _pointerIsOverText = false;
    47.  
    48.     public void OnPointerClick(PointerEventData eventData) => _clicked = true;
    49.  
    50.     private void Start()
    51.     {
    52.         _text = gameObject.GetComponent<TextMeshProUGUI>();
    53.         var canvas = gameObject.GetComponentInParent<Canvas>();
    54.         _optionalCamera = (canvas.renderMode == RenderMode.ScreenSpaceOverlay) ? null : canvas.worldCamera;
    55.     }
    56.  
    57.     private void ExecuteId(Dictionary<string, Action> dict, string linkID)
    58.     {
    59.         if (dict.TryGetValue(linkID, out Action action))
    60.             action?.Invoke();
    61.     }
    62.  
    63.     private void LateUpdate()
    64.     {
    65.         if (!_pointerIsOverText) return;
    66.      
    67.         int hoveredLinkIndex = TMP_TextUtilities.FindIntersectingLink(_text, Input.mousePosition, _optionalCamera);
    68.         string hoveredLinkID = hoveredLinkIndex == -1 ? null : _text.textInfo.linkInfo[hoveredLinkIndex].GetLinkID();
    69.      
    70.         //unhover actions
    71.         bool noLinkIsHovered = hoveredLinkID == null;
    72.         if (noLinkIsHovered)
    73.         {
    74.             if (_currentlyHoveredLinkID != null)
    75.                 ExecuteId(_unhoverActions, _currentlyHoveredLinkID);
    76.              
    77.             _currentlyHoveredLinkID = null;
    78.             return;
    79.         }
    80.      
    81.         //hovered has changed -> hover actions
    82.         bool hoveredLinkHasChanged = hoveredLinkID != _currentlyHoveredLinkID;
    83.         if (hoveredLinkHasChanged)
    84.         {
    85.             if (_currentlyHoveredLinkID != null)
    86.                 ExecuteId(_unhoverActions, _currentlyHoveredLinkID);
    87.              
    88.             ExecuteId(_hoverActions, hoveredLinkID);
    89.             _currentlyHoveredLinkID = hoveredLinkID;
    90.         }
    91.  
    92.         //clicked actions
    93.         if (_clicked)
    94.             ExecuteId(_clickActions, hoveredLinkID);
    95.          
    96.         _clicked = false;
    97.     }
    98. }
     
    DragonCoder likes this.
  33. devRHD

    devRHD

    Joined:
    Apr 17, 2020
    Posts:
    3
    Clearing things out from @Nit_Ram 's script using UnityEvents
    Code (CSharp):
    1. using TMPro;
    2.  
    3. public interface ITMP_LinkHandler
    4. {
    5.     TextMeshProUGUI TMP { get; }
    6.     TMP_LinkInfo HoveredLinkInfo { get; }
    7. }
    Code (CSharp):
    1. using TMPro;
    2. using UnityEngine;
    3. using UnityEngine.Events;
    4. using UnityEngine.EventSystems;
    5. //Text with link example: "Hello, <link=YOUR_ID>this</link> is a link."
    6. [RequireComponent(typeof(TextMeshProUGUI))]
    7. public class TMP_LinkHandler : MonoBehaviour, ITMP_LinkHandler, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
    8. {
    9.     public UnityEvent<ITMP_LinkHandler> OnLinkClick;
    10.     public UnityEvent<ITMP_LinkHandler> OnLinkHover;
    11.     public UnityEvent<ITMP_LinkHandler> OnLinkHoverIn;
    12.     public UnityEvent<ITMP_LinkHandler> OnLinkHoverOut;
    13.  
    14.     private Camera _optionalCamera;
    15.     private int _currentLinkIndex;
    16.     private bool _hovering;
    17.  
    18.     public TextMeshProUGUI TMP { get; private set; }
    19.     public TMP_LinkInfo HoveredLinkInfo { get; private set; }
    20.  
    21.     private void Start()
    22.     {
    23.         TMP = gameObject.GetComponent<TextMeshProUGUI>();
    24.         var canvas = gameObject.GetComponentInParent<Canvas>();
    25.         _optionalCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
    26.     }
    27.  
    28.     private void Update()
    29.     {
    30.         if (!_hovering) return;
    31.  
    32.         var hoveredLinkIndex = TMP_TextUtilities.FindIntersectingLink(TMP, Input.mousePosition, _optionalCamera);
    33.         var currentLinkInfo = hoveredLinkIndex == -1 ? default : TMP.textInfo.linkInfo[hoveredLinkIndex];
    34.    
    35.         // Hover In / Out
    36.         if (_currentLinkIndex != hoveredLinkIndex)
    37.         {
    38.             if (hoveredLinkIndex != -1) OnLinkHoverOut?.Invoke(this);
    39.             if (_currentLinkIndex != -1) OnLinkHoverIn?.Invoke(this);
    40.             _currentLinkIndex = hoveredLinkIndex;
    41.             HoveredLinkInfo = currentLinkInfo;
    42.         }
    43.    
    44.         // Hover
    45.         if (_currentLinkIndex != -1 && _currentLinkIndex == hoveredLinkIndex)
    46.         {
    47.             OnLinkHover?.Invoke(this);
    48.         }
    49.     }
    50.  
    51.     public void OnPointerClick(PointerEventData eventData)
    52.     {
    53.         // Click
    54.         if (_currentLinkIndex != -1) OnLinkClick?.Invoke(this);
    55.     }
    56.     public void OnPointerEnter(PointerEventData eventData) => _hovering = true;
    57.     public void OnPointerExit(PointerEventData eventData) => _hovering = false;
    58. }
    And with lazy manager script to subscribe events on all registered handlers
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TMP_LinkRegistrar : MonoBehaviour
    4. {
    5.     private TMP_LinkHandler _linkHandler;
    6.  
    7.     private void Awake()
    8.     {
    9.         _linkHandler = GetComponent<TMP_LinkHandler>();
    10.     }
    11.  
    12.     private void OnEnable()
    13.     {
    14.         TMP_LinkManager.Instance.RegisterHandler(_linkHandler);
    15.     }
    16.  
    17.     private void OnDisable()
    18.     {
    19.         TMP_LinkManager.Instance.UnregisterHandler(_linkHandler);
    20.     }
    21. }
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Events;
    3.  
    4. public class TMP_LinkManager : MonoBehaviour
    5. {
    6.     public UnityEvent<ITMP_LinkHandler> OnLinkHoverIn;
    7.     public UnityEvent<ITMP_LinkHandler> OnLinkHover;
    8.     public UnityEvent<ITMP_LinkHandler> OnLinkHoverOut;
    9.     public UnityEvent<ITMP_LinkHandler> OnLinkClick;
    10.  
    11.     public static TMP_LinkManager Instance { get; private set; }
    12.  
    13.     private void Awake()
    14.     {
    15.         if (Instance != null && Instance != this) Destroy(this.gameObject);
    16.         else Instance = this;
    17.     }
    18.  
    19.     public void RegisterHandler(TMP_LinkHandler linkHandler)
    20.     {
    21.         SubscribeEvents(linkHandler);
    22.     }
    23.  
    24.     public void UnregisterHandler(TMP_LinkHandler linkHandler)
    25.     {
    26.         UnsubscribeEvents(linkHandler);
    27.     }
    28.  
    29.     private void SubscribeEvents(TMP_LinkHandler linkHandler)
    30.     {
    31.         linkHandler.OnLinkClick.AddListener(handler => OnLinkClick.Invoke(handler));
    32.         linkHandler.OnLinkHover.AddListener(handler => OnLinkHover.Invoke(handler));
    33.         linkHandler.OnLinkHoverIn.AddListener(handler => OnLinkHoverIn.Invoke(handler));
    34.         linkHandler.OnLinkHoverOut.AddListener(handler => OnLinkHoverOut.Invoke(handler));
    35.     }
    36.  
    37.     private void UnsubscribeEvents(TMP_LinkHandler linkHandler)
    38.     {
    39.         linkHandler.OnLinkClick.RemoveListener(handler => OnLinkClick.Invoke(handler));
    40.         linkHandler.OnLinkHover.RemoveListener(handler => OnLinkHover.Invoke(handler));
    41.         linkHandler.OnLinkHoverIn.RemoveListener(handler => OnLinkHoverIn.Invoke(handler));
    42.         linkHandler.OnLinkHoverOut.RemoveListener(handler => OnLinkHoverOut.Invoke(handler));
    43.     }
    44. }
    Example usage
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class ExampleScript : MonoBehaviour
    4. {
    5.     // Assign this script to Handler's OnHover UnityEvent from inspector
    6.     public void DoSomethingOnHover(ITMP_LinkHandler linkHandler)
    7.     {
    8.         var linkInfo = linkHandler.HoveredLinkInfo;
    9.         var linkID = linkInfo.GetLinkID();
    10.  
    11.         switch (linkID)
    12.         {
    13.             case "id":
    14.                 Debug.Log("Hovering!");
    15.                 break;
    16.         }
    17.     }
    18.    
    19.     // Assign this script to Handler's OnClick UnityEvent from inspector
    20.     public void ChangeTextOnClick(ITMP_LinkHandler linkHandler)
    21.     {
    22.         var tmp = linkHandler.TMP;
    23.         var linkInfo = linkHandler.HoveredLinkInfo;
    24.         var linkID = linkInfo.GetLinkID();
    25.        
    26.         switch (linkID)
    27.         {
    28.             case "id1":
    29.                 tmp.text = "New Text!";
    30.                 break;
    31.             case "id2":
    32.                 tmp.text = tmp.text.Replace(linkInfo.GetLinkText(), "New text just for text of the link!");
    33.                 break;
    34.         }
    35.     }
    36. }
     
    Last edited: Mar 19, 2024