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>
This commit is contained in:
parent
e75d9bc1d9
commit
32830a0527
@ -5,6 +5,8 @@ namespace OpenClaw.Shared;
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
@ -483,7 +483,6 @@ public partial class App : Application
|
||||
// Rebuild menu content
|
||||
_trayMenuWindow!.ClearItems();
|
||||
BuildTrayMenuPopup(_trayMenuWindow);
|
||||
_trayMenuWindow.SizeToContent();
|
||||
_trayMenuWindow.ShowAtCursor();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -18,6 +18,8 @@ namespace OpenClawTray.Windows;
|
||||
/// </summary>
|
||||
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<MONITORINFO>() };
|
||||
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
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user