- 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 }
}