- Home /
 
UI Toolkit - Text Best Fit
Is there a way to make the text fit the container (by automatically adjusting its font) using the new UI Toolkit and UI Builder?
Answer by ZakisGrigoroudis · Oct 24, 2021 at 06:31 PM
Thank you @andrew-lukasik, I made a new script following your steps that seems to fit more properly to my needs. It is not perfect though as it doesn't refresh properly in a few cases. Any improvements are welcome.
 using System.Collections.Generic;
 using UnityEngine;
 using UnityEngine.UIElements;
 
 public class LabelAutoFit : Label
 {
     [UnityEngine.Scripting.Preserve]
     public new class UxmlFactory : UxmlFactory<LabelAutoFit, UxmlTraits> { }
 
     [UnityEngine.Scripting.Preserve]
     public new class UxmlTraits : Label.UxmlTraits
     {
         readonly UxmlIntAttributeDescription minFontSize = new UxmlIntAttributeDescription
         {
             name = "min-font-size",
             defaultValue = 10,
             restriction = new UxmlValueBounds {min = "1"}
         };
 
         readonly UxmlIntAttributeDescription maxFontSize = new UxmlIntAttributeDescription
         {
             name = "max-font-size",
             defaultValue = 200,
             restriction = new UxmlValueBounds {min = "1"}
         };
 
         public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription { get { yield break; } }
 
         public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
         {
             base.Init(ve, bag, cc);
 
             LabelAutoFit instance = ve as LabelAutoFit;
             instance.minFontSize = Mathf.Max(minFontSize.GetValueFromBag(bag, cc), 1);
             instance.maxFontSize = Mathf.Max(maxFontSize.GetValueFromBag(bag, cc), 1);
             instance.RegisterCallback<GeometryChangedEvent>(instance.OnGeometryChanged);
             instance.style.fontSize = 1; // Triggers OnGeometryChanged callback
         }
     }
 
     // Setting a limit of max text font refreshes from a single OnGeometryChanged to avoid repeating cycles in some extreme cases
     private const int MAX_FONT_REFRESHES = 2;
 
     private int m_textRefreshes = 0;
 
     public int minFontSize { get; set; }
     public int maxFontSize { get; set; }
 
     // Call this if the font size does not update by just setting the text
     // Should probably wait till the end of frame to get the real font size, instead of using this method
     public void SetText(string text)
     {
         this.text = text;
         UpdateFontSize();
     }
 
     private void OnGeometryChanged(GeometryChangedEvent evt)
     {
         UpdateFontSize();
     }
 
     private void UpdateFontSize()
     {
         if (m_textRefreshes < MAX_FONT_REFRESHES)
         {
             Vector2 textSize = MeasureTextSize(text, float.MaxValue, MeasureMode.AtMost, float.MaxValue, MeasureMode.AtMost);
             float fontSize = Mathf.Max(style.fontSize.value.value, 1); // Unity can return a font size of 0 which would break the auto fit // Should probably wait till the end of frame to get the real font size
             float heightDictatedFontSize = Mathf.Abs(contentRect.height);
             float widthDictatedFontSize = Mathf.Abs(contentRect.width / textSize.x) * fontSize;
             float newFontSize = Mathf.FloorToInt(Mathf.Min(heightDictatedFontSize, widthDictatedFontSize));
             newFontSize = Mathf.Clamp(newFontSize, minFontSize, maxFontSize);
             if (Mathf.Abs(newFontSize - fontSize) > 1)
             {
                 m_textRefreshes++;
                 style.fontSize = new StyleLength(new Length(newFontSize));
             }
         }
         else
         {
             m_textRefreshes = 0;
         }
     }
 }
 
              Answer by andrew-lukasik · Oct 16, 2021 at 07:33 PM
Here is my half-working prototype for a custom Label element that attempts to solve exactly that.
 
It's incomplete and with potential bugs (just a prototype), also with a requirement that you do not use flex grow (it produces undefined behaviour (recursion)) and use Size/Width [%] or Height [%] instead.

 // src* = https://gist.github.com/andrew-raphael-lukasik/8f65a4d7055e29f80376bcb4f9b500af
 using UnityEngine;
 using UnityEngine.UIElements;
 
 // IMPORTANT NOTE:
 // This elemeent doesn't work with flexGrow as it leads to undefined behaviour (recursion).
 // Use Size/Width[%] and Size/Height attributes</b> instead
 
 [UnityEngine.Scripting.Preserve]
 public class LabelAutoFit : UnityEngine.UIElements.Label
 {
 
     public Axis axis { get; set; }
     public float ratio { get; set; }
 
     [UnityEngine.Scripting.Preserve]
     public new class UxmlFactory : UxmlFactory<LabelAutoFit,UxmlTraits> {}
     
     [UnityEngine.Scripting.Preserve]
     public new class UxmlTraits : Label.UxmlTraits// VisualElement.UxmlTraits
     {
         UxmlFloatAttributeDescription _ratio = new UxmlFloatAttributeDescription{
             name = "ratio" ,
             defaultValue = 0.1f ,
             restriction = new UxmlValueBounds{ min="0.0" , max="0.9" , excludeMin=false , excludeMax=true }
         };
         UxmlEnumAttributeDescription<Axis> _axis = new UxmlEnumAttributeDescription<Axis>{
             name = "ratio-axis" ,
             defaultValue = Axis.Horizontal
         };
         public override void Init ( VisualElement ve , IUxmlAttributes bag , CreationContext cc )
         {
             base.Init( ve , bag , cc );
 
             LabelAutoFit instance = ve as LabelAutoFit;
             instance.RegisterCallback<GeometryChangedEvent>( instance.OnGeometryChanged );
 
             instance.ratio = _ratio.GetValueFromBag( bag , cc );
             instance.axis = _axis.GetValueFromBag( bag , cc );
             instance.style.fontSize = 1;// triggers GeometryChangedEvent
         }
     }
 
     void OnGeometryChanged ( GeometryChangedEvent evt )
     {
         float oldRectSize = this.axis==Axis.Vertical ? evt.oldRect.height : evt.oldRect.width;
         float newRectLenght = this.axis==Axis.Vertical ? evt.newRect.height : evt.newRect.width;
         
         float oldFontSize = this.style.fontSize.value.value;
         float newFontSize = newRectLenght * this.ratio;
         
         float fontSizeDelta = Mathf.Abs( oldFontSize - newFontSize );
         float fontSizeDeltaNormalized = fontSizeDelta / Mathf.Max(oldFontSize,1);
 
         if( fontSizeDeltaNormalized>0.01f )
             this.style.fontSize = newFontSize;
     }
 
     public enum Axis { Horizontal , Vertical }
 
 }
 
               If sb knows how to improve on that, please do share your code too.
Your answer