Skip to content

Introduce ThemeDB/ThemeServer, register theme properties with it to define stable theming API for controls and windows, allow to define theme properties in scripts  #4486

Open
@YuriSizov

Description

@YuriSizov

Note: This proposal text is kept as it originally was, but the follow-up comments explore and extend the actual implementation, as well as which parts of the proposal to keep and which to reject. As such, this description predates the most recent title of this proposal.

Describe the project you are working on

Godot engine

Describe the problem or limitation you are having in your project

While theming is a powerful tool for customizing the looks of the UI in your Godot projects, the underlying tech has shortcomings and limitations.

First of all, theme properties are still "stringy" as of the current state of Godot 4.0, and remain one of the last bits of Godot scripting which is so. This means worse code completion, more room for typos, less defined API. As we move towards first-class citizens for other "stringy" properties, theme items feel like a relic of a past. Additionally, the built-in theme properties have poor naming convention (none, really), which is partially obfuscated by the use of strings without proper property definitions.

Secondly, theme properties are weakly defined. While controls require them and use them, and have not only visual details but bits of logic based on the theme properties (e.g. margins of a stylebox), controls don't actually define their contract with the theming system. Instead, they kind of hope that the theme applied to them, or one of the higher-order themes, is going to have the necessary items. The actual definition happens in scene/resources/default_theme.cpp instead, where the default project theme is created, used as a fallback for everything.

The default theme definitions is exactly what we use to create the list of control's theme override properties, for example. Same approach is used in the theme editor. And it's all, once again, string-based. This leads to the default theme missing definitions that some controls require, and, vice versa, having outdated or forever invalid definitions which the corresponding controls don't actually need. And as maintainers we have no tools to validate them, make sure we define all we need to define and remove all the bits we don't actually need.

All in all, it's a frustrating gap between a control and its default styling. And as a result, custom controls don't have a nice way to hook into the existing system and can't benefit from having defined theme overrides and default styling. You may be able to hack into an instance of the default theme, but that's not a good and stable solution, and definitely not an intuitive one.

Thirdly, theming is limited to controls, while it can be useful for other parts of your project. For example, you can use a theme to carry color information to your in-game units. You can of course create the necessary boilerplate code to imitate theme application and propagation to Node2D or Node3D/Spatial, but that becomes harder to sell with Godot 4.0, where we have a new kind of node that also implements theming — Window. Implementing support for both controls and windows required hacking into Control's existing theming logic and making Windows inherently dependent on Control's code to reduce code duplication.

But I think it's a good sign that we might want to extend theming to just about any node, while keeping this system opt-in for the most of them (but enabled by default for controls and windows).

Describe the feature / enhancement and how it helps to overcome the problem or limitation

I propose several interconnected things to make the whole theming system more robust, extensible, and intuitive.

1. Make controls define their theme properties, and store them in ClassDB like normal properties (but separate).

Let's say, we introduce a new macro, ADD_THEME_PROPERTY that would create ClassDB definitions, and provide sensible default values for them if necessary. We'll use these definitions wherever and whenever we need a list of theme properties supported by each control (in the inspector, the theme editor, etc). The default theme specifically and the Theme resource in general remains the same, but using this now defined theming contract we can evaluate if the default theme has all the necessary items and if it has excessive items.

To reduce stringy-ness of theme properties internally, we also introduce a ThemeProperty type, or something similar. We'll use this type to represent each Control's theme property. It will be able to look up definitions in the scene tree the same as control does now, but abstracted away from the rest of the control code. It will also handle local overrides for controls. I think that in the engine code ThemeProperty members can be normal class properties, so accessing them would be natural and intuitive.

2. We expose everything from the first point to scripting.

The ThemeProperty type would be a naturally exposed class. As for properties themselves, I propose we add an annotation @theme_property, to separate them from regular properties (just like, say, signals are separated, despite being member-like). I think this separation is important because theme properties would be their own ClassDB entity. Plus, they have specific pre-defined behavior (like the theme overrides section), and this annotation would be the scripting way to achieve the same as ADD_THEME_PROPERTY internally.

An example would be

extends Control

@theme_property(Theme.DATA_TYPE_COLOR) var background_color : ThemeProperty
# or
@theme_property_color var background_color : ThemeProperty
# or
@theme_property var background_color : ThemePropertyColor

func _draw():
    var rect = Rect2(Vector2(-10, -10), Vector2(20, 20))
    draw_rect(rect, background_color.get())

With such tools, custom controls should become as natural as built-ins, benefiting from the native editor integration and stable, easily discoverable API.

3. Move theming logic from Control to Node, make it opt-in.

This removes the dependency of Window on Control, but also opens up theming to be usable outside of UI. All nodes would have an innate ability to accept theme and theming notifications, but we won't enable it by default. Instead a node needs to explicitly turn this on.

One of the features of theming is theme propagation and theme merging along a branch of nodes. So I think that opting in can be done by defining a property that would store node's own theme. In engine code it can be a ADD_THEME macro, acting similar to ADD_THEME_PROPERTY, but for the theme itself. And in scripting it can be a @theme annotation, e.g.

extends Node

@theme var theme : Theme

Opting in would make nodes a part of the propagation system, so they can affect underlying "themeable" nodes just like controls affect their child controls. Basically, what now happens between an uninterrupted chain of controls is going to happen between all the nodes, with nodes that opted into theming interacting and the rest of the nodes being ignored.

You can view this as a poor man's multiple inheritance/interface implementation. Nodes that opt into the theming system in a way implement an "IThemeable" interface or extend a "Themeable" class, without breaking their existing inheritance chain. I think that my approach fits Godot's and GDScript vision more than interfaces and multiple inheritance, at least as it currently stands.

What I wouldn't change...

... is the Theme resource and the editor UI for anything related. I think what I propose would improve things without any changes there.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

See above.

If this enhancement will not be used often, can it be worked around with a few lines of script?

Points 1 and 2 are about core changes to how theme properties are defined.
Point 3 can be implemented with boilerplate code, but it's not just a few lines.

Is there a reason why this should be core and not an add-on in the asset library?

This is a core change.

Metadata

Metadata

Assignees

Type

No type

Projects

  • Status

    In Discussion

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions