Browse Source

Inset box-shadow

Nikita Tsukanov 5 years ago
parent
commit
d0b041095d
25 changed files with 475 additions and 144 deletions
  1. 27 0
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 0 2
      src/Avalonia.Controls/Border.cs
  3. 0 2
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  4. 37 13
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  5. 3 1
      src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs
  6. 71 24
      src/Avalonia.Visuals/Media/BoxShadow.cs
  7. 2 2
      src/Avalonia.Visuals/Media/DrawingContext.cs
  8. 2 9
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  9. 2 0
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  10. 10 0
      src/Avalonia.Visuals/Rect.cs
  11. 6 6
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  12. 3 10
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  13. 11 29
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  14. 136 0
      src/Avalonia.Visuals/RoundedRect.cs
  15. 89 33
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  16. 2 0
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  17. 5 0
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  18. 2 0
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  19. 8 5
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  20. 2 0
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  21. 2 0
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  22. 45 0
      tests/Avalonia.Visuals.UnitTests/Media/BoxShadowTests.cs
  23. 4 4
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  24. 4 4
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
  25. 2 0
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

+ 27 - 0
samples/RenderDemo/Pages/AnimationsPage.xaml

@@ -151,6 +151,31 @@
           </Animation>
         </Style.Animations>
       </Style>
+      <Style Selector="Border.InsetShadow">
+        <Setter Property="BorderBrush" Value="Black"/>
+        <Setter Property="BorderThickness" Value="1"/>
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Alternate">
+            <KeyFrame Cue="0%">
+              <Setter Property="BoxShadow" Value="inset 0 0 0 2 Red"/>
+            </KeyFrame>
+            <KeyFrame Cue="35%">
+              <Setter Property="BoxShadow" Value="inset 0 0 20 2 Blue"/>
+            </KeyFrame>
+            <KeyFrame Cue="70%">
+              <Setter Property="BoxShadow" Value="inset 0 0 20 30 Green"/>
+            </KeyFrame>
+            <KeyFrame Cue="85%">
+              <Setter Property="BoxShadow" Value="inset 30 0 20 30 Green"/>
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="BoxShadow" Value="inset 30 30 20 30 Green"/>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
     </Styles>
   </UserControl.Styles>
   <Grid>
@@ -171,6 +196,8 @@
         <Border Classes="Test Rect6" Background="Red"/>
         <Border Classes="Test Shadow" Background="PaleGreen" CornerRadius="10" Child="{x:Null}" />
         <Border Classes="Test Shadow" Background="PaleGreen" CornerRadius="0 30 60 0" Child="{x:Null}" />
+        <Border Classes="Test InsetShadow" CornerRadius="10" Child="{x:Null}" />
+        <Border Classes="Test InsetShadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
       </WrapPanel>
     </StackPanel>
   </Grid>

+ 0 - 2
src/Avalonia.Controls/Border.cs

@@ -127,8 +127,6 @@ namespace Avalonia.Controls
         /// <returns>The space taken.</returns>
         protected override Size ArrangeOverride(Size finalSize)
         {
-            _borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);
-
             return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness);
         }
     }

+ 0 - 2
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -336,8 +336,6 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         protected override Size ArrangeOverride(Size finalSize)
         {
-            _borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
-
             return ArrangeOverrideImpl(finalSize, new Vector());
         }
 

+ 37 - 13
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@@ -7,12 +7,24 @@ namespace Avalonia.Controls.Utils
     internal class BorderRenderHelper
     {
         private bool _useComplexRendering;
+        private bool? _backendSupportsIndividualCorners;
         private StreamGeometry _backgroundGeometryCache;
         private StreamGeometry _borderGeometryCache;
+        private Size _size;
+        private Thickness _borderThickness;
+        private CornerRadius _cornerRadius;
+        private bool _initialized;
 
-        public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
+        void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
         {
-            if (borderThickness.IsUniform && cornerRadius.IsUniform)
+            _backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetService<IPlatformRenderInterface>()
+                .SupportsIndividualRoundRects;
+            _size = finalSize;
+            _borderThickness = borderThickness;
+            _cornerRadius = cornerRadius;
+            _initialized = true;
+            
+            if (borderThickness.IsUniform && (cornerRadius.IsUniform || _backendSupportsIndividualCorners == true))
             {
                 _backgroundGeometryCache = null;
                 _borderGeometryCache = null;
@@ -68,19 +80,28 @@ namespace Avalonia.Controls.Utils
             }
         }
 
-        public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background,
-            IBrush borderBrush, BoxShadow boxShadow)
+        public void Render(DrawingContext context,
+            Size finalSize, Thickness borderThickness, CornerRadius cornerRadius,
+            IBrush background, IBrush borderBrush, BoxShadow boxShadow)
+        {
+            if (_size != finalSize
+                || _borderThickness != borderThickness
+                || _cornerRadius != cornerRadius
+                || !_initialized)
+                Update(finalSize, borderThickness, cornerRadius);
+            RenderCore(context, background, borderBrush, boxShadow);
+        }
+        
+        void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadow boxShadow)
         {
             if (_useComplexRendering)
             {
                 var backgroundGeometry = _backgroundGeometryCache;
                 if (backgroundGeometry != null)
                 {
-                    // We are using platform impl here because I'm not sure if we should
-                    // make the box shadow for geometries to be public API
-                    context.PlatformImpl.DrawGeometry(background, null, backgroundGeometry.PlatformImpl, boxShadow);
+                    context.DrawGeometry(background, null, backgroundGeometry);
                 }
-
+                
                 var borderGeometry = _borderGeometryCache;
                 if (borderGeometry != null)
                 {
@@ -89,9 +110,7 @@ namespace Avalonia.Controls.Utils
             }
             else
             {
-                var borderThickness = borders.Top;
-                var top = borderThickness * 0.5;
-
+                var borderThickness = _borderThickness.Top;
                 IPen pen = null;
 
                 if (borderThickness > 0)
@@ -99,9 +118,14 @@ namespace Avalonia.Controls.Utils
                     pen = new Pen(borderBrush, borderThickness);
                 }
 
-                var rect = new Rect(top, top, size.Width - borderThickness, size.Height - borderThickness);
+                var rrect = new RoundedRect(new Rect(_size), _cornerRadius.TopLeft, _cornerRadius.TopRight,
+                    _cornerRadius.BottomRight, _cornerRadius.BottomLeft);
+                if (Math.Abs(borderThickness) > double.Epsilon)
+                {
+                    rrect = rrect.Deflate(borderThickness * 0.5, borderThickness * 0.5);
+                }
 
-                context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft, boxShadow);
+                context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadow);
             }
         }    
 

+ 3 - 1
src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs

@@ -6,6 +6,7 @@ namespace Avalonia.Animation.Animators
     {
         static ColorAnimator s_colorAnimator = new ColorAnimator();
         static DoubleAnimator s_doubleAnimator = new DoubleAnimator();
+        static BoolAnimator s_boolAnimator = new BoolAnimator();
         public override BoxShadow Interpolate(double progress, BoxShadow oldValue, BoxShadow newValue)
         {
             return new BoxShadow
@@ -14,7 +15,8 @@ namespace Avalonia.Animation.Animators
                 OffsetY = s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY),
                 Blur = s_doubleAnimator.Interpolate(progress, oldValue.Blur, newValue.Blur),
                 Spread = s_doubleAnimator.Interpolate(progress, oldValue.Spread, newValue.Spread),
-                Color = s_colorAnimator.Interpolate(progress, oldValue.Color, newValue.Color)
+                Color = s_colorAnimator.Interpolate(progress, oldValue.Color, newValue.Color),
+                IsInset = s_boolAnimator.Interpolate(progress, oldValue.IsInset, newValue.IsInset)
             };
         }
     }

+ 71 - 24
src/Avalonia.Visuals/Media/BoxShadow.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Globalization;
 using Avalonia.Animation.Animators;
 using Avalonia.Utilities;
 
@@ -11,6 +12,7 @@ namespace Avalonia.Media
         public double Blur { get; set; }
         public double Spread { get; set; }
         public Color Color { get; set; }
+        public bool IsInset { get; set; }
 
         static BoxShadow()
         {
@@ -43,37 +45,82 @@ namespace Avalonia.Media
 
         public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0;
 
+        private readonly static char[] s_Separator = new char[] { ' ', '\t' };
+
+        struct ArrayReader
+        {
+            private int _index;
+            private string[] _arr;
+
+            public ArrayReader(string[] arr)
+            {
+                _arr = arr;
+                _index = 0;
+            }
+
+            public bool TryReadString(out string s)
+            {
+                s = null;
+                if (_index >= _arr.Length)
+                    return false;
+                s = _arr[_index];
+                _index++;
+                return true;
+            }
+
+            public string ReadString()
+            {
+                if(!TryReadString(out var rv))
+                    throw new FormatException();
+                return rv;
+            }
+        }
         public static unsafe BoxShadow Parse(string s)
         {
+            if(s == null)
+                throw new ArgumentNullException();
+            if (s.Length == 0)
+                throw new FormatException();
+            if (s[0] == ' ' || s[s.Length - 1] == ' ')
+                s = s.Trim();
+            
             if (s == "none")
                 return default;
+
+            var p = s.Split(s_Separator, StringSplitOptions.RemoveEmptyEntries);
+            if (p.Length < 3 || p.Length > 6)
+                throw new FormatException();
             
-            var separatorCount = 0;
-            var separators = stackalloc char[4];
-            for(var c = 0; c<s.Length; c++)
-                if (s[c] == ',' || s[c] == ' ')
-                {
-                    if (separatorCount == 4)
-                        throw new FormatException("Invalid box-shadow format");
-                    separators[separatorCount] = s[c];
-                    separatorCount++;
-                }
-
-            if (separatorCount != 2 && separatorCount > 4)
-                throw new FormatException("Invalid box-shadow format");
-
-            var tokenizer = new StringTokenizer(s);
-            var offsetX = tokenizer.ReadDouble(separators[0]);
-            var offsetY = tokenizer.ReadDouble(separators[1]);
+            bool inset = false;
+
+            var tokenizer = new ArrayReader(p);
+
+            string firstToken = tokenizer.ReadString();
+            if (firstToken == "inset")
+            {
+                inset = true;
+                firstToken = tokenizer.ReadString();
+            }
+
+            var offsetX = double.Parse(firstToken, CultureInfo.InvariantCulture);
+            var offsetY = double.Parse(tokenizer.ReadString(), CultureInfo.InvariantCulture);
             double blur = 0;
             double spread = 0;
-            if (separatorCount > 2)
-                blur = tokenizer.ReadDouble(separators[2]);
-            if (separatorCount > 3)
-                spread = tokenizer.ReadDouble(separators[3]);
-            var color = Media.Color.Parse(tokenizer.ReadString());
+            
+
+            tokenizer.TryReadString(out var token3);
+            tokenizer.TryReadString(out var token4);
+            tokenizer.TryReadString(out var token5);
+
+            if (token4 != null) 
+                blur = double.Parse(token3, CultureInfo.InvariantCulture);
+            if (token5 != null)
+                spread = double.Parse(token4, CultureInfo.InvariantCulture);
+
+            var color = Color.Parse(token5 ?? token4 ?? token3);
             return new BoxShadow
             {
+                IsInset = inset,
                 OffsetX = offsetX,
                 OffsetY = offsetY,
                 Blur = blur,
@@ -82,7 +129,7 @@ namespace Avalonia.Media
             };
         }
 
-        public Rect TransformBounds(in Rect rect) 
-            => rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur);
+        public Rect TransformBounds(in Rect rect)
+            => IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur);
     }
 }

+ 2 - 2
src/Avalonia.Visuals/Media/DrawingContext.cs

@@ -125,7 +125,7 @@ namespace Avalonia.Media
 
             if (brush != null || PenIsVisible(pen))
             {
-                PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl, default);
+                PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl);
             }
         }
 
@@ -164,7 +164,7 @@ namespace Avalonia.Media
                 radiusY = Math.Min(radiusY, rect.Height / 2);
             }
 
-            PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY, boxShadow);
+            PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadow);
         }
 
         /// <summary>

+ 2 - 9
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@@ -55,8 +55,7 @@ namespace Avalonia.Platform
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
-        /// <param name="boxShadow">The box shadow parameters</param>
-        void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow);
+        void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry);
 
         /// <summary>
         /// Draws a rectangle with the specified Brush and Pen.
@@ -64,18 +63,12 @@ namespace Avalonia.Platform
         /// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
         /// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
         /// <param name="rect">The rectangle bounds.</param>
-        /// <param name="radiusX">The radius in the X dimension of the rounded corners.
-        ///     This value will be clamped to the range of 0 to Width/2
-        /// </param>
-        /// <param name="radiusY">The radius in the Y dimension of the rounded corners.
-        ///     This value will be clamped to the range of 0 to Height/2
-        /// </param>
         /// <param name="boxShadow">Box shadow effect parameters</param>
         /// <remarks>
         /// The brush and the pen can both be null. If the brush is null, then no fill is performed.
         /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
         /// </remarks>
-        void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0,
+        void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect,
             BoxShadow boxShadow = default);
 
         /// <summary>

+ 2 - 0
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@@ -116,5 +116,7 @@ namespace Avalonia.Platform
         /// <param name="width">The glyph run's width.</param>
         /// <returns></returns>
         IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
+
+        bool SupportsIndividualRoundRects { get; }
     }
 }

+ 10 - 0
src/Avalonia.Visuals/Rect.cs

@@ -132,6 +132,16 @@ namespace Avalonia
         /// Gets the bottom position of the rectangle.
         /// </summary>
         public double Bottom => _y + _height;
+        
+        /// <summary>
+        /// Gets the left position.
+        /// </summary>
+        public double Left => _x;
+
+        /// <summary>
+        /// Gets the top position.
+        /// </summary>
+        public double Top => _y;
 
         /// <summary>
         /// Gets the top left point of the rectangle.

+ 6 - 6
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -97,13 +97,13 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow)
+        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
         {
             var next = NextDrawAs<GeometryNode>();
 
-            if (next == null || !next.Item.Equals(Transform, brush, pen, geometry, boxShadow))
+            if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
             {
-                Add(new GeometryNode(Transform, brush, pen, geometry, boxShadow, CreateChildScene(brush)));
+                Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
             }
             else
             {
@@ -149,14 +149,14 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0D, double radiusY = 0D,
+        public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect,
             BoxShadow boxShadow = default)
         {
             var next = NextDrawAs<RectangleNode>();
 
-            if (next == null || !next.Item.Equals(Transform, brush, pen, rect, radiusX, radiusY, boxShadow))
+            if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadow))
             {
-                Add(new RectangleNode(Transform, brush, pen, rect, radiusX, radiusY, boxShadow, CreateChildScene(brush)));
+                Add(new RectangleNode(Transform, brush, pen, rect, boxShadow, CreateChildScene(brush)));
             }
             else
             {

+ 3 - 10
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@@ -19,20 +19,17 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
         /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
-        /// <param name="boxShadow"></param>
         public GeometryNode(Matrix transform,
             IBrush brush,
             IPen pen,
             IGeometryImpl geometry,
-            BoxShadow boxShadow,
             IDictionary<IVisual, Scene> childScenes = null)
-            : base(boxShadow.TransformBounds(geometry.GetRenderBounds(pen)), transform, null)
+            : base(geometry.GetRenderBounds(pen), transform, null)
         {
             Transform = transform;
             Brush = brush?.ToImmutable();
             Pen = pen?.ToImmutable();
             Geometry = geometry;
-            BoxShadow = boxShadow;
             ChildScenes = childScenes;
         }
 
@@ -56,8 +53,6 @@ namespace Avalonia.Rendering.SceneGraph
         /// </summary>
         public IGeometryImpl Geometry { get; }
 
-        public BoxShadow BoxShadow { get; }
-
         /// <inheritdoc/>
         public override IDictionary<IVisual, Scene> ChildScenes { get; }
 
@@ -74,11 +69,9 @@ namespace Avalonia.Rendering.SceneGraph
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
-        public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry,
-            BoxShadow boxShadow)
+        public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry)
         {
             return transform == Transform &&
-                   BoxShadow.Equals(boxShadow, boxShadow) &&
                    Equals(brush, Brush) &&
                    Equals(Pen, pen) &&
                    Equals(geometry, Geometry);
@@ -88,7 +81,7 @@ namespace Avalonia.Rendering.SceneGraph
         public override void Render(IDrawingContextImpl context)
         {
             context.Transform = Transform;
-            context.DrawGeometry(Brush, Pen, Geometry, BoxShadow);
+            context.DrawGeometry(Brush, Pen, Geometry);
         }
 
         /// <inheritdoc/>

+ 11 - 29
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@@ -19,26 +19,21 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="rect">The rectangle to draw.</param>
-        /// <param name="radiusY">The radius in the Y dimension of the rounded corners.</param>
-        /// <param name="radiusX">The radius in the X dimension of the rounded corners.</param>
+        /// <param name="boxShadow">The box shadow parameters</param>
         /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
         public RectangleNode(
             Matrix transform,
             IBrush brush,
             IPen pen,
-            Rect rect,
-            double radiusX,
-            double radiusY,
+            RoundedRect rect,
             BoxShadow boxShadow,
             IDictionary<IVisual, Scene> childScenes = null)
-            : base(boxShadow.TransformBounds(rect), transform, pen)
+            : base(boxShadow.TransformBounds(rect.Rect), transform, pen)
         {
             Transform = transform;
             Brush = brush?.ToImmutable();
             Pen = pen?.ToImmutable();
             Rect = rect;
-            RadiusX = radiusX;
-            RadiusY = radiusY;
             ChildScenes = childScenes;
             BoxShadow = boxShadow;
         }
@@ -61,17 +56,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <summary>
         /// Gets the rectangle to draw.
         /// </summary>
-        public Rect Rect { get; }
-
-        /// <summary>
-        /// The radius in the X dimension of the rounded corners.
-        /// </summary>
-        public double RadiusX { get; }
-
-        /// <summary>
-        /// The radius in the Y dimension of the rounded corners.
-        /// </summary>
-        public double RadiusY { get; }
+        public RoundedRect Rect { get; }
         
         /// <summary>
         /// The parameters for the box-shadow effect
@@ -88,22 +73,19 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="brush">The fill of the other draw operation.</param>
         /// <param name="pen">The stroke of the other draw operation.</param>
         /// <param name="rect">The rectangle of the other draw operation.</param>
-        /// <param name="radiusX"></param>
-        /// <param name="radiusY"></param>
+        /// <param name="boxShadow">The box shadow parameters of the other draw operation</param>
         /// <returns>True if the draw operations are the same, otherwise false.</returns>
         /// <remarks>
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
-        public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY, BoxShadow boxShadow)
+        public bool Equals(Matrix transform, IBrush brush, IPen pen, RoundedRect rect, BoxShadow boxShadow)
         {
             return transform == Transform &&
                    Equals(brush, Brush) &&
                    Equals(Pen, pen) &&
                    Media.BoxShadow.Equals(BoxShadow, boxShadow) &&
-                   rect == Rect &&
-                   Math.Abs(radiusX - RadiusX) < double.Epsilon &&
-                   Math.Abs(radiusY - RadiusY) < double.Epsilon;
+                   rect.Equals(Rect);
         }
 
         /// <inheritdoc/>
@@ -111,7 +93,7 @@ namespace Avalonia.Rendering.SceneGraph
         {
             context.Transform = Transform;
 
-            context.DrawRectangle(Brush, Pen, Rect, RadiusX, RadiusY, BoxShadow);
+            context.DrawRectangle(Brush, Pen, Rect, BoxShadow);
         }
 
         /// <inheritdoc/>
@@ -124,13 +106,13 @@ namespace Avalonia.Rendering.SceneGraph
 
                 if (Brush != null)
                 {
-                    var rect = Rect.Inflate((Pen?.Thickness / 2) ?? 0);
+                    var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
                     return rect.Contains(p);
                 }
                 else
                 {
-                    var borderRect = Rect.Inflate((Pen?.Thickness / 2) ?? 0);
-                    var emptyRect = Rect.Deflate((Pen?.Thickness / 2) ?? 0);
+                    var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
+                    var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
                     return borderRect.Contains(p) && !emptyRect.Contains(p);
                 }
             }

+ 136 - 0
src/Avalonia.Visuals/RoundedRect.cs

@@ -0,0 +1,136 @@
+using System;
+
+namespace Avalonia
+{
+    public struct RoundedRect
+    {
+        public bool Equals(RoundedRect other)
+        {
+            return Rect.Equals(other.Rect) && RadiiTopLeft.Equals(other.RadiiTopLeft) && RadiiTopRight.Equals(other.RadiiTopRight) && RadiiBottomLeft.Equals(other.RadiiBottomLeft) && RadiiBottomRight.Equals(other.RadiiBottomRight);
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is RoundedRect other && Equals(other);
+        }
+
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                var hashCode = Rect.GetHashCode();
+                hashCode = (hashCode * 397) ^ RadiiTopLeft.GetHashCode();
+                hashCode = (hashCode * 397) ^ RadiiTopRight.GetHashCode();
+                hashCode = (hashCode * 397) ^ RadiiBottomLeft.GetHashCode();
+                hashCode = (hashCode * 397) ^ RadiiBottomRight.GetHashCode();
+                return hashCode;
+            }
+        }
+
+        public Rect Rect { get; }
+        public Vector RadiiTopLeft { get; }
+        public Vector RadiiTopRight { get; }
+        public Vector RadiiBottomLeft { get; }
+        public Vector RadiiBottomRight { get; }
+        
+        public RoundedRect(Rect rect, Vector radiiTopLeft, Vector radiiTopRight, Vector radiiBottomRight, Vector radiiBottomLeft)
+        {
+            Rect = rect;
+            RadiiTopLeft = radiiTopLeft;
+            RadiiTopRight = radiiTopRight;
+            RadiiBottomRight = radiiBottomRight;
+            RadiiBottomLeft = radiiBottomLeft;
+        }
+
+        public RoundedRect(Rect rect, double radiusTopLeft, double radiusTopRight, double radiusBottomRight,
+            double radiusBottomLeft)
+            : this(rect,
+                new Vector(radiusTopLeft, radiusTopLeft),
+                new Vector(radiusTopRight, radiusTopRight),
+                new Vector(radiusBottomRight, radiusBottomRight),
+                new Vector(radiusBottomLeft, radiusBottomLeft)
+            )
+        {
+            
+        }
+
+        public RoundedRect(Rect rect, Vector radii) : this(rect, radii, radii, radii, radii) 
+        {
+            
+        }
+
+        public RoundedRect(Rect rect, double radiusX, double radiusY) : this(rect, new Vector(radiusX, radiusY))
+        {
+            
+        }
+
+        public RoundedRect(Rect rect, double radius) : this(rect, radius, radius)
+        {
+            
+        }
+
+        public RoundedRect(Rect rect) : this(rect, 0)
+        {
+            
+        }
+
+        public static implicit operator RoundedRect(Rect r) => new RoundedRect(r);
+
+        public bool IsRounded => RadiiTopLeft != default || RadiiTopRight != default || RadiiBottomRight != default ||
+                                 RadiiBottomLeft != default;
+
+        public bool IsUniform =>
+            RadiiTopLeft.Equals(RadiiTopRight) &&
+            RadiiTopLeft.Equals(RadiiBottomRight) &&
+            RadiiTopLeft.Equals(RadiiBottomLeft);
+
+        public RoundedRect Inflate(double dx, double dy)
+        {
+            return Deflate(-dx, -dy);
+        }
+        
+        public unsafe RoundedRect Deflate(double dx, double dy)
+        {
+            if (!IsRounded)
+                return new RoundedRect(Rect.Deflate(new Thickness(dx, dy)));
+            
+            // Ported from SKRRect
+            var left = Rect.X + dx;
+            var top = Rect.Y + dy;
+            var right = left + Rect.Width - dx * 2;
+            var bottom = top + Rect.Height - dy * 2;
+            var radii = stackalloc Vector[4];
+            radii[0] = RadiiTopLeft;
+            radii[1] = RadiiTopRight;
+            radii[2] = RadiiBottomRight;
+            radii[3] = RadiiBottomLeft;
+            
+            bool degenerate = false;
+            if (right <= left) {
+                degenerate = true;
+                left = right = (left + right)*0.5;
+            }
+            if (bottom <= top) {
+                degenerate = true;
+                top = bottom = (top + bottom) * 0.5;
+            }
+            if (degenerate)
+            {
+                return new RoundedRect(new Rect(left, top, right - left, bottom - top));
+            }
+
+            for (var c = 0; c < 4; c++)
+            {
+                var rx = Math.Max(0, radii[c].X - dx);
+                var ry = Math.Max(0, radii[c].Y - dy);
+                if (rx == 0 || ry == 0)
+                    radii[c] = default;
+                else
+                    radii[c] = new Vector(rx, ry);
+            }
+
+            return new RoundedRect(new Rect(left, top, right - left, bottom - top),
+                radii[0], radii[1], radii[2], radii[3]);
+        }
+    }
+}

+ 89 - 33
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -164,21 +164,11 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow)
+        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
         {
             var impl = (GeometryImpl) geometry;
             var size = geometry.Bounds.Size;
 
-            if(!boxShadow.IsEmpty)
-                using (var shadow = BoxShadowFilter.Create(boxShadow, _currentOpacity, false))
-                {
-                    Canvas.Save();
-                    Canvas.ClipPath(impl.EffectivePath, SKClipOperation.Difference);
-                    Canvas.DrawPath(impl.EffectivePath, shadow.Paint);
-                    Canvas.Restore();
-
-                }
-            
             using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
             using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
             {
@@ -198,55 +188,99 @@ namespace Avalonia.Skia
         {
             public SKPaint Paint;
             private SKImageFilter _filter;
-            private SKImageFilter _dilate;
+            public SKClipOperation ClipOperation;
 
             public static BoxShadowFilter Create(BoxShadow shadow, double opacity, bool skipDilate)
             {
                 var ac = shadow.Color;
                 var spread = (int)shadow.Spread;
-                var dilate = !skipDilate && spread != 0 ? SKImageFilter.CreateDilate(spread, spread) : null;
+                if (shadow.IsInset)
+                    spread = -spread;
+                
                 var filter = SKImageFilter.CreateDropShadow(
                     (float)shadow.OffsetX,
                     (float)shadow.OffsetY,
                     (float)shadow.Blur / 2,
                     (float)shadow.Blur / 2,
                     new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity)),
-                    SKDropShadowImageFilterShadowMode.DrawShadowOnly, dilate);
+                    SKDropShadowImageFilterShadowMode.DrawShadowOnly, null);
+                
                 var paint = new SKPaint { Color = SKColors.White, ImageFilter = filter };
-                return new BoxShadowFilter { Paint = paint, _filter = filter, _dilate = dilate };
+                
+                return new BoxShadowFilter
+                {
+                    Paint = paint, _filter = filter,
+                    ClipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference
+                };
             }
 
             public void Dispose()
             {
                 Paint.Dispose();
                 _filter.Dispose();
-                _dilate?.Dispose();
             }
         }
-        
-        /// <inheritdoc />
-        public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0D, double radiusY = 0D,
-            BoxShadow boxShadow = default)
+
+        SKRect AreaCastingShadowInHole(
+            SKRect hole_rect,
+            float shadow_blur,
+            float shadow_spread,
+            float offsetX, float offsetY)
         {
-            var rc = rect.ToSKRect();
-            var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon;
+            // Adapted from Chromium
+            var bounds = hole_rect;
+
+            bounds.Inflate(shadow_blur, shadow_blur);
+
+            if (shadow_spread < 0)
+                bounds.Inflate(-shadow_spread, -shadow_spread);
+
+            var offset_bounds = bounds;
+            offset_bounds.Offset(-offsetX, -offsetY);
+            bounds.Union(offset_bounds);
+            return bounds;
+        }
+
 
-            if (!boxShadow.IsEmpty)
+        /// <inheritdoc />
+        public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadow boxShadow = default)
+        {
+            var rc = rect.Rect.ToSKRect();
+            var isRounded = rect.IsRounded;
+            var needRoundRect = rect.IsRounded || (!boxShadow.IsEmpty && boxShadow.IsInset);
+            using var skRoundRect = needRoundRect ? new SKRoundRect() : null;
+            if (needRoundRect)
+                skRoundRect.SetRectRadii(rc,
+                    new[]
+                    {
+                        rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(),
+                        rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(),
+                    });
+
+            if (!boxShadow.IsEmpty && !boxShadow.IsInset)
             {
                 using(var shadow = BoxShadowFilter.Create(boxShadow, _currentOpacity, true))
                 {
-                    var shadowRect = rc;
-                    shadowRect.Inflate((float)boxShadow.Spread, (float)boxShadow.Spread);
+                    var spread = (float)boxShadow.Spread;
+                    if (boxShadow.IsInset)
+                        spread = -spread;
+                    
                     Canvas.Save();
                     if (isRounded)
                     {
-                        Canvas.ClipRoundRect(new SKRoundRect(rc, (float)radiusX, (float)radiusY),
-                            SKClipOperation.Difference, true);
-                        Canvas.DrawRoundRect(shadowRect, (float)radiusX, (float)radiusY, shadow.Paint);
+                        using var shadowRect = new SKRoundRect(skRoundRect);
+                        if (spread != 0)
+                            shadowRect.Inflate(spread, spread);
+                        Canvas.ClipRoundRect(skRoundRect,
+                            shadow.ClipOperation, true);
+                        Canvas.DrawRoundRect(shadowRect, shadow.Paint);
                     }
                     else
                     {
-                        Canvas.ClipRect(shadowRect, SKClipOperation.Difference);
+                        var shadowRect = rc;
+                        if (spread != 0)
+                            shadowRect.Inflate(spread, spread);
+                        Canvas.ClipRect(shadowRect, shadow.ClipOperation);
                         Canvas.DrawRect(shadowRect, shadow.Paint);
                     }
                     Canvas.Restore();
@@ -255,11 +289,11 @@ namespace Avalonia.Skia
             
             if (brush != null)
             {
-                using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
+                using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size))
                 {
                     if (isRounded)
                     {
-                        Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint);
+                        Canvas.DrawRoundRect(skRoundRect, paint.Paint);
                     }
                     else
                     {
@@ -269,13 +303,35 @@ namespace Avalonia.Skia
                 }
             }
 
+            if (!boxShadow.IsEmpty && boxShadow.IsInset)
+            {
+                using(var shadow = BoxShadowFilter.Create(boxShadow, _currentOpacity, true))
+                {
+                    var spread = (float)boxShadow.Spread;
+                    var offsetX = (float)boxShadow.OffsetX;
+                    var offsetY = (float)boxShadow.OffsetY;
+                    var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY);
+                    
+                    Canvas.Save();
+                    using var shadowRect = new SKRoundRect(skRoundRect);
+                    if (spread != 0)
+                        shadowRect.Deflate(spread, spread);
+                    Canvas.ClipRoundRect(skRoundRect,
+                        shadow.ClipOperation, true);
+                    using (var outerRRect = new SKRoundRect(outerRect))
+                        Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint);
+                
+                    Canvas.Restore();
+                }
+            }
+            
             if (pen?.Brush != null)
             {
-                using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
+                using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size))
                 {
                     if (isRounded)
                     {
-                        Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint);
+                        Canvas.DrawRoundRect(skRoundRect, paint.Paint);
                     }
                     else
                     {

+ 2 - 0
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -247,5 +247,7 @@ namespace Avalonia.Skia
             return new GlyphRunImpl(textBlob);
 
         }
+
+        public bool SupportsIndividualRoundRects => true;
     }
 }

+ 5 - 0
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@@ -11,6 +11,11 @@ namespace Avalonia.Skia
         {
             return new SKPoint((float)p.X, (float)p.Y);
         }
+        
+        public static SKPoint ToSKPoint(this Vector p)
+        {
+            return new SKPoint((float)p.X, (float)p.Y);
+        }
 
         public static SKRect ToSKRect(this Rect r)
         {

+ 2 - 0
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -240,5 +240,7 @@ namespace Avalonia.Direct2D1
 
             return new GlyphRunImpl(run);
         }
+
+        public bool SupportsIndividualRoundRects => false;
     }
 }

+ 8 - 5
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -199,8 +199,7 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
-        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry,
-            BoxShadow boxShadow)
+        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
         {
             if (brush != null)
             {
@@ -229,10 +228,14 @@ namespace Avalonia.Direct2D1.Media
         }
 
         /// <inheritdoc />
-        public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0D, double radiusY = 0D,
-            BoxShadow boxShadow = default)
+        public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rrect, BoxShadow boxShadow = default)
         {
-            var rc = rect.ToDirect2D();
+            var rc = rrect.Rect.ToDirect2D();
+            var rect = rrect.Rect;
+            var radiusX = Math.Max(rrect.RadiiTopLeft.X,
+                Math.Max(rrect.RadiiTopRight.X, Math.Max(rrect.RadiiBottomRight.X, rrect.RadiiBottomLeft.X)));
+            var radiusY = Math.Max(rrect.RadiiTopLeft.Y,
+                Math.Max(rrect.RadiiTopRight.Y, Math.Max(rrect.RadiiBottomRight.Y, rrect.RadiiBottomLeft.Y)));
             var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon;
 
             if (brush != null)

+ 2 - 0
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@@ -76,5 +76,7 @@ namespace Avalonia.Benchmarks
 
             return new NullGlyphRun();
         }
+
+        public bool SupportsIndividualRoundRects => true;
     }
 }

+ 2 - 0
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -84,5 +84,7 @@ namespace Avalonia.UnitTests
             width = 0;
             return Mock.Of<IGlyphRunImpl>();
         }
+
+        public bool SupportsIndividualRoundRects { get; set; }
     }
 }

+ 45 - 0
tests/Avalonia.Visuals.UnitTests/Media/BoxShadowTests.cs

@@ -0,0 +1,45 @@
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+    public class BoxShadowTests
+    {
+        [Fact]
+        public void BoxShadow_Should_Parse()
+        {
+            foreach (var extraSpaces in new[] { false, true })
+            foreach (var inset in new[] { false, true })
+                for (var componentCount = 2; componentCount < 5; componentCount++)
+                {
+                    var s = (inset ? "inset " : "") + "10 20";
+                    double blur = 0;
+                    double spread = 0;
+                    if (componentCount > 2)
+                    {
+                        s += " 30";
+                        blur = 30;
+                    }
+
+                    if (componentCount > 3)
+                    {
+                        s += " 40";
+                        spread = 40;
+                    }
+
+                    s += " red";
+
+                    if (extraSpaces)
+                        s = " " + s.Replace(" ", "  ") + "   ";
+
+                    var parsed = BoxShadow.Parse(s);
+                    Assert.Equal(inset, parsed.IsInset);
+                    Assert.Equal(10, parsed.OffsetX);
+                    Assert.Equal(20, parsed.OffsetY);
+                    Assert.Equal(blur, parsed.Blur);
+                    Assert.Equal(spread, parsed.Spread);
+                    Assert.Equal(Colors.Red, parsed.Color);
+                }
+        }
+    }
+}

+ 4 - 4
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@@ -473,7 +473,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
                 var animation = new BehaviorSubject<double>(0.5);
 
                 context.Verify(x => x.PushOpacity(0.5), Times.Once);
-                context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default), Times.Once);
+                context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), default), Times.Once);
                 context.Verify(x => x.PopOpacity(), Times.Once);
             }
         }
@@ -503,7 +503,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
                 var animation = new BehaviorSubject<double>(0.5);
 
                 context.Verify(x => x.PushOpacity(0.5), Times.Never);
-                context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default), Times.Never);
+                context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), default), Times.Never);
                 context.Verify(x => x.PopOpacity(), Times.Never);
             }
         }
@@ -528,7 +528,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
                 var animation = new BehaviorSubject<double>(0.5);
 
                 context.Verify(x => x.PushOpacityMask(Brushes.Green, new Rect(0, 0, 100, 100)), Times.Once);
-                context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default), Times.Once);
+                context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), default), Times.Once);
                 context.Verify(x => x.PopOpacityMask(), Times.Once);
             }
         }
@@ -653,7 +653,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
                 var context = GetLayerContext(target, border);
 
                 context.Verify(x => x.PushOpacity(0.5), Times.Never);
-                context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default), Times.Once);
+                context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), default), Times.Once);
                 context.Verify(x => x.PopOpacity(), Times.Never);
             }
         }

+ 4 - 4
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs

@@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
         public void Should_Not_Replace_Identical_DrawOperation()
         {
             var node = new VisualNode(new TestRoot(), null);
-            var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default));
+            var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), default));
             var layers = new SceneLayers(node.Visual);
             var target = new DeferredDrawingContextImpl(null, layers);
 
@@ -133,7 +133,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
         public void Should_Replace_Different_DrawOperation()
         {
             var node = new VisualNode(new TestRoot(), null);
-            var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default));
+            var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), default));
             var layers = new SceneLayers(node.Visual);
             var target = new DeferredDrawingContextImpl(null, layers);
 
@@ -155,7 +155,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
         public void Should_Update_DirtyRects()
         {
             var node = new VisualNode(new TestRoot(), null);
-            var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default);
+            var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), default);
             var layers = new SceneLayers(node.Visual);
             var target = new DeferredDrawingContextImpl(null, layers);
 
@@ -206,7 +206,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
         public void Trimmed_DrawOperations_Releases_Reference()
         {
             var node = new VisualNode(new TestRoot(), null);
-            var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default));
+            var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), default));
             var layers = new SceneLayers(node.Visual);
             var target = new DeferredDrawingContextImpl(null, layers);
 

+ 2 - 0
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@@ -56,6 +56,8 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
             throw new NotImplementedException();
         }
 
+        public bool SupportsIndividualRoundRects { get; set; }
+
         public IFontManagerImpl CreateFontManager()
         {
             return new MockFontManagerImpl();