How to prevent text wrapping between a number and an icon?

Godot Version

4.6.stable

Initial Implementation

We want to format a RichTextLabel with icons. The icons are represented by emojis in text and should be replaced by AtlasTextures in the RichTextLabel. For example:

Input:
"You can only use this ability once per game. Deal 2🗡️ to a random enemy. Then, you gain 2🛡️."

Output:

The code we use for this looks like this:

extends RichTextLabel

const ICON_SIZE := Vector2i(20, 20)

const EMOJI_TO_ICON: Dictionary[String, Texture] = {
	"🗡️": preload("res://sword_icon.tres"),
	"🛡️": preload("res://shield_icon.tres"),
}

const EMOJI_TO_COLOR: Dictionary[String, String] = {
	"🗡️": "be1522",
	"🛡️": "006282",
}


@export var txt: String:
	set(value):
		txt = value
		format_icons(txt)


func _ready() -> void:
	format_icons(txt)


func format_icons(content: String) -> void:
	# Reset the label, so we can build it from the ground up with icons
	text = ""
	
	# Construct regex (should be precompiled in a real project)
	var regex := RegEx.new()
	regex.compile("(?<number>\\d+)(?<emoji>" + "|".join(EMOJI_TO_ICON.keys()) + ")")
	
	var cursor := 0
	for m in regex.search_all(content):
		var number := m.get_string("number")
		var emoji := m.get_string("emoji")
		
		var icon := EMOJI_TO_ICON[emoji]
		
		# Append text before the match
		var before := content.substr(cursor, m.get_start() - cursor)
		append_text(before)
		
		# Append formatted number (optional)
		if number:
			var color := EMOJI_TO_COLOR[emoji]
			append_text("[color=" + color + "][b]" + number + "[/b][/color]")
	
		# Append the icon
		add_image(icon, ICON_SIZE.x, ICON_SIZE.y, Color(1, 1, 1, 1), INLINE_ALIGNMENT_CENTER)
		cursor = m.get_end()
	
	# Add any trailing text
	append_text(content.substr(cursor))

Table-based solution

The issue with the initial implementation is that the number and icon can be separated by text wrapping:

We want the number and icon to be bundled together, so they never get separated by text wrapping. Our current approach for this uses the table tag:

@tool
extends RichTextLabel

const ICON_SIZE := Vector2i(20, 20)

const EMOJI_TO_ICON: Dictionary[String, Texture] = {
	"🗡️": preload("res://sword_icon.tres"),
	"🛡️": preload("res://shield_icon.tres"),
}

const EMOJI_TO_COLOR: Dictionary[String, String] = {
	"🗡️": "be1522",
	"🛡️": "006282",
}

@export var txt: String:
	set(value):
		txt = value
		format_icons(txt)


func _ready() -> void:
	format_icons(txt)


func format_icons(content: String) -> void:
	# Reset the label, so we can build it from the ground up with icons
	text = ""

	# Construct regex (should be precompiled in a real project)
	var regex := RegEx.new()
	regex.compile("(?<number>\\d+)(?<emoji>" + "|".join(EMOJI_TO_ICON.keys()) + ")")
	
	var cursor := 0
	for m in regex.search_all(content):
		var number := m.get_string("number")
		var emoji := m.get_string("emoji")

		var icon := EMOJI_TO_ICON[emoji]

		# Append text before the match
		var before := content.substr(cursor, m.get_start() - cursor)
		append_text(before)

		# Bundle number + icon into a table row
		push_table(2, INLINE_ALIGNMENT_CENTER)
		
		push_cell()
		var color := EMOJI_TO_COLOR[emoji]
		append_text("[color=%s][b]%s[/b][/color]" % [color, number])
		pop() # cell
		
		push_cell()
		add_image(icon, ICON_SIZE.x, ICON_SIZE.y, Color.WHITE, INLINE_ALIGNMENT_CENTER)
		pop() # cell
		
		pop() # table

		cursor = m.get_end()

	# Add any trailing text
	append_text(content.substr(cursor))

The result is as desired. The “2” and “:dagger:” are bundled together and cannot be separated by a line break anymore:

But the number and icon are not vertically aligned with the rest of the text anymore. The image below shows an exaggerated example of a text with very large icons. The icon is vertically aligned thanks to the INLINE_ALIGNMENT_CENTER of the table. But the number is top-aligned.

Questions

  1. Is there a way to vertically center the text inside a RichTextLabel table cell?
  2. Is using an inline table the best way to keep a number and icon together as a non-breaking inline element, or is there a better approach?

Doesn’t appear you have a line break. You are seeing text wrapping.

Line breaks you can filter out. Text wrapping is based on the size of the control.

Oops. I rephrased the question.

About your first example where the text wrapping makes the icons appear on different lines. Are you sure there are no spaces between the number icon and sword icon? I tried this in 4.7 beta3 and two images stay together on same line if there is no space between them.

If there is no space between number and sword, check the RichTextLabel’s Autowrap mode. You should use Word (Smart) mode.

This likely has the solution to your issue: autowrap_mode

I am using that mode.

I checked this and you are right. This also works in 4.6.stable.

However, in my example the number is text added with append_text, and the icon is an image added with add_image. It seems like the combination those two does not keep them on same line, even if there is no space between them.

A solution could be to convert the number to an image and then add it as image, but I can imagine that is pretty resource intensive. I wonder if there are any cleaner solutions.

Try breaking the line on graphemes. It might be considering the image to be it’s own word.

I tried with this text:

[color=red][b]111[/b][/color][img=32]res://icon.svg[/img][color=green][b]222[/b][/color]

which is “111icon222”. For some reason this is wrapped between 111 and the icon, but not between icon and 222. I would say that this is a bug. I suggest making an issue about this.

Confirmed this behavior.

Also confirmed that this does not change the behavior seen.

autowrap_trim_flags = TextServer.BREAK_GRAPHEME_BOUND

There is indeed some inconsistent behavior.


Obtain 1[img=20]res://icon.svg[/img]

Wraps between the 1 and the icon.


Obtain [img=20]res://icon.svg[/img]1

Does not wrap between the icon and 1.

I would say your options are:

  • Put the icon first and the number second.
  • If you use only small numbers, generate separate images (number + icon) for each.
  • Make an issue to GitHub and hope this is fixed before your game is released (there seems to be a lot of RichTextLabel related wrapping issues though.)
  • Don’t let the wrapping bother you. Tell the players this is a deliberate design choice.

Here is code which can be used for a bug submission.

extends RichTextLabel
const ICON_SIZE := Vector2i(20, 20)


func _ready(): 
	bbcode_enabled = true
	custom_minimum_size = Vector2(450.0, 50.0)
	text = "You can only use this ability once per game. Deal "
	text += "[color=be1522][b]2[/b][/color]"
	var icon = preload("res://icon.svg")
	add_image(icon, ICON_SIZE.x, ICON_SIZE.y, Color.RED, INLINE_ALIGNMENT_CENTER)
	append_text(" to a random enemy. Then you gain ")
	append_text("[color=006282][b]4[/b][/color]")
	add_image(icon, ICON_SIZE.x, ICON_SIZE.y, Color.BLUE, INLINE_ALIGNMENT_CENTER)
	pop()
	
	var tween = get_tree().create_tween()
	tween.tween_property(self, "custom_minimum_size", Vector2(350.0, 100.0), 10)
	

Use the bbcode tag [zwj] (zero width joiner) between the number and the image like: [color=be1522][b]2[/b][/color][zwj][img=24]res://icon.svg[/img]

Thanks for the suggestion! It looked very promising. Unfortunately, it did not work.

It should work as long as the RichTextLabel.autowrap_mode is set to Word or Word (Smart). This last one will force a break if there’s no enough space in the line for the full word which is number+emoji in this case and shouldn’t be a problem as long as the textbox isn’t too thin.

You could also try with [wj] which is a word joiner.

You can also try [char=00A0] if you want a non-breaking space.

I tried all combinations of {[zwj], [wj], [char=00A0]} and {Word, Word (Smart)}. But the result is the same for all:

text before [color=be1522][b]2[/b][/color][zwj][img=24]res://icon.svg[/img] after

Update to Godot 4.6.3 It seems to be a bug in 4.6

Edit

Bug report here:

Tested [wj] between text and icon with 4.7 beta3 and it works.

I still wonder why the different order (icon and text) works differently.