Roman
Roman

Reputation: 3941

Why is TextMeshPro not Updating textBounds After Setting text?

I have implemented a tooltip system on hover. For this I use a textmeshpro text and an image. This image should be a simple background depending on the length of the text, which is always different.

If I hover over a certain element, the textmeshpro text is replaced. The background image is moved to the element and the size is shall change based on the length of the text:

// Hover over slot in
public void OnPointerEnter(PointerEventData eventData) {
    if (currentItem != null) {
        Debug.Log("hover over slot item in");
        GamePlay.EnableCanvas(GamePlay.GetHoverCanvas());

        TextMeshProUGUI txtMeshPro = GamePlay.GetHoverCanvas().GetComponentInChildren<TextMeshProUGUI>();

        Debug.Log(currentItem.GetItemDescription(currentItem));
        txtMeshPro.text = currentItem.GetItemDescription(currentItem);

        var hoverCanvasText = txtMeshPro.GetComponent<RectTransform>();
        var hoverCanvasTextBG = GamePlay.GetHoverCanvas().GetComponentInChildren<Image>().GetComponent<RectTransform>();

        hoverCanvasText.position = new Vector3(this.GetComponent<RectTransform>().position.x, this.GetComponent<RectTransform>().position.y + 50, 0);

        hoverCanvasTextBG.position = new Vector3(this.GetComponent<RectTransform>().position.x, this.GetComponent<RectTransform>().position.y + 50, 0);
        hoverCanvasTextBG.sizeDelta = new Vector2(txtMeshPro.textBounds.size.x + 15, txtMeshPro.textBounds.size.y + 15);
        // No clue why image is not resized properly on first hover
    }     
}

The weird issue I have is that the first time I hover over the element, the image size wont change.

Replacing the text and changing the position always works without issues.

Issue description:

First hover over element A: Image background size wont change at all.

Second hover over element B: Image background size changes, but has the correct size of element A

Third hover over element A: Image background size changes, but has the correct size of element B

How is this possible? I dont get whats happening here. The code works without any issues for the position, why the same logic does not work for the size.

Note: I am very new to unity, tinkering arround for a few days now, so I could be a very simple issue, but after researching for a few hours now, I haven't found anything useful.

Upvotes: 7

Views: 10165

Answers (4)

Mike Nelhams
Mike Nelhams

Reputation: 11

I had a similar issue with a Hover Tip window that appears over objects to provide information. On the first time rendering, its dimensions were wrong, but it resized after the next time the text was set.

The fix by Anomalous Underdog works for me and it's efficient compared to forcing UI elements to refresh. By keeping the width fixed, the preferredHeight can be calculated correctly.

Here's the not-working example:

tmpObject.text = newText;
float maxWidth = 200;
float preferredWidth = tmpObject.preferredWidth > maxWidth ? maxWidth : tipText.preferredWidth;
// tmpObject.preferredHeight (and its other bounds) are not refreshed
tmpObject.sizeDelta = new Vector2(preferredWidth, tmpObject.preferredHeight);

Here's the fixed example:

tmpObject.text = newText;
float maxWidth = 200;
float preferredWidth = tmpObject.preferredWidth > maxWidth ? maxWidth : tipText.preferredWidth;
Vector2 preferredDimensions = tmpObject.GetPreferredValues(tip, preferredWidth, 0);
tmpObject.sizeDelta = new Vector2(preferredWidth, preferredDimensions.y);

Upvotes: 1

Vivien Lynn
Vivien Lynn

Reputation: 355

Instead of refreshing all your Canvases, you can use TMP_Text.ForceMeshUpdate(); right after you assign your string. This makes sure your text-information are correct within the same frame.

If the parents of your text uses Layout-Groups, there is a fair chance that you need to update those as well. You do that with LayoutRebuilder.ForceRebuildLayoutImmediate( [Layout-Group.RectTransform] ); (read more here)

Please note that the order is important.

  1. Assign your text
  2. Rebuild Layout-Group
  3. Force Mesh Update

Upvotes: 5

Anomalous Underdog
Anomalous Underdog

Reputation: 260

To prevent the need to force update the Canvas, you can use TextMeshProUGUI.GetPreferredValues() to ascertain ahead of time what the width and height would be for the text currently assigned to it. It'll return a Vector2. You can then use that Vector2 instead of TextMeshProUGUI.textBounds.

Or if you need the size for a certain text without actually assigning it, use TextMeshProUGUI.GetPreferredValues(string text).

There are other overloads of that method to get the size given constraints in either width or height.

Upvotes: 2

Foggzie
Foggzie

Reputation: 9821

After setting the text of a TextMeshProUGUI, the textBounds and its size are not updated until the following frame. This is caused by Unity's deferred layout rebuilding. In their UI Layout Documentation, it reads:

The rebuild will not happen immediately, but at the end of the current frame, just before rendering happens. The reason it is not immediate is that this would cause layouts to be potentially rebuild many times during the same frame, which would be bad for performance.

I'm gonna go ahead and guess GamePlay.GetHoverCanvas() is maintaining an object that's never destroyed and, therefore, your code is working in all cases except its very first run.

There are probably a few ways you can go about fixing this but the easiest would be to call Canvas.ForceUpdateCanvases() between setting the text and reading the size of the textbounds. It's not the fastest function but it should do the trick.

Another option would be to, instead of a synchronous function, kick off a coroutine that sets the text one one frame, yields the next frame, then reads the textbounds on its second frame of execution. (This would likely lead to the size popping in and might look undesirable).

Upvotes: 3

Related Questions