Browse Source

Animation support for decorators (#421)

Co-authored-by: Michael R. P. Ragazzon <[email protected]>
suddenly 2 years ago
parent
commit
e8500731d4

+ 1 - 1
Include/RmlUi/Core/PropertySpecification.h

@@ -120,7 +120,7 @@ public:
 	void SetPropertyDefaults(PropertyDictionary& dictionary) const;
 	void SetPropertyDefaults(PropertyDictionary& dictionary) const;
 
 
 	/// Returns the properties of dictionary converted to a string.
 	/// Returns the properties of dictionary converted to a string.
-	String PropertiesToString(const PropertyDictionary& dictionary) const;
+	String PropertiesToString(const PropertyDictionary& dictionary, bool include_name, char delimiter) const;
 
 
 private:
 private:
 	using Properties = Vector< UniquePtr<PropertyDefinition> >;
 	using Properties = Vector< UniquePtr<PropertyDefinition> >;

+ 6 - 1
Include/RmlUi/Core/StyleSheet.h

@@ -46,6 +46,8 @@ class StyleSheetParser;
 struct PropertySource;
 struct PropertySource;
 struct Sprite;
 struct Sprite;
 
 
+using DecoratorPtrList = Vector<SharedPtr<const Decorator>>;
+
 /**
 /**
 	StyleSheet maintains a single stylesheet definition. A stylesheet can be combined with another stylesheet to create
 	StyleSheet maintains a single stylesheet definition. A stylesheet can be combined with another stylesheet to create
 	a new, merged stylesheet.
 	a new, merged stylesheet.
@@ -66,6 +68,9 @@ public:
 	/// Builds the node index for a combined style sheet.
 	/// Builds the node index for a combined style sheet.
 	void BuildNodeIndex();
 	void BuildNodeIndex();
 
 
+	/// Returns the DecoratorSpecification of the given name, or null if it does not exist.
+	const DecoratorSpecification* GetDecoratorSpecification(const String& name) const;
+
 	/// Returns the Keyframes of the given name, or null if it does not exist.
 	/// Returns the Keyframes of the given name, or null if it does not exist.
 	/// @lifetime The returned pointer becomes invalidated whenever the style sheet is re-generated. Do not store this pointer or references to subobjects around.
 	/// @lifetime The returned pointer becomes invalidated whenever the style sheet is re-generated. Do not store this pointer or references to subobjects around.
 	const Keyframes* GetKeyframes(const String& name) const;
 	const Keyframes* GetKeyframes(const String& name) const;
@@ -78,7 +83,7 @@ public:
 	SharedPtr<const ElementDefinition> GetElementDefinition(const Element* element) const;
 	SharedPtr<const ElementDefinition> GetElementDefinition(const Element* element) const;
 
 
 	/// Returns a list of instanced decorators from the declarations. The instances are cached for faster future retrieval.
 	/// Returns a list of instanced decorators from the declarations. The instances are cached for faster future retrieval.
-	const Vector<SharedPtr<const Decorator>>& InstanceDecorators(const DecoratorDeclarationList& declaration_list, const PropertySource* decorator_source) const;
+	const DecoratorPtrList& InstanceDecorators(const DecoratorDeclarationList& declaration_list, const PropertySource* decorator_source) const;
 
 
 private:
 private:
 	StyleSheet();
 	StyleSheet();

+ 11 - 0
Include/RmlUi/Core/StyleSheetTypes.h

@@ -30,6 +30,7 @@
 #define RMLUI_CORE_STYLESHEETTYPES_H
 #define RMLUI_CORE_STYLESHEETTYPES_H
 
 
 #include "PropertyDictionary.h"
 #include "PropertyDictionary.h"
+#include "Factory.h"
 #include "Types.h"
 #include "Types.h"
 #include "Utilities.h"
 #include "Utilities.h"
 
 
@@ -63,6 +64,16 @@ struct DecoratorDeclaration {
 	DecoratorInstancer* instancer;
 	DecoratorInstancer* instancer;
 	PropertyDictionary properties;
 	PropertyDictionary properties;
 };
 };
+
+struct DecoratorDeclarationView {
+	DecoratorDeclarationView(const DecoratorDeclaration& declaration) : type(declaration.type), instancer(declaration.instancer), properties(declaration.properties) {}
+	DecoratorDeclarationView(const DecoratorSpecification* specification) : type(specification->decorator_type), instancer(Factory::GetDecoratorInstancer(specification->decorator_type)), properties(specification->properties) {}
+
+	const String& type;
+	DecoratorInstancer* instancer;
+	const PropertyDictionary& properties;
+};
+
 struct DecoratorDeclarationList {
 struct DecoratorDeclarationList {
 	Vector<DecoratorDeclaration> list;
 	Vector<DecoratorDeclaration> list;
 	String value;
 	String value;

+ 139 - 3
Source/Core/ElementAnimation.cpp

@@ -27,13 +27,18 @@
  */
  */
 
 
 #include "ElementAnimation.h"
 #include "ElementAnimation.h"
-#include "ElementStyle.h"
-#include "TransformUtilities.h"
+#include "../../Include/RmlUi/Core/DecoratorInstancer.h"
+#include "../../Include/RmlUi/Core/Factory.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/PropertyDefinition.h"
 #include "../../Include/RmlUi/Core/PropertyDefinition.h"
+#include "../../Include/RmlUi/Core/PropertySpecification.h"
 #include "../../Include/RmlUi/Core/StyleSheetSpecification.h"
 #include "../../Include/RmlUi/Core/StyleSheetSpecification.h"
+#include "../../Include/RmlUi/Core/StyleSheetTypes.h"
+#include "../../Include/RmlUi/Core/StyleSheet.h"
 #include "../../Include/RmlUi/Core/Transform.h"
 #include "../../Include/RmlUi/Core/Transform.h"
 #include "../../Include/RmlUi/Core/TransformPrimitive.h"
 #include "../../Include/RmlUi/Core/TransformPrimitive.h"
+#include "ElementStyle.h"
+#include "TransformUtilities.h"
 
 
 namespace Rml {
 namespace Rml {
 
 
@@ -165,6 +170,124 @@ static Property InterpolateProperties(const Property & p0, const Property& p1, f
 		return Property{ TransformPtr(std::move(t)), Property::TRANSFORM };
 		return Property{ TransformPtr(std::move(t)), Property::TRANSFORM };
 	}
 	}
 
 
+	if (p0.unit == Property::DECORATOR && p1.unit == Property::DECORATOR)
+	{
+		auto DiscreteInterpolation = [&]() { return alpha < 0.5f ? p0 : p1; };
+		
+		// We construct DecoratorDeclarationView from declaration if it has instancer, otherwise we look for DecoratorSpecification and return DecoratorDeclarationView from it
+		auto GetDecoratorDeclarationView = [&](const DecoratorDeclaration& declaration) -> DecoratorDeclarationView {
+			if (declaration.instancer)
+				return DecoratorDeclarationView{ declaration };
+
+			const StyleSheet* style_sheet = element.GetStyleSheet();
+			if (!style_sheet)
+			{
+				Log::Message(Log::LT_WARNING, "Failed to get element stylesheet for '%s' decorator rule.", declaration.type.c_str());
+				return DecoratorDeclarationView{ declaration };
+			}
+
+			const DecoratorSpecification* specification = style_sheet->GetDecoratorSpecification(declaration.type);
+			if (!specification)
+			{
+				Log::Message(Log::LT_WARNING, "Could not find DecoratorSpecification for '%s' decorator rule.", declaration.type.c_str());
+				return DecoratorDeclarationView{ declaration };
+			}
+
+			return DecoratorDeclarationView{ specification };
+		};
+
+		auto& ptr0 = p0.value.GetReference<DecoratorsPtr>();
+		auto& ptr1 = p1.value.GetReference<DecoratorsPtr>();
+		if (!ptr0 || !ptr1)
+		{
+			RMLUI_ERRORMSG("Invalid decorator pointer, were the decorator keys properly prepared?");
+			return DiscreteInterpolation();
+		}
+
+		const bool p0_smaller = (ptr0->list.size() < ptr1->list.size());
+		auto& small = (p0_smaller ? ptr0->list : ptr1->list);
+		auto& big = (p0_smaller ? ptr1->list : ptr0->list);
+
+		// Build the new, interpolated decorator.
+		UniquePtr<DecoratorDeclarationList> decorator(new DecoratorDeclarationList);
+		decorator->list.reserve(ptr0->list.size());
+
+		// Interpolate decorators that have common types.
+		for (size_t i = 0; i < small.size(); i++)
+		{
+			DecoratorDeclarationView d0_view{ GetDecoratorDeclarationView(ptr0->list[i]) };
+			DecoratorDeclarationView d1_view{ GetDecoratorDeclarationView(ptr1->list[i]) };
+
+			if (!d0_view.instancer || !d1_view.instancer)
+				return DiscreteInterpolation();
+
+			if (d0_view.instancer != d1_view.instancer || d0_view.type != d1_view.type ||
+				d0_view.properties.GetNumProperties() != d1_view.properties.GetNumProperties())
+			{
+				// Incompatible decorators, fall back to discrete interpolation.
+				return DiscreteInterpolation();
+			}
+
+			decorator->list.push_back(DecoratorDeclaration{ d0_view.type, d0_view.instancer, PropertyDictionary() });
+			PropertyDictionary& props = decorator->list.back().properties;
+
+			const auto& props0 = d0_view.properties.GetProperties();
+			const auto& props1 = d1_view.properties.GetProperties();
+
+			for (const auto& pair0 : props0)
+			{
+				const PropertyId id = pair0.first;
+				const Property& prop0 = pair0.second;
+
+				auto it = props1.find(id);
+				if (it == props1.end())
+				{
+					RMLUI_ERRORMSG("Incompatible decorator properties.");
+					return DiscreteInterpolation();
+				}
+				const Property& prop1 = it->second;
+
+				Property p = InterpolateProperties(prop0, prop1, alpha, element, prop0.definition);
+				p.definition = prop0.definition;
+				props.SetProperty(id, p);
+			}
+		}
+
+		// Append any trailing decorators from the largest list and interpolate against the default values of its type.
+		for (size_t i = small.size(); i < big.size(); i++)
+		{
+			DecoratorDeclarationView dbig_view{ GetDecoratorDeclarationView(big[i]) };
+
+			if (!dbig_view.instancer)
+				return DiscreteInterpolation();
+
+			decorator->list.push_back(DecoratorDeclaration{ dbig_view.type, dbig_view.instancer, PropertyDictionary() });
+			DecoratorDeclaration& d_new = decorator->list.back();
+
+			const PropertySpecification& specification = d_new.instancer->GetPropertySpecification();
+
+			const PropertyMap& props_big = dbig_view.properties.GetProperties();
+			for (const auto& pair_big : props_big)
+			{
+				const PropertyId id = pair_big.first;
+				const PropertyDefinition* underlying_definition = specification.GetProperty(id);
+				if (!underlying_definition)
+					return DiscreteInterpolation();
+
+				const Property& p_big = pair_big.second;
+				const Property& p_small = *underlying_definition->GetDefaultValue();
+				const Property& p_interp0 = (p0_smaller ? p_small : p_big);
+				const Property& p_interp1 = (p0_smaller ? p_big : p_small);
+
+				Property p = InterpolateProperties(p_interp0, p_interp1, alpha, element, p_big.definition);
+				p.definition = p_big.definition;
+				d_new.properties.SetProperty(id, p);
+			}
+		}
+
+		return Property{ DecoratorsPtr(std::move(decorator)), Property::DECORATOR };
+	}
+
 	// Fall back to discrete interpolation for incompatible units.
 	// Fall back to discrete interpolation for incompatible units.
 	return alpha < 0.5f ? p0 : p1;
 	return alpha < 0.5f ? p0 : p1;
 }
 }
@@ -392,6 +515,15 @@ static bool PrepareTransforms(Vector<AnimationKey>& keys, Element& element, int
 	return (count_iterations < max_iterations);
 	return (count_iterations < max_iterations);
 }
 }
 
 
+static void PrepareDecorator(AnimationKey& key)
+{
+	Property& property = key.property;
+	RMLUI_ASSERT(property.value.GetType() == Variant::DECORATORSPTR);
+
+	if (!property.value.GetReference<DecoratorsPtr>())
+		property.value = MakeShared<DecoratorDeclarationList>();
+}
+
 
 
 ElementAnimation::ElementAnimation(PropertyId property_id, ElementAnimationOrigin origin, const Property& current_value, Element& element,
 ElementAnimation::ElementAnimation(PropertyId property_id, ElementAnimationOrigin origin, const Property& current_value, Element& element,
 	double start_world_time, float duration, int num_iterations, bool alternate_direction) :
 	double start_world_time, float duration, int num_iterations, bool alternate_direction) :
@@ -409,7 +541,7 @@ ElementAnimation::ElementAnimation(PropertyId property_id, ElementAnimationOrigi
 
 
 bool ElementAnimation::InternalAddKey(float time, const Property& in_property, Element& element, Tween tween)
 bool ElementAnimation::InternalAddKey(float time, const Property& in_property, Element& element, Tween tween)
 {
 {
-	int valid_properties = (Property::NUMBER_LENGTH_PERCENT | Property::ANGLE | Property::COLOUR | Property::TRANSFORM | Property::KEYWORD);
+	int valid_properties = (Property::NUMBER_LENGTH_PERCENT | Property::ANGLE | Property::COLOUR | Property::TRANSFORM | Property::KEYWORD | Property::DECORATOR);
 
 
 	if (!(in_property.unit & valid_properties))
 	if (!(in_property.unit & valid_properties))
 	{
 	{
@@ -424,6 +556,10 @@ bool ElementAnimation::InternalAddKey(float time, const Property& in_property, E
 	{
 	{
 		result = PrepareTransforms(keys, element, (int)keys.size() - 1);
 		result = PrepareTransforms(keys, element, (int)keys.size() - 1);
 	}
 	}
+	else if (keys.back().property.unit == Property::DECORATOR)
+	{
+		PrepareDecorator(keys.back());
+	}
 
 
 	if (!result)
 	if (!result)
 	{
 	{

+ 1 - 1
Source/Core/ElementDecoration.cpp

@@ -88,7 +88,7 @@ bool ElementDecoration::ReloadDecorators()
 		}
 		}
 	}
 	}
 
 
-	const auto& decorator_list = style_sheet->InstanceDecorators(*decorators_ptr, source);
+	const DecoratorPtrList& decorator_list = style_sheet->InstanceDecorators(*decorators_ptr, source);
 
 
 	for (const SharedPtr<const Decorator>& decorator : decorator_list)
 	for (const SharedPtr<const Decorator>& decorator : decorator_list)
 	{
 	{

+ 3 - 4
Source/Core/PropertyParserDecorator.cpp

@@ -52,19 +52,18 @@ bool PropertyParserDecorator::ParseValue(Property& property, const String& decor
 
 
 	if (decorator_string_value.empty() || decorator_string_value == "none")
 	if (decorator_string_value.empty() || decorator_string_value == "none")
 	{
 	{
-		property.value = Variant();
-		property.unit = Property::UNKNOWN;
+		property.value = Variant(DecoratorsPtr());
+		property.unit = Property::DECORATOR;
 		return true;
 		return true;
 	}
 	}
 
 
 	RMLUI_ZoneScoped;
 	RMLUI_ZoneScoped;
 
 
-	DecoratorDeclarationList decorators;
-
 	// Make sure we don't split inside the parenthesis since they may appear in decorator shorthands.
 	// Make sure we don't split inside the parenthesis since they may appear in decorator shorthands.
 	StringList decorator_string_list;
 	StringList decorator_string_list;
 	StringUtilities::ExpandString(decorator_string_list, decorator_string_value, ',', '(', ')');
 	StringUtilities::ExpandString(decorator_string_list, decorator_string_value, ',', '(', ')');
 
 
+	DecoratorDeclarationList decorators;
 	decorators.value = decorator_string_value;
 	decorators.value = decorator_string_value;
 	decorators.list.reserve(decorator_string_list.size());
 	decorators.list.reserve(decorator_string_list.size());
 
 

+ 23 - 5
Source/Core/PropertySpecification.cpp

@@ -29,11 +29,12 @@
 #include "../../Include/RmlUi/Core/PropertySpecification.h"
 #include "../../Include/RmlUi/Core/PropertySpecification.h"
 #include "../../Include/RmlUi/Core/Debug.h"
 #include "../../Include/RmlUi/Core/Debug.h"
 #include "../../Include/RmlUi/Core/Log.h"
 #include "../../Include/RmlUi/Core/Log.h"
+#include "../../Include/RmlUi/Core/Profiling.h"
 #include "../../Include/RmlUi/Core/PropertyDefinition.h"
 #include "../../Include/RmlUi/Core/PropertyDefinition.h"
 #include "../../Include/RmlUi/Core/PropertyDictionary.h"
 #include "../../Include/RmlUi/Core/PropertyDictionary.h"
-#include "../../Include/RmlUi/Core/Profiling.h"
-#include "PropertyShorthandDefinition.h"
 #include "IdNameMap.h"
 #include "IdNameMap.h"
+#include "PropertyShorthandDefinition.h"
+#include <algorithm>
 #include <limits.h>
 #include <limits.h>
 #include <stdint.h>
 #include <stdint.h>
 
 
@@ -432,13 +433,30 @@ void PropertySpecification::SetPropertyDefaults(PropertyDictionary& dictionary)
 	}
 	}
 }
 }
 
 
-String PropertySpecification::PropertiesToString(const PropertyDictionary& dictionary) const
+String PropertySpecification::PropertiesToString(const PropertyDictionary& dictionary, bool include_name, char delimiter) const
 {
 {
+	const PropertyMap& properties = dictionary.GetProperties();
+
+	// For determinism we print the strings in order of increasing property ids.
+	Vector<PropertyId> ids;
+	ids.reserve(properties.size());
+	for (auto& pair : properties)
+		ids.push_back(pair.first);
+
+	std::sort(ids.begin(), ids.end());
+
 	String result;
 	String result;
-	for (auto& pair : dictionary.GetProperties())
+	for (PropertyId id : ids)
 	{
 	{
-		result += property_map->GetName(pair.first) + ": " + pair.second.ToString() + '\n';
+		const Property& p = properties.find(id)->second;
+		if (include_name)
+			result += property_map->GetName(id) + ": ";
+		result += p.ToString() + delimiter;
 	}
 	}
+
+	if (!result.empty())
+		result.pop_back();
+
 	return result;
 	return result;
 }
 }
 
 

+ 53 - 21
Source/Core/StyleSheet.cpp

@@ -103,8 +103,16 @@ void StyleSheet::BuildNodeIndex()
 	root->BuildIndex(styled_node_index);
 	root->BuildIndex(styled_node_index);
 }
 }
 
 
+const DecoratorSpecification* StyleSheet::GetDecoratorSpecification(const String& name) const
+{
+	auto it = decorator_map.find(name);
+	if (it != decorator_map.end())
+		return &(it->second);
+	return nullptr;
+}
+
 // Returns the Keyframes of the given name, or null if it does not exist.
 // Returns the Keyframes of the given name, or null if it does not exist.
-const Keyframes * StyleSheet::GetKeyframes(const String & name) const
+const Keyframes* StyleSheet::GetKeyframes(const String & name) const
 {
 {
 	auto it = keyframes.find(name);
 	auto it = keyframes.find(name);
 	if (it != keyframes.end())
 	if (it != keyframes.end())
@@ -112,47 +120,71 @@ const Keyframes * StyleSheet::GetKeyframes(const String & name) const
 	return nullptr;
 	return nullptr;
 }
 }
 
 
-const Vector<SharedPtr<const Decorator>>& StyleSheet::InstanceDecorators(const DecoratorDeclarationList& declaration_list, const PropertySource* source) const
+const DecoratorPtrList& StyleSheet::InstanceDecorators(const DecoratorDeclarationList& declaration_list, const PropertySource* source) const
 {
 {
+	RMLUI_ASSERT_NONRECURSIVE; // Since we may return a reference to the below static variable.
+	static DecoratorPtrList non_cached_decorator_list;
+
+	// Empty declaration values are used for interpolated values which we don't want to cache.
+	const bool enable_cache = !declaration_list.value.empty();
+
 	// Generate the cache key. Relative paths of textures may be affected by the source path, and ultimately
 	// Generate the cache key. Relative paths of textures may be affected by the source path, and ultimately
 	// which texture should be displayed. Thus, we need to include this path in the cache key.
 	// which texture should be displayed. Thus, we need to include this path in the cache key.
 	String key;
 	String key;
-	key.reserve(declaration_list.value.size() + 1 + (source ? source->path.size() : 0));
-	key = declaration_list.value;
-	key += ';';
-	if (source)
-		key += source->path;
 
 
-	auto it_cache = decorator_cache.find(key);
-	if (it_cache != decorator_cache.end())
-		return it_cache->second;
+	if (enable_cache)
+	{
+		key.reserve(declaration_list.value.size() + 1 + (source ? source->path.size() : 0));
+		key = declaration_list.value;
+		key += ';';
+		if (source)
+			key += source->path;
+
+		auto it_cache = decorator_cache.find(key);
+		if (it_cache != decorator_cache.end())
+			return it_cache->second;
+	}
+	else
+	{
+		non_cached_decorator_list.clear();
+	}
 
 
-	Vector<SharedPtr<const Decorator>>& decorators = decorator_cache[key];
+	DecoratorPtrList& decorators = enable_cache ? decorator_cache[key] : non_cached_decorator_list;
+	decorators.reserve(declaration_list.list.size());
 
 
 	for (const DecoratorDeclaration& declaration : declaration_list.list)
 	for (const DecoratorDeclaration& declaration : declaration_list.list)
 	{
 	{
+		SharedPtr<Decorator> decorator;
+
 		if (declaration.instancer)
 		if (declaration.instancer)
 		{
 		{
 			RMLUI_ZoneScopedN("InstanceDecorator");
 			RMLUI_ZoneScopedN("InstanceDecorator");
-			
-			if (SharedPtr<Decorator> decorator = declaration.instancer->InstanceDecorator(declaration.type, declaration.properties, DecoratorInstancerInterface(*this, source)))
-				decorators.push_back(std::move(decorator));
-			else
-				Log::Message(Log::LT_WARNING, "Decorator '%s' in '%s' could not be instanced, declared at %s:%d", declaration.type.c_str(), declaration_list.value.c_str(), source ? source->path.c_str() : "", source ? source->line_number : -1);
+			decorator =
+				declaration.instancer->InstanceDecorator(declaration.type, declaration.properties, DecoratorInstancerInterface(*this, source));
+
+			if (!decorator)
+				Log::Message(Log::LT_WARNING, "Decorator '%s' in '%s' could not be instanced, declared at %s:%d", declaration.type.c_str(),
+					declaration_list.value.c_str(), source ? source->path.c_str() : "", source ? source->line_number : -1);
 		}
 		}
 		else
 		else
 		{
 		{
 			// If we have no instancer, this means the type is the name of an @decorator rule.
 			// If we have no instancer, this means the type is the name of an @decorator rule.
-			SharedPtr<Decorator> decorator;
 			auto it_map = decorator_map.find(declaration.type);
 			auto it_map = decorator_map.find(declaration.type);
 			if (it_map != decorator_map.end())
 			if (it_map != decorator_map.end())
 				decorator = it_map->second.decorator;
 				decorator = it_map->second.decorator;
 
 
-			if (decorator)
-				decorators.push_back(std::move(decorator));
-			else
-				Log::Message(Log::LT_WARNING, "Decorator name '%s' could not be found in any @decorator rule, declared at %s:%d", declaration.type.c_str(), source ? source->path.c_str() : "", source ? source->line_number : -1);
+			if (!decorator)
+				Log::Message(Log::LT_WARNING, "Decorator name '%s' could not be found in any @decorator rule, declared at %s:%d",
+					declaration.type.c_str(), source ? source->path.c_str() : "", source ? source->line_number : -1);
 		}
 		}
+
+		if (!decorator)
+		{
+			decorators.clear();
+			break;
+		}
+
+		decorators.push_back(std::move(decorator));
 	}
 	}
 
 
 	return decorators;
 	return decorators;

+ 18 - 1
Source/Core/TypeConverter.cpp

@@ -30,7 +30,9 @@
 #include "../../Include/RmlUi/Core/StyleSheetSpecification.h"
 #include "../../Include/RmlUi/Core/StyleSheetSpecification.h"
 #include "../../Include/RmlUi/Core/StyleSheetTypes.h"
 #include "../../Include/RmlUi/Core/StyleSheetTypes.h"
 #include "../../Include/RmlUi/Core/Animation.h"
 #include "../../Include/RmlUi/Core/Animation.h"
+#include "../../Include/RmlUi/Core/DecoratorInstancer.h"
 #include "../../Include/RmlUi/Core/Transform.h"
 #include "../../Include/RmlUi/Core/Transform.h"
+#include "../../Include/RmlUi/Core/PropertySpecification.h"
 #include "../../Include/RmlUi/Core/TransformPrimitive.h"
 #include "../../Include/RmlUi/Core/TransformPrimitive.h"
 #include "../../Include/RmlUi/Core/PropertyDictionary.h"
 #include "../../Include/RmlUi/Core/PropertyDictionary.h"
 #include "TransformUtilities.h"
 #include "TransformUtilities.h"
@@ -127,8 +129,23 @@ bool TypeConverter<DecoratorsPtr, String>::Convert(const DecoratorsPtr& src, Str
 {
 {
 	if (!src || src->list.empty())
 	if (!src || src->list.empty())
 		dest = "none";
 		dest = "none";
-	else
+	else if (!src->value.empty())
 		dest += src->value;
 		dest += src->value;
+	else
+	{
+		dest.clear();
+		for (const DecoratorDeclaration& declaration : src->list)
+		{
+			dest += declaration.type;
+			if (auto instancer = declaration.instancer)
+			{
+				dest += '(' + instancer->GetPropertySpecification().PropertiesToString(declaration.properties, false, ' ') + ')';
+			}
+			dest += ", ";
+		}
+		if (dest.size() > 2)
+			dest.resize(dest.size() - 2);
+	}
 	return true;
 	return true;
 }
 }
 
 

+ 6 - 1
Tests/Source/Common/TestsInterface.cpp

@@ -33,7 +33,7 @@
 
 
 double TestsSystemInterface::GetElapsedTime()
 double TestsSystemInterface::GetElapsedTime()
 {
 {
-	return 0.0;
+	return elapsed_time;
 }
 }
 
 
 bool TestsSystemInterface::LogMessage(Rml::Log::Type type, const Rml::String& message)
 bool TestsSystemInterface::LogMessage(Rml::Log::Type type, const Rml::String& message)
@@ -78,6 +78,11 @@ void TestsSystemInterface::SetNumExpectedWarnings(int in_num_expected_warnings)
 	num_expected_warnings = in_num_expected_warnings;
 	num_expected_warnings = in_num_expected_warnings;
 }
 }
 
 
+void TestsSystemInterface::SetTime(double t)
+{
+	elapsed_time = t;
+}
+
 void TestsRenderInterface::RenderGeometry(Rml::Vertex* /*vertices*/, int /*num_vertices*/, int* /*indices*/, int /*num_indices*/, const Rml::TextureHandle /*texture*/, const Rml::Vector2f& /*translation*/)
 void TestsRenderInterface::RenderGeometry(Rml::Vertex* /*vertices*/, int /*num_vertices*/, int* /*indices*/, int /*num_indices*/, const Rml::TextureHandle /*texture*/, const Rml::Vector2f& /*translation*/)
 {
 {
 	counters.render_calls += 1;
 	counters.render_calls += 1;

+ 4 - 0
Tests/Source/Common/TestsInterface.h

@@ -43,7 +43,11 @@ public:
 	// warnings and errors until the next call.
 	// warnings and errors until the next call.
 	void SetNumExpectedWarnings(int num_expected_warnings);
 	void SetNumExpectedWarnings(int num_expected_warnings);
 
 
+	void SetTime(double t);
+
 private:
 private:
+	double elapsed_time = 0.0;
+
 	int num_logged_warnings = 0;
 	int num_logged_warnings = 0;
 	int num_expected_warnings = 0;
 	int num_expected_warnings = 0;
 
 

+ 5 - 0
Tests/Source/Common/TestsShell.cpp

@@ -226,3 +226,8 @@ TestsRenderInterface* TestsShell::GetTestsRenderInterface()
 	return &shell_render_interface;
 	return &shell_render_interface;
 #endif
 #endif
 }
 }
+
+TestsSystemInterface* TestsShell::GetTestsSystemInterface()
+{
+	return &tests_system_interface;
+}

+ 4 - 0
Tests/Source/Common/TestsShell.h

@@ -32,6 +32,7 @@
 #include <RmlUi/Core/Types.h>
 #include <RmlUi/Core/Types.h>
 namespace Rml { class RenderInterface; }
 namespace Rml { class RenderInterface; }
 class TestsRenderInterface;
 class TestsRenderInterface;
+class TestsSystemInterface;
 
 
 namespace TestsShell {
 namespace TestsShell {
 
 
@@ -52,11 +53,14 @@ namespace TestsShell {
 	// or until 'ShutdownShell()'.
 	// or until 'ShutdownShell()'.
 	void SetNumExpectedWarnings(int num_warnings);
 	void SetNumExpectedWarnings(int num_warnings);
 
 
+	void SetTime(double t);
+
 	// Stats only available for the dummy renderer.
 	// Stats only available for the dummy renderer.
 	Rml::String GetRenderStats();
 	Rml::String GetRenderStats();
 
 
 	// Returns nullptr if the dummy renderer is not being used.
 	// Returns nullptr if the dummy renderer is not being used.
 	TestsRenderInterface* GetTestsRenderInterface();
 	TestsRenderInterface* GetTestsRenderInterface();
+	TestsSystemInterface* GetTestsSystemInterface();
 }
 }
 
 
 #endif
 #endif

+ 221 - 0
Tests/Source/UnitTests/Animation.cpp

@@ -0,0 +1,221 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "../Common/TestsInterface.h"
+#include "../Common/TestsShell.h"
+#include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/Element.h>
+#include <RmlUi/Core/ElementDocument.h>
+#include <doctest.h>
+
+using namespace Rml;
+
+static const String document_decorator_rml = R"(
+<rml>
+<head>
+	<title>Test</title>
+	<link type="text/rcss" href="/assets/rml.rcss"/>
+	<style>
+		body {
+			left: 0;
+			top: 0;
+			right: 0;
+			bottom: 0;
+		}
+		
+		@decorator from_rule : gradient { %s }
+		@decorator to_rule: gradient{ %s }		
+
+		@keyframes mix {
+			from { decorator: %s; }
+			to   { decorator: %s; }
+		}
+		div {
+			background: #333;
+			height: 64px;
+			width: 64px;
+			animation: mix 0.1s;
+		}
+	</style>
+</head>
+
+<body>
+	<div/>
+</body>
+</rml>
+)";
+
+TEST_CASE("animation.decorator")
+{
+	struct Test {
+		String from_rule;
+		String to_rule;
+		String from;
+		String to;
+		String expected_25p; // expected interpolated value at 25% progression
+	};
+
+	Vector<Test> tests{
+		// Only standard declaration
+		{
+			"", "",
+			
+			"gradient(horizontal transparent transparent)",
+			"gradient(horizontal white white)",
+			
+			"gradient(horizontal rgba(255,255,255,63) rgba(255,255,255,63))",
+		},
+		{
+			"", "",
+			
+			"none",
+			"gradient(horizontal transparent transparent)",
+			
+			"gradient(horizontal rgba(255,255,255,191) rgba(255,255,255,191))",
+		},
+		{
+			"", "",
+			
+			"none",
+			"gradient(horizontal transparent transparent), gradient(vertical transparent transparent)",
+			
+			"gradient(horizontal rgba(255,255,255,191) rgba(255,255,255,191)), gradient(horizontal rgba(255,255,255,191) rgba(255,255,255,191))",
+		},
+		{
+			"", "",
+			
+			"gradient(horizontal transparent transparent), gradient(vertical transparent transparent)",
+			"none",
+			
+			"gradient(horizontal rgba(255,255,255,63) rgba(255,255,255,63)), gradient(vertical rgba(255,255,255,63) rgba(255,255,255,63))",
+		},
+
+		/// Only rule declaration
+		{
+			"direction: horizontal; start-color: transparent; stop-color: transparent;",
+			"direction: horizontal; start-color: white; stop-color: white;",
+			
+			"from_rule",
+			"to_rule",
+			
+			"gradient(horizontal rgba(255,255,255,63) rgba(255,255,255,63))",
+		},
+		{
+			"",
+			"direction: horizontal; start-color: transparent; stop-color: transparent;",
+			
+			"from_rule",
+			"to_rule",
+			
+			"gradient(horizontal rgba(255,255,255,191) rgba(255,255,255,191))",
+		},
+		{
+			"direction: vertical; start-color: transparent; stop-color: transparent;",
+			"",
+			
+			"from_rule",
+			"to_rule",
+			
+			"gradient(vertical rgba(255,255,255,63) rgba(255,255,255,63))",
+		},
+
+		/// Mix rule and standard declaration
+		{
+			"direction: horizontal; start-color: transparent; stop-color: transparent;",
+			"",
+
+			"from_rule",
+			"gradient(horizontal white white)",
+
+			"gradient(horizontal rgba(255,255,255,63) rgba(255,255,255,63))",
+		},
+		{
+			"",
+			"direction: horizontal; start-color: transparent; stop-color: transparent;",
+
+			"none",
+			"to_rule",
+
+			"gradient(horizontal rgba(255,255,255,191) rgba(255,255,255,191))",
+		},
+		{
+			"direction: vertical; start-color: transparent; stop-color: transparent;",
+			"",
+
+			"from_rule",
+			"none",
+
+			"gradient(vertical rgba(255,255,255,63) rgba(255,255,255,63))",
+		},
+		{
+			"", "",
+
+			"from_rule, to_rule",
+			"gradient(horizontal transparent transparent), gradient(vertical transparent transparent)",
+
+			"gradient(horizontal rgba(255,255,255,191) rgba(255,255,255,191)), gradient(horizontal rgba(255,255,255,191) rgba(255,255,255,191))",
+		},
+		{
+			"", "",
+
+			"gradient(horizontal transparent transparent), gradient(vertical transparent transparent)",
+			"from_rule, to_rule",
+
+			"gradient(horizontal rgba(255,255,255,63) rgba(255,255,255,63)), gradient(vertical rgba(255,255,255,63) rgba(255,255,255,63))",
+		},
+	};
+
+	TestsSystemInterface* system_interface = TestsShell::GetTestsSystemInterface();
+	Context* context = TestsShell::GetContext();
+	context->SetDensityIndependentPixelRatio(2.0f);
+
+	for (const Test& test : tests)
+	{
+		const double t_final = 0.1;
+
+		system_interface->SetTime(0.0);
+		String document_rml = Rml::CreateString(document_decorator_rml.size() + 512, document_decorator_rml.c_str(), test.from_rule.c_str(),
+			test.to_rule.c_str(), test.from.c_str(), test.to.c_str());
+
+		ElementDocument* document = context->LoadDocumentFromMemory(document_rml, "assets/");
+		Element* element = document->GetChild(0);
+
+		document->Show();
+		TestsShell::RenderLoop();
+
+		system_interface->SetTime(0.25 * t_final);
+		TestsShell::RenderLoop();
+		CHECK_MESSAGE(element->GetProperty<String>("decorator") == test.expected_25p, "from: ", test.from, ", to: ", test.to);
+
+		document->Close();
+	}
+
+	system_interface->SetTime(0.0);
+
+	TestsShell::ShutdownShell();
+}