Ver Fonte

Merge pull request #3871 from AvaloniaUI/box-shadow

box-shadow support
danwalmsley há 5 anos atrás
pai
commit
e22c800c8b
29 ficheiros alterados com 889 adições e 80 exclusões
  1. 28 0
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 20 5
      src/Avalonia.Controls/Border.cs
  3. 17 4
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  4. 37 9
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  5. 23 0
      src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs
  6. 40 0
      src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs
  7. 133 0
      src/Avalonia.Visuals/Media/BoxShadow.cs
  8. 137 0
      src/Avalonia.Visuals/Media/BoxShadows.cs
  9. 44 0
      src/Avalonia.Visuals/Media/Color.cs
  10. 4 2
      src/Avalonia.Visuals/Media/DrawingContext.cs
  11. 3 7
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  12. 2 0
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  13. 10 0
      src/Avalonia.Visuals/Rect.cs
  14. 4 3
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  15. 5 5
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  16. 17 27
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  17. 136 0
      src/Avalonia.Visuals/RoundedRect.cs
  18. 153 7
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  19. 2 0
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  20. 5 0
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  21. 2 0
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  22. 7 2
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  23. 2 0
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  24. 2 0
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  25. 45 0
      tests/Avalonia.Visuals.UnitTests/Media/BoxShadowTests.cs
  26. 4 4
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  27. 4 4
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
  28. 1 1
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
  29. 2 0
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

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

@@ -134,6 +134,32 @@
           </Animation>
         </Style.Animations>
       </Style>
+      <Style Selector="Border.Shadow">
+        <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, -15 -15 Green"/>
+            </KeyFrame>
+            <KeyFrame Cue="35%">
+              <Setter Property="BoxShadow" Value="inset 0 0 20 2 Blue, -15 20 0 0 Blue"/>
+            </KeyFrame>
+            <KeyFrame Cue="70%">
+              <Setter Property="BoxShadow" Value="inset 0 0 20 30 Green, 20 20 20 0 Red"/>
+            </KeyFrame>
+            <KeyFrame Cue="85%">
+              <Setter Property="BoxShadow" Value="inset 30 0 20 30 Green, 20 20 20 10 Red"/>
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/>
+            </KeyFrame>
+            
+          </Animation>
+        </Style.Animations>
+      </Style>
     </Styles>
   </UserControl.Styles>
   <Grid>
@@ -152,6 +178,8 @@
         <Border Classes="Test Rect4" Background="Navy"/>
         <Border Classes="Test Rect5" Background="SeaGreen"/>
         <Border Classes="Test Rect6" Background="Red"/>
+        <Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
+        <Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
       </WrapPanel>
     </StackPanel>
   </Grid>

+ 20 - 5
src/Avalonia.Controls/Border.cs

@@ -33,6 +33,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
             AvaloniaProperty.Register<Border, CornerRadius>(nameof(CornerRadius));
 
+        /// <summary>
+        /// Defines the <see cref="BoxShadow"/> property.
+        /// </summary>
+        public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
+            AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow));
+        
         private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
 
         /// <summary>
@@ -44,7 +50,8 @@ namespace Avalonia.Controls
                 BackgroundProperty,
                 BorderBrushProperty,
                 BorderThicknessProperty,
-                CornerRadiusProperty);
+                CornerRadiusProperty,
+                BoxShadowProperty);
             AffectsMeasure<Border>(BorderThicknessProperty);
         }
 
@@ -83,14 +90,24 @@ namespace Avalonia.Controls
             get { return GetValue(CornerRadiusProperty); }
             set { SetValue(CornerRadiusProperty, value); }
         }
-
+        
+        /// <summary>
+        /// Gets or sets the box shadow effect parameters
+        /// </summary>
+        public BoxShadows BoxShadow
+        {
+            get => GetValue(BoxShadowProperty);
+            set => SetValue(BoxShadowProperty, value);
+        }
+        
         /// <summary>
         /// Renders the control.
         /// </summary>
         /// <param name="context">The drawing context.</param>
         public override void Render(DrawingContext context)
         {
-            _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
+            _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
+                BoxShadow);
         }
 
         /// <summary>
@@ -110,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);
         }
     }

+ 17 - 4
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -40,7 +40,12 @@ namespace Avalonia.Controls.Presenters
         public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
             Border.CornerRadiusProperty.AddOwner<ContentPresenter>();
 
-
+        /// <summary>
+        /// Defines the <see cref="BoxShadow"/> property.
+        /// </summary>
+        public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
+            Border.BoxShadowProperty.AddOwner<ContentPresenter>();
+        
         /// <summary>
         /// Defines the <see cref="Child"/> property.
         /// </summary>
@@ -132,6 +137,15 @@ namespace Avalonia.Controls.Presenters
             set { SetValue(CornerRadiusProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the box shadow effect parameters
+        /// </summary>
+        public BoxShadows BoxShadow
+        {
+            get => GetValue(BoxShadowProperty);
+            set => SetValue(BoxShadowProperty, value);
+        }
+
         /// <summary>
         /// Gets the control displayed by the presenter.
         /// </summary>
@@ -274,7 +288,8 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         public override void Render(DrawingContext context)
         {
-            _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
+            _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
+                BoxShadow);
         }
 
         /// <summary>
@@ -321,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 - 9
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@@ -1,17 +1,30 @@
 using System;
 using Avalonia.Media;
+using Avalonia.Platform;
 
 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;
@@ -67,7 +80,19 @@ namespace Avalonia.Controls.Utils
             }
         }
 
-        public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
+        public void Render(DrawingContext context,
+            Size finalSize, Thickness borderThickness, CornerRadius cornerRadius,
+            IBrush background, IBrush borderBrush, BoxShadows boxShadows)
+        {
+            if (_size != finalSize
+                || _borderThickness != borderThickness
+                || _cornerRadius != cornerRadius
+                || !_initialized)
+                Update(finalSize, borderThickness, cornerRadius);
+            RenderCore(context, background, borderBrush, boxShadows);
+        }
+
+        void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows)
         {
             if (_useComplexRendering)
             {
@@ -85,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)
@@ -95,11 +118,16 @@ 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);
+                context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows);
             }
-        }    
+        }
 
         private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderGeometryKeypoints keypoints)
         {

+ 23 - 0
src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs

@@ -0,0 +1,23 @@
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+    public class BoxShadowAnimator : Animator<BoxShadow>
+    {
+        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
+            {
+                OffsetX = s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX),
+                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),
+                IsInset = s_boolAnimator.Interpolate(progress, oldValue.IsInset, newValue.IsInset)
+            };
+        }
+    }
+}

+ 40 - 0
src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs

@@ -0,0 +1,40 @@
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+    public class BoxShadowsAnimator :  Animator<BoxShadows>
+    {
+        private static readonly BoxShadowAnimator s_boxShadowAnimator = new BoxShadowAnimator();
+        public override BoxShadows Interpolate(double progress, BoxShadows oldValue, BoxShadows newValue)
+        {
+            int cnt = progress >= 1d ? newValue.Count : oldValue.Count;
+            if (cnt == 0)
+                return new BoxShadows();
+
+            BoxShadow first;
+            if (oldValue.Count > 0 && newValue.Count > 0)
+                first = s_boxShadowAnimator.Interpolate(progress, oldValue[0], newValue[0]);
+            else if (oldValue.Count > 0)
+                first = oldValue[0];
+            else
+                first = newValue[0];
+
+            if (cnt == 1)
+                return new BoxShadows(first);
+
+            var rest = new BoxShadow[cnt - 1];
+            for (var c = 0; c < rest.Length; c++)
+            {
+                var idx = c + 1;
+                if (oldValue.Count > idx && newValue.Count > idx)
+                    rest[c] = s_boxShadowAnimator.Interpolate(progress, oldValue[idx], newValue[idx]);
+                else if (oldValue.Count > idx)
+                    rest[c] = oldValue[idx];
+                else
+                    rest[c] = newValue[idx];
+            }
+
+            return new BoxShadows(first, rest);
+        }
+    }
+}

+ 133 - 0
src/Avalonia.Visuals/Media/BoxShadow.cs

@@ -0,0 +1,133 @@
+using System;
+using System.Globalization;
+using Avalonia.Animation.Animators;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media
+{
+    public struct BoxShadow
+    {
+        public double OffsetX { get; set; }
+        public double OffsetY { get; set; }
+        public double Blur { get; set; }
+        public double Spread { get; set; }
+        public Color Color { get; set; }
+        public bool IsInset { get; set; }
+
+        static BoxShadow()
+        {
+            Animation.Animation.RegisterAnimator<BoxShadowAnimator>(prop =>
+                typeof(BoxShadow).IsAssignableFrom(prop.PropertyType));
+        }
+        
+        public bool Equals(in BoxShadow other)
+        {
+            return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color);
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is BoxShadow other && Equals(other);
+        }
+
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                var hashCode = OffsetX.GetHashCode();
+                hashCode = (hashCode * 397) ^ OffsetY.GetHashCode();
+                hashCode = (hashCode * 397) ^ Blur.GetHashCode();
+                hashCode = (hashCode * 397) ^ Spread.GetHashCode();
+                hashCode = (hashCode * 397) ^ Color.GetHashCode();
+                return hashCode;
+            }
+        }
+
+        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();
+
+            var p = s.Split(s_Separator, StringSplitOptions.RemoveEmptyEntries);
+            if (p.Length == 1 && p[0] == "none")
+                return default;
+            
+            if (p.Length < 3 || p.Length > 6)
+                throw new FormatException();
+            
+            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;
+            
+
+            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,
+                Spread = spread,
+                Color = color
+            };
+        }
+
+        public Rect TransformBounds(in Rect rect)
+            => IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur);
+    }
+}

+ 137 - 0
src/Avalonia.Visuals/Media/BoxShadows.cs

@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using Avalonia.Animation.Animators;
+
+namespace Avalonia.Media
+{
+    public struct BoxShadows
+    {
+        private readonly BoxShadow _first;
+        private readonly BoxShadow[] _list;
+        public int Count { get; }
+
+        static BoxShadows()
+        {
+            Animation.Animation.RegisterAnimator<BoxShadowsAnimator>(prop =>
+                typeof(BoxShadows).IsAssignableFrom(prop.PropertyType));
+        }
+        
+        public BoxShadows(BoxShadow shadow)
+        {
+            _first = shadow;
+            _list = null;
+            Count = 1;
+        }
+
+        public BoxShadows(BoxShadow first, BoxShadow[] rest)
+        {
+            _first = first;
+            _list = rest;
+            Count = 1 + (rest?.Length ?? 0);
+        }
+
+        public BoxShadow this[int c]
+        {
+            get
+            {
+                if (c< 0 || c >= Count)
+                    throw new IndexOutOfRangeException();
+                if (c == 0)
+                    return _first;
+                return _list[c - 1];
+            }
+        }
+
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public struct BoxShadowsEnumerator
+        {
+            private int _index;
+            private BoxShadows _shadows;
+
+            public BoxShadowsEnumerator(BoxShadows shadows)
+            {
+                _shadows = shadows;
+                _index = -1;
+            }
+
+            public BoxShadow Current => _shadows[_index];
+
+            public bool MoveNext()
+            {
+                _index++;
+                return _index < _shadows.Count;
+            }
+        }
+
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public BoxShadowsEnumerator GetEnumerator() => new BoxShadowsEnumerator(this);
+
+        private static readonly char[] s_Separators = new[] { ',' };
+        public static BoxShadows Parse(string s)
+        {
+            var sp = s.Split(s_Separators, StringSplitOptions.RemoveEmptyEntries);
+            if (sp.Length == 0
+                || (sp.Length == 1 &&
+                    (string.IsNullOrWhiteSpace(sp[0])
+                     || sp[0] == "none")))
+                return new BoxShadows();
+
+            var first = BoxShadow.Parse(sp[0]);
+            if (sp.Length == 1)
+                return new BoxShadows(first);
+
+            var rest = new BoxShadow[sp.Length - 1];
+            for (var c = 0; c < rest.Length; c++)
+                rest[c] = BoxShadow.Parse(sp[c + 1]);
+            return new BoxShadows(first, rest);
+        }
+
+        public Rect TransformBounds(in Rect rect)
+        {
+            var final = rect;
+            foreach (var shadow in this)
+                final = final.Union(shadow.TransformBounds(rect));
+            return final;
+        }
+        
+        public bool HasInsetShadows
+        {
+            get
+            {
+                foreach(var boxShadow in this)
+                    if (!boxShadow.IsEmpty && boxShadow.IsInset)
+                        return true;
+                return false;
+            }
+        }
+
+        public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow);
+        
+        public bool Equals(BoxShadows other)
+        {
+            if (other.Count != Count)
+                return false;
+            for(var c=0; c<Count ; c++)
+                if (!this[c].Equals(other[c]))
+                    return false;
+            return true;
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is BoxShadows other && Equals(other);
+        }
+
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                int hashCode = 0;
+                foreach (var s in this)
+                    hashCode = (hashCode * 397) ^ s.GetHashCode();
+                return hashCode;
+            }
+        }
+    }
+}

+ 44 - 0
src/Avalonia.Visuals/Media/Color.cs

@@ -118,6 +118,50 @@ namespace Avalonia.Media
             throw new FormatException($"Invalid color string: '{s}'.");
         }
 
+        /// <summary>
+        /// Parses a color string.
+        /// </summary>
+        /// <param name="s">The color string.</param>
+        /// <param name="color">The parsed color</param>
+        /// <returns>The status of the operation.</returns>
+        public static bool TryParse(ReadOnlySpan<char> s, out Color color)
+        {
+            color = default;
+            if (s == null)
+                return false;
+            if (s.Length == 0)
+                return false;
+
+            if (s[0] == '#')
+            {
+                var or = 0u;
+
+                if (s.Length == 7)
+                {
+                    or = 0xff000000;
+                }
+                else if (s.Length != 9)
+                {
+                    return false;
+                }
+
+                if(!uint.TryParse(s.Slice(1).ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
+                    return false;
+                color = FromUInt32(parsed| or);
+                return true;
+            }
+
+            var knownColor = KnownColors.GetKnownColor(s.ToString());
+
+            if (knownColor != KnownColor.None)
+            {
+                color = knownColor.ToColor();
+                return true;
+            }
+
+            return false;
+        }
+
         /// <summary>
         /// Returns the string representation of the color.
         /// </summary>

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

@@ -141,11 +141,13 @@ namespace Avalonia.Media
         /// <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>
-        public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0)
+        public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0,
+            BoxShadow boxShadow = default)
         {
             if (brush == null && !PenIsVisible(pen))
             {
@@ -162,7 +164,7 @@ namespace Avalonia.Media
                 radiusY = Math.Min(radiusY, rect.Height / 2);
             }
 
-            PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY);
+            PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadow);
         }
 
         /// <summary>

+ 3 - 7
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@@ -63,17 +63,13 @@ 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="boxShadows">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,
+            BoxShadows boxShadow = default);
 
         /// <summary>
         /// Draws text.

+ 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.

+ 4 - 3
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

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

+ 5 - 5
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@@ -19,8 +19,7 @@ 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>
-        public GeometryNode(
-            Matrix transform,
+        public GeometryNode(Matrix transform,
             IBrush brush,
             IPen pen,
             IGeometryImpl geometry,
@@ -64,6 +63,7 @@ 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="geometry">The geometry of the other draw operation.</param>
+        /// <param name="boxShadow">The box shadow parameters</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
@@ -72,9 +72,9 @@ namespace Avalonia.Rendering.SceneGraph
         public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry)
         {
             return transform == Transform &&
-                Equals(brush, Brush) && 
-                Equals(Pen, pen) &&
-                Equals(geometry, Geometry);
+                   Equals(brush, Brush) &&
+                   Equals(Pen, pen) &&
+                   Equals(geometry, Geometry);
         }
 
         /// <inheritdoc/>

+ 17 - 27
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@@ -19,26 +19,23 @@ 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,
+            BoxShadows boxShadows,
             IDictionary<IVisual, Scene> childScenes = null)
-            : base(rect, transform, pen)
+            : base(boxShadows.TransformBounds(rect.Rect), transform, pen)
         {
             Transform = transform;
             Brush = brush?.ToImmutable();
             Pen = pen?.ToImmutable();
             Rect = rect;
-            RadiusX = radiusX;
-            RadiusY = radiusY;
             ChildScenes = childScenes;
+            BoxShadows = boxShadows;
         }
 
         /// <summary>
@@ -59,17 +56,12 @@ 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; }
-
+        public RoundedRect Rect { get; }
+        
         /// <summary>
-        /// The radius in the Y dimension of the rounded corners.
+        /// The parameters for the box-shadow effect
         /// </summary>
-        public double RadiusY { get; }
+        public BoxShadows BoxShadows { get; }
 
         /// <inheritdoc/>
         public override IDictionary<IVisual, Scene> ChildScenes { get; }
@@ -81,21 +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)
+        public bool Equals(Matrix transform, IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows)
         {
             return transform == Transform &&
                    Equals(brush, Brush) &&
                    Equals(Pen, pen) &&
-                   rect == Rect &&
-                   Math.Abs(radiusX - RadiusX) < double.Epsilon &&
-                   Math.Abs(radiusY - RadiusY) < double.Epsilon;
+                   Media.BoxShadows.Equals(BoxShadows, boxShadows) &&
+                   rect.Equals(Rect);
         }
 
         /// <inheritdoc/>
@@ -103,7 +93,7 @@ namespace Avalonia.Rendering.SceneGraph
         {
             context.Transform = Transform;
 
-            context.DrawRectangle(Brush, Pen, Rect, RadiusX, RadiusY);
+            context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
         }
 
         /// <inheritdoc/>
@@ -116,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]);
+        }
+    }
+}

+ 153 - 7
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -33,6 +33,7 @@ namespace Avalonia.Skia
 
         private readonly SKPaint _strokePaint = new SKPaint();
         private readonly SKPaint _fillPaint = new SKPaint();
+        private readonly SKPaint _boxShadowPaint = new SKPaint();
 
         /// <summary>
         /// Context create info.
@@ -184,19 +185,136 @@ namespace Avalonia.Skia
             }
         }
 
+        struct BoxShadowFilter : IDisposable
+        {
+            public SKPaint Paint;
+            private SKImageFilter _filter;
+            public SKClipOperation ClipOperation;
+
+            static float SkBlurRadiusToSigma(double radius) {
+                if (radius <= 0)
+                    return 0.0f;
+                return 0.288675f * (float)radius + 0.5f;
+            }
+            public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity)
+            {
+                var ac = shadow.Color;
+
+                SKImageFilter filter = null;
+                filter = SKImageFilter.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur));
+                var color = new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity));
+
+                paint.Reset();
+                paint.IsAntialias = true;
+                paint.Color = color;
+                paint.ImageFilter = filter;
+                
+                return new BoxShadowFilter
+                {
+                    Paint = paint, _filter = filter,
+                    ClipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference
+                };
+            }
+
+            public void Dispose()
+            {
+                Paint.Reset();
+                Paint = null;
+                _filter?.Dispose();
+            }
+        }
+
+        SKRect AreaCastingShadowInHole(
+            SKRect hole_rect,
+            float shadow_blur,
+            float shadow_spread,
+            float offsetX, float offsetY)
+        {
+            // 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;
+        }
+
+
         /// <inheritdoc />
-        public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY)
+        public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default)
         {
-            var rc = rect.ToSKRect();
-            var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon;
+            if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0)
+                return;
+            // Arbitrary chosen values
+            // On OSX Skia breaks OpenGL context when asked to draw, e. g. (0, 0, 623, 6666600) rect
+            if (rect.Rect.Height > 8192 || rect.Rect.Width > 8192)
+                boxShadows = default;
+
+            var rc = rect.Rect.ToSKRect();
+            var isRounded = rect.IsRounded;
+            var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows);
+            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(),
+                    });
+
+            foreach (var boxShadow in boxShadows)
+            {
+                if (!boxShadow.IsEmpty && !boxShadow.IsInset)
+                {
+                    using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
+                    {
+                        var spread = (float)boxShadow.Spread;
+                        if (boxShadow.IsInset)
+                            spread = -spread;
+
+                        Canvas.Save();
+                        if (isRounded)
+                        {
+                            using var shadowRect = new SKRoundRect(skRoundRect);
+                            if (spread != 0)
+                                shadowRect.Inflate(spread, spread);
+                            Canvas.ClipRoundRect(skRoundRect,
+                                shadow.ClipOperation, true);
+                            
+                            var oldTransform = Transform;
+                            Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
+                            Canvas.DrawRoundRect(shadowRect, shadow.Paint);
+                            Transform = oldTransform;
+                        }
+                        else
+                        {
+                            var shadowRect = rc;
+                            if (spread != 0)
+                                shadowRect.Inflate(spread, spread);
+                            Canvas.ClipRect(rc, shadow.ClipOperation);
+                            var oldTransform = Transform;
+                            Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
+                            Canvas.DrawRect(shadowRect, shadow.Paint);
+                            Transform = oldTransform;
+                        }
+
+                        Canvas.Restore();
+                    }
+                }
+            }
 
             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
                     {
@@ -206,13 +324,41 @@ namespace Avalonia.Skia
                 }
             }
 
+            foreach (var boxShadow in boxShadows)
+            {
+                if (!boxShadow.IsEmpty && boxShadow.IsInset)
+                {
+                    using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
+                    {
+                        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);
+                        
+                        var oldTransform = Transform;
+                        Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
+                        using (var outerRRect = new SKRoundRect(outerRect))
+                            Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint);
+                        Transform = oldTransform;
+                        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

@@ -255,5 +255,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;
     }
 }

+ 7 - 2
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -228,9 +228,14 @@ namespace Avalonia.Direct2D1.Media
         }
 
         /// <inheritdoc />
-        public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY)
+        public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rrect, BoxShadows 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), 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), 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), 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), 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));
+            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));
+            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);
+            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));
+            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);
 

+ 1 - 1
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

@@ -69,7 +69,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 new Matrix(),
                 Brushes.Black,
                 null,
-                geometry);
+                geometry, default);
 
             geometryNode.HitTest(new Point());
         }

+ 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();