Rect Transform Size Limiter

I needed something that makes sure a rect transform with a ContentSizeFitter doesn’t exceed a certain size.

So I put something together quickly. Make sure to slap it on as a component after the content size fitter. With X or Y set to 0 that axis isn’t limited.

https://bitbucket.org/snippets/Democritus/5ex7n4

14 Likes

Thank you!
That’s exactly what we need!

This is exactly what I needed as well. After reading countless posts about people saying to just use preferred width with flexible width set to 0 to control max width (which didn’t work at all in my case), this solved it perfectly.

This is pure gold, thank you.

I slightly modified the code to support also a minSize (you can probably achieve this with Layout element but this way you have only one script to manage everything).

It’s just a dumb extension of your code, so, please, take a look at it if you notice something wrong.

Thanks again.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[ExecuteInEditMode]
public class RectSizeLimiter : UIBehaviour, ILayoutSelfController
{

    public RectTransform rectTransform;

    [SerializeField]
    protected Vector2 m_maxSize = Vector2.zero;

    [SerializeField]
    protected Vector2 m_minSize = Vector2.zero;

    public Vector2 maxSize
    {
        get { return m_maxSize; }
        set
        {
            if (m_maxSize != value)
            {
                m_maxSize = value;
                SetDirty();
            }
        }
    }

    public Vector2 minSize
    {
        get { return m_minSize; }
        set
        {
            if (m_minSize != value)
            {
                m_minSize = value;
                SetDirty();
            }
        }
    }

    private DrivenRectTransformTracker m_Tracker;

    protected override void OnEnable()
    {
        base.OnEnable();
        SetDirty();
    }

    protected override void OnDisable()
    {
        m_Tracker.Clear();
        LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
        base.OnDisable();
    }

    protected void SetDirty()
    {
        if (!IsActive())
            return;

        LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
    }

    public void SetLayoutHorizontal()
    {
        if (m_maxSize.x > 0f && rectTransform.rect.width > m_maxSize.x)
        {
            rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, maxSize.x);
            m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaX);
        }

        if (m_minSize.x > 0f && rectTransform.rect.width < m_minSize.x)
        {
            rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, minSize.x);
            m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaX);
        }

    }

    public void SetLayoutVertical()
    {
        if (m_maxSize.y > 0f && rectTransform.rect.height > m_maxSize.y)
        {
            rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, maxSize.y);
            m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaY);
        }

        if (m_minSize.y > 0f && rectTransform.rect.height < m_minSize.y)
        {
            rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, minSize.y);
            m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaY);
        }

    }

#if UNITY_EDITOR
    protected override void OnValidate() {
        base.OnValidate();
        SetDirty();
    }
#endif

}
4 Likes

@Democide Thank you, finally a reasonable solution to a simple problem.

@Democide Works perfectly. Thank you very much! I finally I have a text box that shrinks and expands to the text size, but forces a wrap of text when it reaches max width. Unity should absolutely add max width/height to the Layout Element.

2 Likes

This is amazing! I’ve only wanted this for 7 years XD

I tried using this to create a textbox with an auto-sizing background image, but ran into the same type of staggered size updates on the parent with the background image, so I added an optional reference to a parent Transform, which makes the parent also automatically fit the text element.

You’ll still have a problem with any other parent that wants to react to the size of the text element, so this still isn’t a fix for all use-cases, but it’s a great start!

Thanks a whole lot, Democide and giggioz!

Usage notes:
To make a textbox, this script is supposed to sit on the GameObject with the TextMeshPRO component on it, after you have added a Content Size Fitter to it (set both its settings to Preferred Size). Do not try to use Unity’s normal Text components; they do not work properly, especially when it comes to tight fits, which make them stop showing due to floating point errors or something.

You also have to drag a reference to the child that the script is sitting on into the “Rect Transform” inspector field. It doesn’t find it automatically, to make it so the script doesn’t necessarily have to sit on the same GameObject that it controls RectTransforms for.

If you wish to have another RectTransform (usually the parent) also be sized along with the main “Rect Transform”, you can also add a reference to the “Parent Rect Transform” inspector field. Set the parent to stretch both horizontally and vertically (click the Anchor Presets button, hold Alt and press the button in the bottom-right with the two blue arrows). The parent does not need any other layout components for this to work.

In order to get margins, don’t use negative values in top/bottom/left/right on the child (text) RectTransform, but instead set the margins on the TextMeshPRO component itself, under Extra Settings. That way the parent can still be positioned properly using anchors.

Code

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[ExecuteInEditMode]
public class RectSizeLimiter : UIBehaviour, ILayoutSelfController
{

    public RectTransform rectTransform;
    public RectTransform parentRectTransform;

    [SerializeField]
    protected Vector2 m_maxSize = Vector2.zero;

    [SerializeField]
    protected Vector2 m_minSize = Vector2.zero;

    public Vector2 maxSize
    {
        get { return m_maxSize; }
        set
        {
            if(m_maxSize != value)
            {
                m_maxSize = value;
                SetDirty();
            }
        }
    }

    public Vector2 minSize
    {
        get { return m_minSize; }
        set
        {
            if(m_minSize != value)
            {
                m_minSize = value;
                SetDirty();
            }
        }
    }

    private DrivenRectTransformTracker m_Tracker;

    protected override void OnEnable()
    {
        base.OnEnable();
        SetDirty();
    }

    protected override void OnDisable()
    {
        m_Tracker.Clear();
        LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
        base.OnDisable();
    }

    protected void SetDirty()
    {
        if(!IsActive())
            return;

        LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
    }

    public void SetLayoutHorizontal()
    {
        if(m_maxSize.x > 0f && rectTransform.rect.width > m_maxSize.x)
        {
            rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, maxSize.x);
            m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaX);
        }

        if(m_minSize.x > 0f && rectTransform.rect.width < m_minSize.x)
        {
            rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, minSize.x);
            m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaX);
        }

        if (parentRectTransform != null)
        {
            parentRectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rectTransform.sizeDelta.x);
            m_Tracker.Add(this, parentRectTransform, DrivenTransformProperties.SizeDeltaX);
        }
    }

    public void SetLayoutVertical()
    {
        if(m_maxSize.y > 0f && rectTransform.rect.height > m_maxSize.y)
        {
            rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, maxSize.y);
            m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaY);
        }

        if(m_minSize.y > 0f && rectTransform.rect.height < m_minSize.y)
        {
            rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, minSize.y);
            m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaY);
        }

        if(parentRectTransform != null)
        {
            parentRectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rectTransform.sizeDelta.y);
            m_Tracker.Add(this, parentRectTransform, DrivenTransformProperties.SizeDeltaY);
        }
    }

#if UNITY_EDITOR
    protected override void OnValidate()
    {
        base.OnValidate();
        SetDirty();
    }
#endif

}

Nice! In my case I needed it to work inside a Horizontal Layout with another item, I wanted fully flexible to occupy all the remaining space.

If the other element’s LayoutElement preferred / min size is too low:

Unfortunately it doesn’t seem possible, but I could at least get a decent result by setting Min Size = Max Size on RectSizeLimiter to avoid being pushed back by the other element, and LayoutElement preferred width on the other element to be high enough.

Also note that I set the pivot where I wanted the rect to be aligned: here, I set it to the right so the “99” text touches the right edge of the parent rect.

The risk however is to get overlap.

And of course, when the parent size changes it gets even harder to control how the who will evolve relatively to each other.

I suppose we’d need a replacement for Layout Element itself to support max size, so this can interoperate with other Layout Elements… I’ve been following Why doesn't Layout Element have Max values? but it just redirected me here.

While this does indeed limit the size, it still reports the larger size to parents. So anything more than basic layouts does not work.

I just want to leave it here, maybe it will help someone in the future.
I took the solution from @giggioz and expanded it. If you need to allow the RectTransform to expand to a certain limit and shrink back depending on the content, then there is such a solution.
I am attaching a video showing the hierarchy of objects, the necessary components for them and how it looks visually (and yes, the entire layout is rebuilt instantly, in one frame.):

And unitypackage with prefab from video:
LimitResizableContent.unitypackage (8.2 KB)

And the script itself:

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[ExecuteInEditMode]
public class RectSizeLimiter : UIBehaviour, ILayoutSelfController
{
    [Header("Main Settings")]
    [SerializeField] public RectTransform mainContent;
    [SerializeField] private Vector2 m_maxSize = Vector2.zero;
    [SerializeField] private Vector2 m_minSize = Vector2.zero;

    [Header("Child Viewport Control Height Settings")]
    [SerializeField] private bool controlViewport;
    [SerializeField] private RectTransform viewport;
    [SerializeField] private LayoutElement viewportLayoutElement;
    [SerializeField] private RectTransform viewportContent;

    public Vector2 maxSize
    {
        get { return m_maxSize; }
        set
        {
            if (m_maxSize != value)
            {
                m_maxSize = value;
                SetDirty();
            }
        }
    }

    public Vector2 minSize
    {
        get { return m_minSize; }
        set
        {
            if (m_minSize != value)
            {
                m_minSize = value;
                SetDirty();
            }
        }
    }

    private DrivenRectTransformTracker m_Tracker;

    protected override void OnEnable()
    {
        base.OnEnable();
        SetDirty();
    }

    protected override void OnDisable()
    {
        m_Tracker.Clear();
        LayoutRebuilder.MarkLayoutForRebuild(mainContent);
        base.OnDisable();
    }

    protected void SetDirty()
    {
        if (!IsActive())
            return;

        LayoutRebuilder.MarkLayoutForRebuild(mainContent);
    }

    public void SetLayoutHorizontal()
    {
        if (m_maxSize.x > 0f && mainContent.rect.width > m_maxSize.x)
        {
            mainContent.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, maxSize.x);
            m_Tracker.Add(this, mainContent, DrivenTransformProperties.SizeDeltaX);
        }

        if (m_minSize.x > 0f && mainContent.rect.width < m_minSize.x)
        {
            mainContent.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, minSize.x);
            m_Tracker.Add(this, mainContent, DrivenTransformProperties.SizeDeltaX);
        }

    }

    private void LateUpdate()
    {
        if (controlViewport)
        {
            ForceRebuildLayoutRecursive(mainContent);

            var notContentFraction = (mainContent.rect.height - viewport.rect.height) / m_maxSize.y;
            var preferredHeight = m_maxSize.y * (1 - notContentFraction);

            if (viewportContent.rect.height > preferredHeight)
            {
                viewportLayoutElement.preferredHeight = preferredHeight;
                ForceRebuildLayoutRecursive(mainContent);
            }
            else
            {
                viewportLayoutElement.preferredHeight = viewportContent.rect.height;
                ForceRebuildLayoutRecursive(mainContent);
            }
        }
    }

    public void SetLayoutVertical()
    {
        if (m_maxSize.y > 0f && mainContent.rect.height > m_maxSize.y)
        {
            mainContent.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, maxSize.y);
            m_Tracker.Add(this, mainContent, DrivenTransformProperties.SizeDeltaY);
        }

        if (m_minSize.y > 0f && mainContent.rect.height < m_minSize.y)
        {
            mainContent.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, minSize.y);
            m_Tracker.Add(this, mainContent, DrivenTransformProperties.SizeDeltaY);
        }
    }

    private void ForceRebuildLayoutRecursive(RectTransform rectTransform)
    {
        if (rectTransform == null)
            return;

        for (int i = 0; i < rectTransform.childCount; i++)
        {
            var child = rectTransform.GetChild(i) as RectTransform;
            if (child != null && child.gameObject.activeSelf)
            {
                ForceRebuildLayoutRecursive(child);
            }
        }

        LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
    }

#if UNITY_EDITOR
    protected override void OnValidate()
    {
        base.OnValidate();
        SetDirty();
    }
#endif

}