WindowPositionHelper.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using Microsoft.UI; 6 using Microsoft.UI.Windowing; 7 using Windows.Graphics; 8 using Windows.Win32; 9 using Windows.Win32.Graphics.Gdi; 10 using Windows.Win32.UI.HiDpi; 11 12 namespace Microsoft.CmdPal.UI.Helpers; 13 14 internal static class WindowPositionHelper 15 { 16 private const int DefaultWidth = 800; 17 private const int DefaultHeight = 480; 18 private const int MinimumVisibleSize = 100; 19 private const int DefaultDpi = 96; 20 21 public static PointInt32? CalculateCenteredPosition(DisplayArea? displayArea, SizeInt32 windowSize, int windowDpi) 22 { 23 if (displayArea is null) 24 { 25 return null; 26 } 27 28 var workArea = displayArea.WorkArea; 29 if (workArea.Width <= 0 || workArea.Height <= 0) 30 { 31 return null; 32 } 33 34 var targetDpi = GetDpiForDisplay(displayArea); 35 var predictedSize = ScaleSize(windowSize, windowDpi, targetDpi); 36 37 // Clamp to work area 38 var width = Math.Min(predictedSize.Width, workArea.Width); 39 var height = Math.Min(predictedSize.Height, workArea.Height); 40 41 return new PointInt32( 42 workArea.X + ((workArea.Width - width) / 2), 43 workArea.Y + ((workArea.Height - height) / 2)); 44 } 45 46 /// <summary> 47 /// Adjusts a saved window rect to ensure it's visible on the nearest display, 48 /// accounting for DPI changes and work area differences. 49 /// </summary> 50 /// 51 public static RectInt32 AdjustRectForVisibility(RectInt32 savedRect, SizeInt32 savedScreenSize, int savedDpi) 52 { 53 var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest); 54 if (displayArea is null) 55 { 56 return savedRect; 57 } 58 59 var workArea = displayArea.WorkArea; 60 if (workArea.Width <= 0 || workArea.Height <= 0) 61 { 62 return savedRect; 63 } 64 65 var targetDpi = GetDpiForDisplay(displayArea); 66 if (savedDpi <= 0) 67 { 68 savedDpi = targetDpi; 69 } 70 71 var hasInvalidSize = savedRect.Width <= 0 || savedRect.Height <= 0; 72 if (hasInvalidSize) 73 { 74 savedRect = savedRect with { Width = DefaultWidth, Height = DefaultHeight }; 75 } 76 77 if (targetDpi != savedDpi) 78 { 79 savedRect = ScaleRect(savedRect, savedDpi, targetDpi); 80 } 81 82 var clampedSize = ClampSize(savedRect.Width, savedRect.Height, workArea); 83 84 var shouldRecenter = hasInvalidSize || 85 IsOffscreen(savedRect, workArea) || 86 savedScreenSize.Width != workArea.Width || 87 savedScreenSize.Height != workArea.Height; 88 89 if (shouldRecenter) 90 { 91 return CenterRectInWorkArea(clampedSize, workArea); 92 } 93 94 return new RectInt32(savedRect.X, savedRect.Y, clampedSize.Width, clampedSize.Height); 95 } 96 97 private static int GetDpiForDisplay(DisplayArea displayArea) 98 { 99 var hMonitor = Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId); 100 if (hMonitor == IntPtr.Zero) 101 { 102 return DefaultDpi; 103 } 104 105 var hr = PInvoke.GetDpiForMonitor( 106 new HMONITOR(hMonitor), 107 MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, 108 out var dpiX, 109 out _); 110 111 return hr.Succeeded && dpiX > 0 ? (int)dpiX : DefaultDpi; 112 } 113 114 private static SizeInt32 ScaleSize(SizeInt32 size, int fromDpi, int toDpi) 115 { 116 if (fromDpi <= 0 || toDpi <= 0 || fromDpi == toDpi) 117 { 118 return size; 119 } 120 121 var scale = (double)toDpi / fromDpi; 122 return new SizeInt32( 123 (int)Math.Round(size.Width * scale), 124 (int)Math.Round(size.Height * scale)); 125 } 126 127 private static RectInt32 ScaleRect(RectInt32 rect, int fromDpi, int toDpi) 128 { 129 var scale = (double)toDpi / fromDpi; 130 return new RectInt32( 131 (int)Math.Round(rect.X * scale), 132 (int)Math.Round(rect.Y * scale), 133 (int)Math.Round(rect.Width * scale), 134 (int)Math.Round(rect.Height * scale)); 135 } 136 137 private static SizeInt32 ClampSize(int width, int height, RectInt32 workArea) => 138 new(Math.Min(width, workArea.Width), Math.Min(height, workArea.Height)); 139 140 private static RectInt32 CenterRectInWorkArea(SizeInt32 size, RectInt32 workArea) => 141 new( 142 workArea.X + ((workArea.Width - size.Width) / 2), 143 workArea.Y + ((workArea.Height - size.Height) / 2), 144 size.Width, 145 size.Height); 146 147 private static bool IsOffscreen(RectInt32 rect, RectInt32 workArea) => 148 rect.X + MinimumVisibleSize > workArea.X + workArea.Width || 149 rect.X + rect.Width - MinimumVisibleSize < workArea.X || 150 rect.Y + MinimumVisibleSize > workArea.Y + workArea.Height || 151 rect.Y + rect.Height - MinimumVisibleSize < workArea.Y; 152 }