diff --git a/src/Perspex.SceneGraph/Point.cs b/src/Perspex.SceneGraph/Point.cs
index d87c570e9c..6a618dd893 100644
--- a/src/Perspex.SceneGraph/Point.cs
+++ b/src/Perspex.SceneGraph/Point.cs
@@ -229,6 +229,24 @@ namespace Perspex
return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y);
}
+ ///
+ /// Transforms the point by a matrix.
+ ///
+ /// The transform.
+ /// The transformed point.
+ public Point Transform(Matrix transform)
+ {
+ var x = X;
+ var y = Y;
+ var xadd = y * transform.M21 + transform.M31;
+ var yadd = x * transform.M12 + transform.M32;
+ x *= transform.M11;
+ x += xadd;
+ y *= transform.M22;
+ y += yadd;
+ return new Point(x, y);
+ }
+
///
/// Returns a new point with the specified X coordinate.
///
diff --git a/src/Perspex.SceneGraph/Rect.cs b/src/Perspex.SceneGraph/Rect.cs
index 0b3658ebfc..af8a32a593 100644
--- a/src/Perspex.SceneGraph/Rect.cs
+++ b/src/Perspex.SceneGraph/Rect.cs
@@ -3,6 +3,7 @@
using System;
using System.Globalization;
+using System.Linq;
namespace Perspex
{
@@ -360,6 +361,47 @@ namespace Perspex
return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom);
}
+ ///
+ /// Returns the axis-aligned bounding box of a transformed rectangle.
+ ///
+ /// The transform.
+ /// The bounding box
+ public Rect TransformToAABB(Matrix matrix)
+ {
+ var points = new[]
+ {
+ TopLeft.Transform(matrix),
+ TopRight.Transform(matrix),
+ BottomRight.Transform(matrix),
+ BottomLeft.Transform(matrix),
+ };
+
+ var left = double.MaxValue;
+ var right = double.MinValue;
+ var top = double.MaxValue;
+ var bottom = double.MinValue;
+
+ foreach (var p in points)
+ {
+ if (p.X < left) left = p.X;
+ if (p.X > right) right = p.X;
+ if (p.Y < top) top = p.Y;
+ if (p.Y > bottom) bottom = p.Y;
+ }
+
+ return new Rect(new Point(left, top), new Point(right, bottom));
+ }
+
+ ///
+ /// Translates the rectangle by an offset.
+ ///
+ /// The offset.
+ /// The translated rectangle.
+ public Rect Translate(Vector offset)
+ {
+ return new Rect(Position + offset, Size);
+ }
+
///
/// Returns the string representation of the rectangle.
///
diff --git a/src/Perspex.SceneGraph/Rendering/RendererMixin.cs b/src/Perspex.SceneGraph/Rendering/RendererMixin.cs
index 89c3ed4d0f..7997ea3d50 100644
--- a/src/Perspex.SceneGraph/Rendering/RendererMixin.cs
+++ b/src/Perspex.SceneGraph/Rendering/RendererMixin.cs
@@ -76,8 +76,24 @@ namespace Perspex.Rendering
/// The visual to render.
/// The drawing context.
public static void Render(this DrawingContext context, IVisual visual)
+ {
+ context.Render(visual, visual.Bounds);
+ }
+
+ ///
+ /// Renders the specified visual.
+ ///
+ /// The visual to render.
+ /// The drawing context.
+ ///
+ /// The current clip rect, in coordinates relative to .
+ ///
+ private static void Render(this DrawingContext context, IVisual visual, Rect clipRect)
{
var opacity = visual.Opacity;
+ var clipToBounds = visual.ClipToBounds;
+ var bounds = new Rect(visual.Bounds.Size);
+
if (visual.IsVisible && opacity > 0)
{
var m = Matrix.CreateTranslation(visual.Bounds.Position);
@@ -88,33 +104,48 @@ namespace Perspex.Rendering
{
var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(origin);
- renderTransform = (-offset)*visual.RenderTransform.Value*(offset);
+ renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
+ }
+
+ m = renderTransform * m;
+
+ if (clipToBounds)
+ {
+ clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size));
}
- m = renderTransform*m;
using (context.PushPostTransform(m))
using (context.PushOpacity(opacity))
- using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : default(DrawingContext.PushedState))
+ using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState))
using (context.PushTransformContainer())
{
visual.Render(context);
+
var lst = GetSortedVisualList(visual.VisualChildren);
+
foreach (var child in lst)
{
- context.Render(child);
+ var childBounds = GetTransformedBounds(child);
+
+ if (clipRect.Intersects(childBounds))
+ {
+ var childClipRect = clipRect.Translate(-childBounds.Position);
+ context.Render(child, childClipRect);
+ }
}
+
ReturnListToPool(lst);
}
}
}
- static void ReturnListToPool(List lst)
+ private static void ReturnListToPool(List lst)
{
lst.Clear();
s_listPool.Push(lst);
}
- static List GetSortedVisualList(IReadOnlyList source)
+ private static List GetSortedVisualList(IReadOnlyList source)
{
var lst = s_listPool.Count == 0 ? new List() : s_listPool.Pop();
for (var c = 0; c < source.Count; c++)
@@ -123,6 +154,18 @@ namespace Perspex.Rendering
return lst;
}
+ private static Rect GetTransformedBounds(IVisual visual)
+ {
+ if (visual.RenderTransform == null)
+ {
+ return visual.Bounds;
+ }
+ else
+ {
+ return visual.Bounds.TransformToAABB(visual.RenderTransform.Value);
+ }
+ }
+
class ZIndexComparer : IComparer
{
public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex);
diff --git a/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs b/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs
index d0bc9d763a..9c243efc0c 100644
--- a/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs
+++ b/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs
@@ -47,7 +47,7 @@ namespace Perspex.VisualTree
var bounds = boundsSubscriptions.CombineLatest().Select(ExtractBounds);
// TODO: Track transform and clip rectangle.
- return bounds.Select(x => new TransformedBounds((Rect)x, (Rect)new Rect(), (Matrix)Matrix.Identity));
+ return bounds.Select(x => new TransformedBounds(x, new Rect(), Matrix.Identity));
}
///
diff --git a/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj b/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj
index d161b1264f..d1a6119151 100644
--- a/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj
+++ b/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj
@@ -40,6 +40,10 @@
4
+
+ ..\..\packages\Moq.4.2.1507.0118\lib\net40\Moq.dll
+ True
+
..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll
@@ -80,6 +84,7 @@
+
diff --git a/tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs b/tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs
new file mode 100644
index 0000000000..29a4a6654f
--- /dev/null
+++ b/tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs
@@ -0,0 +1,188 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Moq;
+using Perspex.Controls;
+using Perspex.Media;
+using Perspex.Rendering;
+using Xunit;
+
+namespace Perspex.SceneGraph.UnitTests
+{
+ public class RenderTests_Culling
+ {
+ [Fact]
+ public void In_Bounds_Control_Should_Be_Rendered()
+ {
+ TestControl target;
+ var container = new Canvas
+ {
+ Width = 100,
+ Height = 100,
+ ClipToBounds = true,
+ Children = new Controls.Controls
+ {
+ (target = new TestControl
+ {
+ Width = 10,
+ Height = 10,
+ [Canvas.LeftProperty] = 98,
+ [Canvas.TopProperty] = 98,
+ })
+ }
+ };
+
+ Render(container);
+
+ Assert.True(target.Rendered);
+ }
+
+ [Fact]
+ public void Out_Of_Bounds_Control_Should_Not_Be_Rendered()
+ {
+ TestControl target;
+ var container = new Canvas
+ {
+ Width = 100,
+ Height = 100,
+ ClipToBounds = true,
+ Children = new Controls.Controls
+ {
+ (target = new TestControl
+ {
+ Width = 10,
+ Height = 10,
+ [Canvas.LeftProperty] = 110,
+ [Canvas.TopProperty] = 110,
+ })
+ }
+ };
+
+ Render(container);
+
+ Assert.False(target.Rendered);
+ }
+
+ [Fact]
+ public void Out_Of_Bounds_Child_Control_Should_Not_Be_Rendered()
+ {
+ TestControl target;
+ var container = new Canvas
+ {
+ Width = 100,
+ Height = 100,
+ ClipToBounds = true,
+ Children = new Controls.Controls
+ {
+ new Canvas
+ {
+ Width = 100,
+ Height = 100,
+ [Canvas.LeftProperty] = 50,
+ [Canvas.TopProperty] = 50,
+ Children = new Controls.Controls
+ {
+ (target = new TestControl
+ {
+ Width = 10,
+ Height = 10,
+ [Canvas.LeftProperty] = 50,
+ [Canvas.TopProperty] = 50,
+ })
+ }
+ }
+ }
+ };
+
+ Render(container);
+
+ Assert.False(target.Rendered);
+ }
+
+
+ [Fact]
+ public void Nested_ClipToBounds_Should_Be_Respected()
+ {
+ TestControl target;
+ var container = new Canvas
+ {
+ Width = 100,
+ Height = 100,
+ ClipToBounds = true,
+ Children = new Controls.Controls
+ {
+ new Canvas
+ {
+ Width = 50,
+ Height = 50,
+ ClipToBounds = true,
+ Children = new Controls.Controls
+ {
+ (target = new TestControl
+ {
+ Width = 10,
+ Height = 10,
+ [Canvas.LeftProperty] = 50,
+ [Canvas.TopProperty] = 50,
+ })
+ }
+ }
+ }
+ };
+
+ Render(container);
+
+ Assert.False(target.Rendered);
+ }
+
+ [Fact]
+ public void RenderTransform_Should_Be_Respected()
+ {
+ TestControl target;
+ var container = new Canvas
+ {
+ Width = 100,
+ Height = 100,
+ ClipToBounds = true,
+ Children = new Controls.Controls
+ {
+ (target = new TestControl
+ {
+ Width = 10,
+ Height = 10,
+ [Canvas.LeftProperty] = 110,
+ [Canvas.TopProperty] = 110,
+ RenderTransform = new TranslateTransform(-100, -100),
+ })
+ }
+ };
+
+ Render(container);
+
+ Assert.True(target.Rendered);
+ }
+
+ private void Render(IControl control)
+ {
+ var ctx = CreateDrawingContext();
+ control.Measure(Size.Infinity);
+ control.Arrange(new Rect(control.DesiredSize));
+ ctx.Render(control);
+ }
+
+ private DrawingContext CreateDrawingContext()
+ {
+ return new DrawingContext(Mock.Of());
+ }
+
+ private class TestControl : Control
+ {
+ public bool Rendered { get; private set; }
+
+ public override void Render(DrawingContext context)
+ {
+ Rendered = true;
+ }
+ }
+ }
+}
diff --git a/tests/Perspex.SceneGraph.UnitTests/packages.config b/tests/Perspex.SceneGraph.UnitTests/packages.config
index 8cbc4ec26f..3cf2b984d5 100644
--- a/tests/Perspex.SceneGraph.UnitTests/packages.config
+++ b/tests/Perspex.SceneGraph.UnitTests/packages.config
@@ -1,5 +1,6 @@
+