diff --git a/samples/RenderTest/Pages/ClippingPage.xaml b/samples/RenderTest/Pages/ClippingPage.xaml index 895edd65ca..0e352c4bec 100644 --- a/samples/RenderTest/Pages/ClippingPage.xaml +++ b/samples/RenderTest/Pages/ClippingPage.xaml @@ -5,7 +5,8 @@ Height="100" Clip="M 58.625 0.07421875 C 50.305778 0.26687364 42.411858 7.0346526 41.806641 15.595703 C 42.446442 22.063923 39.707425 13.710754 36.982422 12.683594 C 29.348395 6.1821635 16.419398 8.4359222 11.480469 17.195312 C 6.0935256 25.476803 9.8118851 37.71125 18.8125 41.6875 C 9.1554771 40.62945 -0.070876925 49.146842 0.21679688 58.857422 C 0.21545578 60.872512 0.56758794 62.88911 1.2617188 64.78125 C 4.3821886 74.16708 16.298268 78.921772 25.03125 74.326172 C 28.266843 72.062552 26.298191 74.214838 25.414062 76.398438 C 21.407348 85.589198 27.295992 97.294293 37.097656 99.501953 C 46.864883 102.3541 57.82177 94.726518 58.539062 84.580078 C 58.142158 79.498998 59.307538 83.392694 61.207031 85.433594 C 67.532324 93.056874 80.440232 93.192029 86.882812 85.630859 C 93.836392 78.456939 92.396838 65.538666 84.115234 60.009766 C 79.783641 57.904836 83.569793 58.802369 86.375 58.193359 C 96.383335 56.457569 102.87506 44.824101 99.083984 35.394531 C 95.963498 26.008711 84.047451 21.254079 75.314453 25.849609 C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344 C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812 C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z "> - + + diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index b724f53f20..860fb2ed9c 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -227,7 +227,7 @@ namespace Avalonia.Media public PushedState PushGeometryClip(Geometry clip) { Contract.Requires(clip != null); - PlatformImpl.PushGeometryClip(clip); + PlatformImpl.PushGeometryClip(clip.PlatformImpl); return new PushedState(this, PushedState.PushedStateType.GeometryClip); } diff --git a/src/Avalonia.Visuals/Media/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Media/IDrawingContextImpl.cs index 4a52bc8324..8d97f45e19 100644 --- a/src/Avalonia.Visuals/Media/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Media/IDrawingContextImpl.cs @@ -104,7 +104,7 @@ namespace Avalonia.Media /// Pushes a clip geometry. /// /// The clip geometry. - void PushGeometryClip(Geometry clip); + void PushGeometryClip(IGeometryImpl clip); void PopGeometryClip(); } diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index 9dc00ac14f..132e00e56b 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -34,6 +34,13 @@ namespace Avalonia.Platform /// true if the geometry contains the point; otherwise, false. bool FillContains(Point point); + /// + /// Intersects the geometry with another geometry. + /// + /// The other geometry. + /// A new representing the intersection. + IGeometryImpl Intersect(IGeometryImpl geometry); + /// /// Indicates whether the geometry's stroke contains the specified point. /// diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 4ac206acf1..e6cf53918e 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -263,6 +263,11 @@ namespace Avalonia.Rendering var bitmap = _layers[layer.LayerRoot].Bitmap; var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); + if (layer.GeometryClip != null) + { + context.PushGeometryClip(layer.GeometryClip); + } + if (layer.OpacityMask == null) { context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect); @@ -271,6 +276,11 @@ namespace Avalonia.Rendering { context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); } + + if (layer.GeometryClip != null) + { + context.PopGeometryClip(); + } } if (_overlay != null) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 0bcefb79c6..03a4929808 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -179,7 +179,7 @@ namespace Avalonia.Rendering.SceneGraph // TODO: Implement } - public void PushGeometryClip(Geometry clip) + public void PushGeometryClip(IGeometryImpl clip) { // TODO: Implement } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 6c5ec7b762..994b4a258e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -45,7 +46,12 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the node's clip geometry, if any. /// - Geometry GeometryClip { get; set; } + IGeometryImpl GeometryClip { get; set; } + + /// + /// Gets a value indicating whether one of the node's ancestors has a geometry clip. + /// + bool HasAncestorGeometryClip { get; } /// /// Gets the child scene graph nodes. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 75ecad7943..e472192c38 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using Avalonia.Media; +using Avalonia.Platform; using Avalonia.Threading; using Avalonia.VisualTree; @@ -162,7 +163,7 @@ namespace Avalonia.Rendering.SceneGraph node.Transform = contextImpl.Transform; node.ClipBounds = bounds.TransformToAABB(node.Transform).Intersect(clip); node.ClipToBounds = clipToBounds; - node.GeometryClip = visual.Clip; + node.GeometryClip = visual.Clip?.PlatformImpl; node.Opacity = opacity; node.OpacityMask = visual.OpacityMask; @@ -293,7 +294,7 @@ namespace Avalonia.Rendering.SceneGraph } var oldLayer = scene.Layers[oldLayerRoot]; - SetDescendentsLayer(node, scene.Layers[newLayerRoot], oldLayer); + PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer); scene.Layers.Remove(oldLayer); } @@ -304,7 +305,7 @@ namespace Avalonia.Rendering.SceneGraph var oldLayer = scene.Layers[oldLayerRoot]; UpdateLayer(node, layer); - SetDescendentsLayer(node, layer, scene.Layers[oldLayerRoot]); + PropagateLayer(node, layer, scene.Layers[oldLayerRoot]); } private static void UpdateLayer(VisualNode node, SceneLayer layer) @@ -321,9 +322,13 @@ namespace Avalonia.Rendering.SceneGraph layer.OpacityMask = null; layer.OpacityMaskRect = Rect.Empty; } + + layer.GeometryClip = node.HasAncestorGeometryClip ? + CreateLayerGeometryClip(node) : + null; } - private static void SetDescendentsLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer) + private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer) { node.LayerRoot = layer.LayerRoot; @@ -335,11 +340,35 @@ namespace Avalonia.Rendering.SceneGraph // If the child is not the start of a new layer, recurse. if (child.LayerRoot != child.Visual) { - SetDescendentsLayer(child, layer, oldLayer); + PropagateLayer(child, layer, oldLayer); } } } + private static IGeometryImpl CreateLayerGeometryClip(VisualNode node) + { + IGeometryImpl result = null; + + for (;;) + { + node = (VisualNode)node.Parent; + + if (node == null || (node.GeometryClip == null && !node.HasAncestorGeometryClip)) + { + break; + } + + if (node?.GeometryClip != null) + { + var transformed = node.GeometryClip.WithTransform(node.Transform); + + result = result == null ? transformed : result.Intersect(transformed); + } + } + + return result; + } + private static IBrush ToImmutable(IBrush brush) { return (brush as IMutableBrush)?.ToImmutable() ?? brush; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs index ffe38b7a8f..253842ea3b 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Media; +using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -20,6 +21,7 @@ namespace Avalonia.Rendering.SceneGraph Opacity = Opacity, OpacityMask = OpacityMask, OpacityMaskRect = OpacityMaskRect, + GeometryClip = GeometryClip, }; } @@ -29,5 +31,6 @@ namespace Avalonia.Rendering.SceneGraph public double Opacity { get; set; } = 1; public IBrush OpacityMask { get; set; } public Rect OpacityMaskRect { get; set; } + public IGeometryImpl GeometryClip { get; set; } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index e3d1ab164a..dd5740e4a9 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -33,6 +34,8 @@ namespace Avalonia.Rendering.SceneGraph Visual = visual; Parent = parent; + HasAncestorGeometryClip = parent != null && + (parent.HasAncestorGeometryClip || parent.GeometryClip != null); } /// @@ -54,7 +57,10 @@ namespace Avalonia.Rendering.SceneGraph public bool ClipToBounds { get; set; } /// - public Geometry GeometryClip { get; set; } + public IGeometryImpl GeometryClip { get; set; } + + /// + public bool HasAncestorGeometryClip { get; } /// /// Gets or sets the opacity of the scene graph node. diff --git a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs index 89c10ee2a2..ee01d5ecb0 100644 --- a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs @@ -356,10 +356,10 @@ namespace Avalonia.Cairo.Media return SetBrush(pen.Brush, destinationSize); } - public void PushGeometryClip(Geometry clip) + public void PushGeometryClip(IGeometryImpl clip) { _context.Save(); - _context.AppendPath(((StreamGeometryImpl)clip.PlatformImpl).Path); + _context.AppendPath(((StreamGeometryImpl)clip).Path); _context.Clip(); } diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs index 18d0060bff..31cba39276 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs @@ -65,6 +65,11 @@ namespace Avalonia.Cairo.Media return _impl.FillContains(point); } + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + public bool StrokeContains(Pen pen, Point point) { return _impl.StrokeContains(pen, point); diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 05b1b27eb6..67fab819f6 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -331,10 +331,10 @@ namespace Avalonia.Skia disposable?.Dispose(); } - public void PushGeometryClip(Geometry clip) + public void PushGeometryClip(IGeometryImpl clip) { Canvas.Save(); - Canvas.ClipPath(((StreamGeometryImpl)clip.PlatformImpl).EffectivePath); + Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath); } public void PopGeometryClip() diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index aebf404c66..11e7f5c79e 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -63,6 +63,11 @@ namespace Avalonia.Skia return GetRenderBounds(0).Contains(point); } + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + public IGeometryImpl WithTransform(Matrix transform) { var result = (StreamGeometryImpl)Clone(); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 1700fc9205..26207c23d2 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -403,14 +403,14 @@ namespace Avalonia.Direct2D1.Media return new SolidColorBrushImpl(null, _renderTarget); } - public void PushGeometryClip(Avalonia.Media.Geometry clip) + public void PushGeometryClip(IGeometryImpl clip) { var parameters = new LayerParameters { ContentBounds = PrimitiveExtensions.RectangleInfinite, MaskTransform = PrimitiveExtensions.Matrix3x2Identity, Opacity = 1, - GeometricMask = ((GeometryImpl)clip.PlatformImpl).Geometry + GeometricMask = ((GeometryImpl)clip).Geometry }; var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_renderTarget); _renderTarget.PushLayer(ref parameters, layer); diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index cc99adccc8..8bb901a1e4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -38,6 +38,18 @@ namespace Avalonia.Direct2D1.Media return Geometry.FillContainsPoint(point.ToSharpDX()); } + /// + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + var result = new PathGeometry(Geometry.Factory); + + using (var sink = result.Open()) + { + Geometry.Combine(((GeometryImpl)geometry).Geometry, CombineMode.Intersect, sink); + return new StreamGeometryImpl(result); + } + } + /// public bool StrokeContains(Avalonia.Media.Pen pen, Point point) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs index a32426843f..4c1bc3d6f7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs @@ -23,7 +23,7 @@ namespace Avalonia.Direct2D1.Media /// Initializes a new instance of the class. /// /// An existing Direct2D . - protected StreamGeometryImpl(PathGeometry geometry) + public StreamGeometryImpl(PathGeometry geometry) : base(geometry) { } diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs index 2a252579fc..6e100270ab 100644 --- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs +++ b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs @@ -7,11 +7,29 @@ namespace Avalonia.UnitTests { public class MockStreamGeometryImpl : IStreamGeometryImpl { - private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); + private MockStreamGeometryContext _context; - public Rect Bounds => _impl.CalculateBounds(); + public MockStreamGeometryImpl() + { + Transform = Matrix.Identity; + _context = new MockStreamGeometryContext(); + } - public Matrix Transform { get; set; } + public MockStreamGeometryImpl(Matrix transform) + { + Transform = transform; + _context = new MockStreamGeometryContext(); + } + + private MockStreamGeometryImpl(Matrix transform, MockStreamGeometryContext context) + { + Transform = transform; + _context = context; + } + + public Rect Bounds => _context.CalculateBounds(); + + public Matrix Transform { get; } public IStreamGeometryImpl Clone() { @@ -20,7 +38,7 @@ namespace Avalonia.UnitTests public bool FillContains(Point point) { - return _impl.FillContains(point); + return _context.FillContains(point); } public bool StrokeContains(Pen pen, Point point) @@ -30,14 +48,19 @@ namespace Avalonia.UnitTests public Rect GetRenderBounds(double strokeThickness) => Bounds; + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + return new MockStreamGeometryImpl(Transform); + } + public IStreamGeometryContextImpl Open() { - return _impl; + return _context; } public IGeometryImpl WithTransform(Matrix transform) { - return this; + return new MockStreamGeometryImpl(transform, _context); } class MockStreamGeometryContext : IStreamGeometryContextImpl diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 5a76c35669..af3abb0136 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -7,6 +7,8 @@ using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; using Avalonia.Layout; +using Moq; +using Avalonia.Platform; namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { @@ -618,7 +620,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph sceneBuilder.UpdateAll(scene); var decoratorNode = scene.FindNode(decorator); - Assert.Same(clip, decoratorNode.GeometryClip); + Assert.Same(clip.PlatformImpl, decoratorNode.GeometryClip); } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs index fcc4c68ffd..e65487ac44 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs @@ -238,5 +238,40 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.Equal(1, scene.Layers.Count); } } + + [Fact] + public void GeometryClip_Should_Affect_Child_Layers() + { + using (TestApplication()) + { + var clip = StreamGeometry.Parse("M100,0 L0,100 100,100"); + Decorator decorator; + Border border; + var tree = new TestRoot + { + Child = decorator = new Decorator + { + Clip = clip, + Margin = new Thickness(12, 16), + Child = border = new Border + { + Opacity = 0.5, + } + } + }; + + var layout = AvaloniaLocator.Current.GetService(); + layout.ExecuteInitialLayoutPass(tree); + + var scene = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + + var borderLayer = scene.Layers[border]; + Assert.Equal( + Matrix.CreateTranslation(12, 16), + ((MockStreamGeometryImpl)borderLayer.GeometryClip).Transform); + } + } } }