src/Avalonia.Base/Media/GeometryBuilder.cs (358 lines of code) (raw):

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