Browse Source
* 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 <maxkatz6@outlook.com>pull/14399/head
committed by
GitHub
12 changed files with 946 additions and 330 deletions
@ -0,0 +1,38 @@ |
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Defines how a background is drawn relative to its border.
|
|||
/// </summary>
|
|||
public enum BackgroundSizing |
|||
{ |
|||
/// <summary>
|
|||
/// The background is drawn up to the inside edge of the border.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The background will never be drawn under the border itself and will not be visible
|
|||
/// underneath the border regardless of border transparency.
|
|||
/// </remarks>
|
|||
InnerBorderEdge = 0, |
|||
|
|||
/// <summary>
|
|||
/// The background is drawn completely to the outside edge of the border.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The background will be visible underneath the border if the border has transparency.
|
|||
/// </remarks>
|
|||
OuterBorderEdge = 1, |
|||
|
|||
/// <summary>
|
|||
/// The background is drawn to the midpoint (center) of the border.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The background will be visible underneath half of the border if the border has transparency.
|
|||
/// For this reason it is not recommended to use <see cref="CenterBorder"/> if transparency is involved.
|
|||
/// <br/><br/>
|
|||
/// This value does not exist in other XAML frameworks and only exists in Avalonia for backwards compatibility
|
|||
/// with legacy code. Before <see cref="BackgroundSizing"/> was added, Avalonia would always render using this
|
|||
/// value (Skia's default).
|
|||
/// </remarks>
|
|||
CenterBorder = 2, |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Contains internal helpers used to build and draw various geometries.
|
|||
/// </summary>
|
|||
internal class GeometryBuilder |
|||
{ |
|||
private const double PiOver2 = 1.57079633; // 90 deg to rad
|
|||
private const double Epsilon = 0.00000153; // Same as LayoutHelper.LayoutEpsilon
|
|||
|
|||
/// <summary>
|
|||
/// Draws a new rounded rectangle within the given geometry context.
|
|||
/// Warning: The caller must manage and dispose the <see cref="StreamGeometryContext"/> externally.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// WinUI: https://github.com/microsoft/microsoft-ui-xaml/blob/93742a178db8f625ba9299f62c21f656e0b195ad/dxaml/xcp/core/core/elements/geometry.cpp#L1072-L1079
|
|||
/// </remarks>
|
|||
/// <param name="context">The geometry context to draw into.</param>
|
|||
/// <param name="keypoints">The rounded rectangle keypoints defining the rectangle to draw.</param>
|
|||
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); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Draws a new rounded rectangle within the given geometry context.
|
|||
/// Warning: The caller must manage and dispose the <see cref="StreamGeometryContext"/> externally.
|
|||
/// </summary>
|
|||
/// <param name="context">The geometry context to draw into.</param>
|
|||
/// <param name="rect">The existing rectangle dimensions without corner radii.</param>
|
|||
/// <param name="radiusX">The radius on the X-axis used to round the corners of the rectangle.</param>
|
|||
/// <param name="radiusY">The radius on the Y-axis used to round the corners of the rectangle.</param>
|
|||
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); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculates the keypoints of a rounded rectangle based on the algorithm in WinUI.
|
|||
/// These keypoints may then be drawn or transformed into other types.
|
|||
/// </summary>
|
|||
/// <param name="outerBounds">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.</param>
|
|||
/// <param name="borderThickness">The unadjusted border thickness of the rounded rectangle.</param>
|
|||
/// <param name="cornerRadius">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).</param>
|
|||
/// <param name="sizing">The sizing mode used to calculate the final rounded rectangle size.</param>
|
|||
/// <returns>New rounded rectangle keypoints.</returns>
|
|||
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; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents the keypoints of a rounded rectangle.
|
|||
/// These keypoints can be shared between methods and turned into geometry.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 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.
|
|||
/// </remarks>
|
|||
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
|
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="RoundedRectKeypoints"/> struct.
|
|||
/// </summary>
|
|||
public RoundedRectKeypoints() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="RoundedRectKeypoints"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="roundedRect">An existing <see cref="RoundedRect"/> to initialize keypoints with.</param>
|
|||
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); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the topmost point in the left line segment of the rectangle.
|
|||
/// </summary>
|
|||
public Point LeftTop { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the leftmost point in the top line segment of the rectangle.
|
|||
/// </summary>
|
|||
public Point TopLeft { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the rightmost point in the top line segment of the rectangle.
|
|||
/// </summary>
|
|||
public Point TopRight { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the topmost point in the right line segment of the rectangle.
|
|||
/// </summary>
|
|||
public Point RightTop { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the bottommost point in the right line segment of the rectangle.
|
|||
/// </summary>
|
|||
public Point RightBottom { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the rightmost point in the bottom line segment of the rectangle.
|
|||
/// </summary>
|
|||
public Point BottomRight { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the leftmost point in the bottom line segment of the rectangle.
|
|||
/// </summary>
|
|||
public Point BottomLeft { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the bottommost point in the left line segment of the rectangle.
|
|||
/// </summary>
|
|||
public Point LeftBottom { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the rounded rectangle is actually rounded on
|
|||
/// any corner. If false the key points represent a simple rectangle.
|
|||
/// </summary>
|
|||
public bool IsRounded |
|||
{ |
|||
get |
|||
{ |
|||
return (TopLeft != LeftTop || |
|||
TopRight != RightTop || |
|||
BottomLeft != LeftBottom || |
|||
BottomRight != RightBottom); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts the keypoints into a simple rectangle (with no corners).
|
|||
/// This is equivalent to the outer rectangle with zero corner radii.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Warning: This will force the keypoints into a simple rectangle without
|
|||
/// any rounded corners. Use <see cref="IsRounded"/> to determine if corner
|
|||
/// information is otherwise available.
|
|||
/// </remarks>
|
|||
/// <returns>A new rectangle representing the keypoints.</returns>
|
|||
public Rect ToRect() |
|||
{ |
|||
return new Rect( |
|||
topLeft: new Point( |
|||
x: LeftTop.X, |
|||
y: TopLeft.Y), |
|||
bottomRight: new Point( |
|||
x: RightBottom.X, |
|||
y: BottomRight.Y)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts the keypoints into a rounded rectangle with elliptical corner radii.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Elliptical corner radius (represented by <see cref="Vector"/>) is more powerful
|
|||
/// than circular corner radius (represented by a <see cref="CornerRadius"/>).
|
|||
/// Elliptical is a superset of circular.
|
|||
/// </remarks>
|
|||
/// <returns>A new rounded rectangle representing the keypoints.</returns>
|
|||
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)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue