Browse Source

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 <maxkatz6@outlook.com>
pull/14399/head
robloo 3 years ago
committed by GitHub
parent
commit
2a9e7e9dcf
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 26
      samples/ControlCatalog/Pages/BorderPage.xaml
  2. 38
      src/Avalonia.Base/Media/BackgroundSizing.cs
  3. 608
      src/Avalonia.Base/Media/GeometryBuilder.cs
  4. 77
      src/Avalonia.Base/Media/RectangleGeometry.cs
  5. 25
      src/Avalonia.Base/RoundedRect.cs
  6. 19
      src/Avalonia.Controls/Border.cs
  7. 1
      src/Avalonia.Controls/ContentControl.cs
  8. 48
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  9. 15
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  10. 92
      src/Avalonia.Controls/Shapes/Rectangle.cs
  11. 266
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  12. 61
      tests/Avalonia.Base.UnitTests/Media/GeometryBuilderTests.cs

26
samples/ControlCatalog/Pages/BorderPage.xaml

@ -5,6 +5,9 @@
d:DesignWidth="400"
x:Class="ControlCatalog.Pages.BorderPage">
<StackPanel Orientation="Vertical" Spacing="4">
<StackPanel.Resources>
<SolidColorBrush x:Key="SemiTransparentSystemAccentBrush" Color="{DynamicResource SystemAccentColor}" Opacity="0.4" />
</StackPanel.Resources>
<TextBlock Classes="h2">A control which decorates a child with a border and background</TextBlock>
<StackPanel Orientation="Vertical"
@ -15,10 +18,25 @@
<TextBlock>Border</TextBlock>
</Border>
<Border Background="{DynamicResource SystemAccentColorDark1}"
BorderBrush="{DynamicResource SystemAccentColor}"
BorderThickness="4"
Padding="16">
<TextBlock>Border and Background</TextBlock>
BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}"
BackgroundSizing="CenterBorder"
BorderThickness="8"
Padding="12">
<TextBlock>Background And CenterBorder</TextBlock>
</Border>
<Border Background="{DynamicResource SystemAccentColorDark1}"
BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}"
BackgroundSizing="InnerBorderEdge"
BorderThickness="8"
Padding="12">
<TextBlock>Background And InnerBorder</TextBlock>
</Border>
<Border Background="{DynamicResource SystemAccentColorDark1}"
BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}"
BackgroundSizing="OuterBorderEdge"
BorderThickness="8"
Padding="12">
<TextBlock>Background And OuterBorderEdge</TextBlock>
</Border>
<Border BorderBrush="{DynamicResource SystemAccentColor}"
BorderThickness="4"

38
src/Avalonia.Base/Media/BackgroundSizing.cs

@ -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,
}
}

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

77
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
/// </summary>
public class RectangleGeometry : Geometry
{
/// <summary>
/// Defines the <see cref="RadiusX"/> property.
/// </summary>
public static readonly StyledProperty<double> RadiusXProperty =
AvaloniaProperty.Register<RectangleGeometry, double>(nameof(RadiusX));
/// <summary>
/// Defines the <see cref="RadiusY"/> property.
/// </summary>
public static readonly StyledProperty<double> RadiusYProperty =
AvaloniaProperty.Register<RectangleGeometry, double>(nameof(RadiusY));
/// <summary>
/// Defines the <see cref="Rect"/> property.
/// </summary>
@ -16,7 +27,10 @@ namespace Avalonia.Media
static RectangleGeometry()
{
AffectsGeometry(RectProperty);
AffectsGeometry(
RadiusXProperty,
RadiusYProperty,
RectProperty);
}
/// <summary>
@ -35,6 +49,47 @@ namespace Avalonia.Media
Rect = rect;
}
/// <summary>
/// Initializes a new instance of the <see cref="RectangleGeometry"/> class.
/// </summary>
/// <param name="rect">The rectangle bounds.</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 RectangleGeometry(Rect rect, double radiusX, double radiusY)
{
Rect = rect;
RadiusX = radiusX;
RadiusY = radiusY;
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// In order for this property to be used, <see cref="Rect"/> must not be set
/// (equal to the default <see cref="Avalonia.Rect"/> value).
/// </remarks>
public double RadiusX
{
get => GetValue(RadiusXProperty);
set => SetValue(RadiusXProperty, value);
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// In order for this property to be used, <see cref="Rect"/> must not be set
/// (equal to the default <see cref="Avalonia.Rect"/> value).
/// </remarks>
public double RadiusY
{
get => GetValue(RadiusYProperty);
set => SetValue(RadiusYProperty, value);
}
/// <summary>
/// Gets or sets the bounds of the rectangle.
/// </summary>
@ -49,9 +104,25 @@ namespace Avalonia.Media
private protected sealed override IGeometryImpl? CreateDefiningGeometry()
{
double radiusX = RadiusX;
double radiusY = RadiusY;
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
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;
}
}
}
}

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

19
src/Avalonia.Controls/Border.cs

@ -23,6 +23,14 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush?> BackgroundProperty =
AvaloniaProperty.Register<Border, IBrush?>(nameof(Background));
/// <summary>
/// Defines the <see cref="BackgroundSizing"/> property.
/// </summary>
public static readonly StyledProperty<BackgroundSizing> BackgroundSizingProperty =
AvaloniaProperty.Register<Border, BackgroundSizing>(
nameof(BackgroundSizing),
BackgroundSizing.CenterBorder);
/// <summary>
/// Defines the <see cref="BorderBrush"/> property.
/// </summary>
@ -59,6 +67,7 @@ namespace Avalonia.Controls
{
AffectsRender<Border>(
BackgroundProperty,
BackgroundSizingProperty,
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty,
@ -91,6 +100,15 @@ namespace Avalonia.Controls
set => SetValue(BackgroundProperty, value);
}
/// <summary>
/// Gets or sets how the background is drawn relative to the border.
/// </summary>
public BackgroundSizing BackgroundSizing
{
get => GetValue(BackgroundSizingProperty);
set => SetValue(BackgroundSizingProperty, value);
}
/// <summary>
/// Gets or sets a brush with which to paint the border.
/// </summary>
@ -168,6 +186,7 @@ namespace Avalonia.Controls
Bounds.Size,
LayoutThickness,
CornerRadius,
BackgroundSizing,
Background,
BorderBrush,
BoxShadow);

1
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),

48
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -25,6 +25,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IBrush?> BackgroundProperty =
Border.BackgroundProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="BackgroundSizing"/> property.
/// </summary>
public static readonly StyledProperty<BackgroundSizing> BackgroundSizingProperty =
Border.BackgroundSizingProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="BorderBrush"/> property.
/// </summary>
@ -171,10 +177,12 @@ namespace Avalonia.Controls.Presenters
{
AffectsRender<ContentPresenter>(
BackgroundProperty,
BackgroundSizingProperty,
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty,
BoxShadowProperty);
BoxShadowProperty,
CornerRadiusProperty);
AffectsArrange<ContentPresenter>(HorizontalContentAlignmentProperty, VerticalContentAlignmentProperty);
AffectsMeasure<ContentPresenter>(BorderThicknessProperty, PaddingProperty);
}
@ -184,45 +192,42 @@ namespace Avalonia.Controls.Presenters
UpdatePseudoClasses();
}
/// <summary>
/// Gets or sets a brush with which to paint the background.
/// </summary>
/// <inheritdoc cref="Border.Background"/>
public IBrush? Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
/// <summary>
/// Gets or sets a brush with which to paint the border.
/// </summary>
/// <inheritdoc cref="Border.BackgroundSizing"/>
public BackgroundSizing BackgroundSizing
{
get => GetValue(BackgroundSizingProperty);
set => SetValue(BackgroundSizingProperty, value);
}
/// <inheritdoc cref="Border.BorderBrush"/>
public IBrush? BorderBrush
{
get => GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
/// <summary>
/// Gets or sets the thickness of the border.
/// </summary>
/// <inheritdoc cref="Border.BorderThickness"/>
public Thickness BorderThickness
{
get => GetValue(BorderThicknessProperty);
set => SetValue(BorderThicknessProperty, value);
}
/// <summary>
/// Gets or sets the radius of the border rounded corners.
/// </summary>
/// <inheritdoc cref="Border.CornerRadius"/>
public CornerRadius CornerRadius
{
get => GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
/// <inheritdoc cref="Border.BoxShadow"/>
public BoxShadows BoxShadow
{
get => GetValue(BoxShadowProperty);
@ -541,7 +546,14 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
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);
}

15
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -22,6 +22,12 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<IBrush?> BackgroundProperty =
Border.BackgroundProperty.AddOwner<TemplatedControl>();
/// <summary>
/// Defines the <see cref="BackgroundSizing"/> property.
/// </summary>
public static readonly StyledProperty<BackgroundSizing> BackgroundSizingProperty =
Border.BackgroundSizingProperty.AddOwner<TemplatedControl>();
/// <summary>
/// Defines the <see cref="BorderBrush"/> property.
/// </summary>
@ -131,6 +137,15 @@ namespace Avalonia.Controls.Primitives
set => SetValue(BackgroundProperty, value);
}
/// <summary>
/// Gets or sets how the control's background is drawn relative to the control's border.
/// </summary>
public BackgroundSizing BackgroundSizing
{
get => GetValue(BackgroundSizingProperty);
set => SetValue(BackgroundSizingProperty, value);
}
/// <summary>
/// Gets or sets the brush used to draw the control's border.
/// </summary>

92
src/Avalonia.Controls/Shapes/Rectangle.cs

@ -7,8 +7,6 @@ namespace Avalonia.Controls.Shapes
/// </summary>
public class Rectangle : Shape
{
private const double PiOver2 = 1.57079633; // 90 deg to rad
/// <summary>
/// Defines the <see cref="RadiusX"/> property.
/// </summary>
@ -30,20 +28,14 @@ namespace Avalonia.Controls.Shapes
StrokeThicknessProperty);
}
/// <summary>
/// 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.
/// </summary>
/// <inheritdoc cref="RectangleGeometry.RadiusX"/>
public double RadiusX
{
get => GetValue(RadiusXProperty);
set => SetValue(RadiusXProperty, value);
}
/// <summary>
/// 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.
/// </summary>
/// <inheritdoc cref="RectangleGeometry.RadiusY"/>
public double RadiusY
{
get => GetValue(RadiusYProperty);
@ -53,85 +45,9 @@ namespace Avalonia.Controls.Shapes
/// <inheritdoc/>
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);
}
/// <inheritdoc/>

266
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
{
/// <summary>
/// Contains helper methods for rendering a <see cref="Border"/>'s background and border to a given context.
/// </summary>
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<IPlatformRenderInterface>()
.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; }
}
}
}

61
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);
}
}
}
Loading…
Cancel
Save