A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

254 lines
8.3 KiB

using System;
using Avalonia.Controls.Chrome;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
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();
Render(renderTimer);
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)
}
};
}
[Fact]
public void Cursor_Should_Follow_Captured_Element()
{
using var app = UnitTestApplication.Start(
TestServices.StyledWindow.With(inputManager: new InputManager()));
var captured = new Border
{
Background = Brushes.Red,
Width = 20,
Cursor = new Cursor(StandardCursorType.SizeWestEast)
};
var other1 = new Border
{
Background = Brushes.Blue,
Width = 100,
Cursor = new Cursor(StandardCursorType.Ibeam)
};
var other2 = new Border
{
Background = Brushes.Blue,
Width = 100,
Cursor = new Cursor(StandardCursorType.Cross)
};
ICursorImpl? currentCursor = null;
var renderTimer = new CompositorTestServices.ManualRenderTimer();
var compositor = RendererMocks.CreateDummyCompositor(renderTimer);
var windowImpl = MockWindowingPlatform.CreateWindowMock(200, 100, compositor);
windowImpl
.Setup(w => w.SetCursor(It.IsAny<ICursorImpl?>()))
.Callback<ICursorImpl?>(cursor => currentCursor = cursor);
var window = new Window(windowImpl.Object)
{
Content = new StackPanel
{
Orientation = Orientation.Horizontal,
Children = { captured, other1, other2 }
}
};
IPointer? pointer = null;
captured.PointerPressed += (_, e) =>
{
e.Pointer.Capture(captured);
pointer = e.Pointer;
};
window.Show();
Render(renderTimer);
var mouse = new MouseDevice();
var root = window.PresentationSource;
// Press inside the first border: the pointer becomes captured and the cursor is its own.
windowImpl.Object.Input!(new RawPointerEventArgs(
mouse, 1, root, RawPointerEventType.LeftButtonDown, new Point(10, 50),
RawInputModifiers.LeftMouseButton));
Assert.NotNull(pointer);
Assert.Same(captured, pointer.Captured);
Assert.Same(captured.Cursor!.PlatformImpl, currentCursor);
var cursorWhileCaptured = currentCursor;
// Drag over the other border. With the pointer still captured by the first border,
// PresentationSource.PointerOverElement changes (it becomes null), but the displayed
// cursor must keep coming from the captured element rather than following the new
// PointerOverElement.
windowImpl.Object.Input!(new RawPointerEventArgs(
mouse, 2, root, RawPointerEventType.Move, new Point(70, 50),
RawInputModifiers.LeftMouseButton));
Assert.Same(captured, pointer.Captured);
Assert.Same(cursorWhileCaptured, currentCursor);
// Changing the captured element's cursor should still work.
var newCursor = new Cursor(StandardCursorType.Hand);
captured.Cursor = newCursor;
Assert.Same(newCursor.PlatformImpl, currentCursor);
// Changing the capture explicitly should update the cursor.
pointer.Capture(other1);
Assert.Same(other1.Cursor!.PlatformImpl, currentCursor);
// Move the pointer to an unrelated element and release the capture:
// it should reset the cursor to match that new element.
((IInputRoot)root).PointerOverElement = other2;
windowImpl.Object.Input!(new RawPointerEventArgs(
mouse, 2, root, RawPointerEventType.Move, new Point(120, 50),
RawInputModifiers.LeftMouseButton));
pointer.Capture(null);
Assert.Same(other2.Cursor!.PlatformImpl, currentCursor);
}
private static void Render(CompositorTestServices.ManualRenderTimer renderTimer)
{
Dispatcher.CurrentDispatcher.RunJobs(null, TestContext.Current.CancellationToken);
renderTimer.TriggerTick();
Dispatcher.CurrentDispatcher.RunJobs(null, TestContext.Current.CancellationToken);
}
}