Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Bug REPORT: Label.MeasureTextSize & WhiteSpace.Normal give abnormal behaviors with spaces & line breaks

Discussion in 'UI Toolkit' started by mikejm_, Aug 13, 2023.

  1. mikejm_

    mikejm_

    Joined:
    Oct 9, 2021
    Posts:
    346
    This is an old bug which has been present since at least 12-18 months ago but there were bigger fish to fry so I did not report it until now. Currently though this is severely impairing any ability to get accurate layout predictions or perform manual layouts when Label is involved.

    This has been reported formally as a bug: [IN-51217]. Instructions are shown on screen with the project, and I have also listed them here to summarize and explain the issue. Bug project code is included at the bottom of the post. Demo project is simple and appears as shown in Editor:

    bug - measure text size.PNG

    HOW TO USE BUG PROJECT:
    Type any string and it will be shown in the gray "TEXT" label at the top of the screen with Debug.Log outputting the resulting MeasureTextSize and worldBound of this label. These output dimensions should match at all times. ie. MeasureTextSize should always return the expected worldBound of a label when that label has no padding/margins.

    MeasureTextSize correctly matches worldBound with common text inputs like "fdkfd" but does not give accurate readings in numerous other conditions. This leads to the following problems which can glitch out any design dependent on it.

    BUGS DEMONSTRATED:

    1) MEASURE TEXT SIZE DOES NOT CAPTURE LEADING OR TRAILING SPACES OR NEW LINE BREAKS
    • Backspace to fully clear the TEST label. Then start typing a few spaces, and you will see the worldBound will reflect the size of these spaces, but MeasureTextSize will continuously output a false size of (1, 1). Only once you add a character like typing space-space-space-J will MeasureTextSize give accurate results again.

    • Similarly, if you type a letter, then a few spaces after, you will see the worldBound will reflect the size of these trailing spaces, but MeasureTextSize will keep returning the same size as if they are not there, until again you add a non-whitespace character at the end.

    • The same results are had by using a new line break (press enter on keyboard) at the start or end of the string (eg. type enter-enter-enter-j-enter-enter and see it is only accurate when the j is pressed and not before or after)

    2) MEASURE TEXT SIZE DOES NOT GIVE ACCURATE RESULTS FOR EMPTY LABEL
    • Press backspace to clear the label. You will see that when it is empty, MeasureTextSize reports dimensions of (0, 0), but worldBound still more accurately gives the actual size of the Label, which is not (0, 0) as the Label still takes up space. This should be matched by MeasureTextSize though it is not.

    3) SPACES ARE NOT CORRECTLY HANDLED IN GENERAL BY LABEL WITH WHITESPACE.NORMAL
    • The label is set to wrap (WhiteSpace.Normal) but wrapping is not correctly managed when spaces trigger the wrapping. It is not clear what is happening to these spaces. But if you type "j-space-space-space-[100 spaces]-space..." you will see the spaces do not wrap or continue to grow the Label.

    • Furthermore, no matter how many spaces you add, if you then press another letter, like "j-space-space-space-[100 spaces]-space-j", the second character will always be at the start of the second line. It is not clear where all these intervening spaces are going or how they are being laid out. Spaces should take up character space just like any other character and wrap into new lines as indents or blank space but are not.

    CONCLUSION:
    Working MeasureTextSize is necessary for any manual layout attempts as otherwise one must rely on worldBound which is delayed by one frame. Manually laying out Labels instantly cannot then work instantly with worldBound. MeasureTextSize is necessary for accurate and glitch free manual layouts.

    Any fixes should hopefully also be represented in character vertex data of UnityEngine.TextCore.Text.TextGenerator, UnityEngine.TextCore.Text.TextInfo textInfo, & UnityEngine.TextCore.Text.TextElementInfo textElementInfo (for example for the space wrapping issue) so that these can all be used to get accurate data on the expected performance/size/layout of a Label and its characters.


    BUG REPORT DEMO CODE:
    If any Unity guys reading here would be kind enough or interested enough to take a look quickly without the bug report, you can quickly add the following to a blank UI Document GameObject with a panel setting of 1440 width x 3040 height, scaling to width:

    Code (csharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UIElements;
    5.  
    6. public class AppScript : MonoBehaviour {
    7.  
    8.     Label label;
    9.     Label explanationLabel;
    10.     string keyboardInputString; //per frame input
    11.     string keyboardText = "TEST"; //stored text inputted by keyboard over time to set to label
    12.     bool keyboardInputChanged = false; //to trigger label update and debug output
    13.  
    14.     // Start is called before the first frame update
    15.     void Start() {
    16.         VisualElement rootVE = GetComponent<UIDocument>().rootVisualElement;
    17.  
    18.         VisualElement bgVE = new();
    19.         bgVE.style.width = bgVE.style.height = new Length(100, LengthUnit.Percent);
    20.         bgVE.style.backgroundColor = Color.white;
    21.         rootVE.Add(bgVE);
    22.  
    23.         //===================================
    24.         //TEST LABEL (TAKES KEYBOARD INPUT)
    25.         //===================================
    26.         label = new Label(keyboardText);
    27.         label.style.backgroundColor = Color.gray;
    28.         label.style.position = Position.Absolute;
    29.         label.style.whiteSpace = WhiteSpace.Normal;
    30.         label.style.fontSize = 80;
    31.         label.style.maxWidth = 1200;
    32.         label.style.paddingBottom = label.style.paddingTop = label.style.paddingLeft = label.style.paddingRight = 0;
    33.         label.style.marginBottom = label.style.marginTop = label.style.marginLeft = label.style.marginRight = 0;
    34.         label.style.alignSelf = Align.Center;
    35.         bgVE.Add(label);
    36.  
    37.         //=========================================================
    38.         //EXPLANATION OF BUG PROJECT AND ISSUES
    39.         //=========================================================
    40.         UnityEngine.InputSystem.Keyboard.current.onTextInput += keyboardInputEventFunction;
    41.         string explanation = "<u><b>HOW TO USE BUG PROJECT:</b></u>";
    42.         explanation += "\n<b>(CLICK ANYWHERE ON SCREEN TO HIDE/SHOW THESE INSTRUCTIONS)</b>";
    43.         explanation += "\n\nType any string and it will be shown in the gray TEXT label above with Debug.Log outputting the resulting MeasureTextSize and worldBound of this label. These output dimensions should match at all times. ie. MeasureTextSize should always return the expected worldBound of a label when that label has no padding/margins.";
    44.         explanation += "\n\nMeasureTextSize correctly matches worldBound with common text inputs like \"fdkfd\" but does not give accurate readings in numerous other conditions. This leads to the following problems which can glitch out any design dependent on it.";
    45.         explanation += "\n\n<u><b>BUGS DEMONSTRATED:</b></u>";
    46.         explanation += "\n\n<b>1) MEASURE TEXT SIZE DOES NOT CAPTURE LEADING OR TRAILING SPACES OR NEW LINE BREAKS</b>";
    47.         explanation += "\n\n- Backspace to fully clear the TEST label. Then start typing a few spaces, and you will see the worldBound will reflect the size of these spaces, but MeasureTextSize will continuously output a false size of (1, 1). Only once you add a character like typing space-space-space-J will MeasureTextSize give accurate results again.";
    48.         explanation += "\n\n- Similarly, of you type a letter, then a few spaces after, you will see the WorldBound will reflect the size of these trailing spaces, but MeasureTextSize will keep returning the same size as if they are not there, until again you add a non-whitespace character at the end.";
    49.         explanation += "\n\n- The same results are had by using a new line break (press enter on keyboard) at the start or end of the string (eg. type enter-enter-enter-j-enter-enter and see it is only accurate when the j is pressed and not before or after)";
    50.         explanation += "\n\n<b>2) MEASURE TEXT SIZE DOES NOT GIVE ACCURATE RESULTS FOR EMPTY LABEL</b>";
    51.         explanation += "\n\n- Press backspace to clear the label. You will see that when it is empty, MeasureTextSize reports dimensions of (0, 0), but WorldBound still more accurately gives the actual size of the Label, which is not (0, 0) as the Label still takes up space. This should be matched by MeasureTextSize though it is not.";
    52.         explanation += "\n\n<b>3) SPACES ARE NOT CORRECTLY HANDLED IN GENERAL BY LABEL WITH WHITESPACE.NORMAL</b>";
    53.         explanation += "\n\n- The label is set to wrap (WhiteSpace.Normal) but wrapping is not correctly managed when spaces trigger the wrapping. It is not clear what is happening to these spaces. But if you type \"j-space-space-space-[100 spaces]-space...\" you will see the spaces do not wrap and continue to grow the Label.";
    54.         explanation += "\n\n- Furthermore, no matter how many spaces you add, if you then press another letter, like \"j-space-space-space-[100 spaces]-space-j\", the second character will always be at the start of the second line. It is not clear where all these intervening spaces are going or how they are being laid out. Spaces should take up character space just like any other character and wrap into new lines as indents or blank space but are not.";
    55.         explanation += "\n\n<u><b>CONCLUSION:</b></u>";
    56.         explanation += "\nWorking MeasureTextSize is necessary for any manual layout attempts as otherwise one must rely on worldBound which is delayed by one frame. Manually laying out Labels instantly cannot then work instantly with worldBound. MeasureTextSize is necessary for accurate and glitch free manual layouts. ";
    57.         explanation += "\n\nAny fixes should also be represented in character vertex data of UnityEngine.TextCore.Text.TextGenerator, UnityEngine.TextCore.Text.TextInfo textInfo, & UnityEngine.TextCore.Text.TextElementInfo textElementInfo (for example for the space wrapping issue) so that these can all be used to get accurate data on the expected performance/size/layout of a Label and its characters.";
    58.  
    59.         Debug.Log(explanation);
    60.  
    61.         explanationLabel = new(explanation);
    62.         explanationLabel.style.whiteSpace = WhiteSpace.Normal;
    63.         explanationLabel.style.maxWidth = 1200;
    64.         explanationLabel.style.fontSize = 30;
    65.         explanationLabel.style.alignSelf = Align.Center;
    66.         bgVE.Add(explanationLabel);
    67.  
    68.         debugOutput();
    69. }
    70.  
    71. // Update is called once per frame
    72. void Update() {
    73.  
    74.         if (keyboardInputChanged) {
    75.  
    76.             //=========================
    77.             //KEYBOARD INPUT
    78.             //=========================
    79.             keyboardInputChanged = false;
    80.             processKeyboardUpdate();
    81.  
    82.             //=========================
    83.             //UPDATE LABEL
    84.             //=========================
    85.             label.text = keyboardText;
    86.  
    87.             //=========================
    88.             //DEBUG OUTPUT
    89.             //=========================
    90.             debugOutput();
    91.         }
    92.         if (Input.GetMouseButtonDown(0)) {
    93.             if (explanationLabel.visible) { explanationLabel.style.visibility = Visibility.Hidden; } else { explanationLabel.style.visibility = StyleKeyword.Null; }
    94.         }
    95.     }
    96.     void debugOutput() {
    97.         StartCoroutine(debugWorldBoundNextFrame());
    98.         IEnumerator debugWorldBoundNextFrame() { //must be delayed 1 frame as takes one frame to layout the change
    99.             yield return null;
    100.             Vector2 measureSize = label.MeasureTextSize(keyboardText, 1200, VisualElement.MeasureMode.AtMost, 0, VisualElement.MeasureMode.Undefined);
    101.             Debug.Log("TEXT: " + "\"" + keyboardText + "\"" + " MEASURED SIZE " + measureSize + " WORLDBOUND " + new Vector2(label.worldBound.width, label.worldBound.height));
    102.  
    103.             explanationLabel.style.marginTop = label.worldBound.height + 100; //manually shift the instruction label so not covering anything
    104.         }
    105.     }
    106.  
    107.     void keyboardInputEventFunction(char c) {
    108.         keyboardInputString += c;
    109.         keyboardInputChanged = true;
    110.     }
    111.     void processKeyboardUpdate() {
    112.  
    113.         foreach (char c in keyboardInputString) {
    114.             if (c == '\b') { // has backspace been pressed
    115.                 if (keyboardText.Length != 0) {
    116.                     keyboardText = keyboardText.Substring(0, keyboardText.Length - 1);
    117.                 }
    118.             }
    119.             else if ((c == '\n') || (c == '\r')) { // enter/return
    120.                 keyboardText += "\n";
    121.             }
    122.             else {
    123.                 keyboardText += c;
    124.             }
    125.         }
    126.         keyboardInputString = "";
    127.     }
    128. }
    Thanks as always for any help.
     
    Last edited: Aug 15, 2023
    magnetic_scho likes this.
  2. mikejm_

    mikejm_

    Joined:
    Oct 9, 2021
    Posts:
    346
    I received the following correspondence on the bug report which is interesting but raises some other questions:

    1) Label.MeasureTextSize()
    Label.MeasureTextSize() will be used whenever we are laying out Labels that need an instant manual reposition of other objects around it. That is the practical use for this function. As noted in the bug report post above, worldBound has a one frame lag, so the only way to instantly know the expected size of a Label is Label.MeasureTextSize().

    This function works in every circumstance (delivering the exact expected worldBound of the Label) except when the Label is empty or containing only spaces or new line breaks or where there are trailing line breaks or spaces.

    So I can't think of any practical value of the current system as it is functioning. Ie. If you are using Label.MeasureTextSize() solely to predict the expected size of the Label with that text, then it should always deliver the expected size of that Label with that text.

    There is no benefit to it resulting in (0,0) for an empty Label when that empty Label will not be (0,0) in size. Or of not accounting for the size of the spaces when a Label with spaces will be a certain size that is not (0,0).

    So I hope the function will be fixable to make it practically useful, as right now it can only be used with numerous wasteful workarounds, ie:
    • Create a second duplicate Label that is empty with identical settings and take it's worldBound to know what space an empty label will take (waste of an extra element that must be laid on on screen also to get worldBound from it).
    • Create a third duplicate Label (or reuse the second one) with just holding a space / line break in it and take it's worldBound minus the x dimension of the empty one's worldbound to know the size of a space and line break to manually compensate.
    It would be much better to have a Label.MeasureTextSize() that delivers the information needed consistently - ie. The dimensions of the expected Label with that text in it, no matter the text.

    (Lastly, just on a minor semantic point, but one that is important, it is also not exactly true as suggested that Label.MeasureTextSize() "measures the text" - if one accesses the vertex data from inside the Label or an equivalent TextCore TextGenerator, one can find the true dimensions of the text itself. Label.MeasureTextSize() returns the size of the Label (which is what one needs for layouts) but just with these minor but design breaking inconsistencies.)

    2) WhiteSpace and Wrapping Management
    Regarding the management of spaces, if WhiteSpace.Normal is intended to collapse all white space like this (which does not match the behavior of any typical word processor, notepad, or web form), is there any way to add a third mode or change it so we have a way to make it function like any typical text display unit? Ie. Like this field I am typing in now? Where spaces wrap and take up space as normal? Or as in notepad.exe with wordwarp enabled or any other program like Microsoft Word?

    Again the current behavior does not seem practically beneficial compared to what would typically be useful or expected. ie. If I type 100 spaces in this forum space, all spaces will take up space and they won't collapse. This is the expected utilization of a text wrap function.

    Or at least it would be useful to have a standard wrap function that matches across the TextCore TextGenerator, Label, and Label.MeasureTextSize() that works this way.

    I am using Label derived classes and many manual layout functions to recreate systems equally complex to this forum or any social media page, and UI Toolkit works wonderfully, but being able to know expected sizes of Labels and have them handle spaces and wrapping normally is very important.

    Thanks again for any help.
     
    Last edited: Sep 8, 2023
    vejab likes this.
  3. vejab

    vejab

    Joined:
    Dec 21, 2021
    Posts:
    85