From 4d559ad7ebdd39fb47034eed74951ca665901a77 Mon Sep 17 00:00:00 2001 From: rope Date: Thu, 8 Oct 2026 10:58:46 +0100 Subject: [PATCH] HDT plugin: keep overlay raised in X so buttons get clicks Co-Authored-By: Claude Opus 5.5 --- apps/hdt-wine-overlay/WineOverlayPlugin.cs | 63 ++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/apps/hdt-wine-overlay/WineOverlayPlugin.cs b/apps/hdt-wine-overlay/WineOverlayPlugin.cs index 9d0693a..e543904 100644 --- a/apps/hdt-wine-overlay/WineOverlayPlugin.cs +++ b/apps/hdt-wine-overlay/WineOverlayPlugin.cs @@ -17,6 +17,9 @@ // and guide tooltips came up as large black squares). Those get a 1/255 clear // color on their render target as soon as they appear. // +// Clicks: XWayland routes them by X stacking order, which Hyprland changes +// behind Wine's back; the poll in OnUpdate keeps the overlay raised in X. +// // Both overlay fixes have to land before the overlay is first shown. HDT loads enabled // plugins (plugins.xml) before it starts its overlay update loop, so OnLoad is // early enough; enabling the plugin mid-session needs an HDT restart. @@ -46,7 +49,7 @@ namespace HdtWineOverlay public string Description => "Transparent, correctly placed overlay under Wine/Proton (Wayland). Restart HDT after enabling."; public string ButtonText => ""; public string Author => "fred"; - public Version Version => new Version(0, 1, 0); + public Version Version => new Version(0, 2, 0); public MenuItem MenuItem => null; private static readonly Color AlmostTransparent = Color.FromArgb(1, 0, 0, 0); @@ -56,6 +59,14 @@ namespace HdtWineOverlay private HwndSource _hookedSource; private RECT _lastGameRect; private DateTime _nextPoll = DateTime.MinValue; + private int _handBacks; + + private static readonly IntPtr HwndTopmost = new IntPtr(-1); + private static readonly IntPtr HwndNoTopmost = new IntPtr(-2); + private const uint SwpNoSize = 0x0001; + private const uint SwpNoMove = 0x0002; + private const uint SwpNoActivate = 0x0010; + private const uint SwpNoOwnerZOrder = 0x0200; public void OnLoad() { @@ -141,10 +152,42 @@ namespace HdtWineOverlay rect.Right += origin.X; rect.Top += origin.Y; rect.Bottom += origin.Y; - if(rect.Equals(_lastGameRect)) + if(!rect.Equals(_lastGameRect)) + { + _lastGameRect = rect; + overlay.UpdatePosition(); + } + + var hwnd = _hookedSource.Handle; + if(GetActiveWindow() == hwnd) + { + // A SetWindowPos now would make Wine hand the overlay to the + // compositor for good. Give activation back to the game instead, + // a bounded number of times so this can't become a focus fight. + if(_handBacks < 5) + { + _handBacks++; + Log.Warn("Overlay is the active window, handing focus back to Hearthstone"); + SetForegroundWindow(game); + } return; - _lastGameRect = rect; - overlay.UpdatePosition(); + } + _handBacks = 0; + + // XWayland gives a click to the topmost X window under the pointer, in + // X's stacking order, not the order things are drawn in. Hyprland + // restacks the game above the overlay in X whenever it focuses it, so + // the overlay still shows on top but its buttons' clicks land in the + // game. Re-raise it while the game has focus. Going non-topmost first + // makes the second call a real z-order change Wine passes on to X + // (a TOPMOST call on an already topmost window is a no-op). The game's + // own clicks are unaffected: the overlay's input shape (HDT's + // WS_EX_TRANSPARENT toggling) decides those, not the stacking. + if(GetForegroundWindow() != game) + return; + const uint flags = SwpNoSize | SwpNoMove | SwpNoActivate | SwpNoOwnerZOrder; + SetWindowPos(hwnd, HwndNoTopmost, 0, 0, 0, 0, flags); + SetWindowPos(hwnd, HwndTopmost, 0, 0, 0, 0, flags); } private void OnToolTipOpened(object sender, RoutedEventArgs e) @@ -272,6 +315,18 @@ namespace HdtWineOverlay [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + [DllImport("user32.dll")] + private static extern IntPtr GetActiveWindow(); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); + [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);