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