I want to Tween a Control resize.. where to begin?

Godot Version

4.2.1

Question

I have some dialogue cutscenes where onscreen characters say various things. I’m implementing speech bubbles as PanelContainer > MarginContainer > Label. I have no idea what I’m doing there, feel free to question that choice if you’ve got extra time :sweat_smile:

My problem is, I want a tween that resizes the Label smoothly whenever the number of lines in the label changes (ex. if the last thing the character said was ‘cool bro’ and the next thing they say is ‘I think you should take the magic sword and cross the river of souls and head for the cave of truth’). Right now I set the text property, there’s a record-scratching sound, and the label has resized itself in a very jarring way.

I’m not sure where to begin with this. My first thought is to have a copy of the PanelContainer>MarginContainer>Label assembly sitting outside the camera view, set the text of that one first, get the size of that one, then Tween the actual, visible Label assembly to that size. That seems dumb though? Is there a concept or built-in method here that I’m missing?

What should the text do during this transition? Would it be full scale but cut off? The default behaviour would wrap the words to fit the box during the entire transition, which would be disorienting as players try to read it.

I think a different transition would be best, scaling the same text box is going to be difficult and raises a few design questions. Maybe a new thought bubble pushes the old one out of the way.

It’s not simple and, yes, you’ll need to copy nodes to calculate the final size.

extends PanelContainer


@export var max_width = 300.0

@onready var label: RichTextLabel = %Label
@onready var duplicated_container: Node = %DuplicatedContainer

var other_label:RichTextLabel


func _ready() -> void:
	# Duplicate, scale it to 0,0 and add it to the duplicated container
	# which is a plain node to escape the PanelContainer layout
	other_label = label.duplicate()
	other_label.global_position = Vector2(500, 100)
	other_label.scale = Vector2.ZERO
	duplicated_container.add_child(other_label)

	await tween_text("[color=red]cool bro[/color]")
	await get_tree().create_timer(3).timeout
	await tween_text('I think you should take the magic sword and cross the river of souls and head for the cave of truth')
	await get_tree().create_timer(3).timeout
	await tween_text('Okay?')
	await get_tree().create_timer(3).timeout
	await tween_text('[font_size=48]What?[/font_size] Did you not understand me?')
	await get_tree().create_timer(3).timeout
	await tween_text('Mauris ut ipsum sem. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed mattis lacus quam, in congue nisl feugiat eu. Nullam in enim at dolor egestas ornare. Vivamus gravida posuere leo vel vulputate. Ut in dignissim mi. Proin ultricies ante quis massa mollis mattis. Etiam sagittis nisl sed lobortis auctor.')
	await get_tree().create_timer(3).timeout
	await tween_text('What about now?')


func tween_text(new_text:String) -> void:
	# Center the text
	new_text = "[center]%s[/center]" % new_text
	
	# Prepare the other label. First try without autowrap to see if the new text fit our max width
	
	# Empty text
	other_label.text = ""
	# Disable autowrap
	other_label.autowrap_mode = TextServer.AUTOWRAP_OFF
	# set the size and custom minimum size to 0,0
	other_label.custom_minimum_size = Vector2.ZERO
	other_label.size = Vector2.ZERO
	# Set the new text
	other_label.text = new_text
	# Wait a frame so it can calculate the new size
	await get_tree().process_frame
	# If the label has autowrap and the other label is bigger than our max width then:
	if not label.autowrap_mode == TextServer.AUTOWRAP_OFF and other_label.size.x >= max_width:
		print("It's bigger %s" % other_label.size)
		# Reset the text
		other_label.text = ""
		# Set the size.x to our max width
		other_label.size.x = max_width
		# Copy the autowrap mode from the original label
		other_label.autowrap_mode = label.autowrap_mode
		# Set the new text again
		other_label.text = new_text
		# Wait a frame so it can calculate the new size
		await get_tree().process_frame
		# Set the size width to the minimum between the content width and the max width.
		# This is needed because some strings can be bigger than our max width with autowrap off but
		# be smaller with autowrap on depending on the font, font size,...
		other_label.size.x = min(other_label.get_content_width(), max_width)
		# Set the size height to the content height
		other_label.size.y = other_label.get_content_height()

	# Create a tween
	var tween = create_tween()
	# that modulates the alpha of the label to 0.0
	tween.tween_property(label, "modulate:a", 0.0, 0.25)
	# while growing or shrinking the panel container size
	# to the other label size plus the minimum size the container can have
	tween.parallel().tween_property(self, 'size', other_label.size + get_minimum_size(), 0.4)
	# Once that finishes, make a callback that set the minimum size to the size of the other label
	# and set the text
	tween.tween_callback(func():
		label.custom_minimum_size = other_label.size
		label.text = new_text
	)
	# Then modulate the alpha back to 1.0 while making a typewritter effect
	tween.tween_property(label, "modulate:a", 1.0, 0.25)
	tween.parallel().tween_property(label, "visible_ratio", 1.0, 0.3).from(0.0)
	# And await for the tween to finish
	await tween.finished

Result:

And here’s a self-contained tscn file (without the font): Tween panel container size to fit size of label Godot 4.2 tscn file · GitHub

1 Like

Wow, this is extraordinary! Thank you so much… lol, how do I buy you a coffee?

1 Like