CmdPal: Fixes and improve main window positioning (#45585)

## Summary of the Pull Request

This PR improves main window positioning:

- Fixes cases where an invalid window size or position was saved.  
- `UpdateWindowPositionInMemory` failed to capture correct values when
the window was minimized or maximized (for example, a minimized window
reports coordinates like `(-32000, -32000)`).
- Improves repositioning logic to use relative anchors (corners and
center). When switching displays, the window should reappear in the
expected position. This also reduces cases that trigger the failsafe
recentering.
- Fixes the dragging rectangle size after switching DPIs - the rectangle
was not adapting, so it when switching from 100 % to 200 % it covered
only left half of the window and had teeny-tiny height.
- Suppresses system DPI handling during summon to prevent double
scaling.
- Makes `WindowPosition` class immutable.
- Adds light-weight failsafe preventing overwriting position with
invalid data.
- Hotfixes a min/max state conflict with the WinUIEx window manager.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #45576
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2026-02-19 19:43:32 +01:00
committed by GitHub
parent dcf4c4d16d
commit 39bfa86335
4 changed files with 185 additions and 68 deletions

View File

@@ -18,7 +18,7 @@ internal static class WindowPositionHelper
private const int MinimumVisibleSize = 100;
private const int DefaultDpi = 96;
public static PointInt32? CalculateCenteredPosition(DisplayArea? displayArea, SizeInt32 windowSize, int windowDpi)
public static RectInt32? CenterOnDisplay(DisplayArea? displayArea, SizeInt32 windowSize, int windowDpi)
{
if (displayArea is null)
{
@@ -32,15 +32,9 @@ internal static class WindowPositionHelper
}
var targetDpi = GetDpiForDisplay(displayArea);
var predictedSize = ScaleSize(windowSize, windowDpi, targetDpi);
// Clamp to work area
var width = Math.Min(predictedSize.Width, workArea.Width);
var height = Math.Min(predictedSize.Height, workArea.Height);
return new PointInt32(
workArea.X + ((workArea.Width - width) / 2),
workArea.Y + ((workArea.Height - height) / 2));
var scaledSize = ScaleSize(windowSize, windowDpi, targetDpi);
var clampedSize = ClampSize(scaledSize.Width, scaledSize.Height, workArea);
return CenterRectInWorkArea(clampedSize, workArea);
}
/// <summary>
@@ -74,6 +68,10 @@ internal static class WindowPositionHelper
savedRect = savedRect with { Width = DefaultWidth, Height = DefaultHeight };
}
// Remember the original size before DPI scaling - needed to compute
// gaps relative to the old screen when repositioning across displays.
var originalSize = new SizeInt32(savedRect.Width, savedRect.Height);
if (targetDpi != savedDpi)
{
savedRect = ScaleRect(savedRect, savedDpi, targetDpi);
@@ -81,12 +79,17 @@ internal static class WindowPositionHelper
var clampedSize = ClampSize(savedRect.Width, savedRect.Height, workArea);
var shouldRecenter = hasInvalidSize ||
IsOffscreen(savedRect, workArea) ||
savedScreenSize.Width != workArea.Width ||
savedScreenSize.Height != workArea.Height;
if (hasInvalidSize)
{
return CenterRectInWorkArea(clampedSize, workArea);
}
if (shouldRecenter)
if (savedScreenSize.Width != workArea.Width || savedScreenSize.Height != workArea.Height)
{
return RepositionRelativeToWorkArea(savedRect, savedScreenSize, originalSize, clampedSize, workArea);
}
if (IsOffscreen(savedRect, workArea))
{
return CenterRectInWorkArea(clampedSize, workArea);
}
@@ -126,27 +129,92 @@ internal static class WindowPositionHelper
private static RectInt32 ScaleRect(RectInt32 rect, int fromDpi, int toDpi)
{
if (fromDpi <= 0 || toDpi <= 0 || fromDpi == toDpi)
{
return rect;
}
// Don't scale position, that's absolute coordinates in virtual screen space
var scale = (double)toDpi / fromDpi;
return new RectInt32(
(int)Math.Round(rect.X * scale),
(int)Math.Round(rect.Y * scale),
rect.X,
rect.Y,
(int)Math.Round(rect.Width * scale),
(int)Math.Round(rect.Height * scale));
}
private static SizeInt32 ClampSize(int width, int height, RectInt32 workArea) =>
new(Math.Min(width, workArea.Width), Math.Min(height, workArea.Height));
private static SizeInt32 ClampSize(int width, int height, RectInt32 workArea)
{
return new SizeInt32(Math.Min(width, workArea.Width), Math.Min(height, workArea.Height));
}
private static RectInt32 CenterRectInWorkArea(SizeInt32 size, RectInt32 workArea) =>
new(
private static RectInt32 RepositionRelativeToWorkArea(RectInt32 savedRect, SizeInt32 savedScreenSize, SizeInt32 originalSize, SizeInt32 clampedSize, RectInt32 workArea)
{
// Treat each axis as a 3-zone grid (start / center / end) so that
// edge-snapped windows stay snapped and centered windows stay centered.
// We don't store the old work area origin, so we use the current one as a
// best estimate (correct when the same physical display changed resolution/DPI/taskbar).
var newX = ScaleAxisByZone(savedRect.X, originalSize.Width, clampedSize.Width, workArea.X, savedScreenSize.Width, workArea.Width);
var newY = ScaleAxisByZone(savedRect.Y, originalSize.Height, clampedSize.Height, workArea.Y, savedScreenSize.Height, workArea.Height);
newX = Math.Clamp(newX, workArea.X, Math.Max(workArea.X, workArea.X + workArea.Width - clampedSize.Width));
newY = Math.Clamp(newY, workArea.Y, Math.Max(workArea.Y, workArea.Y + workArea.Height - clampedSize.Height));
return new RectInt32(newX, newY, clampedSize.Width, clampedSize.Height);
}
/// <summary>
/// Repositions a window along one axis using a 3-zone model (start / center / end).
/// The zone is determined by which third of the old screen the window center falls in.
/// Uses <paramref name="oldWindowSize"/> (pre-DPI-scaling) for gap calculations against
/// the old screen, and <paramref name="newWindowSize"/> (post-scaling) for placement on the new screen.
/// </summary>
private static int ScaleAxisByZone(int savedPos, int oldWindowSize, int newWindowSize, int workAreaOrigin, int oldScreenSize, int newScreenSize)
{
if (oldScreenSize <= 0 || newScreenSize <= 0)
{
return savedPos;
}
var gapFromStart = savedPos - workAreaOrigin;
var windowCenter = gapFromStart + (oldWindowSize / 2);
if (windowCenter >= oldScreenSize / 3 && windowCenter <= oldScreenSize * 2 / 3)
{
// Center zone - keep centered
return workAreaOrigin + ((newScreenSize - newWindowSize) / 2);
}
var gapFromEnd = oldScreenSize - gapFromStart - oldWindowSize;
if (gapFromStart <= gapFromEnd)
{
// Start zone - preserve proportional distance from start edge
var rel = (double)gapFromStart / oldScreenSize;
return workAreaOrigin + (int)Math.Round(rel * newScreenSize);
}
else
{
// End zone - preserve proportional distance from end edge
var rel = (double)gapFromEnd / oldScreenSize;
return workAreaOrigin + newScreenSize - newWindowSize - (int)Math.Round(rel * newScreenSize);
}
}
private static RectInt32 CenterRectInWorkArea(SizeInt32 size, RectInt32 workArea)
{
return new RectInt32(
workArea.X + ((workArea.Width - size.Width) / 2),
workArea.Y + ((workArea.Height - size.Height) / 2),
size.Width,
size.Height);
}
private static bool IsOffscreen(RectInt32 rect, RectInt32 workArea) =>
rect.X + MinimumVisibleSize > workArea.X + workArea.Width ||
rect.X + rect.Width - MinimumVisibleSize < workArea.X ||
rect.Y + MinimumVisibleSize > workArea.Y + workArea.Height ||
rect.Y + rect.Height - MinimumVisibleSize < workArea.Y;
private static bool IsOffscreen(RectInt32 rect, RectInt32 workArea)
{
return rect.X + MinimumVisibleSize > workArea.X + workArea.Width ||
rect.X + rect.Width - MinimumVisibleSize < workArea.X ||
rect.Y + MinimumVisibleSize > workArea.Y + workArea.Height ||
rect.Y + rect.Height - MinimumVisibleSize < workArea.Y;
}
}