diff --git a/src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs b/src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs
index 4824cb141b..4ad67c45b7 100644
--- a/src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs
+++ b/src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs
@@ -2,10 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reactive.Linq;
-using Avalonia.Media;
namespace Avalonia.VisualTree
{
diff --git a/src/Avalonia.SceneGraph/VisualTree/VisualExtensions.cs b/src/Avalonia.SceneGraph/VisualTree/VisualExtensions.cs
index 916f4c9bc6..a2e6e1e29f 100644
--- a/src/Avalonia.SceneGraph/VisualTree/VisualExtensions.cs
+++ b/src/Avalonia.SceneGraph/VisualTree/VisualExtensions.cs
@@ -70,32 +70,57 @@ namespace Avalonia.VisualTree
return visual.GetVisualsAt(p).FirstOrDefault();
}
+ ///
+ /// Enumerates the visible visuals in the visual tree whose bounds contain a point.
+ ///
+ /// The root visual to test.
+ /// The point.
+ /// The visuals at the requested point.
+ public static IEnumerable GetVisualsAt(
+ this IVisual visual,
+ Point p)
+ {
+ Contract.Requires(visual != null);
+
+ return visual.GetVisualsAt(p, x => x.IsVisible);
+ }
+
///
/// Enumerates the visuals in the visual tree whose bounds contain a point.
///
/// The root visual to test.
/// The point.
+ ///
+ /// A filter predicate. If the predicate returns false then the visual and all its
+ /// children will be excluded from the results.
+ ///
/// The visuals at the requested point.
- public static IEnumerable GetVisualsAt(this IVisual visual, Point p)
+ public static IEnumerable GetVisualsAt(
+ this IVisual visual,
+ Point p,
+ Func filter)
{
Contract.Requires(visual != null);
- if (visual.Bounds.Contains(p))
+ if (filter?.Invoke(visual) != false)
{
- p -= visual.Bounds.Position;
+ bool containsPoint = BoundsTracker.GetTransformedBounds((Visual)visual).Contains(p);
- if (visual.VisualChildren.Any())
+ if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Any())
{
- foreach (IVisual child in visual.VisualChildren)
+ foreach (var child in visual.VisualChildren.SortByZIndex())
{
- foreach (IVisual v in child.GetVisualsAt(p))
+ foreach (var result in child.GetVisualsAt(p, filter))
{
- yield return v;
+ yield return result;
}
}
}
- yield return visual;
+ if (containsPoint)
+ {
+ yield return visual;
+ }
}
}
@@ -192,5 +217,39 @@ namespace Avalonia.VisualTree
{
return target.GetVisualAncestors().Any(x => x == visual);
}
+
+ public static IEnumerable SortByZIndex(this IEnumerable elements)
+ {
+ return elements
+ .Select((element, index) => new ZOrderElement
+ {
+ Element = element,
+ Index = index,
+ ZIndex = element.ZIndex,
+ })
+ .OrderBy(x => x, null)
+ .Select(x => x.Element);
+ }
+
+ private class ZOrderElement : IComparable
+ {
+ public IVisual Element { get; set; }
+ public int Index { get; set; }
+ public int ZIndex { get; set; }
+
+ public int CompareTo(ZOrderElement other)
+ {
+ var z = other.ZIndex - ZIndex;
+
+ if (z != 0)
+ {
+ return z;
+ }
+ else
+ {
+ return other.Index - Index;
+ }
+ }
+ }
}
}
diff --git a/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj b/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj
index baf4959fe7..dec45dfc95 100644
--- a/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj
+++ b/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj
@@ -94,6 +94,8 @@
+
+
diff --git a/tests/Avalonia.SceneGraph.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.SceneGraph.UnitTests/VisualTree/MockRenderInterface.cs
new file mode 100644
index 0000000000..226a452007
--- /dev/null
+++ b/tests/Avalonia.SceneGraph.UnitTests/VisualTree/MockRenderInterface.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Avalonia.Media;
+using Avalonia.Platform;
+
+namespace Avalonia.SceneGraph.UnitTests.VisualTree
+{
+ class MockRenderInterface : IPlatformRenderInterface
+ {
+ public IFormattedTextImpl CreateFormattedText(
+ string text,
+ string fontFamilyName,
+ double fontSize,
+ FontStyle fontStyle,
+ TextAlignment textAlignment,
+ FontWeight fontWeight,
+ TextWrapping wrapping)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IRenderTarget CreateRenderer(IPlatformHandle handle)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IStreamGeometryImpl CreateStreamGeometry()
+ {
+ return new MockStreamGeometry();
+ }
+
+ public IBitmapImpl LoadBitmap(Stream stream)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IBitmapImpl LoadBitmap(string fileName)
+ {
+ throw new NotImplementedException();
+ }
+
+ class MockStreamGeometry : IStreamGeometryImpl
+ {
+ private MockStreamGeometryContext _impl = new MockStreamGeometryContext();
+ public Rect Bounds
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public Matrix Transform
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+
+ set
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public IStreamGeometryImpl Clone()
+ {
+ return this;
+ }
+
+ public bool FillContains(Point point)
+ {
+ return _impl.FillContains(point);
+ }
+
+ public Rect GetRenderBounds(double strokeThickness)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IStreamGeometryContextImpl Open()
+ {
+ return _impl;
+ }
+
+ class MockStreamGeometryContext : IStreamGeometryContextImpl
+ {
+ private List points = new List();
+ public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void BeginFigure(Point startPoint, bool isFilled)
+ {
+ points.Add(startPoint);
+ }
+
+ public void CubicBezierTo(Point point1, Point point2, Point point3)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public void EndFigure(bool isClosed)
+ {
+ }
+
+ public void LineTo(Point point)
+ {
+ points.Add(point);
+ }
+
+ public void QuadraticBezierTo(Point control, Point endPoint)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetFillRule(FillRule fillRule)
+ {
+ }
+
+ public bool FillContains(Point point)
+ {
+ // Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html
+ // to determine if the point is in the geometry (since it will always be convex in this situation)
+ for (int i = 0; i < points.Count; i++)
+ {
+ var a = points[i];
+ var b = points[(i + 1) % points.Count];
+ var c = points[(i + 2) % points.Count];
+
+ Vector v0 = c - a;
+ Vector v1 = b - a;
+ Vector v2 = point - a;
+
+ var dot00 = v0 * v0;
+ var dot01 = v0 * v1;
+ var dot02 = v0 * v2;
+ var dot11 = v1 * v1;
+ var dot12 = v1 * v2;
+
+
+ var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
+ var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
+ var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
+ if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
+ }
+ return false;
+ }
+ }
+ }
+ }
+
+}
diff --git a/tests/Avalonia.SceneGraph.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs b/tests/Avalonia.SceneGraph.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs
new file mode 100644
index 0000000000..896b6f8237
--- /dev/null
+++ b/tests/Avalonia.SceneGraph.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs
@@ -0,0 +1,377 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Rendering;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Moq;
+using Xunit;
+
+namespace Avalonia.SceneGraph.UnitTests.VisualTree
+{
+ public class VisualExtensionsTests_GetVisualsAt
+ {
+ [Fact]
+ public void GetVisualsAt_Should_Find_Controls_At_Point()
+ {
+ using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+ {
+ var container = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ Width = 100,
+ Height = 100,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ }
+ };
+
+ container.Measure(Size.Infinity);
+ container.Arrange(new Rect(container.DesiredSize));
+
+ var context = new DrawingContext(Mock.Of());
+ context.Render(container);
+
+ var result = container.GetVisualsAt(new Point(100, 100));
+
+ Assert.Equal(new[] { container.Child, container }, result);
+ }
+ }
+
+ [Fact]
+ public void GetVisualsAt_Should_Not_Find_Invisible_Controls_At_Point()
+ {
+ using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+ {
+ var container = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ Width = 100,
+ Height = 100,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ IsVisible = false,
+ Child = new Border
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ VerticalAlignment = VerticalAlignment.Stretch,
+ }
+ }
+ };
+
+ container.Measure(Size.Infinity);
+ container.Arrange(new Rect(container.DesiredSize));
+
+ var context = new DrawingContext(Mock.Of());
+ context.Render(container);
+
+ var result = container.GetVisualsAt(new Point(100, 100));
+
+ Assert.Equal(new[] { container }, result);
+ }
+ }
+
+ [Fact]
+ public void GetVisualsAt_Should_Not_Find_Control_Outside_Point()
+ {
+ using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+ {
+ var container = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ Width = 100,
+ Height = 100,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ }
+ };
+
+ container.Measure(Size.Infinity);
+ container.Arrange(new Rect(container.DesiredSize));
+
+ var context = new DrawingContext(Mock.Of());
+ context.Render(container);
+
+ var result = container.GetVisualsAt(new Point(10, 10));
+
+ Assert.Equal(new[] { container }, result);
+ }
+ }
+
+ [Fact]
+ public void GetVisualsAt_Should_Return_Top_Controls_First()
+ {
+ using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+ {
+ var container = new Panel
+ {
+ Width = 200,
+ Height = 200,
+ Children = new Controls.Controls
+ {
+ new Border
+ {
+ Width = 100,
+ Height = 100,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ },
+ new Border
+ {
+ Width = 50,
+ Height = 50,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ }
+ }
+ };
+
+ container.Measure(Size.Infinity);
+ container.Arrange(new Rect(container.DesiredSize));
+
+ var context = new DrawingContext(Mock.Of());
+ context.Render(container);
+
+ var result = container.GetVisualsAt(new Point(100, 100));
+
+ Assert.Equal(new[] { container.Children[1], container.Children[0], container }, result);
+ }
+ }
+
+ [Fact]
+ public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex()
+ {
+ using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+ {
+ var container = new Panel
+ {
+ Width = 200,
+ Height = 200,
+ Children = new Controls.Controls
+ {
+ new Border
+ {
+ Width = 100,
+ Height = 100,
+ ZIndex = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ },
+ new Border
+ {
+ Width = 50,
+ Height = 50,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ },
+ new Border
+ {
+ Width = 75,
+ Height = 75,
+ ZIndex = 2,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ }
+ }
+ };
+
+ container.Measure(Size.Infinity);
+ container.Arrange(new Rect(container.DesiredSize));
+
+ var context = new DrawingContext(Mock.Of());
+ context.Render(container);
+
+ var result = container.GetVisualsAt(new Point(100, 100));
+
+ Assert.Equal(new[] { container.Children[2], container.Children[0], container.Children[1], container }, result);
+ }
+ }
+
+ [Fact]
+ public void GetVisualsAt_Should_Find_Control_Translated_Outside_Parent_Bounds()
+ {
+ using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+ {
+ Border target;
+ var container = new Panel
+ {
+ Width = 200,
+ Height = 200,
+ ClipToBounds = false,
+ Children = new Controls.Controls
+ {
+ new Border
+ {
+ Width = 100,
+ Height = 100,
+ ZIndex = 1,
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Top,
+ Child = target = new Border
+ {
+ Width = 50,
+ Height = 50,
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Top,
+ RenderTransform = new TranslateTransform(110, 110),
+ }
+ },
+ }
+ };
+
+ container.Measure(Size.Infinity);
+ container.Arrange(new Rect(container.DesiredSize));
+
+ var context = new DrawingContext(Mock.Of());
+ context.Render(container);
+
+ var result = container.GetVisualsAt(new Point(120, 120));
+
+ Assert.Equal(new IVisual[] { target, container }, result);
+ }
+ }
+
+ [Fact]
+ public void GetVisualsAt_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped()
+ {
+ using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+ {
+ Border target;
+
+ var container = new Panel
+ {
+ Width = 100,
+ Height = 200,
+ Children = new Controls.Controls
+ {
+ new Panel()
+ {
+ Width = 100,
+ Height = 100,
+ Margin = new Thickness(0, 100, 0, 0),
+ ClipToBounds = true,
+ Children = new Controls.Controls
+ {
+ (target = new Border()
+ {
+ Width = 100,
+ Height = 100,
+ Margin = new Thickness(0, -100, 0, 0)
+ })
+ }
+ }
+ }
+ };
+
+ container.Measure(Size.Infinity);
+ container.Arrange(new Rect(container.DesiredSize));
+
+ var context = new DrawingContext(Mock.Of());
+ context.Render(container);
+
+ var result = container.GetVisualsAt(new Point(50, 50));
+
+ Assert.Equal(new[] { container }, result);
+ }
+ }
+
+ [Fact]
+ public void GetVisualsAt_Should_Not_Find_Control_Outside_Scroll_Viewport()
+ {
+ using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+ {
+ Border target;
+ Border item1;
+ Border item2;
+ ScrollContentPresenter scroll;
+
+ var container = new Panel
+ {
+ Width = 100,
+ Height = 200,
+ Children = new Controls.Controls
+ {
+ (target = new Border()
+ {
+ Width = 100,
+ Height = 100
+ }),
+ new Border()
+ {
+ Width = 100,
+ Height = 100,
+ Margin = new Thickness(0, 100, 0, 0),
+ Child = scroll = new ScrollContentPresenter()
+ {
+ Content = new StackPanel()
+ {
+ Children = new Controls.Controls
+ {
+ (item1 = new Border()
+ {
+ Width = 100,
+ Height = 100,
+ }),
+ (item2 = new Border()
+ {
+ Width = 100,
+ Height = 100,
+ }),
+ }
+ }
+ }
+ }
+ }
+ };
+
+ scroll.UpdateChild();
+
+ container.Measure(Size.Infinity);
+ container.Arrange(new Rect(container.DesiredSize));
+
+ var context = new DrawingContext(Mock.Of());
+ context.Render(container);
+
+ var result = container.GetVisualsAt(new Point(50, 150)).First();
+
+ Assert.Equal(item1, result);
+
+ result = container.GetVisualsAt(new Point(50, 50)).First();
+
+ Assert.Equal(target, result);
+
+ scroll.Offset = new Vector(0, 100);
+
+ //we don't have setup LayoutManager so we will make it manually
+ scroll.Parent.InvalidateArrange();
+ container.InvalidateArrange();
+
+ container.Arrange(new Rect(container.DesiredSize));
+ context.Render(container);
+
+ result = container.GetVisualsAt(new Point(50, 150)).First();
+
+ Assert.Equal(item2, result);
+
+ result = container.GetVisualsAt(new Point(50, 50)).First();
+
+ Assert.NotEqual(item1, result);
+ Assert.Equal(target, result);
+ }
+ }
+ }
+}