diff --git a/src/LanguageBar/LanguageBar.cpp b/src/LanguageBar/LanguageBar.cpp index 110e477a..d7d3933e 100644 --- a/src/LanguageBar/LanguageBar.cpp +++ b/src/LanguageBar/LanguageBar.cpp @@ -15,6 +15,8 @@ #include "Ipc.h" #include "fmt/xchar.h" +#include + namespace { // Taskbar/system tray follows SystemUsesLightTheme (0 = dark). @@ -55,6 +57,59 @@ DWORD ResolveThemeIconIndex(DWORD lightIconIndex) } return lightIconIndex; } + +// Design size of the mode icons for language bar / taskbar indicators (at 96 DPI). +constexpr int kSmallIconDesignSize = 16; + +// Avoids relying on USER_DEFAULT_SCREEN_DPI, which older Windows SDK headers +// targeted by this project may not provide. +constexpr int kUserDefaultScreenDpi = 96; + +using GetDpiForMonitorFn = HRESULT(WINAPI *)(HMONITOR, int /*MONITOR_DPI_TYPE*/, UINT *, UINT *); +using SetThreadDpiAwarenessCtxFn = HANDLE(WINAPI *)(HANDLE); + +// Entry points are resolved lazily and cached in these atomics instead of +// function-local statics: the project compiles with /Zc:threadSafeInit-, under +// which magic statics are NOT thread-safe, while GetIcon can be reached from +// several TSF threads of the host process. Failed lookups are not cached - they +// are cheap enough to retry on the next call. +std::atomic s_getDpiForMonitor{nullptr}; +std::atomic s_setThreadDpiAwarenessCtx{nullptr}; + +template +T ResolveOnce(std::atomic &slot, T (*resolver)()) +{ + T resolved = slot.load(std::memory_order_acquire); + if (resolved == nullptr) + { + resolved = resolver(); + if (resolved != nullptr) + { + slot.store(resolved, std::memory_order_release); + } + } + return resolved; +} + +GetDpiForMonitorFn ResolveGetDpiForMonitor() +{ + // shcore.dll ships with Windows 8.1+; most hosts already have it mapped. + HMODULE shcore = ::GetModuleHandleW(L"shcore.dll"); + if (!shcore) + { + shcore = ::LoadLibraryW(L"shcore.dll"); // system DLL; intentionally kept until process exit + } + return shcore ? reinterpret_cast(::GetProcAddress(shcore, "GetDpiForMonitor")) : nullptr; +} + +SetThreadDpiAwarenessCtxFn ResolveSetThreadDpiAwarenessContext() +{ + // user32.dll is always mapped inside any host of this DLL. + HMODULE user32 = ::GetModuleHandleW(L"user32.dll"); + return user32 ? reinterpret_cast( + ::GetProcAddress(user32, "SetThreadDpiAwarenessContext")) // Win10 1607+ + : nullptr; +} } // namespace //+--------------------------------------------------------------------------- @@ -524,45 +579,58 @@ STDAPI CLangBarItemButton::GetIcon(_Out_ HICON *phIcon) DWORD status = 0; GetStatus(&status); - // GetIcon is invoked on the *focused application's* TSF thread, and - // GetSystemMetrics(SM_CXSMICON) follows the process's *system* DPI, which - // Windows keeps lagging behind the per-monitor scale until sign-out and - // caches per process start (e.g. 20px at registry 120% even while the - // monitor really runs at 175% and the taskbar slot is 28px). The shell - // draws the returned HICON scaled into that slot, so a 20px icon gets - // upscaled to 28px and looks blurry (desktop focus only appeared sharp - // while explorer happened to have started at 175% and thus asked 28px). - // Query the primary monitor's *effective* DPI (awareness-independent) and - // size the icon to the real physical small-icon slot. - int desiredSize = 16; - HMONITOR mon = MonitorFromPoint({0, 0}, MONITOR_DEFAULTTOPRIMARY); - UINT dpiX = 0, dpiY = 0; - using GetDpiForMonitorFn = HRESULT(WINAPI *)(HMONITOR, int, UINT *, UINT *); - // shcore.dll ships with Windows 8.1+ and is usually already loaded by the - // host; resolve it on demand so processes that never touched DPI APIs - // still get the real slot size instead of dropping to SM_CXSMICON. - static const auto fnGetDpiForMonitor = - []() -> GetDpiForMonitorFn + // GetIcon runs on the *focused application's* TSF thread, so sizing must + // not depend on that process's DPI awareness. The taskbar lays the input + // indicator out in physical pixels of its monitor (e.g. a 28px slot at + // 175%), while GetSystemMetrics(SM_CXSMICON) reports the system-DPI + // snapshot cached when the host started (e.g. 20px). The shell then has to + // upscale our returned icon into the larger slot, which is the blur of + // issue #50 (focus-dependent only because explorer happens to start at the + // correct scale). + // + // Query the primary monitor's effective DPI instead. Per MS docs, + // GetDpiForMonitor scales its answer to the caller's DPI awareness + // (PROCESS_DPI_UNAWARE -> 96, PROCESS_SYSTEM_DPI_AWARE -> system DPI, + // PROCESS_PER_MONITOR_DPI_AWARE -> the real value), so temporarily flip + // this thread to PER_MONITOR_AWARE_V2 around the query; otherwise unaware + // or system-aware hosts would still end up with the wrong size. Hosts + // lacking these APIs fall back to the legacy SM_CXSMICON path unchanged. + int desiredSize = kSmallIconDesignSize; + bool sizedByMonitorDpi = false; + if (HMONITOR mon = ::MonitorFromPoint({0, 0}, MONITOR_DEFAULTTOPRIMARY)) { - HMODULE shcore = GetModuleHandleW(L"shcore.dll"); - if (!shcore) + SetThreadDpiAwarenessCtxFn setCtx = + ResolveOnce(s_setThreadDpiAwarenessCtx, &ResolveSetThreadDpiAwarenessContext); + HANDLE prevCtx = nullptr; + if (setCtx) { - shcore = LoadLibraryW(L"shcore.dll"); // system DLL; freed at process exit + // (HANDLE)(INT_PTR)-4 == DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2; + // spelled out via casts to avoid requiring WINVER >= Win10 targets. + prevCtx = setCtx(reinterpret_cast(static_cast(-4))); + } + + UINT dpiX = 0; + UINT dpiY = 0; + GetDpiForMonitorFn getDpi = ResolveOnce(s_getDpiForMonitor, &ResolveGetDpiForMonitor); + if (getDpi && SUCCEEDED(getDpi(mon, 0 /* MDT_EFFECTIVE_DPI */, &dpiX, &dpiY)) && dpiX > 0) + { + desiredSize = MulDiv(kSmallIconDesignSize, static_cast(dpiX), kUserDefaultScreenDpi); + sizedByMonitorDpi = true; + } + + if (prevCtx) + { + setCtx(prevCtx); // restore the caller's thread awareness } - return shcore ? reinterpret_cast(GetProcAddress(shcore, "GetDpiForMonitor")) : nullptr; - }(); - if (mon && fnGetDpiForMonitor && - SUCCEEDED(fnGetDpiForMonitor(mon, 0 /* MDT_EFFECTIVE_DPI */, &dpiX, &dpiY)) && dpiX > 0) - { - desiredSize = MulDiv(16, static_cast(dpiX), 96); } - else + if (!sizedByMonitorDpi) { + // No shcore/awareness-switch available: exactly the legacy behavior. desiredSize = GetSystemMetrics(SM_CXSMICON); } if (desiredSize <= 0) { - desiredSize = 16; + desiredSize = kSmallIconDesignSize; } // UAC/secure desktop historically expects at least 24x24. if (_isSecureMode && desiredSize < 24)