Detect when text wraps to add custom behaviour?

Godot Version

Godot 4.2

Question

Hi, I’m trying to implement a dialogue system UI similar to that of Disco Elysium’s. The behaviour I want to emulate can be seen in that image: Every time the text gets wrapped onto a new line, it gets indented, probably once. I’m having a lot of difficulty emulating this behaviour in Godot, however.

There are options to indent every new line of text in godot’s String using the “Indent” function, but this obviously doesn’t notice the new lines that are born out of autowrapping text using Label or RichTextLabel nodes. I haven’t really found anything that detects when those nodes autowrap the text.

Is there a workaround or a way to detect this? It’s a small functionality, but one that adds quite a bit to the customization abilities of text. Essentially, all that is needed is to add “\t” to the string every time it autowraps to a new line.

I was able to do something like this using the bbcode [indent]:

image

This is the code I used:

extends Control

@onready var rich_text_label:RichTextLabel = $MarginContainer/RichTextLabel

var dialogue:Array = [
	"KIM KITSURAGI - \"Hello, I\'m Kim Kitsuragi.\" He [indent]looks unfazed. \"Lieutnant, Precinct 57. You[/indent] [indent]must be from the 41st...[/indent]",
	"[indent]You realize he is waiting for your name.[/indent]"
]

func _ready():
	for d in dialogue:
		rich_text_label.text += str("[p]",d,"[/p]")

That looks like what I want, yeah, but it seems like you would have to manually type out every single indentation, and also know the sizing of the text box while writing the dialogue, which is unfortunately not very realistic

I’m basically describing a scenario where all that work, the [indent] tags you’ve inserted is automatically done through code. All it would require is being able to grab code from the RichTextLabel whenever the text gets autowrapped to the next line, which is currently not possible through Godot itself…

I was checking how the source code does the line breaks in autowrap mode and it seems… complicated LOL.

It looks like you would need to make a similar loop to detect line breaks since there is no signal or method to get a line break position in the string.

Yeah, that goes a bit beyond my ability at the moment haha.

I’ve gone ahead and opened up a suggestion to add this to the engine in a future update, but there’s of course no telling when the devs might get around to implementing this feature.

I’ll welcome any workarounds anybody can think about, but I’ll have to just make do with not using this functionality for now.

It’s kinda possible as long as the “name” doesn’t spill to the second line and the message does not have tabs as we are going to “hijack” the tab stops from the paragraph bbcode tag to move the starting line of the “message” after the “name”. You’ll need two RichTextLabel one for the name and another for the message.

  • Set the root as a Control and with its Layout Mode to Anchors and the Anchors Preset to Full Rect.
  • Enable Fit Content in both RichTextLabels.
  • Set the name RichTextLabel Layout Mode to Anchors and the Anchors Preset to Full Rect.
  • Set the message RichTextLabel Layout Mode to Anchors and the Anchors Preset to Custom. Then set the Anchors Points to 0, 0, 1, 1 and Anchors Offsets to <your desired space>,0,0,0. Reset the Grow Direction Horizontal to Right and the Vertical to Bottom

Scene tree



I did it in code too but I’m not sure if it gets correctly applied or not:

extends Control

@onready var name_rich_text_label: RichTextLabel = $NameRichTextLabel
@onready var message_rich_text_label: RichTextLabel = $MessageRichTextLabel

@export var indent_size = 16.0

func _ready() -> void:
	name_rich_text_label.fit_content = true
	name_rich_text_label.set_anchors_preset(Control.PRESET_FULL_RECT)
	message_rich_text_label.fit_content = true
	message_rich_text_label.set_anchor_and_offset(SIDE_TOP, 0, 0)
	message_rich_text_label.set_anchor_and_offset(SIDE_LEFT, 0, indent_size)
	message_rich_text_label.set_anchor_and_offset(SIDE_BOTTOM, 1, 0)
	message_rich_text_label.set_anchor_and_offset(SIDE_RIGHT, 1, 0)
	message_rich_text_label.grow_horizontal = Control.GROW_DIRECTION_END
	message_rich_text_label.grow_vertical = Control.GROW_DIRECTION_END


func set_dialog(dialog) -> void:
	# wait for the node to be ready if needed
	if not is_node_ready():
		await ready

	# clear contents
	name_rich_text_label.clear()
	message_rich_text_label.clear()

	# add the name line
	name_rich_text_label.push_color(Color.CORNFLOWER_BLUE)
	name_rich_text_label.append_text(dialog.get("name"))
	name_rich_text_label.append_text(' - ')
	# calculate the position of the first line
	var first_line_position = name_rich_text_label.get_content_width() - message_rich_text_label.offset_left
	message_rich_text_label.append_text('[p tab_stops={first_line_position}]\t{message}[/p]'.format({'first_line_position': float(first_line_position), 'message': dialog.get('message')}))
	# set this control minimum size to the message content height
	custom_minimum_size.y = message_rich_text_label.get_content_height()

Then you’ll need to instantiate that scene into the container you want like:

extends Node

const DIALOG_ENTRY_SCENE = preload('res://dialog_entry.tscn')

const DIALOGS = [
	{"name": "Kim Kitsuragi", "message": "Cras dui sem, pretium nec tempor vel, egestas posuere mi. Ut quis ipsum ac diam congue mattis nec ut tellus. Quisque congue fermentum porta. Vestibulum feugiat convallis consequat. Vivamus ac lectus accumsan dolor facilisis malesuada. Nunc quis libero ut libero tincidunt placerat at ut nisl. Aenean a sapien iaculis, mollis lectus sed, auctor justo. Etiam non ligula ultrices, vestibulum massa eget, vulputate nunc. Interdum et malesuada fames ac ante ipsum primis in faucibus."},
	{"name": "You", "message": "Nam tempor sollicitudin lectus."},
	{"name": "Kim Kitsuragi", "message": "Mauris accumsan interdum leo vitae volutpat. Proin et elementum elit, nec pellentesque justo. Aliquam vitae dolor rutrum, imperdiet erat eu, finibus nulla. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus hendrerit eu purus ac iaculis."},
]

@onready var dialog_container: VBoxContainer = $ScrollContainer/HBoxContainer

func _ready() -> void:
	for dialog in DIALOGS:
		var entry = DIALOG_ENTRY_SCENE.instantiate()
		entry.set_dialog(dialog.get("name", "Unknown"), dialog.get("message", "No message?"))
		dialog_container.add_child(entry)

4 Likes

Hello! Sorry for bothering you.

I’m trying to get the same result but it is not working for me. I’ve tried your code and I keep getting errors, and if I reload the project it says that the scene has crashed.

I think these are the lines I’m getting the errors on:

@onready var dialog_container: VBoxContainer = $ScrollContainer/HBoxContainer

func _ready() -> void:
	for dialog in DIALOGS:
		var entry = DIALOG_ENTRY_SCENE.instantiate()
		entry.set_dialog(dialog.get("name", "Unknown"), dialog.get("message", "No message?"))
		dialog_container.add_child(entry)

These are the errors I get:

E 0:00:03:0811   _parse_ext_resource: res://scenes/dialogue_entry.tscn:36 - Parse Error: [ext_resource] referenced non-existent resource at: res://scripts/init.gd
  <C++ Source>   scene/resources/resource_format_text.cpp:162 @ _parse_ext_resource()

E 0:00:03:0811   set_path: Another resource is loaded from path 'res://scenes/dialogue_entry.tscn' (possible cyclic resource inclusion).
  <C++ Error>    Method/function failed.
  <C++ Source>   core/io/resource.cpp:75 @ set_path()

E 0:00:00:0586   init.gd:11 @ _ready(): Node not found: "ScrollContainer/HBoxContainer" (relative to "/root/DialogueEntry/Node").
  <C++ Error>    Method/function failed. Returning: nullptr
  <C++ Source>   scene/main/node.cpp:1620 @ get_node()
  <Stack Trace>  init.gd:11 @ _ready()


The code is all the same, I followed what you wrote step by step, I think I only changed “DialogEntry” to “DialogueEntry” in this attempt, but I’ve tried it many times with the same name as you used and I keep getting the same errors. I’m a total beginner to Godot, I’m currently using 4.2 but have tried the code on many versions, from 3 to 4.2.2.

I’ve been stressing a lot over this, might be a dumb thing to fixate over but I’ve delayed the development of my game until I get this kind of dialogue system to work, and so far I haven’t gotten any results.

Again, I’m so sorry for bothering you, if this isn’t an appropiate way to use the forums or a proper way to ask for help I deeply apologize, but I don’t know any other place to ask this on.

That error means the the node could not be found. You’ll need to change it to whatever node you want to add your DialogueEntry scenes.

The other errors may be related to that same error. I’m not sure.

1 Like

Oh yeah, sorry. I had no idea how to instantiate a scene into a container. Spent some time reading the docs and googling errors, moving things around in the code and nodes, and it finally worked.

One of the errors was caused by me instantiating/calling the scene into/from the same scene, or at least that’s what I understood. The other one was because I didn’t give the proper path for the dialog_container, so as you said, it couldn’t find the node.

A note about this line:
@onready var dialog_container: VBoxContainer = $ScrollContainer/HBoxContainer

If I let it as is, it would throw the error:
Trying to assign value of type 'HBoxContainer' to a variable of type 'VBoxContainer'.

Changing both of them to be a VBoxContainer made it work. Might have been a type from your side perhaps? Anyways, ignoring that small detail, the code works just as intended.

Again, I apologize for having bothered you, and thank you so much for your help.