Browse Source

Prefer window content over underlay in chrome hit testing (#21177)

* Add unit tests for decoration hit testing

* Fix chrome hit testing

* macOS: handle TitleBar element role
pull/21219/head
Julien Lebosquain 1 month ago
committed by GitHub
parent
commit
c85beb3b7f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 11
      src/Avalonia.Controls/PresentationSource/PresentationSource.cs
  2. 3
      src/Avalonia.Native/WindowImpl.cs
  3. 147
      tests/Avalonia.Controls.UnitTests/PresentationSourceTests.cs
  4. 4
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs
  5. 4
      tests/Avalonia.UnitTests/RendererMocks.cs

11
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);
}
}

3
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))
{

147
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<WindowDrawnDecorationsContent>(content, new NameScope())
};
return new ControlTheme(typeof(WindowDrawnDecorations))
{
Setters =
{
new Setter(WindowDrawnDecorations.TemplateProperty, template)
}
};
}
}

4
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -22,13 +22,13 @@ namespace Avalonia.UnitTests
_popupImpl = popupImpl;
}
public static Mock<IWindowImpl> CreateWindowMock(double initialWidth = 800, double initialHeight = 600)
public static Mock<IWindowImpl> CreateWindowMock(double initialWidth = 800, double initialHeight = 600, Compositor? compositor = null)
{
var windowImpl = new Mock<IWindowImpl>();
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);

4
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

Loading…
Cancel
Save