From 3ca5117875baea21982cc7db883ed2e5fffb3f1a Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Sun, 8 Feb 2026 06:44:33 -0800 Subject: [PATCH] Make application DPI aware (Closes #1369) (#1381) Co-authored-by: Alexandru Macocian --- .../Options/ScreenManagerOptions.cs | 2 + .../Services/Screens/ScreenManager.cs | 82 ++++++++++++++++++- Daybreak.Windows/Utils/NativeMethods.cs | 16 ++++ 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/Daybreak.Core/Configuration/Options/ScreenManagerOptions.cs b/Daybreak.Core/Configuration/Options/ScreenManagerOptions.cs index c417b9db..1c9c4589 100644 --- a/Daybreak.Core/Configuration/Options/ScreenManagerOptions.cs +++ b/Daybreak.Core/Configuration/Options/ScreenManagerOptions.cs @@ -11,4 +11,6 @@ public sealed class ScreenManagerOptions public double Y { get; set; } public double Width { get; set; } public double Height { get; set; } + public int DpiX { get; set; } = 96; + public int DpiY { get; set; } = 96; } diff --git a/Daybreak.Windows/Services/Screens/ScreenManager.cs b/Daybreak.Windows/Services/Screens/ScreenManager.cs index 4d6ba292..f739ab98 100644 --- a/Daybreak.Windows/Services/Screens/ScreenManager.cs +++ b/Daybreak.Windows/Services/Screens/ScreenManager.cs @@ -24,6 +24,8 @@ internal sealed class ScreenManager( ILogger logger ) : IScreenManager, IHostedService { + private const int DefaultDpi = 96; + private readonly PhotinoWindow photinoWindow = photinoWindow.ThrowIfNull(); private readonly IOptionsProvider optionsProvider = optionsProvider.ThrowIfNull(); private readonly IOptionsMonitor liveUpdateableOptions = @@ -70,11 +72,15 @@ internal sealed class ScreenManager( this.photinoWindow.Height ); + var currentDpi = GetDpiForPosition(position.Left, position.Top); + var options = this.liveUpdateableOptions.CurrentValue; options.X = position.Left; options.Y = position.Top; options.Width = position.Width; options.Height = position.Height; + options.DpiX = currentDpi.X; + options.DpiY = currentDpi.Y; this.optionsProvider.SaveOption(options); } @@ -85,6 +91,8 @@ internal sealed class ScreenManager( options.Y = 0; options.Width = 0; options.Height = 0; + options.DpiX = DefaultDpi; + options.DpiY = DefaultDpi; this.optionsProvider.SaveOption(options); } @@ -112,11 +120,12 @@ internal sealed class ScreenManager( public Rectangle GetSavedPosition() { + var savedOptions = this.liveUpdateableOptions.CurrentValue; var savedPosition = new Rectangle( - (int)this.liveUpdateableOptions.CurrentValue.X, - (int)this.liveUpdateableOptions.CurrentValue.Y, - (int)this.liveUpdateableOptions.CurrentValue.Width, - (int)this.liveUpdateableOptions.CurrentValue.Height + (int)savedOptions.X, + (int)savedOptions.Y, + (int)savedOptions.Width, + (int)savedOptions.Height ); if ( @@ -139,9 +148,74 @@ internal sealed class ScreenManager( ); } + // Adjust position based on DPI scaling changes + var savedDpiX = savedOptions.DpiX > 0 ? savedOptions.DpiX : DefaultDpi; + var savedDpiY = savedOptions.DpiY > 0 ? savedOptions.DpiY : DefaultDpi; + var currentDpi = GetDpiForPosition(savedPosition.Left, savedPosition.Top); + + if (savedDpiX != currentDpi.X || savedDpiY != currentDpi.Y) + { + var scaleX = (double)currentDpi.X / savedDpiX; + var scaleY = (double)currentDpi.Y / savedDpiY; + savedPosition = new Rectangle( + (int)(savedPosition.X * scaleX), + (int)(savedPosition.Y * scaleY), + (int)(savedPosition.Width * scaleX), + (int)(savedPosition.Height * scaleY) + ); + } + + // Validate that the position is within visible screen bounds + savedPosition = this.EnsurePositionIsOnScreen(savedPosition); + return savedPosition; } + private Rectangle EnsurePositionIsOnScreen(Rectangle position) + { + var screens = this.Screens.ToList(); + if (screens.Count == 0) + { + return position; + } + + // Check if the window center is within any screen + var centerX = position.Left + (position.Width / 2); + var centerY = position.Top + (position.Height / 2); + + var isOnScreen = screens.Any(s => + centerX >= s.Size.Left && centerX <= s.Size.Right && + centerY >= s.Size.Top && centerY <= s.Size.Bottom); + + if (isOnScreen) + { + return position; + } + + // Window is off-screen, move to the first available screen + var firstScreen = screens.First(); + return new Rectangle( + firstScreen.Size.X + (firstScreen.Size.Width / 4), + firstScreen.Size.Y + (firstScreen.Size.Height / 4), + Math.Min(position.Width, firstScreen.Size.Width / 2), + Math.Min(position.Height, firstScreen.Size.Height / 2) + ); + } + + private static Point GetDpiForPosition(int x, int y) + { + var point = new NativeMethods.POINT { X = x, Y = y }; + var monitor = NativeMethods.MonitorFromPoint(point, NativeMethods.MONITOR_DEFAULTTONEAREST); + + if (monitor != IntPtr.Zero && + NativeMethods.GetDpiForMonitor(monitor, NativeMethods.MonitorDpiType.MDT_EFFECTIVE_DPI, out var dpiX, out var dpiY) == 0) + { + return new Point(dpiX, dpiY); + } + + return new Point(DefaultDpi, DefaultDpi); + } + private static IntPtr? GetMainWindowHandle() { var process = Process.GetProcessesByName("gw").FirstOrDefault(); diff --git a/Daybreak.Windows/Utils/NativeMethods.cs b/Daybreak.Windows/Utils/NativeMethods.cs index ef2efd95..c49489df 100644 --- a/Daybreak.Windows/Utils/NativeMethods.cs +++ b/Daybreak.Windows/Utils/NativeMethods.cs @@ -87,6 +87,14 @@ public static class NativeMethods DWMWCP_ROUNDSMALL = 3 } + public enum MonitorDpiType + { + MDT_EFFECTIVE_DPI = 0, + MDT_ANGULAR_DPI = 1, + MDT_RAW_DPI = 2, + MDT_DEFAULT = MDT_EFFECTIVE_DPI + } + [Flags] public enum ThreadAccess : uint { @@ -703,4 +711,12 @@ public static class NativeMethods [DllImport("user32.dll")] public static extern bool GetMonitorInfo(nint hMonitor, ref MONITORINFO lpmi); + [DllImport("shcore.dll")] + public static extern int GetDpiForMonitor(IntPtr hMonitor, MonitorDpiType dpiType, out int dpiX, out int dpiY); + + [DllImport("user32.dll")] + public static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags); + + + }