Răsfoiți Sursa

Add Filter class, to be used with the new 'filter' and 'backdrop-filter' properties

Filters are implemented partially like decorators.
Michael Ragazzon 2 ani în urmă
părinte
comite
a5e3c7fc8d

+ 6 - 0
CMake/FileList.cmake

@@ -80,6 +80,7 @@ set(Core_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserAnimation.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserColour.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserDecorator.h
+    ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserFilter.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserFontEffect.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserKeyword.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserNumber.h
@@ -138,6 +139,7 @@ set(Core_PUB_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Debug.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Decorator.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Dictionary.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/EffectSpecification.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Element.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Element.inl
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ElementDocument.h
@@ -158,6 +160,7 @@ set(Core_PUB_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/EventListenerInstancer.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Factory.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/FileInterface.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Filter.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/FontEffect.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/FontEffectInstancer.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/FontEngineInterface.h
@@ -251,6 +254,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorTiledImage.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorTiledVertical.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DocumentHeader.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/EffectSpecification.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Element.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/ElementAnimation.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/ElementBackgroundBorder.cpp
@@ -298,6 +302,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/Factory.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/FileInterface.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/FileInterfaceDefault.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/Filter.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/FontEffect.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/FontEffectBlur.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/FontEffectGlow.cpp
@@ -340,6 +345,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserAnimation.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserColour.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserDecorator.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserFilter.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserFontEffect.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserKeyword.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserNumber.cpp

+ 2 - 0
Include/RmlUi/Core.h

@@ -41,6 +41,7 @@
 #include "Core/DataTypes.h"
 #include "Core/DataVariable.h"
 #include "Core/Decorator.h"
+#include "Core/EffectSpecification.h"
 #include "Core/Element.h"
 #include "Core/ElementDocument.h"
 #include "Core/ElementInstancer.h"
@@ -53,6 +54,7 @@
 #include "Core/EventListenerInstancer.h"
 #include "Core/Factory.h"
 #include "Core/FileInterface.h"
+#include "Core/Filter.h"
 #include "Core/FontEffect.h"
 #include "Core/FontEffectInstancer.h"
 #include "Core/FontEngineInterface.h"

+ 10 - 1
Include/RmlUi/Core/ComputedValues.h

@@ -157,7 +157,9 @@ namespace Style {
 
 			flex_basis_type(LengthPercentageAuto::Auto), row_gap_type(LengthPercentage::Length), column_gap_type(LengthPercentage::Length),
 
-			vertical_align_type(VerticalAlign::Baseline), drag(Drag::None), tab_index(TabIndex::None), overscroll_behavior(OverscrollBehavior::Auto)
+			vertical_align_type(VerticalAlign::Baseline), drag(Drag::None), tab_index(TabIndex::None), overscroll_behavior(OverscrollBehavior::Auto),
+
+			has_filter(false), has_backdrop_filter(false)
 		{}
 
 		LengthPercentage::Type min_width_type : 1, max_width_type : 1;
@@ -175,6 +177,9 @@ namespace Style {
 		TabIndex tab_index : 1;
 		OverscrollBehavior overscroll_behavior : 1;
 
+		bool has_filter : 1;
+		bool has_backdrop_filter : 1;
+
 		Clip clip;
 
 		float min_width = 0, max_width = FLT_MAX;
@@ -296,6 +301,8 @@ namespace Style {
 		LengthPercentage  column_gap()                 const { return LengthPercentage(rare.column_gap_type, rare.column_gap); }
 		OverscrollBehavior overscroll_behavior()       const { return rare.overscroll_behavior; }
 		float             scrollbar_margin()           const { return rare.scrollbar_margin; }
+		bool              has_filter()                 const { return rare.has_filter; }
+		bool              has_backdrop_filter()        const { return rare.has_backdrop_filter; }
 		
 		// -- Assignment --
 		// Common
@@ -376,6 +383,8 @@ namespace Style {
 		void image_color               (Colourb value)           { rare.image_color                = value; }
 		void overscroll_behavior       (OverscrollBehavior value){ rare.overscroll_behavior        = value; }
 		void scrollbar_margin          (float value)             { rare.scrollbar_margin           = value; }
+		void has_filter                (bool value)              { rare.has_filter                 = value; }
+		void has_backdrop_filter       (bool value)              { rare.has_backdrop_filter        = value; }
 
 		// clang-format on
 

+ 3 - 32
Include/RmlUi/Core/Decorator.h

@@ -29,6 +29,7 @@
 #ifndef RMLUI_CORE_DECORATOR_H
 #define RMLUI_CORE_DECORATOR_H
 
+#include "EffectSpecification.h"
 #include "Header.h"
 #include "PropertyDictionary.h"
 #include "PropertySpecification.h"
@@ -37,22 +38,18 @@
 
 namespace Rml {
 
-class DecoratorInstancer;
 class Element;
 class PropertyDictionary;
-
 struct Sprite;
 struct Texture;
 class StyleSheet;
 class DecoratorInstancerInterface;
-class PropertyDefinition;
 
 /**
     The abstract base class for any visual object that can be attached to any element.
 
     @author Peter Curry
  */
-
 class RMLUICORE_API Decorator {
 public:
 	Decorator();
@@ -94,15 +91,9 @@ private:
 };
 
 /**
-    An element instancer provides a method for allocating and deallocating decorators.
-
-    It is important at the same instancer that allocated a decorator releases it. This ensures there are no issues with
-    memory from different DLLs getting mixed up.
-
-    @author Peter Curry
+    A decorator instancer, which can be inherited from to instance new decorators when encountered in the style sheet.
  */
-
-class RMLUICORE_API DecoratorInstancer {
+class RMLUICORE_API DecoratorInstancer : public EffectSpecification {
 public:
 	DecoratorInstancer();
 	virtual ~DecoratorInstancer();
@@ -114,26 +105,6 @@ public:
 	/// @return A shared_ptr to the decorator if it was instanced successfully.
 	virtual SharedPtr<Decorator> InstanceDecorator(const String& name, const PropertyDictionary& properties,
 		const DecoratorInstancerInterface& instancer_interface) = 0;
-
-	/// Returns the property specification associated with the instancer.
-	const PropertySpecification& GetPropertySpecification() const;
-
-protected:
-	/// Registers a property for the decorator.
-	/// @param[in] property_name The name of the new property (how it is specified through RCSS).
-	/// @param[in] default_value The default value to be used.
-	/// @return The new property definition, ready to have parsers attached.
-	PropertyDefinition& RegisterProperty(const String& property_name, const String& default_value);
-	/// Registers a shorthand property definition. Specify a shorthand name of 'decorator' to parse anonymous decorators.
-	/// @param[in] shorthand_name The name to register the new shorthand property under.
-	/// @param[in] properties A comma-separated list of the properties this definition is shorthand for. The order in which they are specified here is
-	/// the order in which the values will be processed.
-	/// @param[in] type The type of shorthand to declare.
-	/// @param True if all the property names exist, false otherwise.
-	ShorthandId RegisterShorthand(const String& shorthand_name, const String& property_names, ShorthandType type);
-
-private:
-	PropertySpecification properties;
 };
 
 class RMLUICORE_API DecoratorInstancerInterface {

+ 69 - 0
Include/RmlUi/Core/EffectSpecification.h

@@ -0,0 +1,69 @@
+/*
+ * 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-2023 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.
+ *
+ */
+
+#ifndef RMLUI_CORE_EFFECTSPECIFICATION_H
+#define RMLUI_CORE_EFFECTSPECIFICATION_H
+
+#include "Header.h"
+#include "PropertySpecification.h"
+#include "Types.h"
+
+namespace Rml {
+
+class PropertyDefinition;
+
+class RMLUICORE_API EffectSpecification {
+public:
+	EffectSpecification();
+
+	/// Returns the property specification associated with the instancer.
+	const PropertySpecification& GetPropertySpecification() const;
+
+protected:
+	~EffectSpecification();
+
+	/// Registers a property for the decorator.
+	/// @param[in] property_name The name of the new property (how it is specified through RCSS).
+	/// @param[in] default_value The default value to be used.
+	/// @return The new property definition, ready to have parsers attached.
+	PropertyDefinition& RegisterProperty(const String& property_name, const String& default_value);
+
+	/// Registers a shorthand property definition. Specify a shorthand name of 'decorator' to parse anonymous decorators.
+	/// @param[in] shorthand_name The name to register the new shorthand property under.
+	/// @param[in] properties A comma-separated list of the properties this definition is shorthand for. The order in which they are specified here is
+	/// the order in which the values will be processed.
+	/// @param[in] type The type of shorthand to declare.
+	/// @param True if all the property names exist, false otherwise.
+	ShorthandId RegisterShorthand(const String& shorthand_name, const String& property_names, ShorthandType type);
+
+private:
+	PropertySpecification properties;
+};
+
+} // namespace Rml
+#endif

+ 13 - 0
Include/RmlUi/Core/Factory.h

@@ -49,6 +49,8 @@ class EventListener;
 class EventListenerInstancer;
 class FontEffect;
 class FontEffectInstancer;
+class Filter;
+class FilterInstancer;
 class StyleSheetContainer;
 class PropertyDictionary;
 class PropertySpecification;
@@ -126,6 +128,17 @@ public:
 	/// @return The decorator instancer it it exists, nullptr otherwise.
 	static DecoratorInstancer* GetDecoratorInstancer(const String& name);
 
+	/// Registers a non-owning pointer to an instancer that will be used to instance filters.
+	/// @param[in] name The name of the filter the instancer will be called for.
+	/// @param[in] instancer The instancer to call when the filter name is encountered.
+	/// @lifetime The instancer must be kept alive until after the call to Rml::Shutdown.
+	/// @return The added instancer if the registration was successful, nullptr otherwise.
+	static void RegisterFilterInstancer(const String& name, FilterInstancer* instancer);
+	/// Retrieves a filter instancer registered with the factory.
+	/// @param[in] name The name of the desired filter type.
+	/// @return The filter instancer it it exists, nullptr otherwise.
+	static FilterInstancer* GetFilterInstancer(const String& name);
+
 	/// Registers a non-owning pointer to an instancer that will be used to instance font effects.
 	/// @param[in] name The name of the font effect the instancer will be called for.
 	/// @param[in] instancer The instancer to call when the font effect name is encountered.

+ 85 - 0
Include/RmlUi/Core/Filter.h

@@ -0,0 +1,85 @@
+/*
+ * 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-2023 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.
+ *
+ */
+
+#ifndef RMLUI_CORE_FILTER_H
+#define RMLUI_CORE_FILTER_H
+
+#include "EffectSpecification.h"
+#include "Header.h"
+#include "Types.h"
+
+namespace Rml {
+
+class Element;
+class PropertyDictionary;
+
+/**
+    The abstract base class for visual filters that are applied when rendering the element.
+ */
+class RMLUICORE_API Filter {
+public:
+	Filter();
+	virtual ~Filter();
+
+	/// Called on a decorator to generate any required per-element data for a newly decorated element.
+	/// @param[in] element The newly decorated element.
+	/// @return A handle to a decorator-defined data handle, or nullptr if none is needed for the element.
+	virtual CompiledFilterHandle CompileFilter(Element* element) const = 0;
+
+	/// Called to release element data generated by this decorator.
+	/// @param[in] element_data The element data handle to release.
+	virtual void ReleaseCompiledFilter(Element* element, CompiledFilterHandle filter_handle) const = 0;
+
+	/// Allows extending the area being affected by this filter beyond the border box of the element.
+	/// @param[in] element The element the filter is being rendered on.
+	/// @param[in,out] overflow The ink overflow rectangle determining the clipping region to be applied when filtering the current element.
+	/// @note Modifying the ink overflow rectangle affects rendering of all filter decorators active on the current element.
+	/// @note Only affects the 'filter' property, not 'backdrop-filter'.
+	virtual void ExtendInkOverflow(Element* element, Rectanglef& overflow) const;
+
+	/// Value specifying an invalid or non-existent filter handle.
+	static constexpr CompiledFilterHandle INVALID_COMPILEDFILTERHANDLE = 0;
+};
+
+/**
+    A filter instancer, which can be inherited from to instance new filters when encountered in the style sheet.
+ */
+class RMLUICORE_API FilterInstancer : public EffectSpecification {
+public:
+	FilterInstancer();
+	virtual ~FilterInstancer();
+
+	/// Instances a filter given the name and attributes from the RCSS file.
+	/// @param[in] name The type of filter desired. For example, "filter: simple(...)" is declared as type "simple".
+	/// @param[in] properties All RCSS properties associated with the filter.
+	/// @return A shared_ptr to the filter if it was instanced successfully.
+	virtual SharedPtr<Filter> InstanceFilter(const String& name, const PropertyDictionary& properties) = 0;
+};
+
+} // namespace Rml
+#endif

+ 2 - 0
Include/RmlUi/Core/ID.h

@@ -156,6 +156,8 @@ enum class PropertyId : uint8_t {
 
 	Decorator,
 	FontEffect,
+	Filter,
+	BackdropFilter,
 
 	FillImage,
 

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

@@ -68,6 +68,15 @@ struct DecoratorDeclarationList {
 	Vector<DecoratorDeclaration> list;
 	String value;
 };
+struct FilterDeclaration {
+	String type;
+	FilterInstancer* instancer;
+	PropertyDictionary properties;
+};
+struct FilterDeclarationList {
+	Vector<FilterDeclaration> list;
+	String value;
+};
 
 struct MediaBlock {
 	MediaBlock() {}

+ 10 - 0
Include/RmlUi/Core/TypeConverter.h

@@ -123,6 +123,16 @@ class TypeConverter<DecoratorsPtr, String> {
 public:
 	RMLUICORE_API static bool Convert(const DecoratorsPtr& src, String& dest);
 };
+template <>
+class TypeConverter<FiltersPtr, FiltersPtr> {
+public:
+	RMLUICORE_API static bool Convert(const FiltersPtr& src, FiltersPtr& dest);
+};
+template <>
+class TypeConverter<FiltersPtr, String> {
+public:
+	RMLUICORE_API static bool Convert(const FiltersPtr& src, String& dest);
+};
 
 template <>
 class TypeConverter<FontEffectsPtr, FontEffectsPtr> {

+ 4 - 3
Include/RmlUi/Core/Unit.h

@@ -75,9 +75,10 @@ enum class Unit {
 	TRANSITION = 1 << 21,    // transition; fetch as <TransitionList>
 	ANIMATION = 1 << 22,     // animation; fetch as <AnimationList>
 	DECORATOR = 1 << 23,     // decorator; fetch as <DecoratorsPtr>
-	FONTEFFECT = 1 << 24,    // font-effect; fetch as <FontEffectsPtr>
-	COLORSTOPLIST = 1 << 25, // color stop list; fetch as <ColorStopList>
-	SHADOWLIST = 1 << 26,    // shadow list; fetch as <ShadowList>
+	FILTER = 1 << 24,        // decorator; fetch as <FiltersPtr>
+	FONTEFFECT = 1 << 25,    // font-effect; fetch as <FontEffectsPtr>
+	COLORSTOPLIST = 1 << 26, // color stop list; fetch as <ColorStopList>
+	SHADOWLIST = 1 << 27,    // shadow list; fetch as <ShadowList>
 
 	LENGTH = PX | DP | VW | VH | EM | REM | PPI_UNIT,
 	LENGTH_PERCENT = LENGTH | PERCENT,

+ 4 - 1
Include/RmlUi/Core/Variant.h

@@ -70,7 +70,8 @@ public:
 		TRANSITIONLIST = 'T',
 		ANIMATIONLIST = 'A',
 		DECORATORSPTR = 'D',
-		FONTEFFECTSPTR = 'F',
+		FILTERSPTR = 'F',
+		FONTEFFECTSPTR = 'E',
 		VOIDPTR = '*',
 	};
 
@@ -153,6 +154,8 @@ private:
 	void Set(AnimationList&& value);
 	void Set(const DecoratorsPtr& value);
 	void Set(DecoratorsPtr&& value);
+	void Set(const FiltersPtr& value);
+	void Set(FiltersPtr&& value);
 	void Set(const FontEffectsPtr& value);
 	void Set(FontEffectsPtr&& value);
 

+ 1 - 0
Include/RmlUi/Core/Variant.inl

@@ -73,6 +73,7 @@ bool Variant::GetInto(T& value) const
 	case TRANSITIONLIST: return TypeConverter<TransitionList, T>::Convert(*reinterpret_cast<const TransitionList*>(data), value);
 	case ANIMATIONLIST: return TypeConverter<AnimationList, T>::Convert(*reinterpret_cast<const AnimationList*>(data), value);
 	case DECORATORSPTR: return TypeConverter<DecoratorsPtr, T>::Convert(*reinterpret_cast<const DecoratorsPtr*>(data), value);
+	case FILTERSPTR: return TypeConverter<FiltersPtr, T>::Convert(*reinterpret_cast<const FiltersPtr*>(data), value);
 	case FONTEFFECTSPTR: return TypeConverter<FontEffectsPtr, T>::Convert(*reinterpret_cast<const FontEffectsPtr*>(data), value);
 	case NONE: break;
 	}

+ 1 - 17
Source/Core/Decorator.cpp

@@ -30,7 +30,6 @@
 #include "../../Include/RmlUi/Core/PropertyDefinition.h"
 #include "../../Include/RmlUi/Core/StyleSheet.h"
 #include "../../Include/RmlUi/Core/Texture.h"
-#include "TextureDatabase.h"
 #include <algorithm>
 
 namespace Rml {
@@ -77,25 +76,10 @@ const Texture* Decorator::GetTexture(int index) const
 	return &(additional_textures[index]);
 }
 
-DecoratorInstancer::DecoratorInstancer() : properties(10, 10) {}
+DecoratorInstancer::DecoratorInstancer() {}
 
 DecoratorInstancer::~DecoratorInstancer() {}
 
-const PropertySpecification& DecoratorInstancer::GetPropertySpecification() const
-{
-	return properties;
-}
-
-PropertyDefinition& DecoratorInstancer::RegisterProperty(const String& property_name, const String& default_value)
-{
-	return properties.RegisterProperty(property_name, default_value, false, false);
-}
-
-ShorthandId DecoratorInstancer::RegisterShorthand(const String& shorthand_name, const String& property_names, ShorthandType type)
-{
-	return properties.RegisterShorthand(shorthand_name, property_names, type);
-}
-
 const Sprite* DecoratorInstancerInterface::GetSprite(const String& name) const
 {
 	return style_sheet.GetSprite(name);

+ 53 - 0
Source/Core/EffectSpecification.cpp

@@ -0,0 +1,53 @@
+/*
+ * 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-2023 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 "../../Include/RmlUi/Core/EffectSpecification.h"
+#include "../../Include/RmlUi/Core/PropertyDefinition.h"
+
+namespace Rml {
+
+EffectSpecification::EffectSpecification() : properties(10, 10) {}
+
+EffectSpecification::~EffectSpecification() {}
+
+const PropertySpecification& EffectSpecification::GetPropertySpecification() const
+{
+	return properties;
+}
+
+PropertyDefinition& EffectSpecification::RegisterProperty(const String& property_name, const String& default_value)
+{
+	return properties.RegisterProperty(property_name, default_value, false, false);
+}
+
+ShorthandId EffectSpecification::RegisterShorthand(const String& shorthand_name, const String& property_names, ShorthandType type)
+{
+	return properties.RegisterShorthand(shorthand_name, property_names, type);
+}
+
+} // namespace Rml

+ 7 - 2
Source/Core/Element.cpp

@@ -233,11 +233,13 @@ void Element::Render()
 	// Apply our transform
 	ElementUtilities::ApplyTransform(*this);
 
+	meta->decoration.RenderDecorators(RenderStage::Enter);
+
 	// Set up the clipping region for this element.
 	if (ElementUtilities::SetClippingRegion(this))
 	{
 		meta->background_border.Render(this);
-		meta->decoration.RenderDecorators();
+		meta->decoration.RenderDecorators(RenderStage::Decoration);
 
 		{
 			RMLUI_ZoneScopedNC("OnRender", 0x228B22);
@@ -249,6 +251,8 @@ void Element::Render()
 	// Render all elements in our local stacking context.
 	for (Element* element : stacking_context)
 		element->Render();
+
+	meta->decoration.RenderDecorators(RenderStage::Exit);
 }
 
 ElementPtr Element::Clone() const
@@ -1765,6 +1769,7 @@ void Element::OnPropertyChange(const PropertyIdSet& changed_properties)
 		changed_properties.Contains(PropertyId::BorderBottomRightRadius) || //
 		changed_properties.Contains(PropertyId::BorderBottomLeftRadius)     //
 	);
+	const bool filter_changed = (changed_properties.Contains(PropertyId::Filter) || changed_properties.Contains(PropertyId::BackdropFilter));
 
 	// Dirty the background if it's changed.
 	if (border_radius_changed ||                                    //
@@ -1791,7 +1796,7 @@ void Element::OnPropertyChange(const PropertyIdSet& changed_properties)
 	}
 
 	// Dirty the decoration if it's changed.
-	if (border_radius_changed || changed_properties.Contains(PropertyId::Decorator))
+	if (border_radius_changed || filter_changed || changed_properties.Contains(PropertyId::Decorator))
 	{
 		meta->decoration.DirtyDecorators();
 	}

+ 182 - 46
Source/Core/ElementDecoration.cpp

@@ -28,10 +28,14 @@
 
 #include "ElementDecoration.h"
 #include "../../Include/RmlUi/Core/ComputedValues.h"
+#include "../../Include/RmlUi/Core/Context.h"
 #include "../../Include/RmlUi/Core/Decorator.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/ElementDocument.h"
+#include "../../Include/RmlUi/Core/ElementUtilities.h"
+#include "../../Include/RmlUi/Core/Filter.h"
 #include "../../Include/RmlUi/Core/Profiling.h"
+#include "../../Include/RmlUi/Core/RenderInterface.h"
 #include "../../Include/RmlUi/Core/StyleSheet.h"
 
 namespace Rml {
@@ -45,61 +49,89 @@ ElementDecoration::~ElementDecoration()
 
 void ElementDecoration::InstanceDecorators()
 {
-	if (decorators_dirty)
-	{
-		decorators_dirty = false;
-		decorators_data_dirty = true;
-		ReloadDecorators();
-	}
-}
+	if (!decorators_dirty)
+		return;
+
+	decorators_dirty = false;
+	decorators_data_dirty = true;
 
-bool ElementDecoration::ReloadDecorators()
-{
 	RMLUI_ZoneScopedC(0xB22222);
 	ReleaseDecorators();
 
-	if (!element->GetComputedValues().has_decorator())
-		return true;
+	const ComputedValues& computed = element->GetComputedValues();
 
-	const Property* property = element->GetLocalProperty(PropertyId::Decorator);
-	if (!property || property->unit != Unit::DECORATOR)
-		return false;
+	if (computed.has_decorator())
+	{
+		const Property* property = element->GetLocalProperty(PropertyId::Decorator);
+		if (!property || property->unit != Unit::DECORATOR)
+			return;
 
-	DecoratorsPtr decorators_ptr = property->Get<DecoratorsPtr>();
-	if (!decorators_ptr)
-		return false;
+		DecoratorsPtr decorators_ptr = property->Get<DecoratorsPtr>();
+		if (!decorators_ptr)
+			return;
 
-	const StyleSheet* style_sheet = element->GetStyleSheet();
-	if (!style_sheet)
-		return false;
+		const StyleSheet* style_sheet = element->GetStyleSheet();
+		if (!style_sheet)
+			return;
 
-	PropertySource document_source("", 0, "");
-	const PropertySource* source = property->source.get();
+		PropertySource document_source("", 0, "");
+		const PropertySource* source = property->source.get();
 
-	if (!source)
-	{
-		if (ElementDocument* document = element->GetOwnerDocument())
+		if (!source)
 		{
-			document_source.path = document->GetSourceURL();
-			source = &document_source;
+			if (ElementDocument* document = element->GetOwnerDocument())
+			{
+				document_source.path = document->GetSourceURL();
+				source = &document_source;
+			}
 		}
-	}
 
-	const DecoratorPtrList& 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)
-	{
-		if (decorator)
+		for (const SharedPtr<const Decorator>& decorator : decorator_list)
 		{
-			DecoratorHandle decorator_handle;
-			decorator_handle.decorator_data = 0;
-			decorator_handle.decorator = decorator;
+			if (decorator)
+			{
+				DecoratorEntry decorator_handle;
+				decorator_handle.decorator_data = 0;
+				decorator_handle.decorator = decorator;
 
-			decorators.push_back(std::move(decorator_handle));
+				decorators.push_back(std::move(decorator_handle));
+			}
 		}
 	}
 
-	return true;
+	if (computed.has_filter() || computed.has_backdrop_filter())
+	{
+		for (const auto id : {PropertyId::Filter, PropertyId::BackdropFilter})
+		{
+			const Property* property = element->GetLocalProperty(id);
+			if (!property || property->unit != Unit::FILTER)
+				return;
+
+			FiltersPtr filters_ptr = property->Get<FiltersPtr>();
+			if (!filters_ptr)
+				return;
+
+			FilterEntryList& list = (id == PropertyId::Filter ? filters : backdrop_filters);
+			list.reserve(filters_ptr->list.size());
+
+			for (const FilterDeclaration& declaration : filters_ptr->list)
+			{
+				SharedPtr<const Filter> filter = declaration.instancer->InstanceFilter(declaration.type, declaration.properties);
+				if (filter)
+				{
+					list.push_back({std::move(filter), CompiledFilterHandle{}});
+				}
+				else
+				{
+					const auto& source = property->source;
+					Log::Message(Log::LT_WARNING, "Filter '%s' in '%s' could not be instanced, declared at %s:%d", declaration.type.c_str(),
+						filters_ptr->value.c_str(), source ? source->path.c_str() : "", source ? source->line_number : -1);
+				}
+			}
+		}
+	}
 }
 
 void ElementDecoration::ReloadDecoratorsData()
@@ -108,38 +140,142 @@ void ElementDecoration::ReloadDecoratorsData()
 	{
 		decorators_data_dirty = false;
 
-		for (DecoratorHandle& decorator : decorators)
+		for (DecoratorEntry& decorator : decorators)
 		{
 			if (decorator.decorator_data)
 				decorator.decorator->ReleaseElementData(decorator.decorator_data);
 
 			decorator.decorator_data = decorator.decorator->GenerateElementData(element);
 		}
+
+		for (FilterEntryList* list : {&filters, &backdrop_filters})
+		{
+			for (FilterEntry& filter : *list)
+			{
+				if (filter.handle)
+					filter.filter->ReleaseCompiledFilter(element, filter.handle);
+
+				filter.handle = filter.filter->CompileFilter(element);
+			}
+		}
 	}
 }
 
 void ElementDecoration::ReleaseDecorators()
 {
-	for (DecoratorHandle& decorator : decorators)
+	for (DecoratorEntry& decorator : decorators)
 	{
 		if (decorator.decorator_data)
 			decorator.decorator->ReleaseElementData(decorator.decorator_data);
 	}
-
 	decorators.clear();
+
+	for (FilterEntryList* list : {&filters, &backdrop_filters})
+	{
+		for (FilterEntry& filter : *list)
+		{
+			if (filter.handle)
+				filter.filter->ReleaseCompiledFilter(element, filter.handle);
+		}
+		list->clear();
+	}
 }
 
-void ElementDecoration::RenderDecorators()
+void ElementDecoration::RenderDecorators(RenderStage render_stage)
 {
 	InstanceDecorators();
 	ReloadDecoratorsData();
 
-	// Render the decorators attached to this element in its current state.
-	// Render from back to front for correct render order.
-	for (int i = (int)decorators.size() - 1; i >= 0; i--)
+	if (!decorators.empty())
 	{
-		DecoratorHandle& decorator = decorators[i];
-		decorator.decorator->RenderElement(element, decorator.decorator_data);
+		if (render_stage == RenderStage::Decoration)
+		{
+			// Render the decorators attached to this element in its current state.
+			// Render from back to front for correct render order.
+			for (int i = (int)decorators.size() - 1; i >= 0; i--)
+			{
+				DecoratorEntry& decorator = decorators[i];
+				decorator.decorator->RenderElement(element, decorator.decorator_data);
+			}
+		}
+	}
+
+	if (filters.empty() && backdrop_filters.empty())
+		return;
+
+	RenderInterface* render_interface = ::Rml::GetRenderInterface();
+	Context* context = element->GetContext();
+	if (!render_interface || !context)
+		return;
+
+	auto ApplyClippingRegion = [this, render_interface, context](bool extend_ink_overflow) {
+		ElementUtilities::SetClippingRegion(element); // TODO: For backdrop-filter only: Force clipping to our border-box.
+
+		// Find the region being affected by the active filters and apply it as a scissor.
+		Rectanglef filter_region = Rectanglef::MakeInvalid();
+		ElementUtilities::GetBoundingBox(filter_region, element, BoxArea::Auto);
+
+		if (extend_ink_overflow)
+		{
+			for (const auto& filter : filters)
+				filter.filter->ExtendInkOverflow(element, filter_region);
+		}
+
+		Math::ExpandToPixelGrid(filter_region);
+
+		Rectanglei scissor_region = Rectanglei::FromSize(context->GetDimensions());
+		Vector2i clip_position, clip_size;
+		if (context->GetActiveClipRegion(clip_position, clip_size))
+			scissor_region.Intersect(Rectanglei::FromPositionSize(clip_position, clip_size));
+
+		scissor_region.IntersectIfValid(Rectanglei(filter_region));
+
+		render_interface->EnableScissorRegion(true);
+		render_interface->SetScissorRegion(scissor_region.Left(), scissor_region.Top(), scissor_region.Width(), scissor_region.Height());
+	};
+
+	if (!backdrop_filters.empty())
+	{
+		if (render_stage == RenderStage::Enter)
+		{
+			ApplyClippingRegion(false);
+
+			render_interface->PushLayer(LayerFill::Clone);
+
+			FilterHandleList filter_handles;
+			for (auto& filter : backdrop_filters)
+			{
+				if (filter.handle)
+					filter_handles.push_back(filter.handle);
+			}
+
+			render_interface->PopLayer(BlendMode::Replace, filter_handles);
+
+			ElementUtilities::ApplyActiveClipRegion(context);
+		}
+	}
+
+	if (!filters.empty())
+	{
+		if (render_stage == RenderStage::Enter)
+		{
+			render_interface->PushLayer(LayerFill::Clear);
+		}
+		else if (render_stage == RenderStage::Exit)
+		{
+			ApplyClippingRegion(true);
+
+			FilterHandleList filter_handles;
+			for (auto& filter : filters)
+			{
+				if (filter.handle)
+					filter_handles.push_back(filter.handle);
+			}
+
+			render_interface->PopLayer(BlendMode::Blend, filter_handles);
+
+			ElementUtilities::ApplyActiveClipRegion(context);
+		}
 	}
 }
 

+ 15 - 10
Source/Core/ElementDecoration.h

@@ -34,8 +34,11 @@
 namespace Rml {
 
 class Decorator;
+class Filter;
 class Element;
 
+enum class RenderStage { Enter, Decoration, Exit };
+
 /**
     Manages an elements decorator state
 
@@ -44,8 +47,6 @@ class Element;
 
 class ElementDecoration {
 public:
-	/// Constructor
-	/// @param element The element this decorator with acting on
 	ElementDecoration(Element* element);
 	~ElementDecoration();
 
@@ -53,7 +54,7 @@ public:
 	void InstanceDecorators();
 
 	/// Renders all appropriate decorators.
-	void RenderDecorators();
+	void RenderDecorators(RenderStage render_stage);
 
 	/// Mark decorators as dirty and force them to reset themselves.
 	void DirtyDecorators();
@@ -61,25 +62,29 @@ public:
 	void DirtyDecoratorsData();
 
 private:
-	// Releases existing decorators and loads all decorators required by the element's definition.
-	bool ReloadDecorators();
 	// Releases existing element data of decorators, and regenerates it.
 	void ReloadDecoratorsData();
 	// Releases all existing decorators and frees their data.
 	void ReleaseDecorators();
 
-	struct DecoratorHandle {
+	struct DecoratorEntry {
 		SharedPtr<const Decorator> decorator;
 		DecoratorDataHandle decorator_data;
 	};
+	using DecoratorEntryList = Vector<DecoratorEntry>;
 
-	using DecoratorHandleList = Vector<DecoratorHandle>;
+	struct FilterEntry {
+		SharedPtr<const Filter> filter;
+		CompiledFilterHandle handle;
+	};
+	using FilterEntryList = Vector<FilterEntry>;
 
-	// The element this decorator belongs to
 	Element* element;
 
-	// The list of every decorator used by this element in every class.
-	DecoratorHandleList decorators;
+	// The list of decorators and filters used by this element from all style rules.
+	DecoratorEntryList decorators;
+	FilterEntryList filters;
+	FilterEntryList backdrop_filters;
 
 	// If set, a full reload is necessary.
 	bool decorators_dirty = false;

+ 8 - 1
Source/Core/ElementStyle.cpp

@@ -850,8 +850,15 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 			values.has_decorator(p->unit == Unit::DECORATOR);
 			break;
 		case PropertyId::FontEffect:
-			values.has_font_effect((p->unit == Unit::FONTEFFECT));
+			values.has_font_effect(p->unit == Unit::FONTEFFECT);
 			break;
+		case PropertyId::Filter:
+			values.has_filter(p->unit == Unit::FILTER);
+			break;
+		case PropertyId::BackdropFilter:
+			values.has_backdrop_filter(p->unit == Unit::FILTER);
+			break;
+
 		case PropertyId::FlexBasis:
 			values.flex_basis(ComputeLengthPercentageAuto(p, font_size, document_font_size, dp_ratio, vp_dimensions));
 			break;

+ 19 - 0
Source/Core/Factory.cpp

@@ -87,6 +87,10 @@ static ElementInstancerMap element_instancers;
 using DecoratorInstancerMap = UnorderedMap<String, DecoratorInstancer*>;
 static DecoratorInstancerMap decorator_instancers;
 
+// Filter instancers.
+using FilterInstancerMap = UnorderedMap<String, FilterInstancer*>;
+static FilterInstancerMap filter_instancers;
+
 // Font effect instancers.
 using FontEffectInstancerMap = UnorderedMap<String, FontEffectInstancer*>;
 static FontEffectInstancerMap font_effect_instancers;
@@ -483,6 +487,21 @@ DecoratorInstancer* Factory::GetDecoratorInstancer(const String& name)
 	return iterator->second;
 }
 
+void Factory::RegisterFilterInstancer(const String& name, FilterInstancer* instancer)
+{
+	RMLUI_ASSERT(instancer);
+	filter_instancers[StringUtilities::ToLower(name)] = instancer;
+}
+
+FilterInstancer* Factory::GetFilterInstancer(const String& name)
+{
+	auto iterator = filter_instancers.find(name);
+	if (iterator == filter_instancers.end())
+		return nullptr;
+
+	return iterator->second;
+}
+
 void Factory::RegisterFontEffectInstancer(const String& name, FontEffectInstancer* instancer)
 {
 	RMLUI_ASSERT(instancer);

+ 43 - 0
Source/Core/Filter.cpp

@@ -0,0 +1,43 @@
+/*
+ * 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-2023 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 "../../Include/RmlUi/Core/Filter.h"
+
+namespace Rml {
+
+Filter::Filter() {}
+
+Filter::~Filter() {}
+
+void Filter::ExtendInkOverflow(Element* /*element*/, Rectanglef& /*scissor_region*/) const {}
+
+FilterInstancer::FilterInstancer() {}
+
+FilterInstancer::~FilterInstancer() {}
+
+} // namespace Rml

+ 119 - 0
Source/Core/PropertyParserFilter.cpp

@@ -0,0 +1,119 @@
+/*
+ * 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-2023 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 "PropertyParserFilter.h"
+#include "../../Include/RmlUi/Core/Factory.h"
+#include "../../Include/RmlUi/Core/Filter.h"
+#include "../../Include/RmlUi/Core/Profiling.h"
+#include "../../Include/RmlUi/Core/PropertySpecification.h"
+#include "../../Include/RmlUi/Core/StyleSheetTypes.h"
+
+namespace Rml {
+
+PropertyParserFilter::PropertyParserFilter() {}
+
+PropertyParserFilter::~PropertyParserFilter() {}
+
+bool PropertyParserFilter::ParseValue(Property& property, const String& filter_string_value, const ParameterMap& /*parameters*/) const
+{
+	// Filters are declared as
+	//   filter: <filter-value>[ <filter-value> ...];
+	// Where <filter-value> is specified with inline properties
+	//   filter: brightness( <shorthand properties> ) ...;
+
+	if (filter_string_value.empty() || filter_string_value == "none")
+	{
+		property.value = Variant(FiltersPtr());
+		property.unit = Unit::FILTER;
+		return true;
+	}
+
+	RMLUI_ZoneScoped;
+
+	// Make sure we don't split inside the parenthesis since they may appear in filter shorthands.
+	StringList filter_string_list;
+	StringUtilities::ExpandString(filter_string_list, filter_string_value, ' ', '(', ')', true);
+
+	FilterDeclarationList filters;
+	filters.value = filter_string_value;
+	filters.list.reserve(filter_string_list.size());
+
+	// Get or instance each filter in the comma-separated string list
+	for (const String& filter_string : filter_string_list)
+	{
+		const size_t shorthand_open = filter_string.find('(');
+		const size_t shorthand_close = filter_string.rfind(')');
+		const bool invalid_parenthesis = (shorthand_open == String::npos || shorthand_close == String::npos || shorthand_open >= shorthand_close);
+
+		if (invalid_parenthesis)
+		{
+			// We found no parenthesis, filters can only be declared anonymously for now.
+			Log::Message(Log::LT_WARNING, "Invalid syntax for font-effect '%s'.", filter_string.c_str());
+			return false;
+		}
+		else
+		{
+			const String type = StringUtilities::StripWhitespace(filter_string.substr(0, shorthand_open));
+
+			// Check for valid filter type
+			FilterInstancer* instancer = Factory::GetFilterInstancer(type);
+			if (!instancer)
+			{
+				Log::Message(Log::LT_WARNING, "Filter type '%s' not found.", type.c_str());
+				return false;
+			}
+
+			const String shorthand = filter_string.substr(shorthand_open + 1, shorthand_close - shorthand_open - 1);
+			const PropertySpecification& specification = instancer->GetPropertySpecification();
+
+			// Parse the shorthand properties given by the 'filter' shorthand property
+			PropertyDictionary properties;
+			if (!specification.ParsePropertyDeclaration(properties, "filter", shorthand))
+			{
+				// Empty values are allowed in filters, if the value is not empty we must have encountered a parser error.
+				if (!StringUtilities::StripWhitespace(shorthand).empty())
+					return false;
+			}
+
+			// Set unspecified values to their defaults
+			specification.SetPropertyDefaults(properties);
+
+			filters.list.emplace_back(FilterDeclaration{type, instancer, std::move(properties)});
+		}
+	}
+
+	if (filters.list.empty())
+		return false;
+
+	property.value = Variant(MakeShared<FilterDeclarationList>(std::move(filters)));
+	property.unit = Unit::FILTER;
+
+	return true;
+}
+
+} // namespace Rml

+ 50 - 0
Source/Core/PropertyParserFilter.h

@@ -0,0 +1,50 @@
+/*
+ * 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-2023 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.
+ *
+ */
+
+#ifndef RMLUI_CORE_PROPERTYPARSERFILTER_H
+#define RMLUI_CORE_PROPERTYPARSERFILTER_H
+
+#include "../../Include/RmlUi/Core/PropertyParser.h"
+
+namespace Rml {
+
+/**
+    A property parser for the filter property.
+ */
+
+class PropertyParserFilter : public PropertyParser {
+public:
+	PropertyParserFilter();
+	virtual ~PropertyParserFilter();
+
+	/// Called to parse a decorator declaration.
+	bool ParseValue(Property& property, const String& value, const ParameterMap& parameters) const override;
+};
+
+} // namespace Rml
+#endif

+ 7 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -33,6 +33,7 @@
 #include "PropertyParserAnimation.h"
 #include "PropertyParserColour.h"
 #include "PropertyParserDecorator.h"
+#include "PropertyParserFilter.h"
 #include "PropertyParserFontEffect.h"
 #include "PropertyParserKeyword.h"
 #include "PropertyParserNumber.h"
@@ -58,6 +59,7 @@ struct DefaultStyleSheetParsers : NonCopyMoveable {
 	PropertyParserAnimation transition = PropertyParserAnimation(PropertyParserAnimation::TRANSITION_PARSER);
 	PropertyParserColour color = PropertyParserColour();
 	PropertyParserDecorator decorator = PropertyParserDecorator();
+	PropertyParserFilter filter = PropertyParserFilter();
 	PropertyParserFontEffect font_effect = PropertyParserFontEffect();
 	PropertyParserTransform transform = PropertyParserTransform();
 	PropertyParserRatio ratio = PropertyParserRatio();
@@ -252,6 +254,7 @@ void StyleSheetSpecification::RegisterDefaultParsers()
 	RegisterParser("transition", &default_parsers->transition);
 	RegisterParser("color", &default_parsers->color);
 	RegisterParser("decorator", &default_parsers->decorator);
+	RegisterParser("filter", &default_parsers->filter);
 	RegisterParser("font_effect", &default_parsers->font_effect);
 	RegisterParser("transform", &default_parsers->transform);
 	RegisterParser("ratio", &default_parsers->ratio);
@@ -403,8 +406,12 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	RegisterProperty(PropertyId::Transition, "transition", "none", false, false).AddParser("transition");
 	RegisterProperty(PropertyId::Animation, "animation", "none", false, false).AddParser("animation");
 
+	// Decorators and effects
 	RegisterProperty(PropertyId::Decorator, "decorator", "", false, false).AddParser("decorator");
 	RegisterProperty(PropertyId::FontEffect, "font-effect", "", true, false).AddParser("font_effect");
+		
+	RegisterProperty(PropertyId::Filter, "filter", "", false, false).AddParser("filter", "filter");
+	RegisterProperty(PropertyId::BackdropFilter, "backdrop-filter", "", false, false).AddParser("filter");
 
 	// Rare properties (not added to computed values)
 	RegisterProperty(PropertyId::FillImage, "fill-image", "", false, false).AddParser("string");

+ 28 - 11
Source/Core/TypeConverter.cpp

@@ -29,6 +29,7 @@
 #include "../../Include/RmlUi/Core/TypeConverter.h"
 #include "../../Include/RmlUi/Core/Animation.h"
 #include "../../Include/RmlUi/Core/Decorator.h"
+#include "../../Include/RmlUi/Core/Filter.h"
 #include "../../Include/RmlUi/Core/PropertyDictionary.h"
 #include "../../Include/RmlUi/Core/PropertySpecification.h"
 #include "../../Include/RmlUi/Core/StyleSheetSpecification.h"
@@ -161,13 +162,8 @@ bool TypeConverter<AnimationList, String>::Convert(const AnimationList& src, Str
 	return true;
 }
 
-bool TypeConverter<DecoratorsPtr, DecoratorsPtr>::Convert(const DecoratorsPtr& src, DecoratorsPtr& dest)
-{
-	dest = src;
-	return true;
-}
-
-bool TypeConverter<DecoratorsPtr, String>::Convert(const DecoratorsPtr& src, String& dest)
+template <typename EffectsPtr>
+static bool ConvertEffectToString(const EffectsPtr& src, String& dest, const String& separator)
 {
 	if (!src || src->list.empty())
 		dest = "none";
@@ -176,21 +172,42 @@ bool TypeConverter<DecoratorsPtr, String>::Convert(const DecoratorsPtr& src, Str
 	else
 	{
 		dest.clear();
-		for (const DecoratorDeclaration& declaration : src->list)
+		for (const auto& declaration : src->list)
 		{
 			dest += declaration.type;
 			if (auto instancer = declaration.instancer)
 			{
 				dest += '(' + instancer->GetPropertySpecification().PropertiesToString(declaration.properties, false, ' ') + ')';
 			}
-			dest += ", ";
+			if (&declaration != &src->list.back())
+				dest += separator;
 		}
-		if (dest.size() > 2)
-			dest.resize(dest.size() - 2);
 	}
 	return true;
 }
 
+bool TypeConverter<DecoratorsPtr, DecoratorsPtr>::Convert(const DecoratorsPtr& src, DecoratorsPtr& dest)
+{
+	dest = src;
+	return true;
+}
+
+bool TypeConverter<DecoratorsPtr, String>::Convert(const DecoratorsPtr& src, String& dest)
+{
+	return ConvertEffectToString(src, dest, ", ");
+}
+
+bool TypeConverter<FiltersPtr, FiltersPtr>::Convert(const FiltersPtr& src, FiltersPtr& dest)
+{
+	dest = src;
+	return true;
+}
+
+bool TypeConverter<FiltersPtr, String>::Convert(const FiltersPtr& src, String& dest)
+{
+	return ConvertEffectToString(src, dest, " ");
+}
+
 bool TypeConverter<FontEffectsPtr, FontEffectsPtr>::Convert(const FontEffectsPtr& src, FontEffectsPtr& dest)
 {
 	dest = src;

+ 34 - 0
Source/Core/Variant.cpp

@@ -42,6 +42,7 @@ Variant::Variant()
 	static_assert(sizeof(TransitionList) <= LOCAL_DATA_SIZE, "Local data too small for TransitionList");
 	static_assert(sizeof(AnimationList) <= LOCAL_DATA_SIZE, "Local data too small for AnimationList");
 	static_assert(sizeof(DecoratorsPtr) <= LOCAL_DATA_SIZE, "Local data too small for DecoratorsPtr");
+	static_assert(sizeof(FiltersPtr) <= LOCAL_DATA_SIZE, "Local data too small for FiltersPtr");
 	static_assert(sizeof(FontEffectsPtr) <= LOCAL_DATA_SIZE, "Local data too small for FontEffectsPtr");
 }
 
@@ -99,6 +100,12 @@ void Variant::Clear()
 		decorators->~DecoratorsPtr();
 	}
 	break;
+	case FILTERSPTR:
+	{
+		FiltersPtr* decorators = (FiltersPtr*)data;
+		decorators->~FiltersPtr();
+	}
+	break;
 	case FONTEFFECTSPTR:
 	{
 		FontEffectsPtr* font_effects = (FontEffectsPtr*)data;
@@ -121,6 +128,7 @@ void Variant::Set(const Variant& copy)
 	case TRANSITIONLIST: Set(*reinterpret_cast<const TransitionList*>(copy.data)); break;
 	case ANIMATIONLIST: Set(*reinterpret_cast<const AnimationList*>(copy.data)); break;
 	case DECORATORSPTR: Set(*reinterpret_cast<const DecoratorsPtr*>(copy.data)); break;
+	case FILTERSPTR: Set(*reinterpret_cast<const FiltersPtr*>(copy.data)); break;
 	case FONTEFFECTSPTR: Set(*reinterpret_cast<const FontEffectsPtr*>(copy.data)); break;
 	default:
 		memcpy(data, copy.data, LOCAL_DATA_SIZE);
@@ -139,6 +147,7 @@ void Variant::Set(Variant&& other)
 	case TRANSITIONLIST: Set(std::move(*reinterpret_cast<TransitionList*>(other.data))); break;
 	case ANIMATIONLIST: Set(std::move(*reinterpret_cast<AnimationList*>(other.data))); break;
 	case DECORATORSPTR: Set(std::move(*reinterpret_cast<DecoratorsPtr*>(other.data))); break;
+	case FILTERSPTR: Set(std::move(*reinterpret_cast<FiltersPtr*>(other.data))); break;
 	case FONTEFFECTSPTR: Set(std::move(*reinterpret_cast<FontEffectsPtr*>(other.data))); break;
 	default:
 		memcpy(data, other.data, LOCAL_DATA_SIZE);
@@ -373,6 +382,30 @@ void Variant::Set(DecoratorsPtr&& value)
 		new (data) DecoratorsPtr(std::move(value));
 	}
 }
+void Variant::Set(const FiltersPtr& value)
+{
+	if (type == FILTERSPTR)
+	{
+		*(FiltersPtr*)data = value;
+	}
+	else
+	{
+		type = FILTERSPTR;
+		new (data) FiltersPtr(value);
+	}
+}
+void Variant::Set(FiltersPtr&& value)
+{
+	if (type == FILTERSPTR)
+	{
+		(*(FiltersPtr*)data) = std::move(value);
+	}
+	else
+	{
+		type = FILTERSPTR;
+		new (data) FiltersPtr(std::move(value));
+	}
+}
 void Variant::Set(const FontEffectsPtr& value)
 {
 	if (type == FONTEFFECTSPTR)
@@ -448,6 +481,7 @@ bool Variant::operator==(const Variant& other) const
 	case TRANSITIONLIST: return DEFAULT_VARIANT_COMPARE(TransitionList);
 	case ANIMATIONLIST: return DEFAULT_VARIANT_COMPARE(AnimationList);
 	case DECORATORSPTR: return DEFAULT_VARIANT_COMPARE(DecoratorsPtr);
+	case FILTERSPTR: return DEFAULT_VARIANT_COMPARE(FiltersPtr);
 	case FONTEFFECTSPTR: return DEFAULT_VARIANT_COMPARE(FontEffectsPtr);
 	case NONE: return true;
 	}

+ 177 - 0
Tests/Source/UnitTests/Filter.cpp

@@ -0,0 +1,177 @@
+/*
+ * 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-2023 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/TestsShell.h"
+#include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/Element.h>
+#include <RmlUi/Core/ElementDocument.h>
+#include <RmlUi/Core/Factory.h>
+#include <RmlUi/Core/Filter.h>
+#include <RmlUi/Core/PropertyDefinition.h>
+#include <RmlUi/Core/PropertyDictionary.h>
+#include <algorithm>
+#include <doctest.h>
+
+using namespace Rml;
+class FilterTest;
+
+struct CompiledTestFilter {
+	String element_id;
+	const FilterTest* filter;
+};
+static Vector<CompiledTestFilter> compiled_test_filters;
+
+class FilterTest : public Filter {
+public:
+	FilterTest(float value, Unit unit) : value(value), unit(unit) {}
+	~FilterTest() { CHECK(num_compile == num_release); }
+
+	CompiledFilterHandle CompileFilter(Element* element) const override
+	{
+		compiled_test_filters.push_back({element->GetId(), this});
+		num_compile += 1;
+		return CompiledFilterHandle(num_compile);
+	}
+
+	void ReleaseCompiledFilter(Element* /*element*/, CompiledFilterHandle /*filter_handle*/) const override { num_release += 1; }
+
+	float value = 0.f;
+	Unit unit = Unit::UNKNOWN;
+	mutable int num_compile = 0;
+	mutable int num_release = 0;
+};
+
+class FilterTestInstancer : public FilterInstancer {
+public:
+	enum class ValueType { NumberPercent, Angle };
+
+	FilterTestInstancer()
+	{
+		id = RegisterProperty("value", "10cm").AddParser("length").GetId();
+		RegisterShorthand("filter", "value", ShorthandType::FallThrough);
+	}
+
+	SharedPtr<Filter> InstanceFilter(const String& /*name*/, const PropertyDictionary& properties) override
+	{
+		const Property* p_value = properties.GetProperty(id);
+		if (!p_value)
+			return nullptr;
+
+		CHECK(Any(p_value->unit & Unit::LENGTH));
+		num_instances += 1;
+
+		return MakeShared<FilterTest>(p_value->Get<float>(), p_value->unit);
+	}
+
+	int num_instances = 0;
+
+private:
+	PropertyId id = {};
+};
+
+static const String document_filter_rml = R"(
+<rml>
+<head>
+	<style>
+		body {
+			top: 100px;
+			left: 200px;
+			width: 800px;
+			height: 600px;
+		}
+		div {
+			display: block;
+			width: 400px;
+			height: 300px;
+		}
+		#a    { filter: test(); }
+		#b    { filter: test(0); }
+		#c,#d { filter: test(1dp); }
+	</style>
+</head>
+
+<body>
+	<div id="a"/>
+	<div id="b"/>
+	<div id="c"/>
+	<div id="d"/>
+</body>
+</rml>
+)";
+
+TEST_CASE("filter")
+{
+	compiled_test_filters.clear();
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	FilterTestInstancer instancer;
+	Rml::Factory::RegisterFilterInstancer("test", &instancer);
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_filter_rml);
+	document->Show();
+
+	TestsShell::RenderLoop();
+
+	struct ExpectedCompiledFilter {
+		float value;
+		Unit unit;
+	};
+
+	// Map element ID to its expected filter value and unit.
+	UnorderedMap<String, ExpectedCompiledFilter> expected_compiled_filters = {
+		{"a", {10.f, Unit::CM}},
+		{"b", {0.f, Unit::PX}},
+		{"c", {1.f, Unit::DP}},
+		{"d", {1.f, Unit::DP}},
+	};
+	for (const auto& compiled_filter : compiled_test_filters)
+	{
+		INFO("ID #", compiled_filter.element_id);
+		auto it = expected_compiled_filters.find(compiled_filter.element_id);
+
+		const bool id_found = (it != expected_compiled_filters.end());
+		CHECK(id_found);
+		if (!id_found)
+			continue;
+
+		const ExpectedCompiledFilter& expected = it->second;
+		CHECK(compiled_filter.filter->value == expected.value);
+		CHECK(compiled_filter.filter->unit == expected.unit);
+	}
+
+	// Check that filters are not compiled more than once for each element.
+	CHECK(compiled_test_filters.size() == expected_compiled_filters.size());
+
+	// Filters aren't cached like decorators are, so each element will instance a new decorator even if they refer to
+	// the same style rule. Thus, here producing 4 instead of 3 unique instances.
+	CHECK(instancer.num_instances == 4);
+
+	document->Close();
+	TestsShell::ShutdownShell();
+}