diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs b/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs index cef3ca6792..6ef16e2dcd 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs @@ -24,6 +24,12 @@ internal sealed class CompositionHitTestAabbTree private int _root = Null; private int _freeList = Null; + public CompositionHitTestAabbTree(CompositionVisualCollection children) + { + for (var i = 0; i < children.Count; i++) + Add(children[i], i); + } + public void Clear() { _leaves.Clear(); @@ -34,19 +40,10 @@ internal sealed class CompositionHitTestAabbTree _freeList = Null; } - public void Rebuild(CompositionVisualCollection children) - { - Clear(); - - for (var i = 0; i < children.Count; i++) - Add(children[i], i); - } - - public bool Update(CompositionVisual visual) + public void Update(CompositionVisual visual, int order) { if (_leaves.TryGetValue(visual, out var leaf)) { - var order = _nodes[leaf].Order; var state = GetBoundsState(visual, out var bounds); if (state == BoundsState.Bounded) @@ -61,27 +58,51 @@ internal sealed class CompositionHitTestAabbTree _unbounded[visual] = order; } - return true; + return; } - if (_unbounded.TryGetValue(visual, out var unboundedOrder)) + if (_unbounded.ContainsKey(visual)) { var state = GetBoundsState(visual, out var bounds); if (state == BoundsState.Unbounded) { - return true; + _unbounded[visual] = order; + return; } _unbounded.Remove(visual); if (state == BoundsState.Bounded) - CreateLeaf(visual, bounds, unboundedOrder); + CreateLeaf(visual, bounds, order); + + return; + } + + Add(visual, order); + } + + public void Remove(CompositionVisual visual) + { + if (_leaves.TryGetValue(visual, out var leaf)) + { + DestroyLeaf(visual, leaf); + return; + } + + _unbounded.Remove(visual); + } - return true; + public void UpdateOrder(CompositionVisual visual, int order) + { + if (_leaves.TryGetValue(visual, out var leaf)) + { + SetOrder(leaf, order); + return; } - return false; + if (_unbounded.ContainsKey(visual)) + _unbounded[visual] = order; } public void Query(Point point, PooledList results) @@ -218,6 +239,7 @@ internal sealed class CompositionHitTestAabbTree private void MoveLeaf(int leaf, LtrbRect bounds) { + // If the exact bounds still fit inside the fat bounds, the tree shape can stay unchanged. if (_nodes[leaf].Bounds.Contains(bounds)) return; @@ -294,6 +316,14 @@ internal sealed class CompositionHitTestAabbTree var sibling = FindBestSibling(leafBounds); var oldParent = _nodes[sibling].Parent; var newParent = AllocateNode(); + + // Insert by replacing the chosen sibling with a new internal parent: + // + // Before: oldParent After: oldParent + // | | + // sibling newParent + // / \ + // sibling leaf var parentNode = _nodes[newParent]; parentNode.Parent = oldParent; parentNode.Bounds = leafBounds.Union(_nodes[sibling].Bounds); @@ -344,6 +374,7 @@ internal sealed class CompositionHitTestAabbTree var cost1 = GetInsertionCost(child1, leafBounds, inheritanceCost); var cost2 = GetInsertionCost(child2, leafBounds, inheritanceCost); + // Stop descending when pairing with this internal node is already cheaper. if (cost < cost1 && cost < cost2) break; @@ -378,8 +409,14 @@ internal sealed class CompositionHitTestAabbTree var grandParent = parentNode.Parent; var sibling = parentNode.Child1 == leaf ? parentNode.Child2 : parentNode.Child1; + // Collapse the removed leaf's parent and promote the sibling. if (grandParent != Null) { + // Before: grandParent After: grandParent + // | | + // parent sibling + // / \ + // leaf sibling var grandParentNode = _nodes[grandParent]; if (grandParentNode.Child1 == parent) grandParentNode.Child1 = sibling; @@ -396,6 +433,11 @@ internal sealed class CompositionHitTestAabbTree } else { + // If the parent was the root, the sibling becomes the new root. + // + // Before: parent(root) After: sibling(root) + // / \ + // leaf sibling _root = sibling; var siblingNode = _nodes[sibling]; siblingNode.Parent = Null; @@ -416,6 +458,8 @@ internal sealed class CompositionHitTestAabbTree var node = _nodes[index]; var child1 = _nodes[node.Child1]; var child2 = _nodes[node.Child2]; + + // Ancestor bounds always cover both children after insert/remove/rotate. node.Bounds = child1.Bounds.Union(child2.Bounds); node.Height = 1 + Math.Max(child1.Height, child2.Height); _nodes[index] = node; @@ -436,9 +480,11 @@ internal sealed class CompositionHitTestAabbTree var c = _nodes[indexC]; var balance = c.Height - b.Height; + // The right subtree is heavier than the left. Rotate C up. if (balance > 1) return RotateCUp(indexA, indexB, indexC); + // The left subtree is heavier than the right. Rotate B up. if (balance < -1) return RotateBUp(indexA, indexB, indexC); @@ -447,6 +493,19 @@ internal sealed class CompositionHitTestAabbTree private int RotateCUp(int indexA, int indexB, int indexC) { + // Rotate C above A: + // + // Before: A After, if F taller: C + // / \ / \ + // B C A F + // / \ / \ + // F G B G + // + // After, otherwise: C + // / \ + // A G + // / \ + // B F var a = _nodes[indexA]; var c = _nodes[indexC]; var indexF = c.Child1; @@ -458,8 +517,10 @@ internal sealed class CompositionHitTestAabbTree c.Parent = a.Parent; a.Parent = indexC; + // C takes A's old place in the parent chain. ReplaceParentChild(indexA, indexC, c.Parent); + // Keep the taller C child with C, and move the other child under A. if (f.Height > g.Height) { c.Child2 = indexF; @@ -490,6 +551,19 @@ internal sealed class CompositionHitTestAabbTree private int RotateBUp(int indexA, int indexB, int indexC) { + // Rotate B above A: + // + // Before: A After, if D taller: B + // / \ / \ + // B C A D + // / \ / \ + // D E E C + // + // After, otherwise: B + // / \ + // A E + // / \ + // D C var a = _nodes[indexA]; var b = _nodes[indexB]; var indexD = b.Child1; @@ -501,8 +575,10 @@ internal sealed class CompositionHitTestAabbTree b.Parent = a.Parent; a.Parent = indexB; + // B takes A's old place in the parent chain. ReplaceParentChild(indexA, indexB, b.Parent); + // Keep the taller B child with B, and move the other child under A. if (d.Height > e.Height) { b.Child2 = indexD; @@ -565,6 +641,7 @@ internal sealed class CompositionHitTestAabbTree return BoundsState.Bounded; } + // Fatten the bounds by a small amount to avoid having to update the tree for every tiny movement. private static LtrbRect Fatten(LtrbRect bounds) => new(bounds.Left - FatBoundsPadding, bounds.Top - FatBoundsPadding, @@ -602,6 +679,7 @@ internal sealed class CompositionHitTestAabbTree private sealed class CandidateComparer : IComparer { + // Higher child order is topmost, sort descending. public int Compare(Candidate left, Candidate right) => right.Order.CompareTo(left.Order); } } diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs index 26937fb323..c0f5edfeb9 100644 --- a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -10,7 +10,6 @@ namespace Avalonia.Rendering.Composition { internal const int HitTestAabbTreeThreshold = 32; private CompositionHitTestAabbTree? _hitTestChildren; - private bool _hitTestChildrenDirty = true; public CompositionVisualCollection Children { get; private set; } = null!; @@ -26,43 +25,58 @@ namespace Avalonia.Rendering.Composition base.OnRootChangedCore(); } - internal void InvalidateHitTestChildren() + internal void AddHitTestChild(CompositionVisual child) { - _hitTestChildrenDirty = true; + if (_hitTestChildren == null) + return; + + var order = Children.IndexOf(child); + if (order >= 0) + _hitTestChildren.Update(child, order); + + UpdateHitTestChildOrder(); + } + + internal void RemoveHitTestChild(CompositionVisual child) + { + if (_hitTestChildren == null) + return; + + _hitTestChildren.Remove(child); + + UpdateHitTestChildOrder(); + } + + internal void ClearHitTestChildren() + { + _hitTestChildren?.Clear(); } internal void UpdateHitTestChildBounds(CompositionVisual child) { - if (_hitTestChildren == null || _hitTestChildrenDirty) + if (_hitTestChildren == null) return; if (Children.Count < HitTestAabbTreeThreshold) { - _hitTestChildren.Clear(); _hitTestChildren = null; return; } - if (!_hitTestChildren.Update(child)) - _hitTestChildrenDirty = true; + var order = Children.IndexOf(child); + if (order >= 0) + _hitTestChildren.Update(child, order); } internal bool TryQueryHitTestChildren(Point point, PooledList results) { if (Children.Count < HitTestAabbTreeThreshold) { - _hitTestChildren?.Clear(); _hitTestChildren = null; return false; } - _hitTestChildren ??= new CompositionHitTestAabbTree(); - - if (_hitTestChildrenDirty) - { - _hitTestChildren.Rebuild(Children); - _hitTestChildrenDirty = false; - } + _hitTestChildren ??= new CompositionHitTestAabbTree(Children); _hitTestChildren.Query(point, results); return true; @@ -73,22 +87,24 @@ namespace Avalonia.Rendering.Composition { if (Children.Count < HitTestAabbTreeThreshold) { - _hitTestChildren?.Clear(); _hitTestChildren = null; hit = null; return false; } - _hitTestChildren ??= new CompositionHitTestAabbTree(); - - if (_hitTestChildrenDirty) - { - _hitTestChildren.Rebuild(Children); - _hitTestChildrenDirty = false; - } + _hitTestChildren ??= new CompositionHitTestAabbTree(Children); hit = _hitTestChildren.QueryFirst(point, ref hitTest); return true; } + + private void UpdateHitTestChildOrder() + { + if (_hitTestChildren == null) + return; + + for (var i = 0; i < Children.Count; i++) + _hitTestChildren.UpdateOrder(Children[i], i); + } } } diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index e89c3837ff..7ada536938 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -41,7 +41,7 @@ namespace Avalonia.Rendering.Composition partial void OnAdded(CompositionVisual item) { item.Parent = _owner; - InvalidateHitTestChildren(); + AddHitTestChild(item); } partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem) @@ -62,7 +62,7 @@ namespace Avalonia.Rendering.Composition partial void OnRemoved(CompositionVisual item) { item.Parent = null; - InvalidateHitTestChildren(); + RemoveHitTestChild(item); } partial void OnBeforeClear() @@ -71,7 +71,7 @@ namespace Avalonia.Rendering.Composition i.Parent = null; } - partial void OnClear() => InvalidateHitTestChildren(); + partial void OnClear() => ClearHitTestChildren(); partial void OnBeforeAdded(CompositionVisual item) { @@ -80,10 +80,22 @@ namespace Avalonia.Rendering.Composition item.Parent = _owner; } - private void InvalidateHitTestChildren() + private void AddHitTestChild(CompositionVisual item) { if (_owner is CompositionContainerVisual container) - container.InvalidateHitTestChildren(); + container.AddHitTestChild(item); + } + + private void RemoveHitTestChild(CompositionVisual item) + { + if (_owner is CompositionContainerVisual container) + container.RemoveHitTestChild(item); + } + + private void ClearHitTestChildren() + { + if (_owner is CompositionContainerVisual container) + container.ClearHitTestChildren(); } } }