From 44af3b65b59ce22f09650af01a54eb6cd2048e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Su=C3=A1rez?= Date: Thu, 27 Nov 2025 12:20:49 +0100 Subject: [PATCH] [Feature] Add StrokeMiterLimit property to Shape (#20156) * Added StrokeMiterLimitProperty * Added tests --- src/Avalonia.Controls/Shapes/Shape.cs | 20 +- .../Shapes/ShapeTests.cs | 215 ++++++++++++++++++ 2 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/Shapes/ShapeTests.cs diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 8457f4789c..24dfcab88b 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -60,6 +60,12 @@ namespace Avalonia.Controls.Shapes public static readonly StyledProperty StrokeJoinProperty = AvaloniaProperty.Register(nameof(StrokeJoin), PenLineJoin.Miter); + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeMiterLimitProperty = + AvaloniaProperty.Register(nameof(StrokeMiterLimit), 10.0); + private Matrix _transform = Matrix.Identity; private Geometry? _definingGeometry; private Geometry? _renderedGeometry; @@ -191,6 +197,15 @@ namespace Avalonia.Controls.Shapes get => GetValue(StrokeJoinProperty); set => SetValue(StrokeJoinProperty, value); } + + /// + /// Gets or sets the limit on the ratio of the miter length to half the of the pen. + /// + public double StrokeMiterLimit + { + get => GetValue(StrokeMiterLimitProperty); + set => SetValue(StrokeMiterLimitProperty, value); + } private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= OnGeometryChanged; @@ -270,7 +285,8 @@ namespace Avalonia.Controls.Shapes || change.Property == StrokeDashArrayProperty || change.Property == StrokeDashOffsetProperty || change.Property == StrokeLineCapProperty - || change.Property == StrokeJoinProperty) + || change.Property == StrokeJoinProperty + || change.Property == StrokeMiterLimitProperty) { if (change.Property == StrokeProperty || change.Property == StrokeThicknessProperty) @@ -278,7 +294,7 @@ namespace Avalonia.Controls.Shapes InvalidateMeasure(); } - if (Pen.TryModifyOrCreate(ref _strokePen, Stroke, StrokeThickness, StrokeDashArray, StrokeDashOffset, StrokeLineCap, StrokeJoin)) + if (Pen.TryModifyOrCreate(ref _strokePen, Stroke, StrokeThickness, StrokeDashArray, StrokeDashOffset, StrokeLineCap, StrokeJoin, StrokeMiterLimit)) { InvalidateVisual(); } diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/ShapeTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/ShapeTests.cs new file mode 100644 index 0000000000..9aa1e1ac36 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Shapes/ShapeTests.cs @@ -0,0 +1,215 @@ +using Avalonia.Collections; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.UnitTests; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Shapes; + +public class ShapeTests : ScopedTestBase +{ + [Fact] + public void StrokeMiterLimit_Default_Is_Applied_To_Pen() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var pen = RenderAndGetPen(new TestShape + { + StrokeThickness = 4, + Stroke = Brushes.Black + }); + + Assert.NotNull(pen); + Assert.Equal(10, pen!.MiterLimit); + } + + [Fact] + public void StrokeMiterLimit_Update_Refreshes_Pen() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var shape = new TestShape + { + StrokeThickness = 4, + Stroke = Brushes.Black + }; + + RenderAndGetPen(shape); + shape.StrokeMiterLimit = 2; + var pen = RenderAndGetPen(shape); + + Assert.NotNull(pen); + Assert.Equal(2, pen!.MiterLimit); + } + + [Fact] + public void StrokeThickness_Is_Applied_To_Pen() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var pen = RenderAndGetPen(new TestShape + { + StrokeThickness = 6, + Stroke = Brushes.Black + }); + + Assert.NotNull(pen); + Assert.Equal(6, pen!.Thickness); + } + + [Fact] + public void StrokeLineCap_And_Join_Are_Applied_To_Pen() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var pen = RenderAndGetPen(new TestShape + { + Stroke = Brushes.Black, + StrokeThickness = 4, + StrokeLineCap = PenLineCap.Round, + StrokeJoin = PenLineJoin.Bevel + }); + + Assert.NotNull(pen); + Assert.Equal(PenLineCap.Round, pen!.LineCap); + Assert.Equal(PenLineJoin.Bevel, pen.LineJoin); + } + + [Fact] + public void StrokeDashArray_And_Offset_Are_Applied_To_Pen() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var pen = RenderAndGetPen(new TestShape + { + Stroke = Brushes.Black, + StrokeThickness = 4, + StrokeDashArray = new AvaloniaList(1, 2, 3), + StrokeDashOffset = 1.5 + }); + + Assert.NotNull(pen); + Assert.NotNull(pen!.DashStyle); + Assert.Equal(3, pen.DashStyle!.Dashes.Count); + Assert.Equal(1, pen.DashStyle.Dashes[0]); + Assert.Equal(2, pen.DashStyle.Dashes[1]); + Assert.Equal(3, pen.DashStyle.Dashes[2]); + Assert.Equal(1.5, pen.DashStyle.Offset); + } + + [Fact] + public void No_Stroke_Produces_No_Pen() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var pen = RenderAndGetPen(new TestShape + { + Stroke = null, + StrokeThickness = 4 + }); + + Assert.Null(pen); + } + + private static IPen? RenderAndGetPen(Shape shape) + { + using var context = new RecordingDrawingContext(); + shape.Render(context); + return context.LastPen; + } + + private class TestShape : Shape + { + protected override Geometry? CreateDefiningGeometry() => + new RectangleGeometry(new Rect(0, 0, 20, 20)); + } + + private class RecordingDrawingContext : DrawingContext + { + public IPen? LastPen { get; private set; } + + public void Reset() => LastPen = null; + + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) + { + } + + protected override void DrawLineCore(IPen pen, Point p1, Point p2) + { + } + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + LastPen = pen; + } + + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) + { + } + + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) + { + } + + public override void Custom(ICustomDrawOperation custom) + { + } + + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) + { + } + + protected override void PushClipCore(RoundedRect rect) + { + } + + protected override void PushClipCore(Rect rect) + { + } + + protected override void PushGeometryClipCore(Geometry clip) + { + } + + protected override void PushOpacityCore(double opacity) + { + } + + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) + { + } + + protected override void PushRenderOptionsCore(RenderOptions renderOptions) + { + } + + protected override void PushTransformCore(Matrix matrix) + { + } + + protected override void PopClipCore() + { + } + + protected override void PopGeometryClipCore() + { + } + + protected override void PopOpacityCore() + { + } + + protected override void PopOpacityMaskCore() + { + } + + protected override void PopTransformCore() + { + } + + protected override void PopRenderOptionsCore() + { + } + + protected override void DisposeCore() + { + } + } +}