Search Unity

TextMesh Pro Calculate Width of a Text before/without assigning it to a TMP object

Discussion in 'UGUI & TextMesh Pro' started by Piflik, Oct 11, 2019.

  1. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    293
    I am currently trying to upgrade our existing code to TextMeshPro, but I came across a slight issue.

    We have a function to calculate the width (in pixel) of a string, depending on font, fontsize and fontstyle. We use that extensively in a custom input-field (we cannot use the built in one, because we use non-standard input) to position the cursor. I might be blind and/or too inexperienced with TMP, but I can't find a way to do something like that with TMP.

    As a reference, here is the code we use currently:

    Code (CSharp):
    1. public static int Width(this string s, Font font, int fontsize, FontStyle fontstyle = FontStyle.Normal) {
    2.         if (string.IsNullOrEmpty(s))
    3.             return 0;
    4.  
    5.         int w = 0;
    6.         font.RequestCharactersInTexture(s, fontsize, fontstyle);
    7.  
    8.         foreach (char c in s) {
    9.             font.GetCharacterInfo(c, out CharacterInfo cInfo, fontsize);
    10.             w += cInfo.advance;
    11.         }
    12.  
    13.         return w;
    14.     }
    I have found the GetPreferredWidth function, but that would mean to actually change the string of the text object, and the GetPreferredValues(string) function, but this still needs a reference to an existing Text object. I would prefer a way where I would only need to change the Font and FontStyle parameters in above function's signature to their respective TMP counterpart. Can anybody point me in the right direction?
     
  2. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    The following function does allow you to get the preferred width without changing the string of the text object.

    Code (csharp):
    1.  
    2. /// <summary>
    3. /// Function to Calculate the Preferred Width and Height of the text object given a certain string.
    4. /// </summary>
    5. /// <param name="text"></param>
    6. /// <returns></returns>
    7. public Vector2 GetPreferredValues(string text)
    8.  
    However, there is no overload to test different fonts or font size without affecting the text object. As such, I would recommend creating / use a single text object for testing where you would set the properties accordingly in terms of font, fontSize, style, etc and then use the GetPreferredValues() on this specific text object.

    I believe the above would be more efficient than your existing code which requires adding / rasterizing glyphs into a texture and then having to iterate over each character to figure out the total advance value.

    My suggestion would require that you have an existing font asset for each of these fonts which I do not know if you already have or not.
     
  3. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    293
    As I said, I know that function exists and for many use-cases it works, but I need one that works without having a TMP Component already present, because sometimes I need the width before I can even create the Component.

    Having an extra object with a component somewhere off screen is not a solution I can accept, because it is not clean.
     
  4. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    It may not appear to be clean but it would most likely be much more efficient as rasterizing and adding glyphs into a texture is very expensive. This alternative would also be more flexible. Furthermore, this text object could be disabled.

    But as per my previous post, are you going to have font assets already created for each of these fonts you are planning on testing?

    When you perform this check, do you always end up using that font. I need a better understanding to provide better guidance.

    P.S. You stated that you are using this to position the Cursor. Do you always end up using that specific text string with those settings?

    If so, whenever a TMP text object is rendered, the TMP_Text.textInfo gets populated with information about every character, word, line, link, etc. In the case of characters, the textInfo contains their origin and advance values. Ie. everything you would need to position a cursor relative to that text. This is what the TMP Input Field uses to handle all positioning.
     
    Last edited: Oct 11, 2019
  5. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    293
    I do already have the FontAsset, or rather I assume that it exists, since this code is part of our framework which is used by many projects. If the person responsible for a project doesn't use the function correctly, I do not take responsibility.

    The function I would like to have would take a
    TMP_FontAsset
    as a parameter, so it would have to exist (or throw an exception).

    My specific use-case is quite a bit more complex, since I don't use the TMP object directly in the input field. We have wrapper classes for all the Unity GUI components for easier handling, since we create, position and animate all our GUI via code, no prefabs, and no GUI objects already in the scene.
    My input field is using one of these wrapper objects (along with some other stuff), currently with the basic Unity Text, but I want to upgrade it to use TMP. This wrapper object is positioned depending on the object's width, the text's width and the cursor's position within the text (the index of the character it is supposed to be drawn next to), as is the cursor itself, so the part of the text that is currently being edited is always drawn and not clipped by the mask hiding the overflow.
    This already works, both with the Unity Text and the TextMeshProUGUI wrappers (I can use the wrapper-objects reference to its TMP component and call the GetPreferredValues function, also I don't allow multiple lines or anything else that could complicate things).

    But somewhere else I need the same width calculation to create a panel with a TextMeshProUGUI object on top, and I need to decide how wide that panel should be, depending on the text that is displayed on it. I get the string at runtime, before the panel or the TMP object exists. I could create the object with a default width and then resize it afterwards, but I would like to avoid hacks like that.
     
  6. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Reflecting on the above last night as I went to sleep, I guess you could take a similar approach with TMP.

    This implementation would be similar to what you are currently using and provide you with an approximation of the width of the text.

    Unlike the PreferredValues() and other functions which do a full parsing and layout pass which factors in the potential affect of rich text tags in the source string, character and word spacing modifiers, glyph positional adjustments (Kerning) , etc. this function only and simply uses font asset data and glyph metrics to approximate the width.

    This proposed function is very efficient in the sense that it only queries the font asset and does not try to add characters and glyphs in a texture.

    Here is a script that contains this function for you to experiment with.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using TMPro;
    4.  
    5. public class TextWidthApproximator : MonoBehaviour
    6. {
    7.     public string Text;
    8.     public TMP_FontAsset FontAsset;
    9.     public int FontSize;
    10.     public FontStyles Style;
    11.     public TMP_Text TextComponent;
    12.  
    13.     private void Awake()
    14.     {
    15.         Debug.Log("Preferred Width: " + TextComponent.GetPreferredValues(Text).x);
    16.         Debug.Log("Approximated Width: " + TextWidthApproximation(Text, FontAsset, FontSize, Style));
    17.     }
    18.  
    19.     public float TextWidthApproximation (string text, TMP_FontAsset fontAsset, int fontSize, FontStyles style)
    20.     {
    21.         // Compute scale of the target point size relative to the sampling point size of the font asset.
    22.         float pointSizeScale = fontSize / (fontAsset.faceInfo.pointSize * fontAsset.faceInfo.scale);
    23.         float emScale = FontSize * 0.01f;
    24.  
    25.         float styleSpacingAdjustment = (style & FontStyles.Bold) == FontStyles.Bold ? fontAsset.boldSpacing : 0;
    26.         float normalSpacingAdjustment = fontAsset.normalSpacingOffset;
    27.  
    28.         float width = 0;
    29.  
    30.         for (int i = 0; i < text.Length; i++)
    31.         {
    32.             char unicode = text[i];
    33.             TMP_Character character;
    34.             // Make sure the given unicode exists in the font asset.
    35.             if (fontAsset.characterLookupTable.TryGetValue(unicode, out character))
    36.                 width += character.glyph.metrics.horizontalAdvance * pointSizeScale + (styleSpacingAdjustment + normalSpacingAdjustment) * emScale;
    37.         }
    38.  
    39.         return width;
    40.     }
    41. }
    42.  
    This function does factor in things like Face Info scale, normal and bold spacing adjustment modifiers, etc. It does not take into consideration potential scaling tweaks to characters and glyphs possible in the Character and Glyph tables. Ie. this is just something I put together quickly for you to build upon for your needs.
     
    Last edited: Oct 12, 2019
  7. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    293
    Thank you, I'll try this.

    Also, regarding performance, while looking through the TMP code I noticed that the
    TMP_Text.GetPreferredValues()
    function calls
    TMP_Text.GetPreferredWidth()
    and
    TMP_Text.GetPreferredHeight()
    , which are essentially the same function with the only difference being in the last line where they both call
    TMP_Text.CalculatePreferredValues()
    (hich is a 400 LoC function) and then discard the y and x component of the result vector, respectively. So the
    TMP_Text.GetPreferredValues()
    function calculates a vector by calculating it twice.
     
  8. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Correct. The preferred width is first computed without limiting the width of the text container and then the preferred height is then computed.

    See the following documentation about how the Layout system calculates these values.