Skip to content

Commit

Permalink
Add HitTestFirst that allows for hit testing first matching visual. I…
Browse files Browse the repository at this point in the history
…mplement better enumerator that can be used for both first and multiple hits.
  • Loading branch information
MarchingCube committed Dec 7, 2019
1 parent 12f4ae0 commit 9e812f2
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 38 deletions.
4 changes: 3 additions & 1 deletion src/Avalonia.Input/InputExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement e
/// <returns>The topmost <see cref="IInputElement"/> at the specified position.</returns>
public static IInputElement InputHitTest(this IInputElement element, Point p)
{
return element.GetInputElementsAt(p).FirstOrDefault();
Contract.Requires<ArgumentNullException>(element != null);

return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement;
}

private static bool IsHitTestVisible(IVisual visual)
Expand Down
26 changes: 21 additions & 5 deletions src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,23 @@ void DisposeRenderTarget()
/// <inheritdoc/>
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter)
{
if (_renderLoop == null && (_dirty == null || _dirty.Count > 0))
{
// When unit testing the renderLoop may be null, so update the scene manually.
UpdateScene();
}
EnsureCanHitTest();

//It's safe to access _scene here without a lock since
//it's only changed from UI thread which we are currently on
return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty<IVisual>();
}

/// <inheritdoc/>
public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter)
{
EnsureCanHitTest();

//It's safe to access _scene here without a lock since
//it's only changed from UI thread which we are currently on
return _scene?.Item.HitTestFirst(p, root, filter);
}

/// <inheritdoc/>
public void Paint(Rect rect)
{
Expand Down Expand Up @@ -235,6 +242,15 @@ void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisual

internal Scene UnitTestScene() => _scene.Item;

private void EnsureCanHitTest()
{
if (_renderLoop == null && (_dirty == null || _dirty.Count > 0))
{
// When unit testing the renderLoop may be null, so update the scene manually.
UpdateScene();
}
}

private void Render(bool forceComposite)
{
using (var l = _lock.TryLock())
Expand Down
12 changes: 12 additions & 0 deletions src/Avalonia.Visuals/Rendering/IRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ public interface IRenderer : IDisposable
/// <returns>The visuals at the specified point, topmost first.</returns>
IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter);

/// <summary>
/// Hit tests a location to find first visual at the specified point.
/// </summary>
/// <param name="p">The point, in client coordinates.</param>
/// <param name="root">The root of the subtree to search.</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 visual at the specified point, topmost first.</returns>
IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter);

/// <summary>
/// Informs the renderer that the z-ordering of a visual's children has changed.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> f
return HitTest(root, p, filter);
}

public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter)
{
return HitTest(root, p, filter).FirstOrDefault();
}

/// <inheritdoc/>
public void RecalculateChildren(IVisual visual) => AddDirty(visual);

Expand Down
169 changes: 138 additions & 31 deletions src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections.Pooled;
Expand Down Expand Up @@ -129,7 +130,20 @@ public IVisualNode FindNode(IVisual visual)
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter)
{
var node = FindNode(root);
return (node != null) ? HitTest(node, p, null, filter) : Enumerable.Empty<IVisual>();
return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty<IVisual>();
}

/// <summary>
/// Gets the visual at a point in the scene.
/// </summary>
/// <param name="p">The point.</param>
/// <param name="root">The root of the subtree to search.</param>
/// <param name="filter">A filter. May be null.</param>
/// <returns>The visual at the specified point.</returns>
public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter)
{
var node = FindNode(root);
return (node != null) ? HitTestFirst(node, p, filter) : null;
}

/// <summary>
Expand Down Expand Up @@ -159,65 +173,158 @@ private VisualNode Clone(VisualNode source, IVisualNode parent, Dictionary<IVisu
return result;
}

private IEnumerable<IVisual> HitTest(IVisualNode root, Point p, Rect? rootClip, Func<IVisual, bool> filter)
private IVisual HitTestFirst(IVisualNode root, Point p, Func<IVisual, bool> filter)
{
bool FilterAndClip(IVisualNode node, ref Rect? clip)
{
if (filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree)
{
var clipped = false;
using var enumerator = new HitTestEnumerator(root, filter, p, Root);

if (node.ClipToBounds)
{
clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds);
clipped = !clip.Value.Contains(p);
}
enumerator.MoveNext();

if (node.GeometryClip != null)
{
var controlPoint = Root.Visual.TranslatePoint(p, node.Visual);
clipped = !node.GeometryClip.FillContains(controlPoint.Value);
}
return enumerator.Current;
}

return !clipped;
}
private class HitTestEnumerable : IEnumerable<IVisual>
{
private readonly IVisualNode _root;
private readonly Func<IVisual, bool> _filter;
private readonly IVisualNode _sceneRoot;
private readonly Point _point;

public HitTestEnumerable(IVisualNode root, Func<IVisual, bool> filter, Point point, IVisualNode sceneRoot)
{
_root = root;
_filter = filter;
_point = point;
_sceneRoot = sceneRoot;
}

return false;
public IEnumerator<IVisual> GetEnumerator()
{
return new HitTestEnumerator(_root, _filter, _point, _sceneRoot);
}

using (var nodeStack = new PooledStack<(IVisualNode, bool, Rect?)>())
IEnumerator IEnumerable.GetEnumerator()
{
nodeStack.Push((root, false, rootClip));
return GetEnumerator();
}
}

while (nodeStack.Count > 0)
private struct HitTestEnumerator : IEnumerator<IVisual>
{
private readonly PooledStack<Entry> _nodeStack;
private readonly Func<IVisual, bool> _filter;
private readonly IVisualNode _sceneRoot;
private IVisual _current;
private readonly Point _point;

public HitTestEnumerator(IVisualNode root, Func<IVisual, bool> filter, Point point, IVisualNode sceneRoot)
{
_nodeStack = new PooledStack<Entry>();
_nodeStack.Push(new Entry(root, false, null, true));

_filter = filter;
_point = point;
_sceneRoot = sceneRoot;

_current = null;
}

public bool MoveNext()
{
while (_nodeStack.Count > 0)
{
(IVisualNode current, var wasVisited, Rect? currentClip) = nodeStack.Pop();
(var wasVisited, var isRoot, IVisualNode node, Rect? clip) = _nodeStack.Pop();

if (wasVisited && current == root)
if (wasVisited && isRoot)
{
break;
}

var children = current.Children;
var children = node.Children;
int childCount = children.Count;

if (childCount == 0 || wasVisited)
{
if ((wasVisited || FilterAndClip(current, ref currentClip)) && current.HitTest(p))
if ((wasVisited || FilterAndClip(node, ref clip)) && node.HitTest(_point))
{
yield return current.Visual;
_current = node.Visual;

return true;
}
}
else if (FilterAndClip(current, ref currentClip))
else if (FilterAndClip(node, ref clip))
{
nodeStack.Push((current, true, default));
_nodeStack.Push(new Entry(node, true, null));

for (var i = 0; i < childCount; i++)
{
nodeStack.Push((current.Children[i], false, currentClip));
_nodeStack.Push(new Entry(children[i], false, clip));
}
}
}

return false;
}

public void Reset()
{
throw new NotSupportedException();
}

public IVisual Current => _current;

object IEnumerator.Current => Current;

public void Dispose()
{
_nodeStack.Dispose();
}

private bool FilterAndClip(IVisualNode node, ref Rect? clip)
{
if (_filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree)
{
var clipped = false;

if (node.ClipToBounds)
{
clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds);
clipped = !clip.Value.Contains(_point);
}

if (node.GeometryClip != null)
{
var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual);
clipped = !node.GeometryClip.FillContains(controlPoint.Value);
}

return !clipped;
}

return false;
}

private readonly struct Entry
{
public readonly bool WasVisited;
public readonly bool IsRoot;
public readonly IVisualNode Node;
public readonly Rect? Clip;

public Entry(IVisualNode node, bool wasVisited, Rect? clip, bool isRoot = false)
{
Node = node;
WasVisited = wasVisited;
IsRoot = isRoot;
Clip = clip;
}

public void Deconstruct(out bool wasVisited, out bool isRoot, out IVisualNode node, out Rect? clip)
{
wasVisited = WasVisited;
isRoot = IsRoot;
node = Node;
clip = Clip;
}
}
}
}
Expand Down
27 changes: 26 additions & 1 deletion src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,39 @@ public static IEnumerable<IVisual> GetSelfAndVisualAncestors(this IVisual visual
/// </summary>
/// <param name="visual">The root visual to test.</param>
/// <param name="p">The point.</param>
/// <returns>The visuals at the requested point.</returns>
/// <returns>The visual at the requested point.</returns>
public static IVisual GetVisualAt(this IVisual visual, Point p)
{
Contract.Requires<ArgumentNullException>(visual != null);

return visual.GetVisualsAt(p).FirstOrDefault();
}

/// <summary>
/// Gets the first visual 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 visual at the requested point.</returns>
public static IVisual GetVisualAt(this IVisual visual, Point p, Func<IVisual, bool> filter)
{
Contract.Requires<ArgumentNullException>(visual != null);

var root = visual.GetVisualRoot();
var rootPoint = visual.TranslatePoint(p, root);

if (rootPoint.HasValue)
{
return root.Renderer.HitTestFirst(rootPoint.Value, visual, filter);
}

return null;
}

/// <summary>
/// Enumerates the visible visuals in the visual tree whose bounds contain a point.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions tests/Avalonia.Benchmarks/NullRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public void Dispose()

public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter) => null;

public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter) => null;

public void Paint(Rect rect)
{
}
Expand Down
3 changes: 3 additions & 0 deletions tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ private void SetHit(Mock<IRenderer> renderer, IControl hit)
{
renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(new[] { hit });

renderer.Setup(x => x.HitTestFirst(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(hit);
}

private IDisposable TestApplication(IRenderer renderer)
Expand Down
2 changes: 2 additions & 0 deletions tests/Avalonia.LeakTests/ControlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ public void Dispose()

public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter) => null;

public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter) => null;

public void Paint(Rect rect)
{
}
Expand Down

0 comments on commit 9e812f2

Please sign in to comment.