Browse Source

Use render bounds in visual hit testing.

It was previously used in input hit testing but not in visual hit
testing.
pull/584/head
Steven Kirk 10 years ago
parent
commit
1824175e6b
  1. 4
      src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs
  2. 75
      src/Avalonia.SceneGraph/VisualTree/VisualExtensions.cs
  3. 2
      tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj
  4. 164
      tests/Avalonia.SceneGraph.UnitTests/VisualTree/MockRenderInterface.cs
  5. 377
      tests/Avalonia.SceneGraph.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs

4
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
{

75
src/Avalonia.SceneGraph/VisualTree/VisualExtensions.cs

@ -70,32 +70,57 @@ namespace Avalonia.VisualTree
return visual.GetVisualsAt(p).FirstOrDefault();
}
/// <summary>
/// Enumerates the visible visuals in the visual tree whose bounds contain a point.
/// </summary>
/// <param name="visual">The root visual to test.</param>
/// <param name="p">The point.</param>
/// <returns>The visuals at the requested point.</returns>
public static IEnumerable<IVisual> GetVisualsAt(
this IVisual visual,
Point p)
{
Contract.Requires<ArgumentNullException>(visual != null);
return visual.GetVisualsAt(p, x => x.IsVisible);
}
/// <summary>
/// Enumerates the visuals in the visual tree whose bounds contain a point.
/// </summary>
/// <param name="visual">The root visual to test.</param>
/// <param name="p">The point.</param>
/// <param name="filter">
/// A filter predicate. If the predicate returns false then the visual and all its
/// children will be excluded from the results.
/// </param>
/// <returns>The visuals at the requested point.</returns>
public static IEnumerable<IVisual> GetVisualsAt(this IVisual visual, Point p)
public static IEnumerable<IVisual> GetVisualsAt(
this IVisual visual,
Point p,
Func<IVisual, bool> filter)
{
Contract.Requires<ArgumentNullException>(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<IVisual> SortByZIndex(this IEnumerable<IVisual> 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<ZOrderElement>
{
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;
}
}
}
}
}

2
tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj

@ -94,6 +94,8 @@
<Compile Include="VisualTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="VisualTree\BoundsTrackerTests.cs" />
<Compile Include="VisualTree\MockRenderInterface.cs" />
<Compile Include="VisualTree\VisualExtensionsTests_GetVisualsAt.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj">

164
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<Point> points = new List<Point>();
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;
}
}
}
}
}

377
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<IDrawingContextImpl>());
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<IDrawingContextImpl>());
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<IDrawingContextImpl>());
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<IDrawingContextImpl>());
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<IDrawingContextImpl>());
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<IDrawingContextImpl>());
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<IDrawingContextImpl>());
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<IDrawingContextImpl>());
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);
}
}
}
}
Loading…
Cancel
Save