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]