1
0
Benedikt Schroeder 7 жил өмнө
parent
commit
27a666467c

+ 307 - 26
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 System;
+using Avalonia;
 using Avalonia.Media;
 
 namespace Avalonia.Controls
@@ -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 BorderRenderer _borderRenderer = new BorderRenderer();
 
         /// <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);
-            }
+            _borderRenderer.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));
             }
 
+            _borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
+
             return finalSize;
         }
 
@@ -131,18 +123,307 @@ 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);
+        }
+
+
+        internal class BorderRenderer
+        {
+            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 innerRadii = new Radii(cornerRadius, borderThickness, false);
+
+                    StreamGeometry backgroundGeometry = null;
+
+                    //  calculate border / background rendering geometry
+                    if (!innerRect.Width.Equals(0) && !innerRect.Height.Equals(0))
+                    {
+                        backgroundGeometry = new StreamGeometry();
+
+                        using (var ctx = backgroundGeometry.Open())
+                        {
+                            GenerateGeometry(ctx, innerRect, innerRadii);
+                        }
+
+                        _backgroundGeometryCache = backgroundGeometry;
+                    }
+                    else
+                    {
+                        _backgroundGeometryCache = null;
+                    }
+
+                    if (!boundRect.Width.Equals(0) && !boundRect.Height.Equals(0))
+                    {
+                        var outerRadii = new Radii(cornerRadius, borderThickness, true);
+                        var borderGeometry = new StreamGeometry();
+
+                        using (var ctx = borderGeometry.Open())
+                        {
+                            GenerateGeometry(ctx, boundRect, outerRadii);
+
+                            if (backgroundGeometry != null)
+                            {
+                                GenerateGeometry(ctx, innerRect, innerRadii);
+                            }
+                        }
+
+                        _borderGeometryCache = borderGeometry;
+                    }
+                    else
+                    {
+                        _borderGeometryCache = null;
+                    }
+                }
+            }
+
+            public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
             {
-                return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
+                if (_useComplexRendering)
+                {
+                    IBrush brush;
+                    var borderGeometry = _borderGeometryCache;
+                    if (borderGeometry != null && (brush = borderBrush) != null)
+                    {
+                        context.DrawGeometry(brush, null, borderGeometry);
+                    }
+
+                    var backgroundGeometry = _backgroundGeometryCache;
+                    if (backgroundGeometry != null && (brush = background) != null)
+                    {
+                        context.DrawGeometry(brush, null, backgroundGeometry);
+                    }
+                }
+                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 GenerateGeometry(StreamGeometryContext ctx, Rect rect, Radii radii)
+            {
+                //
+                //  Compute the coordinates of the key points
+                //
+
+                var topLeft = new Point(radii.LeftTop, 0);
+                var topRight = new Point(rect.Width - radii.RightTop, 0);
+                var rightTop = new Point(rect.Width, radii.TopRight);
+                var rightBottom = new Point(rect.Width, rect.Height - radii.BottomRight);
+                var bottomRight = new Point(rect.Width - radii.RightBottom, rect.Height);
+                var bottomLeft = new Point(radii.LeftBottom, rect.Height);
+                var leftBottom = new Point(0, rect.Height - radii.BottomLeft);
+                var leftTop = new Point(0, radii.TopLeft);
+
+                //
+                //  Check keypoints for overlap and resolve by partitioning radii according to
+                //  the percentage of each one.  
+                //
+
+                //  Top edge is handled here
+                if (topLeft.X > topRight.X)
+                {
+                    var x = radii.LeftTop / (radii.LeftTop + radii.RightTop) * rect.Width;
+                    topLeft += new Point(x, 0);
+                    topRight += new Point(x, 0);
+                }
+
+                //  Right edge
+                if (rightTop.Y > rightBottom.Y)
+                {
+                    var y = radii.TopRight / (radii.TopRight + radii.BottomRight) * rect.Height;
+                    rightTop += new Point(0, y);
+                    rightBottom += new Point(0, y);
+                }
+
+                //  Bottom edge
+                if (bottomRight.X < bottomLeft.X)
+                {
+                    var x = radii.LeftBottom / (radii.LeftBottom + radii.RightBottom) * rect.Width;
+                    bottomRight += new Point(x, 0);
+                    bottomLeft += new Point(x, 0);
+                }
+
+                // Left edge
+                if (leftBottom.Y < leftTop.Y)
+                {
+                    var y = radii.TopLeft / (radii.TopLeft + radii.BottomLeft) * rect.Height;
+                    leftBottom += new Point(0, y);
+                    leftTop += new Point(0, y);
+                }
+
+                //
+                //  Add on offsets
+                //
+
+                var offset = new Vector(rect.TopLeft.X, rect.TopLeft.Y);
+                topLeft += offset;
+                topRight += offset;
+                rightTop += offset;
+                rightBottom += offset;
+                bottomRight += offset;
+                bottomLeft += offset;
+                leftBottom += offset;
+                leftTop += offset;
+
+                //
+                //  Create the border geometry
+                //
+                ctx.BeginFigure(topLeft, true);
+
+                // Top
+                ctx.LineTo(topRight);
+
+                // TopRight
+                var radiusX = rect.TopRight.X - topRight.X;
+                var radiusY = rightTop.Y - rect.TopRight.Y;
+                if (!radiusX.Equals(0) || !radiusY.Equals(0))
+                {
+                    ctx.ArcTo(rightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+                }
+
+                // Right
+                ctx.LineTo(rightBottom);
+
+                // BottomRight
+                radiusX = rect.BottomRight.X - bottomRight.X;
+                radiusY = rect.BottomRight.Y - rightBottom.Y;
+                if (!radiusX.Equals(0) || !radiusY.Equals(0))
+                {
+                    ctx.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+                }
+
+                // Bottom
+                ctx.LineTo(bottomLeft);
+
+                // BottomLeft
+                radiusX = bottomLeft.X - rect.BottomLeft.X;
+                radiusY = rect.BottomLeft.Y - leftBottom.Y;
+                if (!radiusX.Equals(0) || !radiusY.Equals(0))
+                {
+                    ctx.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+                }
+
+                // Left
+                ctx.LineTo(leftTop);
+
+                // TopLeft
+                radiusX = topLeft.X - rect.TopLeft.X;
+                radiusY = leftTop.Y - rect.TopLeft.Y;
+                if (!radiusX.Equals(0) || !radiusY.Equals(0))
+                {
+                    ctx.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+                }
+
+                ctx.EndFigure(true);
+            }
+
+            private struct Radii
+            {
+                internal Radii(CornerRadius radii, Thickness borders, bool outer)
+                {
+                    var left = 0.5 * borders.Left;
+                    var top = 0.5 * borders.Top;
+                    var right = 0.5 * borders.Right;
+                    var bottom = 0.5 * borders.Bottom;
+
+                    if (outer)
+                    {
+                        if (radii.TopLeft.Equals(0))
+                        {
+                            LeftTop = TopLeft = 0.0;
+                        }
+                        else
+                        {
+                            LeftTop = radii.TopLeft + left;
+                            TopLeft = radii.TopLeft + top;
+                        }
+                        if (radii.TopRight.Equals(0))
+                        {
+                            TopRight = RightTop = 0.0;
+                        }
+                        else
+                        {
+                            TopRight = radii.TopRight + top;
+                            RightTop = radii.TopRight + right;
+                        }
+                        if (radii.BottomRight.Equals(0))
+                        {
+                            RightBottom = BottomRight = 0.0;
+                        }
+                        else
+                        {
+                            RightBottom = radii.BottomRight + right;
+                            BottomRight = radii.BottomRight + bottom;
+                        }
+                        if (radii.BottomLeft.Equals(0))
+                        {
+                            BottomLeft = LeftBottom = 0.0;
+                        }
+                        else
+                        {
+                            BottomLeft = radii.BottomLeft + bottom;
+                            LeftBottom = radii.BottomLeft + left;
+                        }
+                    }
+                    else
+                    {
+                        LeftTop = Math.Max(0.0, radii.TopLeft - left);
+                        TopLeft = Math.Max(0.0, radii.TopLeft - top);
+                        TopRight = Math.Max(0.0, radii.TopRight - top);
+                        RightTop = Math.Max(0.0, radii.TopRight - right);
+                        RightBottom = Math.Max(0.0, radii.BottomRight - right);
+                        BottomRight = Math.Max(0.0, radii.BottomRight - bottom);
+                        BottomLeft = Math.Max(0.0, radii.BottomLeft - bottom);
+                        LeftBottom = Math.Max(0.0, radii.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;
             }
         }
     }

+ 72 - 81
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -31,7 +31,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 +57,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>
@@ -81,6 +81,7 @@ namespace Avalonia.Controls.Presenters
         private IControl _child;
         private bool _createdChild;
         private IDataTemplate _dataTemplate;
+        private readonly Border.BorderRenderer _borderRenderer = new Border.BorderRenderer();
 
         /// <summary>
         /// Initializes static members of the <see cref="ContentPresenter"/> class.
@@ -120,7 +121,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 +158,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); }
@@ -221,7 +222,7 @@ namespace Avalonia.Controls.Presenters
         {
             var content = Content;
             var oldChild = Child;
-            var newChild = CreateChild();            
+            var newChild = CreateChild();
 
             // Remove the old child if we're not recycling it.
             if (oldChild != null && newChild != oldChild)
@@ -277,21 +278,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 +331,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,74 +363,74 @@ 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));
-                }
+            var padding = Padding;
+            var borderThickness = BorderThickness;
+            var horizontalContentAlignment = HorizontalContentAlignment;
+            var verticalContentAlignment = VerticalContentAlignment;
+            var useLayoutRounding = UseLayoutRounding;
+            //Not sure about this part
+            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));
+            }
 
-                size = LayoutHelper.ApplyLayoutConstraints(Child, size);
+            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);
-                }
+            size = LayoutHelper.ApplyLayoutConstraints(Child, size);
 
-                switch (horizontalContentAlignment)
-                {
-                    case HorizontalAlignment.Center:
-                    case HorizontalAlignment.Stretch:
-                        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:
-                    case VerticalAlignment.Stretch:
-                        originY += (availableSizeMinusMargins.Height - size.Height) / 2;
-                        break;
-                    case VerticalAlignment.Bottom:
-                        originY += availableSizeMinusMargins.Height - size.Height;
-                        break;
-                }
+            switch (horizontalContentAlignment)
+            {
+                case HorizontalAlignment.Center:
+                case HorizontalAlignment.Stretch:
+                    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:
+                case VerticalAlignment.Stretch:
+                    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;
         }
 
@@ -447,7 +438,7 @@ namespace Avalonia.Controls.Presenters
         {
             var result = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0;
 
-            if (result == 0 || double.IsNaN(result) || double.IsInfinity(result))
+            if (result.Equals(0) || double.IsNaN(result) || double.IsInfinity(result))
             {
                 throw new Exception($"Invalid LayoutScaling returned from {VisualRoot.GetType()}");
             }

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

+ 3 - 2
src/Avalonia.Themes.Default/Accents/BaseLight.xaml

@@ -1,6 +1,7 @@
 <Style xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-       xmlns:sys="clr-namespace:System;assembly=mscorlib">
+       xmlns:sys="clr-namespace:System;assembly=mscorlib"
+       xmlns:avalonia="clr-namespace:Avalonia;assembly=Avalonia.Visuals">
   <Style.Resources>
     <SolidColorBrush x:Key="ThemeBackgroundBrush">#FFFFFFFF</SolidColorBrush>
     <SolidColorBrush x:Key="ThemeBorderLightBrush">#FFAAAAAA</SolidColorBrush>
@@ -20,7 +21,7 @@
     <SolidColorBrush x:Key="ErrorBrush">Red</SolidColorBrush>
     <SolidColorBrush x:Key="ErrorBrushLight">#10ff0000</SolidColorBrush>
 
-    <sys:Double x:Key="ThemeBorderThickness">2</sys:Double>
+    <avalonia:Thickness x:Key="ThemeBorderThickness">2</avalonia: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) || double.IsNaN(cr1.TopLeft) && double.IsNaN(cr2.TopLeft)))
+                   && (cr1.TopRight.Equals(cr2.TopRight) || (double.IsNaN(cr1.TopRight) && double.IsNaN(cr2.TopRight)))
+                   && (cr1.BottomRight.Equals(cr2.BottomRight) || double.IsNaN(cr1.BottomRight) && double.IsNaN(cr2.BottomRight))
+                   && (cr1.BottomLeft.Equals(cr2.BottomLeft) || double.IsNaN(cr1.BottomLeft) && double.IsNaN(cr2.BottomLeft));
+        }
+
+        public static bool operator !=(CornerRadius cr1, CornerRadius cr2)
+        {
+            return (!(cr1 == cr2));
+        }
+    }
+}

+ 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) },

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

@@ -26,8 +26,15 @@ namespace Avalonia.Direct2D1.Media
         /// <inheritdoc/>
         public Rect GetRenderBounds(Avalonia.Media.Pen pen)
         {
-            var factory = AvaloniaLocator.Current.GetService<Factory>();
-            return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
+            //var factory = AvaloniaLocator.Current.GetService<Factory>();
+            if (pen == null)
+            {
+                return Geometry.GetWidenedBounds(0).ToAvalonia();
+            }
+            else
+            {
+                return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
+            }
         }
 
         /// <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));

+ 13 - 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,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Child = new Border
                 {
                     BorderBrush = Brushes.Black,
-                    BorderThickness = 2,
+                    BorderThickness = new Thickness(2),
                 }
             };
 
@@ -87,7 +87,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 +110,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 +134,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 +159,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 +186,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 +213,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 +240,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 +267,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 +294,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 +321,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 +348,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);
         }
     }
-}
+}