From 2a9e7e9dcfc3e590bdd50b1e874ce70c0fafcc24 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 29 Jan 2024 15:03:11 -0500 Subject: [PATCH] Add BackgroundSizing (#14048) * Add BackgroundSizing enum * Improve formatting in RoundedRect * Add the BackgroundSizing property to relevant controls * Add new GeometryBuilder helper This is based on new ideas and APIs in both WinUI and WPF * Support BackgroundSizing in BorderRenderHelper This also removes the last remnants of dash array support in the internal APIs * Remove old code in BorderRenderHelper moved to GeometryBuilder * Move Rectangle shape geometry calculation into GeometryBuilder * Add RadiusX/Y properties to RectangleGeometry * Use RectangleGeometry directly in the Rectangle shape Since RectangleGeometry now supports RadiusX/Y there is no longer a need to have a separate geometry creation path. * Add WinUI-based CalculateRoundedCornersRectangle() method * Simplify and use the WinUI-algorithm in BorderRenderHelper * Update GeometryBuilder.DrawRoundedCornersRectangle() based on WinUI * Remove obsolete WPF-based CalculateRoundedCornersRectangle method * Update CalculateRoundedCornersRectangleAlternate * Update GeometryBuilderTests * Fix merge * Remove unused dashed line properties in BorderRenderHelper These were already removed in master but could not be merged directly because of the new backgroundSizing parameter. * Optimize CalculateRoundedCornersRectangle In the worse-case up to three Point structs were created during calculation for each keypoint. Now doubles are used during calculations so the Point is created once at the end. * Adjust corner radius calculation within custom GeometryBuilder.CalculateRoundedCornersRectangle() Corner radius is defined at the center of the border stroke rather than the outside edge. * Simplify code in WinUI's algorithm for CalculateRoundedCornersRectangle() * Remove extra BorderRenderHelper.RenderCore method This method does not need to be separate from Render. * Add more CalculateRoundedCornersRectangle tests and adjust method naming * Remove custom CalculateRoundedCornersRectangle() algorithm and always use the WinUI one * Remove some AggressiveInlining attributes * Make parameters clear * Pass RoundedRectKeypoints by ref * Use GeometryCombineMode.Exclude to better calculate border geometries * Update BorderPage.xaml to include BackgroundSizing API --------- Co-authored-by: Max Katz --- samples/ControlCatalog/Pages/BorderPage.xaml | 26 +- src/Avalonia.Base/Media/BackgroundSizing.cs | 38 ++ src/Avalonia.Base/Media/GeometryBuilder.cs | 608 ++++++++++++++++++ src/Avalonia.Base/Media/RectangleGeometry.cs | 77 ++- src/Avalonia.Base/RoundedRect.cs | 25 +- src/Avalonia.Controls/Border.cs | 19 + src/Avalonia.Controls/ContentControl.cs | 1 + .../Presenters/ContentPresenter.cs | 48 +- .../Primitives/TemplatedControl.cs | 15 + src/Avalonia.Controls/Shapes/Rectangle.cs | 92 +-- .../Utils/BorderRenderHelper.cs | 266 ++------ .../Media/GeometryBuilderTests.cs | 61 ++ 12 files changed, 946 insertions(+), 330 deletions(-) create mode 100644 src/Avalonia.Base/Media/BackgroundSizing.cs create mode 100644 src/Avalonia.Base/Media/GeometryBuilder.cs create mode 100644 tests/Avalonia.Base.UnitTests/Media/GeometryBuilderTests.cs diff --git a/samples/ControlCatalog/Pages/BorderPage.xaml b/samples/ControlCatalog/Pages/BorderPage.xaml index 7ec7e81e80..4c2384d686 100644 --- a/samples/ControlCatalog/Pages/BorderPage.xaml +++ b/samples/ControlCatalog/Pages/BorderPage.xaml @@ -5,6 +5,9 @@ d:DesignWidth="400" x:Class="ControlCatalog.Pages.BorderPage"> + + + A control which decorates a child with a border and background Border - Border and Background + BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}" + BackgroundSizing="CenterBorder" + BorderThickness="8" + Padding="12"> + Background And CenterBorder + + + Background And InnerBorder + + + Background And OuterBorderEdge + /// Defines how a background is drawn relative to its border. + /// + public enum BackgroundSizing + { + /// + /// The background is drawn up to the inside edge of the border. + /// + /// + /// The background will never be drawn under the border itself and will not be visible + /// underneath the border regardless of border transparency. + /// + InnerBorderEdge = 0, + + /// + /// The background is drawn completely to the outside edge of the border. + /// + /// + /// The background will be visible underneath the border if the border has transparency. + /// + OuterBorderEdge = 1, + + /// + /// The background is drawn to the midpoint (center) of the border. + /// + /// + /// The background will be visible underneath half of the border if the border has transparency. + /// For this reason it is not recommended to use if transparency is involved. + ///

+ /// This value does not exist in other XAML frameworks and only exists in Avalonia for backwards compatibility + /// with legacy code. Before was added, Avalonia would always render using this + /// value (Skia's default). + ///
+ CenterBorder = 2, + } +} diff --git a/src/Avalonia.Base/Media/GeometryBuilder.cs b/src/Avalonia.Base/Media/GeometryBuilder.cs new file mode 100644 index 0000000000..074c48d8ee --- /dev/null +++ b/src/Avalonia.Base/Media/GeometryBuilder.cs @@ -0,0 +1,608 @@ +// Portions of this source file are adapted from the Windows Presentation Foundation (WPF) project. +// (https://github.com/dotnet/wpf) +// +// Licensed to The Avalonia Project under the MIT License, courtesy of The .NET Foundation. +// +// Portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml/tree/winui3/main) +// +// Licensed to The Avalonia Project under the MIT License. + +// Ignore Spelling: keypoints + +using System; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + /// + /// Contains internal helpers used to build and draw various geometries. + /// + internal class GeometryBuilder + { + private const double PiOver2 = 1.57079633; // 90 deg to rad + private const double Epsilon = 0.00000153; // Same as LayoutHelper.LayoutEpsilon + + /// + /// Draws a new rounded rectangle within the given geometry context. + /// Warning: The caller must manage and dispose the externally. + /// + /// + /// WinUI: https://github.com/microsoft/microsoft-ui-xaml/blob/93742a178db8f625ba9299f62c21f656e0b195ad/dxaml/xcp/core/core/elements/geometry.cpp#L1072-L1079 + /// + /// The geometry context to draw into. + /// The rounded rectangle keypoints defining the rectangle to draw. + public static void DrawRoundedCornersRectangle( + StreamGeometryContext context, + ref RoundedRectKeypoints keypoints) + { + double radiusX; + double radiusY; + + context.BeginFigure(keypoints.TopLeft, isFilled: true); + + // Top + context.LineTo(keypoints.TopRight); + + // TopRight corner + radiusX = keypoints.RightTop.X - keypoints.TopRight.X; + radiusY = keypoints.TopRight.Y - keypoints.RightTop.Y; + radiusX = radiusX > 0 ? radiusX : -radiusX; + radiusY = radiusY > 0 ? radiusY : -radiusY; + + context.ArcTo( + keypoints.RightTop, + new Size(radiusX, radiusY), + rotationAngle: 0.0, + isLargeArc: false, + SweepDirection.Clockwise); + + // Right + context.LineTo(keypoints.RightBottom); + + // BottomRight corner + radiusX = keypoints.RightBottom.X - keypoints.BottomRight.X; + radiusY = keypoints.BottomRight.Y - keypoints.RightBottom.Y; + radiusX = radiusX > 0 ? radiusX : -radiusX; + radiusY = radiusY > 0 ? radiusY : -radiusY; + + if (radiusX != 0 || radiusY != 0) + { + context.ArcTo( + keypoints.BottomRight, + new Size(radiusX, radiusY), + rotationAngle: 0.0, + isLargeArc: false, + SweepDirection.Clockwise); + } + + // Bottom + context.LineTo(keypoints.BottomLeft); + + // BottomLeft corner + radiusX = keypoints.BottomLeft.X - keypoints.LeftBottom.X; + radiusY = keypoints.BottomLeft.Y - keypoints.LeftBottom.Y; + radiusX = radiusX > 0 ? radiusX : -radiusX; + radiusY = radiusY > 0 ? radiusY : -radiusY; + + if (radiusX != 0 || radiusY != 0) + { + context.ArcTo( + keypoints.LeftBottom, + new Size(radiusX, radiusY), + rotationAngle: 0.0, + isLargeArc: false, + SweepDirection.Clockwise); + } + + // Left + context.LineTo(keypoints.LeftTop); + + // TopLeft corner + radiusX = keypoints.TopLeft.X - keypoints.LeftTop.X; + radiusY = keypoints.TopLeft.Y - keypoints.LeftTop.Y; + radiusX = radiusX > 0 ? radiusX : -radiusX; + radiusY = radiusY > 0 ? radiusY : -radiusY; + + if (radiusX != 0 || radiusY != 0) + { + context.ArcTo( + keypoints.TopLeft, + new Size(radiusX, radiusY), + rotationAngle: 0.0, + isLargeArc: false, + SweepDirection.Clockwise); + } + + context.EndFigure(isClosed: true); + } + + /// + /// Draws a new rounded rectangle within the given geometry context. + /// Warning: The caller must manage and dispose the externally. + /// + /// The geometry context to draw into. + /// The existing rectangle dimensions without corner radii. + /// The radius on the X-axis used to round the corners of the rectangle. + /// The radius on the Y-axis used to round the corners of the rectangle. + public static void DrawRoundedCornersRectangle( + StreamGeometryContext context, + Rect rect, + double radiusX, + double radiusY) + { + var arcSize = new Size(radiusX, radiusY); + + // The rectangle is constructed as follows: + // + // (origin) + // Corner 4 Corner 1 + // Top/Left Line 1 Top/Right + // \_ __________ _/ + // | | + // Line 4 | | Line 2 + // _ |__________| _ + // / Line 3 \ + // Corner 3 Corner 2 + // Bottom/Left Bottom/Right + // + // - Lines 1,3 follow the deflated rectangle bounds minus RadiusX + // - Lines 2,4 follow the deflated rectangle bounds minus RadiusY + // - All corners are constructed using elliptical arcs + + context.BeginFigure(new Point(rect.Left + radiusX, rect.Top), isFilled: true); + + // Line 1 + Corner 1 + context.LineTo(new Point(rect.Right - radiusX, rect.Top)); + context.ArcTo( + new Point(rect.Right, rect.Top + radiusY), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + // Line 2 + Corner 2 + context.LineTo(new Point(rect.Right, rect.Bottom - radiusY)); + context.ArcTo( + new Point(rect.Right - radiusX, rect.Bottom), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + // Line 3 + Corner 3 + context.LineTo(new Point(rect.Left + radiusX, rect.Bottom)); + context.ArcTo( + new Point(rect.Left, rect.Bottom - radiusY), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + // Line 4 + Corner 4 + context.LineTo(new Point(rect.Left, rect.Top + radiusY)); + context.ArcTo( + new Point(rect.Left + radiusX, rect.Top), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + context.EndFigure(isClosed: true); + } + + /// + /// Calculates the keypoints of a rounded rectangle based on the algorithm in WinUI. + /// These keypoints may then be drawn or transformed into other types. + /// + /// The outer bounds of the rounded rectangle. + /// This should be the overall bounds and size of the shape/control without any + /// corner radii or border thickness adjustments. + /// The unadjusted border thickness of the rounded rectangle. + /// The unadjusted corner radii of the rounded rectangle. + /// The corner radius is defined to be the middle of the border stroke (center of the border). + /// The sizing mode used to calculate the final rounded rectangle size. + /// New rounded rectangle keypoints. + public static RoundedRectKeypoints CalculateRoundedCornersRectangleWinUI( + Rect outerBounds, + Thickness borderThickness, + CornerRadius cornerRadius, + BackgroundSizing sizing) + { + // This was initially derived from WinUI: + // - CGeometryBuilder::CalculateRoundedCornersRectangle + // https://github.com/microsoft/microsoft-ui-xaml/blob/93742a178db8f625ba9299f62c21f656e0b195ad/dxaml/xcp/core/core/elements/geometry.cpp#L862-L869 + // + // It has been modified to accept a BackgroundSizing parameter directly as well + // as to support BackgroundSizing.CenterBorder. + // + // Keep in mind: + // > In Xaml, the corner radius is defined to be the middle of the stroke + // > (i.e. half the border thickness extends to either side). + + bool fOuter; + Rect boundRect = outerBounds; + + if (sizing == BackgroundSizing.InnerBorderEdge) + { + boundRect = outerBounds.Deflate(borderThickness); + fOuter = false; + } + else if (sizing == BackgroundSizing.OuterBorderEdge) + { + fOuter = true; + } + else // CenterBorder + { + // This is a trick to support a 3rd state (CenterBorder) using the same WinUI-based algorithm. + // The WinUI algorithm only supports the fOuter = True|False parameter. + boundRect = outerBounds.Deflate(borderThickness * 0.5); + fOuter = false; + } + + // Start of WinUI converted code + // WinUI's Point struct fields can be modified directly, Avalonia's Point is read-only. + // Therefore, we will use doubles for calculation so multiple Point structs aren't + // required during calculations -- everything can be done with these double variables. + double fLeftTop; + double fLeftBottom; + double fTopLeft; + double fTopRight; + double fRightTop; + double fRightBottom; + double fBottomLeft; + double fBottomRight; + + double left; + double right; + double top; + double bottom; + + // If the caller wants to take the border into account + // initialize the borders variables + if (borderThickness != default) + { + left = 0.5 * borderThickness.Left; + right = 0.5 * borderThickness.Right; + top = 0.5 * borderThickness.Top; + bottom = 0.5 * borderThickness.Bottom; + } + else + { + left = 0.0; + right = 0.0; + top = 0.0; + bottom = 0.0; + } + + // The following if/else block initializes the variables + // of which the points of the path will be created + // In case of outer, add the border - if any. + // Otherwise (inner rectangle) subtract the border - if any + if (fOuter) + { + if (MathUtilities.AreClose(cornerRadius.TopLeft, 0.0, Epsilon)) + { + fLeftTop = 0.0; + fTopLeft = 0.0; + } + else + { + fLeftTop = cornerRadius.TopLeft + left; + fTopLeft = cornerRadius.TopLeft + top; + } + + if (MathUtilities.AreClose(cornerRadius.TopRight, 0.0, Epsilon)) + { + fTopRight = 0.0; + fRightTop = 0.0; + } + else + { + fTopRight = cornerRadius.TopRight + top; + fRightTop = cornerRadius.TopRight + right; + } + + if (MathUtilities.AreClose(cornerRadius.BottomRight, 0.0, Epsilon)) + { + fRightBottom = 0.0; + fBottomRight = 0.0; + } + else + { + fRightBottom = cornerRadius.BottomRight + right; + fBottomRight = cornerRadius.BottomRight + bottom; + } + + if (MathUtilities.AreClose(cornerRadius.BottomLeft, 0.0, Epsilon)) + { + fBottomLeft = 0.0; + fLeftBottom = 0.0; + } + else + { + fBottomLeft = cornerRadius.BottomLeft + bottom; + fLeftBottom = cornerRadius.BottomLeft + left; + } + } + else + { + fLeftTop = Math.Max(0.0, cornerRadius.TopLeft - left); + fTopLeft = Math.Max(0.0, cornerRadius.TopLeft - top); + fTopRight = Math.Max(0.0, cornerRadius.TopRight - top); + fRightTop = Math.Max(0.0, cornerRadius.TopRight - right); + fRightBottom = Math.Max(0.0, cornerRadius.BottomRight - right); + fBottomRight = Math.Max(0.0, cornerRadius.BottomRight - bottom); + fBottomLeft = Math.Max(0.0, cornerRadius.BottomLeft - bottom); + fLeftBottom = Math.Max(0.0, cornerRadius.BottomLeft - left); + } + + double topLeftX = fLeftTop; + double topLeftY = 0; + + double topRightX = boundRect.Width - fRightTop; + double topRightY = 0; + + double rightTopX = boundRect.Width; + double rightTopY = fTopRight; + + double rightBottomX = boundRect.Width; + double rightBottomY = boundRect.Height - fBottomRight; + + double bottomRightX = boundRect.Width - fRightBottom; + double bottomRightY = boundRect.Height; + + double bottomLeftX = fLeftBottom; + double bottomLeftY = boundRect.Height; + + double leftBottomX = 0; + double leftBottomY = boundRect.Height - fBottomLeft; + + double leftTopX = 0; + double leftTopY = fTopLeft; + + // check keypoints for overlap and resolve by partitioning radii according to + // the percentage of each one. + + // top edge + if (topLeftX > topRightX) + { + double v = (fLeftTop) / (fLeftTop + fRightTop) * boundRect.Width; + topLeftX = v; + topRightX = v; + } + // right edge + if (rightTopY > rightBottomY) + { + double v = (fTopRight) / (fTopRight + fBottomRight) * boundRect.Height; + rightTopY = v; + rightBottomY = v; + } + // bottom edge + if (bottomRightX < bottomLeftX) + { + double v = (fLeftBottom) / (fLeftBottom + fRightBottom) * boundRect.Width; + bottomRightX = v; + bottomLeftX = v; + } + // left edge + if (leftBottomY < leftTopY) + { + double v = (fTopLeft) / (fTopLeft + fBottomLeft) * boundRect.Height; + leftBottomY = v; + leftTopY = v; + } + + // The above code does all calculations without taking into consideration X/Y absolute position. + // In WinUI, this is compensated for in DrawRoundedCornersRectangle(); however, we do this here directly + // when the final keypoints are being created. + var keypoints = new RoundedRectKeypoints(); + keypoints.TopLeft = new Point( + boundRect.X + topLeftX, + boundRect.Y + topLeftY); + keypoints.TopRight = new Point( + boundRect.X + topRightX, + boundRect.Y + topRightY); + + keypoints.RightTop = new Point( + boundRect.X + rightTopX, + boundRect.Y + rightTopY); + keypoints.RightBottom = new Point( + boundRect.X + rightBottomX, + boundRect.Y + rightBottomY); + + keypoints.BottomRight = new Point( + boundRect.X + bottomRightX, + boundRect.Y + bottomRightY); + keypoints.BottomLeft = new Point( + boundRect.X + bottomLeftX, + boundRect.Y + bottomLeftY); + + keypoints.LeftBottom = new Point( + boundRect.X + leftBottomX, + boundRect.Y + leftBottomY); + keypoints.LeftTop = new Point( + boundRect.X + leftTopX, + boundRect.Y + leftTopY); + + return keypoints; + } + + /// + /// Represents the keypoints of a rounded rectangle. + /// These keypoints can be shared between methods and turned into geometry. + /// + /// + /// A rounded rectangle is the base geometric shape used when drawing borders. + /// It is a superset of a simple rectangle (which has corner radii set to zero). + /// These keypoints can be combined together to produce geometries for both background + /// and border elements. + /// + internal struct RoundedRectKeypoints + { + // The following keypoints are defined for a rounded rectangle: + // + // TopLeft TopRight + // *--------------------------------* + // (start) / \ + // LeftTop * * RightTop + // | | + // | | + // LeftBottom * * RightBottom + // \ / + // *--------------------------------* + // BottomLeft BottomRight + // + // Or, for a simple rectangle without corner radii: + // + // TopLeft = LeftTop TopRight = RightTop + // (start) *------------------------------------* + // | | + // | | + // *------------------------------------* + // BottomLeft = LeftBottom BottomRight = RightBottom + + /// + /// Initializes a new instance of the struct. + /// + public RoundedRectKeypoints() + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// An existing to initialize keypoints with. + public RoundedRectKeypoints(RoundedRect roundedRect) + { + LeftTop = new Point( + roundedRect.Rect.TopLeft.X, + roundedRect.Rect.TopLeft.Y + roundedRect.RadiiTopLeft.Y); + TopLeft = new Point( + roundedRect.Rect.TopLeft.X + roundedRect.RadiiTopLeft.X, + roundedRect.Rect.TopLeft.Y); + TopRight = new Point( + roundedRect.Rect.TopRight.X - roundedRect.RadiiTopRight.X, + roundedRect.Rect.TopRight.Y); + RightTop = new Point( + roundedRect.Rect.TopRight.X, + roundedRect.Rect.TopRight.Y + roundedRect.RadiiTopRight.Y); + RightBottom = new Point( + roundedRect.Rect.BottomRight.X, + roundedRect.Rect.BottomRight.Y - roundedRect.RadiiBottomRight.Y); + BottomRight = new Point( + roundedRect.Rect.BottomRight.X - roundedRect.RadiiBottomRight.X, + roundedRect.Rect.BottomRight.Y); + BottomLeft = new Point( + roundedRect.Rect.BottomLeft.X + roundedRect.RadiiBottomLeft.X, + roundedRect.Rect.BottomLeft.Y); + LeftBottom = new Point( + roundedRect.Rect.BottomLeft.X, + roundedRect.Rect.BottomRight.Y - roundedRect.RadiiBottomLeft.Y); + } + + /// + /// Gets the topmost point in the left line segment of the rectangle. + /// + public Point LeftTop { get; set; } + + /// + /// Gets the leftmost point in the top line segment of the rectangle. + /// + public Point TopLeft { get; set; } + + /// + /// Gets the rightmost point in the top line segment of the rectangle. + /// + public Point TopRight { get; set; } + + /// + /// Gets the topmost point in the right line segment of the rectangle. + /// + public Point RightTop { get; set; } + + /// + /// Gets the bottommost point in the right line segment of the rectangle. + /// + public Point RightBottom { get; set; } + + /// + /// Gets the rightmost point in the bottom line segment of the rectangle. + /// + public Point BottomRight { get; set; } + + /// + /// Gets the leftmost point in the bottom line segment of the rectangle. + /// + public Point BottomLeft { get; set; } + + /// + /// Gets the bottommost point in the left line segment of the rectangle. + /// + public Point LeftBottom { get; set; } + + /// + /// Gets a value indicating whether the rounded rectangle is actually rounded on + /// any corner. If false the key points represent a simple rectangle. + /// + public bool IsRounded + { + get + { + return (TopLeft != LeftTop || + TopRight != RightTop || + BottomLeft != LeftBottom || + BottomRight != RightBottom); + } + } + + /// + /// Converts the keypoints into a simple rectangle (with no corners). + /// This is equivalent to the outer rectangle with zero corner radii. + /// + /// + /// Warning: This will force the keypoints into a simple rectangle without + /// any rounded corners. Use to determine if corner + /// information is otherwise available. + /// + /// A new rectangle representing the keypoints. + public Rect ToRect() + { + return new Rect( + topLeft: new Point( + x: LeftTop.X, + y: TopLeft.Y), + bottomRight: new Point( + x: RightBottom.X, + y: BottomRight.Y)); + } + + /// + /// Converts the keypoints into a rounded rectangle with elliptical corner radii. + /// + /// + /// Elliptical corner radius (represented by ) is more powerful + /// than circular corner radius (represented by a ). + /// Elliptical is a superset of circular. + /// + /// A new rounded rectangle representing the keypoints. + public RoundedRect ToRoundedRect() + { + return new RoundedRect( + ToRect(), + radiiTopLeft: new Vector( + x: TopLeft.X - LeftTop.X, + y: LeftTop.Y - TopLeft.Y), + radiiTopRight: new Vector( + x: RightTop.X - TopRight.X, + y: RightTop.Y - TopRight.Y), + radiiBottomRight: new Vector( + x: RightBottom.X - BottomRight.X, + y: BottomRight.Y - RightBottom.Y), + radiiBottomLeft: new Vector( + x: BottomLeft.X - LeftBottom.X, + y: BottomLeft.Y - LeftBottom.Y)); + } + } + } +} diff --git a/src/Avalonia.Base/Media/RectangleGeometry.cs b/src/Avalonia.Base/Media/RectangleGeometry.cs index 01771242a7..e97a240fc4 100644 --- a/src/Avalonia.Base/Media/RectangleGeometry.cs +++ b/src/Avalonia.Base/Media/RectangleGeometry.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.Platform; namespace Avalonia.Media @@ -8,6 +7,18 @@ namespace Avalonia.Media /// public class RectangleGeometry : Geometry { + /// + /// Defines the property. + /// + public static readonly StyledProperty RadiusXProperty = + AvaloniaProperty.Register(nameof(RadiusX)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RadiusYProperty = + AvaloniaProperty.Register(nameof(RadiusY)); + /// /// Defines the property. /// @@ -16,7 +27,10 @@ namespace Avalonia.Media static RectangleGeometry() { - AffectsGeometry(RectProperty); + AffectsGeometry( + RadiusXProperty, + RadiusYProperty, + RectProperty); } /// @@ -35,6 +49,47 @@ namespace Avalonia.Media Rect = rect; } + /// + /// Initializes a new instance of the class. + /// + /// The rectangle bounds. + /// The radius on the X-axis used to round the corners of the rectangle. + /// The radius on the Y-axis used to round the corners of the rectangle. + public RectangleGeometry(Rect rect, double radiusX, double radiusY) + { + Rect = rect; + RadiusX = radiusX; + RadiusY = radiusY; + } + + /// + /// Gets or sets the radius on the X-axis used to round the corners of the rectangle. + /// Corner radii are represented by an ellipse so this is the X-axis width of the ellipse. + /// + /// + /// In order for this property to be used, must not be set + /// (equal to the default value). + /// + public double RadiusX + { + get => GetValue(RadiusXProperty); + set => SetValue(RadiusXProperty, value); + } + + /// + /// Gets or sets the radius on the Y-axis used to round the corners of the rectangle. + /// Corner radii are represented by an ellipse so this is the Y-axis height of the ellipse. + /// + /// + /// In order for this property to be used, must not be set + /// (equal to the default value). + /// + public double RadiusY + { + get => GetValue(RadiusYProperty); + set => SetValue(RadiusYProperty, value); + } + /// /// Gets or sets the bounds of the rectangle. /// @@ -49,9 +104,25 @@ namespace Avalonia.Media private protected sealed override IGeometryImpl? CreateDefiningGeometry() { + double radiusX = RadiusX; + double radiusY = RadiusY; var factory = AvaloniaLocator.Current.GetRequiredService(); - return factory.CreateRectangleGeometry(Rect); + if (radiusX == 0 && radiusY == 0) + { + // Optimization when there are no corner radii + return factory.CreateRectangleGeometry(Rect); + } + else + { + var geometry = factory.CreateStreamGeometry(); + using (var ctx = new StreamGeometryContext(geometry.Open())) + { + GeometryBuilder.DrawRoundedCornersRectangle(ctx, Rect, radiusX, radiusY); + } + + return geometry; + } } } } diff --git a/src/Avalonia.Base/RoundedRect.cs b/src/Avalonia.Base/RoundedRect.cs index 0b23a8c0ca..c39dbd1b1f 100644 --- a/src/Avalonia.Base/RoundedRect.cs +++ b/src/Avalonia.Base/RoundedRect.cs @@ -36,8 +36,13 @@ namespace Avalonia public Vector RadiiTopRight { get; } public Vector RadiiBottomLeft { get; } public Vector RadiiBottomRight { get; } - - public RoundedRect(Rect rect, Vector radiiTopLeft, Vector radiiTopRight, Vector radiiBottomRight, Vector radiiBottomLeft) + + public RoundedRect( + Rect rect, + Vector radiiTopLeft, + Vector radiiTopRight, + Vector radiiBottomRight, + Vector radiiBottomLeft) { Rect = rect; RadiiTopLeft = radiiTopLeft; @@ -46,7 +51,11 @@ namespace Avalonia RadiiBottomLeft = radiiBottomLeft; } - public RoundedRect(Rect rect, double radiusTopLeft, double radiusTopRight, double radiusBottomRight, + public RoundedRect( + Rect rect, + double radiusTopLeft, + double radiusTopRight, + double radiusBottomRight, double radiusBottomLeft) : this(rect, new Vector(radiusTopLeft, radiusTopLeft), @@ -55,34 +64,28 @@ namespace Avalonia new Vector(radiusBottomLeft, radiusBottomLeft) ) { - } public RoundedRect(Rect rect, Vector radii) : this(rect, radii, radii, radii, radii) { - } public RoundedRect(Rect rect, double radiusX, double radiusY) : this(rect, new Vector(radiusX, radiusY)) { - } public RoundedRect(Rect rect, double radius) : this(rect, radius, radius) { - } public RoundedRect(Rect rect) : this(rect, 0) { - } public RoundedRect(in Rect bounds, in CornerRadius radius) : this(bounds, radius.TopLeft, radius.TopRight, radius.BottomRight, radius.BottomLeft) { - } public static implicit operator RoundedRect(Rect r) => new RoundedRect(r); @@ -99,7 +102,7 @@ namespace Avalonia { return Deflate(-dx, -dy); } - + public unsafe RoundedRect Deflate(double dx, double dy) { if (!IsRounded) @@ -143,7 +146,7 @@ namespace Avalonia return new RoundedRect(new Rect(left, top, right - left, bottom - top), radii[0], radii[1], radii[2], radii[3]); } - + /// /// This method should be used internally to check for the rect emptiness /// Once we add support for WPF-like empty rects, there will be an actual implementation diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index e7373a813e..4eb0274ec7 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -23,6 +23,14 @@ namespace Avalonia.Controls public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background)); + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundSizingProperty = + AvaloniaProperty.Register( + nameof(BackgroundSizing), + BackgroundSizing.CenterBorder); + /// /// Defines the property. /// @@ -59,6 +67,7 @@ namespace Avalonia.Controls { AffectsRender( BackgroundProperty, + BackgroundSizingProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty, @@ -91,6 +100,15 @@ namespace Avalonia.Controls set => SetValue(BackgroundProperty, value); } + /// + /// Gets or sets how the background is drawn relative to the border. + /// + public BackgroundSizing BackgroundSizing + { + get => GetValue(BackgroundSizingProperty); + set => SetValue(BackgroundSizingProperty, value); + } + /// /// Gets or sets a brush with which to paint the border. /// @@ -168,6 +186,7 @@ namespace Avalonia.Controls Bounds.Size, LayoutThickness, CornerRadius, + BackgroundSizing, Background, BorderBrush, BoxShadow); diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 4d32658e0d..cf3f1c845a 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -47,6 +47,7 @@ namespace Avalonia.Controls { Name = "PART_ContentPresenter", [~BackgroundProperty] = new TemplateBinding(BackgroundProperty), + [~BackgroundSizingProperty] = new TemplateBinding(BackgroundSizingProperty), [~BorderBrushProperty] = new TemplateBinding(BorderBrushProperty), [~BorderThicknessProperty] = new TemplateBinding(BorderThicknessProperty), [~CornerRadiusProperty] = new TemplateBinding(CornerRadiusProperty), diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 02a8fb7d2c..76a882df12 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -25,6 +25,12 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundSizingProperty = + Border.BackgroundSizingProperty.AddOwner(); + /// /// Defines the property. /// @@ -171,10 +177,12 @@ namespace Avalonia.Controls.Presenters { AffectsRender( BackgroundProperty, + BackgroundSizingProperty, BorderBrushProperty, BorderThicknessProperty, - CornerRadiusProperty, - BoxShadowProperty); + BoxShadowProperty, + CornerRadiusProperty); + AffectsArrange(HorizontalContentAlignmentProperty, VerticalContentAlignmentProperty); AffectsMeasure(BorderThicknessProperty, PaddingProperty); } @@ -184,45 +192,42 @@ namespace Avalonia.Controls.Presenters UpdatePseudoClasses(); } - /// - /// Gets or sets a brush with which to paint the background. - /// + /// public IBrush? Background { get => GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); } - /// - /// Gets or sets a brush with which to paint the border. - /// + /// + public BackgroundSizing BackgroundSizing + { + get => GetValue(BackgroundSizingProperty); + set => SetValue(BackgroundSizingProperty, value); + } + + /// public IBrush? BorderBrush { get => GetValue(BorderBrushProperty); set => SetValue(BorderBrushProperty, value); } - /// - /// Gets or sets the thickness of the border. - /// + /// public Thickness BorderThickness { get => GetValue(BorderThicknessProperty); set => SetValue(BorderThicknessProperty, value); } - /// - /// Gets or sets the radius of the border rounded corners. - /// + /// public CornerRadius CornerRadius { get => GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } - /// - /// Gets or sets the box shadow effect parameters - /// + /// public BoxShadows BoxShadow { get => GetValue(BoxShadowProperty); @@ -541,7 +546,14 @@ namespace Avalonia.Controls.Presenters /// public sealed override void Render(DrawingContext context) { - _borderRenderer.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, + _borderRenderer.Render( + context, + Bounds.Size, + LayoutThickness, + CornerRadius, + BackgroundSizing, + Background, + BorderBrush, BoxShadow); } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index c232fdcc12..1fef382159 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -22,6 +22,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundSizingProperty = + Border.BackgroundSizingProperty.AddOwner(); + /// /// Defines the property. /// @@ -131,6 +137,15 @@ namespace Avalonia.Controls.Primitives set => SetValue(BackgroundProperty, value); } + /// + /// Gets or sets how the control's background is drawn relative to the control's border. + /// + public BackgroundSizing BackgroundSizing + { + get => GetValue(BackgroundSizingProperty); + set => SetValue(BackgroundSizingProperty, value); + } + /// /// Gets or sets the brush used to draw the control's border. /// diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index d60180bab4..cfccf0d588 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -7,8 +7,6 @@ namespace Avalonia.Controls.Shapes /// public class Rectangle : Shape { - private const double PiOver2 = 1.57079633; // 90 deg to rad - /// /// Defines the property. /// @@ -30,20 +28,14 @@ namespace Avalonia.Controls.Shapes StrokeThicknessProperty); } - /// - /// Gets or sets the radius on the X-axis used to round the corners of the rectangle. - /// Corner radii are represented by an ellipse so this is the X-axis width of the ellipse. - /// + /// public double RadiusX { get => GetValue(RadiusXProperty); set => SetValue(RadiusXProperty, value); } - /// - /// Gets or sets the radius on the Y-axis used to round the corners of the rectangle. - /// Corner radii are represented by an ellipse so this is the Y-axis height of the ellipse. - /// + /// public double RadiusY { get => GetValue(RadiusYProperty); @@ -53,85 +45,9 @@ namespace Avalonia.Controls.Shapes /// protected override Geometry CreateDefiningGeometry() { - // TODO: If RectangleGeometry ever supports RadiusX/Y like in WPF, - // this code can be removed/combined with that implementation - - double x = RadiusX; - double y = RadiusY; - - if (x == 0 && y == 0) - { - // Optimization when there are no corner radii - var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); - return new RectangleGeometry(rect); - } - else - { - var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); - var geometry = new StreamGeometry(); - var arcSize = new Size(x, y); - - using (StreamGeometryContext context = geometry.Open()) - { - // The rectangle is constructed as follows: - // - // (origin) - // Corner 4 Corner 1 - // Top/Left Line 1 Top/Right - // \_ __________ _/ - // | | - // Line 4 | | Line 2 - // _ |__________| _ - // / Line 3 \ - // Corner 3 Corner 2 - // Bottom/Left Bottom/Right - // - // - Lines 1,3 follow the deflated rectangle bounds minus RadiusX - // - Lines 2,4 follow the deflated rectangle bounds minus RadiusY - // - All corners are constructed using elliptical arcs - - // Line 1 + Corner 1 - context.BeginFigure(new Point(rect.Left + x, rect.Top), true); - context.LineTo(new Point(rect.Right - x, rect.Top)); - context.ArcTo( - new Point(rect.Right, rect.Top + y), - arcSize, - rotationAngle: PiOver2, - isLargeArc: false, - SweepDirection.Clockwise); - - // Line 2 + Corner 2 - context.LineTo(new Point(rect.Right, rect.Bottom - y)); - context.ArcTo( - new Point(rect.Right - x, rect.Bottom), - arcSize, - rotationAngle: PiOver2, - isLargeArc: false, - SweepDirection.Clockwise); - - // Line 3 + Corner 3 - context.LineTo(new Point(rect.Left + x, rect.Bottom)); - context.ArcTo( - new Point(rect.Left, rect.Bottom - y), - arcSize, - rotationAngle: PiOver2, - isLargeArc: false, - SweepDirection.Clockwise); - - // Line 4 + Corner 4 - context.LineTo(new Point(rect.Left, rect.Top + y)); - context.ArcTo( - new Point(rect.Left + x, rect.Top), - arcSize, - rotationAngle: PiOver2, - isLargeArc: false, - SweepDirection.Clockwise); - - context.EndFigure(true); - } + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); - return geometry; - } + return new RectangleGeometry(rect, RadiusX, RadiusY); } /// diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 4df4fd738a..f86ddb0d2d 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -1,35 +1,38 @@ -using System; -using Avalonia.Collections; -using Avalonia.Media; -using Avalonia.Media.Immutable; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; namespace Avalonia.Controls.Utils { + /// + /// Contains helper methods for rendering a 's background and border to a given context. + /// internal class BorderRenderHelper { private bool _useComplexRendering; private bool? _backendSupportsIndividualCorners; - private StreamGeometry? _backgroundGeometryCache; - private StreamGeometry? _borderGeometryCache; + private Geometry? _backgroundGeometryCache; + private Geometry? _borderGeometryCache; private Size _size; private Thickness _borderThickness; private CornerRadius _cornerRadius; + private BackgroundSizing _backgroundSizing; private bool _initialized; private IPen? _cachedPen; - - void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius) + private void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius, BackgroundSizing backgroundSizing) { _backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetRequiredService() .SupportsIndividualRoundRects; _size = finalSize; _borderThickness = borderThickness; _cornerRadius = cornerRadius; + _backgroundSizing = backgroundSizing; _initialized = true; - if (borderThickness.IsUniform && (cornerRadius.IsUniform || _backendSupportsIndividualCorners == true)) + if (borderThickness.IsUniform && + (cornerRadius.IsUniform || _backendSupportsIndividualCorners == true) && + backgroundSizing == BackgroundSizing.CenterBorder) { _backgroundGeometryCache = null; _borderGeometryCache = null; @@ -41,17 +44,19 @@ namespace Avalonia.Controls.Utils var boundRect = new Rect(finalSize); var innerRect = boundRect.Deflate(borderThickness); - BorderGeometryKeypoints? backgroundKeypoints = null; - StreamGeometry? backgroundGeometry = null; if (innerRect.Width != 0 && innerRect.Height != 0) { - backgroundGeometry = new StreamGeometry(); - backgroundKeypoints = new BorderGeometryKeypoints(innerRect, borderThickness, cornerRadius, true); + var backgroundOuterKeypoints = GeometryBuilder.CalculateRoundedCornersRectangleWinUI( + boundRect, + borderThickness, + cornerRadius, + backgroundSizing); + var backgroundGeometry = new StreamGeometry(); using (var ctx = backgroundGeometry.Open()) { - CreateGeometry(ctx, innerRect, backgroundKeypoints); + GeometryBuilder.DrawRoundedCornersRectangle(ctx, ref backgroundOuterKeypoints); } _backgroundGeometryCache = backgroundGeometry; @@ -63,21 +68,30 @@ namespace Avalonia.Controls.Utils if (boundRect.Width != 0 && boundRect.Height != 0) { - var borderGeometryKeypoints = - new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false); - var borderGeometry = new StreamGeometry(); - - using (var ctx = borderGeometry.Open()) + var borderInnerKeypoints = GeometryBuilder.CalculateRoundedCornersRectangleWinUI( + boundRect, + borderThickness, + cornerRadius, + BackgroundSizing.InnerBorderEdge); + var borderOuterKeypoints = GeometryBuilder.CalculateRoundedCornersRectangleWinUI( + boundRect, + borderThickness, + cornerRadius, + BackgroundSizing.OuterBorderEdge); + + var borderInnerGeometry = new StreamGeometry(); + using (var ctx = borderInnerGeometry.Open()) { - CreateGeometry(ctx, boundRect, borderGeometryKeypoints); + GeometryBuilder.DrawRoundedCornersRectangle(ctx, ref borderInnerKeypoints); + } - if (backgroundGeometry != null) - { - CreateGeometry(ctx, innerRect, backgroundKeypoints!); - } + var borderOuterGeometry = new StreamGeometry(); + using (var ctx = borderOuterGeometry.Open()) + { + GeometryBuilder.DrawRoundedCornersRectangle(ctx, ref borderOuterKeypoints); } - _borderGeometryCache = borderGeometry; + _borderGeometryCache = new CombinedGeometry(GeometryCombineMode.Exclude, borderOuterGeometry, borderInnerGeometry); } else { @@ -86,211 +100,51 @@ namespace Avalonia.Controls.Utils } } - public void Render(DrawingContext context, - Size finalSize, Thickness borderThickness, CornerRadius cornerRadius, - IBrush? background, IBrush? borderBrush, BoxShadows boxShadows) + public void Render( + DrawingContext context, + Size finalSize, + Thickness borderThickness, + CornerRadius cornerRadius, + BackgroundSizing backgroundSizing, + IBrush? background, + IBrush? borderBrush, + BoxShadows boxShadows) { if (_size != finalSize || _borderThickness != borderThickness || _cornerRadius != cornerRadius + || _backgroundSizing != backgroundSizing || !_initialized) - Update(finalSize, borderThickness, cornerRadius); - RenderCore(context, background, borderBrush, boxShadows); - } + { + Update(finalSize, borderThickness, cornerRadius, backgroundSizing); + } - void RenderCore(DrawingContext context, IBrush? background, IBrush? borderBrush, BoxShadows boxShadows) - { if (_useComplexRendering) { - var backgroundGeometry = _backgroundGeometryCache; - if (backgroundGeometry != null) + if (_backgroundGeometryCache != null) { - context.DrawGeometry(background, null, backgroundGeometry); + context.DrawGeometry(background, null, _backgroundGeometryCache); } - var borderGeometry = _borderGeometryCache; - if (borderGeometry != null) + if (_borderGeometryCache != null) { - context.DrawGeometry(borderBrush, null, borderGeometry); + context.DrawGeometry(borderBrush, null, _borderGeometryCache); } } else { - var borderThickness = _borderThickness.Top; + var thickness = _borderThickness.Top; - Pen.TryModifyOrCreate(ref _cachedPen, borderBrush, borderThickness); + Pen.TryModifyOrCreate(ref _cachedPen, borderBrush, thickness); var rect = new Rect(_size); - if (!MathUtilities.IsZero(borderThickness)) - rect = rect.Deflate(borderThickness * 0.5); + if (!MathUtilities.IsZero(thickness)) + rect = rect.Deflate(thickness * 0.5); var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight, _cornerRadius.BottomRight, _cornerRadius.BottomLeft); context.DrawRectangle(background, _cachedPen, rrect, boxShadows); } } - - private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, - BorderGeometryKeypoints keypoints) - { - context.BeginFigure(keypoints.TopLeft, true); - - // Top - context.LineTo(keypoints.TopRight); - - // TopRight corner - var radiusX = boundRect.TopRight.X - keypoints.TopRight.X; - var radiusY = keypoints.RightTop.Y - boundRect.TopRight.Y; - if (radiusX != 0 || radiusY != 0) - { - context.ArcTo(keypoints.RightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); - } - - // Right - context.LineTo(keypoints.RightBottom); - - // BottomRight corner - radiusX = boundRect.BottomRight.X - keypoints.BottomRight.X; - radiusY = boundRect.BottomRight.Y - keypoints.RightBottom.Y; - if (radiusX != 0 || radiusY != 0) - { - context.ArcTo(keypoints.BottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); - } - - // Bottom - context.LineTo(keypoints.BottomLeft); - - // BottomLeft corner - radiusX = keypoints.BottomLeft.X - boundRect.BottomLeft.X; - radiusY = boundRect.BottomLeft.Y - keypoints.LeftBottom.Y; - if (radiusX != 0 || radiusY != 0) - { - context.ArcTo(keypoints.LeftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); - } - - // Left - context.LineTo(keypoints.LeftTop); - - // TopLeft corner - radiusX = keypoints.TopLeft.X - boundRect.TopLeft.X; - radiusY = keypoints.LeftTop.Y - boundRect.TopLeft.Y; - - if (radiusX != 0 || radiusY != 0) - { - context.ArcTo(keypoints.TopLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); - } - - context.EndFigure(true); - } - - private class BorderGeometryKeypoints - { - internal BorderGeometryKeypoints(Rect boundRect, Thickness borderThickness, CornerRadius cornerRadius, - bool inner) - { - var left = 0.5 * borderThickness.Left; - var top = 0.5 * borderThickness.Top; - var right = 0.5 * borderThickness.Right; - var bottom = 0.5 * borderThickness.Bottom; - - double leftTopY; - double topLeftX; - double topRightX; - double rightTopY; - double rightBottomY; - double bottomRightX; - double bottomLeftX; - double leftBottomY; - - if (inner) - { - leftTopY = Math.Max(0, cornerRadius.TopLeft - top) + boundRect.TopLeft.Y; - topLeftX = Math.Max(0, cornerRadius.TopLeft - left) + boundRect.TopLeft.X; - topRightX = boundRect.Width - Math.Max(0, cornerRadius.TopRight - top) + boundRect.TopLeft.X; - rightTopY = Math.Max(0, cornerRadius.TopRight - right) + boundRect.TopLeft.Y; - rightBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomRight - bottom) + - boundRect.TopLeft.Y; - bottomRightX = boundRect.Width - Math.Max(0, cornerRadius.BottomRight - right) + - boundRect.TopLeft.X; - bottomLeftX = Math.Max(0, cornerRadius.BottomLeft - left) + boundRect.TopLeft.X; - leftBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomLeft - bottom) + - boundRect.TopLeft.Y; - } - else - { - leftTopY = cornerRadius.TopLeft + top + boundRect.TopLeft.Y; - topLeftX = cornerRadius.TopLeft + left + boundRect.TopLeft.X; - topRightX = boundRect.Width - (cornerRadius.TopRight + right) + boundRect.TopLeft.X; - rightTopY = cornerRadius.TopRight + top + boundRect.TopLeft.Y; - rightBottomY = boundRect.Height - (cornerRadius.BottomRight + bottom) + boundRect.TopLeft.Y; - bottomRightX = boundRect.Width - (cornerRadius.BottomRight + right) + boundRect.TopLeft.X; - bottomLeftX = cornerRadius.BottomLeft + left + boundRect.TopLeft.X; - leftBottomY = boundRect.Height - (cornerRadius.BottomLeft + bottom) + boundRect.TopLeft.Y; - } - - var leftTopX = boundRect.TopLeft.X; - var topLeftY = boundRect.TopLeft.Y; - var topRightY = boundRect.TopLeft.Y; - var rightTopX = boundRect.Width + boundRect.TopLeft.X; - var rightBottomX = boundRect.Width + boundRect.TopLeft.X; - var bottomRightY = boundRect.Height + boundRect.TopLeft.Y; - var bottomLeftY = boundRect.Height + boundRect.TopLeft.Y; - var leftBottomX = boundRect.TopLeft.X; - - LeftTop = new Point(leftTopX, leftTopY); - TopLeft = new Point(topLeftX, topLeftY); - TopRight = new Point(topRightX, topRightY); - RightTop = new Point(rightTopX, rightTopY); - RightBottom = new Point(rightBottomX, rightBottomY); - BottomRight = new Point(bottomRightX, bottomRightY); - BottomLeft = new Point(bottomLeftX, bottomLeftY); - LeftBottom = new Point(leftBottomX, leftBottomY); - - // Fix overlap - if (TopLeft.X > TopRight.X) - { - var scaledX = topLeftX / (topLeftX + topRightX) * boundRect.Width; - TopLeft = new Point(scaledX, TopLeft.Y); - TopRight = new Point(scaledX, TopRight.Y); - } - - if (RightTop.Y > RightBottom.Y) - { - var scaledY = rightBottomY / (rightTopY + rightBottomY) * boundRect.Height; - RightTop = new Point(RightTop.X, scaledY); - RightBottom = new Point(RightBottom.X, scaledY); - } - - if (BottomRight.X < BottomLeft.X) - { - var scaledX = bottomLeftX / (bottomLeftX + bottomRightX) * boundRect.Width; - BottomRight = new Point(scaledX, BottomRight.Y); - BottomLeft = new Point(scaledX, BottomLeft.Y); - } - - if (LeftBottom.Y < LeftTop.Y) - { - var scaledY = leftTopY / (leftTopY + leftBottomY) * boundRect.Height; - LeftBottom = new Point(LeftBottom.X, scaledY); - LeftTop = new Point(LeftTop.X, scaledY); - } - } - - internal Point LeftTop { get; } - - internal Point TopLeft { get; } - - internal Point TopRight { get; } - - internal Point RightTop { get; } - - internal Point RightBottom { get; } - - internal Point BottomRight { get; } - - internal Point BottomLeft { get; } - - internal Point LeftBottom { get; } - } } } diff --git a/tests/Avalonia.Base.UnitTests/Media/GeometryBuilderTests.cs b/tests/Avalonia.Base.UnitTests/Media/GeometryBuilderTests.cs new file mode 100644 index 0000000000..a7d390bffe --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/GeometryBuilderTests.cs @@ -0,0 +1,61 @@ +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media +{ + public class GeometryBuilderTests + { + [Theory] + [InlineData(20.0, 10.0)] + [InlineData(10.0, 5.0)] + [InlineData(2.0, 1.0)] + [InlineData(1.0, 0.0)] + public void CalculateRoundedCornersRectangleWinUI_InnerBorderEdge_Borders_Larger_Than_Corners_Test( + double uniformBorders, + double uniformCorners) + { + var bounds = new Rect(new Size(100, 100)); + var borderThickness = new Thickness(uniformBorders); + var cornerRadius = new CornerRadius(uniformCorners); + + var points = GeometryBuilder.CalculateRoundedCornersRectangleWinUI(bounds, borderThickness, cornerRadius, BackgroundSizing.InnerBorderEdge); + + Assert.Equal(new Point(uniformBorders, uniformBorders), points.LeftTop); + Assert.Equal(new Point(uniformBorders, uniformBorders), points.TopLeft); + Assert.Equal(new Point(100 - uniformBorders, uniformBorders), points.TopRight); + Assert.Equal(new Point(100 - uniformBorders, uniformBorders), points.RightTop); + Assert.Equal(new Point(100 - uniformBorders, 100 - uniformBorders), points.RightBottom); + Assert.Equal(new Point(100 - uniformBorders, 100 - uniformBorders), points.BottomRight); + Assert.Equal(new Point(uniformBorders, 100 - uniformBorders), points.BottomLeft); + Assert.Equal(new Point(uniformBorders, 100 - uniformBorders), points.LeftBottom); + + Assert.False(points.IsRounded); + } + + [Theory] + [InlineData(20.0, 10.0)] + [InlineData(10.0, 5.0)] + [InlineData(2.0, 1.0)] + public void CalculateRoundedCornersRectangleWinUI_OuterBorderEdge_Borders_Larger_Than_Corners_Test( + double uniformBorders, + double uniformCorners) + { + var bounds = new Rect(new Size(100, 100)); + var borderThickness = new Thickness(uniformBorders); + var cornerRadius = new CornerRadius(uniformCorners); + + var points = GeometryBuilder.CalculateRoundedCornersRectangleWinUI(bounds, borderThickness, cornerRadius, BackgroundSizing.OuterBorderEdge); + + Assert.Equal(new Point(0, uniformBorders), points.LeftTop); + Assert.Equal(new Point(uniformBorders, 0), points.TopLeft); + Assert.Equal(new Point(100 - uniformBorders, 0), points.TopRight); + Assert.Equal(new Point(100, uniformBorders), points.RightTop); + Assert.Equal(new Point(100, 100 - uniformBorders), points.RightBottom); + Assert.Equal(new Point(100 - uniformBorders, 100), points.BottomRight); + Assert.Equal(new Point(uniformBorders, 100), points.BottomLeft); + Assert.Equal(new Point(0, 100 - uniformBorders), points.LeftBottom); + + Assert.True(points.IsRounded); + } + } +}