Search Unity

TextMesh Pro Get the actual width of the visible text

Discussion in 'UGUI & TextMesh Pro' started by Trivium_Dev, Mar 12, 2018.

  1. Trivium_Dev

    Trivium_Dev

    Joined:
    Aug 1, 2017
    Posts:
    78
    Is there a way to get the actual width of the visible text when using "auto size" and "wrapping & overflow" are set to "Disabled" and "Truncate"? I need to manually place a cursor at the end of the text within the TextMeshProUGUI field, and auto-size the font size down if the text gets too long. Currently using
    LayoutUtility.GetPreferredWidth(this.myTMPUGUIObject.rectTransform) and that works well until the text gets too long and the TMProUGUI starts auto-sizing the font size to make it smaller.

    I feel like this should be relatively easy and I'm just missing the functionality to get the width of the text.

    Thanks!
     
  2. Trivium_Dev

    Trivium_Dev

    Joined:
    Aug 1, 2017
    Posts:
    78
    Figured it out - this.myTmpGuiObject.GetRenderedValues(true) gets the size of the visible text regardless of auto-sizing and truncation. However it doesn't include spaces that are at the end of the string, so I have to manually add some width to account for spaces if they are at the end of the string.
     
    shieldgenerator7 likes this.
  3. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Not sure how I missed your initial post but another option is to get the TMP_Text.textBounds which should account for spaces.

    Depending on when you check this property, you might have to call ForceMeshUpdate() just after setting the tex and if the text object hasn’t been regenerated yet.

    You can also use the textInfo.lineInfo[lineIndex] which has additional data per line. Again this data gets populated after the text object has been processed / regenerated. This is again when you would use ForceMeshUpdate() if you are looking to use this data just after setting some of the text properties.

    See if that works for you?
     
  4. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Also forgot to mention that you can also look at the TMP_Text.textInfo.characterInfo[Index] which contains information about the characters which includes their positions and their origin and advance.
     
    mandisaw likes this.
  5. Trivium_Dev

    Trivium_Dev

    Joined:
    Aug 1, 2017
    Posts:
    78
    Hmmm, none of those really give me what I need from what I can tell.

    TMP_Text.textBounds doesn't include spaces at the end of the string, so it's the same as the GetRenderedValues.

    textInfo.lineInfo[lineIndex].width just returns the width of the RectTransform.
    TMP_Text.textInfo.characterInfo[<lastIndexValue>] doesn't seem to have anything about where the character is. There is a .bottomRight but it returns a negative number.
     
  6. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    The lineInfo contains the last visible or last character index. Then using this information, you can access that character using characterInfo[index] and get the origin or advance value. Origin is the same as the previous character's advance and where the character is drawn from based on the x and y offsets. The advance is where the next character's origin should be place. These are used to position the caret when moving through text.
     
  7. Trivium_Dev

    Trivium_Dev

    Joined:
    Aug 1, 2017
    Posts:
    78
    I must be doing something wrong.

    this.text.textInfo.lineInfo[0].lastVisibleCharacterIndex].bottomRight and this.text.textInfo.characterInfo[this.text.textInfo.lineInfo[0].lastVisibleCharacterIndex].xAdvance

    Both don't update when I add spaces to the end of the string. If I had 10 spaces the values don't change, then once I add a non-space it updates to a new one. I'm calling this in an Update function as I don't have any other way to know when the value in the text field has changed. I do see that the "TextMeshPro - InputField" works correctly with spaces, however I can't use that as I'm using an on-screen keyboard so the cursor keeps disappearing when I click keys.
     
  8. Trivium_Dev

    Trivium_Dev

    Joined:
    Aug 1, 2017
    Posts:
    78
    this.text.textInfo.lineInfo[0].lastCharacterIndex].bottomRight and this.text.textInfo.characterInfo[this.text.textInfo.lineInfo[0].lastCharacterIndex].xAdvance

    actually does work... however once the text starts truncating then it's not exactly what I'm looking for, however it's not terrible. I can truncate the text manually (not let them enter more characters after it's hit the cap).

    Thanks for your help!
     
  9. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Using vertices coordinates like bottomRight and whatnot is not ideal as these can be affected by Extra padding. Whereas using origin, advance, ascender, descender is more accurate for things like caret positioning.

    In addition, you can also query / use lineInfo[lineIndex].ascender and descender to determine the top and bottom of the lines as this factors in the affect is <size> and <font> tags or anything that can result in varying ascenders and descenders for individual characters whereas the line ascender and decender take the whole line into consideration.

    BTW: Just to help you visualize some of this stuff, add the TMP_TextInfoDebugTool.cs script to your text object. This will allow you to visualize this information. Looking at that script will also enable you to get more familiar with the content of the textInfo and all substructures.
     
  10. Trivium_Dev

    Trivium_Dev

    Joined:
    Aug 1, 2017
    Posts:
    78
    I also had to check to see if this.text.textInfo.lineCount was 0 because if it is then this.text.textInfo.characterInfo[this.text.textInfo.lineInfo[0].lastCharacterIndex].xAdvance returns whatever it last returned. It's actually interesting that this.text.textInfo.lineInfo[0] doesn't throw an IndexOutOfBounds error when there are no lines.
     
  11. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    See my BTW above.

    In order to minimize allocations / GC, these array sizes do not match exactly the content of the text. So you have to rely on the textInfo.xx like characterCount, lineCount, etc.

    Also keep in mind that most of this data is not updated until the text object is generated. When you do need to this data to be updated right away after changing the text, you can use ForceMeshUpdate() to do that.
     
  12. blindhorstm

    blindhorstm

    Joined:
    Dec 11, 2018
    Posts:
    2
    Super late reply, but I was googling how to do that and found this post.
    Code (CSharp):
    1. this.myTmpGuiObject.GetRenderedValues(true)
    "However it doesn't include spaces that are at the end of the string"

    Thats because if you want to include the spaces then you should mark the bool as false. Look at the method definition
    Code (CSharp):
    1. public Vector2 GetRenderedValues(bool onlyVisibleCharacters);
    "onlyVisibleCharacters"

    Better late than never.

    Have a great day
     
  13. AlejUb

    AlejUb

    Joined:
    Mar 11, 2019
    Posts:
    25
    I have been trying to send the 'center' information of a mesh to a vertex shader using extra vertex attributes.
    Querying charInfo (bottomLeft + topRight) * 0.5f and send that to all four vertices as an extra UV channel.
    However for some reason I have not been able to get the center, it's always offset. Any idea of how to go about it?
    Thanks in advance.
     
  14. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Some of the values pass to the shader will get modified by Unity when batching occurs. Some of the values also get normalized as well. This is an issue when trying to pass custom data in vertex attributes like Normal, Tangent, etc.
     
    AlejUb likes this.
  15. AlejUb

    AlejUb

    Joined:
    Mar 11, 2019
    Posts:
    25
    It's definitely not clear for me and can't seem to understand how and what info gets sent at the mesh generation level.
    For example, without using shaders just to understand a bit:
    Code (CSharp):
    1.  
    2. // On a loop for i = [0, characterCount);
    3. var charInfo = textInfo.characterInfo[i];
    4. var center = (charInfo.bottomLeft + charInfo.topRight) * 0.5f;
    5. vertices[vertexIndex + 0] -= center;
    6. vertices[vertexIndex + 1] -= center;
    7. vertices[vertexIndex + 2] -= center;
    8. vertices[vertexIndex + 3] -= center;
    9.  
    It outputs this, all letters get collapsed to the center of the UI anchor point (as if it had gotten the center of the whole line), not exactly what would be expected from substracting 'the center of a char' to all the meshes.
    upload_2019-11-11_19-27-24.png

    Now, if done on shader it outputs the same thing, so at least there seems to be consistency between what's sent to the shader and what's done directly on the mesh attributes.

    As an interesting extra behaviour, changing the anchor point to any of the RectTransform corners, it will collapse all letters on that corner (instead on the center like on the image above).

    This is taking longer than what I thought. Will dig deeper for a little bit before calling it a failed attempt.

    Thanks a lot for the hints, greatly appreciated.

    FIX: By sending the 'delta' value directly to the shader for each vertex it will survive all sorts of batching as long as it doesn't apply scale or rotation operations.
    That way letters can get collapsed, animated, etc on shaders (by using also charIndex as an attribute).
     
    Last edited: Nov 12, 2019
  16. patSilva

    patSilva

    Joined:
    Mar 25, 2019
    Posts:
    27
    Hello,

    I was having no problems with using Text Bounds except for today. All of a sudden literally out of nowhere the Text Bounds Size variable is giving me what looks like max negative values.

    upload_2021-6-4_11-21-56.png

    Just for a little more context. I was writing a script to organize the text after they get updated. And all of a sudden after playing with the algorithm TMP decided to send me this garbage

    upload_2021-6-4_11-23-46.png

    does anyone have any idea what is happening here?

    EDIT: Nevermind, I am a moron and was doing this while the gameObject that had these scripts was inactive. Apparently they need to be active in order for TextBounds to work appropriately!
     
    Last edited: Jun 4, 2021
    AndersonMarquess and RAW_MUSASHI like this.
  17. MM47

    MM47

    Joined:
    Sep 7, 2012
    Posts:
    25
    Do ForceMeshUpdate(); before getting the bounds.
     
  18. BlakeSchreurs

    BlakeSchreurs

    Joined:
    Aug 10, 2016
    Posts:
    51
    Just tested it with a ForceMeshUpdate(), no difference in behavior.
     
    sergiusz308 likes this.
  19. sergiusz308

    sergiusz308

    Joined:
    Aug 23, 2016
    Posts:
    235
    Don't know how about spaces, but this does not work, at least in Unity 2021.2.2f1 - it returns some default value (29 something in my case) all the time. ForceMeshUpdate does nothing here.

    What worked however is using preferredWidth property of the TextMeshProUGUI object.
     
  20. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    ForceMeshUpdate() should work as expected. Make sure that if you are doing so on disabled objects that you set the additional parameters of the function properly.

    The GetPreferredValues() function should also work as well.

    If you are still not getting the expected results, please provide some example script / scene that would enable me to verify this and provide you with additional feedback / insight.
     
    PrimalCoder likes this.
  21. sergiusz308

    sergiusz308

    Joined:
    Aug 23, 2016
    Posts:
    235
    @Stephan_B Thanks for getting back to this.

    TextMeshPro package v: 3.0.6

    My setup: I got a TextMeshProUGUI asset sitting in the hierarchy and the script which updates its text:

    Code (CSharp):
    1.  
    2. this.Label.SetText(text);
    3. this.Label.ForceMeshUpdate(true, true);
    4.  
    Next, in the same class I have this method:

    Code (CSharp):
    1. private float GetLabelWidth()
    2. {
    3.     if (this.Label != null)
    4.     {
    5.         return this.Label.preferredWidth;
    6.         //return this.Label.textBounds.size.x;
    7.     }
    8.     return -1f;
    9. }
    So first I call SetText(), next, after one frame delay (it's a coroutine) I call GetLabelWidth()

    commented part (about getting size.x) is always returns the same value, regardles of how many times I call ForceMeshUpdate()

     
    PNUMIA-Rob likes this.
  22. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Here is a script that you can use for testing

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using TMPro;
    4.  
    5. public class TestScript : MonoBehaviour
    6. {
    7.     public TMP_Text TextComponent;
    8.  
    9.     private void Awake()
    10.     {
    11.         // The GetPreferredValues() function does an internal layout pass where as such
    12.         // it is not necessary to call ForceMeshUpdate().
    13.         Vector2 preferredValues = TextComponent.GetPreferredValues("A line of text.");
    14.         Debug.Log(preferredValues);
    15.  
    16.         // The preferredWidth property internally does a layout pass where again as such
    17.         // it does not require calling ForceMeshUpdate().
    18.         TextComponent.SetText("A different line of text.");
    19.         Debug.Log(TextComponent.preferredWidth);
    20.  
    21.         // The values of the bounds and textBounds including the content of the textInfo are not updated / populated until the text has been rendered.
    22.         // As such, it is necessary to use ForceMeshUpdate() when trying to access these values prior to the text being rendered.
    23.         // Note that bounds are affected by the RectTransform size and line breaking.
    24.         TextComponent.ForceMeshUpdate();
    25.         Debug.Log(TextComponent.textBounds);
    26.     }
    27. }
    28.  
    Add the above script to some text object and assign the component to the above public field.

    Let me know if you have any questions about the above?
     
    Maxim likes this.
  23. sergiusz308

    sergiusz308

    Joined:
    Aug 23, 2016
    Posts:
    235
    Code (CSharp):
    1. public void SetLabelText(string text)
    2.         {
    3.             if (this.Label != null)
    4.             {
    5.  
    6.                 Vector2 pv = this.Label.GetPreferredValues();
    7.  
    8.                 Debug.Log($"TEST: pv: {pv}, text: {this.Label.text}");
    9.  
    10.                 this.Label.SetText(text);
    11.  
    12.                 Debug.Log($"TEST2: pv: {this.Label.preferredWidth}, text: {this.Label.text}");
    13.  
    14.                 this.Label.ForceMeshUpdate();
    15.  
    16.                 Debug.Log($"TEST3: b: {this.Label.textBounds}, text: {this.Label.text}");
    17.                 Debug.Log($"TEST3: s.x: {this.Label.textBounds.size.x}, text: {this.Label.text}");
    18.             }
    19.         }
    results:

    TEST: pv: (0.00, 0.00), text:
    TEST2: pv: 187,76, text: Press [Q] for Builder Menu
    TEST3: b: Center: (-6.48, 0.00, 0.00), Extents: (6.48, 238.48, 0.00), text: Press [Q] for Builder Menu
    TEST3: s.x: 12,96, text: Press [Q] for Builder Menu

    Since all these values are different, which one is the right one?

    preferredWidth returns 187
    bounds Extents.X returns 6.48
    bounds Size.X returns 12.96
     
  24. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    The GetPreferredValues() function has several overloads.

    GetPreferredValues() would return the width based on the current text.

    GetPreferredValues(string) would return the values based on the new string.

    In your example above, I presume the text was not set on the first call to GetPreferredValues() so it would return zero.

    The preferred width should be correct and reflective of the newly set text.

    The bounds should be correct and as per my previous post would reflect the textBounds factoring in the limitation on the width and potential height of the text. The bounds size.x would be that of the longest line of text again factoring in line breaking which is affected by the current width of the RectTransform.

    So all of the above are correct.

    BTW: Add a TextInfoDebugTool.cs script to your text object. This script is found in the TMP Examples & Extras and will allow you to visualize the bounds and textBounds.
     
    sergiusz308 likes this.
  25. PNUMIA-Rob

    PNUMIA-Rob

    Joined:
    Jan 7, 2015
    Posts:
    33
    Using TMP_Text.preferredWidth did the trick for me. Thanks a ton @sergiusz308 :D
     
    sergiusz308 likes this.
  26. SimonGylver

    SimonGylver

    Joined:
    Mar 15, 2022
    Posts:
    2
    Thank you! I learned the hard way that scroll views, layout groups and Content size fitters don't play well together, and struggled for two full days to get my UI to arrange itself properly. Using textBounds to make my own content size fitter solved everything :))
     
  27. lucbloom

    lucbloom

    Joined:
    Mar 27, 2020
    Posts:
    42
    Looking at the internals of TextMeshPro: (Library\PackageCache\com.unity.textmeshpro@3.0.6\Scripts\Runtime\TMPro_UGUI_Private.cs)
    Code (CSharp):
    1. if (m_canvas == null) { m_canvas = this.canvas; if (m_canvas == null) return; }
    ForceMeshUpdate calls OnPreRenderCanvas. So it appears you have to add it to a Canvas first. If the object is not yet added to a hyrarchy below a Canvas, calling ForceMeshUpdate does not do anything, not even ForceMeshUpdate(true, true).

    This can happen for instance when using Addressables.LoadAssetAsync or when instantiating a Prefab with UnityEngine.Object.Instantiate.
     
  28. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    For Unity 2022.LTS, I found:

    1. preferred, rendered, and textBounds are often wrong - about 10%-20% of UIs are affected (and 100% reproducible when they're wrong)
    2. ForceMeshUpdate - with all combinations of true/false in both args - has zero effect in those cases.
    3. GetPreferredValues is wrong
    4. BUT: GetPreferredValues( text-string, size-of-parent ) works (the version that doesn't need the text-string may also work, but I stopped testing when I eventually found something that was giving almost-correct values
    5. manually calcualting by iterating through the textInfo... data ... works
    6. 4 and 5 above differ by approximately 1.2 pixels; probably I was missing something from my manual calculation from the textInfo, so I'm inclined to trust the GetPreferredValues() call
    Stepping through in debugger it's introducing some absurd artificial values into lineOffset (can't understand why) that then breaks most of the above methods.

    Other observations:
    - I think ForceMeshUpdate has never fully worked for this kind of stuff, it's more of a "RequestMeshUpdateIfYoureFeelingLucky"? In my experience it has always had some cases where it does not (seem to) 'force' anything?
    - The docs could really do with some updates on this, especially given how inconsistent it all is.