Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/Controls/tests/DeviceTests/Elements/Window/WindowTests.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,110 @@ await CreateHandlerAndAddToWindow<IWindowHandler>(mainPage, async (handler) =>
});
}

[Fact]
public async Task WindowsBoundsWhenMaximized()
{
SetupBuilder();
var mainPage = new NavigationPage(new ContentPage());

await CreateHandlerAndAddToWindow<IWindowHandler>(mainPage, async (handler) =>
{
var appWindowPlatform = handler.PlatformView.GetAppWindow();
Assert.NotNull(appWindowPlatform?.Presenter);
var presenter = Assert.IsType<OverlappedPresenter>(appWindowPlatform.Presenter);

// maximize window
presenter.Maximize();
var appWindow = handler.PlatformView.GetWindow();
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
Assert.NotNull(appWindow);

// Compute work-area reference values before polling so the same values
// are used for both the wait predicate and the final assertions.
// Compare against the monitor's work area. This correctly handles negative
// coordinates when the window is on a monitor positioned left of or above
// the primary display, and catches regressions beyond a simple > 0 check.
var displayArea = DisplayArea.GetFromWindowId(appWindowPlatform.Id, DisplayAreaFallback.Nearest);
var workArea = displayArea.WorkArea;
var density = handler.PlatformView.GetDisplayDensity();

// Wait until the MAUI frame reflects the maximized work-area bounds.
// Waiting only for Height > 0 is insufficient: that condition is already true
// before Maximize() is called, so on slow machines the assertions below would
// execute against the pre-maximized frame and become flaky.
await AssertEventually(() =>
Math.Abs(appWindow.Width - workArea.Width / density) < 2 &&
Math.Abs(appWindow.Height - workArea.Height / density) < 2);

Assert.True(Math.Abs(appWindow.X - workArea.X / density) < 2,
$"X should be near work area X ({workArea.X / density:F2}) but was {appWindow.X}");
Assert.True(Math.Abs(appWindow.Y - workArea.Y / density) < 2,
$"Y should be near work area Y ({workArea.Y / density:F2}) but was {appWindow.Y}");
Assert.True(Math.Abs(appWindow.Width - workArea.Width / density) < 2,
$"Width should match work area width ({workArea.Width / density:F2}) but was {appWindow.Width}");
Assert.True(Math.Abs(appWindow.Height - workArea.Height / density) < 2,
$"Height should match work area height ({workArea.Height / density:F2}) but was {appWindow.Height}");
});
}

[Fact]
public async Task WindowsYAndHeightCorrectWhenClosingMaximizedWindow()
{
SetupBuilder();
var mainPage = new NavigationPage(new ContentPage());

double destroyingY = double.NaN;
double destroyingHeight = double.NaN;
double expectedY = double.NaN;
double expectedHeight = double.NaN;

await CreateHandlerAndAddToWindow<IWindowHandler>(mainPage, async (handler) =>
{
var window = handler.VirtualView as Window;
Assert.NotNull(window);

var appWindowPlatform = handler.PlatformView.GetAppWindow();
Assert.NotNull(appWindowPlatform?.Presenter);
var presenter = Assert.IsType<OverlappedPresenter>(appWindowPlatform.Presenter);

// Capture the frame values at destroy time so we can verify them after cleanup
window.Destroying += (s, e) =>
{
destroyingY = window.Y;
destroyingHeight = window.Height;
};

// Capture the expected work-area bounds before waiting for the maximized frame,
// so the same reference values are used for both the wait predicate and the
// post-cleanup assertions.
var displayArea = DisplayArea.GetFromWindowId(appWindowPlatform.Id, DisplayAreaFallback.Nearest);
var workArea = displayArea.WorkArea;
var density = handler.PlatformView.GetDisplayDensity();
expectedY = workArea.Y / density;
expectedHeight = workArea.Height / density;

// Maximize the window and wait until the MAUI frame reflects the maximized bounds.
// Waiting only for Height > 0 is insufficient: that condition is already true
// before Maximize() is called, so on slow machines the captured values at
// Destroying time could come from the pre-maximized frame.
// Do not assert window.Y == 0: on monitors positioned above the primary display
// Y is legitimately negative.
presenter.Maximize();
await AssertEventually(() =>
Math.Abs(window.Height - expectedHeight) < 2 &&
Math.Abs(window.Y - expectedY) < 2);
});

// The window is destroyed during CreateHandlerAndAddToWindow cleanup.
// Assert that the bounds reported at Destroying time match the monitor work area,
// using the same < 2 tolerance as WindowsBoundsWhenMaximized. This catches
// regressions where Y or Height is off by ~8 pixels when closing a maximized window.
Assert.False(double.IsNaN(destroyingHeight), "Window.Destroying event was not raised");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Regression PreventionWindowsYAndHeightCorrectWhenClosingMaximizedWindow checks destroyingHeight for NaN to prove Destroying fired. Add the same sentinel check for destroyingY so a missing or malformed captured Y value reports the lifecycle problem directly instead of falling through to the tolerance assertion.

Assert.True(Math.Abs(destroyingY - expectedY) < 2,
$"Y should be near work area Y ({expectedY:F2}) when closing a maximized window, but was {destroyingY}");
Assert.True(Math.Abs(destroyingHeight - expectedHeight) < 2,
$"Height should match work area height ({expectedHeight:F2}) when closing a maximized window, but was {destroyingHeight}");
}

[Fact]
public async Task ToggleFullscreenTitleBarWorks()
{
Expand Down
24 changes: 24 additions & 0 deletions src/Core/src/Handlers/Window/WindowHandler.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml.Controls;
using Windows.Graphics;
using WinRT.Interop;

namespace Microsoft.Maui.Handlers
{
public partial class WindowHandler : ElementHandler<IWindow, UI.Xaml.Window>
{
// The HWND never changes after the window is created; cache it once to avoid
// a COM/marshalling round-trip on every OnWindowChanged (move/resize) event.
IntPtr _hwnd = IntPtr.Zero;

protected override void ConnectHandler(UI.Xaml.Window platformView)
{
base.ConnectHandler(platformView);
Expand All @@ -20,6 +25,8 @@ protected override void ConnectHandler(UI.Xaml.Window platformView)
platformView.UpdatePosition(VirtualView);
platformView.UpdateSize(VirtualView);

_hwnd = platformView.GetWindowHandle();

var appWindow = platformView.GetAppWindow();
if (appWindow is not null)
{
Expand Down Expand Up @@ -68,6 +75,8 @@ protected override void DisconnectHandler(UI.Xaml.Window platformView)
appWindow.Changed -= OnWindowChanged;
}

_hwnd = IntPtr.Zero;

base.DisconnectHandler(platformView);
}

Expand Down Expand Up @@ -202,9 +211,24 @@ void OnWindowChanged(AppWindow sender, AppWindowChangedEventArgs args)

void UpdateVirtualViewFrame(AppWindow appWindow)
{
// Use the HWND cached at ConnectHandler — it never changes and this method
// runs on every move/resize event, so avoid the COM round-trip each time.
if (_hwnd == IntPtr.Zero)
{
return;
}

var size = appWindow.Size;
var pos = appWindow.Position;

if (appWindow.Presenter is OverlappedPresenter presenter &&
presenter.State == OverlappedPresenterState.Maximized &&
PlatformMethods.TryGetExtendedFrameBounds(_hwnd, out var dwmRect))
{
size = new SizeInt32(dwmRect.Right - dwmRect.Left, dwmRect.Bottom - dwmRect.Top);
pos = new PointInt32(dwmRect.Left, dwmRect.Top);
}

var density = PlatformView.GetDisplayDensity();

VirtualView.FrameChanged(new Rect(
Expand Down
13 changes: 13 additions & 0 deletions src/Essentials/src/Platform/PlatformMethods.windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ public static long GetWindowLongPtr(IntPtr hWnd, WindowLongFlags nIndex)
static extern long GetWindowLongPtr64(IntPtr hWnd, WindowLongFlags nIndex);
}

internal static bool TryGetExtendedFrameBounds(IntPtr hWnd, out RECT bounds)
{
bounds = default;
if (hWnd == IntPtr.Zero)
{
return false;
}

int hr = DwmGetWindowAttribute(hWnd, DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS,
out bounds, Marshal.SizeOf<RECT>());
return hr == 0 && (bounds.Right - bounds.Left) > 0 && (bounds.Bottom - bounds.Top) > 0;
}

[DllImport("user32.dll")]
public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);

Expand Down
Loading