Is there auto font size like in Unity

Godot 4.2.1

Is there any built in features that make it so when text is too big for a label it shrinks to fit the label like the “Auto Font Size” feature in Unity. Or any code that would do something similar?

EDIT: mrcdk’s solution worked well for single line text, but I wanted it to work on multi line text, so I created this solution using what I had learned from mrcdk’s solution (namely the _set() function):

EDIT 2: I still prefer my solution for multi line text, but eclextic has a better single line solution below.

class_name AutoFontSizeLabel extends Label
## A label that will scale the font size to fit its size, betweem the min and max sizes.

## Smallest possible font size text can be in the label.
@export var minimum_size := 10

## Largest possible font size text can be in the label, also default font size when there is not much text.
@export var maximum_size := 20


func _ready():
	# Clip text must be true otherwise label just resizes rather than hiding lines,
	# which the update_font_size function relies on to resize font.
	clip_text = true


func _set(property, value):
	if property == "text" and text != value:
		text = value
		add_theme_font_size_override("font_size", maximum_size)
		update_font_size()
		return true
	
	return false


func update_font_size():
	var font_size = get_theme_font_size("font_size")
	
	while get_visible_line_count() < get_line_count():
		font_size -= 1
		
		add_theme_font_size_override("font_size", font_size)

No, not out of the box.

I did an implementation to answer a similar question:

@tool
class_name AutoSizeLabel extends Label


@export var max_font_size = 56


func _ready() -> void:
	clip_text = true
	item_rect_changed.connect(_on_item_rect_changed)


func _set(property: StringName, value: Variant) -> bool:
	match property:
		"text":
			# listen for text changes
			update_font_size()

	return false


func update_font_size() -> void:
	var font = get_theme_font("font")
	var font_size = get_theme_font_size("font_size")

	var line = TextLine.new()
	line.direction = text_direction
	line.flags = justification_flags
	line.alignment = horizontal_alignment

	for i in 20:
		line.clear()
		var created = line.add_string(text, font, font_size)
		if created:
			var text_size = line.get_line_width()

			if text_size > floor(size.x):
				font_size -= 1
			elif font_size < max_font_size:
				font_size += 1
			else:
				break
		else:
			push_warning('Could not create a string')
			break

	add_theme_font_size_override("font_size", font_size)


func _on_item_rect_changed() -> void:
	update_font_size()
2 Likes

Could you explain why you loop 20 times specifically ?

There was no reason at all, I just chose a number of iterations at random. You can tweak it and use whatever number you want.

1 Like

Here is an improved, iterative algorithm relying on the same method, just using the divide and conquer approach. This approach results in us getting between 1-9 iterations at any time, with any size value and any text size length, while also not having a constant iteration factor like 20 in the previous comment. It also supports a minimum font size.

I used RichTextLabel here, but you quite literally only need to change the extends RichTextLabel to extends Label and you would be ready to go.


AutoSizer.gd:

# AutoSizer is just a class to remove the hassle of code duplication. Feel free to remove it, if you only need one type of Label.
class_name AutoSizer

static func update_font_size_label(label: AutoSizeLabel) -> void:
	_update_font_size(label, "font", "font_size", Vector2i(label.min_font_size, label.max_font_size), label.text)

static func update_font_size_richlabel(label: AutoSizeRichLabel) -> void:
	_update_font_size(label, "normal_font", "normal_font_size", Vector2i(label.min_font_size, label.max_font_size), label.text)

static func _update_font_size(label: Control, font_name: StringName, font_style_name: StringName, font_size_range: Vector2i, text: String) -> void:
	var font := label.get_theme_font(font_name)

	var line := TextLine.new()
	line.direction = label.text_direction as TextServer.Direction
	line.flags = TextServer.JUSTIFICATION_NONE
	line.alignment = HORIZONTAL_ALIGNMENT_LEFT
	
	while true:
		line.clear()
		
		var mid_font_size := font_size_range.x + roundi((font_size_range.y - font_size_range.x) * 0.5)
		if !line.add_string(text, font, mid_font_size):
			push_warning("Could not create a string!")
			return
		
		var text_width := line.get_line_width()
		if text_width >= floori(label.size.x):
			if font_size_range.y == mid_font_size:
				break
			
			font_size_range.y = mid_font_size
		
		if text_width < floori(label.size.x):
			if font_size_range.x == mid_font_size:
				break
			
			font_size_range.x = mid_font_size
	
	label.add_theme_font_size_override(font_style_name, font_size_range.x)



AutoSizeLabel.gd:

@tool
class_name AutoSizeLabel extends Label

@export var min_font_size := 8 :
	set(v):
		min_font_size = clampi(v, 1, max_font_size)
		update()

@export var max_font_size := 56 :
	set(v):
		max_font_size = clampi(v, min_font_size, 191)
		update()

func _ready() -> void:
	clip_text = true
	item_rect_changed.connect(update)

func _set(property: StringName, value: Variant) -> bool:
	# Listen for changes to text
	if property == "text":
		text = value
		update()
		return true
	
	return false

func update() -> void:
	return AutoSizer.update_font_size_label(self)



AutoSizeRichLabel.gd:

@tool
class_name AutoSizeRichLabel extends RichTextLabel

@export var min_font_size := 8 :
	set(v):
		min_font_size = clampi(v, 1, max_font_size)
		update()

@export var max_font_size := 56 :
	set(v):
		max_font_size = clampi(v, min_font_size, 191)
		update()

func _ready() -> void:
	item_rect_changed.connect(update)

func _set(property: StringName, value: Variant) -> bool:
	# Listen for changes to text
	if property == "text":
		text = value
		update()
		return true
	
	return false

func update() -> void:
	return AutoSizer.update_font_size_richlabel(self)



PS: Sorry for necroposting, but searching on Google for “Godot Font Autosize” returns this back as the top result and I wanted to share a better solution.

1 Like

I just tested it out and is it supposed to only work for a single line? It works well, just not what I am thinking of.
Edit: Nevermind, I see you were improving the single line one

1 Like

Oh I was a little bit weirded by your unedited request, now I get it. Yes I did a little mistake for Multiline RichTexts. Instead of using TextLine for processing the width, you can use TextParagraph for a multiline solution. I’ll provide the fix shortly after this comment.

Edit:
@dinotor I’m sorry, after reworking the system a couple times, here is the actual multiline system, respecting word wrap, only actually going smaller once it truly becomes necessary (when the next line wouldn’t be visible anymore)… We get about a max of 8 iterations with mostly 3 iterations with this algorithm, depending on the min max font size values.
For some reason it doesn’t work as well, when using a FontVariation. I think there is no fix to that though… The backend of Godot would have to be adapted.

(Godots Text/Font System needs to be overhauled asap xd. It actually is horrible…)

AutoSizer.gd

class_name AutoSizer

static func get_text_paragraph() -> TextParagraph:
	var line := TextParagraph.new()
	line.justification_flags = TextServer.JUSTIFICATION_NONE
	line.alignment = HORIZONTAL_ALIGNMENT_LEFT
	
	return line

static func update_font_size_by_height(label: AutoSizeRichLabel) -> void:
	var font_size_range := Vector2i(label.min_font_size, label.max_font_size)
	var font := label.get_theme_font("normal_font")
	
	var paragraph := get_text_paragraph()
	paragraph.width = label.size.x
	paragraph.break_flags = TextServer.BREAK_MANDATORY | TextServer.BREAK_WORD_BOUND | TextServer.BREAK_ADAPTIVE
	
	while true:
		paragraph.clear()
		
		var mid_font_size := font_size_range.x + roundi((font_size_range.y - font_size_range.x) * 0.5)
		if !paragraph.add_string(label.text, font, mid_font_size):
			push_warning("Could not create a string!")
			return
		
		var text_height: int = paragraph.get_size().y
		
		if text_height > label.size.y:
			if font_size_range.y == mid_font_size:
				break
			
			font_size_range.y = mid_font_size
		
		if text_height <= label.size.y:
			if font_size_range.x == mid_font_size:
				break
			
			font_size_range.x = mid_font_size
	
	label.add_theme_font_size_override("normal_font_size", font_size_range.x)


AutoSizeRichLabel.gd

@tool
class_name AutoSizeRichLabel extends RichTextLabel

@export var min_font_size := 8 :
	set(v):
		min_font_size = clampi(v, 1, max_font_size)
		update()

@export var max_font_size := 56 :
	set(v):
		max_font_size = clampi(v, min_font_size, 191)
		update()

func _ready() -> void:
	item_rect_changed.connect(update)

func _set(property: StringName, value: Variant) -> bool:
	# Listen for changes to text
	if property == "text":
		text = value
		update()
		return true
	
	return false

func update() -> void:
	return AutoSizer.update_font_size_by_height(self)
1 Like

If people still checking how to do it for warped Label text auto sizing, instead of RichTextLabel Provided by @eclextic
here’s the copy of it
the “normal_font” from eclextic doesnt work for me, so i have to changed it to just “font”

AutoSizer.gd

class_name AutoSizer

static func get_text_paragraph() -> TextParagraph:
	var line := TextParagraph.new()
	line.justification_flags = TextServer.JUSTIFICATION_NONE
	line.alignment = HORIZONTAL_ALIGNMENT_LEFT
	
	return line

static func update_font_size_by_height(label: AutoSizeLabel) -> void:
	var font_size_range := Vector2i(label.min_font_size, label.max_font_size)
	var font := label.get_theme_font("font")
	
	var paragraph := get_text_paragraph()
	paragraph.width = label.size.x
	paragraph.break_flags = TextServer.BREAK_MANDATORY | TextServer.BREAK_WORD_BOUND | TextServer.BREAK_ADAPTIVE
	
	while true:
		paragraph.clear()
		
		var mid_font_size := font_size_range.x + roundi((font_size_range.y - font_size_range.x) * 0.5)
		if !paragraph.add_string(label.text, font, mid_font_size):
			push_warning("Could not create a string!")
			return
		
		var text_height: int = paragraph.get_size().y
		
		if text_height > label.size.y:
			if font_size_range.y == mid_font_size:
				break
			
			font_size_range.y = mid_font_size
		
		if text_height <= label.size.y:
			if font_size_range.x == mid_font_size:
				break
			
			font_size_range.x = mid_font_size
	label.add_theme_font_size_override("font_size", font_size_range.x)
	#Detect if lines got clipped. If yes, force it to reduce size to fit inside the original 
	while label.get_visible_line_count()<label.get_line_count():
		font_size_range.x-=1
		label.add_theme_font_size_override("font_size", font_size_range.x)

AutoSizeLabel.gd

@tool
class_name AutoSizeLabel extends Label

@export var min_font_size := 8 :
	set(v):
		min_font_size = clampi(v, 1, max_font_size)
		update()

@export var max_font_size := 56 :
	set(v):
		max_font_size = clampi(v, min_font_size, 191)
		update()

func _ready() -> void:
	clip_text=true
	item_rect_changed.connect(update)

func _set(property: StringName, value: Variant) -> bool:
	# Listen for changes to text
	if property == "text":
		text = value
		update()
		return true
	
	return false

func update() -> void:
	return AutoSizer.update_font_size_by_height(self)
1 Like