Search Unity

TextMesh Pro How to parse tmpro text that contains formatting tags and remove lines

Discussion in 'UGUI & TextMesh Pro' started by Munchy2007, Oct 15, 2018.

  1. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,735
    Hi, I'm trying to remove the first line from the start of a tmpro text element which may contain formatting tags, but I'm struggling to work out how to account for the tags when determining the start of the 2nd line, this short example illustrates the problem.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using TMPro;
    5.  
    6. public class TestTmpro : MonoBehaviour {
    7.  
    8.     void Start () {
    9.         var tmpText = GetComponent<TextMeshProUGUI>();
    10.  
    11.         // This works and the resulting text is 'Line two'
    12.         tmpText.text = "Hello World\nLine two";
    13.         tmpText.ForceMeshUpdate();
    14.         tmpText.text = tmpText.text.Substring(tmpText.textInfo.lineInfo[1].firstVisibleCharacterIndex);
    15.  
    16.         // This doesn't work proprly, the resulting text is:-
    17.         //      00>Hello World</color>\n<color=#0055FF>Line two</color>
    18.         // whereas I'd like it to be
    19.         //      <color=#0055FF>Line two</color>
    20.         tmpText.text = "<color=#FF5500>Hello World</color>\n<color=#0055FF>Line two</color>";
    21.         tmpText.ForceMeshUpdate();
    22.         tmpText.text = tmpText.text.Substring(tmpText.textInfo.lineInfo[1].firstVisibleCharacterIndex);
    23.     }
    24. }
    It works perfectly okay when there are no formatting tags, but otherwise not. Is there a better way to approach this. Please note the length and/or existence of the formatting tags isn't known in advance.

    Many thanks.
     
    MilenaRocha likes this.
  2. Julien_at_work

    Julien_at_work

    Joined:
    Aug 9, 2018
    Posts:
    35
    If the formatting tags are not wrapped around the whole string (only around each line), you may be able to split them by the newline character.
     
  3. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,735
    Unfortunately the tags can be anywhere in the line, not necessarily at either end.

    The problem is that the tmpro.textinfo that I use to determine which lines to display only provides information about the parsed text, there's no (as far as I can tell) way to retrieve the unparsed text that represents what you can see as the line in the text element. This makes manipulating the contents of the unparsed string quite difficult and if you want to manually remove lines from the text element, how else is there to do it?

    At the moment I think that, I could get the LineInfo.firstCharIndex of the first line I want to display and then work my way backwards until I find a newline char and remove everything including and before that. Which is pretty much as Julien_at_work suggests.

    Unless anyone can suggest a more elegant solution, it looks like that's the approach I'll have to take.
     
    Last edited: Oct 15, 2018
    MilenaRocha likes this.
  4. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    In the CharacterInfo the index references the character in the original string. So if you iterate over the character info index vs. the string you can effectively figure out which are displayable characters vs. rich text.

    You can also take a look at the TMP_InputField which has to do this and includes a few utility functions to handle this.
     
    Munchy2007 likes this.
  5. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,735
  6. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,735
    Where can I find the TMP_InputField script, I only have dlls in my tmpro version?
     
  7. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    I thought you were already using the version of TMP delivered via the Package Manager which includes source code.

    I guess since you only want to look at the code, just install Unity 2018.2 and create an empty project so you can browse the TMP stuff.
     
    Munchy2007 likes this.
  8. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,735
    After messing around with this on and off for quite a while, I came to the conclusion that there was no reliable way to find the start of an unparsed line, and so include its formatting tags, by using the parsed character index returned by textInfo.lineInfo.firstCharacterIndex.

    But in a moment of clarity I realised that I could just temporarily turn of RichText formatting before I remove the line and turn it back on again. This works perfectly for what I want. I'm not sure how performant switching between no formatting and RTF formatting is, but for my purposes it doesn't cause any problems.

    In case anyone else finds it useful here's how I ended up implementing it.

    Code (CSharp):
    1.  
    2.     [SerializeField]
    3.     TextMeshProUGUI displayText;
    4.     [SerializeField]
    5.     int maxLines = 6;
    6.  
    7.     void RemoveExcessLinesFromStart()
    8.     {
    9.         displayText.richText = false;
    10.         displayText.ForceMeshUpdate();
    11.         var lineCount = displayText.textInfo.lineCount;
    12.         if (lineCount > maxLines)
    13.         {
    14.             var line = lineCount - maxLines;
    15.             displayText.text = displayText.text.Substring(displayText.textInfo.lineInfo[line].firstCharacterIndex);
    16.         }
    17.         displayText.richText = true;
    18.     }
    I'd still be interested to find out if there's a better way to do this.
     
    Novack and mikapote like this.
  9. AliAnkara

    AliAnkara

    Joined:
    Dec 17, 2017
    Posts:
    7
    Hello Munchy2007,
    Any progress on this?
     
  10. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,735
    Hi @AliAnkara,

    sadly not entirely. In fact, I found that even the above solution, failed in some circumstances resulting in a loss of formatting at the start of the string.

    My goal was to produce text that scrolled from the bottom upwards and be truncated at the top to fit within a window.

    In the end, I went with this code to manipulate the text
    Code (CSharp):
    1. [SerializeField] TextMeshProUGUI displayText;
    2. [SerializeField] int maxLines = 20;
    3.  
    4. void RemoveExcessLinesFromStart()
    5.     {
    6.         displayText.ForceMeshUpdate();
    7.         var lineCount = displayText.textInfo.lineCount;
    8.  
    9.         if (lineCount >= maxLines)
    10.         {
    11.             displayText.alignment = TextAlignmentOptions.BottomLeft;
    12.         }
    13.  
    14.         if (lineCount > maxLines + 10)
    15.         {
    16.             var line = lineCount - maxLines - 10;
    17.             displayText.text = displayText.text.Substring(displayText.textInfo.lineInfo[line].firstCharacterIndex);
    18.         }
    19.     }
    and I made sure that I left a few more lines than I wanted to display (+10 in this example) and then used masking to hide the extra (and possibly incorrectly formatted) lines.

    There again, not ideal and if there is a better way to do it I couldn't figure it out; but at least this worked.
     
    Last edited: Jan 6, 2020
  11. visca_c

    visca_c

    Joined:
    Apr 7, 2014
    Posts:
    30
    I recently ran into similar problem, where I needed to remove exccess line of text, but keep rich text tags intact. In my case, I only want to keep two lines of text, anymore lines would be removed down to 2. Here's what I came up with:
    Code (CSharp):
    1. private void TrimTextToFitLineLineLimit()
    2.     {
    3.         var textComponent = inputField.textComponent;
    4.         var textInfo = textComponent.textInfo;
    5.         int lineCount = textComponent.textInfo.lineCount;
    6.         while (lineCount > 2)
    7.         {
    8.            //-2 because last index is zero-width white space
    9.            int lastParsedCharIndex = textInfo.characterInfo[textInfo.characterCount - 2].index;
    10.            inputField.text = inputField.text.Remove(lastParsedCharIndex, 1);
    11.            //Hacky way to set string position dirty
    12.            //without this line textInfo does not update properly.
    13.            inputField.caretPosition = inputField.caretPosition;
    14.            lineCount = inputField.textComponent.textInfo.lineCount;
    15.         }
    16.     }
     
    Last edited: Dec 20, 2021