Create hyperlinks in text with Unity UI's default text component

Hello all,

I understand such a feature is available with TextMeshPro, but I want to implement it for Unity UI’s default text component for a project.

Now, to implement this, first thing I need is to identify the word that was clicked/touched in the pointer click event on the text component with the PointerEventData.position that gives the screen position where it was touched/clicked. Once I have the word, I’ll verify if its a link using below regex:

Regex urlregex = new Regex(@"(^|[\n ])(?<url>(www|ftp)\.[^ ,""\s<]*)", RegexOptions.IgnoreCase);
Regex httpurlregex = new Regex(@"(^|[\n ])(?<url>(http://www\.|http://|https://)[^ ,""\s<]*)", RegexOptions.IgnoreCase);

So if it is a link then I will open the browser with that link which was clicked on. That’s it.

Now, I am looking for a way to find out the word from the whole text that was clicked on the pointer click event on the Text component. Could anyone please guide me on how to achieve this?

Also, if somebody has a better approach to implement this, please share it here.

Look forward to some help regarding this. Thanks in advance.

Out of curiosity why not use TMP?

I have 2 project-specific reasons,

  1. When having support for more than 10 languages in a project, TMP takes too much time for loading text from the fallback fonts than the default text component. In this case, the default text component loads fonts almost instantaneously even if there are 20 fallback fonts assigned.
  2. The quality of basic text rendering of the default UI has almost become at par with TMP, at least for my eyes.

P.S. - I just want to implement an API similar to ‘FindIntersectingLink’ from TMP_TextUtilities for the default Text component.

I would like to get a better understanding of how you have these font assets and fallback setup just to make sure it is optimum and to see if there might be ways to make additional improvements to achieve the performance you seek.

UI.Text uses the legacy text rendering system which is on maintenance. As such there has been no change there so the text rendering should be as it has always been.

TMP by contrast is on active development along with the new TextCore which is an improved text parsing and layout engine that will eventually replace TMP’s internal one. TextCore will also be used for text layout and rendering in UIElements. Basically I need to make sure TMP and this engine provides the level of functionality and performance our users need.

P.S. UI.Text will be around for a while so depending on the state of the project, it may not make sense to switch but the forward direction is TMP / TextCore.

For reference, you can imagine the setup of a comment board, where each comment can have its own language glyphs to be rendered in a scroll view in a mobile app.

Now, to give a picture of the test setup that I tried, I took a text of 5-6 normal size sentences put together in a paragraph. Then, I translated them to the 10 languages which I wanted the app to support and then set each to a list item in a script. Now, on runtime, I just instantiated text prefabs to a simple vertical scroll view and set each text prefab to one language paragraph, thus giving me 10 text prefabs in the view each with a specific language text. Using fallback fonts, I set up 9 fonts of the 9 languages as the fallback fonts of the English font.

Now, to actually identify the load time difference, I put a button on the screen and then fill the scroll view on the click of the button.

With TMP, the load time was significant in this setup, while the default text component loaded it quite quickly without much of a visual difference in the text rendering.

Now, I understand TMP being under active development, but here the prompt load of the glyphs by the default text component was quite amazing, just like an HTML website. For me its little hard to believe UI.Text is the same as before, cause I had an epiphany when I just put UI.Text and TMP side by side in Unity 2019.2.16 and set 62 reference text each with font size from 10 to 72 to find that for some texts in this range(20 - 72) hardly had any difference. Only in case of lower font sizes (<20) the UI.Text had some blurs, but rest both had nearly the same visual (at least for my eyes). I just thought the guys from Unity would have taken notes from you and improved the Text rendering to make it more usable.

But jokes apart, I am not saying UI.Text has become more usable than TMP, actually, it is still far primitive, but I am not sure how they load different language glyphs so fast but it is quite impressive.

P.S. - I forgot to mention, all the tests were done on an Android device - Moto X4

Now that I have quenched the thirst of your curiosity, could you please help me with creating something similar to ‘FindIntersectingLink’ from TMP_TextUtilities for the default Text component? :slight_smile:

One of the likely reasons for the performance different is rendering of bitmap glyphs in an atlas texture vs. signed distance field which is significantly more expensive to do dynamically. Using a mixture of static and dynamic font assets can significantly mitigate this additional overhead while making it possible to support the languages needed.

Note that TMP does include bitmap rendering modes but there are other trade offs in using bitmap rendering. With Signed Distance Field, we only need to raster each glyph once in the atlas texture to make it possible to render this glyph at any point size / scale / resolution. By contrast with bitmap you need to raster each glyph for every point size used resulting in multiple copies of each glyph in the atlas texture which in turn results in altas textures quickly growing to 4096 X 4096 which then impacts runtime rendering performance on those lower end devices.

With bitmap rendering, you also lose the ability to use material presets to dynamically style the text to add outline, shadows, etc. This can be done with bitmap using the Outline component or Shadow component with UI.Text but those have 5x to 10x runtime performance degradation over SDF with outline and shadow. So you trade one time loading performance differences for additional and persistent runtime performance overhead.

There are additional differences between SDF and Bitmap rendering but those are the primary ones that you should keep in mind as you move forward.

It would be great if you could submit a bug report that includes the project and scene used for testing. This would enable me to confirm if in fact the differences are related to SDF rasterization vs. Bitmap or something else. It would also enable me to review how you have these font asset and fallback setup to make sure it is optimum (again looking for improving the loading time). Lastly to look for other improvements to further reduce loading times overall.

I wish I had time but my hands are full making sure TMP and the new TextCore will provide the functionality and performance that you need as well as of the growing needs of our users.

How about this, I’ll file a bug report with my test setup to help you identify the performance gap (I’ll use my time to do this cause I want TMP to improve in this area), while in return, you can at least point me to an API that I could use to identify the word in UI.Text at given a screen position and then I can work my way up. I guess pointing me to such an API or something related shouldn’t take up much of your time.

I will confirm this tomorrow but unlike TMP where all the required information is contained in the TMP_TextInfo, that is not the case with the Legacy text generator. For instance, word navigation is possible in the UI Input Field but this logic is implemented in the InputField.cs file which is part of the UI package which should be visible in the project tab in 2019.3 as seen below.

I’ll follow up tomorrow on this.

Hi @Stephan_B ,

I saw the script InputField.cs and found GetCharacterIndexFromPosition(Vector2 pos) which returns character index from a vector2 position in local space. So I took the screen position in OnPointerDown and obtained the local space position using RectTransformUtility.ScreenPointToLocalPointInRectangle in Text component’s RectTransform. So now I have the character that was clicked on in the Text Component. Cool!

Now, I just put up a simple logic to identify the word using this character index from the Substring method and Voila, I have the word that was clicked on in the Text component printed in the logs.

I’ll put together an API and share it here for everybody’s reference, but before that please share your views on this implementation. Is there a better/faster way to do this?

I was able to put together a utility script for detecting hyperlinks in UI.Text, all thanks to @Stephan_B who pointed me in the right direction. Thanks, Stephan!

I am laying it out here for people looking to do this with UI.Text.

using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.UI;

public class UITextUtilities : MonoBehaviour
{
    /// <summary>
    /// Given an input string returns if it is a URL
    /// </summary>
    /// <param name="str">Input string.</param>
    /// <returns>Whether or not input string is a URL.</returns>
    public static bool hasLinkText(string str)
    {
        string strWOHTMLTags = RemoveHtmlLikeTags(str);
        string URL_WO_HTTP_PATTERN = @"(^|[\n ])(?<url>(www|ftp)\.[^ ,""\s<]*)";
        string URL_WITH_HTTP_PATTERN = @"(^|[\n ])(?<url>(http://www\.|http://|https://)[^ ,""\s<]*)";

        Regex urlregex = new Regex(URL_WO_HTTP_PATTERN, RegexOptions.IgnoreCase);
        Regex httpurlregex = new Regex(URL_WITH_HTTP_PATTERN, RegexOptions.IgnoreCase);

        return urlregex.IsMatch(strWOHTMLTags) || httpurlregex.IsMatch(strWOHTMLTags);
    }

    /// <summary>
    /// Given an input string returns a string without any HTML like tags
    /// </summary>
    /// <param name="str">Input string.</param>
    /// <returns>String without any HTML like tags.</returns>
    public static string RemoveHtmlLikeTags(string str)
    {
        string HTML_LIKE_TAGS_PATTERN = @"<[^>]+>| ";
        return Regex.Replace(str, HTML_LIKE_TAGS_PATTERN, "").Trim();
    }

    /// <summary>
    /// Given an input Text component, clicked position in local space and UI camera returns the word in the value of the Text component that was got clicked
    /// </summary>
    /// <param name="textComp">Text component.</param>
    /// <param name="position">Click position.</param>
    /// <param name="camera">UI camera.</param>
    /// <returns>Word in the value of the Text component that was clicked.</returns>
    public static string FindIntersectingWord(Text textComp, Vector3 position, Camera camera)
    {
        Vector2 localPosition;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(textComp.GetComponent<RectTransform>(),
            position, camera, out localPosition);
        int characterIndex = GetCharacterIndexFromPosition(textComp, localPosition);
        if (!string.IsNullOrWhiteSpace(GetCharFromIndex(textComp, characterIndex)))
        {
            return GetWordFromCharIndex(textComp.text, characterIndex);
        }
        return "";
    }

    private static string GetCharFromIndex(Text textComp, int index)
    {
        var tempArr = textComp.text.ToCharArray();
        if (index != -1 && index < tempArr.Length)
        {
            return tempArr[index] + "";
        }
        return "";
    }

    private static int GetCharacterIndexFromPosition(Text textComp, Vector2 pos)
    {
        TextGenerator gen = textComp.cachedTextGenerator;

        if (gen.lineCount == 0)
            return 0;

        int line = GetUnclampedCharacterLineFromPosition(textComp, pos, gen);
        if (line < 0)
            return 0;
        if (line >= gen.lineCount)
            return gen.characterCountVisible;

        int startCharIndex = gen.lines[line].startCharIdx;
        int endCharIndex = GetLineEndPosition(gen, line);

        for (int i = startCharIndex; i < endCharIndex; i++)
        {
            if (i >= gen.characterCountVisible)
                break;

            UICharInfo charInfo = gen.characters[i];
            Vector2 charPos = charInfo.cursorPos / textComp.pixelsPerUnit;

            float distToCharStart = pos.x - charPos.x;
            float distToCharEnd = charPos.x + (charInfo.charWidth / textComp.pixelsPerUnit) - pos.x;
            if (distToCharStart < distToCharEnd)
                return i;
        }
        return endCharIndex;
    }

    private static int GetUnclampedCharacterLineFromPosition(Text textComp, Vector2 pos, TextGenerator generator)
    {
        // transform y to local scale
        float y = pos.y * textComp.pixelsPerUnit;
        float lastBottomY = 0.0f;

        for (int i = 0; i < generator.lineCount; ++i)
        {
            float topY = generator.lines[i].topY;
            float bottomY = topY - generator.lines[i].height;

            // pos is somewhere in the leading above this line
            if (y > topY)
            {
                // determine which line we're closer to
                float leading = topY - lastBottomY;
                if (y > topY - 0.5f * leading)
                    return i - 1;
                else
                    return i;
            }

            if (y > bottomY)
                return i;

            lastBottomY = bottomY;
        }

        // Position is after last line.
        return generator.lineCount;
    }

    private static int GetLineStartPosition(TextGenerator gen, int line)
    {
        line = Mathf.Clamp(line, 0, gen.lines.Count - 1);
        return gen.lines[line].startCharIdx;
    }

    private static int GetLineEndPosition(TextGenerator gen, int line)
    {
        line = Mathf.Max(line, 0);
        if (line + 1 < gen.lines.Count)
            return gen.lines[line + 1].startCharIdx - 1;
        return gen.characterCountVisible;
    }

    private static string GetWordFromCharIndex(string str, int characterIndex)
    {
        string firstPartOfStr = str.Substring(0, characterIndex);
        string secondPartOfStr = str.Substring(characterIndex);

        string firstPart = firstPartOfStr;
        //Check for last index of space in first part of str and get text till that
        int lastIndexOfSpace = firstPartOfStr.LastIndexOf(' ');
        if (lastIndexOfSpace != -1)
        {
            firstPart = firstPartOfStr.Substring(lastIndexOfSpace);
        }

        string secondPart = secondPartOfStr;
        //Check for first index of space in second part of str and get text till that
        int firstIndexOfSpace = secondPartOfStr.IndexOf(' ');
        if (firstIndexOfSpace != -1)
        {
            secondPart = secondPartOfStr.Substring(0, firstIndexOfSpace);
        }

        //Check for new lines in first and second parts of the word and trim it
        int IndexOfNewLineInFirstPart = firstPart.IndexOf('\n');
        if (IndexOfNewLineInFirstPart != -1)
        {
            firstPart = firstPart.Substring(firstPart.IndexOf('\n'));
        }
        int IndexOfNewLineInSecondPart = secondPart.IndexOf('\n');
        if (IndexOfNewLineInSecondPart != -1)
        {
            secondPart = secondPart.Substring(0, secondPart.IndexOf('\n'));
        }
        return firstPart.Replace("\n", "").Replace("\r", "") + secondPart.Replace("\n", "").Replace("\r", "");
    }
}

Put this utility script in your project and then add an EventTrigger on the Text component where you want to detect the click event on a hyperlink inside the text value. Then in OnPointerClick, you can have something like this:

public void OnPointerClick(PointerEventData eventData)
    {
        string clickedWord = UITextUtilities.FindIntersectingWord(textComp, eventData.position, UICamera);
        if (!string.IsNullOrEmpty(clickedWord) && UITextUtilities.hasLinkText(clickedWord))
        {
            string actualUrl = UITextUtilities.RemoveHtmlLikeTags(clickedWord);
            Debug.Log("Opening link: " + actualUrl);
            Application.OpenURL(actualUrl);
        }
    }

*Please Note:
The UICamera mentioned above is an Orthographic Camera rendering the corresponding UI Canvas of the Text component. I have not tested it for other scenarios. Feel free to modify it as per your needs!

3 Likes

Hi! Sorry to bother you, but i kinda can’t make it work. Can you make several screenshots as a simple tutorial, please?

Well, sure. Here it goes.

Step 1: Add the UITextUtilities.cs script in your project.
P.S. I have updated the script to fix some bugs I came across while creating this tutorial.

Step 2: Add Unity UI’s text component to your scene. This should automatically add a Canvas object and an EventSystem object, but in case it doesn’t add it as shown below:

Step 3: Change the Main Camera in the scene to Orthographic. It is necessary to render the canvas in camera space, in this case, so that on pointer click, the screen position can be directly translated into position on the canvas component. Alternatively, you can create a new orthographic UI Camera in your scene just for rendering UI Canvas.

Step 4: Update the render mode of the Canvas object to ‘Screen Space - Camera’ and assign the Main Camera (Orthographic or UI camera) to the Render Camera

Step 5: Add a new script and call it TextClickContrller.cs. This script simply holds the OnPointerClick event handler and checks if the clicked word is a link, then opens it using Application.OpenURL() API.

Step 6: Now, add this TextClickController.cs to a Unity UI Text component where you want to detect and allow a link click.

Step 7: Enter a dummy text with a link like ‘Some dummy text www.vuexr.com and another dummy text’ in the text component and hit play.

Step 8: In play mode, click on the link in the text component in the game view, and voila, it should open the link in a browser!

Sorry, could not upload screenshots besides the steps, I am only allowed 5 files here. So I have attached a zip with screenshots marked with the corresponding step number and also the scripts that I have used in this tutorial for reference.

Cheers!

6304863–698058–Unity Text Link Click Tutorial.zip (882 KB)

2 Likes

Thank you very much! I really needed this!
EDIT: Yep, it works now. Thanks again! Pity i couldn’t make it work with Canvas “Screen Space - Overlay”.

Thank you sooooo much for this tutorial and files. I’ve been trying for weeks to do this. You are a legend! I have one small question though and i’m wondering if you would know how to do this.

I would like to use hyperlinks within text. So rather than the text saying “www.youtube.com” i would like the text to just say “CLICK HERE” and then it will open “www.youtube.com”. Is that possible? Thanks

P.S - How can i open the links in a new tab/window?

Well, Unity UI’s Text component does not support much of the RichText markups as what TextMeshPro does (Here is a reference of the supported ones). So having something like an anchor tag in HTML won’t be possible with the Unity UI’s default Text component as of now. Also, as @Stephan_B mentioned, UI.Text is not under active development. So such a feature might not come in the future as well.

You can use link tag of TextMeshPro for this or if you want to stick to Unity’s native UI, then you can simply use a button with the text “Click Here”, with the button graphic texture set to none and open the required url on button click.

P.S. if you want to open links in a new tab/window, you can use Application.OpenURL(url).

In the next preview release series of the TMP package which will be version 1.6.x for Unity 2018.4, version 2.2.x for Unity 2019.4 and version 3.2.x for Unity 202x or newer, I am planning on adding a “href” attribute to the link tag.

I am also planning on adding an and tag which will be very similar to the tag minus the ID part. This new tag will also include an “href” attribute.

I am looking to make the use of these tags and setting up potential interactions with them more flexible and intuitive.

2 Likes

@Stephan_B I also need it , Any idea when href is coming?

This is something I am planning to add to the next set of preview releases that I am currently working on.

1 Like

Confirming this still works in 2022, and it took me less than 10 minutes to pop it in to my app (I’m slow). Beautiful solution sir!