Auto-resizing RichTextLabel with minimum and maximum size

Godot Version

v4.2.1.stable.official [b09f793f5]

Question

I need a RichTextLabel that has a minimum and a maximum width, but is otherwise only as wide as its content.

This sounds so trivial … but I could rip my hair out.

Context: I am creating speech bubbles that resize according to the text length.

Illustration:

Research:

  • fit_content works – more or less – as expected
  • Long discussion regarding rect_max_size-property with an open proposal linked

In CSS this would be easy:

display: inline-block; /* Make element as wide as content ... */
min-width: 100px;  /* ... but at least 100px wide ... */
max-width: 500px; /* ... and at maximum 500px wide */

Question

Is there an easy way to do this that I am missing?
(Or a complicated recommendation?)

Thanks in advance.

There’s not easy way. The RichTextLabel fits its content to the given size. If you give it 100 pixels width it will do its best to fit its content to 100 pixel width. if you give it 500 pixels it will do the same.

What you ask is still possible but it’s not a simple thing.

extends RichTextLabel

@export var max_width:float = 500.0


var strings = ["hi", "hello!", "Hello there handsome!", "Lorem [b]ipsum[/b] dolor sit amet, [font_size=16]consectetur adipiscing elit.[/font_size] Ut faucibus consectetur sapien sed malesuada. Sed pellentesque tempus consequat. Aliquam ac facilisis sem. In convallis, quam quis interdum eleifend, lectus nibh mattis enim, ac iaculis sapien magna in ligula. Praesent sit amet aliquet erat, ac tempor sem. Sed sollicitudin enim vitae sem aliquam, a semper ante sodales. Vestibulum luctus venenatis velit in auctor. Nunc luctus mollis lacus. Morbi viverra congue diam, nec dapibus velit elementum at. In accumsan hendrerit diam. Integer feugiat laoreet cursus. Phasellus laoreet ipsum a laoreet viverra. Aliquam posuere enim dolor, sit amet pulvinar diam gravida ac. Maecenas cursus ultrices nunc eu semper."]


func _ready() -> void:
	size = Vector2.ZERO
	fit_content = true
	finished.connect(_fit_width, CONNECT_DEFERRED)
	text = strings[0]
	await get_tree().create_timer(3).timeout
	text = strings[1]
	await get_tree().create_timer(3).timeout
	text = strings[2]
	await get_tree().create_timer(3).timeout
	text = strings[3]


func _fit_width() -> void:
	# block the signals so "finished" does not trigger this function again
	set_block_signals(true)
	var original_autowrap = autowrap_mode
	# save the position
	var tmp = global_position
	# move it out of the way to avoid flashing
	global_position.x = -100000
	# disable autowrap
	autowrap_mode = TextServer.AUTOWRAP_OFF
	# make it 0, 0
	size = Vector2.ZERO
	# wait one frame
	await get_tree().process_frame
	# now we have the size with no autowrap
	# if the width is bigger than max width clamp it
	var w = clampf(size.x, 0, max_width)
	var h = size.y
	# restore the autowrap mode
	autowrap_mode = original_autowrap
	# set the maximum size we got
	size.x = w
	# wait one frame for the text to resize
	await get_tree().process_frame
	# if the height is bigger than before we have multiple lines
	# and we may need to make the width smaller
	if size.y > h:
		# save the height
		h = size.y
		# keep lowering the width until the height changes
		while true:
			# lower the width a bit
			size.x -= 10
			# wait one frame
			await get_tree().process_frame
			# check if the height changed
			if not is_equal_approx(size.y, h):
				# if it changed we made the textbox too small
				# restore the width and break the while loop
				size.x += 10
				break
	# wait one frame
	await get_tree().process_frame
	# restore the height
	size.y = h
	# restore the original position
	global_position = tmp
	# unblock the signals
	set_block_signals(false)

Result:

I did it in the same RichTextLabel node to quickly test it. Because it takes a few frames to get the final size, it would be better if you use an off-screen RichTextLabel to get the final size and then copy that information to the one you are showing

2 Likes

This is awesome! I had hoped it wasn’t that hard but well … maybe Godot introduces a maximum width property at some point.

Thanks for the detailed answer. Will use your approach (with the off-screen clone for measuring).