diff --git a/src/Avalonia.Visuals/Media/ConicGradientBrush.cs b/src/Avalonia.Visuals/Media/ConicGradientBrush.cs
new file mode 100644
index 0000000000..7c1266fa17
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/ConicGradientBrush.cs
@@ -0,0 +1,55 @@
+using Avalonia.Media.Immutable;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Paints an area with a swept circular gradient.
+ ///
+ public sealed class ConicGradientBrush : GradientBrush, IConicGradientBrush
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty CenterProperty =
+ AvaloniaProperty.Register(
+ nameof(Center),
+ RelativePoint.Center);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty AngleProperty =
+ AvaloniaProperty.Register(
+ nameof(Angle),
+ 0);
+
+ static ConicGradientBrush()
+ {
+ AffectsRender(CenterProperty, AngleProperty);
+ }
+
+ ///
+ /// Gets or sets the center point of the gradient.
+ ///
+ public RelativePoint Center
+ {
+ get { return GetValue(CenterProperty); }
+ set { SetValue(CenterProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the angle of the start and end of the sweep, measured from above the center point.
+ ///
+ public double Angle
+ {
+ get { return GetValue(AngleProperty); }
+ set { SetValue(AngleProperty, value); }
+ }
+
+ ///
+ public override IBrush ToImmutable()
+ {
+ return new ImmutableConicGradientBrush(this);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/IConicGradientBrush.cs b/src/Avalonia.Visuals/Media/IConicGradientBrush.cs
new file mode 100644
index 0000000000..5368dd1851
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/IConicGradientBrush.cs
@@ -0,0 +1,19 @@
+namespace Avalonia.Media
+{
+ ///
+ /// Paints an area with a conic gradient.
+ ///
+ public interface IConicGradientBrush : IGradientBrush
+ {
+ ///
+ /// Gets the center point for the gradient.
+ ///
+ RelativePoint Center { get; }
+
+ ///
+ /// Gets the starting angle for the gradient in degrees, measured from
+ /// the point above the center point.
+ ///
+ double Angle { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs
new file mode 100644
index 0000000000..d3c80dfcad
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media.Immutable
+{
+ ///
+ /// A brush that draws with a sweep gradient.
+ ///
+ public class ImmutableConicGradientBrush : ImmutableGradientBrush, IConicGradientBrush
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The gradient stops.
+ /// The opacity of the brush.
+ /// The spread method.
+ /// The center point for the gradient.
+ /// The starting angle for the gradient.
+ public ImmutableConicGradientBrush(
+ IReadOnlyList gradientStops,
+ double opacity = 1,
+ GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad,
+ RelativePoint? center = null,
+ double angle = 0)
+ : base(gradientStops, opacity, spreadMethod)
+ {
+ Center = center ?? RelativePoint.Center;
+ Angle = angle;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The brush from which this brush's properties should be copied.
+ public ImmutableConicGradientBrush(ConicGradientBrush source)
+ : base(source)
+ {
+ Center = source.Center;
+ Angle = source.Angle;
+ }
+
+ ///
+ public RelativePoint Center { get; }
+
+ ///
+ public double Angle { get; }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index a32b3327c2..44e0c82110 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -641,6 +641,23 @@ namespace Avalonia.Skia
}
}
+ break;
+ }
+ case IConicGradientBrush conicGradient:
+ {
+ var center = conicGradient.Center.ToPixels(targetSize).ToSKPoint();
+
+ // Skia's default is that angle 0 is from the right hand side of the center point
+ // but we are matching CSS where the vertical point above the center is 0.
+ var angle = (float)(conicGradient.Angle - 90);
+ var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y);
+
+ using (var shader =
+ SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation))
+ {
+ paintWrapper.Paint.Shader = shader;
+ }
+
break;
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index ace658654d..136ff63f3d 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
@@ -423,6 +423,7 @@ namespace Avalonia.Direct2D1.Media
var solidColorBrush = brush as ISolidColorBrush;
var linearGradientBrush = brush as ILinearGradientBrush;
var radialGradientBrush = brush as IRadialGradientBrush;
+ var conicGradientBrush = brush as IConicGradientBrush;
var imageBrush = brush as IImageBrush;
var visualBrush = brush as IVisualBrush;
@@ -438,6 +439,11 @@ namespace Avalonia.Direct2D1.Media
{
return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationSize);
}
+ else if (conicGradientBrush != null)
+ {
+ // there is no Direct2D implementation of Conic Gradients so use Radial as a stand-in
+ return new SolidColorBrushImpl(conicGradientBrush, _deviceContext);
+ }
else if (imageBrush?.Source != null)
{
return new ImageBrushImpl(
diff --git a/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs
index f93b4a2e08..fea1ca9157 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs
@@ -16,5 +16,22 @@ namespace Avalonia.Direct2D1.Media
}
);
}
+
+ ///
+ /// Direct2D has no ConicGradient implementation so fall back to a solid colour brush based on
+ /// the first gradient stop.
+ ///
+ public SolidColorBrushImpl(IConicGradientBrush brush, SharpDX.Direct2D1.DeviceContext target)
+ {
+ PlatformBrush = new SharpDX.Direct2D1.SolidColorBrush(
+ target,
+ brush?.GradientStops[0].Color.ToDirect2D() ?? new SharpDX.Mathematics.Interop.RawColor4(),
+ new SharpDX.Direct2D1.BrushProperties
+ {
+ Opacity = brush != null ? (float)brush.Opacity : 1.0f,
+ Transform = target.Transform
+ }
+ );
+ }
}
}
diff --git a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs
new file mode 100644
index 0000000000..db69a0a028
--- /dev/null
+++ b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs
@@ -0,0 +1,178 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using System.Threading.Tasks;
+using Xunit;
+
+#if AVALONIA_SKIA
+namespace Avalonia.Skia.RenderTests
+#else
+namespace Avalonia.Direct2D1.RenderTests.Media
+#endif
+{
+ public class ConicGradientBrushTests : TestBase
+ {
+ public ConicGradientBrushTests() : base(@"Media\ConicGradientBrush")
+ {
+ }
+
+ [Fact]
+ public async Task ConicGradientBrush_RedBlue()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ Background = new ConicGradientBrush
+ {
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Blue, Offset = 1 }
+ }
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ [Fact]
+ public async Task ConicGradientBrush_RedBlue_Rotation()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ Background = new ConicGradientBrush
+ {
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Blue, Offset = 1 }
+ },
+ Angle = 90
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ [Fact]
+ public async Task ConicGradientBrush_RedBlue_Center()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ Background = new ConicGradientBrush
+ {
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Blue, Offset = 1 }
+ },
+ Center = new RelativePoint(0.25, 0.25, RelativeUnit.Relative)
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ [Fact]
+ public async Task ConicGradientBrush_RedBlue_Center_and_Rotation()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ Background = new ConicGradientBrush
+ {
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Blue, Offset = 1 }
+ },
+ Center = new RelativePoint(0.25, 0.25, RelativeUnit.Relative),
+ Angle = 90
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ [Fact]
+ public async Task ConicGradientBrush_RedBlue_SoftEdge()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ Background = new ConicGradientBrush
+ {
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Blue, Offset = 0.5 },
+ new GradientStop { Color = Colors.Red, Offset = 1 },
+ }
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ [Fact]
+ public async Task ConicGradientBrush_Umbrella()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ Background = new ConicGradientBrush
+ {
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Yellow, Offset = 0.1667 },
+ new GradientStop { Color = Colors.Lime, Offset = 0.3333 },
+ new GradientStop { Color = Colors.Aqua, Offset = 0.5000 },
+ new GradientStop { Color = Colors.Blue, Offset = 0.6667 },
+ new GradientStop { Color = Colors.Magenta, Offset = 0.8333 },
+ new GradientStop { Color = Colors.Red, Offset = 1 },
+ }
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+ }
+}
diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png
new file mode 100644
index 0000000000..8e08d02f66
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png
new file mode 100644
index 0000000000..8e08d02f66
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png
new file mode 100644
index 0000000000..8e08d02f66
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png
new file mode 100644
index 0000000000..8e08d02f66
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png
new file mode 100644
index 0000000000..8e08d02f66
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png
new file mode 100644
index 0000000000..8e08d02f66
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png
new file mode 100644
index 0000000000..563bbfcc61
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png
new file mode 100644
index 0000000000..424ac52059
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png
new file mode 100644
index 0000000000..a8719e2e70
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png
new file mode 100644
index 0000000000..1c79db413b
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png
new file mode 100644
index 0000000000..4cf8eddd81
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png
new file mode 100644
index 0000000000..98dfda782d
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png differ