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); + } + } + } +}