From 2ebf3e4c392a2c09aa57d5573a8b12dbe69d1759 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 3 Apr 2022 13:22:44 -0400 Subject: [PATCH 1/7] Add RadiusX/RadiusY to Rectangle --- src/Avalonia.Controls/Shapes/Rectangle.cs | 118 +++++++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index f3db2644a9..c7653b2ed2 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -2,19 +2,133 @@ using Avalonia.Media; namespace Avalonia.Controls.Shapes { + /// + /// Represents a rectangle with optional rounded corners. + /// public class Rectangle : Shape { + /// + /// Defines the property. + /// + public static readonly StyledProperty RadiusXProperty = + AvaloniaProperty.Register(nameof(RadiusX)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RadiusYProperty = + AvaloniaProperty.Register(nameof(RadiusY)); + static Rectangle() { AffectsGeometry(BoundsProperty, StrokeThicknessProperty); } + /// + /// Gets or sets the radius on the X-axis used to round the corners of the rectangle. + /// Corner radii are represented by an ellipse so this is the X-axis width of the ellipse. + /// + public double RadiusX + { + get => GetValue(RadiusXProperty); + set => SetValue(RadiusXProperty, value); + } + + /// + /// Gets or sets the radius on the Y-axis used to round the corners of the rectangle. + /// Corner radii are represented by an ellipse so this is the Y-axis height of the ellipse. + /// + public double RadiusY + { + get => GetValue(RadiusYProperty); + set => SetValue(RadiusYProperty, value); + } + + /// protected override Geometry CreateDefiningGeometry() { - var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); - return new RectangleGeometry(rect); + // 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(); + double piOver2 = 1.57079633; // 90 deg to rad + + 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), + new Size(x, y), + 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), + new Size(x, y), + 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), + new Size(x, y), + 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), + new Size(x, y), + rotationAngle: piOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + context.EndFigure(true); + } + + return geometry; + } } + /// protected override Size MeasureOverride(Size availableSize) { return new Size(StrokeThickness, StrokeThickness); From d0423727a9ba72ec18f69f116378e4f55d69baa6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 3 Apr 2022 13:27:41 -0400 Subject: [PATCH 2/7] Add Rectangle examples with corner radius --- samples/ControlCatalog/Pages/CanvasPage.xaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/Pages/CanvasPage.xaml b/samples/ControlCatalog/Pages/CanvasPage.xaml index 0c0d897f04..747df593b3 100644 --- a/samples/ControlCatalog/Pages/CanvasPage.xaml +++ b/samples/ControlCatalog/Pages/CanvasPage.xaml @@ -4,7 +4,7 @@ A panel which lays out its children by explicit coordinates - + @@ -12,8 +12,9 @@ - + + From 4c2cba19ff97949d8495c2e68d45c95c211852f6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 3 Apr 2022 13:27:55 -0400 Subject: [PATCH 3/7] Specify the angle units in comments --- src/Avalonia.Visuals/Media/StreamGeometryContext.cs | 4 ++-- src/Avalonia.Visuals/Platform/IGeometryContext.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 88aba8365e..ed6065eae4 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -41,7 +41,7 @@ namespace Avalonia.Media /// /// The destination point. /// The radii of an oval whose perimeter is used to draw the angle. - /// The rotation angle of the oval that specifies the curve. + /// The rotation angle (in radians) of the oval that specifies the curve. /// true to draw the arc greater than 180 degrees; otherwise, false. /// /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. @@ -59,7 +59,7 @@ namespace Avalonia.Media /// /// The destination point. /// The radii of an oval whose perimeter is used to draw the angle. - /// The rotation angle of the oval that specifies the curve. + /// The rotation angle (in radians) of the oval that specifies the curve. /// true to draw the arc greater than 180 degrees; otherwise, false. /// /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs index 3f7273b53b..87db9f1dd4 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryContext.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryContext.cs @@ -13,7 +13,7 @@ namespace Avalonia.Platform /// /// The destination point. /// The radii of an oval whose perimeter is used to draw the angle. - /// The rotation angle of the oval that specifies the curve. + /// The rotation angle (in radians) of the oval that specifies the curve. /// true to draw the arc greater than 180 degrees; otherwise, false. /// /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. @@ -60,4 +60,4 @@ namespace Avalonia.Platform /// The fill rule. void SetFillRule(FillRule fillRule); } -} \ No newline at end of file +} From 67ff1a7cd1e4be0a70357dc7d45cc6face2ab12d Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 3 Apr 2022 13:28:10 -0400 Subject: [PATCH 4/7] Add missing comment for SupportsIndividualRoundRects --- src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index e4a280938d..acf103b893 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -168,6 +168,9 @@ namespace Avalonia.Platform /// IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun); + /// + /// Gets a value indicating whether the platform supports rectangles with rounded corners. + /// bool SupportsIndividualRoundRects { get; } /// From 7eb6cb31e7d7605b26484fd50d3505bbdfdf5909 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 3 Apr 2022 13:46:21 -0400 Subject: [PATCH 5/7] Share the same arc size struct --- src/Avalonia.Controls/Shapes/Rectangle.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index c7653b2ed2..24c61747aa 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -63,7 +63,8 @@ namespace Avalonia.Controls.Shapes { var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); var geometry = new StreamGeometry(); - double piOver2 = 1.57079633; // 90 deg to rad + var arcSize = new Size(x, y); + var piOver2 = 1.57079633; // 90 deg to rad using (StreamGeometryContext context = geometry.Open()) { @@ -89,7 +90,7 @@ namespace Avalonia.Controls.Shapes context.LineTo(new Point(rect.Right - x, rect.Top)); context.ArcTo( new Point(rect.Right, rect.Top + y), - new Size(x, y), + arcSize, rotationAngle: piOver2, isLargeArc: false, SweepDirection.Clockwise); @@ -98,7 +99,7 @@ namespace Avalonia.Controls.Shapes context.LineTo(new Point(rect.Right, rect.Bottom - y)); context.ArcTo( new Point(rect.Right - x, rect.Bottom), - new Size(x, y), + arcSize, rotationAngle: piOver2, isLargeArc: false, SweepDirection.Clockwise); @@ -107,7 +108,7 @@ namespace Avalonia.Controls.Shapes context.LineTo(new Point(rect.Left + x, rect.Bottom)); context.ArcTo( new Point(rect.Left, rect.Bottom - y), - new Size(x, y), + arcSize, rotationAngle: piOver2, isLargeArc: false, SweepDirection.Clockwise); @@ -116,7 +117,7 @@ namespace Avalonia.Controls.Shapes context.LineTo(new Point(rect.Left, rect.Top + y)); context.ArcTo( new Point(rect.Left + x, rect.Top), - new Size(x, y), + arcSize, rotationAngle: piOver2, isLargeArc: false, SweepDirection.Clockwise); From 928cb35fcaef6f3fd3e2fab7aff1d18209867bf1 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 4 Apr 2022 16:32:29 -0400 Subject: [PATCH 6/7] Update after review --- src/Avalonia.Controls/Shapes/Rectangle.cs | 6 +++++- src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index 24c61747aa..969d512338 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -21,7 +21,11 @@ namespace Avalonia.Controls.Shapes static Rectangle() { - AffectsGeometry(BoundsProperty, StrokeThicknessProperty); + AffectsGeometry( + BoundsProperty, + RadiusXProperty, + RadiusYProperty, + StrokeThicknessProperty); } /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index acf103b893..444cc2eb8c 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -169,8 +169,12 @@ namespace Avalonia.Platform IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun); /// - /// Gets a value indicating whether the platform supports rectangles with rounded corners. + /// Gets a value indicating whether the platform directly supports rectangles with rounded corners. /// + /// + /// Some platform renderers can't directly handle rounded corners on rectangles. + /// In this case, code that requires rounded corners must generate and retain a geometry instead. + /// bool SupportsIndividualRoundRects { get; } /// From 9f4241684a72beb2c1478cab4fd676888ec7756a Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 4 Apr 2022 16:35:34 -0400 Subject: [PATCH 7/7] Separate out const for better theoretical performance --- src/Avalonia.Controls/Shapes/Rectangle.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index 969d512338..d60180bab4 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -7,6 +7,8 @@ namespace Avalonia.Controls.Shapes /// public class Rectangle : Shape { + private const double PiOver2 = 1.57079633; // 90 deg to rad + /// /// Defines the property. /// @@ -68,7 +70,6 @@ namespace Avalonia.Controls.Shapes var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); var geometry = new StreamGeometry(); var arcSize = new Size(x, y); - var piOver2 = 1.57079633; // 90 deg to rad using (StreamGeometryContext context = geometry.Open()) { @@ -95,7 +96,7 @@ namespace Avalonia.Controls.Shapes context.ArcTo( new Point(rect.Right, rect.Top + y), arcSize, - rotationAngle: piOver2, + rotationAngle: PiOver2, isLargeArc: false, SweepDirection.Clockwise); @@ -104,7 +105,7 @@ namespace Avalonia.Controls.Shapes context.ArcTo( new Point(rect.Right - x, rect.Bottom), arcSize, - rotationAngle: piOver2, + rotationAngle: PiOver2, isLargeArc: false, SweepDirection.Clockwise); @@ -113,7 +114,7 @@ namespace Avalonia.Controls.Shapes context.ArcTo( new Point(rect.Left, rect.Bottom - y), arcSize, - rotationAngle: piOver2, + rotationAngle: PiOver2, isLargeArc: false, SweepDirection.Clockwise); @@ -122,7 +123,7 @@ namespace Avalonia.Controls.Shapes context.ArcTo( new Point(rect.Left + x, rect.Top), arcSize, - rotationAngle: piOver2, + rotationAngle: PiOver2, isLargeArc: false, SweepDirection.Clockwise);