diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 4c0a551c72..c54829d750 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -739,6 +739,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { self = [super init]; [self setWantsBestResolutionOpenGLSurface:true]; + [self setWantsLayer:YES]; _parent = parent; _area = nullptr; return self; 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 a0df186eb7..5a8711a21c 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 @@ -302,10 +299,7 @@ namespace Avalonia.Controls /// The window scaling. protected virtual void HandleScalingChanged(double scaling) { - foreach (ILayoutable control in this.GetSelfAndVisualDescendants()) - { - control.InvalidateMeasure(); - } + LayoutHelper.InvalidateSelfAndChildrenMeasure(this); } /// diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1816a6c81d..f66a248aaf 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -336,7 +336,6 @@ namespace Avalonia.Controls if (close) { PlatformImpl?.Dispose(); - HandleClosed(); } } } 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/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index cfb4b14b1c..c235bcd90f 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.VisualTree; namespace Avalonia.Layout { @@ -61,5 +62,31 @@ namespace Avalonia.Layout return availableSize; } + + /// + /// Invalidates measure for given control and all visual children recursively. + /// + public static void InvalidateSelfAndChildrenMeasure(ILayoutable control) + { + void InnerInvalidateMeasure(IVisual target) + { + if (target is ILayoutable targetLayoutable) + { + targetLayoutable.InvalidateMeasure(); + } + + var visualChildren = target.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (int i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + InnerInvalidateMeasure(child); + } + } + + InnerInvalidateMeasure(control); + } } } diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 4732808b91..9d0a5c57ee 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; @@ -694,12 +693,9 @@ namespace Avalonia.Layout } /// - protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent) + protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { - foreach (ILayoutable i in this.GetSelfAndVisualDescendants()) - { - i.InvalidateMeasure(); - } + LayoutHelper.InvalidateSelfAndChildrenMeasure(this); base.OnVisualParentChanged(oldParent, newParent); } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index cf3c4dc855..5e1bcde2f6 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -288,7 +288,7 @@ namespace Avalonia var list = new AvaloniaList { ResetBehavior = ResetBehavior.Remove, - Validate = ValidateLogicalChild + Validate = logical => ValidateLogicalChild(logical) }; list.CollectionChanged += LogicalChildrenCollectionChanged; _logicalChildren = list; diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index f4306d3929..465b3a65be 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; @@ -121,7 +120,7 @@ namespace Avalonia { var visualChildren = new AvaloniaList(); visualChildren.ResetBehavior = ResetBehavior.Remove; - visualChildren.Validate = ValidateVisualChild; + visualChildren.Validate = visual => ValidateVisualChild(visual); visualChildren.CollectionChanged += VisualChildrenChanged; VisualChildren = visualChildren; } @@ -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..8e3c7e0765 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,53 @@ namespace Avalonia.VisualTree { Contract.Requires(visual != null); - return visual.GetSelfAndVisualAncestors().Intersect(target.GetSelfAndVisualAncestors()) - .FirstOrDefault(); + if (target is null) + { + return null; + } + + void GoUpwards(ref IVisual node, int count) + { + for (int i = 0; i < count; ++i) + { + node = node.VisualParent; + } + } + + // 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); + + if (firstHeight > secondHeight) + { + GoUpwards(ref visual, firstHeight - secondHeight); + } + else + { + GoUpwards(ref target, secondHeight - firstHeight); + } + + if (visual == target) + { + return visual; + } + + while (visual != null && target != null) + { + IVisual firstParent = visual.VisualParent; + IVisual secondParent = target.VisualParent; + + if (firstParent == secondParent) + { + return firstParent; + } + + visual = visual.VisualParent; + target = target.VisualParent; + } + + return null; } /// @@ -69,6 +140,57 @@ 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; + } + + return FindDescendantOfTypeCore(visual); + } + /// /// Enumerates an and its ancestors in the visual tree. /// @@ -249,6 +371,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/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index cb9d3389e4..a431ffcc1a 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -79,7 +79,7 @@ namespace Avalonia.X11 atoms = atoms.Concat(new[] {_x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE}) .ToArray(); XChangeProperty(_x11.Display, window, property, - target, 32, PropertyMode.Replace, atoms, atoms.Length); + _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length); return property; } else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 2c6425e26c..ed32382760 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1098,7 +1098,10 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target); - + + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern HRESULT RevokeDragDrop(IntPtr hwnd); + [DllImport("ole32.dll", EntryPoint = "OleInitialize")] public static extern HRESULT OleInitialize(IntPtr val); diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs index d454c797fa..c6e04a29b4 100644 --- a/src/Windows/Avalonia.Win32/OleContext.cs +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -7,9 +7,9 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class OleContext + internal class OleContext { - private static OleContext fCurrent; + private static OleContext s_current; internal static OleContext Current { @@ -18,13 +18,12 @@ namespace Avalonia.Win32 if (!IsValidOleThread()) return null; - if (fCurrent == null) - fCurrent = new OleContext(); - return fCurrent; + if (s_current == null) + s_current = new OleContext(); + return s_current; } } - private OleContext() { UnmanagedMethods.HRESULT res = UnmanagedMethods.OleInitialize(IntPtr.Zero); @@ -43,9 +42,21 @@ namespace Avalonia.Win32 internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target) { if (hwnd?.HandleDescriptor != "HWND" || target == null) + { return false; + } return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK; } + + internal bool UnregisterDragDrop(IPlatformHandle hwnd) + { + if (hwnd?.HandleDescriptor != "HWND") + { + return false; + } + + return UnmanagedMethods.RevokeDragDrop(hwnd.Handle) == UnmanagedMethods.HRESULT.S_OK; + } } } diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index b17e0d6c09..37d047689c 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -6,7 +6,7 @@ using IDataObject = Avalonia.Input.IDataObject; namespace Avalonia.Win32 { - class OleDropTarget : IDropTarget + internal class OleDropTarget : IDropTarget { private readonly IInputRoot _target; private readonly ITopLevelImpl _tl; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0a39a9e0f6..0f5db58dfe 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -253,6 +253,11 @@ namespace Avalonia.Win32 public void Dispose() { + if (_dropTarget != null) + { + OleContext.Current?.UnregisterDragDrop(Handle); + _dropTarget = null; + } if (_hwnd != IntPtr.Zero) { UnmanagedMethods.DestroyWindow(_hwnd); 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.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index d87014f646..0508edd92f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -228,8 +228,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var windowImpl = Mock.Of(x => x.Scaling == 1); - var target = new Window(windowImpl); + var target = new Window(); target.Show(); target.Close(); diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index c33ec72141..a6701ef655 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -28,6 +28,10 @@ namespace Avalonia.UnitTests return CreatePopupMock().Object; }); + mock.Setup(x => x.Dispose()).Callback(() => + { + mock.Object.Closed?.Invoke(); + }); PixelPoint pos = default; mock.SetupGet(x => x.Position).Returns(() => pos); mock.Setup(x => x.Move(It.IsAny())).Callback(new Action(np => pos = np)); diff --git a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs index a8d8c07d8b..781169cfa6 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs @@ -2,12 +2,184 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Visuals.UnitTests { public class VisualExtensionsTests { + [Fact] + public void FindAncestorOfType_Finds_Direct_Parent() + { + StackPanel target; + + var root = new TestRoot + { + Child = target = new StackPanel() + }; + + Assert.Equal(root, target.FindAncestorOfType()); + } + + [Fact] + public void FindAncestorOfType_Finds_Ancestor_Of_Nested_Child() + { + Button target; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + (target = new Button()) + } + } + } + } + }; + + Assert.Equal(root, target.FindAncestorOfType()); + } + + [Fact] + public void FindDescendantOfType_Finds_Direct_Child() + { + StackPanel target; + + var root = new TestRoot + { + Child = target = new StackPanel() + }; + + Assert.Equal(target, root.FindDescendantOfType()); + } + + [Fact] + public void FindDescendantOfType_Finds_Nested_Child() + { + Button target; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + (target = new Button()) + } + } + } + } + }; + + Assert.Equal(target, root.FindDescendantOfType