/ src / modules / cmdpal / Microsoft.CmdPal.UI / Helpers / WindowPositionHelper.cs
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  }