Procházet zdrojové kódy

Add 'linear-gradient' and 'repeating-linear-gradient' decorators

- New property parser for color stop lists.
- Shorthand parser: Support for repeating, comma-separated patterns.
- Supports most of CSS syntax:
  - Angle and 'to <direction>' syntax for direction.
  - Multiple color stops, locations in length or percentage units, up to two locations per color.
  - Hints and color interpolation methods are not supported.
- Uses shader to draw the gradient.
- GL3 renderer support.
Michael Ragazzon před 2 roky
rodič
revize
b6efab1d8b

+ 163 - 12
Backends/RmlUi_Renderer_GL3.cpp

@@ -28,6 +28,7 @@
 
 #include "RmlUi_Renderer_GL3.h"
 #include <RmlUi/Core/Core.h>
+#include <RmlUi/Core/DecorationTypes.h>
 #include <RmlUi/Core/FileInterface.h>
 #include <RmlUi/Core/GeometryUtilities.h>
 #include <RmlUi/Core/Log.h>
@@ -57,6 +58,8 @@
 static constexpr int NUM_MSAA_SAMPLES = 2;
 
 #define RMLUI_PREMULTIPLIED_ALPHA 1
+
+#define MAX_NUM_STOPS 16
 #define BLUR_SIZE 7
 #define BLUR_NUM_WEIGHTS ((BLUR_SIZE + 1) / 2)
 
@@ -65,7 +68,7 @@ static constexpr int NUM_MSAA_SAMPLES = 2;
 
 #define RMLUI_SHADER_HEADER     \
 	RMLUI_SHADER_HEADER_VERSION \
-	"#define RMLUI_PREMULTIPLIED_ALPHA " RMLUI_STRINGIFY(RMLUI_PREMULTIPLIED_ALPHA) "\n"
+	"#define RMLUI_PREMULTIPLIED_ALPHA " RMLUI_STRINGIFY(RMLUI_PREMULTIPLIED_ALPHA) "\n#define MAX_NUM_STOPS " RMLUI_STRINGIFY(MAX_NUM_STOPS) "\n"
 
 static const char* shader_vert_main = RMLUI_SHADER_HEADER R"(
 uniform vec2 _translate;
@@ -116,6 +119,50 @@ void main() {
 }
 )";
 
+enum class ShaderGradientFunction { Linear, RepeatingLinear }; // Must match shader definitions below.
+static const char* shader_frag_gradient = RMLUI_SHADER_HEADER R"(
+#define LINEAR 0
+#define REPEATING_LINEAR 1
+#define PI 3.14159265
+
+uniform int _func; // one of the above definitions
+uniform vec2 _p;   // starting point
+uniform vec2 _v;   // vector to ending point
+uniform vec4 _stop_colors[MAX_NUM_STOPS];
+uniform float _stop_positions[MAX_NUM_STOPS]; // normalized, 0 -> starting point, 1 -> ending point
+uniform int _num_stops;
+
+in vec2 fragTexCoord;
+in vec4 fragColor;
+out vec4 finalColor;
+
+vec4 mix_stop_colors(float t) {
+	vec4 color = _stop_colors[0];
+
+	for (int i = 1; i < _num_stops; i++)
+		color = mix(color, _stop_colors[i], smoothstep(_stop_positions[i-1], _stop_positions[i], t));
+
+	return color;
+}
+
+void main() {
+	float t = 0;
+
+	float dist_square = dot(_v, _v);
+	vec2 V = fragTexCoord - _p;
+	t = dot(_v, V) / dist_square;
+
+	if (_func == REPEATING_LINEAR)
+	{
+		float t0 = _stop_positions[0];
+		float t1 = _stop_positions[_num_stops - 1];
+		t = t0 + mod(t - t0, t1 - t0);
+	}
+
+	finalColor = fragColor * mix_stop_colors(t);
+}
+)";
+
 static const char* shader_vert_passthrough = RMLUI_SHADER_HEADER R"(
 in vec2 inPosition;
 in vec2 inTexCoord0;
@@ -204,6 +251,7 @@ enum class ProgramId {
 	None,
 	Color,
 	Texture,
+	Gradient,
 	Passthrough,
 	ColorMatrix,
 	Blur,
@@ -219,6 +267,7 @@ enum class VertShaderId {
 enum class FragShaderId {
 	Color,
 	Texture,
+	Gradient,
 	Passthrough,
 	ColorMatrix,
 	Blur,
@@ -235,13 +284,19 @@ enum class UniformId {
 	TexCoordMin,
 	TexCoordMax,
 	Weights,
+	Func,
+	P,
+	V,
+	StopColors,
+	StopPositions,
+	NumStops,
 	Count,
 };
 
 namespace Gfx {
 
 static const char* const program_uniform_names[(size_t)UniformId::Count] = {"_translate", "_transform", "_tex", "_color", "_color_matrix",
-	"_texelOffset", "_texCoordMin", "_texCoordMax", "_weights[0]"};
+	"_texelOffset", "_texCoordMin", "_texCoordMax", "_weights[0]", "_func", "_p", "_v", "_stop_colors[0]", "_stop_positions[0]", "_num_stops"};
 
 enum class VertexAttribute { Position, Color0, TexCoord0, Count };
 static const char* const vertex_attribute_names[(size_t)VertexAttribute::Count] = {"inPosition", "inColor0", "inTexCoord0"};
@@ -272,6 +327,7 @@ static const VertShaderDefinition vert_shader_definitions[] = {
 static const FragShaderDefinition frag_shader_definitions[] = {
 	{FragShaderId::Color,       "color",        shader_frag_color},
 	{FragShaderId::Texture,     "texture",      shader_frag_texture},
+	{FragShaderId::Gradient,    "gradient",     shader_frag_gradient},
 	{FragShaderId::Passthrough, "passthrough",  shader_frag_passthrough},
 	{FragShaderId::ColorMatrix, "color_matrix", shader_frag_color_matrix},
 	{FragShaderId::Blur,        "blur",         shader_frag_blur},
@@ -280,6 +336,7 @@ static const FragShaderDefinition frag_shader_definitions[] = {
 static const ProgramDefinition program_definitions[] = {
 	{ProgramId::Color,       "color",        VertShaderId::Main,        FragShaderId::Color},
 	{ProgramId::Texture,     "texture",      VertShaderId::Main,        FragShaderId::Texture},
+	{ProgramId::Gradient,    "gradient",     VertShaderId::Main,        FragShaderId::Gradient},
 	{ProgramId::Passthrough, "passthrough",  VertShaderId::Passthrough, FragShaderId::Passthrough},
 	{ProgramId::ColorMatrix, "color_matrix", VertShaderId::Passthrough, FragShaderId::ColorMatrix},
 	{ProgramId::Blur,        "blur",         VertShaderId::Blur,        FragShaderId::Blur},
@@ -1202,6 +1259,16 @@ void RenderInterface_GL3::DrawFullscreenQuad(Rml::Vector2f uv_offset, Rml::Vecto
 	RenderGeometry(vertices, 4, indices, 6, RenderInterface_GL3::TexturePostprocess, {});
 }
 
+static Rml::Colourf ToPremultipliedAlpha(Rml::Colourb c0)
+{
+	Rml::Colourf result;
+	result.alpha = (1.f / 255.f) * float(c0.alpha);
+	result.red = (1.f / 255.f) * float(c0.red) * result.alpha;
+	result.green = (1.f / 255.f) * float(c0.green) * result.alpha;
+	result.blue = (1.f / 255.f) * float(c0.blue) * result.alpha;
+	return result;
+}
+
 static void SigmaToParameters(const float desired_sigma, int& out_pass_level, float& out_sigma)
 {
 	constexpr int max_num_passes = 10;
@@ -1493,6 +1560,100 @@ void RenderInterface_GL3::ReleaseCompiledFilter(Rml::CompiledFilterHandle filter
 	delete reinterpret_cast<CompiledFilter*>(filter);
 }
 
+enum class CompiledShaderType { Invalid = 0, Gradient };
+struct CompiledShader {
+	CompiledShaderType type;
+
+	// Gradient
+	ShaderGradientFunction gradient_function;
+	Rml::Vector2f p;
+	Rml::Vector2f v;
+	Rml::Vector<float> stop_positions;
+	Rml::Vector<Rml::Colourf> stop_colors;
+};
+
+Rml::CompiledShaderHandle RenderInterface_GL3::CompileShader(const Rml::String& name, const Rml::Dictionary& parameters)
+{
+	auto ApplyColorStopList = [](CompiledShader& shader, const Rml::Dictionary& shader_parameters) {
+		auto it = shader_parameters.find("color_stop_list");
+		RMLUI_ASSERT(it != shader_parameters.end() && it->second.GetType() == Rml::Variant::COLORSTOPLIST);
+		const Rml::ColorStopList& color_stop_list = it->second.GetReference<Rml::ColorStopList>();
+		const int num_stops = Rml::Math::Min((int)color_stop_list.size(), MAX_NUM_STOPS);
+
+		shader.stop_positions.resize(num_stops);
+		shader.stop_colors.resize(num_stops);
+		for (int i = 0; i < num_stops; i++)
+		{
+			const Rml::ColorStop& stop = color_stop_list[i];
+			RMLUI_ASSERT(stop.position.unit == Rml::Unit::NUMBER);
+			shader.stop_positions[i] = stop.position.number;
+			shader.stop_colors[i] = ToPremultipliedAlpha(stop.color);
+		}
+	};
+
+	CompiledShader shader = {};
+
+	if (name == "linear-gradient")
+	{
+		shader.type = CompiledShaderType::Gradient;
+		const bool repeating = Rml::Get(parameters, "repeating", false);
+		shader.gradient_function = (repeating ? ShaderGradientFunction::RepeatingLinear : ShaderGradientFunction::Linear);
+		shader.p = Rml::Get(parameters, "p0", Rml::Vector2f(0.f));
+		shader.v = Rml::Get(parameters, "p1", Rml::Vector2f(0.f)) - shader.p;
+		ApplyColorStopList(shader, parameters);
+	}
+
+	if (shader.type != CompiledShaderType::Invalid)
+		return reinterpret_cast<Rml::CompiledShaderHandle>(new CompiledShader(std::move(shader)));
+
+	Rml::Log::Message(Rml::Log::LT_WARNING, "Unsupported shader type '%s'.", name.c_str());
+	return {};
+}
+
+void RenderInterface_GL3::RenderShader(Rml::CompiledShaderHandle shader_handle, Rml::CompiledGeometryHandle geometry_handle,
+	Rml::Vector2f translation, Rml::TextureHandle /*texture*/)
+{
+	RMLUI_ASSERT(geometry_handle);
+	const CompiledShader& shader = *reinterpret_cast<CompiledShader*>(shader_handle);
+	const CompiledShaderType type = shader.type;
+	const Gfx::CompiledGeometryData& geometry = *reinterpret_cast<Gfx::CompiledGeometryData*>(geometry_handle);
+
+	switch (type)
+	{
+	case CompiledShaderType::Gradient:
+	{
+		RMLUI_ASSERT(shader.stop_positions.size() == shader.stop_colors.size());
+		const int num_stops = (int)shader.stop_positions.size();
+
+		UseProgram(ProgramId::Gradient);
+		glUniform1i(GetUniformLocation(UniformId::Func), static_cast<int>(shader.gradient_function));
+		glUniform2f(GetUniformLocation(UniformId::P), shader.p.x, shader.p.y);
+		glUniform2f(GetUniformLocation(UniformId::V), shader.v.x, shader.v.y);
+		glUniform1i(GetUniformLocation(UniformId::NumStops), num_stops);
+		glUniform1fv(GetUniformLocation(UniformId::StopPositions), num_stops, shader.stop_positions.data());
+		glUniform4fv(GetUniformLocation(UniformId::StopColors), num_stops, shader.stop_colors[0]);
+
+		SubmitTransformUniform(translation);
+		glBindVertexArray(geometry.vao);
+		glDrawElements(GL_TRIANGLES, geometry.draw_count, GL_UNSIGNED_INT, (const GLvoid*)0);
+		glBindVertexArray(0);
+	}
+	break;
+	case CompiledShaderType::Invalid:
+	{
+		Rml::Log::Message(Rml::Log::LT_WARNING, "Unhandled render shader %d.", (int)type);
+	}
+	break;
+	}
+
+	Gfx::CheckGLError("RenderShader");
+}
+
+void RenderInterface_GL3::ReleaseCompiledShader(Rml::CompiledShaderHandle effect_handle)
+{
+	delete reinterpret_cast<CompiledShader*>(effect_handle);
+}
+
 void RenderInterface_GL3::BlitTopLayerToPostprocessPrimary()
 {
 	const Gfx::FramebufferData& source = render_layers.GetTopLayer();
@@ -1504,16 +1665,6 @@ void RenderInterface_GL3::BlitTopLayerToPostprocessPrimary()
 	glBlitFramebuffer(0, 0, source.width, source.height, 0, 0, destination.width, destination.height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
 }
 
-static inline Rml::Colourf ToPremultipliedAlpha(Rml::Colourb c0)
-{
-	Rml::Colourf result;
-	result.alpha = (1.f / 255.f) * float(c0.alpha);
-	result.red = (1.f / 255.f) * float(c0.red) * result.alpha;
-	result.green = (1.f / 255.f) * float(c0.green) * result.alpha;
-	result.blue = (1.f / 255.f) * float(c0.blue) * result.alpha;
-	return result;
-}
-
 void RenderInterface_GL3::RenderFilters(const Rml::FilterHandleList& filter_handles)
 {
 	for (const Rml::CompiledFilterHandle filter_handle : filter_handles)

+ 5 - 0
Backends/RmlUi_Renderer_GL3.h

@@ -89,6 +89,11 @@ public:
 	Rml::CompiledFilterHandle CompileFilter(const Rml::String& name, const Rml::Dictionary& parameters) override;
 	void ReleaseCompiledFilter(Rml::CompiledFilterHandle filter) override;
 
+	Rml::CompiledShaderHandle CompileShader(const Rml::String& name, const Rml::Dictionary& parameters) override;
+	void RenderShader(Rml::CompiledShaderHandle shader_handle, Rml::CompiledGeometryHandle geometry_handle, Rml::Vector2f translation,
+		Rml::TextureHandle texture) override;
+	void ReleaseCompiledShader(Rml::CompiledShaderHandle effect_handle) override;
+
 	// Can be passed to RenderGeometry() to enable texture rendering without changing the bound texture.
 	static constexpr Rml::TextureHandle TextureEnableWithoutBinding = Rml::TextureHandle(-1);
 	// Can be passed to RenderGeometry() to leave the bound texture and used program unchanged.

+ 2 - 0
CMake/FileList.cmake

@@ -82,6 +82,7 @@ set(Core_HDR_FILES
     ${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/PropertyParserColorStopList.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserColour.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserDecorator.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserFilter.h
@@ -353,6 +354,7 @@ set(Core_SRC_FILES
     ${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/PropertyParserColorStopList.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserColour.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserDecorator.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserFilter.cpp

+ 1 - 0
Include/RmlUi/Core.h

@@ -40,6 +40,7 @@
 #include "Core/DataTypeRegister.h"
 #include "Core/DataTypes.h"
 #include "Core/DataVariable.h"
+#include "Core/DecorationTypes.h"
 #include "Core/Decorator.h"
 #include "Core/EffectSpecification.h"
 #include "Core/Element.h"

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

@@ -34,6 +34,19 @@
 
 namespace Rml {
 
+struct ColorStop {
+	Colourb color;
+	NumericValue position;
+};
+inline bool operator==(const ColorStop& a, const ColorStop& b)
+{
+	return a.color == b.color && a.position == b.position;
+}
+inline bool operator!=(const ColorStop& a, const ColorStop& b)
+{
+	return !(a == b);
+}
+
 struct BoxShadow {
 	Colourb color;
 	NumericValue offset_x, offset_y;

+ 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<ColorStopList, ColorStopList> {
+public:
+	RMLUICORE_API static bool Convert(const ColorStopList& src, ColorStopList& dest);
+};
+template <>
+class TypeConverter<ColorStopList, String> {
+public:
+	RMLUICORE_API static bool Convert(const ColorStopList& src, String& dest);
+};
+
 template <>
 class TypeConverter<BoxShadowList, BoxShadowList> {
 public:

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

@@ -90,6 +90,7 @@ struct Transition;
 struct TransitionList;
 struct DecoratorDeclarationList;
 struct FilterDeclarationList;
+struct ColorStop;
 struct BoxShadow;
 enum class EventId : uint16_t;
 enum class PropertyId : uint8_t;
@@ -101,6 +102,7 @@ using FileHandle = uintptr_t;
 using TextureHandle = uintptr_t;
 using CompiledGeometryHandle = uintptr_t;
 using CompiledFilterHandle = uintptr_t;
+using CompiledShaderHandle = uintptr_t;
 using DecoratorDataHandle = uintptr_t;
 using FontFaceHandle = uintptr_t;
 using FontEffectsHandle = uintptr_t;
@@ -128,6 +130,7 @@ struct FontEffects {
 	FontEffectList list;
 	String value;
 };
+using ColorStopList = Vector<ColorStop>;
 using BoxShadowList = Vector<BoxShadow>;
 
 // Additional smart pointers

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

@@ -72,6 +72,7 @@ public:
 		DECORATORSPTR = 'D',
 		FILTERSPTR = 'F',
 		FONTEFFECTSPTR = 'E',
+		COLORSTOPLIST = 'C',
 		BOXSHADOWLIST = 'S',
 		VOIDPTR = '*',
 	};
@@ -159,6 +160,8 @@ private:
 	void Set(FiltersPtr&& value);
 	void Set(const FontEffectsPtr& value);
 	void Set(FontEffectsPtr&& value);
+	void Set(const ColorStopList& value);
+	void Set(ColorStopList&& value);
 	void Set(const BoxShadowList& value);
 	void Set(BoxShadowList&& 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 COLORSTOPLIST: return TypeConverter<ColorStopList, T>::Convert(*(ColorStopList*)data, value); break;
 	case BOXSHADOWLIST: return TypeConverter<BoxShadowList, T>::Convert(*reinterpret_cast<const BoxShadowList*>(data), value);
 	case NONE: break;
 	}

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

@@ -63,6 +63,8 @@
 
 .transform, .filter.transform_all > .box { transform: rotate3d(0.2, 0.4, 0.1, 15deg); }
 
+.gradient { decorator: linear-gradient(110deg, #fff3, #fff 10%, #c33 250dp, #3c3, #33c, #000 90%, #0003); }
+
 .brightness { filter: brightness(0.5); }
 .contrast { filter: contrast(0.5); }
 .sepia { filter: sepia(80%); }
@@ -155,6 +157,7 @@
 
 <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>
+<div class="box big gradient"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 
 <div class="box hue_rotate"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 

+ 281 - 0
Source/Core/DecoratorGradient.cpp

@@ -34,9 +34,120 @@
 #include "../../Include/RmlUi/Core/GeometryUtilities.h"
 #include "../../Include/RmlUi/Core/Math.h"
 #include "../../Include/RmlUi/Core/PropertyDefinition.h"
+#include "ComputeProperty.h"
 
 namespace Rml {
 
+// Returns the point along the input line ('line_point', 'line_vector') closest to the input 'point'.
+static Vector2f IntersectionPointToLineNormal(const Vector2f point, const Vector2f line_point, const Vector2f line_vector)
+{
+	const Vector2f delta = line_point - point;
+	return line_point - delta.DotProduct(line_vector) * line_vector;
+}
+
+/// Convert all color stop positions to normalized numbers.
+/// @param[in] element The element to resolve lengths against.
+/// @param[in] gradient_line_length The length of the gradient line, along which color stops are placed.
+/// @param[in] soft_spacing The desired minimum distance between stops to avoid aliasing, in normalized number units.
+/// @param[in] unresolved_stops
+/// @return A list of resolved color stops, all in number units.
+static ColorStopList ResolveColorStops(Element* element, const float gradient_line_length, const float soft_spacing,
+	const ColorStopList& unresolved_stops)
+{
+	ColorStopList stops = unresolved_stops;
+	const int num_stops = (int)stops.size();
+
+	// Resolve all lengths, percentages, and angles to numbers. After this step all stops with a unit other than Number are considered as Auto.
+	for (ColorStop& stop : stops)
+	{
+		if (Any(stop.position.unit & Unit::LENGTH))
+		{
+			const float resolved_position = element->ResolveLength(stop.position);
+			stop.position = NumericValue(resolved_position / gradient_line_length, Unit::NUMBER);
+		}
+		else if (stop.position.unit == Unit::PERCENT)
+		{
+			stop.position = NumericValue(stop.position.number * 0.01f, Unit::NUMBER);
+		}
+		else if (Any(stop.position.unit & Unit::ANGLE))
+		{
+			stop.position = NumericValue(ComputeAngle(stop.position) * (1.f / (2.f * Math::RMLUI_PI)), Unit::NUMBER);
+		}
+	}
+
+	// Resolve auto positions of the first and last color stops.
+	auto resolve_edge_stop = [](ColorStop& stop, float auto_to_number) {
+		if (stop.position.unit != Unit::NUMBER)
+			stop.position = NumericValue(auto_to_number, Unit::NUMBER);
+	};
+	resolve_edge_stop(stops[0], 0.f);
+	resolve_edge_stop(stops[num_stops - 1], 1.f);
+
+	// Ensures that color stop positions are strictly increasing, and have at least 1px spacing to avoid aliasing.
+	auto nudge_stop = [prev_position = stops[0].position.number](ColorStop& stop, bool update_prev = true) mutable {
+		stop.position.number = Math::Max(stop.position.number, prev_position);
+		if (update_prev)
+			prev_position = stop.position.number;
+	};
+	int auto_begin_i = -1;
+
+	// Evenly space stops with sequential auto indices, and nudge stop positions to ensure strictly increasing positions.
+	for (int i = 1; i < num_stops; i++)
+	{
+		ColorStop& stop = stops[i];
+		if (stop.position.unit != Unit::NUMBER)
+		{
+			// Mark the first of any consecutive auto stops.
+			if (auto_begin_i < 0)
+				auto_begin_i = i;
+		}
+		else if (auto_begin_i < 0)
+		{
+			// The stop has a definite position and there are no previous autos to handle, just ensure it is properly spaced.
+			nudge_stop(stop);
+		}
+		else
+		{
+			// Space out all the previous auto stops, indices [auto_begin_i, i).
+			nudge_stop(stop, false);
+			const int num_auto_stops = i - auto_begin_i;
+			const float t0 = stops[auto_begin_i - 1].position.number;
+			const float t1 = stop.position.number;
+
+			for (int j = 0; j < num_auto_stops; j++)
+			{
+				const float fraction_along_t0_t1 = float(j + 1) / float(num_auto_stops + 1);
+				stops[j + auto_begin_i].position = NumericValue(t0 + (t1 - t0) * fraction_along_t0_t1, Unit::NUMBER);
+				nudge_stop(stops[j + auto_begin_i]);
+			}
+
+			nudge_stop(stop);
+			auto_begin_i = -1;
+		}
+	}
+
+	// Ensures that stops are placed some minimum distance from each other to avoid aliasing, if possible.
+	for (int i = 1; i < num_stops - 1; i++)
+	{
+		const float p0 = stops[i - 1].position.number;
+		const float p1 = stops[i].position.number;
+		const float p2 = stops[i + 1].position.number;
+		float& new_position = stops[i].position.number;
+
+		if (p1 - p0 < soft_spacing)
+		{
+			if (p2 - p0 < 2.f * soft_spacing)
+				new_position = 0.5f * (p2 + p0);
+			else
+				new_position = p0 + soft_spacing;
+		}
+	}
+
+	RMLUI_ASSERT(std::all_of(stops.begin(), stops.end(), [](auto&& stop) { return stop.position.unit == Unit::NUMBER; }));
+
+	return stops;
+}
+
 DecoratorStraightGradient::DecoratorStraightGradient() {}
 
 DecoratorStraightGradient::~DecoratorStraightGradient() {}
@@ -138,4 +249,174 @@ SharedPtr<Decorator> DecoratorStraightGradientInstancer::InstanceDecorator(const
 	return nullptr;
 }
 
+DecoratorLinearGradient::DecoratorLinearGradient() {}
+
+DecoratorLinearGradient::~DecoratorLinearGradient() {}
+
+bool DecoratorLinearGradient::Initialise(bool in_repeating, Corner in_corner, float in_angle, const ColorStopList& in_color_stops)
+{
+	repeating = in_repeating;
+	corner = in_corner;
+	angle = in_angle;
+	color_stops = in_color_stops;
+	return !color_stops.empty();
+}
+
+DecoratorDataHandle DecoratorLinearGradient::GenerateElementData(Element* element) const
+{
+	RenderInterface* render_interface = GetRenderInterface();
+	if (!render_interface)
+		return INVALID_DECORATORDATAHANDLE;
+
+	RMLUI_ASSERT(!color_stops.empty());
+
+	BoxArea box_area = BoxArea::Padding;
+	const Box& box = element->GetBox();
+	const Vector2f dimensions = box.GetSize(box_area);
+
+	LinearGradientShape gradient_shape = CalculateShape(dimensions);
+
+	// One-pixel minimum color stop spacing to avoid aliasing.
+	const float soft_spacing = 1.f / gradient_shape.length;
+
+	ColorStopList resolved_stops = ResolveColorStops(element, gradient_shape.length, soft_spacing, color_stops);
+
+	auto element_data = MakeUnique<ElementData>();
+	element_data->shader = render_interface->CompileShader("linear-gradient",
+		Dictionary{
+			{"angle", Variant(angle)},
+			{"p0", Variant(gradient_shape.p0)},
+			{"p1", Variant(gradient_shape.p1)},
+			{"length", Variant(gradient_shape.length)},
+			{"repeating", Variant(repeating)},
+			{"color_stop_list", Variant(std::move(resolved_stops))},
+		});
+
+	if (!element_data->shader)
+		return INVALID_DECORATORDATAHANDLE;
+
+	Geometry& geometry = element_data->geometry;
+
+	const ComputedValues& computed = element->GetComputedValues();
+	const byte alpha = byte(computed.opacity() * 255.f);
+	GeometryUtilities::GenerateBackground(&geometry, box, Vector2f(), computed.border_radius(), Colourb(255, alpha), box_area);
+
+	const Vector2f render_offset = box.GetPosition(box_area);
+	for (Vertex& vertex : geometry.GetVertices())
+		vertex.tex_coord = vertex.position - render_offset;
+
+	return reinterpret_cast<DecoratorDataHandle>(element_data.release());
+}
+
+void DecoratorLinearGradient::ReleaseElementData(DecoratorDataHandle handle) const
+{
+	ElementData* element_data = reinterpret_cast<ElementData*>(handle);
+	GetRenderInterface()->ReleaseCompiledShader(element_data->shader);
+	delete element_data;
+}
+
+void DecoratorLinearGradient::RenderElement(Element* element, DecoratorDataHandle handle) const
+{
+	ElementData* element_data = reinterpret_cast<ElementData*>(handle);
+	element_data->geometry.RenderWithShader(element_data->shader, element->GetAbsoluteOffset(BoxArea::Border));
+}
+
+DecoratorLinearGradient::LinearGradientShape DecoratorLinearGradient::CalculateShape(Vector2f dim) const
+{
+	using uint = unsigned int;
+	const Vector2f corners[(int)Corner::Count] = {Vector2f(dim.x, 0), dim, Vector2f(0, dim.y), Vector2f(0, 0)};
+	const Vector2f center = 0.5f * dim;
+
+	uint quadrant = 0;
+	Vector2f line_vector;
+
+	if (corner == Corner::None)
+	{
+		// Find the target quadrant and unit vector for the given angle.
+		quadrant = uint(Math::NormaliseAngle(angle) * (4.f / (2.f * Math::RMLUI_PI))) % 4u;
+		line_vector = Vector2f(Math::Sin(angle), -Math::Cos(angle));
+	}
+	else
+	{
+		// Quadrant given by the corner, need to find the vector perpendicular to the line connecting the neighboring corners.
+		quadrant = uint(corner);
+		const Vector2f v_neighbors = (corners[(quadrant + 1u) % 4u] - corners[(quadrant + 3u) % 4u]).Normalise();
+		line_vector = {v_neighbors.y, -v_neighbors.x};
+	}
+
+	const uint quadrant_opposite = (quadrant + 2u) % 4u;
+
+	const Vector2f starting_point = IntersectionPointToLineNormal(corners[quadrant_opposite], center, line_vector);
+	const Vector2f ending_point = IntersectionPointToLineNormal(corners[quadrant], center, line_vector);
+
+	const float length = Math::Absolute(dim.x * line_vector.x) + Math::Absolute(-dim.y * line_vector.y);
+
+	return LinearGradientShape{starting_point, ending_point, length};
+}
+
+DecoratorLinearGradientInstancer::DecoratorLinearGradientInstancer()
+{
+	ids.angle = RegisterProperty("angle", "180deg").AddParser("angle").GetId();
+	ids.direction_to = RegisterProperty("to", "unspecified").AddParser("keyword", "unspecified, to").GetId();
+	// See Direction enum for keyword values.
+	ids.direction_x = RegisterProperty("direction-x", "unspecified").AddParser("keyword", "unspecified=0, left=8, right=2").GetId();
+	ids.direction_y = RegisterProperty("direction-y", "unspecified").AddParser("keyword", "unspecified=0, top=1, bottom=4").GetId();
+	ids.color_stop_list = RegisterProperty("color-stops", "").AddParser("color_stop_list").GetId();
+
+	RegisterShorthand("direction", "angle, to, direction-x, direction-y, direction-x", ShorthandType::FallThrough);
+	RegisterShorthand("decorator", "direction?, color-stops#", ShorthandType::RecursiveCommaSeparated);
+}
+
+DecoratorLinearGradientInstancer::~DecoratorLinearGradientInstancer() {}
+
+SharedPtr<Decorator> DecoratorLinearGradientInstancer::InstanceDecorator(const String& name, const PropertyDictionary& properties_,
+	const DecoratorInstancerInterface& /*interface_*/)
+{
+	const Property* p_angle = properties_.GetProperty(ids.angle);
+	const Property* p_direction_to = properties_.GetProperty(ids.direction_to);
+	const Property* p_direction_x = properties_.GetProperty(ids.direction_x);
+	const Property* p_direction_y = properties_.GetProperty(ids.direction_y);
+	const Property* p_color_stop_list = properties_.GetProperty(ids.color_stop_list);
+
+	if (!p_angle || !p_direction_to || !p_direction_x || !p_direction_y || !p_color_stop_list)
+		return nullptr;
+
+	using Corner = DecoratorLinearGradient::Corner;
+	Corner corner = Corner::None;
+	float angle = 0.f;
+
+	if (p_direction_to->Get<bool>())
+	{
+		const Direction direction = (Direction)(p_direction_x->Get<int>() | p_direction_y->Get<int>());
+		switch (direction)
+		{
+		case Direction::Top: angle = 0.f; break;
+		case Direction::Right: angle = 0.5f * Math::RMLUI_PI; break;
+		case Direction::Bottom: angle = Math::RMLUI_PI; break;
+		case Direction::Left: angle = 1.5f * Math::RMLUI_PI; break;
+		case Direction::TopLeft: corner = Corner::TopLeft; break;
+		case Direction::TopRight: corner = Corner::TopRight; break;
+		case Direction::BottomRight: corner = Corner::BottomRight; break;
+		case Direction::BottomLeft: corner = Corner::BottomLeft; break;
+		case Direction::None:
+		default: return nullptr; break;
+		}
+	}
+	else
+	{
+		angle = ComputeAngle(p_angle->GetNumericValue());
+	}
+
+	if (p_color_stop_list->unit != Unit::COLORSTOPLIST)
+		return nullptr;
+	const ColorStopList& color_stop_list = p_color_stop_list->value.GetReference<ColorStopList>();
+	const bool repeating = (name == "repeating-linear-gradient");
+
+	auto decorator = MakeShared<DecoratorLinearGradient>();
+	if (decorator->Initialise(repeating, corner, angle, color_stop_list))
+		return decorator;
+
+	return nullptr;
+}
+
 } // namespace Rml

+ 66 - 0
Source/Core/DecoratorGradient.h

@@ -29,7 +29,9 @@
 #ifndef RMLUI_CORE_DECORATORGRADIENT_H
 #define RMLUI_CORE_DECORATORGRADIENT_H
 
+#include "../../Include/RmlUi/Core/DecorationTypes.h"
 #include "../../Include/RmlUi/Core/Decorator.h"
+#include "../../Include/RmlUi/Core/Geometry.h"
 #include "../../Include/RmlUi/Core/ID.h"
 
 namespace Rml {
@@ -75,5 +77,69 @@ private:
 	PropertyIds ids;
 };
 
+/**
+    Linear gradient.
+ */
+class DecoratorLinearGradient : public Decorator {
+public:
+	enum class Corner { TopRight, BottomRight, BottomLeft, TopLeft, None, Count = None };
+
+	DecoratorLinearGradient();
+	virtual ~DecoratorLinearGradient();
+
+	bool Initialise(bool repeating, Corner corner, float angle, const ColorStopList& color_stops);
+
+	DecoratorDataHandle GenerateElementData(Element* element) const override;
+	void ReleaseElementData(DecoratorDataHandle element_data) const override;
+
+	void RenderElement(Element* element, DecoratorDataHandle element_data) const override;
+
+private:
+	struct LinearGradientShape {
+		// Gradient line starting and ending points.
+		Vector2f p0, p1;
+		float length;
+	};
+	struct ElementData {
+		Geometry geometry;
+		CompiledShaderHandle shader;
+	};
+
+	LinearGradientShape CalculateShape(Vector2f box_dimensions) const;
+
+	bool repeating = false;
+	Corner corner = Corner::None;
+	float angle = 0.f;
+	ColorStopList color_stops;
+};
+
+class DecoratorLinearGradientInstancer : public DecoratorInstancer {
+public:
+	DecoratorLinearGradientInstancer();
+	~DecoratorLinearGradientInstancer();
+
+	SharedPtr<Decorator> InstanceDecorator(const String& name, const PropertyDictionary& properties,
+		const DecoratorInstancerInterface& instancer_interface) override;
+
+private:
+	enum class Direction {
+		None = 0,
+		Top = 1,
+		Right = 2,
+		Bottom = 4,
+		Left = 8,
+		TopLeft = Top | Left,
+		TopRight = Top | Right,
+		BottomRight = Bottom | Right,
+		BottomLeft = Bottom | Left,
+	};
+	struct PropertyIds {
+		PropertyId angle;
+		PropertyId direction_to, direction_x, direction_y;
+		PropertyId color_stop_list;
+	};
+	PropertyIds ids;
+};
+
 } // namespace Rml
 #endif

+ 3 - 0
Source/Core/Factory.cpp

@@ -153,6 +153,7 @@ struct DefaultInstancers {
 	DecoratorTiledImageInstancer decorator_image;
 	DecoratorNinePatchInstancer decorator_ninepatch;
 	DecoratorStraightGradientInstancer decorator_straight_gradient;
+	DecoratorLinearGradientInstancer decorator_linear_gradient;
 
 	// Filters
 	FilterBasicInstancer filter_hue_rotate = {FilterBasicInstancer::ValueType::Angle, "0rad"};
@@ -244,6 +245,8 @@ bool Factory::Initialise()
 	RegisterDecoratorInstancer("gradient", &default_instancers->decorator_straight_gradient);
 	RegisterDecoratorInstancer("horizontal-gradient", &default_instancers->decorator_straight_gradient);
 	RegisterDecoratorInstancer("vertical-gradient", &default_instancers->decorator_straight_gradient);
+	RegisterDecoratorInstancer("linear-gradient", &default_instancers->decorator_linear_gradient);
+	RegisterDecoratorInstancer("repeating-linear-gradient", &default_instancers->decorator_linear_gradient);
 
 	// Filter instancers
 	RegisterFilterInstancer("hue-rotate", &default_instancers->filter_hue_rotate);

+ 100 - 0
Source/Core/PropertyParserColorStopList.cpp

@@ -0,0 +1,100 @@
+/*
+ * 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 "PropertyParserColorStopList.h"
+#include "../../Include/RmlUi/Core/ComputedValues.h"
+#include "../../Include/RmlUi/Core/DecorationTypes.h"
+#include <string.h>
+
+namespace Rml {
+
+PropertyParserColorStopList::PropertyParserColorStopList(PropertyParser* parser_color) :
+	parser_color(parser_color), parser_length_percent_angle(Unit::LENGTH_PERCENT | Unit::ANGLE, Unit::PERCENT)
+{
+	RMLUI_ASSERT(parser_color);
+}
+
+PropertyParserColorStopList::~PropertyParserColorStopList() {}
+
+bool PropertyParserColorStopList::ParseValue(Property& property, const String& value, const ParameterMap& parameters) const
+{
+	const ParameterMap empty_parameter_map;
+
+	if (value.empty())
+		return false;
+
+	StringList color_stop_str_list;
+	StringUtilities::ExpandString(color_stop_str_list, value, ',', '(', ')');
+
+	if (color_stop_str_list.empty())
+		return false;
+
+	const Unit accepted_units = (parameters.count("angle") ? (Unit::ANGLE | Unit::PERCENT) : Unit::LENGTH_PERCENT);
+
+	ColorStopList color_stops;
+	color_stops.reserve(color_stop_str_list.size());
+
+	for (const String& color_stop_str : color_stop_str_list)
+	{
+		StringList values;
+		StringUtilities::ExpandString(values, color_stop_str, ' ', '(', ')', true);
+
+		if (values.empty() || values.size() > 3)
+			return false;
+
+		Property p_color;
+		if (!parser_color->ParseValue(p_color, values[0], empty_parameter_map))
+			return false;
+
+		ColorStop color_stop = {};
+		color_stop.color = p_color.Get<Colourb>();
+
+		if (values.size() <= 1)
+			color_stops.push_back(color_stop);
+
+		for (size_t i = 1; i < values.size(); i++)
+		{
+			Property p_position(Style::LengthPercentageAuto::Auto);
+			if (!parser_length_percent_angle.ParseValue(p_position, values[i], empty_parameter_map))
+				return false;
+
+			if (Any(p_position.unit & accepted_units))
+				color_stop.position = NumericValue(p_position.Get<float>(), p_position.unit);
+			else if (p_position.unit != Unit::KEYWORD)
+				return false;
+
+			color_stops.push_back(color_stop);
+		}
+	}
+
+	property.value = Variant(std::move(color_stops));
+	property.unit = Unit::COLORSTOPLIST;
+
+	return true;
+}
+} // namespace Rml

+ 55 - 0
Source/Core/PropertyParserColorStopList.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_PROPERTYPARSERCOLORSTOPLIST_H
+#define RMLUI_CORE_PROPERTYPARSERCOLORSTOPLIST_H
+
+#include "../../Include/RmlUi/Core/PropertyParser.h"
+#include "../../Include/RmlUi/Core/Types.h"
+#include "PropertyParserNumber.h"
+
+namespace Rml {
+
+/**
+    A property parser that parses color stop lists, particularly for gradients.
+ */
+
+class PropertyParserColorStopList : public PropertyParser {
+public:
+	PropertyParserColorStopList(PropertyParser* parser_color);
+	virtual ~PropertyParserColorStopList();
+
+	bool ParseValue(Property& property, const String& value, const ParameterMap& parameters) const override;
+
+private:
+	PropertyParser* parser_color;
+	PropertyParserNumber parser_length_percent_angle;
+};
+
+} // namespace Rml
+#endif

+ 8 - 7
Source/Core/PropertyShorthandDefinition.h

@@ -41,15 +41,17 @@ enum class ShorthandItemType { Invalid, Property, Shorthand };
 
 // Each entry in a shorthand points either to another shorthand or a property
 struct ShorthandItem {
-	ShorthandItem() : type(ShorthandItemType::Invalid), property_id(PropertyId::Invalid), property_definition(nullptr), optional(false) {}
-	ShorthandItem(PropertyId id, const PropertyDefinition* definition, bool optional) :
-		type(ShorthandItemType::Property), property_id(id), property_definition(definition), optional(optional)
+	ShorthandItem() : type(ShorthandItemType::Invalid), property_id(PropertyId::Invalid), property_definition(nullptr) {}
+	ShorthandItem(PropertyId id, const PropertyDefinition* definition, bool optional, bool repeats) :
+		type(ShorthandItemType::Property), optional(optional), repeats(repeats), property_id(id), property_definition(definition)
 	{}
-	ShorthandItem(ShorthandId id, const ShorthandDefinition* definition, bool optional) :
-		type(ShorthandItemType::Shorthand), shorthand_id(id), shorthand_definition(definition), optional(optional)
+	ShorthandItem(ShorthandId id, const ShorthandDefinition* definition, bool optional, bool repeats) :
+		type(ShorthandItemType::Shorthand), optional(optional), repeats(repeats), shorthand_id(id), shorthand_definition(definition)
 	{}
 
-	ShorthandItemType type;
+	ShorthandItemType type = ShorthandItemType::Invalid;
+	bool optional = false;
+	bool repeats = false;
 	union {
 		PropertyId property_id;
 		ShorthandId shorthand_id;
@@ -58,7 +60,6 @@ struct ShorthandItem {
 		const PropertyDefinition* property_definition;
 		const ShorthandDefinition* shorthand_definition;
 	};
-	bool optional;
 };
 
 // A list of shorthands or properties

+ 25 - 7
Source/Core/PropertySpecification.cpp

@@ -138,6 +138,7 @@ ShorthandId PropertySpecification::RegisterShorthand(const String& shorthand_nam
 	{
 		ShorthandItem item;
 		bool optional = false;
+		bool repeats = false;
 		String name = raw_name;
 
 		if (!raw_name.empty() && raw_name.back() == '?')
@@ -145,13 +146,18 @@ ShorthandId PropertySpecification::RegisterShorthand(const String& shorthand_nam
 			optional = true;
 			name.pop_back();
 		}
+		if (!raw_name.empty() && raw_name.back() == '#')
+		{
+			repeats = true;
+			name.pop_back();
+		}
 
 		PropertyId property_id = property_map->GetId(name);
 		if (property_id != PropertyId::Invalid)
 		{
 			// We have a valid property
 			if (const PropertyDefinition* property = GetProperty(property_id))
-				item = ShorthandItem(property_id, property, optional);
+				item = ShorthandItem(property_id, property, optional, repeats);
 		}
 		else
 		{
@@ -162,7 +168,7 @@ ShorthandId PropertySpecification::RegisterShorthand(const String& shorthand_nam
 			if (shorthand_id != ShorthandId::Invalid && (type == ShorthandType::RecursiveRepeat || type == ShorthandType::RecursiveCommaSeparated))
 			{
 				if (const ShorthandDefinition* shorthand = GetShorthand(shorthand_id))
-					item = ShorthandItem(shorthand_id, shorthand, optional);
+					item = ShorthandItem(shorthand_id, shorthand, optional, repeats);
 			}
 		}
 
@@ -267,10 +273,10 @@ bool PropertySpecification::ParseShorthandDeclaration(PropertyDictionary& dictio
 		return false;
 
 	// Handle the special behavior of the flex shorthand first, otherwise it acts like 'FallThrough'.
-	if (shorthand_definition->type == ShorthandType::Flex)
+	if (shorthand_definition->type == ShorthandType::Flex && !property_values.empty())
 	{
 		RMLUI_ASSERT(shorthand_definition->items.size() == 3);
-		if (!property_values.empty() && property_values[0] == "none")
+		if (property_values[0] == "none")
 		{
 			property_values = {"0", "0", "auto"};
 		}
@@ -292,8 +298,7 @@ bool PropertySpecification::ParseShorthandDeclaration(PropertyDictionary& dictio
 		}
 	}
 
-	// If this definition is a 'box'-style shorthand (x-top, x-right, x-bottom, x-left, etc) and there are fewer
-	// than four values
+	// If this definition is a 'box'-style shorthand (x-top, x-right, x-bottom, x-left, etc) and there are fewer than four values
 	if (shorthand_definition->type == ShorthandType::Box && property_values.size() < 4)
 	{
 		// This array tells which property index each side is parsed from
@@ -360,12 +365,22 @@ bool PropertySpecification::ParseShorthandDeclaration(PropertyDictionary& dictio
 		}
 
 		size_t subvalue_i = 0;
+		String temp_subvalue;
 		for (size_t i = 0; i < shorthand_definition->items.size() && subvalue_i < property_values.size(); i++)
 		{
 			bool result = false;
 
 			const String* subvalue = &property_values[subvalue_i];
+
 			const ShorthandItem& item = shorthand_definition->items[i];
+			if (item.repeats)
+			{
+				property_values.erase(property_values.begin(), property_values.begin() + subvalue_i);
+				temp_subvalue.clear();
+				StringUtilities::JoinString(temp_subvalue, property_values);
+				subvalue = &temp_subvalue;
+			}
+
 			if (item.type == ShorthandItemType::Property)
 				result = ParsePropertyDeclaration(dictionary, item.property_id, *subvalue);
 			else if (item.type == ShorthandItemType::Shorthand)
@@ -373,8 +388,11 @@ bool PropertySpecification::ParseShorthandDeclaration(PropertyDictionary& dictio
 
 			if (result)
 				subvalue_i += 1;
-			else if (!item.optional)
+			else if (item.repeats || !item.optional)
 				return false;
+
+			if (item.repeats)
+				break;
 		}
 	}
 	else

+ 3 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -32,6 +32,7 @@
 #include "IdNameMap.h"
 #include "PropertyParserAnimation.h"
 #include "PropertyParserBoxShadow.h"
+#include "PropertyParserColorStopList.h"
 #include "PropertyParserColour.h"
 #include "PropertyParserDecorator.h"
 #include "PropertyParserFilter.h"
@@ -59,6 +60,7 @@ struct DefaultStyleSheetParsers : NonCopyMoveable {
 	PropertyParserAnimation animation = PropertyParserAnimation(PropertyParserAnimation::ANIMATION_PARSER);
 	PropertyParserAnimation transition = PropertyParserAnimation(PropertyParserAnimation::TRANSITION_PARSER);
 	PropertyParserColour color = PropertyParserColour();
+	PropertyParserColorStopList color_stop_list = PropertyParserColorStopList(&color);
 	PropertyParserDecorator decorator = PropertyParserDecorator();
 	PropertyParserFilter filter = PropertyParserFilter();
 	PropertyParserFontEffect font_effect = PropertyParserFontEffect();
@@ -255,6 +257,7 @@ void StyleSheetSpecification::RegisterDefaultParsers()
 	RegisterParser("animation", &default_parsers->animation);
 	RegisterParser("transition", &default_parsers->transition);
 	RegisterParser("color", &default_parsers->color);
+	RegisterParser("color_stop_list", &default_parsers->color_stop_list);
 	RegisterParser("decorator", &default_parsers->decorator);
 	RegisterParser("filter", &default_parsers->filter);
 	RegisterParser("font_effect", &default_parsers->font_effect);

+ 23 - 0
Source/Core/TypeConverter.cpp

@@ -224,6 +224,29 @@ bool TypeConverter<FontEffectsPtr, String>::Convert(const FontEffectsPtr& src, S
 	return true;
 }
 
+bool TypeConverter<ColorStopList, ColorStopList>::Convert(const ColorStopList& src, ColorStopList& dest)
+{
+	dest = src;
+	return true;
+}
+
+bool TypeConverter<ColorStopList, String>::Convert(const ColorStopList& src, String& dest)
+{
+	dest.clear();
+	for (size_t i = 0; i < src.size(); i++)
+	{
+		const ColorStop& stop = src[i];
+		dest += ToString(stop.color);
+
+		if (Any(stop.position.unit & Unit::NUMBER_LENGTH_PERCENT))
+			dest += " " + ToString(stop.position.number) + ToString(stop.position.unit);
+
+		if (i < src.size() - 1)
+			dest += ", ";
+	}
+	return true;
+}
+
 bool TypeConverter<BoxShadowList, BoxShadowList>::Convert(const BoxShadowList& src, BoxShadowList& dest)
 {
 	dest = src;

+ 35 - 0
Source/Core/Variant.cpp

@@ -45,6 +45,8 @@ Variant::Variant()
 	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");
+	static_assert(sizeof(ColorStopList) <= LOCAL_DATA_SIZE, "Local data too small for ColorStopList");
+	static_assert(sizeof(BoxShadowList) <= LOCAL_DATA_SIZE, "Local data too small for BoxShadowList");
 }
 
 Variant::Variant(const Variant& copy)
@@ -113,6 +115,12 @@ void Variant::Clear()
 		font_effects->~shared_ptr();
 	}
 	break;
+	case COLORSTOPLIST:
+	{
+		ColorStopList* value = (ColorStopList*)data;
+		value->~ColorStopList();
+	}
+	break;
 	case BOXSHADOWLIST:
 	{
 		BoxShadowList* value = (BoxShadowList*)data;
@@ -137,6 +145,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 COLORSTOPLIST: Set(*reinterpret_cast<const ColorStopList*>(copy.data)); break;
 	case BOXSHADOWLIST: Set(*reinterpret_cast<const BoxShadowList*>(copy.data)); break;
 	default:
 		memcpy(data, copy.data, LOCAL_DATA_SIZE);
@@ -157,6 +166,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 COLORSTOPLIST: Set(std::move(*reinterpret_cast<ColorStopList*>(other.data))); break;
 	case BOXSHADOWLIST: Set(std::move(*reinterpret_cast<BoxShadowList*>(other.data))); break;
 	default:
 		memcpy(data, other.data, LOCAL_DATA_SIZE);
@@ -439,6 +449,30 @@ void Variant::Set(FontEffectsPtr&& value)
 		new (data) FontEffectsPtr(std::move(value));
 	}
 }
+void Variant::Set(const ColorStopList& value)
+{
+	if (type == COLORSTOPLIST)
+	{
+		*(ColorStopList*)data = value;
+	}
+	else
+	{
+		type = COLORSTOPLIST;
+		new (data) ColorStopList(value);
+	}
+}
+void Variant::Set(ColorStopList&& value)
+{
+	if (type == COLORSTOPLIST)
+	{
+		(*(ColorStopList*)data) = std::move(value);
+	}
+	else
+	{
+		type = COLORSTOPLIST;
+		new (data) ColorStopList(std::move(value));
+	}
+}
 void Variant::Set(const BoxShadowList& value)
 {
 	if (type == BOXSHADOWLIST)
@@ -516,6 +550,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 COLORSTOPLIST: return DEFAULT_VARIANT_COMPARE(ColorStopList);
 	case BOXSHADOWLIST: return DEFAULT_VARIANT_COMPARE(BoxShadowList);
 	case NONE: return true;
 	}

+ 102 - 0
Tests/Data/VisualTests/shader_linear_gradient.rml

@@ -0,0 +1,102 @@
+<rml>
+<head>
+	<title>linear-gradient</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<meta name="Description" content="A variety of linear-gradient backgrounds. Each group should match their description." />
+	<meta name="Backend" content="Requires backend support for rendering with compiled shaders." />
+	<link rel="source" href="https://www.w3.org/TR/css-images-3/#linear-gradients" />
+	<style>
+		body {
+			background: #ddd;
+			color: #444;
+			width: 900dp;
+		}
+		div {
+			margin: 5dp;
+			width: 200dp;
+			height: 100dp;
+			box-sizing: border-box;
+			border: 1dp #bbb;
+			float: left;
+		}
+		group {
+			margin-left: 1em;
+			display: flow-root;
+			margin-bottom: 1em;
+		}
+
+		.yellow_blue > :nth-child(1) { decorator: linear-gradient(yellow, blue); }
+		.yellow_blue > :nth-child(2) { decorator: linear-gradient(yellow, blue); }
+		.yellow_blue > :nth-child(3) { decorator: linear-gradient(180deg, yellow, blue); }
+		.yellow_blue > :nth-child(4) { decorator: linear-gradient(to bottom, yellow, blue); }
+		.yellow_blue > :nth-child(5) { decorator: linear-gradient(to top, blue, yellow); }
+		.yellow_blue > :nth-child(6) { decorator: linear-gradient(to bottom, yellow 0%, blue 100%); }
+
+		.yellow_blue-angle > :nth-child(1) { decorator: linear-gradient(135deg, yellow, blue); }
+		.yellow_blue-angle > :nth-child(2) { decorator: linear-gradient(-45deg, blue, yellow); }
+
+		.three_color > :nth-child(1) { decorator: linear-gradient(yellow, blue 20%, #0f0); }
+
+		.corner_to_corner > :nth-child(1) { decorator: linear-gradient(to top right, red, white, blue); }
+		.corner_to_corner > :nth-child(2) { decorator: linear-gradient(to bottom right, red, white, blue); }
+		.corner_to_corner > :nth-child(3) { decorator: linear-gradient(to bottom left, red, white, blue); }
+		.corner_to_corner > :nth-child(4) { decorator: linear-gradient(to top left, red, white, blue); }
+
+		.corner_to_corner > :nth-child(5) { decorator: linear-gradient(to top right, red 49%, white, blue 51%); }
+		.corner_to_corner > :nth-child(6) { decorator: linear-gradient(to bottom right, red 49%, white, blue 51%); }
+		.corner_to_corner > :nth-child(7) { decorator: linear-gradient(to bottom left, red 48%, white, blue 52%); width: 50dp; }
+		.corner_to_corner > :nth-child(8) { decorator: linear-gradient(to top left, red 48%, white, blue 52%); width: 50dp; }
+
+		.repeating > :nth-child(1) { decorator: repeating-linear-gradient(50deg, red, white, blue 20%); }
+		.repeating > :nth-child(2) { decorator: repeating-linear-gradient(red, blue 20px, red 40px); }
+
+		.premultiplied_alpha > :nth-child(1) { decorator: linear-gradient(90deg, red, transparent, blue); background: #fff; }
+	</style>
+</head>
+
+<body>
+Yellow (top) to blue (bottom) [equivalent]
+<group class="yellow_blue">
+	<div/>
+	<div/>
+	<div/>
+	<div/>
+	<div/>
+	<div/>
+</group>
+
+Yellow (top-left) to blue (bottom-right) [equivalent]
+<group class="yellow_blue-angle">
+	<div/>
+	<div/>
+</group>
+
+Yellow (top), blue, green (bottom)
+<group class="three_color">
+	<div/>
+</group>
+
+Corner-to-corner, first: red (bottom-left), white, blue (top-right)
+<group class="corner_to_corner">
+	<div/>
+	<div/>
+	<div/>
+	<div/>
+	<div/>
+	<div/>
+	<div/>
+	<div/>
+</group>
+
+Repeating linear gradients
+<group class="repeating">
+	<div/>
+	<div/>
+</group>
+
+Red (left), white, blue (right). Should not show any grayish transition colors if the backend correctly interpolates in premultiplied alpha space.
+<group class="premultiplied_alpha">
+	<div/>
+</group>
+</body>
+</rml>

+ 97 - 32
Tests/Source/UnitTests/Properties.cpp

@@ -29,8 +29,10 @@
 #include "../Common/TestsInterface.h"
 #include <RmlUi/Core/Context.h>
 #include <RmlUi/Core/Core.h>
+#include <RmlUi/Core/DecorationTypes.h>
 #include <RmlUi/Core/Element.h>
 #include <RmlUi/Core/ElementDocument.h>
+#include <RmlUi/Core/StyleSheetTypes.h>
 #include <doctest.h>
 
 using namespace Rml;
@@ -50,42 +52,105 @@ TEST_CASE("Properties")
 	Context* context = Rml::CreateContext("main", window_size);
 	ElementDocument* document = context->CreateDocument();
 
-	struct FlexTestCase {
-		String flex_value;
-
-		struct ExpectedValues {
-			float flex_grow;
-			float flex_shrink;
-			String flex_basis;
-		} expected;
-	};
-
-	FlexTestCase tests[] = {
-		{"", {0.f, 1.f, "auto"}},
-		{"none", {0.f, 0.f, "auto"}},
-		{"auto", {1.f, 1.f, "auto"}},
-		{"1", {1.f, 1.f, "0px"}},
-		{"2", {2.f, 1.f, "0px"}},
-		{"2 0", {2.f, 0.f, "0px"}},
-		{"2 3", {2.f, 3.f, "0px"}},
-		{"2 auto", {2.f, 1.f, "auto"}},
-		{"2 0 auto", {2.f, 0.f, "auto"}},
-		{"0 0 auto", {0.f, 0.f, "auto"}},
-		{"0 0 50px", {0.f, 0.f, "50px"}},
-		{"0 0 50px", {0.f, 0.f, "50px"}},
-		{"0 0 0", {0.f, 0.f, "0px"}},
-	};
-
-	for (const FlexTestCase& test : tests)
+	SUBCASE("flex")
 	{
-		if (!test.flex_value.empty())
+		struct FlexTestCase {
+			String flex_value;
+
+			struct ExpectedValues {
+				float flex_grow;
+				float flex_shrink;
+				String flex_basis;
+			} expected;
+		};
+
+		FlexTestCase tests[] = {
+			{"", {0.f, 1.f, "auto"}},
+			{"none", {0.f, 0.f, "auto"}},
+			{"auto", {1.f, 1.f, "auto"}},
+			{"1", {1.f, 1.f, "0px"}},
+			{"2", {2.f, 1.f, "0px"}},
+			{"2 0", {2.f, 0.f, "0px"}},
+			{"2 3", {2.f, 3.f, "0px"}},
+			{"2 auto", {2.f, 1.f, "auto"}},
+			{"2 0 auto", {2.f, 0.f, "auto"}},
+			{"0 0 auto", {0.f, 0.f, "auto"}},
+			{"0 0 50px", {0.f, 0.f, "50px"}},
+			{"0 0 50px", {0.f, 0.f, "50px"}},
+			{"0 0 0", {0.f, 0.f, "0px"}},
+		};
+
+		for (const FlexTestCase& test : tests)
 		{
-			CHECK(document->SetProperty("flex", test.flex_value));
+			if (!test.flex_value.empty())
+			{
+				CHECK(document->SetProperty("flex", test.flex_value));
+			}
+
+			CHECK(document->GetProperty<float>("flex-grow") == test.expected.flex_grow);
+			CHECK(document->GetProperty<float>("flex-shrink") == test.expected.flex_shrink);
+			CHECK(document->GetProperty("flex-basis")->ToString() == test.expected.flex_basis);
 		}
+	}
 
-		CHECK(document->GetProperty<float>("flex-grow") == test.expected.flex_grow);
-		CHECK(document->GetProperty<float>("flex-shrink") == test.expected.flex_shrink);
-		CHECK(document->GetProperty("flex-basis")->ToString() == test.expected.flex_basis);
+	SUBCASE("gradient")
+	{
+		auto ParseGradient = [&](const String& value) -> ColorStopList {
+			document->SetProperty("decorator", "linear-gradient(" + value + ")");
+			auto decorators = document->GetProperty<DecoratorsPtr>("decorator");
+			if (!decorators || decorators->list.size() != 1)
+				return {};
+			for (auto& id_property : decorators->list.front().properties.GetProperties())
+			{
+				if (id_property.second.unit == Unit::COLORSTOPLIST)
+					return id_property.second.Get<ColorStopList>();
+			}
+			return {};
+		};
+
+		struct GradientTestCase {
+			String value;
+			ColorStopList expected_color_stops;
+		};
+
+		GradientTestCase test_cases[] = {
+			{
+				"red, blue",
+				{
+					ColorStop{Colourb(255, 0, 0), NumericValue{}},
+					ColorStop{Colourb(0, 0, 255), NumericValue{}},
+				},
+			},
+			{
+				"red 5px, blue 50%",
+				{
+					ColorStop{Colourb(255, 0, 0), NumericValue{5.f, Unit::PX}},
+					ColorStop{Colourb(0, 0, 255), NumericValue{50.f, Unit::PERCENT}},
+				},
+			},
+			{
+				"red, #00f 50%, rgba(0, 255,0, 150) 10dp",
+				{
+					ColorStop{Colourb(255, 0, 0), NumericValue{}},
+					ColorStop{Colourb(0, 0, 255), NumericValue{50.f, Unit::PERCENT}},
+					ColorStop{Colourb(0, 255, 0, 150), NumericValue{10.f, Unit::DP}},
+				},
+			},
+			{
+				"red 50px 20%, blue 10in",
+				{
+					ColorStop{Colourb(255, 0, 0), NumericValue{50.f, Unit::PX}},
+					ColorStop{Colourb(255, 0, 0), NumericValue{20.f, Unit::PERCENT}},
+					ColorStop{Colourb(0, 0, 255), NumericValue{10.f, Unit::INCH}},
+				},
+			},
+		};
+
+		for (const GradientTestCase& test_case : test_cases)
+		{
+			const ColorStopList result = ParseGradient(test_case.value);
+			CHECK(result == test_case.expected_color_stops);
+		}
 	}
 
 	Rml::Shutdown();