Bläddra i källkod

Add support for the 'box-shadow' property

- Allow textures to be saved from the current layer through render interface.
- Implement GL3 support for saving textures.
- Extend bounding box to include ink overflow from box shadows.
Michael Ragazzon 2 år sedan
förälder
incheckning
655cd9a0fd

+ 41 - 0
Backends/RmlUi_Renderer_GL3.cpp

@@ -1666,6 +1666,47 @@ void RenderInterface_GL3::PopLayer(Rml::BlendMode blend_mode, const Rml::FilterH
 	Gfx::CheckGLError("PopLayer");
 }
 
+Rml::TextureHandle RenderInterface_GL3::SaveLayerAsTexture(Rml::Vector2i dimensions)
+{
+	Rml::TextureHandle render_texture = {};
+	if (!GenerateTexture(render_texture, nullptr, dimensions))
+		return {};
+
+	BlitTopLayerToPostprocessPrimary();
+
+	RMLUI_ASSERT(scissor_state.Valid() && render_texture);
+	const Rml::Rectanglei initial_scissor_state = scissor_state;
+	EnableScissorRegion(false);
+
+	const Gfx::FramebufferData& source = render_layers.GetPostprocessPrimary();
+	const Gfx::FramebufferData& destination = render_layers.GetPostprocessSecondary();
+	glBindFramebuffer(GL_READ_FRAMEBUFFER, source.framebuffer);
+	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destination.framebuffer);
+
+	Rml::Rectanglei bounds = initial_scissor_state;
+
+	// Flip the image vertically, as that convention is used for textures, and move to origin.
+	glBlitFramebuffer(                                  //
+		bounds.Left(), source.height - bounds.Bottom(), // src0
+		bounds.Right(), source.height - bounds.Top(),   // src1
+		0, bounds.Height(),                             // dst0
+		bounds.Width(), 0,                              // dst1
+		GL_COLOR_BUFFER_BIT, GL_NEAREST                 //
+	);
+
+	glBindTexture(GL_TEXTURE_2D, (GLuint)render_texture);
+
+	const Gfx::FramebufferData& texture_source = destination;
+	glBindFramebuffer(GL_READ_FRAMEBUFFER, texture_source.framebuffer);
+	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, bounds.Width(), bounds.Height());
+
+	SetScissor(initial_scissor_state);
+	glBindFramebuffer(GL_FRAMEBUFFER, render_layers.GetTopLayer().framebuffer);
+	Gfx::CheckGLError("SaveLayerAsTexture");
+
+	return render_texture;
+}
+
 void RenderInterface_GL3::UseProgram(ProgramId program_id)
 {
 	RMLUI_ASSERT(program_data);

+ 2 - 0
Backends/RmlUi_Renderer_GL3.h

@@ -84,6 +84,8 @@ public:
 	void PushLayer(Rml::LayerFill layer_fill) override;
 	void PopLayer(Rml::BlendMode blend_mode, const Rml::FilterHandleList& filters) override;
 
+	Rml::TextureHandle SaveLayerAsTexture(Rml::Vector2i dimensions) override;
+
 	Rml::CompiledFilterHandle CompileFilter(const Rml::String& name, const Rml::Dictionary& parameters) override;
 	void ReleaseCompiledFilter(Rml::CompiledFilterHandle filter) override;
 

+ 3 - 0
CMake/FileList.cmake

@@ -81,6 +81,7 @@ set(Core_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/precompiled.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertiesIterator.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserAnimation.h
+    ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserBoxShadow.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserColour.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserDecorator.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserFilter.h
@@ -140,6 +141,7 @@ set(Core_PUB_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataTypes.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataVariable.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Debug.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DecorationTypes.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
@@ -350,6 +352,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyDefinition.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyDictionary.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserAnimation.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserBoxShadow.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserColour.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserDecorator.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserFilter.cpp

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

@@ -159,7 +159,7 @@ namespace Style {
 
 			vertical_align_type(VerticalAlign::Baseline), drag(Drag::None), tab_index(TabIndex::None), overscroll_behavior(OverscrollBehavior::Auto),
 
-			has_filter(false), has_backdrop_filter(false)
+			has_filter(false), has_backdrop_filter(false), has_box_shadow(false)
 		{}
 
 		LengthPercentage::Type min_width_type : 1, max_width_type : 1;
@@ -179,6 +179,7 @@ namespace Style {
 
 		bool has_filter : 1;
 		bool has_backdrop_filter : 1;
+		bool has_box_shadow : 1;
 
 		Clip clip;
 
@@ -305,6 +306,7 @@ namespace Style {
 		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; }
+		bool              has_box_shadow()             const { return rare.has_box_shadow; }
 		
 		// -- Assignment --
 		// Common
@@ -387,6 +389,7 @@ namespace Style {
 		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; }
+		void has_box_shadow            (bool value)              { rare.has_box_shadow             = value; }
 
 		// clang-format on
 

+ 55 - 0
Include/RmlUi/Core/DecorationTypes.h

@@ -0,0 +1,55 @@
+/*
+ * 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_DECORATIONTYPES_H
+#define RMLUI_CORE_DECORATIONTYPES_H
+
+#include "NumericValue.h"
+#include "Types.h"
+
+namespace Rml {
+
+struct BoxShadow {
+	Colourb color;
+	NumericValue offset_x, offset_y;
+	NumericValue blur_radius;
+	NumericValue spread_distance;
+	bool inset = false;
+};
+inline bool operator==(const BoxShadow& a, const BoxShadow& b)
+{
+	return a.color == b.color && a.offset_x == b.offset_x && a.offset_y == b.offset_y && a.blur_radius == b.blur_radius &&
+		a.spread_distance == b.spread_distance && a.inset == b.inset;
+}
+inline bool operator!=(const BoxShadow& a, const BoxShadow& b)
+{
+	return !(a == b);
+}
+
+} // namespace Rml
+#endif

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

@@ -104,7 +104,7 @@ public:
 	/// Returns a rectangle covering the element's area in window coordinate space.
 	/// @param[in] out_rectangle The resulting rectangle covering the projected element's box.
 	/// @param[in] element The element to find the bounding box of.
-	/// @param[in] area The box area to consider.
+	/// @param[in] area The box area to consider, 'Auto' means the border box in addition to any ink overflow.
 	/// @return True on success, otherwise false.
 	static bool GetBoundingBox(Rectanglef& out_rectangle, Element* element, BoxArea area);
 

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

@@ -158,6 +158,7 @@ enum class PropertyId : uint8_t {
 	FontEffect,
 	Filter,
 	BackdropFilter,
+	BoxShadow,
 
 	FillImage,
 

+ 5 - 0
Include/RmlUi/Core/RenderInterface.h

@@ -147,6 +147,11 @@ public:
 	/// @param[in] filters A list of compiled filters which should be applied to the top layer before blending.
 	virtual void PopLayer(BlendMode blend_mode, const FilterHandleList& filters);
 
+	/// Called by RmlUi when it wants to store the current layer as a new texture to be rendered later with geometry.
+	/// @param[in] dimensions The dimensions of the texture, to be copied from the top-left part of the viewport.
+	/// @return The handle to the new texture.
+	virtual TextureHandle SaveLayerAsTexture(Vector2i dimensions);
+
 	/// Called by RmlUi when it wants to compile a new filter.
 	/// @param[in] name The name of the filter.
 	/// @param[in] parameters The list of name-value parameters specified for the filter.

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

@@ -145,6 +145,17 @@ public:
 	RMLUICORE_API static bool Convert(const FontEffectsPtr& src, String& dest);
 };
 
+template <>
+class TypeConverter<BoxShadowList, BoxShadowList> {
+public:
+	RMLUICORE_API static bool Convert(const BoxShadowList& src, BoxShadowList& dest);
+};
+template <>
+class TypeConverter<BoxShadowList, String> {
+public:
+	RMLUICORE_API static bool Convert(const BoxShadowList& src, String& dest);
+};
+
 } // namespace Rml
 
 #include "TypeConverter.inl"

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

@@ -90,6 +90,7 @@ struct Transition;
 struct TransitionList;
 struct DecoratorDeclarationList;
 struct FilterDeclarationList;
+struct BoxShadow;
 enum class EventId : uint16_t;
 enum class PropertyId : uint8_t;
 enum class MediaQueryId : uint8_t;
@@ -127,6 +128,7 @@ struct FontEffects {
 	FontEffectList list;
 	String value;
 };
+using BoxShadowList = Vector<BoxShadow>;
 
 // Additional smart pointers
 using TransformPtr = SharedPtr<Transform>;

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

@@ -78,7 +78,7 @@ enum class Unit {
 	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>
+	BOXSHADOWLIST = 1 << 27, // shadow list; fetch as <BoxShadowList>
 
 	LENGTH = PX | DP | VW | VH | EM | REM | PPI_UNIT,
 	LENGTH_PERCENT = LENGTH | PERCENT,

+ 3 - 0
Include/RmlUi/Core/Variant.h

@@ -72,6 +72,7 @@ public:
 		DECORATORSPTR = 'D',
 		FILTERSPTR = 'F',
 		FONTEFFECTSPTR = 'E',
+		BOXSHADOWLIST = 'S',
 		VOIDPTR = '*',
 	};
 
@@ -158,6 +159,8 @@ private:
 	void Set(FiltersPtr&& value);
 	void Set(const FontEffectsPtr& value);
 	void Set(FontEffectsPtr&& value);
+	void Set(const BoxShadowList& value);
+	void Set(BoxShadowList&& value);
 
 	template <typename T, typename = std::enable_if_t<std::is_enum<T>::value>>
 	void Set(const T value);

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

@@ -75,6 +75,7 @@ bool Variant::GetInto(T& value) const
 	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 BOXSHADOWLIST: return TypeConverter<BoxShadowList, T>::Convert(*reinterpret_cast<const BoxShadowList*>(data), value);
 	case NONE: break;
 	}
 

+ 26 - 0
Samples/basic/effect/data/effect.rml

@@ -78,6 +78,28 @@
 
 .dropshadow { filter: drop-shadow(#f33f 30px 20px 5px); }
 
+.boxshadow_blur {
+	box-shadow:
+		#f00f  40px  30px 25px 0px,
+		#00ff -40px -30px 45px 0px,
+		#0f08 -60px  70px 60px 0px,
+		#333a  0px  0px 30px 15px inset
+		;
+	margin-top: 100px;
+	margin-bottom: 100px;
+}
+.boxshadow_trail {
+	box-shadow:
+		#f66 30dp 30dp 0 0,
+		#c88 60dp 60dp 0 0,
+		#baa 90dp 90dp 0 0,
+		#ffac 0 0 .8em 8dp inset
+		;
+	margin-bottom: 100px;
+	filter: opacity(1); /* Tests filter clipping behavior when element has ink overflow due to box-shadow. */
+}
+.boxshadow_inset { box-shadow: #f4fd 10px 5px 5px 3px inset; }
+
 </style>
 </head>
 <body
@@ -127,6 +149,10 @@
 	data-class-transform_all="transform_all"
 >
 
+<div class="box boxshadow_blur transform"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
+<div class="box boxshadow_trail"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
+<div class="box boxshadow_inset"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
+
 <div class="box"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 <div class="box big"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 

+ 1 - 0
Samples/basic/effect/data/effect_style.rcss

@@ -83,6 +83,7 @@ handle.size:hover, handle.size:active {
 	width: 400dp;
 	height: 480dp;
 	overflow: auto;
+	overscroll-behavior: contain;
 
 	background: #fffc;
 	border: 2dp #555a;

+ 2 - 1
Source/Core/Element.cpp

@@ -1766,7 +1766,8 @@ void Element::OnPropertyChange(const PropertyIdSet& changed_properties)
 	if (border_radius_changed ||                                    //
 		changed_properties.Contains(PropertyId::BackgroundColor) || //
 		changed_properties.Contains(PropertyId::Opacity) ||         //
-		changed_properties.Contains(PropertyId::ImageColor))        //
+		changed_properties.Contains(PropertyId::ImageColor) ||      //
+		changed_properties.Contains(PropertyId::BoxShadow))         //
 	{
 		meta->background_border.DirtyBackground();
 	}

+ 241 - 20
Source/Core/ElementBackgroundBorder.cpp

@@ -29,6 +29,8 @@
 #include "ElementBackgroundBorder.h"
 #include "../../Include/RmlUi/Core/Box.h"
 #include "../../Include/RmlUi/Core/ComputedValues.h"
+#include "../../Include/RmlUi/Core/Context.h"
+#include "../../Include/RmlUi/Core/DecorationTypes.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/GeometryUtilities.h"
 
@@ -49,7 +51,10 @@ void ElementBackgroundBorder::Render(Element* element)
 		border_dirty = false;
 	}
 
-	if (Geometry* geometry = GetGeometry(BackgroundType::BackgroundBorder))
+	Geometry* shadow_geometry = GetGeometry(BackgroundType::BoxShadow);
+	if (shadow_geometry && *shadow_geometry)
+		shadow_geometry->Render(element->GetAbsoluteOffset(BoxArea::Border));
+	else if (Geometry* geometry = GetGeometry(BackgroundType::BackgroundBorder))
 		geometry->Render(element->GetAbsoluteOffset(BoxArea::Border));
 }
 
@@ -85,6 +90,23 @@ Geometry* ElementBackgroundBorder::GetClipGeometry(Element* element, BoxArea cli
 	return &geometry;
 }
 
+Geometry* ElementBackgroundBorder::GetGeometry(BackgroundType type)
+{
+	auto it = backgrounds.find(type);
+	if (it != backgrounds.end())
+		return &it->second.geometry;
+	return nullptr;
+}
+
+ElementBackgroundBorder::Background& ElementBackgroundBorder::GetOrCreateBackground(BackgroundType type)
+{
+	auto it = backgrounds.find(type);
+	if (it != backgrounds.end())
+		return it->second;
+
+	return backgrounds.emplace(type, Background{}).first->second;
+}
+
 void ElementBackgroundBorder::GenerateGeometry(Element* element)
 {
 	const ComputedValues& computed = element->GetComputedValues();
@@ -97,15 +119,20 @@ void ElementBackgroundBorder::GenerateGeometry(Element* element)
 		computed.border_left_color(),
 	};
 	const Vector4f border_radius = computed.border_radius();
+	const bool has_box_shadow = computed.has_box_shadow();
 
-	// Apply opacity
-	const float opacity = computed.opacity();
-	background_color.alpha = (byte)(opacity * (float)background_color.alpha);
-
-	if (opacity < 1)
+	if (!has_box_shadow)
 	{
-		for (int i = 0; i < 4; ++i)
-			border_colors[i].alpha = (byte)(opacity * (float)border_colors[i].alpha);
+		// Apply opacity except if we have a box shadow. In the latter case the background is rendered opaquely into the box-shadow texture, while
+		// opacity is applied to the entire box-shadow texture when that is rendered.
+		const float opacity = computed.opacity();
+		if (opacity < 1.f)
+		{
+			background_color.alpha = (byte)(opacity * (float)background_color.alpha);
+
+			for (int i = 0; i < 4; ++i)
+				border_colors[i].alpha = (byte)(opacity * (float)border_colors[i].alpha);
+		}
 	}
 
 	Geometry& geometry = GetOrCreateBackground(BackgroundType::BackgroundBorder).geometry;
@@ -117,23 +144,217 @@ void ElementBackgroundBorder::GenerateGeometry(Element* element)
 		const Box& box = element->GetBox(i, offset);
 		GeometryUtilities::GenerateBackgroundBorder(&geometry, box, offset, border_radius, background_color, border_colors);
 	}
-}
 
-Geometry* ElementBackgroundBorder::GetGeometry(BackgroundType type)
-{
-	auto it = backgrounds.find(type);
-	if (it != backgrounds.end())
-		return &it->second.geometry;
-	return nullptr;
+	if (has_box_shadow)
+	{
+		const Property* p_box_shadow = element->GetLocalProperty(PropertyId::BoxShadow);
+		RMLUI_ASSERT(p_box_shadow->value.GetType() == Variant::BOXSHADOWLIST);
+		BoxShadowList shadow_list = p_box_shadow->value.Get<BoxShadowList>();
+
+		GenerateBoxShadow(element, std::move(shadow_list), border_radius, computed.opacity());
+	}
 }
 
-ElementBackgroundBorder::Background& ElementBackgroundBorder::GetOrCreateBackground(BackgroundType type)
+void ElementBackgroundBorder::GenerateBoxShadow(Element* element, BoxShadowList shadow_list, const Vector4f border_radius, const float opacity)
 {
-	auto it = backgrounds.find(type);
-	if (it != backgrounds.end())
-		return it->second;
+	// Find the box-shadow texture dimension and offset required to cover all box-shadows and element boxes combined.
+	Vector2f element_offset_in_texture;
+	Vector2i texture_dimensions;
 
-	return backgrounds.emplace(type, Background{}).first->second;
+	// Resolve all lengths to px units.
+	for (BoxShadow& shadow : shadow_list)
+	{
+		shadow.blur_radius = NumericValue(element->ResolveLength(shadow.blur_radius), Unit::PX);
+		shadow.spread_distance = NumericValue(element->ResolveLength(shadow.spread_distance), Unit::PX);
+		shadow.offset_x = NumericValue(element->ResolveLength(shadow.offset_x), Unit::PX);
+		shadow.offset_y = NumericValue(element->ResolveLength(shadow.offset_y), Unit::PX);
+	}
+
+	{
+		Vector2f extend_min;
+		Vector2f extend_max;
+
+		// Extend the render-texture to encompass box-shadow blur and spread.
+		for (const BoxShadow& shadow : shadow_list)
+		{
+			if (!shadow.inset)
+			{
+				const float extend = 1.5f * shadow.blur_radius.number + shadow.spread_distance.number;
+				const Vector2f offset = {shadow.offset_x.number, shadow.offset_y.number};
+				extend_min = Math::Min(extend_min, offset - Vector2f(extend));
+				extend_max = Math::Max(extend_max, offset + Vector2f(extend));
+			}
+		}
+
+		Rectanglef texture_region;
+
+		// Extend the render-texture further to cover all the element's boxes.
+		for (int i = 0; i < element->GetNumBoxes(); i++)
+		{
+			Vector2f offset;
+			const Box& box = element->GetBox(i, offset);
+			texture_region.Join(Rectanglef::FromPositionSize(offset, box.GetSize(BoxArea::Border)));
+		}
+
+		texture_region.ExtendTopLeft(-extend_min);
+		texture_region.ExtendBottomRight(extend_max);
+		Math::ExpandToPixelGrid(texture_region);
+
+		element_offset_in_texture = -texture_region.TopLeft();
+		texture_dimensions = Vector2i(texture_region.Size());
+	}
+
+	Geometry& background_border_geometry = *GetGeometry(BackgroundType::BackgroundBorder);
+
+	// Callback for generating the box-shadow texture. Using a callback ensures that the texture can be regenerated at any time, for example if the
+	// device loses its GPU context and the client calls Rml::ReleaseTextures().
+	auto p_callback = [&background_border_geometry, element, border_radius, texture_dimensions, element_offset_in_texture, shadow_list](
+						  RenderInterface* render_interface, const String& /*name*/, TextureHandle& out_handle, Vector2i& out_dimensions) -> bool {
+		Context* context = element->GetContext();
+		if (!context)
+		{
+			RMLUI_ERROR;
+			return false;
+		}
+
+		Geometry geometry_padding;        // Render geometry for inner box-shadow.
+		Geometry geometry_padding_border; // Clipping mask for outer box-shadow.
+
+		bool has_inner_shadow = false;
+		bool has_outer_shadow = false;
+		for (const BoxShadow& shadow : shadow_list)
+		{
+			if (shadow.inset)
+				has_inner_shadow = true;
+			else
+				has_outer_shadow = true;
+		}
+
+		// Generate the geometry for all the element's boxes and extend the render-texture further to cover all of them.
+		for (int i = 0; i < element->GetNumBoxes(); i++)
+		{
+			Vector2f offset;
+			const Box& box = element->GetBox(i, offset);
+
+			if (has_inner_shadow)
+				GeometryUtilities::GenerateBackground(&geometry_padding, box, offset, border_radius, Colourb(255), BoxArea::Padding);
+			if (has_outer_shadow)
+				GeometryUtilities::GenerateBackground(&geometry_padding_border, box, offset, border_radius, Colourb(255), BoxArea::Border);
+		}
+
+		RenderManager& render_manager = context->GetRenderManager();
+		const RenderState initial_render_state = render_manager.GetState();
+		render_manager.ResetState();
+		render_manager.SetScissorRegion(Rectanglei::FromSize(texture_dimensions));
+
+		render_interface->PushLayer(LayerFill::Clear);
+
+		background_border_geometry.Render(element_offset_in_texture);
+
+		for (int shadow_index = (int)shadow_list.size() - 1; shadow_index >= 0; shadow_index--)
+		{
+			const BoxShadow& shadow = shadow_list[shadow_index];
+			const Vector2f shadow_offset = {shadow.offset_x.number, shadow.offset_y.number};
+			const bool inset = shadow.inset;
+			const float spread_distance = shadow.spread_distance.number;
+			const float blur_radius = shadow.blur_radius.number;
+
+			Vector4f spread_radii = border_radius;
+			for (int i = 0; i < 4; i++)
+			{
+				float& radius = spread_radii[i];
+				float spread_factor = (inset ? -1.f : 1.f);
+				if (radius < spread_distance)
+				{
+					const float ratio_minus_one = (radius / spread_distance) - 1.f;
+					spread_factor *= 1.f + ratio_minus_one * ratio_minus_one * ratio_minus_one;
+				}
+				radius = Math::Max(radius + spread_factor * spread_distance, 0.f);
+			}
+
+			Geometry shadow_geometry;
+
+			// Generate the shadow geometry. For outer box-shadows it is rendered normally, while for inner box-shadows it is used as a clipping mask.
+			for (int i = 0; i < element->GetNumBoxes(); i++)
+			{
+				Vector2f offset;
+				Box box = element->GetBox(i, offset);
+				const float signed_spread_distance = (inset ? -spread_distance : spread_distance);
+				offset -= Vector2f(signed_spread_distance);
+
+				for (int j = 0; j < Box::num_edges; j++)
+				{
+					BoxEdge edge = (BoxEdge)j;
+					const float new_size = box.GetEdge(BoxArea::Padding, edge) + signed_spread_distance;
+					box.SetEdge(BoxArea::Padding, edge, new_size);
+				}
+
+				GeometryUtilities::GenerateBackground(&shadow_geometry, box, offset, spread_radii, shadow.color,
+					inset ? BoxArea::Padding : BoxArea::Border);
+			}
+
+			CompiledFilterHandle blur = {};
+			if (blur_radius > 0.5f)
+			{
+				blur = render_interface->CompileFilter("blur", Dictionary{{"radius", Variant(blur_radius)}});
+				if (blur)
+					render_interface->PushLayer(LayerFill::Clear);
+			}
+
+			if (inset)
+			{
+				render_manager.SetClipMask(ClipMaskOperation::SetInverse, &shadow_geometry, shadow_offset + element_offset_in_texture);
+
+				for (Rml::Vertex& vertex : geometry_padding.GetVertices())
+					vertex.colour = shadow.color;
+
+				geometry_padding.Release();
+				geometry_padding.Render(element_offset_in_texture);
+
+				render_manager.SetClipMask(ClipMaskOperation::Set, &geometry_padding, element_offset_in_texture);
+			}
+			else
+			{
+				render_manager.SetClipMask(ClipMaskOperation::SetInverse, &geometry_padding_border, element_offset_in_texture);
+				shadow_geometry.Render(shadow_offset + element_offset_in_texture);
+			}
+
+			if (blur)
+			{
+				render_interface->PopLayer(BlendMode::Blend, {blur});
+				render_interface->ReleaseCompiledFilter(blur);
+			}
+		}
+
+		TextureHandle shadow_texture = render_interface->SaveLayerAsTexture(texture_dimensions);
+		if (!shadow_texture)
+			return false;
+
+		render_interface->PopLayer(BlendMode::Discard, {});
+
+		render_manager.SetState(initial_render_state);
+
+		out_dimensions = texture_dimensions;
+		out_handle = shadow_texture;
+
+		return true;
+	};
+
+	// Generate the geometry for the box-shadow texture.
+	Background& shadow_background = GetOrCreateBackground(BackgroundType::BoxShadow);
+	Geometry& shadow_geometry = shadow_background.geometry;
+	Texture& shadow_texture = shadow_background.texture;
+	RMLUI_ASSERT(!shadow_geometry);
+
+	Vector<Vertex>& vertices = shadow_geometry.GetVertices();
+	Vector<int>& indices = shadow_geometry.GetIndices();
+	vertices.resize(4);
+	indices.resize(6);
+	const byte alpha = byte(opacity * 255.f);
+	GeometryUtilities::GenerateQuad(vertices.data(), indices.data(), -element_offset_in_texture, Vector2f(texture_dimensions), Colourb(255, alpha));
+
+	shadow_texture.Set("box-shadow", p_callback);
+	shadow_geometry.SetTexture(&shadow_texture);
 }
 
 } // namespace Rml

+ 4 - 3
Source/Core/ElementBackgroundBorder.h

@@ -47,17 +47,18 @@ public:
 	Geometry* GetClipGeometry(Element* element, BoxArea clip_area);
 
 private:
-	enum class BackgroundType { BackgroundBorder, ClipBorder, ClipPadding, ClipContent, Count };
+	enum class BackgroundType { BackgroundBorder, BoxShadow, ClipBorder, ClipPadding, ClipContent, Count };
 	struct Background {
 		Geometry geometry;
 		Texture texture;
 	};
 
-	void GenerateGeometry(Element* element);
-
 	Geometry* GetGeometry(BackgroundType type);
 	Background& GetOrCreateBackground(BackgroundType type);
 
+	void GenerateGeometry(Element* element);
+	void GenerateBoxShadow(Element* element, BoxShadowList shadow_list, const Vector4f border_radius, const float opacity);
+
 	bool background_dirty = false;
 	bool border_dirty = false;
 

+ 3 - 0
Source/Core/ElementStyle.cpp

@@ -858,6 +858,9 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 		case PropertyId::BackdropFilter:
 			values.has_backdrop_filter(p->unit == Unit::FILTER);
 			break;
+		case PropertyId::BoxShadow:
+			values.has_box_shadow(p->unit == Unit::BOXSHADOWLIST);
+			break;
 
 		case PropertyId::FlexBasis:
 			values.flex_basis(ComputeLengthPercentageAuto(p, font_size, document_font_size, dp_ratio, vp_dimensions));

+ 27 - 1
Source/Core/ElementUtilities.cpp

@@ -29,6 +29,7 @@
 #include "../../Include/RmlUi/Core/ElementUtilities.h"
 #include "../../Include/RmlUi/Core/Context.h"
 #include "../../Include/RmlUi/Core/Core.h"
+#include "../../Include/RmlUi/Core/DecorationTypes.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/ElementScroll.h"
 #include "../../Include/RmlUi/Core/Factory.h"
@@ -286,11 +287,36 @@ bool ElementUtilities::GetBoundingBox(Rectanglef& out_rectangle, Element* elemen
 {
 	RMLUI_ASSERT(element);
 
+	Vector2f shadow_extent_top_left, shadow_extent_bottom_right;
 	if (box_area == BoxArea::Auto)
+	{
+		// 'Auto' acts like border box extended to encompass any ink overflow, which includes the element's box-shadow.
+		// TODO: Include ink overflow due to filters (e.g. blur or drop-shadow).
 		box_area = BoxArea::Border;
 
+		if (const Property* p_box_shadow = element->GetLocalProperty(PropertyId::BoxShadow))
+		{
+			RMLUI_ASSERT(p_box_shadow->value.GetType() == Variant::BOXSHADOWLIST);
+			const BoxShadowList& shadow_list = p_box_shadow->value.GetReference<BoxShadowList>();
+
+			for (const BoxShadow& shadow : shadow_list)
+			{
+				if (!shadow.inset)
+				{
+					const float extent = 1.5f * element->ResolveLength(shadow.blur_radius) + element->ResolveLength(shadow.spread_distance);
+					const Vector2f offset = {element->ResolveLength(shadow.offset_x), element->ResolveLength(shadow.offset_y)};
+
+					shadow_extent_top_left = Math::Max(shadow_extent_top_left, -offset + Vector2f(extent));
+					shadow_extent_bottom_right = Math::Max(shadow_extent_bottom_right, offset + Vector2f(extent));
+				}
+			}
+		}
+	}
+
 	// Element bounds in non-transformed space.
-	const Rectanglef bounds = Rectanglef::FromPositionSize(element->GetAbsoluteOffset(box_area), element->GetBox().GetSize(box_area));
+	Rectanglef bounds = Rectanglef::FromPositionSize(element->GetAbsoluteOffset(box_area), element->GetBox().GetSize(box_area));
+	bounds.ExtendTopLeft(shadow_extent_top_left);
+	bounds.ExtendBottomRight(shadow_extent_bottom_right);
 
 	const TransformState* transform_state = element->GetTransformState();
 	const Matrix4f* transform = (transform_state ? transform_state->GetTransform() : nullptr);

+ 118 - 0
Source/Core/PropertyParserBoxShadow.cpp

@@ -0,0 +1,118 @@
+/*
+ * 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 "PropertyParserBoxShadow.h"
+#include "../../Include/RmlUi/Core/DecorationTypes.h"
+#include "../../Include/RmlUi/Core/StringUtilities.h"
+
+namespace Rml {
+
+PropertyParserBoxShadow::PropertyParserBoxShadow(PropertyParser* parser_color, PropertyParser* parser_length) :
+	parser_color(parser_color), parser_length(parser_length)
+{
+	RMLUI_ASSERT(parser_color && parser_length);
+}
+
+bool PropertyParserBoxShadow::ParseValue(Property& property, const String& value, const ParameterMap& /*parameters*/) const
+{
+	if (value.empty() || value == "none")
+	{
+		property.value = Variant();
+		property.unit = Unit::UNKNOWN;
+		return true;
+	}
+
+	StringList shadows_string_list;
+	{
+		auto lowercase_value = StringUtilities::ToLower(value);
+		StringUtilities::ExpandString(shadows_string_list, lowercase_value, ',');
+	}
+
+	if (shadows_string_list.empty())
+		return false;
+
+	const ParameterMap empty_parameter_map;
+
+	BoxShadowList shadow_list;
+	shadow_list.reserve(shadows_string_list.size());
+
+	for (const String& shadow_str : shadows_string_list)
+	{
+		StringList arguments;
+		StringUtilities::ExpandString(arguments, shadow_str, ' ');
+		if (arguments.empty())
+			return false;
+
+		shadow_list.push_back({});
+		BoxShadow& shadow = shadow_list.back();
+
+		int length_argument_index = 0;
+
+		for (const String& argument : arguments)
+		{
+			if (argument.empty())
+				continue;
+
+			Property prop;
+			if (parser_length->ParseValue(prop, argument, empty_parameter_map))
+			{
+				switch (length_argument_index)
+				{
+				case 0: shadow.offset_x = prop.GetNumericValue(); break;
+				case 1: shadow.offset_y = prop.GetNumericValue(); break;
+				case 2: shadow.blur_radius = prop.GetNumericValue(); break;
+				case 3: shadow.spread_distance = prop.GetNumericValue(); break;
+				default: return false;
+				}
+				length_argument_index += 1;
+			}
+			else if (argument == "inset")
+			{
+				shadow.inset = true;
+			}
+			else if (parser_color->ParseValue(prop, argument, empty_parameter_map))
+			{
+				shadow.color = prop.Get<Colourb>();
+			}
+			else
+			{
+				return false;
+			}
+		}
+
+		if (length_argument_index < 2)
+			return false;
+	}
+
+	property.unit = Unit::BOXSHADOWLIST;
+	property.value = Variant(std::move(shadow_list));
+
+	return true;
+}
+
+} // namespace Rml

+ 57 - 0
Source/Core/PropertyParserBoxShadow.h

@@ -0,0 +1,57 @@
+/*
+ * 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_PROPERTYPARSERBOXSHADOW_H
+#define RMLUI_CORE_PROPERTYPARSERBOXSHADOW_H
+
+#include "../../Include/RmlUi/Core/PropertyParser.h"
+
+namespace Rml {
+
+/**
+    Parses the RCSS 'box-shadow' property.
+*/
+
+class PropertyParserBoxShadow : public PropertyParser {
+public:
+	PropertyParserBoxShadow(PropertyParser* parser_color, PropertyParser* parser_length);
+
+	/// Called to parse a RCSS declaration.
+	/// @param[out] property The property to set the parsed value on.
+	/// @param[in] value The raw value defined for this property.
+	/// @param[in] parameters The parameters defined for this property.
+	/// @return True if the value was validated successfully, false otherwise.
+	bool ParseValue(Property& property, const String& value, const ParameterMap& parameters) const override;
+
+private:
+	PropertyParser* parser_color;
+	PropertyParser* parser_length;
+};
+
+} // namespace Rml
+#endif

+ 5 - 0
Source/Core/RenderInterface.cpp

@@ -73,6 +73,11 @@ void RenderInterface::PushLayer(LayerFill /*layer_fill*/) {}
 
 void RenderInterface::PopLayer(BlendMode /*blend_mode*/, const FilterHandleList& /*filters*/) {}
 
+TextureHandle RenderInterface::SaveLayerAsTexture(Vector2i /*dimensions*/)
+{
+	return TextureHandle{};
+}
+
 CompiledFilterHandle RenderInterface::CompileFilter(const String& /*name*/, const Dictionary& /*parameters*/)
 {
 	return CompiledFilterHandle{};

+ 5 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -31,6 +31,7 @@
 #include "../../Include/RmlUi/Core/PropertyIdSet.h"
 #include "IdNameMap.h"
 #include "PropertyParserAnimation.h"
+#include "PropertyParserBoxShadow.h"
 #include "PropertyParserColour.h"
 #include "PropertyParserDecorator.h"
 #include "PropertyParserFilter.h"
@@ -64,6 +65,7 @@ struct DefaultStyleSheetParsers : NonCopyMoveable {
 	PropertyParserTransform transform = PropertyParserTransform();
 	PropertyParserRatio ratio = PropertyParserRatio();
 	PropertyParserNumber resolution = PropertyParserNumber(Unit::X);
+	PropertyParserBoxShadow box_shadow = PropertyParserBoxShadow(&color, &length);
 };
 
 StyleSheetSpecification::StyleSheetSpecification() :
@@ -259,6 +261,7 @@ void StyleSheetSpecification::RegisterDefaultParsers()
 	RegisterParser("transform", &default_parsers->transform);
 	RegisterParser("ratio", &default_parsers->ratio);
 	RegisterParser("resolution", &default_parsers->resolution);
+	RegisterParser("box_shadow", &default_parsers->box_shadow);
 }
 
 void StyleSheetSpecification::RegisterDefaultProperties()
@@ -412,6 +415,8 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 		
 	RegisterProperty(PropertyId::Filter, "filter", "", false, false).AddParser("filter", "filter");
 	RegisterProperty(PropertyId::BackdropFilter, "backdrop-filter", "", false, false).AddParser("filter");
+	
+	RegisterProperty(PropertyId::BoxShadow, "box-shadow", "none", false, false).AddParser("box_shadow");
 
 	// Rare properties (not added to computed values)
 	RegisterProperty(PropertyId::FillImage, "fill-image", "", false, false).AddParser("string");

+ 34 - 0
Source/Core/TypeConverter.cpp

@@ -28,6 +28,7 @@
 
 #include "../../Include/RmlUi/Core/TypeConverter.h"
 #include "../../Include/RmlUi/Core/Animation.h"
+#include "../../Include/RmlUi/Core/DecorationTypes.h"
 #include "../../Include/RmlUi/Core/Decorator.h"
 #include "../../Include/RmlUi/Core/Filter.h"
 #include "../../Include/RmlUi/Core/PropertyDictionary.h"
@@ -223,4 +224,37 @@ bool TypeConverter<FontEffectsPtr, String>::Convert(const FontEffectsPtr& src, S
 	return true;
 }
 
+bool TypeConverter<BoxShadowList, BoxShadowList>::Convert(const BoxShadowList& src, BoxShadowList& dest)
+{
+	dest = src;
+	return true;
+}
+
+bool TypeConverter<BoxShadowList, String>::Convert(const BoxShadowList& src, String& dest)
+{
+	dest.clear();
+	String temp, str_unit;
+	for (size_t i = 0; i < src.size(); i++)
+	{
+		const BoxShadow& shadow = src[i];
+		for (const NumericValue* value : {&shadow.offset_x, &shadow.offset_y, &shadow.blur_radius, &shadow.spread_distance})
+		{
+			if (TypeConverter<Unit, String>::Convert(value->unit, str_unit))
+				temp += " " + ToString(value->number) + str_unit;
+		}
+
+		if (shadow.inset)
+			temp += " inset";
+
+		dest += "rgba(" + ToString(shadow.color) + ')' + temp;
+
+		if (i < src.size() - 1)
+		{
+			dest += ", ";
+			temp.clear();
+		}
+	}
+	return true;
+}
+
 } // namespace Rml

+ 34 - 0
Source/Core/Variant.cpp

@@ -27,6 +27,7 @@
  */
 
 #include "../../Include/RmlUi/Core/Variant.h"
+#include "../../Include/RmlUi/Core/DecorationTypes.h"
 #include <string.h>
 
 namespace Rml {
@@ -112,6 +113,12 @@ void Variant::Clear()
 		font_effects->~shared_ptr();
 	}
 	break;
+	case BOXSHADOWLIST:
+	{
+		BoxShadowList* value = (BoxShadowList*)data;
+		value->~BoxShadowList();
+	}
+	break;
 	default: break;
 	}
 	type = NONE;
@@ -130,6 +137,7 @@ void Variant::Set(const Variant& copy)
 	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;
+	case BOXSHADOWLIST: Set(*reinterpret_cast<const BoxShadowList*>(copy.data)); break;
 	default:
 		memcpy(data, copy.data, LOCAL_DATA_SIZE);
 		type = copy.type;
@@ -149,6 +157,7 @@ void Variant::Set(Variant&& other)
 	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;
+	case BOXSHADOWLIST: Set(std::move(*reinterpret_cast<BoxShadowList*>(other.data))); break;
 	default:
 		memcpy(data, other.data, LOCAL_DATA_SIZE);
 		type = other.type;
@@ -430,6 +439,30 @@ void Variant::Set(FontEffectsPtr&& value)
 		new (data) FontEffectsPtr(std::move(value));
 	}
 }
+void Variant::Set(const BoxShadowList& value)
+{
+	if (type == BOXSHADOWLIST)
+	{
+		*(BoxShadowList*)data = value;
+	}
+	else
+	{
+		type = BOXSHADOWLIST;
+		new (data) BoxShadowList(value);
+	}
+}
+void Variant::Set(BoxShadowList&& value)
+{
+	if (type == BOXSHADOWLIST)
+	{
+		(*(BoxShadowList*)data) = std::move(value);
+	}
+	else
+	{
+		type = BOXSHADOWLIST;
+		new (data) BoxShadowList(std::move(value));
+	}
+}
 
 Variant& Variant::operator=(const Variant& copy)
 {
@@ -483,6 +516,7 @@ bool Variant::operator==(const Variant& other) const
 	case DECORATORSPTR: return DEFAULT_VARIANT_COMPARE(DecoratorsPtr);
 	case FILTERSPTR: return DEFAULT_VARIANT_COMPARE(FiltersPtr);
 	case FONTEFFECTSPTR: return DEFAULT_VARIANT_COMPARE(FontEffectsPtr);
+	case BOXSHADOWLIST: return DEFAULT_VARIANT_COMPARE(BoxShadowList);
 	case NONE: return true;
 	}
 	RMLUI_ERRORMSG("Variant comparison not implemented for this type.");