HDT: Wine overlay plugin, no_focus on overlay
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dae90efcf9
commit
bc74760172
3 changed files with 200 additions and 2 deletions
22
apps/hdt-wine-overlay/HdtWineOverlay.csproj
Normal file
22
apps/hdt-wine-overlay/HdtWineOverlay.csproj
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<!-- Build: HDT_EXE=<path to app-x.y.z/HearthstoneDeckTracker.exe> dotnet build -c Release
|
||||||
|
Output bin/Release/net472/HdtWineOverlay.dll goes in %AppData%/HearthstoneDeckTracker/Plugins. -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net472</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<AssemblyName>HdtWineOverlay</AssemblyName>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
|
||||||
|
<Reference Include="PresentationCore" />
|
||||||
|
<Reference Include="PresentationFramework" />
|
||||||
|
<Reference Include="WindowsBase" />
|
||||||
|
<Reference Include="System.Xaml" />
|
||||||
|
<Reference Include="HearthstoneDeckTracker">
|
||||||
|
<HintPath>$(HDT_EXE)</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
171
apps/hdt-wine-overlay/WineOverlayPlugin.cs
Normal file
171
apps/hdt-wine-overlay/WineOverlayPlugin.cs
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
// HDT plugin: makes the in-game overlay work under Wine/Proton on Hyprland.
|
||||||
|
//
|
||||||
|
// Wine's X11 driver decides per window whether it is "managed" (handed to the
|
||||||
|
// compositor, which tiles/floats/focuses it) or override-redirect (placed where
|
||||||
|
// the app says, stacked on top, left alone). A popup becomes managed for good
|
||||||
|
// once it is the active window during a show or SetWindowPos. WPF's Show()
|
||||||
|
// activates by default, so HDT's overlay ends up managed: Hyprland places it
|
||||||
|
// wherever it likes instead of over the game.
|
||||||
|
//
|
||||||
|
// Separately, Wine cuts every alpha-0 pixel of a layered window out of the X11
|
||||||
|
// shape, and XWayland shows those pixels as opaque black. Painting the window
|
||||||
|
// background at alpha 1/255 keeps every pixel inside the shape while staying
|
||||||
|
// invisible.
|
||||||
|
//
|
||||||
|
// Both 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.
|
||||||
|
//
|
||||||
|
// Approach from alexzfe/hdt-omarchy (Utility/Wine.cs), done as a plugin so the
|
||||||
|
// official HDT build and its updater stay in place.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Hearthstone_Deck_Tracker;
|
||||||
|
using Hearthstone_Deck_Tracker.Plugins;
|
||||||
|
using Hearthstone_Deck_Tracker.Utility.Logging;
|
||||||
|
|
||||||
|
namespace HdtWineOverlay
|
||||||
|
{
|
||||||
|
public class WineOverlayPlugin : IPlugin
|
||||||
|
{
|
||||||
|
public string Name => "Wine Overlay Fix";
|
||||||
|
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 MenuItem MenuItem => null;
|
||||||
|
|
||||||
|
private HwndSource _hookedSource;
|
||||||
|
private RECT _lastGameRect;
|
||||||
|
private DateTime _nextPoll = DateTime.MinValue;
|
||||||
|
|
||||||
|
public void OnLoad()
|
||||||
|
{
|
||||||
|
if(!IsWine())
|
||||||
|
{
|
||||||
|
Log.Info("Not running under Wine, nothing to do");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var overlay = Core.Overlay;
|
||||||
|
var handle = new WindowInteropHelper(overlay).Handle;
|
||||||
|
if(handle != IntPtr.Zero && overlay.IsVisible)
|
||||||
|
Log.Warn("Overlay was already shown before this plugin loaded; restart HDT for the fix to apply");
|
||||||
|
|
||||||
|
// Never activate on show: keeps Wine from making the overlay a managed window.
|
||||||
|
overlay.ShowActivated = false;
|
||||||
|
|
||||||
|
if(!(overlay.Background is SolidColorBrush { Color.A: > 0 }))
|
||||||
|
{
|
||||||
|
var brush = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
|
||||||
|
brush.Freeze();
|
||||||
|
overlay.Background = brush;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(handle != IntPtr.Zero)
|
||||||
|
HookMouseActivate(handle);
|
||||||
|
else
|
||||||
|
overlay.SourceInitialized += OnOverlaySourceInitialized;
|
||||||
|
|
||||||
|
Log.Info("Applied Wine overlay workarounds");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnUnload()
|
||||||
|
{
|
||||||
|
Core.Overlay.SourceInitialized -= OnOverlaySourceInitialized;
|
||||||
|
_hookedSource?.RemoveHook(WndProc);
|
||||||
|
_hookedSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnButtonPress()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by HDT every 100ms on the UI thread.
|
||||||
|
public void OnUpdate()
|
||||||
|
{
|
||||||
|
if(_hookedSource == null || DateTime.UtcNow < _nextPoll)
|
||||||
|
return;
|
||||||
|
_nextPoll = DateTime.UtcNow.AddMilliseconds(250);
|
||||||
|
|
||||||
|
// HDT follows the game window through an out-of-context WinEvent hook
|
||||||
|
// (EVENT_OBJECT_LOCATIONCHANGE), which Wine doesn't deliver for
|
||||||
|
// compositor-driven moves. Poll the game's client rect instead.
|
||||||
|
var overlay = Core.Overlay;
|
||||||
|
if(!overlay.IsContentVisible)
|
||||||
|
return;
|
||||||
|
var game = FindWindow("UnityWndClass", "Hearthstone");
|
||||||
|
if(game == IntPtr.Zero || !GetClientRect(game, out var rect))
|
||||||
|
return;
|
||||||
|
var origin = new POINT();
|
||||||
|
if(!ClientToScreen(game, ref origin))
|
||||||
|
return;
|
||||||
|
rect.Left += origin.X;
|
||||||
|
rect.Right += origin.X;
|
||||||
|
rect.Top += origin.Y;
|
||||||
|
rect.Bottom += origin.Y;
|
||||||
|
if(rect.Equals(_lastGameRect))
|
||||||
|
return;
|
||||||
|
_lastGameRect = rect;
|
||||||
|
overlay.UpdatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnOverlaySourceInitialized(object sender, EventArgs e)
|
||||||
|
=> HookMouseActivate(new WindowInteropHelper(Core.Overlay).Handle);
|
||||||
|
|
||||||
|
private void HookMouseActivate(IntPtr handle)
|
||||||
|
{
|
||||||
|
_hookedSource = HwndSource.FromHwnd(handle);
|
||||||
|
_hookedSource?.AddHook(WndProc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking an overlay button would otherwise activate the window, which
|
||||||
|
// makes Wine turn it into a managed window on the next position update.
|
||||||
|
private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||||
|
{
|
||||||
|
const int wmMouseActivate = 0x0021;
|
||||||
|
const int maNoActivate = 3;
|
||||||
|
if(msg != wmMouseActivate)
|
||||||
|
return IntPtr.Zero;
|
||||||
|
handled = true;
|
||||||
|
return new IntPtr(maNoActivate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWine()
|
||||||
|
{
|
||||||
|
var ntdll = GetModuleHandle("ntdll.dll");
|
||||||
|
return ntdll != IntPtr.Zero && GetProcAddress(ntdll, "wine_get_version") != IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct RECT
|
||||||
|
{
|
||||||
|
public int Left, Top, Right, Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct POINT
|
||||||
|
{
|
||||||
|
public int X, Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
|
||||||
|
private static extern IntPtr GetModuleHandle(string lpModuleName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
|
||||||
|
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -380,12 +380,17 @@ in
|
||||||
|
|
||||||
-- Hearthstone Deck Tracker's overlay: a transparent WPF window HDT
|
-- Hearthstone Deck Tracker's overlay: a transparent WPF window HDT
|
||||||
-- itself keeps moved and sized over Hearthstone (title from its
|
-- itself keeps moved and sized over Hearthstone (title from its
|
||||||
-- OverlayWindow XAML). The generic tile rule above would tile it
|
-- OverlayWindow XAML). The Wine plugin in apps/hdt-wine-overlay
|
||||||
-- like a game; float it bare and never hand it focus.
|
-- keeps it override-redirect; this is the safety net if Wine ever
|
||||||
|
-- hands it over as a managed window, which the tile rule above
|
||||||
|
-- would tile. no_focus matters either way: Hyprland hit-tests the
|
||||||
|
-- window box, not the X11 input shape, so focus-follows-mouse
|
||||||
|
-- handed focus to the overlay whenever the cursor was over the game.
|
||||||
hl.window_rule({
|
hl.window_rule({
|
||||||
name = "hdt-overlay",
|
name = "hdt-overlay",
|
||||||
match = { class = "^steam_app_(0|default)$", title = "^HearthstoneOverlay$" },
|
match = { class = "^steam_app_(0|default)$", title = "^HearthstoneOverlay$" },
|
||||||
float = true,
|
float = true,
|
||||||
|
no_focus = true,
|
||||||
no_initial_focus = true,
|
no_initial_focus = true,
|
||||||
decorate = false,
|
decorate = false,
|
||||||
border_size = 0,
|
border_size = 0,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue