From 32830a0527a28862a11364d7ed5a008044700cbc Mon Sep 17 00:00:00 2001 From: Scott Hanselman Date: Tue, 5 May 2026 13:57:33 -0700 Subject: [PATCH] fix: refresh tray menu sizing on DPI changes Size the tray menu against the target cursor monitor DPI instead of the hidden window's stale size, and invalidate cached flyout geometry when DPI or rasterization scale changes. This keeps the first tray menu open after a display-scale change from rendering with stale compressed measurements. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Shared/MenuSizingHelper.cs | 20 +++++ src/OpenClaw.Tray.WinUI/App.xaml.cs | 1 - .../Windows/TrayMenuWindow.xaml.cs | 86 +++++++++++++------ .../MenuSizingHelperTests.cs | 24 ++++++ 4 files changed, 103 insertions(+), 28 deletions(-) diff --git a/src/OpenClaw.Shared/MenuSizingHelper.cs b/src/OpenClaw.Shared/MenuSizingHelper.cs index 72db271..9eb67a8 100644 --- a/src/OpenClaw.Shared/MenuSizingHelper.cs +++ b/src/OpenClaw.Shared/MenuSizingHelper.cs @@ -5,6 +5,8 @@ namespace OpenClaw.Shared; /// public static class MenuSizingHelper { + private const double ScaleTolerance = 0.001; + public static int ConvertPixelsToViewUnits(int pixels, uint dpi) { if (pixels <= 0) return 0; @@ -13,6 +15,19 @@ public static class MenuSizingHelper return Math.Max(1, (int)Math.Floor(pixels * 96.0 / dpi)); } + public static bool HasDpiOrScaleChanged(uint previousDpi, double previousRasterizationScale, uint currentDpi, double currentRasterizationScale) + { + previousDpi = NormalizeDpi(previousDpi); + currentDpi = NormalizeDpi(currentDpi); + + if (previousDpi != currentDpi) + return true; + + var previousScale = NormalizeScale(previousRasterizationScale); + var currentScale = NormalizeScale(currentRasterizationScale); + return Math.Abs(previousScale - currentScale) > ScaleTolerance; + } + public static int CalculateWindowHeight(int contentHeight, int workAreaHeight, int minimumHeight = 100) { if (contentHeight < 0) contentHeight = 0; @@ -25,4 +40,9 @@ public static class MenuSizingHelper var desiredHeight = Math.Max(contentHeight, minimumVisibleHeight); return Math.Min(desiredHeight, workAreaHeight); } + + private static uint NormalizeDpi(uint dpi) => dpi == 0 ? 96u : dpi; + + private static double NormalizeScale(double scale) => + double.IsFinite(scale) && scale > 0 ? scale : 1.0; } diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index faa5db8..0b3968d 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -483,7 +483,6 @@ public partial class App : Application // Rebuild menu content _trayMenuWindow!.ClearItems(); BuildTrayMenuPopup(_trayMenuWindow); - _trayMenuWindow.SizeToContent(); _trayMenuWindow.ShowAtCursor(); } catch (Exception ex) diff --git a/src/OpenClaw.Tray.WinUI/Windows/TrayMenuWindow.xaml.cs b/src/OpenClaw.Tray.WinUI/Windows/TrayMenuWindow.xaml.cs index 434fffb..83a2422 100644 --- a/src/OpenClaw.Tray.WinUI/Windows/TrayMenuWindow.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Windows/TrayMenuWindow.xaml.cs @@ -18,6 +18,8 @@ namespace OpenClawTray.Windows; /// public sealed partial class TrayMenuWindow : WindowEx { + private const int MenuWidthViewUnits = 320; + #region Win32 Imports [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] @@ -111,6 +113,8 @@ public sealed partial class TrayMenuWindow : WindowEx private string? _activeFlyoutKey; private bool _isShown; private global::Windows.Graphics.RectInt32? _lastMoveAndResizeRect; + private uint _lastMeasureDpi; + private double _lastMeasureRasterizationScale; public TrayMenuWindow() : this(ownerMenu: null) { @@ -188,28 +192,11 @@ public sealed partial class TrayMenuWindow : WindowEx var monitorInfo = new MONITORINFO { cbSize = Marshal.SizeOf() }; GetMonitorInfo(hMonitor, ref monitorInfo); var workArea = monitorInfo.rcWork; - - int menuWidthPx; - int menuHeightPx; - try - { - menuWidthPx = this.AppWindow.Size.Width; - menuHeightPx = this.AppWindow.Size.Height; - } - catch - { - menuWidthPx = 0; - menuHeightPx = 0; - } - - if (menuWidthPx <= 0 || menuHeightPx <= 0) - { - var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); - uint dpi = GetEffectiveMonitorDpi(hMonitor, hwnd); - double scale = dpi / 96.0; - menuWidthPx = (int)(320 * scale); - menuHeightPx = (int)(_menuHeight * scale); - } + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + var dpi = GetEffectiveMonitorDpi(hMonitor, hwnd); + SizeToContent(workArea.Bottom - workArea.Top, dpi); + var menuWidthPx = ConvertViewUnitsToPixels(MenuWidthViewUnits, dpi); + var menuHeightPx = ConvertViewUnitsToPixels(_menuHeight, dpi); const int margin = 8; @@ -219,7 +206,16 @@ public sealed partial class TrayMenuWindow : WindowEx workArea.Left, workArea.Top, workArea.Right, workArea.Bottom, margin); - this.Move(x, y); + var targetRect = new global::Windows.Graphics.RectInt32(x, y, menuWidthPx, menuHeightPx); + if (!RectEquals(_lastMoveAndResizeRect, targetRect)) + { + AppWindow.MoveAndResize(targetRect); + _lastMoveAndResizeRect = targetRect; + } + } + else + { + SizeToContent(); } ApplyRoundedWindowRegion(); @@ -251,6 +247,7 @@ public sealed partial class TrayMenuWindow : WindowEx var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); var dpi = GetEffectiveMonitorDpi(hMonitor, hwnd); + SizeToContent(monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top, dpi); var submenuWidthPx = ConvertViewUnitsToPixels(280, dpi); var submenuHeightPx = ConvertViewUnitsToPixels(_menuHeight, dpi); @@ -523,24 +520,60 @@ public sealed partial class TrayMenuWindow : WindowEx /// public void SizeToContent() { + if (TryGetCurrentMonitorMetrics(out var workAreaHeightPx, out var dpi)) + { + SizeToContent(workAreaHeightPx, dpi); + return; + } + + SizeToContent(0, 96); + } + + private void SizeToContent(int workAreaHeightPx, uint dpi) + { + PrepareLayoutForMeasurement(dpi); + // Measure the actual content size instead of estimating - MenuPanel.Measure(new global::Windows.Foundation.Size(320, double.PositiveInfinity)); + MenuPanel.Measure(new global::Windows.Foundation.Size(MenuWidthViewUnits, double.PositiveInfinity)); var desiredHeight = MenuPanel.DesiredSize.Height; // Add border chrome (1px border top+bottom = 2px, plus small rounding buffer) var contentHeight = (int)Math.Ceiling(desiredHeight) + 4; _menuHeight = Math.Max(contentHeight, 100); - if (TryGetCurrentMonitorMetrics(out var workAreaHeightPx, out var dpi)) + if (workAreaHeightPx > 0) { var workAreaHeight = MenuSizingHelper.ConvertPixelsToViewUnits(workAreaHeightPx, dpi); _menuHeight = MenuSizingHelper.CalculateWindowHeight(_menuHeight, workAreaHeight); } - this.SetWindowSize(320, _menuHeight); + this.SetWindowSize(MenuWidthViewUnits, _menuHeight); ApplyRoundedWindowRegion(); } + private void PrepareLayoutForMeasurement(uint dpi) + { + dpi = dpi == 0 ? 96 : dpi; + var rasterizationScale = RootGrid.XamlRoot?.RasterizationScale ?? dpi / 96.0; + var dpiChanged = _lastMeasureDpi != 0 + && MenuSizingHelper.HasDpiOrScaleChanged(_lastMeasureDpi, _lastMeasureRasterizationScale, dpi, rasterizationScale); + + _lastMeasureDpi = dpi; + _lastMeasureRasterizationScale = rasterizationScale; + + if (dpiChanged) + { + _lastMoveAndResizeRect = null; + HideActiveFlyout(); + } + + RootGrid.InvalidateMeasure(); + RootGrid.InvalidateArrange(); + MenuPanel.InvalidateMeasure(); + MenuPanel.InvalidateArrange(); + RootGrid.UpdateLayout(); + } + private bool TryGetCurrentMonitorMetrics(out int workAreaHeight, out uint dpi) { workAreaHeight = 0; @@ -685,7 +718,6 @@ public sealed partial class TrayMenuWindow : WindowEx } } - flyoutWindow.SizeToContent(); _activeFlyoutOwner = ownerButton; _activeFlyoutKey = flyoutKey; } diff --git a/tests/OpenClaw.Tray.Tests/MenuSizingHelperTests.cs b/tests/OpenClaw.Tray.Tests/MenuSizingHelperTests.cs index f4e395d..e8d1edb 100644 --- a/tests/OpenClaw.Tray.Tests/MenuSizingHelperTests.cs +++ b/tests/OpenClaw.Tray.Tests/MenuSizingHelperTests.cs @@ -64,6 +64,30 @@ public class MenuSizingHelperTests Assert.Equal(800, MenuSizingHelper.ConvertPixelsToViewUnits(1000, 120)); } + [Fact] + public void HasDpiOrScaleChanged_SameDpiAndScale_ReturnsFalse() + { + Assert.False(MenuSizingHelper.HasDpiOrScaleChanged(120, 1.25, 120, 1.25)); + } + + [Fact] + public void HasDpiOrScaleChanged_DifferentDpi_ReturnsTrue() + { + Assert.True(MenuSizingHelper.HasDpiOrScaleChanged(96, 1.0, 120, 1.25)); + } + + [Fact] + public void HasDpiOrScaleChanged_DifferentRasterizationScale_ReturnsTrue() + { + Assert.True(MenuSizingHelper.HasDpiOrScaleChanged(120, 1.0, 120, 1.25)); + } + + [Fact] + public void HasDpiOrScaleChanged_NormalizesInvalidInputs() + { + Assert.False(MenuSizingHelper.HasDpiOrScaleChanged(0, double.NaN, 96, 1.0)); + } + // ── CalculateWindowHeight ─────────────────────────────────────── [Fact]