Skip to content

Instantly share code, notes, and snippets.

@neoRiley
Last active May 27, 2025 19:42
Show Gist options
  • Save neoRiley/44c8836942272c8456d647449130e562 to your computer and use it in GitHub Desktop.
Save neoRiley/44c8836942272c8456d647449130e562 to your computer and use it in GitHub Desktop.
`ScalableFontLabel.cs` is a subclass of TextElement (Label etc) and gives you extra properties to dial in your font scaling. If you just want to create it with code, I've added an extension method. Be sure to grab all 3 files regardless. Add the 3 files to your project, and you should be able to add via the UI Builder IDE
using System;
using UnityEngine.UIElements;
using Utils.Extensions;
namespace N30R1L37.UI
{
[UxmlElement]
public partial class ScalableFontLabel : TextElement, IDisposable
{
private TextElementScaler _scaler;
private TextScaleMode _scaleMode = TextScaleMode.Both;
[UxmlAttribute("scale-direction")]
public TextScaleMode ScaleMode
{
get => _scaleMode;
set
{
_scaleMode = value;
}
}
private float _scale = 1.0f;
[UxmlAttribute("scale")]
public float Scale
{
get => _scale;
set
{
_scale = Math.Max(0.01f, value);
if (_scaler == null) return;
_scaler.SetScale(value);
}
}
private float _charWidth = 0.695f;
[UxmlAttribute("char-width")]
public float CharWidth
{
get => _charWidth;
set
{
_charWidth = Math.Clamp(value, 0.5f, 0.695f);
if (_scaler == null) return;
_scaler.SetCharWidth(value);
}
}
private float _charHeight = 1;
[UxmlAttribute("char-height")]
public float CharHeight
{
get => _charHeight;
set
{
_charHeight = Math.Clamp(value, 0.5f, 1f);
if (_scaler == null) return;
_scaler.SetCharHeight(value);
}
}
public ScalableFontLabel()
{
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
if (evt == null) return;
_scaler = this.EnableScaling(this, Scale, ScaleMode);
}
private void OnDetachFromPanel(DetachFromPanelEvent evt)
{
Dispose();
}
public void Dispose()
{
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
}
}
}
namespace N30R1L37.UI
{
using UnityEngine;
using UnityEngine.UIElements;
public enum TextScaleMode
{
Both,
WidthOnly,
HeightOnly
}
public class TextElementScaler
{
private VisualElement _parent;
private TextElement _textElement;
private float _scale;
private TextScaleMode _scaleMode;
private float _baseFontSize;
// Heuristic estimate for character dimensions at fontSize = 1
private float _charWidth = 0.695f; // adjust if needed per font: 0.5f to 0.695f is acceptable
private float _charHeight = 1f; // adjust if needed per font
public TextElementScaler(VisualElement parent, TextElement textElement, float scale, TextScaleMode scaleMode)
{
_parent = parent;
_textElement = textElement;
_scale = scale;
_scaleMode = scaleMode;
if (_textElement.style.fontSize.value.value > 0)
{
_baseFontSize = _textElement.style.fontSize.value.value;
}
else
{
_textElement.style.fontSize = 14f;
_baseFontSize = 14f;
}
_textElement.style.flexGrow = 1f;
_textElement.style.flexShrink = 0f;
_textElement.style.alignSelf = Align.Stretch;
_parent.RegisterCallback<GeometryChangedEvent>(OnParentResized);
UpdateFontSize();
// 🔐 Store this scaler in userData to prevent GC
_textElement.userData = this;
}
public void SetScale(float value)
{
_scale = value;
_parent.MarkDirtyRepaint();
UpdateFontSize();
}
public void SetCharWidth(float charWidth)
{
_charWidth = charWidth;
_parent.MarkDirtyRepaint();
UpdateFontSize();
}
public void SetCharHeight(float charHeight)
{
_charHeight = charHeight;
_parent.MarkDirtyRepaint();
UpdateFontSize();
}
private void OnParentResized(GeometryChangedEvent evt)
{
UpdateFontSize();
}
private void UpdateFontSize()
{
if (string.IsNullOrEmpty(_textElement.text))
return;
// Drawable area of the element itself
float elementWidth = _textElement.resolvedStyle.width
- _textElement.resolvedStyle.paddingLeft
- _textElement.resolvedStyle.paddingRight
- _textElement.resolvedStyle.borderLeftWidth
- _textElement.resolvedStyle.borderRightWidth;
float elementHeight = _textElement.resolvedStyle.height
- _textElement.resolvedStyle.paddingTop
- _textElement.resolvedStyle.paddingBottom
- _textElement.resolvedStyle.borderTopWidth
- _textElement.resolvedStyle.borderBottomWidth;
// Count real characters (no line breaks)
string text = _textElement.text.Replace("\n", "");
int totalCharCount = text.Length;
// Estimate chars that can fit per line
int maxCharsPerLine = Mathf.Max(1, Mathf.FloorToInt(elementWidth / (_charWidth * _baseFontSize)));
// Estimate number of lines based on wrap + \n
int manualLineBreaks = _textElement.text.Split('\n').Length - 1;
int estimatedWrappedLines = Mathf.CeilToInt(totalCharCount / (float)maxCharsPerLine);
int totalLines = Mathf.Max(1, estimatedWrappedLines + manualLineBreaks);
// Height must now be divided by line count
float maxFontSizeByHeight = elementHeight / (totalLines * _charHeight);
// Width constraint remains the same
float maxFontSizeByWidth = elementWidth / (totalCharCount * _charWidth);
float maxFontSize = 0f;
switch (_scaleMode)
{
case TextScaleMode.WidthOnly:
maxFontSize = Mathf.Min(maxFontSizeByWidth, maxFontSizeByHeight);
break;
case TextScaleMode.HeightOnly:
maxFontSize = maxFontSizeByHeight;
break;
case TextScaleMode.Both:
maxFontSize = Mathf.Min(maxFontSizeByWidth, maxFontSizeByHeight);
break;
}
float scaledFontSize = maxFontSize * _scale;
scaledFontSize = Mathf.Clamp(scaledFontSize, 1f, 200f);
_textElement.style.fontSize = scaledFontSize;
}
}
}
using N30R1L37.UI;
using UnityEngine.UIElements;
namespace Utils.Extensions
{
public static class TextScalerExtensions
{
public static TextElementScaler EnableScaling(this TextElement element, VisualElement parent, float scale, TextScaleMode mode)
{
return new TextElementScaler(parent, element, scale, mode);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment