Is there a uGUI replacement for GUIStyle.CalcSize?

In Unity 4.6, I have an uGUI text box in which I want to gradually display a paragraph of text one letter at a time. However, when I do this, there’s an undesirable ‘flickering’ effect at the end of each line, as words grow too long and are automatically wrapped to the next line.

For example, the text box will first display:

This is a very long line that exceeds the wi

and then moments later:

This is a very long line that exceeds the
width

My first instinct on how to avoid this issue is to calculate the estimated width in pixels of the string being displayed, and manually insert line breaks before the next word to be displayed when it would cause the line to exceed the width of the textbox and wrap. Using the old system the GUIStyle.CalcSize function could perform this task. However, I do not see any equivalent function in the new UI system.

Do I have any alternative to creating a legacy GUIStyle for the sole purpose of calculating line width?

The new GUI system is actually way more open than the old one. You can use the TextGenerator used by your Text instance. You can trigger the population of the cachedTextGeneratorForLayout TextGenerator by reading preferredWidth of your Text. Once done you should be able to read it’s lines property which holds a cached List of UILineInfo structs, one for each generated line. With that information you should be able to split the whole text into lines at once.

Since the answer seems to be “no,” for the benefit of anyone who stumbles across this question later looking for a similar solution, the one I went with was this subroutine that I call using “while (RequestText (out newText))” from the coroutine displaying the text (make sure the text object has word wrap turned off):

	public bool RequestText(out string newText) {
		char[] s = fullText.ToCharArray ();
		if (textPosition >= fullText.Length) {
			newText = "";
			return false;
		} else if (s[textPosition] == ' ') {
			// grab text until we hit another space
			newText = " ";
			int i = 1;
			while (textPosition+i < fullText.Length && s[textPosition+i] != ' ') {
				newText += s[textPosition+i];
				i++;
			}
			txt.GetComponent<UnityEngine.UI.Text>().text = partialText+newText;
			if (txt.GetComponent<UnityEngine.UI.Text>().preferredWidth > txt.sizeDelta.x) {
				// the next word is too long; insert a line break
				newText = " 

";
} else {
newText = " ";
}
textPosition++;
return true;
} else {
newText = s[textPosition].ToString ();
textPosition++;
return true;
}
}

I was concerned that actually assigning the text to the textbox would cause flickering, but that doesn’t seem to happen - I guess Unity is smart enough to not blit the text to screen until the next frame.

Hello.

Let me just add that you can in fact calculate how many characters until a new line is produced by two methods:

1 - If your font is monospaced (every character is the same length), you can add a fake character, get the Texts preferredWidth and then use that to divide the rectTransform width:

public int CalculateTextWidthInCharactersMonospaced ()
	{
		string backupText = text.text;
		text.text = "M";
		var count = Mathf.FloorToInt (text.rectTransform.rect.width / text.preferredWidth);
		text.text = backupText;
		return count;
	}

2 - If your font is not monospaced, add a fake character in a loop while the Text width is bigger than the preferredWidth, then count the characters and subtract one (the one that caused new line) :

public int CalculateTextWidthInCharactersNonMonospaced ()
	{
		string backupText = text.text;
		text.text = "M";
		while (text.rectTransform.rect.width > text.preferredWidth)
			text.text += "M";
		var count = text.text.Length - 1;
		text.text = backupText;
		return count;
	}

Both methods require that the Text component is already rendered on screen, so, if you want to calculate it at the Start() method you have to delay its execution for at least 1 frame, here is a full example that sets a public variable so you can see it in the editor :

public int CalculatedMaxCharWidth = 0;
	Text text;
	void Awake ()
	{
		text = GetComponentInChildren<Text> ();
	}
	IEnumerator Start ()
	{
		yield return null;
		CalculatedMaxCharWidth = CalculateTextWidthInCharactersNonMonospaced ();
	}
	public int CalculateTextWidthInCharactersMonospaced ()
	{
		string backupText = text.text;
		text.text = "M";
		var count = Mathf.FloorToInt (text.rectTransform.rect.width / text.preferredWidth);
		text.text = backupText;
		return count;
	}
	public int CalculateTextWidthInCharactersNonMonospaced ()
	{
		string backupText = text.text;
		text.text = "M";
		while (text.rectTransform.rect.width > text.preferredWidth)
			text.text += "M";
		var count = text.text.Length - 1;
		text.text = backupText;
		return count;
	}

Hopefully this helps someone :slight_smile:

Came across this since I have been trying to do scrolling text and had the same problem. I dug a bit deeper and found there is something we can use for a replacement for GUIStyle.CalcSize… at least for the purposes we need.

textWeWantToDisplay = "this is just some text we want to display";

TextGenerationSettings generationSettings = txt.GetGenerationSettings(txt.rectTransform.rect.size); 
float textHeight = txt.cachedTextGeneratorForLayout.GetPreferredHeight(textWeWantToDisplay, generationSettings);
Debug.Log("textHeight: " + textHeight);

Where our Text component is “txt”. This will log the height of what our Text component will be for the given textWeWantToDisplay.

We can use this by constantly checking the height we have displayed to the screen so far before we write each word. After calculating the height with the new word, if we find that the calculated height is bigger then we can insert a ’
’ new line character.

EDIT: Actually just quickly wrote this up now and it works great :slight_smile: Can be improved but it’s a nice start. To use it, when you’re scrolling (or typewriting) your characters, if you detect a space character, " ", you can call doesNextWordWrap(). If it returns true, write a newline instead of a space.

private bool doesNextWordWrap(string textParagraph, string entireTextParagraph, int index) {
	string nextWord = getNextWord(entireTextParagraph, index);
	TextGenerationSettings generationSettings = txt.GetGenerationSettings(txt.rectTransform.rect.size); 
	float originalTextHeight = txt.cachedTextGeneratorForLayout.GetPreferredHeight(textParagraph, generationSettings);
	float newTextHeight = txt.cachedTextGeneratorForLayout.GetPreferredHeight(textParagraph + nextWord, generationSettings);
	if(newTextHeight > originalTextHeight) {
		return true;
	}
	return false;
}

private string getNextWord(string entireTextParagraph, int index) {
	string textAfterParagraph = entireTextParagraph.Substring(index + 1);
	string[] nextWords = textAfterParagraph.Split(new [] {" "}, System.StringSplitOptions.RemoveEmptyEntries);
	if(nextWords.Length > 0) {
		return nextWords[0];
	}
	return "";
}

On another note, I tried using the cachedTextGeneratorForLayout after reading it’s preferredWidth, as @Bunny83 suggested.

txt.text = textWeWantToDisplay;
float discardme = txt.cachedTextGeneratorForLayout.GetPreferredWidth(textWeWantToDisplay, txt.GetGenerationSettings(txt.rectTransform.rect.size));
UILineInfo[] lineInfo = txt.cachedTextGenerator.GetLinesArray();
Debug.Log("line amount: " + lineInfo.Length);
for(int j=0; j<lineInfo.Length; j++) {
	Debug.Log("line's character start index: " + lineInfo[j].startCharIdx);
}
txt.text = "";

It was delayed by one, so for example the first time I ran it, it didn’t have any lines. But the next time I called it, it said it had 2 lines, when really I had 3 lines. This repeated, where it always showed the previous value for “textWeWantToDisplay”. I’m sure a solution could be worked out of it, but I think it would be a bit messier because of the delayed value. A shame since knowing the character index makes this much easier, just pop in a new line character before each index.