Limit Max Width of Layout Component?

Hi! I am probably going to hack something together with parenting like suggested above, but in my case, it means rebuilding my whole (already rather complicated) UI hierarchy containing ScrollViews, masks, and various layout groups. There should really be an option for Max height/width.

Edit: Actually, couldn’t figure out how to set up the hierarchy to make it work. Definitely cannot replicate any of the results shown above. Instead, I wrote this simple script that does just the resizing and nothing else:

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

[ExecuteInEditMode]
public class ResizeRectBasedOnHDelta : MonoBehaviour
{
    public float maxHeight = 580f;

    public RectTransform rtChild;
    public RectTransform rtParent;

    void Update()
    {
        rtParent.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, Mathf.Min(maxHeight, rtChild.rect.height));
    }
}

Wouldn’t this cause a rebuild of the UI every frame since you’re setting the size every frame?
That would be bad for performance (depending on the UI, but still bad for performance if it does dirty the canvas every frame due to this).

You could also create a script inheriting from UIBehaviour and use OnRectTransformDimensionsChange
Check the size of the panel and if it is above the limit disable the content size fitter.
Or have a child with OnRectTransformDimensionChange to call the parent to check whether it should enable or disable content size fitting or not.

1 Like

I like the idea of detecting a dimensions change, but I can’t figure out how to use OnRectTransformDimensionsChange. It doesn’t appear to be one of Unity’s magic functions like Update or a delegate. Do you think you could give an example, @MaskedMouse ?

Update: I tried doing this on the child, but it crashes Unity every time.

[ExecuteInEditMode]
public class ResizeRectBasedOnHDelta : UnityEngine.EventSystems.UIBehaviour
{
    public float maxHeight = 580f;

    public RectTransform rtChild;
    public RectTransform rtParent;

    override protected void OnRectTransformDimensionsChange()
    {
        rtParent.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, Mathf.Min(maxHeight, rtChild.rect.height));
    }
}

Because you’re introducing a loop now.
OnRectTransformDimensionsChange is called when the width / height of a rect transform changes. i.e. when your children are expanding this method will be called.
Since you’re setting the size with SetSizeWithCurrentAnchors, you’re causing a change in width and height and thus call OnRectTransformDimensionsChange again. It is supposed to be a event based call (more efficient) where you check whether the width / height is higher than a maximum. If so then stop expanding by disabling the Content Size Fitter. Not to set the size again.

I’m usually focussed on performance of UI. Since UGUI isn’t the best in performance to start with. With larger UI setups things tend to get slow rather quickly.

The problem with this script is, when it has reached the maximum it won’t shrink back.

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

namespace UIExtensions
{
    [ExecuteInEditMode]
    [RequireComponent(typeof(RectTransform), typeof(ContentSizeFitter))]
    public class MaxExpandableRect : UIBehaviour
    {
        public Vector2 MaximumSize;
        public ContentSizeFitter SizeFitter;
   
        protected override void OnRectTransformDimensionsChange()
        {
            var rectTransform = (RectTransform) transform;

            // If it reached the maximum size Horizontally
            if (rectTransform.sizeDelta.x > MaximumSize.x)
            {
                SizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
                rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, MaximumSize.x);           
            }
       
            // If it reached the maximum size Vertically
            if (rectTransform.sizeDelta.y > MaximumSize.y)
            {
                SizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained;
                rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, MaximumSize.y);
            }
        }
    }
}

Then another solution would be creating your own ContentSizeFitter ofcourse. Because the problem is the content size fitter itself not defining a maximum size. Only a preferred size.

A simple copy pasta of the original ContentSizeFitter renamed to Extension.
I’ve changed it up a bit conform coding standards that I hold myself to.
I’ve removed the SetPropertyUtility.SetStruct calls because they are internal methods they can’t be used. But afaik all it does is checking if the enum has changed or not. If it has, set it dirty.
Then when the HandleSelfFittingAlongAxis is being called, clamp the value of the preferred maximum size.

edit: noticed a few refactoring errors.

using UnityEngine.EventSystems;

namespace UnityEngine.UI
{
    [AddComponentMenu("Layout/Content Size Fitter With Max", 141)]
    [ExecuteAlways]
    [RequireComponent(typeof(RectTransform))]
    public class ContentSizeFitterExtension : UIBehaviour, ILayoutSelfController
    {
        /// <summary>
        /// The size fit modes avaliable to use.
        /// </summary>
        public enum FitMode
        {
            /// <summary>
            /// Don't perform any resizing.
            /// </summary>
            Unconstrained,

            /// <summary>
            /// Resize to the minimum size of the content.
            /// </summary>
            MinSize,

            /// <summary>
            /// Resize to the preferred size of the content.
            /// </summary>
            PreferredSize
        }

        [SerializeField] 
        protected FitMode horizontalFit = FitMode.Unconstrained;

        /// <summary>
        /// The fit mode to use to determine the width.
        /// </summary>
        public FitMode HorizontalFit
        {
            get => horizontalFit;
            set
            {
                if (horizontalFit == value) return;
                horizontalFit = value;
                SetDirty();
            }
        }

        [SerializeField]
        protected FitMode verticalFit = FitMode.Unconstrained;

        /// <summary>
        /// The fit mode to use to determine the height.
        /// </summary>
        public FitMode VerticalFit
        {
            get => verticalFit;
            set
            {
                if (verticalFit == value) return;
                verticalFit = value;
                SetDirty();
            }
        }

        [Tooltip("Maximum Preferred size when using Preferred Size")]
        public Vector2 MaximumPreferredSize;

        [System.NonSerialized] 
        private RectTransform rectTransform;

        private RectTransform RectTransform
        {
            get
            {
                if (rectTransform == null)
                    rectTransform = GetComponent<RectTransform>();
                return rectTransform;
            }
        }

        private DrivenRectTransformTracker tracker;

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

        protected override void OnDisable()
        {
            tracker.Clear();
            LayoutRebuilder.MarkLayoutForRebuild(RectTransform);
            base.OnDisable();
        }

        protected override void OnRectTransformDimensionsChange()
        {
            SetDirty();
        }

        private void HandleSelfFittingAlongAxis(int axis)
        {
            var fitting = (axis == 0 ? HorizontalFit : VerticalFit);
            if (fitting == FitMode.Unconstrained)
            {
                // Keep a reference to the tracked transform, but don't control its properties:
                tracker.Add(this, RectTransform, DrivenTransformProperties.None);
                return;
            }

            tracker.Add(this, RectTransform, (axis == 0 ? DrivenTransformProperties.SizeDeltaX : DrivenTransformProperties.SizeDeltaY));

            switch (fitting)
            {
                // Set size to min or preferred size
                case FitMode.MinSize:
                    RectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis) axis, LayoutUtility.GetMinSize(rectTransform, axis));
                    break;

                case FitMode.PreferredSize:
                    RectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis) axis, Mathf.Clamp(LayoutUtility.GetPreferredSize(rectTransform, axis), 0, axis == 0 ? MaximumPreferredSize.x : MaximumPreferredSize.y));
                    break;
            }
        }

        /// <summary>
        /// Calculate and apply the horizontal component of the size to the RectTransform
        /// </summary>
        public virtual void SetLayoutHorizontal()
        {
            tracker.Clear();
            HandleSelfFittingAlongAxis(0);
        }

        /// <summary>
        /// Calculate and apply the vertical component of the size to the RectTransform
        /// </summary>
        public virtual void SetLayoutVertical()
        {
            HandleSelfFittingAlongAxis(1);
        }

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

            LayoutRebuilder.MarkLayoutForRebuild(RectTransform);
        }

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

    #endif
    }
}
1 Like

But PLEASE, never put a tutorial video into a gif - you can’t pause or rewind it!!!

3 Likes

Alright, let’s not get to 2021.

It’s actually a very simple fix.

The solution is to create your own content size fitter class by extending the existing one, except your custom class has a max width and height.

You need two files: one for the component itself, and another for its editor.

I called mine ContentSizeFitterWithMax, but pick whichever name you like.

ContentSizeFitterWithMax.cs:

using System;

namespace UnityEngine.UI
{
    [AddComponentMenu("Layout/Content Size Fitter With Max", 141)]
    [ExecuteAlways]
    [RequireComponent(typeof(RectTransform))]
    public class ContentSizeFitterWithMax : ContentSizeFitter
    {
        [NonSerialized]
        private RectTransform m_Rect;

        private RectTransform rectTransform
        {
            get
            {
                if (m_Rect == null)
                {
                    m_Rect = GetComponent<RectTransform>();
                }

                return m_Rect;
            }
        }

        [SerializeField]
        private float m_MaxWidth = -1;

        public float maxWidth
        {
            get => m_MaxWidth;
            set => m_MaxWidth = value;
        }

        [SerializeField]
        private float m_MaxHeight = -1;

        public float maxHeight
        {
            get => m_MaxHeight;
            set => m_MaxHeight = value;
        }

        public override void SetLayoutHorizontal()
        {
            base.SetLayoutHorizontal();

            if (maxWidth > 0)
            {
                if (horizontalFit == FitMode.MinSize)
                {
                    rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Mathf.Min(LayoutUtility.GetMinSize(m_Rect, 0), maxWidth));
                }
                else if (horizontalFit == FitMode.PreferredSize)
                {
                    rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Mathf.Min(LayoutUtility.GetPreferredSize(m_Rect, 0), maxWidth));
                }
            }
        }

        public override void SetLayoutVertical()
        {
            base.SetLayoutVertical();

            if (maxHeight > 0)
            {
                if (verticalFit == FitMode.MinSize)
                {
                    rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, Mathf.Min(LayoutUtility.GetMinSize(m_Rect, 1), maxHeight));
                }
                else if (verticalFit == FitMode.PreferredSize)
                {
                    rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, Mathf.Min(LayoutUtility.GetPreferredSize(m_Rect, 1), maxHeight));
                }
            }
        }
    }
}

ContentSizeFitterWithMaxEditor.cs:

using UnityEngine.UI;

namespace UnityEditor.UI
{
    [CustomEditor(typeof(ContentSizeFitterWithMax), true)]
    [CanEditMultipleObjects]
    public class ContentSizeFitterWithMaxEditor : ContentSizeFitterEditor
    {
        SerializedProperty m_MaxWidth;

        SerializedProperty m_MaxHeight;

        protected override void OnEnable()
        {
            base.OnEnable();
            m_MaxWidth = serializedObject.FindProperty("m_MaxWidth");
            m_MaxHeight = serializedObject.FindProperty("m_MaxHeight");
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            EditorGUILayout.PropertyField(m_MaxWidth, true);
            EditorGUILayout.PropertyField(m_MaxHeight, true);
            serializedObject.ApplyModifiedProperties();

            base.OnInspectorGUI();
        }
    }
}

Proof that it works: I set my max width to 400 on this dialogue choice text, so short choices will expand, but longer choices will wrap, exactly like requested by OP.

2h5q88

24 Likes

Unfortunately…
Using the content size fitter is nice, but once you’re already working with a Layout Group, you can’t use a content size fitter without potentially running into issues. I’m bumping into this once again with my UI, this time more specifically I just want to limit the width of a text field that is long, but I also don’t want it to force the parent layout group to be wider than it needs to be. The only way this works is if there is some max width solution that works within a layout group. I’m fiddling around with it, and I’ll try to remember to come back and follow up if I come up with a decent workaround.

Update:
I did get something working for my needs. It was kind of particular though so I’ll explain the scenario:
I have a horizontally nesting menu that scrolls vertically, but each menu is sized horizontally according to its widest layout item. This is making use of nested horizontal and vertical layout groups, with a large flexible width for the empty area that essentially pushes the menus down to the smallest width they can be without truncating text.
BUT there are some text labels I want to wrap when they get a certain width, but if the preferred size is smaller we should use that (so we don’t unnecessary increase the width of the menu), but the width can also increase up to the width of the parent RectTransform’s width (so that we aren’t awkwardly word wrapping to prevent from making the menu wider than it needs to be if the menu is already fairly wide). It’s a little awkward, so perhaps a screenshot will better explain:

The small explainer text is the bit in question. You can see how it wants to at least be a little bigger than the contents of the menu would be without it, but if left to expand as much as it would like, it would cause that menu to take up the entirety of the width left. So what I ended up doing was creating a LayoutGroup for it and attached this UIBehaviour I made based on the default implementation of LayoutElement:

    [ExecuteInEditMode]
    [RequireComponent(typeof(RectTransform))]
    public class MaxWidthLayoutElement : UIBehaviour, ILayoutElement {
        private float m_PreferredWidth;
      
        public float MaxForcedWidth;
        public LayoutGroup LayoutGroup;

        public virtual float minWidth => -1;
        public virtual float minHeight => -1;
        public virtual float preferredWidth => m_PreferredWidth;
        public virtual float preferredHeight => -1;
        public virtual float flexibleWidth => -1;
        public virtual float flexibleHeight => -1;
        public virtual int layoutPriority => 1;

        public virtual void CalculateLayoutInputHorizontal() { }
        public virtual void CalculateLayoutInputVertical() { }

        #region Unity Lifetime calls

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

        protected override void OnTransformParentChanged() {
            LayoutGroup.CalculateLayoutInputHorizontal();
            var preferredWidth = LayoutGroup.preferredWidth;
            var parentWidth = ((RectTransform)transform.parent).rect.width;
            m_PreferredWidth = math.max(parentWidth, math.min(preferredWidth, MaxForcedWidth));
            SetDirty();
        }

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

        protected override void OnDidApplyAnimationProperties() {
            SetDirty();
        }

        protected override void OnBeforeTransformParentChanged() {
            SetDirty();
        }

        #endregion

        protected void SetDirty() {
            if (!IsActive()) { return; }
            LayoutRebuilder.MarkLayoutForRebuild(transform as RectTransform);
        }

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

        #endif
    }

It feels a little awkward, but it seems to work nicely in all use cases.

It’s 2021 and still no easy way to do this, but [mention|veD02kxt7fIjJUpxGtWfaw==]'s post saves another day, this is a working solution, thank you!

I managed to make this.

A background picture as parent, with layout group controlling child size, and thanks to @LazloBonin , his [quote=“LazloBonin, post:46, topic: 575269, username:LazloBonin”]
ContentSizeFitterWithMax.cs:
[/quote]
7217245--867667--Background panel.png

A ScrollRect, as one of children of background pic, with one layout group controlling child size. I only set Content and Permanent Scrollbar for Scroll Rect. Please do not assign Viewport.
7217245--867670--ScrollRect.png

A mask as viewport. But not assigned in ScrollRect. With Mask for world space UI, or Rect Mask 2D for Overlay/Screen Space, and image required by mask. Layout Element. A Script to get preferred layout width and height.
7217245--867673--ViewPort.png 7217245--867676--ViewPort-Maxed.png
SimpleGetPreferredChildLayout.cs

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

[RequireComponent(typeof(LayoutElement))]
[ExecuteAlways]
public class SimpleGetPreferredChildLayout : UIBehaviour
{
    [SerializeField]
    private LayoutElement m_layout;

    private LayoutElement Layout
    {
        get
        {
            if (m_layout == null) m_layout = GetComponent<LayoutElement>();
            return m_layout;
        }
    }

    [SerializeField]
    private TMP_Text m_TextComponent;

    private TMP_Text TextComponent
    {
        get
        {
            if (m_TextComponent == null) m_TextComponent = GetComponentInChildren<TMP_Text>();
            return m_TextComponent;
        }
    }

    private RectTransform m_textRect;

    private RectTransform TextRect
    {
        get
        {
            if (m_textRect == null) m_textRect = TextComponent.GetComponent<RectTransform>();
            return m_textRect;
        }
    }

    protected override void OnEnable()
    {
        base.OnEnable();
        // Subscribe to event fired when text object has been regenerated.
        TMPro_EventManager.TEXT_CHANGED_EVENT.Add(ON_TEXT_CHANGED);
    }

    protected override void OnDisable()
    {
        base.OnDisable();
        TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(ON_TEXT_CHANGED);
    }

    protected void ON_TEXT_CHANGED(Object obj)
    {
        if (obj == TextComponent)
            OnRectTransformDimensionsChange();
    }

    protected override void OnRectTransformDimensionsChange()
    {
        base.OnRectTransformDimensionsChange();

        if (transform.childCount != 1) return;
        Layout.preferredHeight = LayoutUtility.GetPreferredHeight(TextRect);
        Layout.preferredWidth = LayoutUtility.GetPreferredWidth(TextRect);
    }
}

A Text Mesh UGUI as content with basic content size fitter, Vertical Fit set to Preferred Size. RectTransform’s anchor stretches on the top.
7217245--867679--Content.png

Hey all, just wanted to share my solution. I’m quite happy with its simplicity, and it also works in edit mode. Add this component beside your ContentSizeFitter + layout element or group (e.g. VerticalLayoutGroup):

using UnityEngine;
using UnityEngine.UI;

[ExecuteAlways]
[RequireComponent(typeof(ContentSizeFitter))]
public class ContentSizeFitterMaxWidth : MonoBehaviour
{
    public float maxWidth;

    RectTransform _rtfm;
    ContentSizeFitter _fitter;
    ILayoutElement _layout;

    void OnEnable()
    {
        _rtfm = (RectTransform)transform;
        _fitter = GetComponent<ContentSizeFitter>();
        _layout = GetComponent<ILayoutElement>();
    }

    void Update()
    {
        _fitter.horizontalFit = _layout.preferredWidth > maxWidth
            ? ContentSizeFitter.FitMode.Unconstrained
            : ContentSizeFitter.FitMode.PreferredSize;
       
        if (_layout.preferredWidth > maxWidth)
        {
            _fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
            _rtfm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, maxWidth);
        }
        else
            _fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
    }

    void OnValidate() => OnEnable();
}
1 Like

This worked nicely for me! Thanks for sharing, @bilalakil

2 Likes

I know that setting max Widths on a Layout Component are no suppoused to work on every resolution but, in case you want to maintain the “ratio” of a predefined width in a predefined resolution, on the rest of resolutions, and for whatever reasons you can’t use anchors (maybe you are restricted by parents or children behaviours), here is my modification on @bilalakil solution:

using UnityEngine;
using UnityEngine.UI;

[ExecuteAlways]
[RequireComponent(typeof(ContentSizeFitter))]
public class ContentSizeFitterMaxWidth : MonoBehaviour
{
    public float maxWidth;

    private float widthRatio;
    private RectTransform _rtfm;
    private ContentSizeFitter _fitter;
    private ILayoutElement _layout;

    private void OnValidate() => OnEnable();

    private void OnEnable()
    {
        _rtfm = (RectTransform)transform;
        _fitter = GetComponent<ContentSizeFitter>();
        _layout = GetComponent<ILayoutElement>();

        widthRatio = maxWidth / Screen.width;
    }

    private void Update()
    {
        maxWidth = widthRatio * Screen.width;

        _fitter.horizontalFit = _layout.preferredWidth > maxWidth
            ? ContentSizeFitter.FitMode.Unconstrained
            : ContentSizeFitter.FitMode.PreferredSize;

        if (_layout.preferredWidth > maxWidth)
        {
            _fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
            _rtfm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, maxWidth);
        }
        else
            _fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
    }

}

I am sorry to bump this. I need this, BUT instead of wrapping the text to a new line, I want to automatically shorten it with an Ellipsis. Doesn’t work.

I can confirm that this super simple suggestion solved my problem without writing any custom code.

EDIT

I have spoken too early. It didn’t work :frowning:

This worked for me. Thanks @bilalakil !

2 Likes

Short story: This solution worked for me. I used @LazloBonin 's version because it supports both horizontal and vertical maximum limits.

Long story:
I came back to this problem (and this forum thread) again and again over the years while working on different projects. My goal was to find the perfect solution without writing a custom script and by simply trying to understand how Unity layout system works. Today, I think, I fully figured it out and noticed that it’s not possible without writing a custom ContentSizeFitter. So, I gave up on my goal and started using @LazloBonin 's version.

Thank you very much.

2 Likes

Huge help, thank you so much!

1 Like

This worked for me :
If parent contains Horizontal or Vertical Layout Group, Enable ‘Control Child Size’ and Disable ‘Child Force Expand’, then add ‘Layout Element’ to child and set Preferred Height/Width.

1 Like

Thank you, you are a saint.

For anyone using this on a newer version of unity, the Control Child Size need to be checked for Width and Height for both vertical layout groups.

I found something working for me, I try to explain (it’s really easy to reproduct):

Make one gameObject :
stretch : (free = blue cross) don’t know if it matter, anyway;
Anchors : Min X & Y = 0; Max X &Y = 1;
Pivot : X = 0; Y = 0,5;
*T
extMeshPro
* with Alignement Left;
*Content Size Fitter : Horizontal = Preferred Size; Vertical = Unconstrained;
*Horizontal Layout Group : Child Align = Upper Left; Control Child Size = Width; (Left = 13; Spacing = 4 but not really important)

Then, make 3 gameObject child one the first:
For All :
*stretch : top left
*Anchors : Min X = 0 & Y = 1; Max X = 0 & Y = 1;
*Pivot : X = 0,5; Y = 0,5;
*TextMeshPro with >For 1rst Go : (for the example write : 50) & Alignement Right;
For 2nd Go : (for the example write : /) & Alignement Right;
For 3rd Go : (for the example write : 50) & Alignement Left;

That’s it. Now just change the value on the textMeshpro of the 1rst Gameobject(of child), just add some 0000 and see the natural extension that you want.