/ src / modules / ZoomIt / ZoomIt / Utility.cpp
Utility.cpp
  1  //==============================================================================
  2  //
  3  // Zoomit
  4  // Sysinternals - www.sysinternals.com
  5  //
  6  // Utility functions
  7  //
  8  //==============================================================================
  9  #include "pch.h"
 10  #include "Utility.h"
 11  #include <string>
 12  
 13  #pragma comment(lib, "uxtheme.lib")
 14  
 15  //----------------------------------------------------------------------------
 16  // Dark Mode - Static/Global State
 17  //----------------------------------------------------------------------------
 18  static bool g_darkModeInitialized = false;
 19  static bool g_darkModeEnabled = false;
 20  static HBRUSH g_darkBackgroundBrush = nullptr;
 21  static HBRUSH g_darkControlBrush = nullptr;
 22  static HBRUSH g_darkSurfaceBrush = nullptr;
 23  
 24  // Theme override from registry (defined in ZoomItSettings.h)
 25  extern DWORD g_ThemeOverride;
 26  
 27  // Preferred App Mode values for Windows 10/11 dark mode
 28  enum class PreferredAppMode
 29  {
 30      Default,
 31      AllowDark,
 32      ForceDark,
 33      ForceLight,
 34      Max
 35  };
 36  
 37  // Undocumented ordinals from uxtheme.dll for dark mode support
 38  using fnSetPreferredAppMode = PreferredAppMode(WINAPI*)(PreferredAppMode appMode);
 39  using fnAllowDarkModeForWindow = bool(WINAPI*)(HWND hWnd, bool allow);
 40  using fnShouldAppsUseDarkMode = bool(WINAPI*)();
 41  using fnRefreshImmersiveColorPolicyState = void(WINAPI*)();
 42  using fnFlushMenuThemes = void(WINAPI*)();
 43  
 44  static fnSetPreferredAppMode pSetPreferredAppMode = nullptr;
 45  static fnAllowDarkModeForWindow pAllowDarkModeForWindow = nullptr;
 46  static fnShouldAppsUseDarkMode pShouldAppsUseDarkMode = nullptr;
 47  static fnRefreshImmersiveColorPolicyState pRefreshImmersiveColorPolicyState = nullptr;
 48  static fnFlushMenuThemes pFlushMenuThemes = nullptr;
 49  
 50  //----------------------------------------------------------------------------
 51  //
 52  // InitializeDarkModeSupport
 53  //
 54  // Initialize dark mode function pointers from uxtheme.dll
 55  //
 56  //----------------------------------------------------------------------------
 57  static void InitializeDarkModeSupport()
 58  {
 59      if (g_darkModeInitialized)
 60          return;
 61  
 62      g_darkModeInitialized = true;
 63  
 64      HMODULE hUxTheme = GetModuleHandleW(L"uxtheme.dll");
 65      if (hUxTheme)
 66      {
 67          // These are undocumented ordinal exports
 68          // Ordinal 135: SetPreferredAppMode (Windows 10 1903+)
 69          pSetPreferredAppMode = reinterpret_cast<fnSetPreferredAppMode>(
 70              GetProcAddress(hUxTheme, MAKEINTRESOURCEA(135)));
 71          // Ordinal 133: AllowDarkModeForWindow
 72          pAllowDarkModeForWindow = reinterpret_cast<fnAllowDarkModeForWindow>(
 73              GetProcAddress(hUxTheme, MAKEINTRESOURCEA(133)));
 74          // Ordinal 132: ShouldAppsUseDarkMode
 75          pShouldAppsUseDarkMode = reinterpret_cast<fnShouldAppsUseDarkMode>(
 76              GetProcAddress(hUxTheme, MAKEINTRESOURCEA(132)));
 77          // Ordinal 104: RefreshImmersiveColorPolicyState
 78          pRefreshImmersiveColorPolicyState = reinterpret_cast<fnRefreshImmersiveColorPolicyState>(
 79              GetProcAddress(hUxTheme, MAKEINTRESOURCEA(104)));
 80          // Ordinal 136: FlushMenuThemes
 81          pFlushMenuThemes = reinterpret_cast<fnFlushMenuThemes>(
 82              GetProcAddress(hUxTheme, MAKEINTRESOURCEA(136)));
 83  
 84          // Set preferred app mode based on our theme override or system setting
 85          // Note: We check g_ThemeOverride directly here because IsDarkModeEnabled
 86          // calls InitializeDarkModeSupport, which would cause recursion
 87          if (pSetPreferredAppMode)
 88          {
 89              bool useDarkMode = false;
 90              if (g_ThemeOverride == 0)
 91              {
 92                  useDarkMode = false; // Force light
 93              }
 94              else if (g_ThemeOverride == 1)
 95              {
 96                  useDarkMode = true; // Force dark
 97              }
 98              else if (pShouldAppsUseDarkMode)
 99              {
100                  useDarkMode = pShouldAppsUseDarkMode(); // Use system setting
101              }
102  
103              if (useDarkMode)
104              {
105                  pSetPreferredAppMode(PreferredAppMode::ForceDark);
106              }
107              else
108              {
109                  pSetPreferredAppMode(PreferredAppMode::ForceLight);
110              }
111          }
112  
113          // Flush menu themes to apply dark mode to context menus
114          if (pFlushMenuThemes)
115          {
116              pFlushMenuThemes();
117          }
118      }
119  
120      // Update cached dark mode state
121      g_darkModeEnabled = false;
122      if (g_ThemeOverride == 0)
123      {
124          g_darkModeEnabled = false;
125      }
126      else if (g_ThemeOverride == 1)
127      {
128          g_darkModeEnabled = true;
129      }
130      else if (pShouldAppsUseDarkMode)
131      {
132          g_darkModeEnabled = pShouldAppsUseDarkMode();
133      }
134  }
135  
136  //----------------------------------------------------------------------------
137  //
138  // IsDarkModeEnabled
139  //
140  //----------------------------------------------------------------------------
141  bool IsDarkModeEnabled()
142  {
143      // Check for theme override from registry (0=light, 1=dark, 2+=system)
144      if (g_ThemeOverride == 0)
145      {
146          return false; // Force light mode
147      }
148      else if (g_ThemeOverride == 1)
149      {
150          return true; // Force dark mode
151      }
152  
153      InitializeDarkModeSupport();
154  
155      // Check the undocumented API first
156      if (pShouldAppsUseDarkMode)
157      {
158          return pShouldAppsUseDarkMode();
159      }
160  
161      // Fallback: Check registry for system theme preference
162      HKEY hKey;
163      if (RegOpenKeyExW(HKEY_CURRENT_USER,
164          L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
165          0, KEY_READ, &hKey) == ERROR_SUCCESS)
166      {
167          DWORD value = 1;
168          DWORD size = sizeof(value);
169          RegQueryValueExW(hKey, L"AppsUseLightTheme", nullptr, nullptr,
170              reinterpret_cast<LPBYTE>(&value), &size);
171          RegCloseKey(hKey);
172          return value == 0; // 0 = dark mode, 1 = light mode
173      }
174  
175      return false;
176  }
177  
178  //----------------------------------------------------------------------------
179  //
180  // RefreshDarkModeState
181  //
182  //----------------------------------------------------------------------------
183  void RefreshDarkModeState()
184  {
185      InitializeDarkModeSupport();
186  
187      if (pRefreshImmersiveColorPolicyState)
188      {
189          pRefreshImmersiveColorPolicyState();
190      }
191  
192      // Update preferred app mode based on our IsDarkModeEnabled (respects override)
193      bool useDark = IsDarkModeEnabled();
194      if (pSetPreferredAppMode)
195      {
196          if (useDark)
197          {
198              pSetPreferredAppMode(PreferredAppMode::ForceDark);
199          }
200          else
201          {
202              pSetPreferredAppMode(PreferredAppMode::ForceLight);
203          }
204      }
205  
206      // Flush menu themes to apply dark mode to context menus
207      if (pFlushMenuThemes)
208      {
209          pFlushMenuThemes();
210      }
211  
212      g_darkModeEnabled = useDark;
213  }
214  
215  //----------------------------------------------------------------------------
216  //
217  // SetDarkModeForWindow
218  //
219  //----------------------------------------------------------------------------
220  void SetDarkModeForWindow(HWND hWnd, bool enable)
221  {
222      InitializeDarkModeSupport();
223  
224      if (pAllowDarkModeForWindow)
225      {
226          pAllowDarkModeForWindow(hWnd, enable);
227      }
228  
229      // Use DWMWA_USE_IMMERSIVE_DARK_MODE attribute (Windows 10 build 17763+)
230      // Attribute 20 is DWMWA_USE_IMMERSIVE_DARK_MODE
231      BOOL useDarkMode = enable ? TRUE : FALSE;
232      HMODULE hDwmapi = GetModuleHandleW(L"dwmapi.dll");
233      if (hDwmapi)
234      {
235          using fnDwmSetWindowAttribute = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD);
236          auto pDwmSetWindowAttribute = reinterpret_cast<fnDwmSetWindowAttribute>(
237              GetProcAddress(hDwmapi, "DwmSetWindowAttribute"));
238          if (pDwmSetWindowAttribute)
239          {
240              // Try attribute 20 first (Windows 11 / newer Windows 10)
241              HRESULT hr = pDwmSetWindowAttribute(hWnd, 20, &useDarkMode, sizeof(useDarkMode));
242              if (FAILED(hr))
243              {
244                  // Fall back to attribute 19 (older Windows 10)
245                  pDwmSetWindowAttribute(hWnd, 19, &useDarkMode, sizeof(useDarkMode));
246              }
247          }
248      }
249  }
250  
251  //----------------------------------------------------------------------------
252  //
253  // GetDarkModeBrush / GetDarkModeControlBrush / GetDarkModeSurfaceBrush
254  //
255  //----------------------------------------------------------------------------
256  HBRUSH GetDarkModeBrush()
257  {
258      if (!g_darkBackgroundBrush)
259      {
260          g_darkBackgroundBrush = CreateSolidBrush(DarkMode::BackgroundColor);
261      }
262      return g_darkBackgroundBrush;
263  }
264  
265  HBRUSH GetDarkModeControlBrush()
266  {
267      if (!g_darkControlBrush)
268      {
269          g_darkControlBrush = CreateSolidBrush(DarkMode::ControlColor);
270      }
271      return g_darkControlBrush;
272  }
273  
274  HBRUSH GetDarkModeSurfaceBrush()
275  {
276      if (!g_darkSurfaceBrush)
277      {
278          g_darkSurfaceBrush = CreateSolidBrush(DarkMode::SurfaceColor);
279      }
280      return g_darkSurfaceBrush;
281  }
282  
283  //----------------------------------------------------------------------------
284  //
285  // ApplyDarkModeToDialog
286  //
287  //----------------------------------------------------------------------------
288  void ApplyDarkModeToDialog(HWND hDlg)
289  {
290      if (IsDarkModeEnabled())
291      {
292          SetDarkModeForWindow(hDlg, true);
293  
294          // Set dark theme for the dialog
295          SetWindowTheme(hDlg, L"DarkMode_Explorer", nullptr);
296  
297          // Apply dark theme to common controls (buttons, edit boxes, etc.)
298          EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL {
299              wchar_t className[64] = { 0 };
300              GetClassNameW(hChild, className, _countof(className));
301  
302              // Apply appropriate theme based on control type
303              if (_wcsicmp(className, L"Button") == 0)
304              {
305                  // Check if this is a checkbox or radio button
306                  LONG style = GetWindowLong(hChild, GWL_STYLE);
307                  LONG buttonType = style & BS_TYPEMASK;
308                  if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX ||
309                      buttonType == BS_3STATE || buttonType == BS_AUTO3STATE ||
310                      buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON)
311                  {
312                      // Subclass checkbox/radio for dark mode painting - but keep DarkMode_Explorer theme
313                      // for proper hit testing (empty theme can break mouse interaction)
314                      SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
315                      SetWindowSubclass(hChild, CheckboxSubclassProc, 2, 0);
316                  }
317                  else if (buttonType == BS_GROUPBOX)
318                  {
319                      // Subclass group box for dark mode painting
320                      SetWindowTheme(hChild, L"", L"");
321                      SetWindowSubclass(hChild, GroupBoxSubclassProc, 4, 0);
322                  }
323                  else
324                  {
325                      SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
326                  }
327              }
328              else if (_wcsicmp(className, L"Edit") == 0)
329              {
330                  // Use empty theme and subclass for dark mode border drawing
331                  SetWindowTheme(hChild, L"", L"");
332                  SetWindowSubclass(hChild, EditControlSubclassProc, 3, 0);
333              }
334              else if (_wcsicmp(className, L"ComboBox") == 0)
335              {
336                  SetWindowTheme(hChild, L"DarkMode_CFD", nullptr);
337              }
338              else if (_wcsicmp(className, L"SysListView32") == 0 ||
339                       _wcsicmp(className, L"SysTreeView32") == 0)
340              {
341                  SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
342              }
343              else if (_wcsicmp(className, L"msctls_trackbar32") == 0)
344              {
345                  // Subclass trackbar controls for dark mode painting
346                  SetWindowTheme(hChild, L"", L"");
347                  SetWindowSubclass(hChild, SliderSubclassProc, 1, 0);
348              }
349              else if (_wcsicmp(className, L"SysTabControl32") == 0)
350              {
351                  // Use empty theme for tab control to allow dark background
352                  SetWindowTheme(hChild, L"", L"");
353              }
354              else if (_wcsicmp(className, L"msctls_updown32") == 0)
355              {
356                  SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
357              }
358              else if (_wcsicmp(className, L"msctls_hotkey32") == 0)
359              {
360                  // Subclass hotkey controls for dark mode painting
361                  SetWindowTheme(hChild, L"", L"");
362                  SetWindowSubclass(hChild, HotkeyControlSubclassProc, 1, 0);
363              }
364              else if (_wcsicmp(className, L"Static") == 0)
365              {
366                  // Check if this is a text label (not an owner-draw or image control)
367                  LONG style = GetWindowLong(hChild, GWL_STYLE);
368                  LONG staticType = style & SS_TYPEMASK;
369  
370                  // Options header uses a dedicated static subclass (to support large title font).
371                  // Avoid applying the generic static subclass on top of it.
372                  const int controlId = GetDlgCtrlID( hChild );
373                  if( controlId == IDC_VERSION || controlId == IDC_COPYRIGHT )
374                  {
375                      SetWindowTheme( hChild, L"", L"" );
376                      return TRUE;
377                  }
378  
379                  if (staticType == SS_LEFT || staticType == SS_CENTER || staticType == SS_RIGHT ||
380                      staticType == SS_LEFTNOWORDWRAP || staticType == SS_SIMPLE)
381                  {
382                      // Subclass text labels for proper dark mode painting
383                      SetWindowTheme(hChild, L"", L"");
384                      SetWindowSubclass(hChild, StaticTextSubclassProc, 5, 0);
385                  }
386                  else
387                  {
388                      // Other static controls (icons, bitmaps, frames) - just remove theme
389                      SetWindowTheme(hChild, L"", L"");
390                  }
391              }
392              else
393              {
394                  SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
395              }
396              return TRUE;
397          }, 0);
398      }
399      else
400      {
401          // Light mode - remove dark mode
402          SetDarkModeForWindow(hDlg, false);
403          SetWindowTheme(hDlg, nullptr, nullptr);
404  
405          EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL {
406              // Remove subclass from controls
407              wchar_t className[64] = { 0 };
408              GetClassNameW(hChild, className, _countof(className));
409              if (_wcsicmp(className, L"msctls_hotkey32") == 0)
410              {
411                  RemoveWindowSubclass(hChild, HotkeyControlSubclassProc, 1);
412              }
413              else if (_wcsicmp(className, L"msctls_trackbar32") == 0)
414              {
415                  RemoveWindowSubclass(hChild, SliderSubclassProc, 1);
416              }
417              else if (_wcsicmp(className, L"Button") == 0)
418              {
419                  LONG style = GetWindowLong(hChild, GWL_STYLE);
420                  LONG buttonType = style & BS_TYPEMASK;
421                  if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX ||
422                      buttonType == BS_3STATE || buttonType == BS_AUTO3STATE ||
423                      buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON)
424                  {
425                      RemoveWindowSubclass(hChild, CheckboxSubclassProc, 2);
426                  }
427                  else if (buttonType == BS_GROUPBOX)
428                  {
429                      RemoveWindowSubclass(hChild, GroupBoxSubclassProc, 4);
430                  }
431              }
432              else if (_wcsicmp(className, L"Edit") == 0)
433              {
434                  RemoveWindowSubclass(hChild, EditControlSubclassProc, 3);
435              }
436              else if (_wcsicmp(className, L"Static") == 0)
437              {
438                  RemoveWindowSubclass(hChild, StaticTextSubclassProc, 5);
439              }
440              SetWindowTheme(hChild, nullptr, nullptr);
441              return TRUE;
442          }, 0);
443      }
444  }
445  
446  //----------------------------------------------------------------------------
447  //
448  // HandleDarkModeCtlColor
449  //
450  //----------------------------------------------------------------------------
451  HBRUSH HandleDarkModeCtlColor(HDC hdc, HWND hCtrl, UINT message)
452  {
453      if (!IsDarkModeEnabled())
454      {
455          return nullptr;
456      }
457  
458      switch (message)
459      {
460      case WM_CTLCOLORDLG:
461          SetBkColor(hdc, DarkMode::BackgroundColor);
462          SetTextColor(hdc, DarkMode::TextColor);
463          return GetDarkModeBrush();
464  
465      case WM_CTLCOLORSTATIC:
466          SetBkMode(hdc, TRANSPARENT);
467          // Use dimmed color for disabled static controls
468          if (!IsWindowEnabled(hCtrl))
469          {
470              SetTextColor(hdc, RGB(100, 100, 100));
471          }
472          else
473          {
474              SetTextColor(hdc, DarkMode::TextColor);
475          }
476          return GetDarkModeBrush();
477  
478      case WM_CTLCOLORBTN:
479          SetBkColor(hdc, DarkMode::ControlColor);
480          SetTextColor(hdc, DarkMode::TextColor);
481          return GetDarkModeControlBrush();
482  
483      case WM_CTLCOLOREDIT:
484          SetBkColor(hdc, DarkMode::SurfaceColor);
485          SetTextColor(hdc, DarkMode::TextColor);
486          return GetDarkModeSurfaceBrush();
487  
488      case WM_CTLCOLORLISTBOX:
489          SetBkColor(hdc, DarkMode::SurfaceColor);
490          SetTextColor(hdc, DarkMode::TextColor);
491          return GetDarkModeSurfaceBrush();
492      }
493  
494      return nullptr;
495  }
496  
497  //----------------------------------------------------------------------------
498  //
499  // ApplyDarkModeToMenu
500  //
501  // Uses undocumented uxtheme functions to enable dark mode for menus
502  //
503  //----------------------------------------------------------------------------
504  void ApplyDarkModeToMenu(HMENU hMenu)
505  {
506      if (!hMenu)
507      {
508          return;
509      }
510  
511      if (!IsDarkModeEnabled())
512      {
513          // Light mode - clear any dark background
514          MENUINFO mi = { sizeof(mi) };
515          mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
516          mi.hbrBack = nullptr;
517          SetMenuInfo(hMenu, &mi);
518          return;
519      }
520  
521      // For popup menus, we need to use MENUINFO to set the background
522      MENUINFO mi = { sizeof(mi) };
523      mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
524      mi.hbrBack = GetDarkModeSurfaceBrush();
525      SetMenuInfo(hMenu, &mi);
526  }
527  
528  //----------------------------------------------------------------------------
529  //
530  // RefreshWindowTheme
531  //
532  // Forces a window and all its children to redraw with current theme
533  //
534  //----------------------------------------------------------------------------
535  void RefreshWindowTheme(HWND hWnd)
536  {
537      if (!hWnd)
538      {
539          return;
540      }
541  
542      // Reapply theme to this window
543      ApplyDarkModeToDialog(hWnd);
544  
545      // Force redraw
546      RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_FRAME);
547  }
548  
549  //----------------------------------------------------------------------------
550  //
551  // CleanupDarkModeResources
552  //
553  //----------------------------------------------------------------------------
554  void CleanupDarkModeResources()
555  {
556      if (g_darkBackgroundBrush)
557      {
558          DeleteObject(g_darkBackgroundBrush);
559          g_darkBackgroundBrush = nullptr;
560      }
561      if (g_darkControlBrush)
562      {
563          DeleteObject(g_darkControlBrush);
564          g_darkControlBrush = nullptr;
565      }
566      if (g_darkSurfaceBrush)
567      {
568          DeleteObject(g_darkSurfaceBrush);
569          g_darkSurfaceBrush = nullptr;
570      }
571  }
572  
573  //----------------------------------------------------------------------------
574  //
575  // InitializeDarkMode
576  //
577  // Public wrapper to initialize dark mode support early in app startup
578  //
579  //----------------------------------------------------------------------------
580  void InitializeDarkMode()
581  {
582      InitializeDarkModeSupport();
583  }
584  
585  //----------------------------------------------------------------------------
586  //
587  // ForceRectInBounds
588  //
589  //----------------------------------------------------------------------------
590  RECT ForceRectInBounds( RECT rect, const RECT& bounds )
591  {
592      if( rect.left < bounds.left )
593      {
594          rect.right += bounds.left - rect.left;
595          rect.left = bounds.left;
596      }
597      if( rect.top < bounds.top )
598      {
599          rect.bottom += bounds.top - rect.top;
600          rect.top = bounds.top;
601      }
602      if( rect.right > bounds.right )
603      {
604          rect.left -= rect.right - bounds.right;
605          rect.right = bounds.right;
606      }
607      if( rect.bottom > bounds.bottom )
608      {
609          rect.top -= rect.bottom - bounds.bottom;
610          rect.bottom = bounds.bottom;
611      }
612      return rect;
613  }
614  
615  //----------------------------------------------------------------------------
616  //
617  // GetDpiForWindow
618  //
619  //----------------------------------------------------------------------------
620  UINT GetDpiForWindowHelper( HWND window )
621  {
622      auto function = reinterpret_cast<UINT (WINAPI *)(HWND)>(GetProcAddress( GetModuleHandleW( L"user32.dll" ), "GetDpiForWindow" ));
623      if( function )
624      {
625          return function( window );
626      }
627  
628      wil::unique_hdc hdc{GetDC( nullptr )};
629      return static_cast<UINT>(GetDeviceCaps( hdc.get(), LOGPIXELSX ));
630  }
631  
632  //----------------------------------------------------------------------------
633  //
634  // GetMonitorRectFromCursor
635  //
636  //----------------------------------------------------------------------------
637  RECT GetMonitorRectFromCursor()
638  {
639      POINT point;
640      GetCursorPos( &point );
641      MONITORINFO monitorInfo{};
642      monitorInfo.cbSize = sizeof( monitorInfo );
643      GetMonitorInfoW( MonitorFromPoint( point, MONITOR_DEFAULTTONEAREST ), &monitorInfo );
644      return monitorInfo.rcMonitor;
645  }
646  
647  //----------------------------------------------------------------------------
648  //
649  // RectFromPointsMinSize
650  //
651  //----------------------------------------------------------------------------
652  #ifdef _MSC_VER
653      // avoid making RectFromPointsMinSize constexpr since that leads to link errors
654      #pragma warning(push)
655      #pragma warning(disable: 26497)
656  #endif
657  
658  RECT RectFromPointsMinSize( POINT a, POINT b, LONG minSize )
659  {
660      RECT rect;
661      if( a.x <= b.x )
662      {
663          rect.left = a.x;
664          rect.right = b.x + 1;
665          if( (rect.right - rect.left) < minSize )
666          {
667              rect.right = rect.left + minSize;
668          }
669      }
670      else
671      {
672          rect.left = b.x;
673          rect.right = a.x + 1;
674          if( (rect.right - rect.left) < minSize )
675          {
676              rect.left = rect.right - minSize;
677          }
678      }
679      if( a.y <= b.y )
680      {
681          rect.top = a.y;
682          rect.bottom = b.y + 1;
683          if( (rect.bottom - rect.top) < minSize )
684          {
685              rect.bottom = rect.top + minSize;
686          }
687      }
688      else
689      {
690          rect.top = b.y;
691          rect.bottom = a.y + 1;
692          if( (rect.bottom - rect.top) < minSize )
693          {
694              rect.top = rect.bottom - minSize;
695          }
696      }
697      return rect;
698  }
699  #ifdef _MSC_VER
700      #pragma warning(pop)
701  #endif
702  //----------------------------------------------------------------------------
703  //
704  // ScaleForDpi
705  //
706  //----------------------------------------------------------------------------
707  int ScaleForDpi( int value, UINT dpi )
708  {
709      return MulDiv( value, static_cast<int>(dpi), USER_DEFAULT_SCREEN_DPI );
710  }
711  
712  //----------------------------------------------------------------------------
713  //
714  // ScalePointInRects
715  //
716  //----------------------------------------------------------------------------
717  POINT ScalePointInRects( POINT point, const RECT& source, const RECT& target )
718  {
719      const SIZE sourceSize{ source.right - source.left, source.bottom - source.top };
720      const POINT sourceCenter{ source.left + sourceSize.cx / 2, source.top + sourceSize.cy / 2 };
721      const SIZE targetSize{ target.right - target.left, target.bottom - target.top };
722      const POINT targetCenter{ target.left + targetSize.cx / 2, target.top + targetSize.cy / 2 };
723  
724      return { targetCenter.x + MulDiv( point.x - sourceCenter.x, targetSize.cx, sourceSize.cx ),
725               targetCenter.y + MulDiv( point.y - sourceCenter.y, targetSize.cy, sourceSize.cy ) };
726  }
727  
728  //----------------------------------------------------------------------------
729  //
730  // ScaleDialogForDpi
731  //
732  // Scales a dialog and all its child controls for the specified DPI.
733  // oldDpi defaults to DPI_BASELINE (96) for initial scaling.
734  //
735  //----------------------------------------------------------------------------
736  void ScaleDialogForDpi( HWND hDlg, UINT newDpi, UINT oldDpi )
737  {
738      if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 )
739      {
740          return;
741      }
742  
743      // With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created.
744      // We only need to scale when moving between monitors with different DPIs.
745      // When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling.
746      if( oldDpi == DPI_BASELINE )
747      {
748          return;
749      }
750  
751      // Scale the dialog window itself
752      RECT dialogRect;
753      GetWindowRect( hDlg, &dialogRect );
754      int dialogWidth = MulDiv( dialogRect.right - dialogRect.left, newDpi, oldDpi );
755      int dialogHeight = MulDiv( dialogRect.bottom - dialogRect.top, newDpi, oldDpi );
756      SetWindowPos( hDlg, nullptr, 0, 0, dialogWidth, dialogHeight, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE );
757  
758      // Enumerate and scale all child controls
759      HWND hChild = GetWindow( hDlg, GW_CHILD );
760      while( hChild != nullptr )
761      {
762          RECT childRect;
763          GetWindowRect( hChild, &childRect );
764          MapWindowPoints( nullptr, hDlg, reinterpret_cast<LPPOINT>(&childRect), 2 );
765  
766          int x = MulDiv( childRect.left, newDpi, oldDpi );
767          int y = MulDiv( childRect.top, newDpi, oldDpi );
768          int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi );
769          int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi );
770  
771          SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE );
772  
773          // Scale the font for the control
774          HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 ));
775          if( hFont != nullptr )
776          {
777              LOGFONT lf{};
778              if( GetObject( hFont, sizeof(lf), &lf ) )
779              {
780                  lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
781                  HFONT hNewFont = CreateFontIndirect( &lf );
782                  if( hNewFont )
783                  {
784                      SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
785                      // Note: The old font might be shared, so we don't delete it here
786                      // The system will clean up fonts when the dialog is destroyed
787                  }
788              }
789          }
790  
791          hChild = GetWindow( hChild, GW_HWNDNEXT );
792      }
793  
794      // Also scale the dialog's own font
795      HFONT hDialogFont = reinterpret_cast<HFONT>(SendMessage( hDlg, WM_GETFONT, 0, 0 ));
796      if( hDialogFont != nullptr )
797      {
798          LOGFONT lf{};
799          if( GetObject( hDialogFont, sizeof(lf), &lf ) )
800          {
801              lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
802              HFONT hNewFont = CreateFontIndirect( &lf );
803              if( hNewFont )
804              {
805                  SendMessage( hDlg, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
806              }
807          }
808      }
809  }
810  
811  //----------------------------------------------------------------------------
812  //
813  // ScaleChildControlsForDpi
814  //
815  // Scales a window's direct child controls (and their fonts) for the specified DPI.
816  // Unlike ScaleDialogForDpi, this does not resize the parent window itself.
817  //
818  // This is useful for child dialogs used as tab pages: the tab page window is
819  // already scaled when the parent options dialog is scaled, but the controls
820  // inside the page are not (because they are grandchildren of the options dialog).
821  //
822  //----------------------------------------------------------------------------
823  void ScaleChildControlsForDpi( HWND hParent, UINT newDpi, UINT oldDpi )
824  {
825      if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 )
826      {
827          return;
828      }
829  
830      // With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created.
831      // We only need to scale when moving between monitors with different DPIs.
832      // When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling.
833      if( oldDpi == DPI_BASELINE )
834      {
835          return;
836      }
837  
838      HWND hChild = GetWindow( hParent, GW_CHILD );
839      while( hChild != nullptr )
840      {
841          RECT childRect;
842          GetWindowRect( hChild, &childRect );
843          MapWindowPoints( nullptr, hParent, reinterpret_cast<LPPOINT>(&childRect), 2 );
844  
845          int x = MulDiv( childRect.left, newDpi, oldDpi );
846          int y = MulDiv( childRect.top, newDpi, oldDpi );
847          int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi );
848          int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi );
849  
850          SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE );
851  
852          // Scale the font for the control
853          HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 ));
854          if( hFont != nullptr )
855          {
856              LOGFONT lf{};
857              if( GetObject( hFont, sizeof(lf), &lf ) )
858              {
859                  lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
860                  HFONT hNewFont = CreateFontIndirect( &lf );
861                  if( hNewFont )
862                  {
863                      SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
864                  }
865              }
866          }
867  
868          hChild = GetWindow( hChild, GW_HWNDNEXT );
869      }
870  }
871  
872  //----------------------------------------------------------------------------
873  //
874  // HandleDialogDpiChange
875  //
876  // Handles WM_DPICHANGED message for dialogs. Call this from the dialog's
877  // WndProc when WM_DPICHANGED is received.
878  //
879  //----------------------------------------------------------------------------
880  void HandleDialogDpiChange( HWND hDlg, WPARAM wParam, LPARAM lParam, UINT& currentDpi )
881  {
882      UINT newDpi = HIWORD( wParam );
883      if( newDpi != currentDpi && newDpi != 0 )
884      {
885          const RECT* pSuggestedRect = reinterpret_cast<const RECT*>(lParam);
886  
887          // Scale the dialog controls from the current DPI to the new DPI
888          ScaleDialogForDpi( hDlg, newDpi, currentDpi );
889  
890          // Move and resize the dialog to the suggested rectangle
891          SetWindowPos( hDlg, nullptr,
892              pSuggestedRect->left,
893              pSuggestedRect->top,
894              pSuggestedRect->right - pSuggestedRect->left,
895              pSuggestedRect->bottom - pSuggestedRect->top,
896              SWP_NOZORDER | SWP_NOACTIVATE );
897  
898          currentDpi = newDpi;
899      }
900  }