diff --git a/docs/docs/customize/styling.md b/docs/docs/customize/styling.md index feb7d2899..4bcc2beea 100644 --- a/docs/docs/customize/styling.md +++ b/docs/docs/customize/styling.md @@ -64,7 +64,7 @@ The `bar:height` key is a special key that is used to communicate to Whim the de Notes: - The corresponding style must not contain any property other than `Height`. -- This setting is overwritten if `Height` is explicitly set in . +- This setting is overwritten if `Height` is explicitly set in - this may be required in some monitor configurations - tracked in [#887](https://github.com/dalyIsaac/Whim/issues/887) - The actual height of the bar may differ from the specified one due to overflowing elements. > [!NOTE] diff --git a/src/Whim.Bar/BarPlugin.cs b/src/Whim.Bar/BarPlugin.cs index fea9bd7f3..a58a731b4 100644 --- a/src/Whim.Bar/BarPlugin.cs +++ b/src/Whim.Bar/BarPlugin.cs @@ -79,7 +79,7 @@ private void ShowAll() foreach (BarWindow barWindow in _monitorBarMap.Values) { barWindow.UpdateRect(); - deferPosHandle.DeferWindowPos(barWindow.WindowState); + deferPosHandle.DeferWindowPos(barWindow.WindowState, forceTwoPasses: true); _context.NativeManager.SetWindowCorners( barWindow.WindowState.Window.Handle, DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DONOTROUND diff --git a/src/Whim.Tests/Native/DeferWindowPosHandleTests.cs b/src/Whim.Tests/Native/DeferWindowPosHandleTests.cs index 988868a18..fbe871432 100644 --- a/src/Whim.Tests/Native/DeferWindowPosHandleTests.cs +++ b/src/Whim.Tests/Native/DeferWindowPosHandleTests.cs @@ -170,6 +170,43 @@ WindowPosState windowPosState2 AssertSetWindowPos(internalCtx, windowPosState2, expectedCallCount: numPasses); } + [Theory, AutoSubstituteData] + internal void Dispose_ForceTwoPasses( + IContext ctx, + IInternalContext internalCtx, + WindowPosState windowPosState1, + WindowPosState windowPosState2 + ) + { + // Given multiple windows, and a monitor which has a scale factor == 100 + int monitorScaleFactor = 100; + int numPasses = 2; + + using DeferWindowPosHandle handle = new(ctx, internalCtx); + internalCtx.DeferWindowPosManager.CanDoLayout().Returns(true); + + IMonitor monitor1 = Substitute.For(); + monitor1.ScaleFactor.Returns(100); + IMonitor monitor2 = Substitute.For(); + monitor2.ScaleFactor.Returns(monitorScaleFactor); + + ctx.MonitorManager.GetEnumerator().Returns((_) => new List() { monitor1, monitor2 }.GetEnumerator()); + + // When disposing + handle.DeferWindowPos(windowPosState1.WindowState, windowPosState1.HwndInsertAfter, windowPosState1.Flags); + handle.DeferWindowPos( + windowPosState2.WindowState, + windowPosState2.HwndInsertAfter, + windowPosState2.Flags, + forceTwoPasses: true + ); + handle.Dispose(); + + // Then the windows are laid out twice + AssertSetWindowPos(internalCtx, windowPosState1, expectedCallCount: numPasses); + AssertSetWindowPos(internalCtx, windowPosState2, expectedCallCount: numPasses); + } + [Theory, AutoSubstituteData] internal void Dispose_NoWindowOffset(IContext ctx, IInternalContext internalCtx, WindowPosState windowPosState) { diff --git a/src/Whim/Native/DeferWindowPosHandle.cs b/src/Whim/Native/DeferWindowPosHandle.cs index 402963102..295b12f2f 100644 --- a/src/Whim/Native/DeferWindowPosHandle.cs +++ b/src/Whim/Native/DeferWindowPosHandle.cs @@ -23,6 +23,8 @@ public sealed class DeferWindowPosHandle : IDisposable private readonly List _windowStates = new(); private readonly List _minimizedWindowStates = new(); + private bool forceTwoPasses; + /// /// The default flags to use when setting the window position. /// @@ -80,13 +82,25 @@ IEnumerable windowStates /// The flags to use when setting the window position. This overrides the default flags Whim sets, /// except when the window is maximized or minimized. /// + /// + /// Window scaling can be finicky. Whim will set a window's position twice for windows in monitors + /// with non-100% scaling. Regardless of this, some windows need this even for windows with + /// 100% scaling. + /// public void DeferWindowPos( IWindowState windowState, HWND? hwndInsertAfter = null, - SET_WINDOW_POS_FLAGS? flags = null + SET_WINDOW_POS_FLAGS? flags = null, + bool forceTwoPasses = false ) { Logger.Debug($"Adding window {windowState.Window} after {hwndInsertAfter} with flags {flags}"); + + if (forceTwoPasses) + { + this.forceTwoPasses = true; + } + // We use HWND_BOTTOM, as modifying the Z-order of a window // may cause EVENT_SYSTEM_FOREGROUND to be set, which in turn // causes the relevant window to be focused, when the user hasn't @@ -136,7 +150,7 @@ public void Dispose() int numPasses = 1; foreach (IMonitor monitor in _context.MonitorManager) { - if (monitor.ScaleFactor != 100) + if (monitor.ScaleFactor != 100 || forceTwoPasses) { numPasses = 2; break;