diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 002c5ea3f2..06ad8a4837 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/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 /// /// A control which decorates a child with a border and background. /// - public class Border : Decorator + public partial class Border : Decorator { /// /// Defines the property. @@ -25,14 +27,16 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty BorderThicknessProperty = - AvaloniaProperty.Register(nameof(BorderThickness)); + public static readonly StyledProperty BorderThicknessProperty = + AvaloniaProperty.Register(nameof(BorderThickness)); /// /// Defines the property. /// - public static readonly StyledProperty CornerRadiusProperty = - AvaloniaProperty.Register(nameof(CornerRadius)); + public static readonly StyledProperty CornerRadiusProperty = + AvaloniaProperty.Register(nameof(CornerRadius)); + + private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); /// /// Initializes static members of the class. @@ -63,7 +67,7 @@ namespace Avalonia.Controls /// /// Gets or sets the thickness of the border. /// - public double BorderThickness + public Thickness BorderThickness { get { return GetValue(BorderThicknessProperty); } set { SetValue(BorderThicknessProperty, value); } @@ -72,7 +76,7 @@ namespace Avalonia.Controls /// /// Gets or sets the radius of the border rounded corners. /// - public float CornerRadius + public CornerRadius CornerRadius { get { return GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } @@ -84,21 +88,7 @@ namespace Avalonia.Controls /// The drawing context. 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); } /// @@ -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); } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 54626e9e97..6acbc047ae 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/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 /// /// Defines the property. /// - public static readonly StyledProperty BorderThicknessProperty = + public static readonly StyledProperty BorderThicknessProperty = Border.BorderThicknessProperty.AddOwner(); /// @@ -57,7 +58,7 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly StyledProperty CornerRadiusProperty = + public static readonly StyledProperty CornerRadiusProperty = Border.CornerRadiusProperty.AddOwner(); /// @@ -76,11 +77,12 @@ namespace Avalonia.Controls.Presenters /// Defines the property. /// public static readonly StyledProperty PaddingProperty = - Border.PaddingProperty.AddOwner(); + Decorator.PaddingProperty.AddOwner(); private IControl _child; private bool _createdChild; private IDataTemplate _dataTemplate; + private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); /// /// Initializes static members of the class. @@ -120,7 +122,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets or sets the thickness of the border. /// - public double BorderThickness + public Thickness BorderThickness { get { return GetValue(BorderThicknessProperty); } set { SetValue(BorderThicknessProperty, value); } @@ -157,7 +159,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets or sets the radius of the border rounded corners. /// - public float CornerRadius + public CornerRadius CornerRadius { get { return GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } @@ -277,21 +279,7 @@ namespace Avalonia.Controls.Presenters /// 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); } /// @@ -344,7 +332,11 @@ namespace Avalonia.Controls.Presenters /// protected override Size ArrangeOverride(Size finalSize) { - return ArrangeOverrideImpl(finalSize, new Vector()); + finalSize = ArrangeOverrideImpl(finalSize, new Vector()); + + _borderRenderer.Update(finalSize, BorderThickness, CornerRadius); + + return finalSize; } /// @@ -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; } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 6deef7c7b9..77735f3f12 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty BorderThicknessProperty = + public static readonly StyledProperty BorderThicknessProperty = Border.BorderThicknessProperty.AddOwner(); /// @@ -132,7 +132,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the thickness of the control's border. /// - public double BorderThickness + public Thickness BorderThickness { get { return GetValue(BorderThicknessProperty); } set { SetValue(BorderThicknessProperty, value); } diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs new file mode 100644 index 0000000000..d9169e51f3 --- /dev/null +++ b/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; + } + + } +} diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index cb86598a42..4c85e172ff 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -20,7 +20,7 @@ Red #10ff0000 - 2 + 2 0.5 10 diff --git a/src/Avalonia.Visuals/CornerRadius.cs b/src/Avalonia.Visuals/CornerRadius.cs new file mode 100644 index 0000000000..db0ac97d49 --- /dev/null +++ b/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); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 87347d64b1..900746d05a 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/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")] \ No newline at end of file diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index dc9be7341d..ead9fd004a 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -90,7 +90,12 @@ namespace Avalonia /// /// Gets a value indicating whether all sides are set to 0. /// - public bool IsEmpty => Left == 0 && Top == 0 && Right == 0 && Bottom == 0; + public bool IsEmpty => Left.Equals(0) && IsUniform; + + /// + /// Gets a value indicating whether all sides are equal. + /// + public bool IsUniform => Left.Equals(Right) && Top.Equals(Bottom) && Right.Equals(Bottom); /// /// Compares two Thicknesses. diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 0ce2a1a992..bd6acfdad1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -31,6 +31,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs new file mode 100644 index 0000000000..5da0efae1b --- /dev/null +++ b/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); + } + } +} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs index 1cf5b6a58e..1ae24c8a34 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs +++ b/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) }, diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 3e89dcc9b7..f057d191b6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/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 /// public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia(); - /// public Geometry Geometry { get; } /// diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs index 33af55fdf9..d24a646f74 100644 --- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs +++ b/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."); } diff --git a/tests/Avalonia.Controls.UnitTests/BorderTests.cs b/tests/Avalonia.Controls.UnitTests/BorderTests.cs index c0d2a39ab2..3660a7b4ca 100644 --- a/tests/Avalonia.Controls.UnitTests/BorderTests.cs +++ b/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)); diff --git a/tests/Avalonia.RenderTests/Controls/BorderTests.cs b/tests/Avalonia.RenderTests/Controls/BorderTests.cs index 3bd5a6e1cb..7d2e40c3b4 100644 --- a/tests/Avalonia.RenderTests/Controls/BorderTests.cs +++ b/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", diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index 099b022862..cfa15ae304 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/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 diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs index fab867f428..4703daca25 100644 --- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs +++ b/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 diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index a7c559668b..5ef559b887 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/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 diff --git a/tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs b/tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs new file mode 100644 index 0000000000..bc0bbdc867 --- /dev/null +++ b/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); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs b/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs index bd694d073a..03bf395d1e 100644 --- a/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs +++ b/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); } } -} +} \ No newline at end of file diff --git a/tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png b/tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png new file mode 100644 index 0000000000..9deb45aaeb Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png b/tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png new file mode 100644 index 0000000000..a4bfa75eb8 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png differ diff --git a/tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png b/tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png new file mode 100644 index 0000000000..9deb45aaeb Binary files /dev/null and b/tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png differ diff --git a/tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png b/tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png new file mode 100644 index 0000000000..a4bfa75eb8 Binary files /dev/null and b/tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png differ