diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs index 8f298d2e30..37db4c4a06 100644 --- a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs +++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs @@ -144,14 +144,7 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi WindowDecorationsElementRole? IInputRoot.HitTestChromeElement(Point point) { - // Check all visuals at the point (not just topmost) because chrome elements - // may be in the underlay layer which sits below the TopLevel in the visual tree. - foreach (var visual in RootVisual.GetVisualsAt(point, ChromeHitTestFilter)) - { - var role = GetChromeRoleFromVisual(visual); - if (role != null) - return role; - } - return null; + var visual = RootVisual.GetVisualAt(point, ChromeHitTestFilter); + return GetChromeRoleFromVisual(visual); } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 76f47150db..5de5445f92 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Chrome; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; @@ -154,7 +155,7 @@ namespace Avalonia.Native return true; }); - if(visual == null) + if (visual == null || WindowDecorationProperties.GetElementRole(visual) == WindowDecorationsElementRole.TitleBar) { if (_doubleClickHelper.IsDoubleClick(e.Timestamp, e.Position)) { diff --git a/tests/Avalonia.Controls.UnitTests/PresentationSourceTests.cs b/tests/Avalonia.Controls.UnitTests/PresentationSourceTests.cs new file mode 100644 index 0000000000..5e96e31f5f --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/PresentationSourceTests.cs @@ -0,0 +1,147 @@ +using System; +using Avalonia.Controls.Chrome; +using Avalonia.Controls.Platform; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Controls.UnitTests; + +public sealed class PresentationSourceTests : ScopedTestBase +{ + [Fact] + public void ChromeHitTest_Prefers_Overlay_Over_Content() + { + var overlay = new Border + { + Background = Brushes.Red, + [WindowDecorationProperties.ElementRoleProperty] = WindowDecorationsElementRole.TitleBar + }; + + var content = new Border + { + Background = Brushes.Blue + }; + + DoChromeHitTest( + underlay: null, + content: content, + overlay: overlay, + expectedChromeVisual: overlay, + expectedRole: WindowDecorationsElementRole.TitleBar); + } + + [Fact] + public void ChromeHitTest_Prefers_Content_Over_Underlay() + { + var underlay = new Border + { + Background = Brushes.Red, + [WindowDecorationProperties.ElementRoleProperty] = WindowDecorationsElementRole.TitleBar + }; + + var content = new Border + { + Background = Brushes.Blue + }; + + DoChromeHitTest( + underlay: underlay, + content: content, + overlay: null, + expectedChromeVisual: content, + expectedRole: null); + } + + private static void DoChromeHitTest( + Control? underlay, + Control? content, + Control? overlay, + Visual? expectedChromeVisual, + WindowDecorationsElementRole? expectedRole) + { + const double width = 100; + const double height = 100; + + if (underlay is not null) + { + underlay.Width = width; + underlay.Height = height; + } + + if (content is not null) + { + content.Width = width; + content.Height = height; + } + + if (overlay is not null) + { + overlay.Width = width; + overlay.Height = height; + } + + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var decorations = new WindowDrawnDecorationsContent + { + Underlay = underlay, + Overlay = overlay + }; + + Application.Current!.Resources.Add(typeof(WindowDrawnDecorations), CreateDecorationsTheme(decorations)); + + var renderTimer = new CompositorTestServices.ManualRenderTimer(); + var compositor = RendererMocks.CreateDummyCompositor(renderTimer); + var windowImpl = MockWindowingPlatform.CreateWindowMock(width, height, compositor); + windowImpl.Setup(w => w.IsClientAreaExtendedToDecorations).Returns(true); + windowImpl.Setup(w => w.RequestedDrawnDecorations).Returns(PlatformRequestedDrawnDecoration.TitleBar); + windowImpl.Setup(w => w.NeedsManagedDecorations).Returns(true); + + var window = new Window(windowImpl.Object) + { + WindowDecorations = WindowDecorations.Full, + ExtendClientAreaToDecorationsHint = true, + Content = content + }; + + window.Show(); + + Dispatcher.CurrentDispatcher.RunJobs(null, TestContext.Current.CancellationToken); + renderTimer.TriggerTick(); + Dispatcher.CurrentDispatcher.RunJobs(null, TestContext.Current.CancellationToken); + + var hitTestPoint = new Point(width / 2, height / 2); + + var clientVisual = window.GetVisualAt(hitTestPoint); + Assert.Same(window.Content, clientVisual); + + var chromeVisual = window.PresentationSource.RootVisual.GetVisualAt(hitTestPoint); + Assert.Same(expectedChromeVisual, chromeVisual); + + var chromeRole = ((IInputRoot)window.PresentationSource).HitTestChromeElement(hitTestPoint); + Assert.Equal(expectedRole, chromeRole); + } + + private static ControlTheme CreateDecorationsTheme(WindowDrawnDecorationsContent content) + { + var template = new WindowDrawnDecorationsTemplate + { + Content = (IServiceProvider? _) => new TemplateResult(content, new NameScope()) + }; + + return new ControlTheme(typeof(WindowDrawnDecorations)) + { + Setters = + { + new Setter(WindowDrawnDecorations.TemplateProperty, template) + } + }; + } +} diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 024d198967..298785df32 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -22,13 +22,13 @@ namespace Avalonia.UnitTests _popupImpl = popupImpl; } - public static Mock CreateWindowMock(double initialWidth = 800, double initialHeight = 600) + public static Mock CreateWindowMock(double initialWidth = 800, double initialHeight = 600, Compositor? compositor = null) { var windowImpl = new Mock(); var clientSize = new Size(initialWidth, initialHeight); windowImpl.SetupAllProperties(); - var compositor = RendererMocks.CreateDummyCompositor(); + compositor ??= RendererMocks.CreateDummyCompositor(); windowImpl.Setup(x => x.Compositor).Returns(compositor); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); diff --git a/tests/Avalonia.UnitTests/RendererMocks.cs b/tests/Avalonia.UnitTests/RendererMocks.cs index 9b172fe342..6c7b49fb00 100644 --- a/tests/Avalonia.UnitTests/RendererMocks.cs +++ b/tests/Avalonia.UnitTests/RendererMocks.cs @@ -16,8 +16,8 @@ namespace Avalonia.UnitTests return renderer; } - public static Compositor CreateDummyCompositor() => - new(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null, false, + public static Compositor CreateDummyCompositor(IRenderTimer? renderTimer = null) => + new(RenderLoop.FromTimer(renderTimer ?? new CompositorTestServices.ManualRenderTimer()), null, false, new CompositionCommitScheduler(), true, Dispatcher.UIThread); class CompositionCommitScheduler : ICompositorScheduler