How do I make properties ignore the `set` function when they're initialized?

Godot Version

v4.7.2.stable.official [ed1daf0bf]

Question

My game has a few different fonts, and in order to quickly switch between them and change custom_minimum_size to adapt to them, I added a function that does exactly that.

@export var font_style: Styles:
	set(value):
		font_style = value
		update_font_style() # load the fonts
		
		match value:
			Styles.DIALOG:
				custom_minimum_size = MIN_SIZE_DIALOG
			Styles.DIALOG_SMALL:
				custom_minimum_size = MIN_SIZE_DIALOG
			Styles.MENU:
				custom_minimum_size = MIN_SIZE_MENU
			Styles.TITLE_CARD:
				custom_minimum_size = MIN_SIZE_TITLE_CARD
		
		if font_style == Styles.DIALOG or font_style == Styles.DIALOG_SMALL:
			text_color = ColorPresets.REGULAR
		elif font_style == Styles.TITLE_CARD:
			text_color = ColorPresets.GOLD

However, while this is really handy when I need to change the font style, it’s not so handy when my labels change their minimum size after a reload. So how can I say something like “I want you to change minimum size when you don’t have a custom font resource or if you have one but it’s not the same as the one you already have loaded”? I tried if not has_theme_font_override("font"): update_font_style(), but that doesn’t work since when the node is initialized, it doesn’t have a font loaded either way

Is this an editor script? Do you change font_style at run time? Maybe you can add if Engine.is_editor_hint() if you do only use this export for changing in-editor?

I know it’s common enough to use await ready to delay setters until the node is in the scene tree, maybe you could check if get_parent() returns null to determine if it isn’t in the scene.

I think it would still mess things up but only when the scene is reloaded in the editor, since it still has to load the font resource and all

But since I usually append my labels as children of other nodes, wouldn’t it never return null?

Then maybe await ready is what you want, maybe sharing update_font_style() would help clarify what data you are dependent on, and what exactly is going wrong vs what you expect

it should be non-null, to see it in the scene at all get_parent must be non-null. But nodes aren’t yet in the scene at the time of @export’s initialization, at least as I understand it get_parent() may return null at that time, could be wrong.

There’s a common idiom for setters that need to run some code only if some initialization condition is met. It’s typically done for ready state but can be used for any custom state.
In the setter, run the dependent code only if condition is met. When some other part of the code initializes the condition - call the setter explicitly immediately afterwards.

@export var foo:
	set(value):
		foo = value
		if is_node_ready():
			# do dependent stuff

func _ready():
	foo = foo

Didn’t it work this way until some update? Or did I get it wrong? Because I explicitly have font_size set an extra time in _ready() and I think I wouldn’t’ve had it if it always worked the way it does now

Edit: okay wait this works in the editor but not in the game for some reason

We don’t know what your actual condition is. It may still not be met when this node’s _ready() is called.

Okay so I have this stupid setup where instead of instancing those labels, I just have them saved as a class so I can spawn a new one without having to worry about how many of their properties are unique and how many are not

Here’s what happens in _ready():

func _ready() -> void:
	if !(material is PaletteMaterial):
		material = PaletteMaterial.new()
		material.shader = load("res://resources/shaders/palette-swap-shader.gdshader")
		var ref_palette := load("res://assets/gfx/ui/font/font-palette-gold.png") as CompressedTexture2D
		material.set_shader_parameter("source_palette", ref_palette)
	
	add_theme_constant_override("line_spacing", 0)
	mouse_filter = Control.MOUSE_FILTER_IGNORE
	
	#update_font_style()
	update_color()

With the update_font_style() line commented, it doesn’t really affect the result in any way, so I’m still stumped as to what’s happening. Is the minimum size just not saved because it’s technically a completely new node every time? If so, why are the properties saved?

Also I tried setting their minimum size with custom_minimum_size = Vector2(min_size.x, min_size.y * get_line_count() + offset), but for some reason it doesn’t return line count correctly in some cases and returns character count instead? Why is that?

Which properties?

Text, alignment, and also exported ones like font_style and text_color

Where are they saved? You said your label is a class. Is it only a script or saved as a scene? Post the class code.

It’s kinda both a scene and a script but I think I only ever spawn them with CustomLabel.new() (idk how it actually happens in the editor). Basically I never use the Instantiate Child Scene button for them.

In every scene where CustomLabels are used? Like the options menu, pause screen, etc

@tool
class_name CustomLabel
extends Label


enum Styles {
	DIALOG,
	DIALOG_SMALL,
	MENU,
	TITLE_CARD,
}

const MIN_SIZE_DIALOG := Vector2(8, 8)
const MIN_SIZE_MENU := Vector2(16, 16)
const MIN_SIZE_TITLE_CARD := Vector2(32, 32)

@export var font_style: Styles:
	set(value):
		font_style = value
		if is_node_ready():
			update_font_style()
			set_min_size()

enum ColorPresets {
	REGULAR, ## White palette. Used by regular labels and unselected buttons.
	YELLOW, ## Yellow palette. Used by focused buttons.
	RED, ## Glossy red palette. Used by start menu buttons and warning labels.
	GOLD, ## Fancy golden palette. Mostly used by the title card font.
}

@export var text_color: ColorPresets: ## The palette used by the text.
	set(value):
		text_color = value
		update_color()


func _ready() -> void:
	if !(material is PaletteMaterial):
		material = PaletteMaterial.new()
		material.shader = load("res://resources/shaders/palette-swap-shader.gdshader")
		var ref_palette := load("res://assets/gfx/ui/font/font-palette-gold.png") as CompressedTexture2D
		material.set_shader_parameter("source_palette", ref_palette)
		update_color()
	
	add_theme_constant_override("line_spacing", 0)
	mouse_filter = Control.MOUSE_FILTER_IGNORE


func update_font_style(style := font_style) -> void:
	var font: FontFile
	
	match style:
		Styles.DIALOG:
			font = load("res://assets/gfx/ui/font/font-dialog.png")
		Styles.DIALOG_SMALL:
			font = load("res://assets/gfx/ui/font/font-dialog-small.png")
		Styles.MENU:
			font = load("res://assets/gfx/ui/font/font-menu.png")
		Styles.TITLE_CARD:
			font = load("res://assets/gfx/ui/font/font-titlecard.png")
	
	add_theme_font_override("font", font)
	
	if font_style == Styles.DIALOG or font_style == Styles.DIALOG_SMALL:
		text_color = ColorPresets.REGULAR
	elif font_style == Styles.TITLE_CARD:
		text_color = ColorPresets.GOLD
	
	if font_style == Styles.DIALOG:
		uppercase = false
	else:
		uppercase = true


func set_min_size() -> void:
	var min_size: Vector2
	var paragraph_spacing: int
	
	match font_style:
		Styles.DIALOG:
			min_size = MIN_SIZE_DIALOG
		Styles.DIALOG_SMALL:
			min_size = MIN_SIZE_DIALOG
		Styles.MENU:
			min_size = MIN_SIZE_MENU
		Styles.TITLE_CARD:
			min_size = MIN_SIZE_TITLE_CARD
	
	if font_style != Styles.TITLE_CARD:
		paragraph_spacing = 4
	else:
		paragraph_spacing = 0
	
	custom_minimum_size = Vector2(min_size.x, min_size.y + paragraph_spacing)


func update_color(color := text_color) -> void:
	if !(material is PaletteMaterial):
		return
	
	var palette: CompressedTexture2D
	
	match color:
		ColorPresets.REGULAR:
			palette = load("res://assets/gfx/ui/font/font-palette-white.png")
		ColorPresets.YELLOW:
			palette = load("res://assets/gfx/ui/font/font-palette-yellow.png")
		ColorPresets.RED:
			palette = load("res://assets/gfx/ui/font/font-palette-red.png")
		ColorPresets.GOLD:
			palette = load("res://assets/gfx/ui/font/font-palette-gold.png")
	
	var palette_image := palette.get_image()
	for x in palette_image.get_width():
		material.set_shader_parameter("color" + str(x), Color(palette_image.get_pixel(x, 0)))


func get_text_length_pixels() -> int:
	var text_length_pixels := 0
	var text_displayed := text.to_upper()
	
	for i in text_displayed:
		text_length_pixels += get_theme_font("font").get_char_size(ord(i), 16).x
	
	return text_length_pixels

You can separate the property from the variable used to store the property:

var _font_style: Styles
@export var font_style: Styles:
	get():
		return _font_style    
	set(value):
		_font_style = value
		update_font_style() # load the fonts


Write to font_style if you want to call the setter. Write to _font_style if you want to bypass the setter.

I’m honestly more curious now as to why get_line_count() isn’t working as expected, cuz if it did, this wouldn’t be a problem at all