I'm making a GUI theme. I'd like to have a dominant color that appears in various controls. This color needs to be animated (flashing and transitions).

Can I tell control's styleboxes to reference the same color so I can animate only that color instead of individually animating each stylebox? I know I can add color properties to themes but didn't find a way to reference these colors from styleboxes.

I haven't worked with themes yet. But if you have a StyleBoxFlat, for example, it has a bg_color property which should be able to animate via a Tween (interpolate_property) or manually via code. I guess the trick would be getting the correct color value from the theme. I see Theme has a function get_color, maybe that will do what you need?

I can animate a color property in a stylebox, no problem. However, inside a single theme there can be many styleboxes. For example, only a button can have up to 5 styleboxes for various states, and each of these can again have multiple color properties (background, border, shadow...).

A theme can then end up with several dozens of color properties scattered through different styleboxes, all having the same rgb value (in my case). Animating a transition of that color now becomes tedious as you need to collect all of those properties and launch an identical tween for each one.

Themes allow adding custom color values. It can be done via gui (Edit Theme / Add Item) or via code ( Theme.set_color() ). But for the life of me I can't figure out if such colors can be assigned by reference to actual color properties in styleboxes. That would make total sense as it'd allow for easy "centralized" color scheme alteration.

For example: My Color and Button / Bg Color are color values I added to the theme. But I don't see a way to use them at all, other than to read them via script and assign them manually to actual color properties in styleboxes. Is that how they're meant to be used? I'd expect some automation here.

So yeah, you can do this. Having the same global theme applied to Control nodes and update the theme colors, all the Nodes will then get the new colors (after an update). However, the string names of the properties are hard coded and need to be exactly what Godot is looking for. I do not see a way to make up arbitrary values and use them automatically (though you could still use get_color and set_color manually). Here is my code that I tested.

extends Control

onready var custom_theme = preload("res://CustomTheme.tres")

func _input(event):
	if event.is_action_pressed("change_color"):
		custom_theme.set_color("font_color", "Button", Color(randf(), randf(), randf()))
		propagate_call("update")

The "font_color" is the set value for fonts in a button. So that is needed. The second argument is the Node type to apply it to. And the last is the Color itself. This works, but buttons don't update every frame unless something happens. So you won't see the new color until you roll over the button. If you have a small number of GUI elements, you can call update() on them, that would be more optimized. Or you can call propagate_call("update") on the parent node (of the GUI) and it will call update on all the children, changing the color. If you want to know what the magic strings are, you'll have to look in the theme source code. https://github.com/godotengine/godot/blob/master/scene/resources/default_theme/default_theme.cpp

That works, but actually propagate_call seems very slow. It's much faster if I just call update() directly.

extends Control

onready var custom_theme = preload("res://CustomTheme.tres")
onready var button_a = get_node("ButtonA")
onready var button_b = get_node("ButtonB")

func _ready():
	randomize()

func _input(event):
	if event.is_action_pressed("change_color"):
		custom_theme.set_color("font_color", "Button", Color(randf(), randf(), randf()))
		button_a.update()
		button_b.update()

You can of course save the nodes to an array by looping in get_children() or get_nodes_in_group() or whatever, so you don't have to manually write all the get_node code.

Hm, so not only that I must update each color in the theme directly, I must also update each control after that? This makes animating smooth color transition a bit tedious.

And I still don't know magic strings for properties inside styleboxes or even if I can set them from Theme api, or need to go directly to styleboxes. Magic strings in that source file are only for properties shared by the whole control class. Don't know how to change, for example, hover/bg_color.

You have to get the style box first.

custom_theme.get_stylebox("hover", "Button").bg_color = Color.red

In terms of animating, I agree that may be hard. Let me look into it.

Yep, just figured that out. And apparently, if you change something in the stylebox, all controls update automatically. Which doesn't happen if only Theme.set_color() is called as in your previous example. A bit weird.

The only way I currently see to animate this is to have some master-function like change_gui_color() that'll manually go through all relevant styleboxes/properties in the theme and set the colors. Then interpolate that function via tween.

So I found you can set properties directly using path notation. So you don't have to use the get/set. You can see the path you need when you roll over them in the inspector.

tween_real.interpolate_property(custom_theme, "Button/colors/font_color", null, Color(randf(), randf(), randf()), 1.0, Tween.TRANS_QUAD, Tween.EASE_IN_OUT)

So that works, and the property is animated, but the buttons do not update on screen because they have to draw and the font_color doesn't trigger the NOTIFICATION_DRAW (which you can do manually by calling update).

However, I found a hack. Since you can animate the stylebox, and that DOES trigger the draw. So all you have to do is animate something (like bg_color) and set it to the same color as itself. It won't look any different, but it will trigger the draws for exactly the length of time of the animation. Kind of ugly, but it works.

extends Control

onready var custom_theme = preload("res://CustomTheme.tres")
onready var normal_box = custom_theme.get_stylebox("normal", "Button")
onready var button_a = get_node("ButtonA")
onready var button_b = get_node("ButtonB")
onready var tween_real = get_node("Tween1")
onready var tween_fake = get_node("Tween2")

func _ready():
	randomize()

func _input(event):
	if event.is_action_pressed("animate_color"):
		tween_real.interpolate_property(custom_theme, "Button/colors/font_color", null, Color(randf(), randf(), randf()), 1.0, Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
		tween_real.start()
		tween_fake.interpolate_property(normal_box, "bg_color", null, normal_box.bg_color, 1.0, Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
		tween_fake.start()
	if event.is_action_pressed("animate_bg"):
		tween_real.interpolate_property(normal_box, "bg_color", null, Color(randf(), randf(), randf()), 1.0, Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
		tween_real.start()

All clear now! I'll do something like that. Thanks!

I always have to animate some colors in styleboxes anyway, so that'll trigger the redraw.

Only need to decide if it's better to launch many tweens or have a function that sets all colors and tween it with interpolate_method(). Many tweens is probably more efficient as no script code needs to be executed while animation is spinning.

a year later