diff --git a/src/Avalonia.SceneGraph/Media/DrawingContext.cs b/src/Avalonia.SceneGraph/Media/DrawingContext.cs
index 90a9749e2a..da33347215 100644
--- a/src/Avalonia.SceneGraph/Media/DrawingContext.cs
+++ b/src/Avalonia.SceneGraph/Media/DrawingContext.cs
@@ -127,7 +127,8 @@ namespace Avalonia.Media
Matrix,
Opacity,
Clip,
- MatrixContainer
+ MatrixContainer,
+ GeometryClip
}
public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix))
@@ -149,10 +150,12 @@ namespace Avalonia.Media
_context._states.Pop();
if (_type == PushedStateType.Matrix)
_context.CurrentTransform = _matrix;
- else if(_type == PushedStateType.Clip)
+ else if (_type == PushedStateType.Clip)
_context._impl.PopClip();
- else if(_type == PushedStateType.Opacity)
+ else if (_type == PushedStateType.Opacity)
_context._impl.PopOpacity();
+ else if (_type == PushedStateType.GeometryClip)
+ _context._impl.PopGeometryClip();
else if (_type == PushedStateType.MatrixContainer)
{
var cont = _context._transformContainers.Pop();
@@ -174,6 +177,18 @@ namespace Avalonia.Media
return new PushedState(this, PushedState.PushedStateType.Clip);
}
+ ///
+ /// Pushes a clip geometry.
+ ///
+ /// The clip geometry.
+ /// A disposable used to undo the clip geometry.
+ public PushedState PushGeometryClip(Geometry clip)
+ {
+ Contract.Requires(clip != null);
+ _impl.PushGeometryClip(clip);
+ return new PushedState(this, PushedState.PushedStateType.GeometryClip);
+ }
+
///
/// Pushes an opacity value.
///
diff --git a/src/Avalonia.SceneGraph/Media/IDrawingContext.cs b/src/Avalonia.SceneGraph/Media/IDrawingContext.cs
index 31f5b0d38a..a798726cca 100644
--- a/src/Avalonia.SceneGraph/Media/IDrawingContext.cs
+++ b/src/Avalonia.SceneGraph/Media/IDrawingContext.cs
@@ -69,7 +69,6 @@ namespace Avalonia.Media
/// Pushes a clip rectange.
///
/// The clip rectangle.
- /// A disposable used to undo the clip rectangle.
void PushClip(Rect clip);
void PopClip();
@@ -78,9 +77,16 @@ namespace Avalonia.Media
/// Pushes an opacity value.
///
/// The opacity.
- /// A disposable used to undo the opacity.
void PushOpacity(double opacity);
void PopOpacity();
+
+ ///
+ /// Pushes a clip geometry.
+ ///
+ /// The clip geometry.
+ void PushGeometryClip(Geometry clip);
+
+ void PopGeometryClip();
}
}
diff --git a/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs b/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs
index a089d71127..70f999c860 100644
--- a/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs
+++ b/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs
@@ -34,6 +34,12 @@ namespace Avalonia.Media
{ 'z', Command.Close },
};
+ private static readonly Dictionary FillRules = new Dictionary
+ {
+ {'0', FillRule.EvenOdd },
+ {'1', FillRule.NonZero }
+ };
+
private StreamGeometry _geometry;
private readonly StreamGeometryContext _context;
@@ -90,8 +96,7 @@ namespace Avalonia.Media
switch (command)
{
case Command.FillRule:
- // TODO: Implement.
- reader.Read();
+ _context.SetFillRule(ReadFillRule(reader));
break;
case Command.Move:
@@ -226,6 +231,24 @@ namespace Avalonia.Media
}
}
+ private static FillRule ReadFillRule(StringReader reader)
+ {
+ int i = reader.Read();
+ if (i == -1)
+ {
+ throw new InvalidDataException("Invalid fill rule");
+ }
+ char c = (char)i;
+ FillRule rule;
+
+ if (!FillRules.TryGetValue(c, out rule))
+ {
+ throw new InvalidDataException("Invalid fill rule");
+ }
+
+ return rule;
+ }
+
private static double ReadDouble(StringReader reader)
{
ReadWhitespace(reader);
diff --git a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
index a8899c7dcd..b26d825064 100644
--- a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
+++ b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
@@ -119,6 +119,7 @@ namespace Avalonia.Rendering
using (context.PushPostTransform(m))
using (context.PushOpacity(opacity))
using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState))
+ using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState))
using (context.PushTransformContainer())
{
visual.Render(context);
diff --git a/src/Avalonia.SceneGraph/Visual.cs b/src/Avalonia.SceneGraph/Visual.cs
index 9cf97c2767..8bdbfecb4a 100644
--- a/src/Avalonia.SceneGraph/Visual.cs
+++ b/src/Avalonia.SceneGraph/Visual.cs
@@ -39,6 +39,12 @@ namespace Avalonia
public static readonly StyledProperty ClipToBoundsProperty =
AvaloniaProperty.Register(nameof(ClipToBounds));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ClipProperty =
+ AvaloniaProperty.Register(nameof(Clip));
+
///
/// Defines the property.
///
@@ -127,6 +133,15 @@ namespace Avalonia
set { SetValue(ClipToBoundsProperty, value); }
}
+ ///
+ /// Gets or sets the geometry clip for this visual.
+ ///
+ public Geometry Clip
+ {
+ get { return GetValue(ClipProperty); }
+ set { SetValue(ClipProperty, value); }
+ }
+
///
/// Gets a value indicating whether this scene graph node and all its parents are visible.
///
diff --git a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs
index 4d6a33b99c..fb4416f78e 100644
--- a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs
+++ b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs
@@ -41,6 +41,11 @@ namespace Avalonia.VisualTree
///
bool ClipToBounds { get; set; }
+ ///
+ /// Gets or sets the geometry clip for this visual.
+ ///
+ Geometry Clip { get; set; }
+
///
/// Gets a value indicating whether this scene graph node is attached to a visual root.
///
diff --git a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs
index 74e1c17cf0..07b544ad3b 100644
--- a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs
+++ b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs
@@ -340,5 +340,17 @@ namespace Avalonia.Cairo.Media
return SetBrush(pen.Brush, destinationSize);
}
+
+ public void PushGeometryClip(Geometry clip)
+ {
+ _context.Save();
+ _context.AppendPath(((StreamGeometryImpl)clip.PlatformImpl).Path);
+ _context.Clip();
+ }
+
+ public void PopGeometryClip()
+ {
+ _context.Restore();
+ }
}
}
diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs
index 88d86182f8..1938d7e49e 100644
--- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs
+++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs
@@ -64,7 +64,11 @@ namespace Avalonia.Cairo.Media
internal bool FillContains(Point point)
{
- return _context.InFill(point.X, point.Y);
+ using (var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, 0, 0)))
+ {
+ context.AppendPath(Path);
+ return context.InFill(point.X, point.Y);
+ }
}
public void LineTo(Point point)
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 6abe2ac971..00b54e8c5a 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -288,6 +288,17 @@ namespace Avalonia.Skia
{
}
+ public void PushGeometryClip(Geometry clip)
+ {
+ Canvas.Save();
+ Canvas.ClipPath(((StreamGeometryImpl)clip.PlatformImpl).EffectivePath);
+ }
+
+ public void PopGeometryClip()
+ {
+ Canvas.Restore();
+ }
+
private Matrix _currentTransform = Matrix.Identity;
public Matrix Transform
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs
index add11e3cff..699a5ba18b 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs
@@ -276,6 +276,11 @@ namespace Avalonia.Direct2D1.Media
}
public void PopOpacity()
+ {
+ PopLayer();
+ }
+
+ private void PopLayer()
{
var layer = _layers.Pop();
if (layer != null)
@@ -324,5 +329,26 @@ namespace Avalonia.Direct2D1.Media
return new SolidColorBrushImpl((Avalonia.Media.SolidColorBrush)null, _renderTarget);
}
}
+
+ public void PushGeometryClip(Avalonia.Media.Geometry clip)
+ {
+ var parameters = new LayerParameters
+ {
+ ContentBounds = PrimitiveExtensions.RectangleInfinite,
+ MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
+ Opacity = 1,
+ GeometricMask = ((GeometryImpl)clip.PlatformImpl).Geometry
+ };
+ var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_renderTarget);
+ _renderTarget.PushLayer(ref parameters, layer);
+
+ _layers.Push(layer);
+
+ }
+
+ public void PopGeometryClip()
+ {
+ PopLayer();
+ }
}
}
diff --git a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems
index 52f6a93b11..79ceddf3bb 100644
--- a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems
+++ b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems
@@ -8,7 +8,7 @@
Avalonia.RenderTests
-
+
@@ -22,5 +22,6 @@
+
-
+
\ No newline at end of file
diff --git a/tests/Avalonia.RenderTests/GeometryClippingTests.cs b/tests/Avalonia.RenderTests/GeometryClippingTests.cs
new file mode 100644
index 0000000000..355606bb43
--- /dev/null
+++ b/tests/Avalonia.RenderTests/GeometryClippingTests.cs
@@ -0,0 +1,51 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+#if AVALONIA_CAIRO
+namespace Avalonia.Cairo.RenderTests
+#elif AVALONIA_SKIA
+namespace Avalonia.Skia.RenderTests
+#else
+namespace Avalonia.Direct2D1.RenderTests
+#endif
+{
+ public class GeometryClippingTests : TestBase
+ {
+ public GeometryClippingTests()
+ :base("GeometryClipping")
+ {
+ }
+
+ [Fact]
+ public void Geometry_Clip_Clips_Path()
+ {
+ var target = new Canvas
+ {
+ Clip = StreamGeometry.Parse("F1 M 0,0 H 76 V 76 Z"),
+ Width = 76,
+ Height = 76,
+ Children = new Avalonia.Controls.Controls
+ {
+ new Path
+ {
+ Width = 32,
+ Height = 40,
+ [Canvas.LeftProperty] = 23,
+ [Canvas.TopProperty] = 18,
+ Stretch = Stretch.Fill,
+ Fill = Brushes.Black,
+ Data = StreamGeometry.Parse("F1 M 27,18L 23,26L 33,30L 24,38L 33,46L 23,50L 27,58L 45,58L 55,38L 45,18L 27,18 Z")
+ }
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+ }
+}
diff --git a/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png b/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png
new file mode 100644
index 0000000000..892899507b
Binary files /dev/null and b/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/GeometryClipping/Geometry_Clip_Clips_Path.expected.png b/tests/TestFiles/Direct2D1/GeometryClipping/Geometry_Clip_Clips_Path.expected.png
new file mode 100644
index 0000000000..892899507b
Binary files /dev/null and b/tests/TestFiles/Direct2D1/GeometryClipping/Geometry_Clip_Clips_Path.expected.png differ
diff --git a/tests/TestFiles/Skia/GeometryClipping/Geometry_Clip_Clips_Path.expected.png b/tests/TestFiles/Skia/GeometryClipping/Geometry_Clip_Clips_Path.expected.png
new file mode 100644
index 0000000000..892899507b
Binary files /dev/null and b/tests/TestFiles/Skia/GeometryClipping/Geometry_Clip_Clips_Path.expected.png differ