Fix several issues with ExtendClientAreaToDecorationsHint on Windows (#20217)
* Add extended client area Win32 integration tests * Fix Win32 extended client areas * Run Win32 integration tests in pipelines * Add extra tests and fixes when CanResize=false * Use WM_GETMINMAXINFO to maximize captionless windows * Use dotnet run on CI for IntegrationTests.Win32 * Address review
This commit is contained in:
committed by
GitHub
parent
d39cb62a5d
commit
884051ed10
@@ -285,6 +285,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.Per
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.PerTest.UnitTests", "tests\Avalonia.Headless.XUnit.PerTest.UnitTests\Avalonia.Headless.XUnit.PerTest.UnitTests.csproj", "{26918642-829D-4FA2-B60A-BE8D83F4E063}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.IntegrationTests.Win32", "tests\Avalonia.IntegrationTests.Win32\Avalonia.IntegrationTests.Win32.csproj", "{11522B0D-BF31-42D5-8FC5-41E58F319AF9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -659,6 +661,10 @@ Global
|
||||
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -742,6 +748,7 @@ Global
|
||||
{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
{342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
{26918642-829D-4FA2-B60A-BE8D83F4E063} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
{11522B0D-BF31-42D5-8FC5-41E58F319AF9} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
|
||||
|
||||
@@ -87,10 +87,16 @@ jobs:
|
||||
displayName: 'Build test project'
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: 'tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj'
|
||||
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Run Win32 Integration Tests'
|
||||
inputs:
|
||||
command: 'run'
|
||||
projects: 'tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj'
|
||||
|
||||
- task: VSTest@2
|
||||
displayName: 'Run Integration Tests'
|
||||
displayName: 'Run Appium Integration Tests'
|
||||
inputs:
|
||||
testAssemblyVer2: '**\bin\**\Avalonia.IntegrationTests.Appium.dll'
|
||||
runSettingsFile: 'tests\Avalonia.IntegrationTests.Appium\record-video.runsettings'
|
||||
|
||||
@@ -27,12 +27,19 @@ namespace Avalonia.Controls.Chrome
|
||||
|
||||
if (window.WindowState != WindowState.FullScreen)
|
||||
{
|
||||
Height = Math.Max(0, window.WindowDecorationMargin.Top);
|
||||
|
||||
if (_captionButtons != null)
|
||||
{
|
||||
_captionButtons.Height = Height;
|
||||
}
|
||||
var height = Math.Max(0, window.WindowDecorationMargin.Top);
|
||||
Height = height;
|
||||
_captionButtons?.Height = window.SystemDecorations == SystemDecorations.Full ? height : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: apparently the titlebar was supposed to be displayed when hovering the top of the screen,
|
||||
// to mimic macOS behavior. This has been broken for years. It actually only partially works if the
|
||||
// window is FullScreen right on startup, and only once. Any size change will then break it.
|
||||
// Disable it for now.
|
||||
// TODO: restore that behavior so that it works in all cases
|
||||
Height = 0;
|
||||
_captionButtons?.Height = 0;
|
||||
}
|
||||
|
||||
IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false;
|
||||
@@ -79,6 +86,7 @@ namespace Avalonia.Controls.Chrome
|
||||
PseudoClasses.Set(":normal", x == WindowState.Normal);
|
||||
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
|
||||
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
|
||||
UpdateSize(window);
|
||||
}),
|
||||
window.GetObservable(Window.IsExtendedIntoWindowDecorationsProperty)
|
||||
.Subscribe(_ => UpdateSize(window))
|
||||
|
||||
@@ -869,6 +869,14 @@ namespace Avalonia.Win32.Interop
|
||||
DWMWCP_ROUNDSMALL
|
||||
}
|
||||
|
||||
public enum DwmNCRenderingPolicy : uint
|
||||
{
|
||||
DWMNCRP_USEWINDOWSTYLE,
|
||||
DWMNCRP_DISABLED,
|
||||
DWMNCRP_ENABLED,
|
||||
DWMNCRP_LAST
|
||||
}
|
||||
|
||||
public enum MapVirtualKeyMapTypes : uint
|
||||
{
|
||||
MAPVK_VK_TO_VSC = 0x00,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia.Automation.Peers;
|
||||
using Avalonia.Controls;
|
||||
@@ -57,13 +58,79 @@ namespace Avalonia.Win32
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
case WindowsMessage.WM_NCCALCSIZE:
|
||||
case WindowsMessage.WM_NCCALCSIZE when ToInt32(wParam) == 1:
|
||||
{
|
||||
if (ToInt32(wParam) == 1 && (_windowProperties.Decorations == SystemDecorations.None || _isClientAreaExtended))
|
||||
if (_windowProperties.Decorations == SystemDecorations.None)
|
||||
return IntPtr.Zero;
|
||||
|
||||
// When the client area is extended into the frame, we are still requesting the standard styles matching
|
||||
// the wanted decorations (such as WS_CAPTION or WS_BORDER) along with window bounds larger than the client size.
|
||||
// This allows the window to have the standard resize borders *outside* of the client area.
|
||||
// The logic for this lies in the Resize() method.
|
||||
//
|
||||
// After this happens, WM_NCCALCSIZE provides us with a new window area matching those requested bounds.
|
||||
// We need to adjust that area back to our preferred client area, keeping the resize borders around it.
|
||||
//
|
||||
// The same logic applies when the window gets maximized, the only difference being that Windows chose
|
||||
// the final bounds instead of us.
|
||||
if (_isClientAreaExtended)
|
||||
{
|
||||
GetWindowPlacement(hWnd, out var placement);
|
||||
if (placement.ShowCmd == ShowWindowCommand.ShowMinimized)
|
||||
break;
|
||||
|
||||
var paramsObj = Marshal.PtrToStructure<NCCALCSIZE_PARAMS>(lParam);
|
||||
ref var rect = ref paramsObj.rgrc[0];
|
||||
|
||||
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
|
||||
var adjuster = CreateWindowRectAdjuster();
|
||||
var borderThickness = new RECT();
|
||||
|
||||
// We told Windows we have a caption, but since we're actually extending into it, it should not be taken into account.
|
||||
if (style.HasAllFlags(WindowStyles.WS_CAPTION))
|
||||
{
|
||||
if (placement.ShowCmd == ShowWindowCommand.ShowMaximized)
|
||||
{
|
||||
adjuster.Adjust(ref borderThickness, style & ~WindowStyles.WS_CAPTION | WindowStyles.WS_BORDER | WindowStyles.WS_THICKFRAME, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
adjuster.Adjust(ref borderThickness, style, 0);
|
||||
|
||||
var thinBorderThickness = new RECT();
|
||||
adjuster.Adjust(ref thinBorderThickness, style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME) | WindowStyles.WS_BORDER, 0);
|
||||
borderThickness.top = thinBorderThickness.top;
|
||||
}
|
||||
}
|
||||
else if (style.HasAllFlags(WindowStyles.WS_BORDER))
|
||||
{
|
||||
if (placement.ShowCmd == ShowWindowCommand.ShowMaximized)
|
||||
{
|
||||
adjuster.Adjust(ref borderThickness, style, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
adjuster.Adjust(ref borderThickness, style, 0);
|
||||
|
||||
var thinBorderThickness = new RECT();
|
||||
adjuster.Adjust(ref thinBorderThickness, style & ~WindowStyles.WS_THICKFRAME, 0);
|
||||
borderThickness.top = thinBorderThickness.top;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
adjuster.Adjust(ref borderThickness, style, 0);
|
||||
}
|
||||
|
||||
rect.left -= borderThickness.left;
|
||||
rect.top -= borderThickness.top;
|
||||
rect.right -= borderThickness.right;
|
||||
rect.bottom -= borderThickness.bottom;
|
||||
|
||||
Marshal.StructureToPtr(paramsObj, lParam, false);
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -699,11 +766,6 @@ namespace Avalonia.Win32
|
||||
|
||||
UpdateWindowProperties(newWindowProperties);
|
||||
|
||||
if (windowState == WindowState.Maximized)
|
||||
{
|
||||
MaximizeWithoutCoveringTaskbar();
|
||||
}
|
||||
|
||||
WindowStateChanged?.Invoke(windowState);
|
||||
|
||||
if (_isClientAreaExtended)
|
||||
@@ -739,6 +801,44 @@ namespace Avalonia.Win32
|
||||
|
||||
_maxTrackSize = mmi.ptMaxTrackSize;
|
||||
|
||||
// A window without a caption (i.e. None and BorderOnly decorations) maximizes to the whole screen
|
||||
// by default. Adjust that to the screen's working area instead.
|
||||
var style = GetStyle();
|
||||
if (!style.HasAllFlags(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME))
|
||||
{
|
||||
var screen = Screen.ScreenFromHwnd(Hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
|
||||
if (screen?.WorkingArea is { } workingArea)
|
||||
{
|
||||
var x = workingArea.X;
|
||||
var y = workingArea.Y;
|
||||
var cx = workingArea.Width;
|
||||
var cy = workingArea.Height;
|
||||
|
||||
var adjuster = CreateWindowRectAdjuster();
|
||||
var borderThickness = new RECT();
|
||||
|
||||
var adjustedStyle = style & ~WindowStyles.WS_CAPTION;
|
||||
|
||||
if (style.HasAllFlags(WindowStyles.WS_BORDER))
|
||||
adjustedStyle |= WindowStyles.WS_BORDER;
|
||||
|
||||
if (style.HasAllFlags(WindowStyles.WS_CAPTION))
|
||||
adjustedStyle |= WindowStyles.WS_THICKFRAME;
|
||||
|
||||
adjuster.Adjust(ref borderThickness, adjustedStyle, 0);
|
||||
|
||||
x += borderThickness.left;
|
||||
y += borderThickness.top;
|
||||
cx += -borderThickness.left + borderThickness.right;
|
||||
cy += -borderThickness.top + borderThickness.bottom;
|
||||
|
||||
mmi.ptMaxPosition.X = x;
|
||||
mmi.ptMaxPosition.Y = y;
|
||||
mmi.ptMaxSize.X = cx;
|
||||
mmi.ptMaxSize.Y = cy;
|
||||
}
|
||||
}
|
||||
|
||||
if (_minSize.Width > 0)
|
||||
{
|
||||
mmi.ptMinTrackSize.X =
|
||||
|
||||
@@ -18,36 +18,24 @@ namespace Avalonia.Win32
|
||||
// Get the window rectangle.
|
||||
GetWindowRect(hWnd, out var rcWindow);
|
||||
|
||||
var scaling = (uint)(RenderScaling * StandardDpi);
|
||||
var relativeScaling = RenderScaling / PrimaryScreenRenderScaling;
|
||||
|
||||
// Get the frame rectangle, adjusted for the style without a caption.
|
||||
var rcFrame = new RECT();
|
||||
var borderThickness = new RECT();
|
||||
if (Win32Platform.WindowsVersion < PlatformConstants.Windows10_1607)
|
||||
|
||||
var isMaximized = GetWindowPlacement(hWnd, out var placement) && placement.ShowCmd == ShowWindowCommand.ShowMaximized;
|
||||
if (!isMaximized)
|
||||
{
|
||||
AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0);
|
||||
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
|
||||
if (style.HasAllFlags(WindowStyles.WS_THICKFRAME))
|
||||
{
|
||||
var adjuster = CreateWindowRectAdjuster();
|
||||
adjuster.Adjust(ref rcFrame, style & ~WindowStyles.WS_CAPTION, 0);
|
||||
adjuster.Adjust(ref borderThickness, style, 0);
|
||||
|
||||
rcFrame.top = (int)(rcFrame.top * relativeScaling);
|
||||
rcFrame.right = (int)(rcFrame.right * relativeScaling);
|
||||
rcFrame.left = (int)(rcFrame.left * relativeScaling);
|
||||
rcFrame.bottom = (int)(rcFrame.bottom * relativeScaling);
|
||||
|
||||
AdjustWindowRectEx(ref borderThickness, (uint)GetStyle(), false, 0);
|
||||
|
||||
borderThickness.top = (int)(borderThickness.top * relativeScaling);
|
||||
borderThickness.right = (int)(borderThickness.right * relativeScaling);
|
||||
borderThickness.left = (int)(borderThickness.left * relativeScaling);
|
||||
borderThickness.bottom = (int)(borderThickness.bottom * relativeScaling);
|
||||
borderThickness.left *= -1;
|
||||
borderThickness.top *= -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AdjustWindowRectExForDpi(ref rcFrame, WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION, false, 0, scaling);
|
||||
AdjustWindowRectExForDpi(ref borderThickness, GetStyle(), false, 0, scaling);
|
||||
}
|
||||
|
||||
borderThickness.left *= -1;
|
||||
borderThickness.top *= -1;
|
||||
|
||||
if (_extendTitleBarHint >= 0)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia.Collections.Pooled;
|
||||
using Avalonia.Controls;
|
||||
@@ -61,7 +62,6 @@ namespace Avalonia.Win32
|
||||
private bool _isFullScreenActive;
|
||||
private bool _isClientAreaExtended;
|
||||
private Thickness _extendedMargins;
|
||||
private Thickness _offScreenMargin;
|
||||
private double _extendTitleBarHint = -1;
|
||||
private WindowResizeReason _resizeReason;
|
||||
private MOUSEMOVEPOINT _lastWmMousePoint;
|
||||
@@ -150,6 +150,8 @@ namespace Avalonia.Win32
|
||||
|
||||
_wmPointerEnabled = Win32Platform.WindowsVersion >= PlatformConstants.Windows8;
|
||||
|
||||
Screen = Win32Platform.Instance.Screen;
|
||||
|
||||
CreateWindow();
|
||||
_framebuffer = new FramebufferManager(_hwnd);
|
||||
|
||||
@@ -173,7 +175,6 @@ namespace Avalonia.Win32
|
||||
}
|
||||
}
|
||||
|
||||
Screen = Win32Platform.Instance.Screen;
|
||||
_storageProvider = new Win32StorageProvider(this);
|
||||
_inputPane = WindowsInputPane.TryCreate(this);
|
||||
_nativeControlHost = new Win32NativeControlHost(this, !UseRedirectionBitmap);
|
||||
@@ -240,8 +241,6 @@ namespace Avalonia.Win32
|
||||
}
|
||||
}
|
||||
|
||||
private double PrimaryScreenRenderScaling => Screen.AllScreens.FirstOrDefault(screen => screen.IsPrimary)?.Scaling ?? 1;
|
||||
|
||||
private ICompositionEffectsSurface? CompositionEffectsSurface => _glSurface as ICompositionEffectsSurface;
|
||||
private bool UseRedirectionBitmap { get; }
|
||||
|
||||
@@ -573,10 +572,11 @@ namespace Avalonia.Win32
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lastWindowState == WindowState.FullScreen)
|
||||
if (_lastWindowState == WindowState.FullScreen && _isFullScreenActive)
|
||||
{
|
||||
// Fullscreen mode is really a restored window without a frame filling the whole monitor.
|
||||
// It doesn't make sense to resize the window in this state, so ignore this request.
|
||||
// (If the fullscreen mode isn't yet active, continue normally so that our normal window size gets saved.)
|
||||
Logger.TryGet(LogEventLevel.Warning, LogArea.Win32Platform)?.Log(this, "Ignoring resize event on fullscreen window.");
|
||||
return;
|
||||
}
|
||||
@@ -591,7 +591,19 @@ namespace Avalonia.Win32
|
||||
bottom = requestedClientHeight
|
||||
};
|
||||
|
||||
var requestedWindowRect = _isClientAreaExtended ? requestedClientRect : ClientRectToWindowRect(requestedClientRect);
|
||||
var requestedWindowRect = ClientRectToWindowRect(requestedClientRect);
|
||||
|
||||
if (_isClientAreaExtended)
|
||||
{
|
||||
// We told Windows we have a caption, but since we're actually extending into it,
|
||||
// it should be excluded from the final window bounds.
|
||||
if (_windowProperties.Decorations != SystemDecorations.None)
|
||||
{
|
||||
var borderOnlyRect = ClientRectToWindowRect(requestedClientRect, WindowStyles.WS_BORDER);
|
||||
requestedWindowRect.top = borderOnlyRect.top;
|
||||
}
|
||||
}
|
||||
|
||||
var windowWidth = requestedWindowRect.Width;
|
||||
var windowHeight = requestedWindowRect.Height;
|
||||
|
||||
@@ -1048,29 +1060,53 @@ namespace Avalonia.Win32
|
||||
{
|
||||
if (fullscreen)
|
||||
{
|
||||
GetWindowRect(_hwnd, out var windowRect);
|
||||
|
||||
_savedWindowInfo.WindowRect = windowRect;
|
||||
|
||||
var current = GetStyle();
|
||||
var currentEx = GetExtendedStyle();
|
||||
Screen? screen;
|
||||
|
||||
GetWindowPlacement(_hwnd, out var placement);
|
||||
var isMinimized = placement.ShowCmd == ShowWindowCommand.ShowMinimized;
|
||||
RECT windowRect;
|
||||
|
||||
// When minimized, we can't use GetWindowRect since the window is actually way outside the screen.
|
||||
// Instead, fall back to WINDOWPLACEMENT.NormalPosition (which is in working area coordinates).
|
||||
if (isMinimized)
|
||||
{
|
||||
windowRect = placement.NormalPosition;
|
||||
screen = Screen.ScreenFromRect(windowRect.ToPixelRect());
|
||||
if (screen?.WorkingArea is { } workingArea)
|
||||
{
|
||||
windowRect.left += workingArea.X;
|
||||
windowRect.top += workingArea.Y;
|
||||
windowRect.right += workingArea.X;
|
||||
windowRect.bottom += workingArea.Y;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GetWindowRect(_hwnd, out windowRect);
|
||||
screen = Screen.ScreenFromHwnd(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
|
||||
}
|
||||
|
||||
_savedWindowInfo.WindowRect = windowRect;
|
||||
_savedWindowInfo.Style = current;
|
||||
_savedWindowInfo.ExStyle = currentEx;
|
||||
|
||||
// Set new window style and size.
|
||||
SetStyle(current & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME), false);
|
||||
SetStyle(current & ~WindowStyles.WS_OVERLAPPEDWINDOW, false);
|
||||
SetExtendedStyle(currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false);
|
||||
|
||||
// On expand, if we're given a window_rect, grow to it, otherwise do
|
||||
// not resize.
|
||||
var screen = Screen.ScreenFromHwnd(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
|
||||
if (screen?.Bounds is { } window_rect)
|
||||
if (screen?.Bounds is { } screenBounds)
|
||||
{
|
||||
_isFullScreenActive = true;
|
||||
SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y,
|
||||
window_rect.Width, window_rect.Height,
|
||||
SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
|
||||
|
||||
if (isMinimized)
|
||||
UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Restore);
|
||||
|
||||
SetWindowPos(_hwnd, IntPtr.Zero, screenBounds.X, screenBounds.Y, screenBounds.Width, screenBounds.Height,
|
||||
SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1101,44 +1137,27 @@ namespace Avalonia.Win32
|
||||
|
||||
private MARGINS UpdateExtendMargins()
|
||||
{
|
||||
RECT borderThickness = new RECT();
|
||||
RECT borderCaptionThickness = new RECT();
|
||||
var borderThickness = new RECT();
|
||||
var borderCaptionThickness = new RECT();
|
||||
var style = GetStyle();
|
||||
|
||||
var scaling = (uint)(RenderScaling * StandardDpi);
|
||||
var relativeScaling = RenderScaling / PrimaryScreenRenderScaling;
|
||||
|
||||
if (Win32Platform.WindowsVersion < PlatformConstants.Windows10_1607)
|
||||
{
|
||||
AdjustWindowRectEx(ref borderCaptionThickness, (uint)GetStyle(), false, 0);
|
||||
AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle() & ~WindowStyles.WS_CAPTION), false, 0);
|
||||
|
||||
borderCaptionThickness.top = (int)(borderCaptionThickness.top * relativeScaling);
|
||||
borderCaptionThickness.right = (int)(borderCaptionThickness.right * relativeScaling);
|
||||
borderCaptionThickness.left = (int)(borderCaptionThickness.left * relativeScaling);
|
||||
borderCaptionThickness.bottom = (int)(borderCaptionThickness.bottom * relativeScaling);
|
||||
|
||||
borderThickness.top = (int)(borderThickness.top * relativeScaling);
|
||||
borderThickness.right = (int)(borderThickness.right * relativeScaling);
|
||||
borderThickness.left = (int)(borderThickness.left * relativeScaling);
|
||||
borderThickness.bottom = (int)(borderThickness.bottom * relativeScaling);
|
||||
}
|
||||
else
|
||||
{
|
||||
AdjustWindowRectExForDpi(ref borderCaptionThickness, GetStyle(), false, 0, scaling);
|
||||
AdjustWindowRectExForDpi(ref borderThickness, GetStyle() & ~WindowStyles.WS_CAPTION, false, 0, scaling);
|
||||
}
|
||||
var adjuster = CreateWindowRectAdjuster();
|
||||
adjuster.Adjust(ref borderCaptionThickness, style, 0);
|
||||
adjuster.Adjust(ref borderThickness, style & ~WindowStyles.WS_CAPTION, 0);
|
||||
|
||||
borderThickness.left *= -1;
|
||||
borderThickness.top *= -1;
|
||||
borderCaptionThickness.left *= -1;
|
||||
borderCaptionThickness.top *= -1;
|
||||
|
||||
bool wantsTitleBar = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1;
|
||||
|
||||
if (!wantsTitleBar)
|
||||
if (_extendChromeHints.HasAnyFlag(ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.PreferSystemChrome) &&
|
||||
_windowProperties.Decorations == SystemDecorations.Full)
|
||||
{
|
||||
borderCaptionThickness.top = 1;
|
||||
if (_extendTitleBarHint != -1)
|
||||
borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling);
|
||||
}
|
||||
else
|
||||
borderCaptionThickness.top = borderThickness.top;
|
||||
|
||||
//using a default margin of 0 when using WinUiComp removes artefacts when resizing. See issue #8316
|
||||
var defaultMargin = UseRedirectionBitmap ? 1 : 0;
|
||||
@@ -1148,22 +1167,15 @@ namespace Avalonia.Win32
|
||||
margins.cxRightWidth = defaultMargin;
|
||||
margins.cyBottomHeight = defaultMargin;
|
||||
|
||||
if (_extendTitleBarHint != -1)
|
||||
{
|
||||
borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling);
|
||||
}
|
||||
|
||||
margins.cyTopHeight = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : defaultMargin;
|
||||
|
||||
if (WindowState == WindowState.Maximized)
|
||||
{
|
||||
_extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / RenderScaling, 0, 0);
|
||||
_offScreenMargin = new Thickness(borderThickness.left / RenderScaling, borderThickness.top / RenderScaling, borderThickness.right / RenderScaling, borderThickness.bottom / RenderScaling);
|
||||
}
|
||||
else
|
||||
{
|
||||
_extendedMargins = new Thickness(0, (borderCaptionThickness.top) / RenderScaling, 0, 0);
|
||||
_offScreenMargin = new Thickness();
|
||||
}
|
||||
|
||||
return margins;
|
||||
@@ -1183,15 +1195,22 @@ namespace Avalonia.Win32
|
||||
}
|
||||
GetWindowRect(_hwnd, out var rcWindow);
|
||||
|
||||
if (_isClientAreaExtended && WindowState != WindowState.FullScreen)
|
||||
if (_isClientAreaExtended && WindowState != WindowState.FullScreen && GetStyle().HasAllFlags(WindowStyles.WS_BORDER))
|
||||
{
|
||||
var margins = UpdateExtendMargins();
|
||||
DwmExtendFrameIntoClientArea(_hwnd, ref margins);
|
||||
|
||||
unsafe
|
||||
// On Windows 11 21H2 and later, corners are configurable.
|
||||
// When doing so, we need to make sure that DWM draws the non-client frame for that to work correctly.
|
||||
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) {
|
||||
SetWindowCornerPreference(DwmWindowCornerPreference.DWMWCP_ROUND);
|
||||
SetNCRenderingPolicy(DwmNCRenderingPolicy.DWMNCRP_ENABLED);
|
||||
}
|
||||
else
|
||||
{
|
||||
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_ROUND;
|
||||
DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int));
|
||||
// On older versions, we need to disable painting the non-client area to avoid issues
|
||||
// (alternatively, we could return 0 in WM_NCPAINT in this case).
|
||||
SetNCRenderingPolicy(DwmNCRenderingPolicy.DWMNCRP_DISABLED);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1199,9 +1218,13 @@ namespace Avalonia.Win32
|
||||
var margins = new MARGINS();
|
||||
DwmExtendFrameIntoClientArea(_hwnd, ref margins);
|
||||
|
||||
_offScreenMargin = new Thickness();
|
||||
_extendedMargins = new Thickness();
|
||||
|
||||
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
|
||||
SetWindowCornerPreference(DwmWindowCornerPreference.DWMWCP_DEFAULT);
|
||||
|
||||
SetNCRenderingPolicy(DwmNCRenderingPolicy.DWMNCRP_USEWINDOWSTYLE);
|
||||
|
||||
unsafe
|
||||
{
|
||||
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_DEFAULT;
|
||||
@@ -1229,6 +1252,12 @@ namespace Avalonia.Win32
|
||||
ExtendClientAreaToDecorationsChanged?.Invoke(_isClientAreaExtended);
|
||||
}
|
||||
|
||||
private unsafe void SetWindowCornerPreference(DwmWindowCornerPreference value)
|
||||
=> DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &value, sizeof(int));
|
||||
|
||||
private unsafe void SetNCRenderingPolicy(DwmNCRenderingPolicy value)
|
||||
=> DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_NCRENDERING_POLICY, &value, sizeof(int));
|
||||
|
||||
private void ShowWindow(WindowState state, bool activate)
|
||||
{
|
||||
if (_isClientAreaExtended)
|
||||
@@ -1275,11 +1304,6 @@ namespace Avalonia.Win32
|
||||
UnmanagedMethods.ShowWindow(_hwnd, command.Value);
|
||||
}
|
||||
|
||||
if (state == WindowState.Maximized)
|
||||
{
|
||||
MaximizeWithoutCoveringTaskbar();
|
||||
}
|
||||
|
||||
if (!Design.IsDesignMode && activate)
|
||||
{
|
||||
SetFocus(_hwnd);
|
||||
@@ -1322,33 +1346,6 @@ namespace Avalonia.Win32
|
||||
}
|
||||
}
|
||||
|
||||
private void MaximizeWithoutCoveringTaskbar()
|
||||
{
|
||||
var screen = Screen.ScreenFromHwnd(Hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
|
||||
if (screen?.WorkingArea is { } workingArea)
|
||||
{
|
||||
var x = workingArea.X;
|
||||
var y = workingArea.Y;
|
||||
var cx = workingArea.Width;
|
||||
var cy = workingArea.Height;
|
||||
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
|
||||
|
||||
if (!style.HasFlag(WindowStyles.WS_THICKFRAME))
|
||||
{
|
||||
// When calling SetWindowPos on a maximized window it automatically adjusts
|
||||
// for "hidden" borders which are placed offscreen, EVEN IF THE WINDOW HAS
|
||||
// NO BORDERS, meaning that the window is placed wrong when we have CanResize
|
||||
// == false. Account for this here.
|
||||
var borderThickness = BorderThickness;
|
||||
x -= (int)borderThickness.Left;
|
||||
cx += (int)borderThickness.Left + (int)borderThickness.Right;
|
||||
cy += (int)borderThickness.Bottom;
|
||||
}
|
||||
|
||||
SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW | SetWindowPosFlags.SWP_FRAMECHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
private WindowStyles GetWindowStateStyles()
|
||||
{
|
||||
return GetStyle() & WindowStateMask;
|
||||
@@ -1462,7 +1459,7 @@ namespace Avalonia.Win32
|
||||
exStyle &= ~WindowStyles.WS_EX_APPWINDOW;
|
||||
}
|
||||
|
||||
WindowStyles style = WindowStyles.WS_CLIPCHILDREN | WindowStyles.WS_OVERLAPPEDWINDOW | WindowStyles.WS_CLIPSIBLINGS;
|
||||
var style = WindowStyles.WS_CLIPCHILDREN | WindowStyles.WS_CLIPSIBLINGS;
|
||||
|
||||
if (this is EmbeddedWindowImpl)
|
||||
style |= WindowStyles.WS_CHILD;
|
||||
@@ -1470,40 +1467,26 @@ namespace Avalonia.Win32
|
||||
if (IsWindowVisible(_hwnd))
|
||||
style |= WindowStyles.WS_VISIBLE;
|
||||
|
||||
if (newProperties.IsResizable || newProperties.WindowState == WindowState.Maximized)
|
||||
switch (newProperties.Decorations)
|
||||
{
|
||||
case SystemDecorations.Full:
|
||||
style |= WindowStyles.WS_BORDER | WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU;
|
||||
|
||||
if (newProperties.IsMinimizable)
|
||||
style |= WindowStyles.WS_MINIMIZEBOX;
|
||||
|
||||
if (newProperties.IsMaximizable || (newProperties.WindowState == WindowState.Maximized && newProperties.IsResizable))
|
||||
style |= WindowStyles.WS_MAXIMIZEBOX;
|
||||
|
||||
break;
|
||||
|
||||
case SystemDecorations.BorderOnly:
|
||||
style |= WindowStyles.WS_BORDER;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newProperties.Decorations != SystemDecorations.None && newProperties.IsResizable)
|
||||
style |= WindowStyles.WS_THICKFRAME;
|
||||
else
|
||||
style &= ~WindowStyles.WS_THICKFRAME;
|
||||
|
||||
if (newProperties.IsMinimizable)
|
||||
style |= WindowStyles.WS_MINIMIZEBOX;
|
||||
else
|
||||
style &= ~WindowStyles.WS_MINIMIZEBOX;
|
||||
|
||||
if (newProperties.IsMaximizable || (newProperties.WindowState == WindowState.Maximized && newProperties.IsResizable))
|
||||
style |= WindowStyles.WS_MAXIMIZEBOX;
|
||||
else
|
||||
style &= ~WindowStyles.WS_MAXIMIZEBOX;
|
||||
|
||||
const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_BORDER | WindowStyles.WS_SYSMENU;
|
||||
|
||||
if (newProperties.Decorations == SystemDecorations.Full)
|
||||
{
|
||||
style |= fullDecorationFlags;
|
||||
}
|
||||
else
|
||||
{
|
||||
style &= ~(fullDecorationFlags | WindowStyles.WS_THICKFRAME);
|
||||
|
||||
if (newProperties.Decorations == SystemDecorations.BorderOnly && newProperties.WindowState != WindowState.Maximized && newProperties.IsResizable)
|
||||
{
|
||||
style |= WindowStyles.WS_THICKFRAME | WindowStyles.WS_BORDER;
|
||||
}
|
||||
else if(newProperties.WindowState == WindowState.Maximized && _isClientAreaExtended)
|
||||
{
|
||||
style |= WindowStyles.WS_THICKFRAME;
|
||||
}
|
||||
}
|
||||
|
||||
var windowStates = GetWindowStateStyles();
|
||||
style &= ~WindowStateMask;
|
||||
@@ -1577,14 +1560,8 @@ namespace Avalonia.Win32
|
||||
var style = styleOverride ?? GetStyle();
|
||||
var extendedStyle = extendedStyleOverride ?? GetExtendedStyle();
|
||||
|
||||
var result = Win32Platform.WindowsVersion < PlatformConstants.Windows10_1607
|
||||
? AdjustWindowRectEx(ref clientRect, (uint)style, false, (uint)extendedStyle)
|
||||
: AdjustWindowRectExForDpi(ref clientRect, style, false, extendedStyle, (uint)(RenderScaling * StandardDpi));
|
||||
|
||||
if (!result)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
|
||||
}
|
||||
var adjuster = CreateWindowRectAdjuster();
|
||||
adjuster.Adjust(ref clientRect, style, extendedStyle);
|
||||
|
||||
return clientRect;
|
||||
}
|
||||
@@ -1681,7 +1658,7 @@ namespace Avalonia.Win32
|
||||
public Thickness ExtendedMargins => _extendedMargins;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Thickness OffScreenMargin => _offScreenMargin;
|
||||
public Thickness OffScreenMargin => default;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0);
|
||||
@@ -1692,6 +1669,9 @@ namespace Avalonia.Win32
|
||||
/// <inheritdoc/>
|
||||
public CustomWndProcHookCallback? WndProcHookCallback { get; set; }
|
||||
|
||||
private WindowRectAdjuster CreateWindowRectAdjuster()
|
||||
=> new(this);
|
||||
|
||||
private ResizeReasonScope SetResizeReason(WindowResizeReason reason)
|
||||
{
|
||||
var old = _resizeReason;
|
||||
@@ -1704,7 +1684,7 @@ namespace Avalonia.Win32
|
||||
public WindowStyles Style { get; set; }
|
||||
public WindowStyles ExStyle { get; set; }
|
||||
public RECT WindowRect { get; set; }
|
||||
};
|
||||
}
|
||||
|
||||
protected struct WindowProperties
|
||||
{
|
||||
@@ -1748,5 +1728,38 @@ namespace Avalonia.Win32
|
||||
public int Time;
|
||||
public PixelPoint Pt;
|
||||
}
|
||||
|
||||
private struct WindowRectAdjuster
|
||||
{
|
||||
private static readonly bool s_hasAdjustWindowRectExForDpi = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 14393);
|
||||
|
||||
private readonly double _relativeScaling;
|
||||
private readonly uint _dpi;
|
||||
|
||||
public WindowRectAdjuster(WindowImpl owner)
|
||||
{
|
||||
if (s_hasAdjustWindowRectExForDpi)
|
||||
_dpi = (uint)(owner.RenderScaling * StandardDpi);
|
||||
else
|
||||
{
|
||||
var primaryScaling = owner.Screen.AllScreens.FirstOrDefault(screen => screen.IsPrimary)?.Scaling ?? 1;
|
||||
_relativeScaling = owner.RenderScaling / primaryScaling;
|
||||
}
|
||||
}
|
||||
|
||||
public void Adjust(ref RECT rect, WindowStyles style, WindowStyles exStyle)
|
||||
{
|
||||
if (s_hasAdjustWindowRectExForDpi)
|
||||
AdjustWindowRectExForDpi(ref rect, style, false, exStyle, _dpi);
|
||||
else
|
||||
{
|
||||
AdjustWindowRectEx(ref rect, (uint)style, false, (uint)exStyle);
|
||||
rect.top = (int)(rect.top * _relativeScaling);
|
||||
rect.right = (int)(rect.right * _relativeScaling);
|
||||
rect.left = (int)(rect.left * _relativeScaling);
|
||||
rect.bottom = (int)(rect.bottom * _relativeScaling);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/Skia/Avalonia.Skia/Avalonia.Skia.csproj" />
|
||||
<ProjectReference Include="../../src/Windows/Avalonia.Win32/Avalonia.Win32.csproj" />
|
||||
<ProjectReference Include="../../src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="3.2.1" />
|
||||
<PackageReference Include="xunit.v3.extensibility.core" Version="3.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="../../build/SharedVersion.props" />
|
||||
<Import Project="../../build/ReferenceCoreLibraries.props" />
|
||||
<Import Project="../../build/BuildTargets.targets" />
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Chrome;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.VisualTree;
|
||||
using Xunit;
|
||||
|
||||
namespace Avalonia.IntegrationTests.Win32;
|
||||
|
||||
public abstract class ExtendClientAreaWindowTests : IDisposable
|
||||
{
|
||||
private const double ClientWidth = 200;
|
||||
private const double ClientHeight = 200;
|
||||
|
||||
private Window? _window;
|
||||
|
||||
private Window Window
|
||||
{
|
||||
get
|
||||
{
|
||||
Assert.NotNull(_window);
|
||||
return _window;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract SystemDecorations Decorations { get; }
|
||||
|
||||
public static MatrixTheoryData<bool, WindowState> States
|
||||
=> new([true, false], Enum.GetValues<WindowState>());
|
||||
|
||||
private async Task InitWindowAsync(WindowState state, bool canResize)
|
||||
{
|
||||
Assert.Null(_window);
|
||||
|
||||
_window = new Window
|
||||
{
|
||||
CanResize = canResize,
|
||||
WindowState = state,
|
||||
SystemDecorations = Decorations,
|
||||
ExtendClientAreaToDecorationsHint = true,
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome,
|
||||
Width = ClientWidth,
|
||||
Height = ClientHeight,
|
||||
WindowStartupLocation = WindowStartupLocation.Manual,
|
||||
Position = new PixelPoint(50, 50),
|
||||
Content = new Border
|
||||
{
|
||||
Background = Brushes.DodgerBlue,
|
||||
BorderBrush = Brushes.Yellow,
|
||||
BorderThickness = new Thickness(1)
|
||||
}
|
||||
};
|
||||
|
||||
_window.Show();
|
||||
|
||||
await Window.WhenLoadedAsync();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(States))]
|
||||
public async Task Normal_State_Respects_Client_Size(bool canResize, WindowState initialState)
|
||||
{
|
||||
await InitWindowAsync(initialState, canResize);
|
||||
|
||||
if (initialState != WindowState.Normal)
|
||||
Window.WindowState = WindowState.Normal;
|
||||
|
||||
// The client size should have been kept
|
||||
var expected = PixelSize.FromSize(new Size(ClientWidth, ClientHeight), Window.RenderScaling);
|
||||
var clientSize = Window.GetWin32ClientSize();
|
||||
Assert.Equal(expected, clientSize);
|
||||
|
||||
VerifyNormalState(canResize);
|
||||
}
|
||||
|
||||
protected abstract void VerifyNormalState(bool canResize);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(States))]
|
||||
public async Task Maximized_State_Fills_Screen_Working_Area(bool canResize, WindowState initialState)
|
||||
{
|
||||
await InitWindowAsync(initialState, canResize);
|
||||
|
||||
if (initialState != WindowState.Maximized)
|
||||
Window.WindowState = WindowState.Maximized;
|
||||
|
||||
// The client size should match the screen working area
|
||||
var clientSize = Window.GetWin32ClientSize();
|
||||
var screenWorkingArea = Window.GetScreen().WorkingArea;
|
||||
Assert.Equal(screenWorkingArea.Size, clientSize);
|
||||
|
||||
VerifyMaximizedState();
|
||||
}
|
||||
|
||||
protected abstract void VerifyMaximizedState();
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(States))]
|
||||
public async Task FullScreen_State_Fills_Screen(bool canResize, WindowState initialState)
|
||||
{
|
||||
await InitWindowAsync(initialState, canResize);
|
||||
|
||||
if (initialState != WindowState.FullScreen)
|
||||
Window.WindowState = WindowState.FullScreen;
|
||||
|
||||
// The client size should match the screen bounds
|
||||
var clientSize = Window.GetWin32ClientSize();
|
||||
var screenBounds = Window.GetScreen().Bounds;
|
||||
Assert.Equal(screenBounds.Width, clientSize.Width);
|
||||
Assert.Equal(screenBounds.Height, clientSize.Height);
|
||||
|
||||
// The window size should also match the screen bounds
|
||||
var windowBounds = Window.GetWin32WindowBounds();
|
||||
Assert.Equal(screenBounds, windowBounds);
|
||||
|
||||
// And no visible title bar
|
||||
AssertNoTitleBar();
|
||||
}
|
||||
|
||||
protected void AssertHasBorder()
|
||||
{
|
||||
var clientSize = Window.GetWin32ClientSize();
|
||||
var windowBounds = Window.GetWin32WindowBounds();
|
||||
Assert.NotEqual(clientSize.Width, windowBounds.Width);
|
||||
Assert.NotEqual(clientSize.Height, windowBounds.Height);
|
||||
}
|
||||
|
||||
protected void AssertNoBorder()
|
||||
{
|
||||
var clientSize = Window.GetWin32ClientSize();
|
||||
var windowBounds = Window.GetWin32WindowBounds();
|
||||
Assert.Equal(clientSize.Width, windowBounds.Width);
|
||||
Assert.Equal(clientSize.Height, windowBounds.Height);
|
||||
}
|
||||
|
||||
protected (double TitleBarHeight, double ButtonsHeight) GetTitleBarInfo()
|
||||
{
|
||||
var titleBar = Window.GetVisualDescendants().OfType<TitleBar>().FirstOrDefault();
|
||||
Assert.NotNull(titleBar);
|
||||
|
||||
var buttons = titleBar.GetVisualDescendants().OfType<CaptionButtons>().FirstOrDefault();
|
||||
Assert.NotNull(buttons);
|
||||
|
||||
return (titleBar.Height, buttons.Height);
|
||||
}
|
||||
|
||||
private void AssertNoTitleBar()
|
||||
{
|
||||
var (titleBarHeight, buttonsHeight) = GetTitleBarInfo();
|
||||
Assert.Equal(0, titleBarHeight);
|
||||
Assert.Equal(0, buttonsHeight);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _window?.Close();
|
||||
|
||||
public sealed class DecorationsFull : ExtendClientAreaWindowTests
|
||||
{
|
||||
protected override SystemDecorations Decorations
|
||||
=> SystemDecorations.Full;
|
||||
|
||||
protected override void VerifyNormalState(bool canResize)
|
||||
{
|
||||
AssertHasBorder();
|
||||
AssertLargeTitleBarWithButtons();
|
||||
}
|
||||
|
||||
protected override void VerifyMaximizedState()
|
||||
=> AssertLargeTitleBarWithButtons();
|
||||
|
||||
private void AssertLargeTitleBarWithButtons()
|
||||
{
|
||||
var (titleBarHeight, buttonsHeight) = GetTitleBarInfo();
|
||||
Assert.True(titleBarHeight > 20);
|
||||
Assert.True(buttonsHeight > 20);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DecorationsBorderOnly : ExtendClientAreaWindowTests
|
||||
{
|
||||
protected override SystemDecorations Decorations
|
||||
=> SystemDecorations.BorderOnly;
|
||||
|
||||
protected override void VerifyNormalState(bool canResize)
|
||||
{
|
||||
AssertHasBorder();
|
||||
|
||||
if (canResize)
|
||||
AssertSmallTitleBarWithoutButtons();
|
||||
else
|
||||
AssertNoTitleBar();
|
||||
}
|
||||
|
||||
protected override void VerifyMaximizedState()
|
||||
=> AssertNoTitleBar();
|
||||
|
||||
private void AssertSmallTitleBarWithoutButtons()
|
||||
{
|
||||
var (titleBarHeight, buttonsHeight) = GetTitleBarInfo();
|
||||
Assert.True(titleBarHeight < 10);
|
||||
Assert.NotEqual(0, titleBarHeight);
|
||||
Assert.Equal(0, buttonsHeight);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DecorationsNone : ExtendClientAreaWindowTests
|
||||
{
|
||||
protected override SystemDecorations Decorations
|
||||
=> SystemDecorations.None;
|
||||
|
||||
protected override void VerifyNormalState(bool canResize)
|
||||
{
|
||||
AssertNoBorder();
|
||||
AssertNoTitleBar();
|
||||
}
|
||||
|
||||
protected override void VerifyMaximizedState()
|
||||
=> AssertNoTitleBar();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Themes.Fluent;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace Avalonia.IntegrationTests.Win32.Infrastructure;
|
||||
|
||||
internal static class AppManager
|
||||
{
|
||||
private static readonly Lazy<Task<Dispatcher>> s_initTask = new(CreateUIThread, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
private static readonly CancellationTokenSource s_cancellation = new();
|
||||
|
||||
public static void Stop()
|
||||
=> s_cancellation.Cancel();
|
||||
|
||||
private static Task<Dispatcher> CreateUIThread()
|
||||
{
|
||||
var tcs = new TaskCompletionSource<Dispatcher>();
|
||||
|
||||
var uiThread = new Thread(() =>
|
||||
{
|
||||
var appBuilder = AppBuilder
|
||||
.Configure<Application>()
|
||||
.UseWin32()
|
||||
.UseSkia()
|
||||
.SetupWithoutStarting();
|
||||
|
||||
appBuilder.Instance!.Styles.Add(new FluentTheme());
|
||||
|
||||
// Ensure that Dispatcher.UIThread is initialized on this thread
|
||||
var dispatcher = Dispatcher.UIThread;
|
||||
dispatcher.VerifyAccess();
|
||||
tcs.TrySetResult(dispatcher);
|
||||
|
||||
dispatcher.MainLoop(s_cancellation.Token);
|
||||
})
|
||||
{
|
||||
Name = "UI Thread"
|
||||
};
|
||||
|
||||
uiThread.Start();
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public static Task<Dispatcher> EnsureAppInitializedAsync()
|
||||
=> s_initTask.Value;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Reflection;
|
||||
using Xunit.v3;
|
||||
|
||||
namespace Avalonia.IntegrationTests.Win32.Infrastructure;
|
||||
|
||||
internal sealed class AvaloniaTestFramework : XunitTestFramework
|
||||
{
|
||||
protected override ITestFrameworkExecutor CreateExecutor(Assembly assembly)
|
||||
=> new AvaloniaTestFrameworkExecutor(new XunitTestAssembly(assembly, null, assembly.GetName().Version));
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit.Internal;
|
||||
using Xunit.Sdk;
|
||||
using Xunit.v3;
|
||||
|
||||
namespace Avalonia.IntegrationTests.Win32.Infrastructure;
|
||||
|
||||
internal sealed class AvaloniaTestFrameworkExecutor(IXunitTestAssembly testAssembly)
|
||||
: ITestFrameworkExecutor
|
||||
{
|
||||
public async ValueTask RunTestCases(
|
||||
IReadOnlyCollection<ITestCase> testCases,
|
||||
IMessageSink executionMessageSink,
|
||||
ITestFrameworkExecutionOptions executionOptions,
|
||||
CancellationToken? cancellationToken = null)
|
||||
{
|
||||
var seed = executionOptions.Seed() ?? testAssembly.ModuleVersionID.GetHashCode();
|
||||
Randomizer.Seed = seed == int.MinValue ? int.MaxValue : Math.Abs(seed);
|
||||
var executor = new XunitTestFrameworkExecutor(testAssembly);
|
||||
|
||||
var dispatcher = await AppManager.EnsureAppInitializedAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await dispatcher
|
||||
.InvokeAsync(async () =>
|
||||
{
|
||||
using (new PreserveWorkingFolder(testAssembly))
|
||||
using (new InvariantCultureScope())
|
||||
{
|
||||
await executor
|
||||
.RunTestCases(
|
||||
testCases.Cast<IXunitTestCase>().ToArray(),
|
||||
executionMessageSink,
|
||||
executionOptions,
|
||||
cancellationToken.GetValueOrDefault())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AppManager.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Avalonia.IntegrationTests.Win32.Infrastructure;
|
||||
|
||||
internal sealed class InvariantCultureScope : IDisposable
|
||||
{
|
||||
private readonly CultureInfo? _previousCulture;
|
||||
private readonly CultureInfo? _previousUICulture;
|
||||
|
||||
public InvariantCultureScope()
|
||||
{
|
||||
_previousCulture = CultureInfo.CurrentCulture;
|
||||
_previousUICulture = CultureInfo.CurrentUICulture;
|
||||
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_previousCulture is not null)
|
||||
CultureInfo.CurrentCulture = _previousCulture;
|
||||
|
||||
if (_previousUICulture is not null)
|
||||
CultureInfo.CurrentUICulture = _previousUICulture;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using Avalonia.IntegrationTests.Win32.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
[assembly: TestFramework(typeof(AvaloniaTestFramework))]
|
||||
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)]
|
||||
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Chrome;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.VisualTree;
|
||||
using Xunit;
|
||||
|
||||
namespace Avalonia.IntegrationTests.Win32;
|
||||
|
||||
public abstract class StandardWindowTests : IDisposable
|
||||
{
|
||||
private const double ClientWidth = 200;
|
||||
private const double ClientHeight = 200;
|
||||
|
||||
private Window? _window;
|
||||
|
||||
private Window Window
|
||||
{
|
||||
get
|
||||
{
|
||||
Assert.NotNull(_window);
|
||||
return _window;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract SystemDecorations Decorations { get; }
|
||||
|
||||
protected abstract bool HasCaption { get; }
|
||||
|
||||
public static MatrixTheoryData<bool, WindowState> States
|
||||
=> new([true, false], Enum.GetValues<WindowState>());
|
||||
|
||||
private async Task InitWindowAsync(WindowState state, bool canResize)
|
||||
{
|
||||
Assert.Null(_window);
|
||||
|
||||
_window = new Window
|
||||
{
|
||||
CanResize = canResize,
|
||||
WindowState = state,
|
||||
SystemDecorations = Decorations,
|
||||
ExtendClientAreaToDecorationsHint = false,
|
||||
Width = ClientWidth,
|
||||
Height = ClientHeight,
|
||||
WindowStartupLocation = WindowStartupLocation.Manual,
|
||||
Position = new PixelPoint(50, 50),
|
||||
Content = new Border
|
||||
{
|
||||
Background = Brushes.DodgerBlue,
|
||||
BorderBrush = Brushes.Yellow,
|
||||
BorderThickness = new Thickness(1)
|
||||
}
|
||||
};
|
||||
|
||||
_window.Show();
|
||||
|
||||
await Window.WhenLoadedAsync();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(States))]
|
||||
public async Task Maximized_State_Fills_Screen_Working_Area(bool canResize, WindowState initialState)
|
||||
{
|
||||
await InitWindowAsync(initialState, canResize);
|
||||
|
||||
if (initialState != WindowState.Maximized)
|
||||
Window.WindowState = WindowState.Maximized;
|
||||
|
||||
// The client size should match the screen working area
|
||||
var clientSize = Window.GetWin32ClientSize();
|
||||
var screenWorkingArea = Window.GetScreen().WorkingArea;
|
||||
|
||||
if (HasCaption)
|
||||
{
|
||||
Assert.Equal(screenWorkingArea.Size.Width, clientSize.Width);
|
||||
Assert.True(clientSize.Height < screenWorkingArea.Size.Height);
|
||||
}
|
||||
else
|
||||
Assert.Equal(screenWorkingArea.Size, clientSize);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(States))]
|
||||
public async Task FullScreen_State_Fills_Screen(bool canResize, WindowState initialState)
|
||||
{
|
||||
await InitWindowAsync(initialState, canResize);
|
||||
|
||||
if (initialState != WindowState.FullScreen)
|
||||
Window.WindowState = WindowState.FullScreen;
|
||||
|
||||
// The client size should match the screen bounds
|
||||
var clientSize = Window.GetWin32ClientSize();
|
||||
var screenBounds = Window.GetScreen().Bounds;
|
||||
Assert.Equal(screenBounds.Width, clientSize.Width);
|
||||
Assert.Equal(screenBounds.Height, clientSize.Height);
|
||||
|
||||
// The window size should also match the screen bounds
|
||||
var windowBounds = Window.GetWin32WindowBounds();
|
||||
Assert.Equal(screenBounds, windowBounds);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _window?.Close();
|
||||
|
||||
public sealed class DecorationsFull : StandardWindowTests
|
||||
{
|
||||
protected override SystemDecorations Decorations
|
||||
=> SystemDecorations.Full;
|
||||
|
||||
protected override bool HasCaption
|
||||
=> true;
|
||||
}
|
||||
|
||||
public sealed class DecorationsBorderOnly : StandardWindowTests
|
||||
{
|
||||
protected override SystemDecorations Decorations
|
||||
=> SystemDecorations.BorderOnly;
|
||||
|
||||
protected override bool HasCaption
|
||||
=> false;
|
||||
}
|
||||
|
||||
public sealed class DecorationsNone : StandardWindowTests
|
||||
{
|
||||
protected override SystemDecorations Decorations
|
||||
=> SystemDecorations.None;
|
||||
|
||||
protected override bool HasCaption
|
||||
=> false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Avalonia.IntegrationTests.Win32;
|
||||
|
||||
internal static partial class UnmanagedMethods
|
||||
{
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static partial bool GetClientRect(IntPtr hwnd, out RECT lpRect);
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static partial bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
|
||||
|
||||
public struct RECT
|
||||
{
|
||||
public int left;
|
||||
public int top;
|
||||
public int right;
|
||||
public int bottom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform;
|
||||
using Xunit;
|
||||
|
||||
namespace Avalonia.IntegrationTests.Win32;
|
||||
|
||||
internal static class WindowExtensions
|
||||
{
|
||||
public static PixelRect ToPixelRect(this UnmanagedMethods.RECT rect)
|
||||
=> new(new PixelPoint(rect.left, rect.top), new PixelPoint(rect.right, rect.bottom));
|
||||
|
||||
public static Task WhenLoadedAsync(this Window window)
|
||||
{
|
||||
if (window.IsLoaded)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var tcs = new TaskCompletionSource();
|
||||
window.Loaded += OnLoaded;
|
||||
return tcs.Task;
|
||||
|
||||
void OnLoaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
window.Loaded -= OnLoaded;
|
||||
tcs.TrySetResult();
|
||||
}
|
||||
}
|
||||
|
||||
public static Screen GetScreen(this Window window)
|
||||
{
|
||||
var screen = window.Screens.ScreenFromWindow(window);
|
||||
Assert.NotNull(screen);
|
||||
return screen;
|
||||
}
|
||||
|
||||
public static PixelSize GetWin32ClientSize(this Window window)
|
||||
{
|
||||
var platformHandle = window.TryGetPlatformHandle();
|
||||
Assert.NotNull(platformHandle);
|
||||
|
||||
Assert.True(UnmanagedMethods.GetClientRect(platformHandle.Handle, out var rect));
|
||||
return rect.ToPixelRect().Size;
|
||||
}
|
||||
|
||||
public static PixelRect GetWin32WindowBounds(this Window window)
|
||||
{
|
||||
var platformHandle = window.TryGetPlatformHandle();
|
||||
Assert.NotNull(platformHandle);
|
||||
|
||||
Assert.True(UnmanagedMethods.GetWindowRect(platformHandle.Handle, out var rect));
|
||||
return rect.ToPixelRect();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user