Ver código fonte

Merge branch 'master' into fixes/geometryimpl-nre

Steven Kirk 7 anos atrás
pai
commit
aae2e80cab
24 arquivos alterados com 600 adições e 135 exclusões
  1. 19 29
      src/Avalonia.Controls/Border.cs
  2. 69 78
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  3. 2 2
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  4. 279 0
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  5. 1 1
      src/Avalonia.Themes.Default/Accents/BaseLight.xaml
  6. 97 0
      src/Avalonia.Visuals/CornerRadius.cs
  7. 1 0
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  8. 6 1
      src/Avalonia.Visuals/Thickness.cs
  9. 1 0
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  10. 19 0
      src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs
  11. 1 0
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs
  12. 0 2
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  13. 1 1
      tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
  14. 1 1
      tests/Avalonia.Controls.UnitTests/BorderTests.cs
  15. 53 13
      tests/Avalonia.RenderTests/Controls/BorderTests.cs
  16. 1 1
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  17. 1 1
      tests/Avalonia.RenderTests/Shapes/PathTests.cs
  18. 3 3
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  19. 43 0
      tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs
  20. 2 2
      tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs
  21. BIN
      tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png
  22. BIN
      tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png
  23. BIN
      tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png
  24. BIN
      tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png

+ 19 - 29
src/Avalonia.Controls/Border.cs

@@ -1,6 +1,8 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia;
+using Avalonia.Controls.Utils;
 using Avalonia.Media;
 
 namespace Avalonia.Controls
@@ -8,7 +10,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control which decorates a child with a border and background.
     /// </summary>
-    public class Border : Decorator
+    public partial class Border : Decorator
     {
         /// <summary>
         /// Defines the <see cref="Background"/> property.
@@ -25,14 +27,16 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="BorderThickness"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> BorderThicknessProperty =
-            AvaloniaProperty.Register<Border, double>(nameof(BorderThickness));
+        public static readonly StyledProperty<Thickness> BorderThicknessProperty =
+            AvaloniaProperty.Register<Border, Thickness>(nameof(BorderThickness));
 
         /// <summary>
         /// Defines the <see cref="CornerRadius"/> property.
         /// </summary>
-        public static readonly StyledProperty<float> CornerRadiusProperty =
-            AvaloniaProperty.Register<Border, float>(nameof(CornerRadius));
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
+            AvaloniaProperty.Register<Border, CornerRadius>(nameof(CornerRadius));
+
+        private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
 
         /// <summary>
         /// Initializes static members of the <see cref="Border"/> class.
@@ -63,7 +67,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the thickness of the border.
         /// </summary>
-        public double BorderThickness
+        public Thickness BorderThickness
         {
             get { return GetValue(BorderThicknessProperty); }
             set { SetValue(BorderThicknessProperty, value); }
@@ -72,7 +76,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the radius of the border rounded corners.
         /// </summary>
-        public float CornerRadius
+        public CornerRadius CornerRadius
         {
             get { return GetValue(CornerRadiusProperty); }
             set { SetValue(CornerRadiusProperty, value); }
@@ -84,21 +88,7 @@ namespace Avalonia.Controls
         /// <param name="context">The drawing context.</param>
         public override void Render(DrawingContext context)
         {
-            var background = Background;
-            var borderBrush = BorderBrush;
-            var borderThickness = BorderThickness;
-            var cornerRadius = CornerRadius;
-            var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
-
-            if (background != null)
-            {
-                context.FillRectangle(background, rect, cornerRadius);
-            }
-
-            if (borderBrush != null && borderThickness > 0)
-            {
-                context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
-            }
+            _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
         }
 
         /// <summary>
@@ -120,10 +110,12 @@ namespace Avalonia.Controls
         {
             if (Child != null)
             {
-                var padding = Padding + new Thickness(BorderThickness);
+                var padding = Padding + BorderThickness;
                 Child.Arrange(new Rect(finalSize).Deflate(padding));
             }
 
+            _borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);           
+
             return finalSize;
         }
 
@@ -131,19 +123,17 @@ namespace Avalonia.Controls
             Size availableSize,
             IControl child,
             Thickness padding,
-            double borderThickness)
+            Thickness borderThickness)
         {
-            padding += new Thickness(borderThickness);
+            padding += borderThickness;
 
             if (child != null)
             {
                 child.Measure(availableSize.Deflate(padding));
                 return child.DesiredSize.Inflate(padding);
             }
-            else
-            {
-                return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
-            }
+
+            return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
         }
     }
 }

+ 69 - 78
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -4,6 +4,7 @@
 using System;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
@@ -31,7 +32,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Defines the <see cref="BorderThickness"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> BorderThicknessProperty =
+        public static readonly StyledProperty<Thickness> BorderThicknessProperty =
             Border.BorderThicknessProperty.AddOwner<ContentPresenter>();
 
         /// <summary>
@@ -57,7 +58,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Defines the <see cref="CornerRadius"/> property.
         /// </summary>
-        public static readonly StyledProperty<float> CornerRadiusProperty =
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
             Border.CornerRadiusProperty.AddOwner<ContentPresenter>();
 
         /// <summary>
@@ -76,11 +77,12 @@ namespace Avalonia.Controls.Presenters
         /// Defines the <see cref="Padding"/> property.
         /// </summary>
         public static readonly StyledProperty<Thickness> PaddingProperty =
-            Border.PaddingProperty.AddOwner<ContentPresenter>();
+            Decorator.PaddingProperty.AddOwner<ContentPresenter>();
 
         private IControl _child;
         private bool _createdChild;
         private IDataTemplate _dataTemplate;
+        private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
 
         /// <summary>
         /// Initializes static members of the <see cref="ContentPresenter"/> class.
@@ -120,7 +122,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Gets or sets the thickness of the border.
         /// </summary>
-        public double BorderThickness
+        public Thickness BorderThickness
         {
             get { return GetValue(BorderThicknessProperty); }
             set { SetValue(BorderThicknessProperty, value); }
@@ -157,7 +159,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Gets or sets the radius of the border rounded corners.
         /// </summary>
-        public float CornerRadius
+        public CornerRadius CornerRadius
         {
             get { return GetValue(CornerRadiusProperty); }
             set { SetValue(CornerRadiusProperty, value); }
@@ -277,21 +279,7 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         public override void Render(DrawingContext context)
         {
-            var background = Background;
-            var borderBrush = BorderBrush;
-            var borderThickness = BorderThickness;
-            var cornerRadius = CornerRadius;
-            var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
-
-            if (background != null)
-            {
-                context.FillRectangle(background, rect, cornerRadius);
-            }
-
-            if (borderBrush != null && borderThickness > 0)
-            {
-                context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
-            }
+            _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
         }
 
         /// <summary>
@@ -344,7 +332,11 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         protected override Size ArrangeOverride(Size finalSize)
         {
-            return ArrangeOverrideImpl(finalSize, new Vector());
+            finalSize = ArrangeOverrideImpl(finalSize, new Vector());
+
+            _borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
+
+            return finalSize;
         }
 
         /// <summary>
@@ -372,70 +364,69 @@ namespace Avalonia.Controls.Presenters
 
         internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
         {
-            if (Child != null)
-            {
-                var padding = Padding;
-                var borderThickness = BorderThickness;
-                var horizontalContentAlignment = HorizontalContentAlignment;
-                var verticalContentAlignment = VerticalContentAlignment;
-                var useLayoutRounding = UseLayoutRounding;
-                var availableSizeMinusMargins = new Size(
-                    Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness),
-                    Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness));
-                var size = availableSizeMinusMargins;
-                var scale = GetLayoutScale();
-                var originX = offset.X + padding.Left + borderThickness;
-                var originY = offset.Y + padding.Top + borderThickness;
-
-                if (horizontalContentAlignment != HorizontalAlignment.Stretch)
-                {
-                    size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
-                }
+            if (Child == null) return finalSize;
 
-                if (verticalContentAlignment != VerticalAlignment.Stretch)
-                {
-                    size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
-                }
-
-                if (useLayoutRounding)
-                {
-                    size = new Size(
-                        Math.Ceiling(size.Width * scale) / scale,
-                        Math.Ceiling(size.Height * scale) / scale);
-                    availableSizeMinusMargins = new Size(
-                        Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
-                        Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
-                }
+            var padding = Padding;
+            var borderThickness = BorderThickness;
+            var horizontalContentAlignment = HorizontalContentAlignment;
+            var verticalContentAlignment = VerticalContentAlignment;
+            var useLayoutRounding = UseLayoutRounding;
+            var availableSizeMinusMargins = new Size(
+                Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness.Left - borderThickness.Right),
+                Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness.Top - borderThickness.Bottom));
+            var size = availableSizeMinusMargins;
+            var scale = GetLayoutScale();
+            var originX = offset.X + padding.Left + borderThickness.Left;
+            var originY = offset.Y + padding.Top + borderThickness.Top;
+
+            if (horizontalContentAlignment != HorizontalAlignment.Stretch)
+            {
+                size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
+            }
+            
+            if (verticalContentAlignment != VerticalAlignment.Stretch)
+            {
+                size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
+            }
 
-                switch (horizontalContentAlignment)
-                {
-                    case HorizontalAlignment.Center:
-                        originX += (availableSizeMinusMargins.Width - size.Width) / 2;
-                        break;
-                    case HorizontalAlignment.Right:
-                        originX += availableSizeMinusMargins.Width - size.Width;
-                        break;
-                }
+            if (useLayoutRounding)
+            {
+                size = new Size(
+                    Math.Ceiling(size.Width * scale) / scale,
+                    Math.Ceiling(size.Height * scale) / scale);
+                availableSizeMinusMargins = new Size(
+                    Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
+                    Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
+            }
 
-                switch (verticalContentAlignment)
-                {
-                    case VerticalAlignment.Center:
-                        originY += (availableSizeMinusMargins.Height - size.Height) / 2;
-                        break;
-                    case VerticalAlignment.Bottom:
-                        originY += availableSizeMinusMargins.Height - size.Height;
-                        break;
-                }
+            switch (horizontalContentAlignment)
+            {
+                case HorizontalAlignment.Center:
+                    originX += (availableSizeMinusMargins.Width - size.Width) / 2;
+                    break;
+                case HorizontalAlignment.Right:
+                    originX += availableSizeMinusMargins.Width - size.Width;
+                    break;
+            }
 
-                if (useLayoutRounding)
-                {
-                    originX = Math.Floor(originX * scale) / scale;
-                    originY = Math.Floor(originY * scale) / scale;
-                }
+            switch (verticalContentAlignment)
+            {
+                case VerticalAlignment.Center:
+                    originY += (availableSizeMinusMargins.Height - size.Height) / 2;
+                    break;
+                case VerticalAlignment.Bottom:
+                    originY += availableSizeMinusMargins.Height - size.Height;
+                    break;
+            }
 
-                Child.Arrange(new Rect(originX, originY, size.Width, size.Height));
+            if (useLayoutRounding)
+            {
+                originX = Math.Floor(originX * scale) / scale;
+                originY = Math.Floor(originY * scale) / scale;
             }
 
+            Child.Arrange(new Rect(originX, originY, size.Width, size.Height));
+
             return finalSize;
         }
 

+ 2 - 2
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="BorderThickness"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> BorderThicknessProperty =
+        public static readonly StyledProperty<Thickness> BorderThicknessProperty =
             Border.BorderThicknessProperty.AddOwner<TemplatedControl>();
 
         /// <summary>
@@ -132,7 +132,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets or sets the thickness of the control's border.
         /// </summary>
-        public double BorderThickness
+        public Thickness BorderThickness
         {
             get { return GetValue(BorderThicknessProperty); }
             set { SetValue(BorderThicknessProperty, value); }

+ 279 - 0
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@@ -0,0 +1,279 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Utils
+{
+    internal class BorderRenderHelper
+    {
+        private bool _useComplexRendering;
+        private StreamGeometry _backgroundGeometryCache;
+        private StreamGeometry _borderGeometryCache;
+
+        public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
+        {
+            if (borderThickness.IsUniform && cornerRadius.IsUniform)
+            {
+                _backgroundGeometryCache = null;
+                _borderGeometryCache = null;
+                _useComplexRendering = false;
+            }
+            else
+            {
+                _useComplexRendering = true;
+
+                var boundRect = new Rect(finalSize);
+                var innerRect = boundRect.Deflate(borderThickness);
+                var innerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, false);
+
+                StreamGeometry backgroundGeometry = null;
+
+                if (innerRect.Width != 0 && innerRect.Height != 0)
+                {
+                    backgroundGeometry = new StreamGeometry();
+
+                    using (var ctx = backgroundGeometry.Open())
+                    {
+                        CreateGeometry(ctx, innerRect, innerCoordinates);
+                    }
+
+                    _backgroundGeometryCache = backgroundGeometry;
+                }
+                else
+                {
+                    _backgroundGeometryCache = null;
+                }
+
+                if (boundRect.Width != 0 && innerRect.Height != 0)
+                {
+                    var outerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, true);
+                    var borderGeometry = new StreamGeometry();
+
+                    using (var ctx = borderGeometry.Open())
+                    {
+                        CreateGeometry(ctx, boundRect, outerCoordinates);
+
+                        if (backgroundGeometry != null)
+                        {
+                            CreateGeometry(ctx, innerRect, innerCoordinates);
+                        }
+                    }
+
+                    _borderGeometryCache = borderGeometry;
+                }
+                else
+                {
+                    _borderGeometryCache = null;
+                }
+            }
+        }
+
+        public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
+        {
+            if (_useComplexRendering)
+            {
+                var backgroundGeometry = _backgroundGeometryCache;
+                if (backgroundGeometry != null)
+                {
+                    context.DrawGeometry(background, null, backgroundGeometry);
+                }
+
+                var borderGeometry = _borderGeometryCache;
+                if (borderGeometry != null)
+                {
+                    context.DrawGeometry(borderBrush, null, borderGeometry);
+                }
+            }
+            else
+            {
+                var borderThickness = borders.Left;
+                var cornerRadius = (float)radii.TopLeft;
+                var rect = new Rect(size);
+
+                if (background != null)
+                {
+                    context.FillRectangle(background, rect.Deflate(borders), cornerRadius);
+                }
+
+                if (borderBrush != null && borderThickness > 0)
+                {
+                    context.DrawRectangle(new Pen(borderBrush, borderThickness), rect.Deflate(borderThickness), cornerRadius);
+                }
+            }
+        }
+
+        private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderCoordinates borderCoordinates)
+        {
+            var topLeft = new Point(borderCoordinates.LeftTop, 0);
+            var topRight = new Point(boundRect.Width - borderCoordinates.RightTop, 0);
+            var rightTop = new Point(boundRect.Width, borderCoordinates.TopRight);
+            var rightBottom = new Point(boundRect.Width, boundRect.Height - borderCoordinates.BottomRight);
+            var bottomRight = new Point(boundRect.Width - borderCoordinates.RightBottom, boundRect.Height);
+            var bottomLeft = new Point(borderCoordinates.LeftBottom, boundRect.Height);
+            var leftBottom = new Point(0, boundRect.Height - borderCoordinates.BottomLeft);
+            var leftTop = new Point(0, borderCoordinates.TopLeft);
+
+
+            if (topLeft.X > topRight.X)
+            {
+                var scaledX = borderCoordinates.LeftTop / (borderCoordinates.LeftTop + borderCoordinates.RightTop) * boundRect.Width;
+                topLeft = new Point(scaledX, topLeft.Y);
+                topRight = new Point(scaledX, topRight.Y);
+            }
+
+            if (rightTop.Y > rightBottom.Y)
+            {
+                var scaledY = borderCoordinates.TopRight / (borderCoordinates.TopRight + borderCoordinates.BottomRight) * boundRect.Height;
+                rightTop = new Point(rightTop.X, scaledY);
+                rightBottom = new Point(rightBottom.X, scaledY);
+            }
+
+            if (bottomRight.X < bottomLeft.X)
+            {
+                var scaledX = borderCoordinates.LeftBottom / (borderCoordinates.LeftBottom + borderCoordinates.RightBottom) * boundRect.Width;
+                bottomRight = new Point(scaledX, bottomRight.Y);
+                bottomLeft = new Point(scaledX, bottomLeft.Y);
+            }
+
+            if (leftBottom.Y < leftTop.Y)
+            {
+                var scaledY = borderCoordinates.TopLeft / (borderCoordinates.TopLeft + borderCoordinates.BottomLeft) * boundRect.Height;
+                leftBottom = new Point(leftBottom.X, scaledY);
+                leftTop = new Point(leftTop.X, scaledY);
+            }
+
+            var offset = new Vector(boundRect.TopLeft.X, boundRect.TopLeft.Y);
+            topLeft += offset;
+            topRight += offset;
+            rightTop += offset;
+            rightBottom += offset;
+            bottomRight += offset;
+            bottomLeft += offset;
+            leftBottom += offset;
+            leftTop += offset;
+
+            context.BeginFigure(topLeft, true);
+
+            //Top
+            context.LineTo(topRight);
+
+            //TopRight corner
+            var radiusX = boundRect.TopRight.X - topRight.X;
+            var radiusY = rightTop.Y - boundRect.TopRight.Y;
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(rightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            //Right
+            context.LineTo(rightBottom);
+
+            //BottomRight corner
+            radiusX = boundRect.BottomRight.X - bottomRight.X;
+            radiusY = boundRect.BottomRight.Y - rightBottom.Y;
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            //Bottom
+            context.LineTo(bottomLeft);
+
+            //BottomLeft corner
+            radiusX = bottomLeft.X - boundRect.BottomLeft.X;
+            radiusY = boundRect.BottomLeft.Y - leftBottom.Y;
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            //Left
+            context.LineTo(leftTop);
+
+            //TopLeft corner
+            radiusX = topLeft.X - boundRect.TopLeft.X;
+            radiusY = leftTop.Y - boundRect.TopLeft.Y;
+
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            context.EndFigure(true);
+        }
+
+        private struct BorderCoordinates
+        {
+            internal BorderCoordinates(CornerRadius cornerRadius, Thickness borderThickness, bool isOuter)
+            {
+                var left = 0.5 * borderThickness.Left;
+                var top = 0.5 * borderThickness.Top;
+                var right = 0.5 * borderThickness.Right;
+                var bottom = 0.5 * borderThickness.Bottom;
+
+                if (isOuter)
+                {
+                    if (cornerRadius.TopLeft == 0)
+                    {
+                        LeftTop = TopLeft = 0.0;
+                    }
+                    else
+                    {
+                        LeftTop = cornerRadius.TopLeft + left;
+                        TopLeft = cornerRadius.TopLeft + top;
+                    }
+                    if (cornerRadius.TopRight == 0)
+                    {
+                        TopRight = RightTop = 0;
+                    }
+                    else
+                    {
+                        TopRight = cornerRadius.TopRight + top;
+                        RightTop = cornerRadius.TopRight + right;
+                    }
+                    if (cornerRadius.BottomRight == 0)
+                    {
+                        RightBottom = BottomRight = 0;
+                    }
+                    else
+                    {
+                        RightBottom = cornerRadius.BottomRight + right;
+                        BottomRight = cornerRadius.BottomRight + bottom;
+                    }
+                    if (cornerRadius.BottomLeft == 0)
+                    {
+                        BottomLeft = LeftBottom = 0;
+                    }
+                    else
+                    {
+                        BottomLeft = cornerRadius.BottomLeft + bottom;
+                        LeftBottom = cornerRadius.BottomLeft + left;
+                    }
+                }
+                else
+                {
+                    LeftTop = Math.Max(0, cornerRadius.TopLeft - left);
+                    TopLeft = Math.Max(0, cornerRadius.TopLeft - top);
+                    TopRight = Math.Max(0, cornerRadius.TopRight - top);
+                    RightTop = Math.Max(0, cornerRadius.TopRight - right);
+                    RightBottom = Math.Max(0, cornerRadius.BottomRight - right);
+                    BottomRight = Math.Max(0, cornerRadius.BottomRight - bottom);
+                    BottomLeft = Math.Max(0, cornerRadius.BottomLeft - bottom);
+                    LeftBottom = Math.Max(0, cornerRadius.BottomLeft - left);
+                }
+            }
+
+            internal readonly double LeftTop;
+            internal readonly double TopLeft;
+            internal readonly double TopRight;
+            internal readonly double RightTop;
+            internal readonly double RightBottom;
+            internal readonly double BottomRight;
+            internal readonly double BottomLeft;
+            internal readonly double LeftBottom;
+        }
+
+    }
+}

+ 1 - 1
src/Avalonia.Themes.Default/Accents/BaseLight.xaml

@@ -20,7 +20,7 @@
     <SolidColorBrush x:Key="ErrorBrush">Red</SolidColorBrush>
     <SolidColorBrush x:Key="ErrorBrushLight">#10ff0000</SolidColorBrush>
 
-    <sys:Double x:Key="ThemeBorderThickness">2</sys:Double>
+    <Thickness x:Key="ThemeBorderThickness">2</Thickness>
     <sys:Double x:Key="ThemeDisabledOpacity">0.5</sys:Double>
 
     <sys:Double x:Key="FontSizeSmall">10</sys:Double>

+ 97 - 0
src/Avalonia.Visuals/CornerRadius.cs

@@ -0,0 +1,97 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Globalization;
+using System.Linq;
+
+namespace Avalonia
+{
+    public struct CornerRadius
+    {
+        public CornerRadius(double uniformRadius)
+        {
+            TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius;
+
+        }
+        public CornerRadius(double top, double bottom)
+        {
+            TopLeft = TopRight = top;
+            BottomLeft = BottomRight = bottom;
+        }
+        public CornerRadius(double topLeft, double topRight, double bottomRight, double bottomLeft)
+        {
+            TopLeft = topLeft;
+            TopRight = topRight;
+            BottomRight = bottomRight;
+            BottomLeft = bottomLeft;
+        }
+
+        public double TopLeft { get; }
+        public double TopRight { get; }
+        public double BottomRight { get; }
+        public double BottomLeft { get; }
+        public bool IsEmpty => TopLeft.Equals(0) && IsUniform;
+        public bool IsUniform => TopLeft.Equals(TopRight) && BottomLeft.Equals(BottomRight) && TopRight.Equals(BottomRight);
+
+        public override bool Equals(object obj)
+        {
+            if (obj is CornerRadius)
+            {
+                return this == (CornerRadius)obj;
+            }
+            return false;
+        }
+
+        public override int GetHashCode()
+        {
+            return TopLeft.GetHashCode() ^ TopRight.GetHashCode() ^ BottomLeft.GetHashCode() ^ BottomRight.GetHashCode();
+        }
+
+        public override string ToString()
+        {
+            return $"{TopLeft},{TopRight},{BottomRight},{BottomLeft}";
+        }
+
+        public static CornerRadius Parse(string s, CultureInfo culture)
+        {
+            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(x => x.Trim())
+                .ToList();
+
+            switch (parts.Count)
+            {
+                case 1:
+                    var uniform = double.Parse(parts[0], culture);
+                    return new CornerRadius(uniform);
+                case 2:
+                    var top = double.Parse(parts[0], culture);
+                    var bottom = double.Parse(parts[1], culture);
+                    return new CornerRadius(top, bottom);
+                case 4:
+                    var topLeft = double.Parse(parts[0], culture);
+                    var topRight = double.Parse(parts[1], culture);
+                    var bottomRight = double.Parse(parts[2], culture);
+                    var bottomLeft = double.Parse(parts[3], culture);
+                    return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft);
+                default:
+                    {
+                        throw new FormatException("Invalid CornerRadius.");
+                    }
+            }
+        }
+
+        public static bool operator ==(CornerRadius cr1, CornerRadius cr2)
+        {
+            return cr1.TopLeft.Equals(cr2.TopLeft)
+                   && cr1.TopRight.Equals(cr2.TopRight)
+                   && cr1.BottomRight.Equals(cr2.BottomRight) 
+                   && cr1.BottomLeft.Equals(cr2.BottomLeft);
+        }
+
+        public static bool operator !=(CornerRadius cr1, CornerRadius cr2)
+        {
+            return !(cr1 == cr2);
+        }
+    }
+}

+ 1 - 0
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@@ -9,6 +9,7 @@ using Avalonia.Metadata;
 [assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
 
 [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
 [assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]

+ 6 - 1
src/Avalonia.Visuals/Thickness.cs

@@ -90,7 +90,12 @@ namespace Avalonia
         /// <summary>
         /// Gets a value indicating whether all sides are set to 0.
         /// </summary>
-        public bool IsEmpty => Left == 0 && Top == 0 && Right == 0 && Bottom == 0;
+        public bool IsEmpty => Left.Equals(0) && IsUniform;
+
+        /// <summary>
+        /// Gets a value indicating whether all sides are equal.
+        /// </summary>
+        public bool IsUniform => Left.Equals(Right) && Top.Equals(Bottom) && Right.Equals(Bottom);
 
         /// <summary>
         /// Compares two Thicknesses.

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -31,6 +31,7 @@
         </Compile>
         <Compile Include="AvaloniaXamlLoaderPortableXaml.cs" />
         <Compile Include="AvaloniaXamlLoader.cs" />
+        <Compile Include="Converters\CornerRadiusTypeConverter.cs" />
         <Compile Include="Converters\MatrixTypeConverter.cs" />
         <Compile Include="Converters\RectTypeConverter.cs" />
         <Compile Include="Converters\SetterValueTypeConverter.cs" />

+ 19 - 0
src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs

@@ -0,0 +1,19 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Avalonia.Markup.Xaml.Converters
+{
+    public class CornerRadiusTypeConverter : TypeConverter
+    {
+        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+        {
+            return sourceType == typeof(string);
+        }
+
+        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+        {
+            return CornerRadius.Parse((string)value, culture);
+        }
+    }
+}

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs

@@ -43,6 +43,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml
             { typeof(Selector), typeof(SelectorTypeConverter)},
             { typeof(SolidColorBrush), typeof(BrushTypeConverter) },
             { typeof(Thickness), typeof(ThicknessTypeConverter) },
+            { typeof(CornerRadius), typeof(CornerRadiusTypeConverter) },
             { typeof(TimeSpan), typeof(TimeSpanTypeConverter) },
             //{ typeof(Uri), typeof(Converters.UriTypeConverter) },
             { typeof(Cursor), typeof(CursorTypeConverter) },

+ 0 - 2
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@@ -1,7 +1,6 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System;
 using Avalonia.Platform;
 using SharpDX.Direct2D1;
 
@@ -20,7 +19,6 @@ namespace Avalonia.Direct2D1.Media
         /// <inheritdoc/>
         public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia();
 
-        /// <inheritdoc/>
         public Geometry Geometry { get; }
 
         /// <inheritdoc/>

+ 1 - 1
tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs

@@ -33,7 +33,7 @@ namespace Avalonia.Benchmarks.Styling
 
             var border = (Border)textBox.GetVisualChildren().Single();
 
-            if (border.BorderThickness != 2)
+            if (border.BorderThickness != new Thickness(2))
             {
                 throw new Exception("Styles not applied.");
             }

+ 1 - 1
tests/Avalonia.Controls.UnitTests/BorderTests.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests
             var target = new Border
             {
                 Padding = new Thickness(6),
-                BorderThickness = 4,
+                BorderThickness = new Thickness(4)
             };
 
             target.Measure(new Size(100, 100));

+ 53 - 13
tests/Avalonia.RenderTests/Controls/BorderTests.cs

@@ -31,7 +31,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 1,
+                    BorderThickness = new Thickness(1),
                 }
             };
 
@@ -50,7 +50,47 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task Border_Uniform_CornerRadius()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 200,
+                Height = 200,
+                Child = new Border
+                {
+                    BorderBrush = Brushes.Black,
+                    BorderThickness = new Thickness(2),
+                    CornerRadius = new CornerRadius(16),
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task Border_NonUniform_CornerRadius()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 200,
+                Height = 200,
+                Child = new Border
+                {
+                    BorderBrush = Brushes.Black,
+                    BorderThickness = new Thickness(2),
+                    CornerRadius = new CornerRadius(16, 4, 7, 10),
                 }
             };
 
@@ -87,7 +127,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new Border
                     {
                         Background = Brushes.Red,
@@ -110,7 +150,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Padding = new Thickness(2),
                     Child = new Border
                     {
@@ -134,7 +174,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new Border
                     {
                         Background = Brushes.Red,
@@ -159,7 +199,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -186,7 +226,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -213,7 +253,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -240,7 +280,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -267,7 +307,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -294,7 +334,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -321,7 +361,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",
@@ -348,7 +388,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                     Child = new TextBlock
                     {
                         Text = "Foo",

+ 1 - 1
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@@ -42,7 +42,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                         new Border
                         {
                             BorderBrush = Brushes.Blue,
-                            BorderThickness = 2,
+                            BorderThickness = new Thickness(2),
                             HorizontalAlignment = HorizontalAlignment.Center,
                             VerticalAlignment = VerticalAlignment.Center,
                             Child = new TextBlock

+ 1 - 1
tests/Avalonia.RenderTests/Shapes/PathTests.cs

@@ -316,7 +316,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
                 Child = new Border
                 {
                     BorderBrush = Brushes.Red,
-                    BorderThickness = 1,
+                    BorderThickness = new Thickness(1),
                     HorizontalAlignment = HorizontalAlignment.Center,
                     VerticalAlignment = VerticalAlignment.Center,
                     Child = new Path

+ 3 - 3
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@@ -151,7 +151,7 @@ namespace Avalonia.Styling.UnitTests
             {
                 Setters = new[]
                 {
-                    new Setter(Border.BorderThicknessProperty, 4),
+                    new Setter(Border.BorderThicknessProperty, new Thickness(4)),
                 }
             };
 
@@ -162,9 +162,9 @@ namespace Avalonia.Styling.UnitTests
 
             style.Attach(border, null);
 
-            Assert.Equal(4, border.BorderThickness);
+            Assert.Equal(new Thickness(4), border.BorderThickness);
             root.Child = null;
-            Assert.Equal(0, border.BorderThickness);
+            Assert.Equal(new Thickness(0), border.BorderThickness);
         }
 
         private class Class1 : Control

+ 43 - 0
tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs

@@ -0,0 +1,43 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Globalization;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests
+{
+    public class CornerRadiusTests
+    {
+        [Fact]
+        public void Parse_Parses_Single_Uniform_Radius()
+        {
+            var result = CornerRadius.Parse("3.4", CultureInfo.InvariantCulture);
+
+            Assert.Equal(new CornerRadius(3.4), result);
+        }
+
+        [Fact]
+        public void Parse_Parses_Top_Bottom()
+        {
+            var result = CornerRadius.Parse("1.1,2.2", CultureInfo.InvariantCulture);
+
+            Assert.Equal(new CornerRadius(1.1, 2.2), result);
+        }
+
+        [Fact]
+        public void Parse_Parses_TopLeft_TopRight_BottomRight_BottomLeft()
+        {
+            var result = CornerRadius.Parse("1.1,2.2,3.3,4.4", CultureInfo.InvariantCulture);
+
+            Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
+        }
+
+        [Fact]
+        public void Parse_Accepts_Spaces()
+        {
+            var result = CornerRadius.Parse("1.1 2.2 3.3 4.4", CultureInfo.InvariantCulture);
+
+            Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
+        }
+    }
+}

+ 2 - 2
tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs

@@ -4,7 +4,7 @@
 using System.Globalization;
 using Xunit;
 
-namespace Avalonia.Visuals.UnitTests.Media
+namespace Avalonia.Visuals.UnitTests
 {
     public class ThicknessTests
     {
@@ -40,4 +40,4 @@ namespace Avalonia.Visuals.UnitTests.Media
             Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result);
         }
     }
-}
+}

BIN
tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png


BIN
tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png


BIN
tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png


BIN
tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png