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 @@ - + + diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index f3db2644a9..d60180bab4 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -2,19 +2,139 @@ using Avalonia.Media; namespace Avalonia.Controls.Shapes { + /// + /// Represents a rectangle with optional rounded corners. + /// public class Rectangle : Shape { + private const double PiOver2 = 1.57079633; // 90 deg to rad + + /// + /// 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); + AffectsGeometry( + BoundsProperty, + RadiusXProperty, + RadiusYProperty, + 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(); + 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); + } + + return geometry; + } } + /// protected override Size MeasureOverride(Size availableSize) { return new Size(StrokeThickness, StrokeThickness); 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 +} diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index e4a280938d..444cc2eb8c 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -168,6 +168,13 @@ namespace Avalonia.Platform /// IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun); + /// + /// 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; } ///