Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 326 additions & 2 deletions api/Avalonia.nupkg.xml

Large diffs are not rendered by default.

25 changes: 24 additions & 1 deletion src/Avalonia.Base/Media/PolylineGeometry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class PolylineGeometry : Geometry

private IList<Point> _points;
private IDisposable? _pointsObserver;
private readonly FillRule _fillRule;

static PolylineGeometry()
{
Expand All @@ -40,15 +41,28 @@ static PolylineGeometry()
public PolylineGeometry()
{
_points = new Points();
_fillRule = FillRule.EvenOdd;
}

/// <summary>
/// <summary>
/// Initializes a new instance of the <see cref="PolylineGeometry"/> class.
/// </summary>
public PolylineGeometry(IEnumerable<Point> points, bool isFilled)
{
_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>
Expand All @@ -70,10 +84,18 @@ public bool IsFilled
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()
Expand All @@ -83,6 +105,7 @@ public override Geometry Clone()

using (var context = geometry.Open())
{
context.SetFillRule(_fillRule);
var points = Points;
var isFilled = IsFilled;
if (points.Count > 0)
Expand Down
16 changes: 14 additions & 2 deletions src/Avalonia.Controls/Shapes/Polygon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ public class Polygon : Shape
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()
Expand All @@ -25,9 +28,18 @@ public IList<Point> Points
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);
}
}
}
18 changes: 16 additions & 2 deletions src/Avalonia.Controls/Shapes/Polyline.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Media;
using Avalonia.Data;

Expand All @@ -10,10 +11,13 @@ public class Polyline : Shape
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()
Expand All @@ -27,9 +31,19 @@ public IList<Point> Points
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);
}
}
}
48 changes: 48 additions & 0 deletions tests/Avalonia.Controls.UnitTests/Shapes/PolygonTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;

Expand All @@ -25,4 +26,51 @@ public void Polygon_Will_Update_Geometry_On_Shapes_Collection_Content_Change()

root.Child = null;
}

[Fact]
public void FillRule_On_Polygon_Is_Applied_To_DefiningGeometry()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Tested in the Gallery already but there is not a specific Shapes Page. Should include it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested with:

<TextBlock Classes="h3" Margin="0,12,0,4">Path FillRule</TextBlock>
  <TextBlock Text="EvenOdd creates holes for nested figures; NonZero keeps the interior filled when the contours wind in the same direction." />
  <StackPanel Orientation="Horizontal" Spacing="12">
    <StackPanel Spacing="4">
      <TextBlock Text="EvenOdd" />
      <Border Background="White" BorderBrush="Gainsboro" BorderThickness="1" Padding="8" CornerRadius="4">
        <Path Width="140"
              Height="140"
              Stretch="Fill"
              Fill="Orange"
              Stroke="Black"
              StrokeThickness="1"
              Data="F1 M 10,10 L 130,10 130,130 10,130 Z M 50,50 L 90,50 90,90 50,90 Z"/>
      </Border>
    </StackPanel>
    <StackPanel Spacing="4">
      <TextBlock Text="NonZero" />
      <Border Background="White" BorderBrush="Gainsboro" BorderThickness="1" Padding="8" CornerRadius="4">
        <Path Width="140"
              Height="140"
              Stretch="Fill"
              Fill="Orange"
              Stroke="Black"
              StrokeThickness="1"
              Data="F0 M 10,10 L 130,10 130,130 10,130 Z M 50,50 L 90,50 90,90 50,90 Z"/>
      </Border>
    </StackPanel>
  </StackPanel>
  <TextBlock Classes="h3" Margin="0,12,0,4">Polygon/Polyline FillRule</TextBlock>
  <TextBlock Text="Self-intersecting polygons show different winding behavior for EvenOdd and NonZero." />
  <StackPanel Orientation="Horizontal" Spacing="12">
    <StackPanel Spacing="4">
      <TextBlock Text="EvenOdd" />
      <Border Background="White" BorderBrush="Gainsboro" BorderThickness="1" Padding="8" CornerRadius="4">
        <Polygon Width="160"
                 Height="160"
                 Stretch="Fill"
                 Fill="Gold"
                 Stroke="Black"
                 StrokeThickness="1"
                 FillRule="EvenOdd"
                 Points="50,0  21,90  98,35  2,35  79,90" />
      </Border>
    </StackPanel>
    <StackPanel Spacing="4">
      <TextBlock Text="NonZero" />
      <Border Background="White" BorderBrush="Gainsboro" BorderThickness="1" Padding="8" CornerRadius="4">
        <Polygon Width="160"
                 Height="160"
                 Stretch="Fill"
                 Fill="Gold"
                 Stroke="Black"
                 StrokeThickness="1"
                 FillRule="NonZero"
                 Points="50,0  21,90  98,35  2,35  79,90" />
      </Border>
    </StackPanel>
  </StackPanel>
  <TextBlock Classes="h3" Margin="0,12,0,4">Polyline FillRule</TextBlock>
  <TextBlock Text="A jagged filled polyline using NonZero/EvenOdd winding (adapted from WPF FillRule sample)." />
  <StackPanel Orientation="Horizontal" Spacing="12">
    <StackPanel Spacing="4">
      <TextBlock Text="EvenOdd" />
      <Border Background="White" BorderBrush="Gainsboro" BorderThickness="1" Padding="8" CornerRadius="4">
        <Polyline Width="200"
                  Height="140"
                  Stretch="Fill"
                  Fill="Red"
                  Stroke="Black"
                  StrokeThickness="2"
                  FillRule="EvenOdd"
                  Points="10,140
                  60,20
                  110,140
                  20,60
                  180,60
                  90,140
                  140,20
                  10,140" />
      </Border>
    </StackPanel>
    <StackPanel Spacing="4">
      <TextBlock Text="NonZero" />
      <Border Background="White" BorderBrush="Gainsboro" BorderThickness="1" Padding="8" CornerRadius="4">
        <Polyline Width="200"
                  Height="140"
                  Stretch="Fill"
                  Fill="Red"
                  Stroke="Black"
                  StrokeThickness="2"
                  FillRule="NonZero"
                  Points="10,140
                  60,20
                  110,140
                  20,60
                  180,60
                  90,140
                  140,20
                  10,140" />
      </Border>
    </StackPanel>
  </StackPanel>

{
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);
}
}
63 changes: 63 additions & 0 deletions tests/Avalonia.Controls.UnitTests/Shapes/PolylineTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;

Expand All @@ -25,4 +26,66 @@ public void Polyline_Will_Update_Geometry_On_Shapes_Collection_Content_Change()

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);
}
}
33 changes: 33 additions & 0 deletions tests/Avalonia.RenderTests/Shapes/PolygonTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,39 @@ public PolygonTests()
{
}

[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()
{
Expand Down
69 changes: 69 additions & 0 deletions tests/Avalonia.RenderTests/Shapes/PolylineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,75 @@ public PolylineTests()
{
}

[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()
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.