diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
index 6a0396e52a..a3e4b2a17c 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
-using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
@@ -133,8 +132,21 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
///
public Visual? HitTestFirst(Point p, Visual root, Func? filter)
{
- // TODO: Optimize
- return HitTest(p, root, filter).FirstOrDefault();
+ using var _ = Diagnostic.PerformingHitTest();
+
+ if (root.CompositionVisual == null)
+ return null;
+
+ Func? f = filter == null ? null : v =>
+ {
+ if (v is CompositionDrawListVisual dlv)
+ return filter(dlv.Visual);
+ return true;
+ };
+
+ return CompositionTarget.TryHitTestFirst(p, root.CompositionVisual, f) is CompositionDrawListVisual dv
+ ? dv.Visual
+ : null;
}
///
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
index c4e2038450..29d546be4a 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
@@ -28,6 +28,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
_drawList?.Dispose();
_drawList = value;
_drawListChanged = true;
+ Root?.InvalidateHitTestIndex();
RegisterForSerialization();
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionHitTestRTree.cs b/src/Avalonia.Base/Rendering/Composition/CompositionHitTestRTree.cs
new file mode 100644
index 0000000000..44e28892c4
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionHitTestRTree.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Collections.Pooled;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+
+namespace Avalonia.Rendering.Composition;
+
+internal readonly struct CompositionHitTestCandidate(CompositionVisual visual, int order)
+{
+
+ public CompositionVisual Visual { get; } = visual;
+ public int Order { get; } = order;
+}
+
+internal sealed class CompositionHitTestRTree
+{
+ private const int MaxChildren = 8;
+ private readonly List _entries = new();
+ private readonly List _unbounded = new();
+ private Node? _root;
+ private CompositionVisual? _indexedRoot;
+
+ public ulong Revision { get; private set; }
+
+ public bool IsCurrent(CompositionVisual root, ulong revision) =>
+ ReferenceEquals(_indexedRoot, root) && Revision == revision;
+
+ public void Rebuild(CompositionVisual? root, ulong revision)
+ {
+ _indexedRoot = root;
+ Revision = revision;
+ _root = null;
+ _entries.Clear();
+ _unbounded.Clear();
+
+ if (root == null)
+ return;
+
+ var order = 0;
+ AddVisual(root, ref order);
+ _root = BuildTree(_entries);
+ _entries.Clear();
+ }
+
+ public void Query(Point point, PooledList results)
+ {
+ if (_root != null)
+ Query(_root, point, results);
+
+ foreach (var candidate in _unbounded)
+ results.Add(candidate);
+ }
+
+ private void AddVisual(CompositionVisual visual, ref int order)
+ {
+ if (visual is CompositionContainerVisual container)
+ {
+ for (var c = container.Children.Count - 1; c >= 0; c--)
+ AddVisual(container.Children[c], ref order);
+ }
+
+ if (visual is not CompositionDrawListVisual drawListVisual)
+ return;
+
+ var visualOrder = order++;
+
+ if (drawListVisual.Visual is ICustomHitTest)
+ {
+ if (drawListVisual.TryGetServerGlobalTransform() != null)
+ _unbounded.Add(new CompositionHitTestCandidate(drawListVisual, visualOrder));
+ return;
+ }
+
+ if (TryGetTransformedBounds(drawListVisual, out var bounds))
+ _entries.Add(new Entry(bounds, drawListVisual, visualOrder));
+ }
+
+ private static bool TryGetTransformedBounds(CompositionDrawListVisual visual, out LtrbRect bounds)
+ {
+ bounds = default;
+
+ if (visual.DrawList?.Bounds is not { } localBounds)
+ return false;
+
+ if (visual.TryGetServerGlobalTransform() is not { } transform)
+ return false;
+
+ bounds = localBounds.TransformToAABB(transform);
+ return !bounds.IsZeroSize;
+ }
+
+ private static Node? BuildTree(List entries)
+ {
+ if (entries.Count == 0)
+ return null;
+
+ entries.Sort(CompareEntries);
+
+ var nodes = new List((entries.Count + MaxChildren - 1) / MaxChildren);
+ for (var i = 0; i < entries.Count; i += MaxChildren)
+ {
+ var count = Math.Min(MaxChildren, entries.Count - i);
+ var leafEntries = new Entry[count];
+ entries.CopyTo(i, leafEntries, 0, count);
+ nodes.Add(new Node(GetBounds(leafEntries), leafEntries, null));
+ }
+
+ while (nodes.Count > 1)
+ {
+ nodes.Sort(CompareNodes);
+
+ var parents = new List((nodes.Count + MaxChildren - 1) / MaxChildren);
+ for (var i = 0; i < nodes.Count; i += MaxChildren)
+ {
+ var count = Math.Min(MaxChildren, nodes.Count - i);
+ var children = new Node[count];
+ nodes.CopyTo(i, children, 0, count);
+ parents.Add(new Node(GetBounds(children), null, children));
+ }
+
+ nodes = parents;
+ }
+
+ return nodes[0];
+ }
+
+ private static void Query(Node node, Point point, PooledList results)
+ {
+ if (!Contains(node.Bounds, point))
+ return;
+
+ if (node.Entries != null)
+ {
+ foreach (var entry in node.Entries)
+ {
+ if (Contains(entry.Bounds, point))
+ results.Add(new CompositionHitTestCandidate(entry.Visual, entry.Order));
+ }
+ }
+ else if (node.Children != null)
+ {
+ foreach (var child in node.Children)
+ Query(child, point, results);
+ }
+ }
+
+ private static bool Contains(LtrbRect bounds, Point point) =>
+ point.X >= bounds.Left && point.X <= bounds.Right &&
+ point.Y >= bounds.Top && point.Y <= bounds.Bottom;
+
+ private static int CompareEntries(Entry left, Entry right)
+ {
+ var result = (left.Bounds.Left + left.Bounds.Right).CompareTo(right.Bounds.Left + right.Bounds.Right);
+ return result != 0
+ ? result
+ : (left.Bounds.Top + left.Bounds.Bottom).CompareTo(right.Bounds.Top + right.Bounds.Bottom);
+ }
+
+ private static int CompareNodes(Node left, Node right)
+ {
+ var result = (left.Bounds.Left + left.Bounds.Right).CompareTo(right.Bounds.Left + right.Bounds.Right);
+ return result != 0
+ ? result
+ : (left.Bounds.Top + left.Bounds.Bottom).CompareTo(right.Bounds.Top + right.Bounds.Bottom);
+ }
+
+ private static LtrbRect GetBounds(Entry[] entries)
+ {
+ var bounds = entries[0].Bounds;
+ for (var i = 1; i < entries.Length; i++)
+ bounds = Union(bounds, entries[i].Bounds);
+ return bounds;
+ }
+
+ private static LtrbRect GetBounds(Node[] nodes)
+ {
+ var bounds = nodes[0].Bounds;
+ for (var i = 1; i < nodes.Length; i++)
+ bounds = Union(bounds, nodes[i].Bounds);
+ return bounds;
+ }
+
+ private static LtrbRect Union(LtrbRect left, LtrbRect right) =>
+ new(
+ Math.Min(left.Left, right.Left),
+ Math.Min(left.Top, right.Top),
+ Math.Max(left.Right, right.Right),
+ Math.Max(left.Bottom, right.Bottom));
+
+ private readonly struct Entry(LtrbRect bounds, CompositionVisual visual, int order)
+ {
+ public LtrbRect Bounds { get; } = bounds;
+ public CompositionVisual Visual { get; } = visual;
+ public int Order { get; } = order;
+ }
+
+ private sealed class Node(LtrbRect bounds, Entry[]? entries, Node[]? children)
+ {
+ public LtrbRect Bounds { get; } = bounds;
+ public Entry[]? Entries { get; } = entries;
+ public Node[]? Children { get; } = children;
+ }
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
index f00eef0d10..8f90f284cd 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
@@ -11,17 +11,24 @@ namespace Avalonia.Rendering.Composition
///
internal partial class CompositionTarget
{
+ private readonly CompositionHitTestRTree _hitTestIndex = new();
+ private bool _hitTestIndexDirty = true;
+
partial void OnRootChanged()
{
if (Root != null)
Root.Root = this;
+ InvalidateHitTestIndex();
}
partial void OnRootChanging()
{
if (Root != null)
Root.Root = null;
+ InvalidateHitTestIndex();
}
+
+ internal void InvalidateHitTestIndex() => _hitTestIndexDirty = true;
///
/// Attempts to perform a hit-tst
@@ -29,13 +36,21 @@ namespace Avalonia.Rendering.Composition
///
public PooledList? TryHitTest(Point point, CompositionVisual? root, Func? filter)
{
- point *= Scaling;
- Server.Readback.NextRead();
+ using var candidates = QueryHitTestCandidates(point, out var globalPoint);
root ??= Root;
if (root == null)
return null;
var res = new PooledList();
- HitTestCore(root, point, res, filter);
+
+ if (candidates == null)
+ return res;
+
+ foreach (var candidate in candidates)
+ {
+ if (HitTestCandidate(root, candidate.Visual, globalPoint, filter))
+ res.Add(candidate.Visual);
+ }
+
return res;
}
@@ -85,36 +100,91 @@ namespace Avalonia.Rendering.Composition
return false;
}
- void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList result,
+ PooledList? QueryHitTestCandidates(Point point, out Point globalPoint)
+ {
+ globalPoint = point * Scaling;
+ Server.Readback.NextRead();
+
+ if (Root == null)
+ return null;
+
+ if (_hitTestIndexDirty || !_hitTestIndex.IsCurrent(Root, Server.Readback.ReadRevision))
+ {
+ _hitTestIndex.Rebuild(Root, Server.Readback.ReadRevision);
+ _hitTestIndexDirty = false;
+ }
+
+ var candidates = new PooledList();
+ _hitTestIndex.Query(globalPoint, candidates);
+ candidates.Sort(static (left, right) => left.Order.CompareTo(right.Order));
+ return candidates;
+ }
+
+ static bool HitTestCandidate(CompositionVisual root, CompositionVisual visual, Point globalPoint,
Func? filter)
{
+ using var path = new PooledList();
+
+ for (var current = visual; current != null; current = current.Parent)
+ {
+ path.Add(current);
+ if (ReferenceEquals(current, root))
+ break;
+ }
+
+ if (path.Count == 0 || !ReferenceEquals(path[path.Count - 1], root))
+ return false;
+
+ Point point = default;
+ for (var c = path.Count - 1; c >= 0; c--)
+ {
+ if (!HitTestVisual(path[c], globalPoint, filter, out point))
+ return false;
+ }
+
+ return visual.HitTest(point);
+ }
+
+ static bool HitTestVisual(CompositionVisual visual, Point globalPoint, Func? filter,
+ out Point point)
+ {
+ point = default;
+
if (visual.Visible == false)
- return;
-
+ return false;
+
if (filter != null && !filter(visual))
- return;
-
- if (!TryTransformTo(visual, globalPoint, out var point))
- return;
+ return false;
+
+ if (!TryTransformTo(visual, globalPoint, out point))
+ return false;
if (visual.ClipToBounds
&& (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y))
- return;
+ return false;
if (visual.Clip?.FillContains(point) == false)
- return;
-
- // Inspect children
- if (visual is CompositionContainerVisual cv)
- for (var c = cv.Children.Count - 1; c >= 0; c--)
+ return false;
+
+ return true;
+ }
+
+ public CompositionVisual? TryHitTestFirst(Point point, CompositionVisual? root, Func? filter)
+ {
+ using var candidates = QueryHitTestCandidates(point, out var globalPoint);
+ root ??= Root;
+ if (root == null || candidates == null)
+ return null;
+
+ foreach (var candidate in candidates)
+ {
+ if (HitTestCandidate(root, candidate.Visual, globalPoint, filter))
{
- var ch = cv.Children[c];
- HitTestCore(ch, globalPoint, result, filter);
+ return candidate.Visual;
}
-
- // Hit-test the current node
- if (visual.HitTest(point))
- result.Add(visual);
+ }
+
+ return null;
}
///
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs
index 0ad70d7102..ae5b0cd3f1 100644
--- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
+using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing.Nodes;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
@@ -22,11 +23,27 @@ internal class CompositionRenderData : ICompositorSerializable, IDisposable
public ServerCompositionRenderData Server { get; }
private PooledInlineList _resources;
private PooledInlineList _items;
+ private LtrbRect? _bounds;
+ private bool _boundsValid;
private bool _itemsSent;
public void AddResource(ICompositionRenderResource resource) => _resources.Add(resource);
public void Add(IRenderDataItem item) => _items.Add(item);
-
+
+ public LtrbRect? Bounds
+ {
+ get
+ {
+ if (!_boundsValid)
+ {
+ _bounds = CalculateRenderBounds();
+ _boundsValid = true;
+ }
+
+ return _bounds;
+ }
+ }
+
public void Dispose()
{
if (!_itemsSent)
@@ -47,6 +64,15 @@ internal class CompositionRenderData : ICompositorSerializable, IDisposable
public SimpleServerObject TryGetServer(Compositor c) => Server;
+ private LtrbRect? CalculateRenderBounds()
+ {
+ LtrbRect? totalBounds = null;
+ foreach (var item in _items)
+ totalBounds = LtrbRect.FullUnion(totalBounds, item.Bounds);
+
+ return ServerCompositionRenderData.ApplyRenderBoundsRounding(totalBounds);
+ }
+
public void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
writer.Write(_items.Count);
diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs
index 35be380425..9d34fa1c9b 100644
--- a/src/Avalonia.Base/Rendering/Composition/Visual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs
@@ -17,10 +17,30 @@ namespace Avalonia.Rendering.Composition
{
}
- partial void OnRootChanged() => OnRootChangedCore();
+ partial void OnRootChanged()
+ {
+ OnRootChangedCore();
+ Root?.InvalidateHitTestIndex();
+ }
+
+ partial void OnRootChanging() => Root?.InvalidateHitTestIndex();
partial void OnParentChanged() => Root = Parent?.Root;
+ partial void OnVisibleChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnClipChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnClipToBoundsChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnOffsetChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnSizeChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnAnchorPointChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnCenterPointChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnRotationAngleChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnOrientationChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnScaleChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnTransformMatrixChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnAdornedVisualChanged() => Root?.InvalidateHitTestIndex();
+ partial void OnAdornerIsClippedChanged() => Root?.InvalidateHitTestIndex();
+
public IBrush? OpacityMask
{
get => _opacityMask;
diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs
index 5663b86af3..3cb07a79fe 100644
--- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs
+++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs
@@ -38,7 +38,11 @@ namespace Avalonia.Rendering.Composition
public void RemoveAll() => Clear();
- partial void OnAdded(CompositionVisual item) => item.Parent = _owner;
+ partial void OnAdded(CompositionVisual item)
+ {
+ item.Parent = _owner;
+ _owner.Root?.InvalidateHitTestIndex();
+ }
partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem)
{
@@ -55,10 +59,15 @@ namespace Avalonia.Rendering.Composition
}
}
- partial void OnRemoved(CompositionVisual item) => item.Parent = null;
+ partial void OnRemoved(CompositionVisual item)
+ {
+ _owner.Root?.InvalidateHitTestIndex();
+ item.Parent = null;
+ }
partial void OnBeforeClear()
{
+ _owner.Root?.InvalidateHitTestIndex();
foreach (var i in this)
i.Parent = null;
}
diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs
index ec481328b7..5a6429b2bd 100644
--- a/tests/Avalonia.UnitTests/CompositorTestServices.cs
+++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs
@@ -108,7 +108,7 @@ public class CompositorTestServices : IDisposable
public void AssertHitTestFirst(Point pt, Func? filter, Visual? expected)
{
RunJobs();
- var tested = Renderer.HitTest(pt, TopLevel, filter).First();
+ var tested = Renderer.HitTestFirst(pt, TopLevel, filter);
Assert.Equal(expected, tested);
}