From 7a13d819fdbf0cc26b144bda2a97ec59344f39b7 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 14 Nov 2019 01:03:58 +0100 Subject: [PATCH] Optimize and add new efficient visual tree extensions. --- src/Avalonia.Controls/ItemsControl.cs | 25 +-- .../WindowNotificationManager.cs | 10 +- .../Primitives/SelectingItemsControl.cs | 18 +- src/Avalonia.Controls/TopLevel.cs | 8 +- src/Avalonia.Input/FocusManager.cs | 18 +- src/Avalonia.Input/Pointer.cs | 8 +- src/Avalonia.Layout/Layoutable.cs | 32 +++- src/Avalonia.Visuals/Visual.cs | 20 ++- .../VisualTree/VisualExtensions.cs | 168 +++++++++++++++++- .../Traversal/VisualTreeTraversal.cs | 64 +++++++ .../VisualExtensionsTests.cs | 76 ++++++++ 11 files changed, 392 insertions(+), 55 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index bf22f0a08a..94c34b28d6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -5,7 +5,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; @@ -324,20 +323,24 @@ namespace Avalonia.Controls return; } - var current = focus.Current - .GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.VisualParent == container); + IVisual current = focus.Current; - if (current != null) + while (current != null) { - var next = GetNextControl(container, direction.Value, current, false); - - if (next != null) + if (current.VisualParent == container && current is IInputElement inputElement) { - focus.Focus(next, NavigationMethod.Directional); - e.Handled = true; + IInputElement next = GetNextControl(container, direction.Value, inputElement, false); + + if (next != null) + { + focus.Focus(next, NavigationMethod.Directional); + e.Handled = true; + } + + break; } + + current = current.VisualParent; } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index aa91224572..1a9347e317 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -149,15 +149,9 @@ namespace Avalonia.Controls.Notifications /// The that will be the host. private void Install(Window host) { - var adornerLayer = host.GetVisualDescendants() - .OfType() - .FirstOrDefault() - ?.AdornerLayer; + var adornerLayer = host.FindDescendantOfType()?.AdornerLayer; - if (adornerLayer != null) - { - adornerLayer.Children.Add(this); - } + adornerLayer?.Children.Add(this); } } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 329b086a7c..a5bbcec186 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -13,7 +13,6 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Logging; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -269,11 +268,20 @@ namespace Avalonia.Controls.Primitives /// The container or null if the event did not originate in a container. protected IControl GetContainerFromEventSource(IInteractive eventSource) { - var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1); + var parent = (IVisual)eventSource; - return item; + while (parent != null) + { + if (parent is IControl control && control.LogicalParent == this + && ItemContainerGenerator?.IndexFromContainer(control) != -1) + { + return control; + } + + parent = parent.VisualParent; + } + + return null; } /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 293809bf51..131a1304d7 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using System.Reactive.Linq; -using Avalonia.Controls.Notifications; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; @@ -15,7 +13,6 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; -using Avalonia.VisualTree; using JetBrains.Annotations; namespace Avalonia.Controls @@ -296,10 +293,7 @@ namespace Avalonia.Controls /// The window scaling. protected virtual void HandleScalingChanged(double scaling) { - foreach (ILayoutable control in this.GetSelfAndVisualDescendants()) - { - control.InvalidateMeasure(); - } + InvalidateSelfAndDescendantsMeasure(); } /// diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 104ac9cb61..77902a7390 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -180,18 +180,18 @@ namespace Avalonia.Input if (sender == e.Source && ev.MouseButton == MouseButton.Left) { - var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement); + IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement; - if (element == null || !CanFocus(element)) + while (element != null) { - element = element.GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(CanFocus); - } + if (element is IInputElement inputElement && CanFocus(inputElement)) + { + Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers); - if (element != null) - { - Instance?.Focus(element, NavigationMethod.Pointer, ev.InputModifiers); + break; + } + + element = element.VisualParent; } } } diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 819d231b31..00222e92cf 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -55,9 +55,11 @@ namespace Avalonia.Input Captured.DetachedFromVisualTree += OnCaptureDetached; } - IInputElement GetNextCapture(IVisual parent) => - parent as IInputElement ?? parent.GetVisualAncestors().OfType().FirstOrDefault(); - + IInputElement GetNextCapture(IVisual parent) + { + return parent as IInputElement ?? parent.FindAncestorOfType(); + } + private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) { Capture(GetNextCapture(e.Parent)); diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 4732808b91..f9e438cbeb 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using Avalonia.Logging; using Avalonia.VisualTree; @@ -693,14 +692,37 @@ namespace Avalonia.Layout return finalSize; } - /// - protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent) + /// + /// Invalidates measure for this instance and all visual children. + /// + protected void InvalidateSelfAndDescendantsMeasure() { - foreach (ILayoutable i in this.GetSelfAndVisualDescendants()) + void InnerInvalidateMeasure(IVisual target) { - i.InvalidateMeasure(); + if (target is ILayoutable layoutable) + { + layoutable.InvalidateMeasure(); + } + + var visualChildren = target.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (int i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + InnerInvalidateMeasure(child); + } } + InnerInvalidateMeasure(this); + } + + /// + protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) + { + InvalidateSelfAndDescendantsMeasure(); + base.OnVisualParentChanged(oldParent, newParent); } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index f4306d3929..c70e9a49fb 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Specialized; using System.Linq; -using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Data; using Avalonia.Logging; @@ -173,7 +172,22 @@ namespace Avalonia /// public bool IsEffectivelyVisible { - get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); } + get + { + IVisual node = this; + + while (node != null) + { + if (!node.IsVisible) + { + return false; + } + + node = node.VisualParent; + } + + return true; + } } /// @@ -552,7 +566,7 @@ namespace Avalonia if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) { - var root = this.GetVisualAncestors().OfType().FirstOrDefault(); + var root = this.FindAncestorOfType(); var e = new VisualTreeAttachmentEventArgs(_visualParent, root); OnAttachedToVisualTreeCore(e); } diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index 567b676b1e..623e64d136 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -14,7 +14,7 @@ namespace Avalonia.VisualTree public static class VisualExtensions { /// - /// Calculates the distance from a visual's . + /// Calculates the distance from a visual's ancestor. /// /// The visual. /// The ancestor visual. @@ -30,13 +30,39 @@ namespace Avalonia.VisualTree while (visual != null && visual != ancestor) { - ++result; visual = visual.VisualParent; + + result++; } return visual != null ? result : -1; } + /// + /// Calculates the distance from a visual's root. + /// + /// The visual. + /// + /// The number of steps from the visual to the root. + /// + public static int CalculateDistanceFromRoot(IVisual visual) + { + Contract.Requires(visual != null); + + var result = 0; + + visual = visual?.VisualParent; + + while (visual != null) + { + visual = visual.VisualParent; + + result++; + } + + return result; + } + /// /// Tries to get the first common ancestor of two visuals. /// @@ -47,8 +73,64 @@ namespace Avalonia.VisualTree { Contract.Requires(visual != null); - return visual.GetSelfAndVisualAncestors().Intersect(target.GetSelfAndVisualAncestors()) - .FirstOrDefault(); + if (target is null) + { + return null; + } + + IVisual GoUpwards(ref IVisual node, int count, IVisual parentCandidate) + { + for (int i = 0; i < count; ++i) + { + node = node.VisualParent; + + // Other node can be our ancestor so we might find it early on. + if (node == parentCandidate) + { + return node; + } + } + + return null; + } + + // We want to find lowest node first, then make sure that both nodes are at the same height. + // By doing that we can sometimes find out that other node is our lowest common ancestor. + var firstHeight = CalculateDistanceFromRoot(visual); + var secondHeight = CalculateDistanceFromRoot(target); + + IVisual found = firstHeight > secondHeight ? + GoUpwards(ref visual, firstHeight - secondHeight, target) : + GoUpwards(ref target, secondHeight - firstHeight, target); + + if (found != null) + { + return found; + } + + if (visual == target) + { + return visual; + } + + while (true) + { + IVisual firstParent = visual.VisualParent; + IVisual secondParent = target.VisualParent; + + if (firstParent == secondParent) + { + return firstParent; + } + + visual = visual.VisualParent; + target = target.VisualParent; + + if (visual == null || target == null) + { + return null; + } + } } /// @@ -69,6 +151,59 @@ namespace Avalonia.VisualTree } } + /// + /// Finds first ancestor of given type. + /// + /// Ancestor type. + /// The visual. + /// If given visual should be included in search. + /// First ancestor of given type. + public static T FindAncestorOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + IVisual parent = includeSelf ? visual : visual.VisualParent; + + while (parent != null) + { + if (parent is T result) + { + return result; + } + + parent = parent.VisualParent; + } + + return null; + } + + /// + /// Finds first descendant of given type. + /// + /// Descendant type. + /// The visual. + /// If given visual should be included in search. + /// First descendant of given type. + public static T FindDescendantOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + if (includeSelf && visual is T result) + { + return result; + } + + FindDescendantOfTypeCore(visual); + + return null; + } + /// /// Enumerates an and its ancestors in the visual tree. /// @@ -249,6 +384,31 @@ namespace Avalonia.VisualTree .Select(x => x.Element); } + private static T FindDescendantOfTypeCore(IVisual visual) where T : class + { + var visualChildren = visual.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + if (child is T result) + { + return result; + } + + var childResult = FindDescendantOfTypeCore(child); + + if (!(childResult is null)) + { + return childResult; + } + } + + return null; + } + private class ZOrderElement : IComparable { public IVisual Element { get; set; } diff --git a/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs new file mode 100644 index 0000000000..fc2380d670 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Traversal +{ + [MemoryDiagnoser] + public class VisualTreeTraversal + { + private readonly TestRoot _root; + private readonly List _controls = new List(); + private readonly List _shuffledControls; + + public VisualTreeTraversal() + { + var panel = new StackPanel(); + _root = new TestRoot { Child = panel, Renderer = new NullRenderer()}; + _controls.Add(panel); + _controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 4); + + var random = new Random(1); + + _shuffledControls = _controls.OrderBy(r => random.Next()).ToList(); + + _root.LayoutManager.ExecuteInitialLayoutPass(_root); + } + + [Benchmark] + public void FindAncestorOfType_Linq() + { + foreach (Control control in _controls) + { + control.GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(); + } + } + + [Benchmark] + public void FindAncestorOfType_Optimized() + { + foreach (Control control in _controls) + { + control.FindAncestorOfType(); + } + } + + [Benchmark] + public void FindCommonVisualAncestor() + { + foreach (IVisual first in _controls) + { + foreach (Control second in _shuffledControls) + { + first.FindCommonVisualAncestor(second); + } + } + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs index a8d8c07d8b..51806135af 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs @@ -2,12 +2,88 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Visuals.UnitTests { public class VisualExtensionsTests { + [Fact] + public void FindCommonVisualAncestor_Two_Subtrees_Uniform_Height() + { + Control left, right; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + new Decorator + { + Child = new Decorator + { + Child = left = new Decorator() + } + }, + new Decorator + { + Child = new Decorator + { + Child = right = new Decorator() + } + } + } + } + }; + + var ancestor = left.FindCommonVisualAncestor(right); + Assert.Equal(root.Child, ancestor); + + ancestor = right.FindCommonVisualAncestor(left); + Assert.Equal(root.Child, ancestor); + } + + [Fact] + public void FindCommonVisualAncestor_Two_Subtrees_NonUniform_Height() + { + Control left, right; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + new Decorator + { + Child = new Decorator + { + Child = left = new Decorator() + } + }, + new Decorator + { + Child = new Decorator + { + Child = new Decorator + { + Child = right = new Decorator() + } + } + } + } + } + }; + + var ancestor = left.FindCommonVisualAncestor(right); + Assert.Equal(root.Child, ancestor); + + ancestor = right.FindCommonVisualAncestor(left); + Assert.Equal(root.Child, ancestor); + } + [Fact] public void TranslatePoint_Should_Respect_RenderTransforms() {