From 27a666467cc36d860790f8e3e9e04bebe78818a9 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 22 Mar 2018 11:31:54 +0100 Subject: [PATCH 1/6] Initial --- src/Avalonia.Controls/Border.cs | 333 ++++++++++++++++-- .../Presenters/ContentPresenter.cs | 153 ++++---- .../Primitives/TemplatedControl.cs | 4 +- .../Accents/BaseLight.xaml | 5 +- src/Avalonia.Visuals/CornerRadius.cs | 97 +++++ src/Avalonia.Visuals/Thickness.cs | 7 +- .../Avalonia.Markup.Xaml.csproj | 1 + .../Converters/CornerRadiusTypeConverter.cs | 19 + .../AvaloniaDefaultTypeConverters.cs | 1 + .../Avalonia.Direct2D1/Media/GeometryImpl.cs | 11 +- .../Styling/ApplyStyling.cs | 2 +- .../BorderTests.cs | 2 +- .../Controls/BorderTests.cs | 26 +- .../Media/VisualBrushTests.cs | 2 +- .../Avalonia.RenderTests/Shapes/PathTests.cs | 2 +- .../Avalonia.Styling.UnitTests/StyleTests.cs | 6 +- .../CornerRadiusTests.cs | 43 +++ .../ThicknessTests.cs | 4 +- 18 files changed, 582 insertions(+), 136 deletions(-) create mode 100644 src/Avalonia.Visuals/CornerRadius.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 002c5ea3f2..4ddf81565a 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 System; +using Avalonia; using Avalonia.Media; namespace Avalonia.Controls @@ -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 BorderRenderer _borderRenderer = new BorderRenderer(); /// /// 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); - } + _borderRenderer.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)); } + _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; } } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index d0a438cc2b..2d623dcbf7 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly StyledProperty BorderThicknessProperty = + public static readonly StyledProperty BorderThicknessProperty = Border.BorderThicknessProperty.AddOwner(); /// @@ -57,7 +57,7 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly StyledProperty CornerRadiusProperty = + public static readonly StyledProperty CornerRadiusProperty = Border.CornerRadiusProperty.AddOwner(); /// @@ -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(); /// /// Initializes static members of the class. @@ -120,7 +121,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 +158,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); } @@ -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 /// 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 +331,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,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()}"); } 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.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index cb86598a42..ebb14579b4 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -1,6 +1,7 @@