mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-06 19:26:39 +02:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user