diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs index c5ea114b7d95..cc34e9d47d3f 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs @@ -97,13 +97,30 @@ IWindow CreateWindowForContent(IElement view) return window; } + protected Task CreateHandlerAndAddToWindow(IElement view, Func action) + { + return CreateHandlerAndAddToWindow(CreateWindowForContent(view), handler => + { + return action(); + }, MauiContext, null); + } + + protected Task CreateHandlerAndAddToWindow(IElement view, Func action) + where THandler : class, IElementHandler + { + return CreateHandlerAndAddToWindow(view, handler => + { + return action(handler); + }, MauiContext, null); + } + protected Task CreateHandlerAndAddToWindow(IElement view, Action action) { return CreateHandlerAndAddToWindow(CreateWindowForContent(view), handler => { action(); return Task.CompletedTask; - }); + }, MauiContext, null); } protected Task CreateHandlerAndAddToWindow(IElement view, Action action) @@ -113,7 +130,7 @@ protected Task CreateHandlerAndAddToWindow(IElement view, Action(window, async () => await OnLoadedAsync(content as VisualElement); + // Gives time for the measure/layout pass to settle + await Task.Yield(); if (view is VisualElement veBeingTested) await OnLoadedAsync(veBeingTested); diff --git a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.Android.cs index 1a1bf7c92176..bbb9ae082f88 100644 --- a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.Android.cs @@ -113,7 +113,7 @@ await CreateHandlerAndAddToWindow(new Window(tabbedPage), asy BottomNavigationView GetBottomNavigationView(TabbedViewHandler tabViewHandler) { - var layout = (tabViewHandler.PlatformView as Android.Views.IViewParent).FindParent((view) => view is CoordinatorLayout) + var layout = tabViewHandler.PlatformView.FindParent((view) => view is CoordinatorLayout) as CoordinatorLayout; return layout.GetFirstChildOfType(); diff --git a/src/Controls/tests/DeviceTests/Elements/VisualElementTree/FindVisualTreeElementInsideTestCase.cs b/src/Controls/tests/DeviceTests/Elements/VisualElementTree/FindVisualTreeElementInsideTestCase.cs new file mode 100644 index 000000000000..8173d0f4550d --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/VisualElementTree/FindVisualTreeElementInsideTestCase.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Xunit.Abstractions; + +namespace Microsoft.Maui.DeviceTests +{ + public class FindVisualTreeElementInsideTestCase : IXunitSerializable + { + + public string TestCaseName { get; private set; } + + public FindVisualTreeElementInsideTestCase() { } + + public FindVisualTreeElementInsideTestCase(string testCaseName) + { + TestCaseName = testCaseName; + } + + public void Deserialize(IXunitSerializationInfo info) + { + TestCaseName = info.GetValue(nameof(TestCaseName)); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(TestCaseName), TestCaseName, typeof(string)); + } + + public override string ToString() + { + return TestCaseName; + } + + public (VisualElement rootView, VisualElement testView) CreateVisualElement() + { + switch (TestCaseName) + { + case "CollectionView": + { + var cv = new CollectionView(); + NestingView view = new NestingView(); + cv.ItemTemplate = new DataTemplate(() => + { + return view; + }); + cv.ItemsSource = new[] { 0 }; + return (cv, view); + } + case "ContentView": + { + var contentView = new ContentView(); + NestingView view = new NestingView(); + contentView.ControlTemplate = new ControlTemplate(() => + { + return view; + }); + return (contentView, view); + } + } + + throw new Exception(String.Concat(TestCaseName, " ", "Not Found")); + } + } + + public class FindVisualTreeElementInsideTestCases : IEnumerable + { + private readonly List _data = new() + { + new object[] { new FindVisualTreeElementInsideTestCase("CollectionView") }, + new object[] { new FindVisualTreeElementInsideTestCase("ContentView") } + }; + + public IEnumerator GetEnumerator() + => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/VisualElementTree/VisualElementTreeTests.cs b/src/Controls/tests/DeviceTests/Elements/VisualElementTree/VisualElementTreeTests.cs new file mode 100644 index 000000000000..36ddc4b86a5b --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/VisualElementTree/VisualElementTreeTests.cs @@ -0,0 +1,214 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Handlers; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Hosting; +using Microsoft.Maui.Platform; +using Xunit; +using System.Collections.Generic; +using ContentView = Microsoft.Maui.Controls.ContentView; +using Microsoft.Maui.Controls.Handlers.Items; +#if ANDROID || IOS || MACCATALYST +using ShellHandler = Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer; +#endif + +namespace Microsoft.Maui.DeviceTests +{ + [Category(TestCategory.VisualElementTree)] +#if ANDROID || IOS || MACCATALYST + [Collection(ControlsHandlerTestBase.RunInNewWindowCollection)] +#endif + public partial class VisualElementTreeTests : ControlsHandlerTestBase + { + void SetupBuilder() + { + EnsureHandlerCreated(builder => + { + builder.SetupShellHandlers(); + + builder.ConfigureMauiHandlers(handlers => + { +#if IOS || MACCATALYST + handlers.AddHandler(typeof(Controls.NavigationPage), typeof(Controls.Handlers.Compatibility.NavigationRenderer)); +#else + handlers.AddHandler(typeof(Controls.NavigationPage), typeof(NavigationViewHandler)); +#endif + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + }); + }); + } + + [Fact] + public async Task GetVisualTreeElements() + { + SetupBuilder(); + var label = new Label() { Text = "Find Me" }; + var page = new ContentPage() { Title = "Title Page" }; + page.Content = new VerticalStackLayout() + { + label + }; + + var rootPage = await InvokeOnMainThreadAsync(() => + new NavigationPage(page) + ); + + await CreateHandlerAndAddToWindow(rootPage, async handler => + { + await OnFrameSetToNotEmpty(label); + var locationOnScreen = label.GetLocationOnScreen().Value; + var labelFrame = label.Frame; + var window = rootPage.Window; + + // Find label at the top left corner + var topLeft = new Graphics.Point(locationOnScreen.X + 1, locationOnScreen.Y + 1); + + Assert.True(window.GetVisualTreeElements(topLeft).Contains(label), $"Unable to find label using top left coordinate: {topLeft} with label location: {label.GetBoundingBox()}"); + + // find label at the bottom right corner + var bottomRight = new Graphics.Point( + locationOnScreen.X + labelFrame.Width - 1, + locationOnScreen.Y + labelFrame.Height - 1); + + Assert.True(window.GetVisualTreeElements(bottomRight).Contains(label), $"Unable to find label using bottom right coordinate: {bottomRight} with label location: {label.GetBoundingBox()}"); + + // Ensure that the point directly outside the bounds of the label doesn't + // return the label + Assert.DoesNotContain(label, window.GetVisualTreeElements( + locationOnScreen.X + labelFrame.Width + 1, + locationOnScreen.Y + labelFrame.Height + 1 + )); + + }); + } + + [Fact] + public async Task FindPlatformViewInsideLayout() + { + SetupBuilder(); + var button = new Button(); + VerticalStackLayout views = new VerticalStackLayout() + { + new VerticalStackLayout() + { + button + } + }; + + await CreateHandlerAndAddToWindow(views, () => + { + var platformView = button.ToPlatform(); + var foundTreeElement = button.ToPlatform().GetVisualTreeElement(); + + Assert.Equal(button, foundTreeElement); + }); + } + + [Fact] + public async Task FindPlatformViewInsideScrollView() + { + SetupBuilder(); + var button = new Button(); + ScrollView view = new ScrollView() + { + Content = button + }; + + await CreateHandlerAndAddToWindow(view, () => + { + var platformView = button.ToPlatform(); + var foundTreeElement = button.ToPlatform().GetVisualTreeElement(); + + Assert.Equal(button, foundTreeElement); + }); + } + + [Fact] + public async Task FindPlatformViewViaDefaultContainer() + { + SetupBuilder(); + var button = new Button(); + NestingView view = new NestingView(); + view.AddLogicalChild(button); + + await CreateHandlerAndAddToWindow(view, () => + { + var platformView = button.ToPlatform(); + var foundTreeElement = button.ToPlatform().GetVisualTreeElement(); + + Assert.Equal(button, foundTreeElement); + }); + } + + [Fact] + public async Task FindVisualTreeElementWithArbitraryPlatformViewsAdded() + { + SetupBuilder(); + var button = new Button(); + NestingView view = new NestingView(); + + await CreateHandlerAndAddToWindow(view, (handler) => + { + handler + .PlatformView + .AddChild() + .AddChild() + .AddChild() + .AddChild(button, view); + + var platformView = button.ToPlatform(); + var foundTreeElement = button.ToPlatform().GetVisualTreeElement(); + + Assert.Equal(button, foundTreeElement); + }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task FindFirstMauiParentElement(bool searchAncestors) + { + SetupBuilder(); + var viewToLocate = new NestingView(); + NestingView view = new NestingView(); + + await CreateHandlerAndAddToWindow(view, (handler) => + { + var nestedChild = + handler.PlatformView + .AddChild(viewToLocate, view) + .AddChild() + .AddChild() + .AddChild(); + + var foundTreeElement = nestedChild.GetVisualTreeElement(searchAncestors); + + if (searchAncestors) + Assert.Equal(viewToLocate, foundTreeElement); + else + Assert.Null(foundTreeElement); + }); + } + + [Theory] + [ClassData(typeof(FindVisualTreeElementInsideTestCases))] + public async Task FindPlatformViewInsideView(FindVisualTreeElementInsideTestCase testCase) + { + SetupBuilder(); + + VisualElement rootView; + VisualElement viewToLocate; + + (rootView, viewToLocate) = testCase.CreateVisualElement(); + await CreateHandlerAndAddToWindow(rootView, () => + { + var platformView = viewToLocate.ToPlatform(); + var foundTreeElement = platformView.GetVisualTreeElement(); + Assert.Equal(viewToLocate, foundTreeElement); + }); + } + } +} diff --git a/src/Controls/tests/DeviceTests/Elements/VisualElementTreeTests.cs b/src/Controls/tests/DeviceTests/Elements/VisualElementTreeTests.cs deleted file mode 100644 index 9eaefc87f183..000000000000 --- a/src/Controls/tests/DeviceTests/Elements/VisualElementTreeTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Controls.Handlers; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Hosting; -using Microsoft.Maui.Platform; -using Xunit; -#if ANDROID || IOS || MACCATALYST -using ShellHandler = Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer; -#endif - -namespace Microsoft.Maui.DeviceTests -{ - [Category(TestCategory.VisualElementTree)] -#if ANDROID || IOS || MACCATALYST - [Collection(ControlsHandlerTestBase.RunInNewWindowCollection)] -#endif - public partial class VisualElementTreeTests : ControlsHandlerTestBase - { - void SetupBuilder() - { - EnsureHandlerCreated(builder => - { - builder.ConfigureMauiHandlers(handlers => - { - handlers.AddHandler(typeof(Controls.Shell), typeof(ShellHandler)); -#if IOS || MACCATALYST - handlers.AddHandler(typeof(Controls.NavigationPage), typeof(Controls.Handlers.Compatibility.NavigationRenderer)); -#else - handlers.AddHandler(typeof(Controls.NavigationPage), typeof(NavigationViewHandler)); -#endif - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); -#if WINDOWS - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); -#endif - }); - }); - } - - [Fact] - public async Task GetVisualTreeElements() - { - SetupBuilder(); - var label = new Label() { Text = "Find Me" }; - var page = new ContentPage() { Title = "Title Page" }; - page.Content = new VerticalStackLayout() - { - label - }; - - var rootPage = await InvokeOnMainThreadAsync(() => - new NavigationPage(page) - ); - - await CreateHandlerAndAddToWindow(rootPage, async handler => - { - await OnFrameSetToNotEmpty(label); - var locationOnScreen = label.GetLocationOnScreen().Value; - var labelFrame = label.Frame; - var window = rootPage.Window; - - // Find label at the top left corner - var topLeft = new Graphics.Point(locationOnScreen.X + 1, locationOnScreen.Y + 1); - - Assert.True(window.GetVisualTreeElements(topLeft).Contains(label), $"Unable to find label using top left coordinate: {topLeft} with label location: {label.GetBoundingBox()}"); - - // find label at the bottom right corner - var bottomRight = new Graphics.Point( - locationOnScreen.X + labelFrame.Width - 1, - locationOnScreen.Y + labelFrame.Height - 1); - - Assert.True(window.GetVisualTreeElements(bottomRight).Contains(label), $"Unable to find label using bottom right coordinate: {bottomRight} with label location: {label.GetBoundingBox()}"); - - // Ensure that the point directly outside the bounds of the label doesn't - // return the label - Assert.DoesNotContain(label, window.GetVisualTreeElements( - locationOnScreen.X + labelFrame.Width + 1, - locationOnScreen.Y + labelFrame.Height + 1 - )); - - }); - } - } -} diff --git a/src/Controls/tests/DeviceTests/TestClasses/NestingView.cs b/src/Controls/tests/DeviceTests/TestClasses/NestingView.cs new file mode 100644 index 000000000000..17e58769c307 --- /dev/null +++ b/src/Controls/tests/DeviceTests/TestClasses/NestingView.cs @@ -0,0 +1,123 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Handlers; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Hosting; +using Microsoft.Maui.Platform; +using Xunit; +#if IOS || MACCATALYST +using PlatformView = UIKit.UIView; +using ParentView = UIKit.UIView; +#elif ANDROID +using PlatformView = Android.Views.View; +using ParentView = Android.Views.IViewParent; +using Android.Content; +#elif WINDOWS +using PlatformView = Microsoft.UI.Xaml.FrameworkElement; +using ParentView = Microsoft.UI.Xaml.DependencyObject; +#elif TIZEN +using PlatformView = Tizen.NUI.BaseComponents.View; +using ParentView = Tizen.NUI.BaseComponents.View; +#else +using PlatformView = System.Object; +using ParentView = System.Object; +#endif + +namespace Microsoft.Maui.DeviceTests +{ + public class NestingView : View + { + protected override void OnHandlerChanged() + { + base.OnHandlerChanged(); + + if (Handler is NestingViewHandler sh) + { + foreach (var element in this.LogicalChildrenInternalBackingStore) + { + sh.PlatformView.AddChild(element.ToPlatform(Handler.MauiContext)); + } + } + } + } + + public class NestingViewPlatformView : +#if WINDOWS + UI.Xaml.Controls.StackPanel +#elif ANDROID + AndroidX.AppCompat.Widget.LinearLayoutCompat +#else + UIKit.UIView +#endif + { + +#if ANDROID + public NestingViewPlatformView(Context context) : base(context) + { + } +#endif + + public NestingViewPlatformView AddChild() + { + var nextChild = new NestingViewPlatformView( +#if ANDROID + Context +#endif + ); + + return (NestingViewPlatformView)AddChild(nextChild); + } + + + public PlatformView AddChild(IView view, NestingView rootNestingView) + { + var platformView = view.ToPlatform(rootNestingView.Handler.MauiContext); + AddChild(platformView); + rootNestingView.AddLogicalChild((Element)view); + return platformView; + } + + public T AddChild(IView view, NestingView rootNestingView) + where T : PlatformView + { + var platformView = view.ToPlatform(rootNestingView.Handler.MauiContext); + AddChild(platformView); + rootNestingView.AddLogicalChild((Element)view); + return (T)platformView; + } + + public PlatformView AddChild(PlatformView platformView) + { +#if WINDOWS + this.Children.Add(platformView); +#elif ANDROID + this.AddView(platformView); +#else + this.AddSubview(platformView); +#endif + + return platformView; + } + } + + public class NestingViewHandler : ViewHandler + { + public static IPropertyMapper + Mapper = new PropertyMapper(); + + public NestingViewHandler() + : base(Mapper) + { + } + + protected override NestingViewPlatformView CreatePlatformView() + { + return new NestingViewPlatformView( +#if ANDROID + Context +#endif + ); + } + } +} diff --git a/src/Core/src/Core/Extensions/VisualTreeElementExtensions.cs b/src/Core/src/Core/Extensions/VisualTreeElementExtensions.cs index 23728841e493..89f15a9b8d68 100644 --- a/src/Core/src/Core/Extensions/VisualTreeElementExtensions.cs +++ b/src/Core/src/Core/Extensions/VisualTreeElementExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform; #if WINDOWS using Microsoft.UI.Xaml; @@ -10,6 +11,26 @@ using WinRect = Windows.Foundation.Rect; #endif +#if (NETSTANDARD || !PLATFORM) || (NET6_0_OR_GREATER && !IOS && !ANDROID) +using IPlatformViewHandler = Microsoft.Maui.IViewHandler; +#endif +#if IOS || MACCATALYST +using PlatformView = UIKit.UIView; +using ParentView = UIKit.UIView; +#elif ANDROID +using PlatformView = Android.Views.View; +using ParentView = Android.Views.IViewParent; +#elif WINDOWS +using PlatformView = Microsoft.UI.Xaml.FrameworkElement; +using ParentView = Microsoft.UI.Xaml.DependencyObject; +#elif TIZEN +using PlatformView = Tizen.NUI.BaseComponents.View; +using ParentView = Tizen.NUI.BaseComponents.View; +#else +using PlatformView = System.Object; +using ParentView = System.Object; +#endif + namespace Microsoft.Maui { public static class VisualTreeElementExtensions @@ -136,6 +157,137 @@ static List GetVisualTreeElementsWindowsInternal(IVisualTree } #endif + /// + /// Locates the that's a best fit for the given platform view. + /// + /// + /// If an exact counterpart isn't found, then the + /// first within the ancestors of the given platform view will + /// be returned. + /// + /// The platform view. + /// + /// A visual tree element if found, otherwise. + /// + internal static IVisualTreeElement? GetVisualTreeElement( + this PlatformView platformView) => + platformView.GetVisualTreeElement(true); + + /// + /// Locates the that's a best fit for the given platform view. + /// + /// + /// If an exact counterpart isn't found, then the + /// first within the ancestors of the given platform view will + /// be returned. + /// + /// The platform view. + /// + /// to search within the ancestors of the given platform view; + /// otherwise, . + /// + /// A visual tree element if found, otherwise. + /// + internal static IVisualTreeElement? GetVisualTreeElement( + this PlatformView platformView, bool searchAncestors) + { + var platformParentPath = new List(); + IVisualTreeElement? foundParent = null; + + // Locate the first Platform View we can find that can return us its Maui Element + var nearestParentContainer = + platformView + .FindParent(x => + { + if (x is PlatformView pv) + platformParentPath.Add(pv); + + if (x is IVisualTreeElementProvidable backing) + { + foundParent = backing.GetElement(); + return foundParent is not null; + } + + return false; + }); + + platformParentPath.Reverse(); + + if (foundParent?.IsThisMyPlatformView(platformView) == true) + return foundParent; + + if (nearestParentContainer is null || foundParent is null) + return null; + + // Now that we have an xplat starting point + // Let's search back down the xplat tree to figure out what IElement to return + // This searches down the xplat tree to figure out what path going down the xplat tree + // matches up against the path we took to go up the platform tree + var returnValue = FindNextChild(foundParent, platformView, platformParentPath); + + // If we aren't searching ancestors, then we only want to return + // IVTE if it matches the found platformView + if (!searchAncestors && + returnValue != null && + !returnValue.IsThisMyPlatformView(platformView)) + { + return null; + } + + return returnValue; + + static IVisualTreeElement? FindNextChild( + IVisualTreeElement parent, + PlatformView platformView, + List platformParentPath) + { + var children = parent.GetVisualChildren(); + IVisualTreeElement? childMatch = null; + foreach (var child in children) + { + if (child is not IVisualTreeElement childVTE) + { + return parent; + } + + if (childVTE.IsThisMyPlatformView(platformView)) + { + return childVTE; + } + + // We only want to check children with platform components that have been realized + if (childVTE is IElement element && + element.Handler is IPlatformViewHandler pvh && + pvh.PlatformView is not null) + { + var indexOfPlatformView = platformParentPath.IndexOf(pvh.PlatformView); + + if (indexOfPlatformView < 0) + continue; + + childMatch = child; + platformParentPath.RemoveRange(0, indexOfPlatformView + 1); + break; + } + } + + // If I've ran out of children then we just return the parent + // as the furthest down element we've been able to match to + if (childMatch is null) + return parent; + + return FindNextChild(childMatch, platformView, platformParentPath); + } + } + + internal static bool IsThisMyPlatformView(this IVisualTreeElement? visualTreeElement, PlatformView platformView) + { + if (visualTreeElement is IElement element) + return element.IsThisMyPlatformView(platformView); + + return false; + } + static List GetVisualTreeElementsInternal(IVisualTreeElement visualElement, Predicate intersectElementBounds) { var elements = new List(); diff --git a/src/Core/src/Core/IVisualTreeElementProvidable.cs b/src/Core/src/Core/IVisualTreeElementProvidable.cs new file mode 100644 index 000000000000..67360cb83cb3 --- /dev/null +++ b/src/Core/src/Core/IVisualTreeElementProvidable.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Maui +{ + interface IVisualTreeElementProvidable + { + IVisualTreeElement? GetElement(); + } +} diff --git a/src/Core/src/Platform/Android/ContentViewGroup.cs b/src/Core/src/Platform/Android/ContentViewGroup.cs index 98a121e9a320..d04aa8aab953 100644 --- a/src/Core/src/Platform/Android/ContentViewGroup.cs +++ b/src/Core/src/Platform/Android/ContentViewGroup.cs @@ -9,7 +9,7 @@ namespace Microsoft.Maui.Platform { - public class ContentViewGroup : PlatformContentViewGroup, ICrossPlatformLayoutBacking + public class ContentViewGroup : PlatformContentViewGroup, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable { IBorderStroke? _clip; readonly Context _context; @@ -131,5 +131,16 @@ internal IBorderStroke? Clip Path? platformPath = clipShape.ToPlatform(bounds, strokeThickness, true); return platformPath; } + + IVisualTreeElement? IVisualTreeElementProvidable.GetElement() + { + if (CrossPlatformLayout is IVisualTreeElement layoutElement && + layoutElement.IsThisMyPlatformView(this)) + { + return layoutElement; + } + + return null; + } } } \ No newline at end of file diff --git a/src/Core/src/Platform/Android/LayoutViewGroup.cs b/src/Core/src/Platform/Android/LayoutViewGroup.cs index 2c2bbf0b4325..6b4f40dad1d2 100644 --- a/src/Core/src/Platform/Android/LayoutViewGroup.cs +++ b/src/Core/src/Platform/Android/LayoutViewGroup.cs @@ -11,7 +11,7 @@ namespace Microsoft.Maui.Platform { - public class LayoutViewGroup : ViewGroup, ICrossPlatformLayoutBacking + public class LayoutViewGroup : ViewGroup, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable { readonly ARect _clipRect = new(); readonly Context _context; @@ -131,5 +131,16 @@ public override bool OnTouchEvent(MotionEvent? e) return base.OnTouchEvent(e); } + + IVisualTreeElement? IVisualTreeElementProvidable.GetElement() + { + if (CrossPlatformLayout is IVisualTreeElement layoutElement && + layoutElement.IsThisMyPlatformView(this)) + { + return layoutElement; + } + + return null; + } } } diff --git a/src/Core/src/Platform/Android/MauiSwipeView.cs b/src/Core/src/Platform/Android/MauiSwipeView.cs index 5679a06efeae..f4872d467c3c 100644 --- a/src/Core/src/Platform/Android/MauiSwipeView.cs +++ b/src/Core/src/Platform/Android/MauiSwipeView.cs @@ -209,7 +209,7 @@ void PropagateParentTouch() AView? itemContentView = null; - var parentFound = _contentView.Parent.FindParent(parent => + var parentFound = _contentView.FindParent(parent => { if (parent is RecyclerView) return true; diff --git a/src/Core/src/Platform/Android/ViewGroupExtensions.cs b/src/Core/src/Platform/Android/ViewGroupExtensions.cs index 7760be357421..cbedac60170a 100644 --- a/src/Core/src/Platform/Android/ViewGroupExtensions.cs +++ b/src/Core/src/Platform/Android/ViewGroupExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using AView = Android.Views.View; using AViewGroup = Android.Views.ViewGroup; @@ -53,5 +54,13 @@ public static bool TryGetFirstChildOfType(this AViewGroup viewGroup, [NotNull result = viewGroup.GetFirstChildOfType(); return result is not null; } + + internal static T? GetChildAt(this AView view, int index) where T : AView + { + if (view is AViewGroup viewGroup && viewGroup.ChildCount < index) + return (T?)viewGroup.GetChildAt(index); + + return null; + } } } diff --git a/src/Core/src/Platform/Tizen/ViewExtensions.cs b/src/Core/src/Platform/Tizen/ViewExtensions.cs index f83deb13883a..4689a1f4035f 100644 --- a/src/Core/src/Platform/Tizen/ViewExtensions.cs +++ b/src/Core/src/Platform/Tizen/ViewExtensions.cs @@ -341,5 +341,10 @@ internal static bool NeedsContainer(this IView? view) return false; } + + internal static T? GetChildAt(this NView view, int index) where T : NView + { + return (T?)view.Children[index]; + } } } diff --git a/src/Core/src/Platform/ViewExtensions.cs b/src/Core/src/Platform/ViewExtensions.cs index 0d93a60082e6..6bd2317507ff 100644 --- a/src/Core/src/Platform/ViewExtensions.cs +++ b/src/Core/src/Platform/ViewExtensions.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.Maui.Media; using System.IO; +using System.Collections.Generic; #if (NETSTANDARD || !PLATFORM) || (NET6_0_OR_GREATER && !IOS && !ANDROID) using IPlatformViewHandler = Microsoft.Maui.IViewHandler; @@ -66,11 +67,25 @@ public static IPlatformViewHandler ToHandler(this IView view, IMauiContext conte return default; } - internal static ParentView? FindParent(this ParentView? view, Func searchExpression) + // Only Windows and Android have different types for the Parent type +#if WINDOWS || ANDROID + internal static ParentView? FindParent(this PlatformView? view, Func searchExpression) { - if (searchExpression(view)) - return view; + if (view?.Parent is ParentView pv) + { + if (searchExpression(pv)) + return pv; + + return pv.FindParent(searchExpression); + } + return default; + } +#else + internal +#endif + static ParentView? FindParent(this ParentView? view, Func searchExpression) + { while (view != null) { var parent = view?.GetParent() as ParentView; @@ -100,6 +115,17 @@ public static IPlatformViewHandler ToHandler(this IView view, IMauiContext conte } #endif + internal static bool IsThisMyPlatformView(this IElement? element, PlatformView platformView) + { + if (element is not null && + element.Handler is IPlatformViewHandler pvh) + { + return pvh.PlatformView == platformView || pvh.ContainerView == platformView; + } + + return false; + } + internal static IDisposable OnUnloaded(this IElement element, Action action) { #if PLATFORM @@ -159,5 +185,39 @@ internal static bool IsLoadedOnPlatform(this IElement element) #endif } + + +#if PLATFORM + internal static T? FindDescendantView(this PlatformView view, Func predicate) where T : PlatformView + { + var queue = new Queue(); + queue.Enqueue(view); + + while (queue.Count > 0) + { + var descendantView = queue.Dequeue(); + + if (descendantView is T result && predicate.Invoke(result)) + return result; + + int i = 0; + PlatformView? child; + while ((child = descendantView?.GetChildAt(i)) is not null) + { +#if TIZEN + // I had to add this check for Tizen to compile. + // I think Tizen isn't accounting for the null check + // in the while loop correctly + if (child is null) + break; +#endif + queue.Enqueue(child); + i++; + } + } + + return null; + } +#endif } } diff --git a/src/Core/src/Platform/Windows/MauiPanel.cs b/src/Core/src/Platform/Windows/MauiPanel.cs index e729243e16fc..c8866fb651c9 100644 --- a/src/Core/src/Platform/Windows/MauiPanel.cs +++ b/src/Core/src/Platform/Windows/MauiPanel.cs @@ -6,7 +6,7 @@ namespace Microsoft.Maui.Platform { - public abstract class MauiPanel : Panel, ICrossPlatformLayoutBacking + public abstract class MauiPanel : Panel, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable { public ICrossPlatformLayout? CrossPlatformLayout { @@ -52,5 +52,16 @@ protected override WSize ArrangeOverride(WSize finalSize) return actual.ToPlatform(); } + + IVisualTreeElement? IVisualTreeElementProvidable.GetElement() + { + if (CrossPlatformLayout is IVisualTreeElement layoutElement && + layoutElement.IsThisMyPlatformView(this)) + { + return layoutElement; + } + + return null; + } } } diff --git a/src/Core/src/Platform/Windows/ViewExtensions.cs b/src/Core/src/Platform/Windows/ViewExtensions.cs index 6d355b1d84f6..d1e9ca3fdee2 100644 --- a/src/Core/src/Platform/Windows/ViewExtensions.cs +++ b/src/Core/src/Platform/Windows/ViewExtensions.cs @@ -354,6 +354,14 @@ internal static Graphics.Rect GetBoundingBox(this FrameworkElement? platformView return null; } + internal static T? GetChildAt(this DependencyObject view, int index) where T : DependencyObject + { + if (VisualTreeHelper.GetChildrenCount(view) >= index) + return null; + + return VisualTreeHelper.GetChild(view, index) as T; + } + internal static void UnfocusControl(Control control) { if (control == null || !control.IsEnabled) diff --git a/src/Core/src/Platform/iOS/MauiView.cs b/src/Core/src/Platform/iOS/MauiView.cs index 83375f185e38..7264700a7b03 100644 --- a/src/Core/src/Platform/iOS/MauiView.cs +++ b/src/Core/src/Platform/iOS/MauiView.cs @@ -6,7 +6,7 @@ namespace Microsoft.Maui.Platform { - public abstract class MauiView : UIView, ICrossPlatformLayoutBacking + public abstract class MauiView : UIView, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable { static bool? _respondsToSafeArea; @@ -138,5 +138,23 @@ public override void SetNeedsLayout() base.SetNeedsLayout(); Superview?.SetNeedsLayout(); } + + IVisualTreeElement? IVisualTreeElementProvidable.GetElement() + { + + if (View is IVisualTreeElement viewElement && + viewElement.IsThisMyPlatformView(this)) + { + return viewElement; + } + + if (CrossPlatformLayout is IVisualTreeElement layoutElement && + layoutElement.IsThisMyPlatformView(this)) + { + return layoutElement; + } + + return null; + } } } diff --git a/src/Core/src/Platform/iOS/ViewExtensions.cs b/src/Core/src/Platform/iOS/ViewExtensions.cs index eb8f6e24057f..ab2f37f247d3 100644 --- a/src/Core/src/Platform/iOS/ViewExtensions.cs +++ b/src/Core/src/Platform/iOS/ViewExtensions.cs @@ -2,13 +2,9 @@ using System.Collections.Generic; using System.Numerics; using System.Threading.Tasks; -using CoreAnimation; using CoreGraphics; using Foundation; -using Microsoft.Maui.Devices; using Microsoft.Maui.Graphics; -using Microsoft.Maui.Media; -using ObjCRuntime; using UIKit; using static Microsoft.Maui.Primitives.Dimension; @@ -239,21 +235,10 @@ public static void UpdateBorder(this UIView platformView, IView view) wrapperView.Border = border; } - internal static T? FindDescendantView(this UIView view, Func predicate) where T : UIView + internal static T? GetChildAt(this UIView view, int index) where T : UIView { - var queue = new Queue(); - queue.Enqueue(view); - - while (queue.Count > 0) - { - var descendantView = queue.Dequeue(); - - if (descendantView is T result && predicate.Invoke(result)) - return result; - - for (var i = 0; i < descendantView.Subviews?.Length; i++) - queue.Enqueue(descendantView.Subviews[i]); - } + if (index < view.Subviews.Length) + return (T?)view.Subviews[index]; return null; } diff --git a/src/Core/tests/DeviceTests/Handlers/Layout/LayoutHandlerTests.cs b/src/Core/tests/DeviceTests/Handlers/Layout/LayoutHandlerTests.cs index f4c77b61c469..d351b79ab580 100644 --- a/src/Core/tests/DeviceTests/Handlers/Layout/LayoutHandlerTests.cs +++ b/src/Core/tests/DeviceTests/Handlers/Layout/LayoutHandlerTests.cs @@ -224,7 +224,7 @@ public async Task ContainerViewDifferentThanPlatformView() layout.Add(containedButton); _ = await CreateHandlerAsync(layout); var handler = containedButton.Handler as IPlatformViewHandler; - Assert.NotEqual(handler.PlatformView, handler.ContainerView); + await InvokeOnMainThreadAsync(() => Assert.NotEqual(handler.PlatformView, handler.ContainerView)); } LabelStub CreateZTestLabel(int zIndex) diff --git a/src/TestUtils/src/DeviceTests/AssertionExtensions.cs b/src/TestUtils/src/DeviceTests/AssertionExtensions.cs index 77c12aceb6c1..2314bda67841 100644 --- a/src/TestUtils/src/DeviceTests/AssertionExtensions.cs +++ b/src/TestUtils/src/DeviceTests/AssertionExtensions.cs @@ -74,10 +74,10 @@ public static Task AssertHasContainer(this IView view, bool expectation) // to the Visual Tree var platformViewHandler = (IPlatformViewHandler)view.Handler!; var platformView = platformViewHandler.PlatformView!; - -#if WINDOWS var mauiContext = platformViewHandler.MauiContext ?? throw new InvalidOperationException("MauiContext cannot be null here"); var dispatcher = mauiContext.GetDispatcher(); + +#if WINDOWS return dispatcher.DispatchAsync(async () => { if (platformView.XamlRoot is null) @@ -92,8 +92,7 @@ public static Task AssertHasContainer(this IView view, bool expectation) }); #else - RunAssertions(); - return Task.CompletedTask; + return dispatcher.DispatchAsync(RunAssertions); #endif void RunAssertions() {