[Feature] Add FillRule property for Polyline/Polygon (#20159)
* Implement FillRuleProperty on Polyline * Implement FillRule Property on Polygon * Add tests * Added comments to new public APIs * Added more comments * More tests * More tests * Updated API * Added render tests * More render tests * Reintroducing the original PolylineGeometry constructor * Updated API file --------- Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
This commit is contained in:
@@ -27,6 +27,7 @@ namespace Avalonia.Media
|
||||
|
||||
private IList<Point> _points;
|
||||
private IDisposable? _pointsObserver;
|
||||
private readonly FillRule _fillRule;
|
||||
|
||||
static PolylineGeometry()
|
||||
{
|
||||
@@ -40,8 +41,10 @@ namespace Avalonia.Media
|
||||
public PolylineGeometry()
|
||||
{
|
||||
_points = new Points();
|
||||
_fillRule = FillRule.EvenOdd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PolylineGeometry"/> class.
|
||||
/// </summary>
|
||||
@@ -49,6 +52,17 @@ namespace Avalonia.Media
|
||||
{
|
||||
_points = new Points(points);
|
||||
IsFilled = isFilled;
|
||||
_fillRule = FillRule.EvenOdd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PolylineGeometry"/> class.
|
||||
/// </summary>
|
||||
public PolylineGeometry(IEnumerable<Point> points, bool isFilled, FillRule fillRule)
|
||||
{
|
||||
_points = new Points(points);
|
||||
IsFilled = isFilled;
|
||||
_fillRule = fillRule;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,10 +84,18 @@ namespace Avalonia.Media
|
||||
set => SetValue(IsFilledProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets how the intersecting areas of the polyline are combined.
|
||||
/// </summary>
|
||||
public FillRule FillRule => _fillRule;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Geometry Clone()
|
||||
{
|
||||
return new PolylineGeometry(Points, IsFilled);
|
||||
return new PolylineGeometry(Points, IsFilled, _fillRule)
|
||||
{
|
||||
Transform = Transform
|
||||
};
|
||||
}
|
||||
|
||||
private protected sealed override IGeometryImpl? CreateDefiningGeometry()
|
||||
@@ -83,6 +105,7 @@ namespace Avalonia.Media
|
||||
|
||||
using (var context = geometry.Open())
|
||||
{
|
||||
context.SetFillRule(_fillRule);
|
||||
var points = Points;
|
||||
var isFilled = IsFilled;
|
||||
if (points.Count > 0)
|
||||
|
||||
@@ -9,9 +9,12 @@ namespace Avalonia.Controls.Shapes
|
||||
public static readonly StyledProperty<IList<Point>> PointsProperty =
|
||||
AvaloniaProperty.Register<Polygon, IList<Point>>("Points");
|
||||
|
||||
public static readonly StyledProperty<FillRule> FillRuleProperty =
|
||||
AvaloniaProperty.Register<Polygon, FillRule>(nameof(FillRule));
|
||||
|
||||
static Polygon()
|
||||
{
|
||||
AffectsGeometry<Polygon>(PointsProperty);
|
||||
AffectsGeometry<Polygon>(PointsProperty, FillRuleProperty);
|
||||
}
|
||||
|
||||
public Polygon()
|
||||
@@ -25,9 +28,18 @@ namespace Avalonia.Controls.Shapes
|
||||
set => SetValue(PointsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how the interior of the polygon is determined when a <see cref="Shape.Fill"/> is applied.
|
||||
/// </summary>
|
||||
public FillRule FillRule
|
||||
{
|
||||
get => GetValue(FillRuleProperty);
|
||||
set => SetValue(FillRuleProperty, value);
|
||||
}
|
||||
|
||||
protected override Geometry CreateDefiningGeometry()
|
||||
{
|
||||
return new PolylineGeometry { Points = Points, IsFilled = true };
|
||||
return new PolylineGeometry(Points, isFilled: true, fillRule: FillRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Data;
|
||||
|
||||
@@ -10,10 +11,13 @@ namespace Avalonia.Controls.Shapes
|
||||
public static readonly StyledProperty<IList<Point>> PointsProperty =
|
||||
AvaloniaProperty.Register<Polyline, IList<Point>>("Points");
|
||||
|
||||
public static readonly StyledProperty<FillRule> FillRuleProperty =
|
||||
AvaloniaProperty.Register<Polyline, FillRule>(nameof(FillRule));
|
||||
|
||||
static Polyline()
|
||||
{
|
||||
StrokeThicknessProperty.OverrideDefaultValue<Polyline>(1);
|
||||
AffectsGeometry<Polyline>(PointsProperty);
|
||||
AffectsGeometry<Polyline>(PointsProperty, FillRuleProperty);
|
||||
}
|
||||
|
||||
public Polyline()
|
||||
@@ -27,9 +31,19 @@ namespace Avalonia.Controls.Shapes
|
||||
set => SetValue(PointsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how the interior of the polyline is determined when a <see cref="Shape.Fill"/> is applied.
|
||||
/// </summary>
|
||||
public FillRule FillRule
|
||||
{
|
||||
get => GetValue(FillRuleProperty);
|
||||
set => SetValue(FillRuleProperty, value);
|
||||
}
|
||||
|
||||
protected override Geometry CreateDefiningGeometry()
|
||||
{
|
||||
return new PolylineGeometry { Points = Points, IsFilled = false };
|
||||
var isFilled = Fill != null;
|
||||
return new PolylineGeometry(Points, isFilled, FillRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.UnitTests;
|
||||
using Xunit;
|
||||
|
||||
@@ -25,4 +26,51 @@ public class PolygonTests : ScopedTestBase
|
||||
|
||||
root.Child = null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillRule_On_Polygon_Is_Applied_To_DefiningGeometry()
|
||||
{
|
||||
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
||||
|
||||
var target = new Polygon
|
||||
{
|
||||
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
|
||||
FillRule = FillRule.NonZero
|
||||
};
|
||||
|
||||
target.Measure(Size.Infinity);
|
||||
|
||||
var geometry = Assert.IsType<PolylineGeometry>(target.DefiningGeometry);
|
||||
Assert.Equal(FillRule.NonZero, geometry.FillRule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Polygon_Equals_Closed_Polyline_Bounds()
|
||||
{
|
||||
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
||||
|
||||
var polyline = new Polyline
|
||||
{
|
||||
Points = new Points
|
||||
{
|
||||
new Point(0, 0),
|
||||
new Point(10, 0),
|
||||
new Point(10, 10),
|
||||
new Point(0, 10),
|
||||
new Point(0, 0)
|
||||
},
|
||||
FillRule = FillRule.NonZero
|
||||
};
|
||||
|
||||
var polygon = new Polygon
|
||||
{
|
||||
Points = new Points { new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10) },
|
||||
FillRule = FillRule.NonZero
|
||||
};
|
||||
|
||||
polyline.Measure(Size.Infinity);
|
||||
polygon.Measure(Size.Infinity);
|
||||
|
||||
Assert.Equal(polygon.DefiningGeometry!.Bounds, polyline.DefiningGeometry!.Bounds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.UnitTests;
|
||||
using Xunit;
|
||||
|
||||
@@ -25,4 +26,66 @@ public class PolylineTests : ScopedTestBase
|
||||
|
||||
root.Child = null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillRule_On_Polyline_Is_Applied_To_DefiningGeometry()
|
||||
{
|
||||
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
||||
|
||||
var target = new Polyline
|
||||
{
|
||||
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
|
||||
Fill = Brushes.Red,
|
||||
FillRule = FillRule.NonZero
|
||||
};
|
||||
|
||||
target.Measure(Size.Infinity);
|
||||
|
||||
var geometry = Assert.IsType<PolylineGeometry>(target.DefiningGeometry);
|
||||
Assert.Equal(FillRule.NonZero, geometry.FillRule);
|
||||
Assert.True(geometry.IsFilled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillRule_Differs_Between_EvenOdd_And_NonZero()
|
||||
{
|
||||
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
||||
|
||||
var evenOdd = new Polyline
|
||||
{
|
||||
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
|
||||
Fill = Brushes.Red,
|
||||
FillRule = FillRule.EvenOdd
|
||||
};
|
||||
|
||||
var nonZero = new Polyline
|
||||
{
|
||||
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
|
||||
Fill = Brushes.Red,
|
||||
FillRule = FillRule.NonZero
|
||||
};
|
||||
|
||||
evenOdd.Measure(Size.Infinity);
|
||||
nonZero.Measure(Size.Infinity);
|
||||
|
||||
Assert.Equal(FillRule.EvenOdd, Assert.IsType<PolylineGeometry>(evenOdd.DefiningGeometry).FillRule);
|
||||
Assert.Equal(FillRule.NonZero, Assert.IsType<PolylineGeometry>(nonZero.DefiningGeometry).FillRule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_Fill_Is_Null_Polyline_Geometry_Is_Not_Filled()
|
||||
{
|
||||
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
||||
|
||||
var target = new Polyline
|
||||
{
|
||||
Points = new Points { new Point(0, 0), new Point(10, 10), new Point(20, 0) },
|
||||
FillRule = FillRule.NonZero,
|
||||
Fill = null
|
||||
};
|
||||
|
||||
target.Measure(Size.Infinity);
|
||||
var geometry = Assert.IsType<PolylineGeometry>(target.DefiningGeometry);
|
||||
Assert.False(geometry.IsFilled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,39 @@ namespace Avalonia.Skia.RenderTests
|
||||
{
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(FillRule.EvenOdd)]
|
||||
[InlineData(FillRule.NonZero)]
|
||||
public async Task Polygon_FillRule(FillRule fillRule)
|
||||
{
|
||||
var target = new Decorator
|
||||
{
|
||||
Padding = new Thickness(8),
|
||||
Width = 220,
|
||||
Height = 220,
|
||||
Child = new Polygon
|
||||
{
|
||||
Stroke = Brushes.Black,
|
||||
StrokeThickness = 2,
|
||||
Fill = Brushes.Gold,
|
||||
Points = new Points
|
||||
{
|
||||
new Point(50, 0),
|
||||
new Point(21, 90),
|
||||
new Point(98, 35),
|
||||
new Point(2, 35),
|
||||
new Point(79, 90)
|
||||
},
|
||||
Stretch = Stretch.Uniform,
|
||||
FillRule = fillRule
|
||||
}
|
||||
};
|
||||
|
||||
var testName = $"{nameof(Polygon_FillRule)}_{fillRule}";
|
||||
await RenderToFile(target, testName);
|
||||
CompareImages(testName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Polygon_1px_Stroke()
|
||||
{
|
||||
|
||||
@@ -13,6 +13,75 @@ namespace Avalonia.Skia.RenderTests
|
||||
{
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(FillRule.EvenOdd)]
|
||||
[InlineData(FillRule.NonZero)]
|
||||
public async Task Polyline_FillRule(FillRule fillRule)
|
||||
{
|
||||
var target = new Decorator
|
||||
{
|
||||
Padding = new Thickness(8),
|
||||
Width = 260,
|
||||
Height = 180,
|
||||
Child = new Polyline
|
||||
{
|
||||
Stroke = Brushes.Black,
|
||||
StrokeThickness = 2,
|
||||
Fill = Brushes.OrangeRed,
|
||||
Points = new Points
|
||||
{
|
||||
new Point(10, 170),
|
||||
new Point(60, 20),
|
||||
new Point(110, 170),
|
||||
new Point(20, 70),
|
||||
new Point(240, 70),
|
||||
new Point(130, 170),
|
||||
new Point(190, 20),
|
||||
new Point(10, 170),
|
||||
},
|
||||
Stretch = Stretch.Uniform,
|
||||
FillRule = fillRule
|
||||
}
|
||||
};
|
||||
|
||||
var testName = $"{nameof(Polyline_FillRule)}_{fillRule}";
|
||||
await RenderToFile(target, testName);
|
||||
CompareImages(testName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Polyline_FillRule_NoFill()
|
||||
{
|
||||
var target = new Decorator
|
||||
{
|
||||
Padding = new Thickness(8),
|
||||
Width = 260,
|
||||
Height = 180,
|
||||
Child = new Polyline
|
||||
{
|
||||
Stroke = Brushes.Black,
|
||||
StrokeThickness = 2,
|
||||
Fill = null,
|
||||
Points = new Points
|
||||
{
|
||||
new Point(10, 170),
|
||||
new Point(60, 20),
|
||||
new Point(110, 170),
|
||||
new Point(20, 70),
|
||||
new Point(240, 70),
|
||||
new Point(130, 170),
|
||||
new Point(190, 20),
|
||||
new Point(10, 170),
|
||||
},
|
||||
Stretch = Stretch.Uniform,
|
||||
FillRule = FillRule.EvenOdd
|
||||
}
|
||||
};
|
||||
|
||||
await RenderToFile(target);
|
||||
CompareImages();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Polyline_1px_Stroke()
|
||||
{
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Reference in New Issue
Block a user