https://www.reddit.com/r/Windhawk/comments/1wm3dfy/idea_for_mod_independent_corner_radius/
| // ==WindhawkMod== | |
| // @id custom-corner-radius-per-corner-experiment | |
| // @name Custom Window Corner Radius (per-corner experiment) | |
| // @description Customizes window corner radius in Windows 11, making corners more or less rounded | |
| // @version 1.3 | |
| // @author m417z | |
| // @github https://github.com/m417z | |
| // @twitter https://twitter.com/m417z | |
| // @homepage https://m417z.com/ | |
| // @include dwm.exe | |
| // @architecture x86-64 | |
| // @compilerOptions -lgdi32 -lole32 -lwevtapi -ld2d1 -ld3d11 | |
| // ==/WindhawkMod== | |
| // Source code is published under The GNU General Public License v3.0. | |
| // | |
| // For bug reports and feature requests, please open an issue here: | |
| // https://github.com/ramensoftware/windhawk-mods/issues | |
| // | |
| // For pull requests, development takes place here: | |
| // https://github.com/m417z/my-windhawk-mods | |
| // ==WindhawkModReadme== | |
| /* | |
| # Custom Window Corner Radius | |
| Customizes Windows 11 app window corner radius. Make corners more rounded than | |
| the default 8px, or reduce the radius for less rounded or completely sharp | |
| corners. | |
| The mod was [originally | |
| submitted](https://github.com/ramensoftware/windhawk-mods/pull/3587) by | |
| [Kanak415](https://github.com/kanak-buet19). The option for maximized and | |
| snapped windows is based on a | |
| [mod](https://github.com/ramensoftware/windhawk-mods/pull/5022) by [Alexey | |
| Lavrinenko](https://github.com/leshaalexey). | |
|  | |
| ## ⚠ Important usage note ⚠ | |
| This mod needs to hook into `dwm.exe` to work. Please navigate to Windhawk's | |
| Settings > Advanced settings > More advanced settings > Process inclusion list, | |
| and make sure that `dwm.exe` is in the list. | |
|  | |
| ## Additional notes | |
| - Some elements, such as context menus, use a smaller radius (4px by default). | |
| This can be customized separately with the "Small corner radius" option. | |
| - Standard tooltips can be customized separately with the "Tooltip corner | |
| radius" option. | |
| - Each window corner can have its own radius with the "Per-corner radius" | |
| options, for example to round only the top corners. | |
| - Windows 11 squares off the corners of maximized and snapped windows. The | |
| "Rounded corners for maximized and snapped windows" option enables rounding | |
| for them. | |
| - Some elements, such as the taskbar, the Start menu, and the notification | |
| center, are unaffected by this mod. Some of them can be customized using other | |
| mods, such as Windows 11 Taskbar Styler. | |
| - Disabling the mod instantly restores default behavior - no system files are | |
| modified. | |
| ## Compatibility | |
| - When using this mod alongside Translucent Flyouts, set its `CornerType` option | |
| to `0` ("Don't Change") to prevent conflicts between the two. | |
| */ | |
| // ==/WindhawkModReadme== | |
| // ==WindhawkModSettings== | |
| /* | |
| - radius: 12 | |
| $name: Corner radius | |
| $description: >- | |
| Corner radius in pixels. Default Win11 is 8. Use smaller values (e.g. 4 or | |
| 0) for less rounded or sharp corners, or larger values (e.g. 10-20) for more | |
| rounded corners. Values above 20 may cause visual artifacts depending on | |
| your DPI scaling. | |
| Set to -1 to keep the original radius. | |
| - perCornerRadius: | |
| - topLeft: -1 | |
| $name: Top-left corner | |
| - topRight: -1 | |
| $name: Top-right corner | |
| - bottomLeft: -1 | |
| $name: Bottom-left corner | |
| - bottomRight: -1 | |
| $name: Bottom-right corner | |
| $name: Per-corner radius | |
| $description: >- | |
| Overrides the corner radius of individual window corners, for example to | |
| round only the top corners. Set a corner to -1 to use the "Corner radius" | |
| value. | |
| These options don't apply to elements that use the small or tooltip radius, | |
| and are ignored when "Corner radius" is -1. | |
| - smallRadius: 6 | |
| $name: Small corner radius | |
| $description: >- | |
| Corner radius for elements that use a smaller radius, such as context menus. | |
| Default Win11 is 4. | |
| Set to -1 to keep the original radius. | |
| - tooltipRadius: -1 | |
| $name: Tooltip corner radius | |
| $description: >- | |
| Corner radius for standard tooltips. Note that this doesn't affect modern | |
| (WinUI) tooltips. Values above 8 may cause visual artifacts depending on | |
| your DPI scaling. | |
| Set to -1 to leave tooltips unchanged. | |
| - roundMaximizedAndSnapped: none | |
| $name: Rounded corners for maximized and snapped windows | |
| $description: >- | |
| Windows 11 squares off the corners of maximized and snapped windows. This | |
| option enables rounding for them using the "Corner radius" value above. | |
| $options: | |
| - none: Leave them square | |
| - snapped: Round snapped windows | |
| - snappedAndMaximized: Round snapped and maximized windows | |
| - excludedPrograms: [""] | |
| $name: Excluded programs | |
| $description: >- | |
| Windows of these programs keep their original corner radius. | |
| Entries can be process names, paths or application IDs, for example: | |
| mspaint.exe | |
| C:\Windows\System32\notepad.exe | |
| Microsoft.WindowsCalculator_8wekyb3d8bbwe!App | |
| */ | |
| // ==/WindhawkModSettings== | |
| #include | |
| #include // Must appear before propkey.h | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| #include | |
| enum class RoundMaximizedAndSnapped { | |
| none, | |
| snapped, | |
| snappedAndMaximized, | |
| }; | |
| // Corners are indexed in the order top-left, top-right, bottom-left, | |
| // bottom-right throughout. | |
| constexpr int kCornerCount = 4; | |
| struct { | |
| float radius; | |
| // How far each corner's radius falls short of `radius`, in DIPs. All zero | |
| // unless per-corner radii are configured, in which case `radius` is the | |
| // largest of them. | |
| float cornerRadiusDelta[kCornerCount]; | |
| bool perCornerRadius; | |
| float smallRadius; | |
| float tooltipRadius; | |
| RoundMaximizedAndSnapped roundMaximizedAndSnapped; | |
| std::unordered_set excludedPrograms; | |
| } g_settings; | |
| using GetWindowData_t = void*(WINAPI*)(void* pThis); | |
| GetWindowData_t GetWindowData_Original; | |
| using IsMaximizedOrSnapped_t = bool(WINAPI*)(void* pThis); | |
| IsMaximizedOrSnapped_t IsMaximizedOrSnapped_Original; | |
| // Captured (not hooked) address of CWindowData::IsGhostWindow. We disassemble | |
| // its first few instructions at init time to recover the HWND member offset. | |
| void* IsGhostWindow_Func; | |
| // HWND offset within CWindowData. Recovered at init time from the first | |
| // `mov rcx, qword ptr [rcx+disp]` instruction in CWindowData::IsGhostWindow, | |
| // which loads its HWND member as the first argument to GetPropW. Stays at | |
| // SIZE_MAX if the pattern can't be matched, in which case HWND lookup is | |
| // disabled and tooltip-specific behavior gracefully degrades. | |
| size_t g_windowDataHwndOffset = SIZE_MAX; | |
| // Scans the first `limit` instructions of `func` for a match against `regex` | |
| // and returns the value of the first capture group parsed as hex. Mirrors the | |
| // helper used by taskbar-button-scroll / taskbar-icon-size. | |
| size_t OffsetFromAssemblyRegex(void* func, | |
| size_t defValue, | |
| std::regex regex, | |
| int limit = 30) { | |
| BYTE* p = (BYTE*)func; | |
| for (int i = 0; i < limit; i++) { | |
| WH_DISASM_RESULT result; | |
| if (!Wh_Disasm(p, &result)) { | |
| break; | |
| } | |
| p += result.length; | |
| std::string_view s = result.text; | |
| if (s == "ret") { | |
| break; | |
| } | |
| std::match_results match; | |
| if (std::regex_match(s.begin(), s.end(), match, regex)) { | |
| // Wh_Log(L"%S", result.text); | |
| return std::stoull(match[1], nullptr, 16); | |
| } | |
| } | |
| Wh_Log(L"Failed for %p", func); | |
| return defValue; | |
| } | |
| HWND HwndFromTopLevelWindow(void* pThis) { | |
| if (g_windowDataHwndOffset == SIZE_MAX || !GetWindowData_Original) { | |
| return nullptr; | |
| } | |
| void* pData = GetWindowData_Original(pThis); | |
| if (!pData) { | |
| return nullptr; | |
| } | |
| HWND hwnd = *(HWND*)((BYTE*)pData + g_windowDataHwndOffset); | |
| return IsWindow(hwnd) ? hwnd : nullptr; | |
| } | |
| bool HwndHasClass(HWND hwnd, PCWSTR className) { | |
| if (!hwnd) { | |
| return false; | |
| } | |
| WCHAR buf[32]; | |
| return GetClassNameW(hwnd, buf, ARRAYSIZE(buf)) && | |
| _wcsicmp(buf, className) == 0; | |
| } | |
| bool IsTopLevelWindowTooltip(void* pThis) { | |
| return HwndHasClass(HwndFromTopLevelWindow(pThis), L"tooltips_class32"); | |
| } | |
| typedef struct _SYSTEM_PROCESS_ID_INFORMATION { | |
| HANDLE ProcessId; | |
| UNICODE_STRING ImageName; | |
| } SYSTEM_PROCESS_ID_INFORMATION; | |
| // The image path of a process in NT form, e.g. | |
| // \Device\HarddiskVolume3\Windows\System32\notepad.exe. Unlike | |
| // QueryFullProcessImageName, this needs no handle to the process, which dwm.exe | |
| // can't get: it runs under a virtual account that isn't in the DACL of the | |
| // processes owning the windows it composes. | |
| std::wstring GetProcessImageNtPath(DWORD processId) { | |
| using NtQuerySystemInformation_t = | |
| LONG(NTAPI*)(ULONG systemInformationClass, PVOID systemInformation, | |
| ULONG systemInformationLength, PULONG returnLength); | |
| static NtQuerySystemInformation_t pNtQuerySystemInformation = []() { | |
| HMODULE ntdll = GetModuleHandle(L"ntdll.dll"); | |
| return ntdll ? (NtQuerySystemInformation_t)GetProcAddress( | |
| ntdll, "NtQuerySystemInformation") | |
| : nullptr; | |
| }(); | |
| if (!pNtQuerySystemInformation) { | |
| return std::wstring{}; | |
| } | |
| constexpr ULONG kSystemProcessIdInformation = 88; | |
| constexpr LONG kStatusInfoLengthMismatch = 0xC0000004; | |
| SYSTEM_PROCESS_ID_INFORMATION info{ | |
| .ProcessId = (HANDLE)(ULONG_PTR)processId, | |
| }; | |
| // An empty ImageName makes the call report the size it needs. | |
| LONG status = pNtQuerySystemInformation(kSystemProcessIdInformation, &info, | |
| sizeof(info), nullptr); | |
| if (status != kStatusInfoLengthMismatch) { | |
| Wh_Log(L"Size query failed for pid=%u: %08X", processId, status); | |
| return std::wstring{}; | |
| } | |
| std::wstring path(info.ImageName.MaximumLength / sizeof(WCHAR), L'\0'); | |
| info.ImageName.Buffer = path.data(); | |
| info.ImageName.Length = 0; | |
| status = pNtQuerySystemInformation(kSystemProcessIdInformation, &info, | |
| sizeof(info), nullptr); | |
| if (status < 0) { | |
| Wh_Log(L"Query failed for pid=%u: %08X", processId, status); | |
| return std::wstring{}; | |
| } | |
| path.resize(info.ImageName.Length / sizeof(WCHAR)); | |
| return path; | |
| } | |
| // Turns \Device\HarddiskVolume3\Windows\... into C:\Windows\..., or returns an | |
| // empty string if no drive letter maps to the device. | |
| std::wstring NtPathToDosPath(const std::wstring& ntPath) { | |
| WCHAR drives[512]; | |
| DWORD len = GetLogicalDriveStrings(ARRAYSIZE(drives), drives); | |
| if (!len || len > ARRAYSIZE(drives)) { | |
| return std::wstring{}; | |
| } | |
| for (PCWSTR drive = drives; *drive; drive += wcslen(drive) + 1) { | |
| WCHAR driveName[] = {drive[0], L':', L'\0'}; | |
| WCHAR target[MAX_PATH]; | |
| if (!QueryDosDevice(driveName, target, ARRAYSIZE(target))) { | |
| continue; | |
| } | |
| size_t targetLen = wcslen(target); | |
| if (ntPath.length() > targetLen && ntPath[targetLen] == L'\\' && | |
| _wcsnicmp(ntPath.c_str(), target, targetLen) == 0) { | |
| return driveName + ntPath.substr(targetLen); | |
| } | |
| } | |
| return std::wstring{}; | |
| } | |
| // The AppUserModelID explicitly set on a window. It's what identifies packaged | |
| // apps, whose windows belong to a shared host process. shell32 is resolved on | |
| // demand to keep it out of dwm.exe unless exclusions are actually used. | |
| std::wstring GetWindowAppId(HWND hWnd) { | |
| using SHGetPropertyStoreForWindow_t = | |
| HRESULT(WINAPI*)(HWND hwnd, REFIID riid, void** ppv); | |
| static SHGetPropertyStoreForWindow_t pSHGetPropertyStoreForWindow = []() { | |
| HMODULE shell32 = LoadLibraryEx(L"shell32.dll", nullptr, | |
| LOAD_LIBRARY_SEARCH_SYSTEM32); | |
| return shell32 ? (SHGetPropertyStoreForWindow_t)GetProcAddress( | |
| shell32, "SHGetPropertyStoreForWindow") | |
| : nullptr; | |
| }(); | |
| std::wstring result; | |
| if (!pSHGetPropertyStoreForWindow) { | |
| Wh_Log(L"SHGetPropertyStoreForWindow isn't available"); | |
| return result; | |
| } | |
| IPropertyStore* propertyStore; | |
| HRESULT hr = | |
| pSHGetPropertyStoreForWindow(hWnd, IID_PPV_ARGS(&propertyStore)); | |
| if (FAILED(hr)) { | |
| Wh_Log(L"SHGetPropertyStoreForWindow failed for hwnd=%p: %08X", hWnd, | |
| hr); | |
| return result; | |
| } | |
| PROPVARIANT pv; | |
| PropVariantInit(&pv); | |
| hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &pv); | |
| if (SUCCEEDED(hr)) { | |
| if (pv.vt == VT_LPWSTR && pv.pwszVal) { | |
| result = pv.pwszVal; | |
| } | |
| PropVariantClear(&pv); | |
| } else { | |
| Wh_Log(L"GetValue failed for hwnd=%p: %08X", hWnd, hr); | |
| } | |
| propertyStore->Release(); | |
| return result; | |
| } | |
| void MakeUpper(std::wstring* str) { | |
| LCMapStringEx(LOCALE_NAME_USER_DEFAULT, LCMAP_UPPERCASE, str->data(), | |
| static_cast(str->length()), str->data(), | |
| static_cast(str->length()), nullptr, nullptr, 0); | |
| } | |
| bool IsWindowExcluded(HWND hWnd) { | |
| DWORD dwProcessId = 0; | |
| GetWindowThreadProcessId(hWnd, &dwProcessId); | |
| std::wstring processPathUpper; | |
| if (dwProcessId) { | |
| std::wstring ntPath = GetProcessImageNtPath(dwProcessId); | |
| processPathUpper = NtPathToDosPath(ntPath); | |
| if (processPathUpper.empty()) { | |
| // Without a drive letter no configured path can match, but the | |
| // last component is still the file name. | |
| processPathUpper = std::move(ntPath); | |
| } | |
| MakeUpper(&processPathUpper); | |
| } | |
| if (!processPathUpper.empty()) { | |
| if (g_settings.excludedPrograms.contains(processPathUpper)) { | |
| Wh_Log(L"hwnd=%p excluded by path: %s", hWnd, | |
| processPathUpper.c_str()); | |
| return true; | |
| } | |
| size_t fileNamePos = processPathUpper.rfind(L'\\'); | |
| if (fileNamePos != std::wstring::npos) { | |
| std::wstring fileNameUpper = | |
| processPathUpper.substr(fileNamePos + 1); | |
| if (!fileNameUpper.empty() && | |
| g_settings.excludedPrograms.contains(fileNameUpper)) { | |
| Wh_Log(L"hwnd=%p excluded by file name: %s", hWnd, | |
| fileNameUpper.c_str()); | |
| return true; | |
| } | |
| } | |
| } | |
| std::wstring appIdUpper = GetWindowAppId(hWnd); | |
| MakeUpper(&appIdUpper); | |
| if (!appIdUpper.empty() && | |
| g_settings.excludedPrograms.contains(appIdUpper)) { | |
| Wh_Log(L"hwnd=%p excluded by app id: %s", hWnd, appIdUpper.c_str()); | |
| return true; | |
| } | |
| Wh_Log(L"hwnd=%p not excluded, path=[%s], appId=[%s]", hWnd, | |
| processPathUpper.c_str(), appIdUpper.c_str()); | |
| return false; | |
| } | |
| constexpr WCHAR kWindowExclusionProp[] = L"Windhawk_Excluded_" WH_MOD_ID; | |
| const HANDLE kWindowNotExcluded = (HANDLE)1; | |
| const HANDLE kWindowExcluded = (HANDLE)2; | |
| // Resolving the program of a window is expensive, and the hooks run as part of | |
| // composing every frame, so the verdict is cached in a window property. The | |
| // properties are dropped when the settings change and when the mod is unloaded. | |
| bool IsWindowExcludedCached(HWND hWnd) { | |
| HANDLE prop = GetProp(hWnd, kWindowExclusionProp); | |
| if (!prop) { | |
| prop = IsWindowExcluded(hWnd) ? kWindowExcluded : kWindowNotExcluded; | |
| if (!SetProp(hWnd, kWindowExclusionProp, prop)) { | |
| Wh_Log(L"SetProp failed for hwnd=%p: %u", hWnd, GetLastError()); | |
| } | |
| } | |
| return prop == kWindowExcluded; | |
| } | |
| // Windows whose HWND can't be recovered are never excluded, same as the other | |
| // HWND-based checks. | |
| bool IsTopLevelWindowExcluded(void* pThis) { | |
| if (g_settings.excludedPrograms.empty()) { | |
| return false; | |
| } | |
| HWND hwnd = HwndFromTopLevelWindow(pThis); | |
| if (!hwnd) { | |
| Wh_Log(L"No hwnd for %p, can't check exclusions", pThis); | |
| return false; | |
| } | |
| return IsWindowExcludedCached(hwnd); | |
| } | |
| void ClearWindowExclusionProps() { | |
| EnumWindows( | |
| [](HWND hWnd, LPARAM) -> BOOL { | |
| RemoveProp(hWnd, kWindowExclusionProp); | |
| return TRUE; | |
| }, | |
| 0); | |
| } | |
| float RadiusForOriginal(float orig, bool isTooltip) { | |
| // In new builds, multiple hooks fire in sequence (GetRadiusFromCornerStyle | |
| // -> GetFloatCornerRadiusForCurrentStyle -> SetBorderParameters), so a | |
| // downstream hook may see a value already replaced by an upstream hook. | |
| // Skip replacement if the value already matches a configured radius to keep | |
| // the function idempotent. | |
| if (orig == g_settings.radius || orig == g_settings.smallRadius || | |
| orig == g_settings.tooltipRadius) { | |
| return orig; | |
| } | |
| if (isTooltip && g_settings.tooltipRadius >= 0.0f) { | |
| return g_settings.tooltipRadius; | |
| } | |
| // Win11 defaults: 4.0 for smaller radius, 8.0 for larger radius. Use middle | |
| // point as a threshold. Don't override if new value is negative. | |
| float newValue = orig < 6.0f ? g_settings.smallRadius : g_settings.radius; | |
| if (newValue < 0.0f) { | |
| return orig; | |
| } | |
| return newValue; | |
| } | |
| // Whether `radius`, as returned by RadiusForOriginal, is the window radius, | |
| // the one per-corner radii apply to. | |
| bool IsPerCornerRadius(float radius) { | |
| return g_settings.perCornerRadius && radius == g_settings.radius; | |
| } | |
| // DWM computes a single radius per window and hands it to the functions that | |
| // build the clip geometry and draw the border. Per-corner radii are applied | |
| // there: while a call whose output should get them is in progress, this holds | |
| // how far each corner falls short of that single radius, in pixels, and the | |
| // hooks on those functions adjust each corner accordingly. | |
| struct CornerRadiusDeltas { | |
| float px[kCornerCount]; | |
| }; | |
| thread_local const CornerRadiusDeltas* g_cornerRadiusDeltas; | |
| class CornerRadiusDeltasScope { | |
| public: | |
| explicit CornerRadiusDeltasScope(const CornerRadiusDeltas* deltas) | |
| : m_prev(g_cornerRadiusDeltas) { | |
| g_cornerRadiusDeltas = deltas; | |
| } | |
| ~CornerRadiusDeltasScope() { g_cornerRadiusDeltas = m_prev; } | |
| CornerRadiusDeltasScope(const CornerRadiusDeltasScope&) = delete; | |
| CornerRadiusDeltasScope& operator=(const CornerRadiusDeltasScope&) = delete; | |
| private: | |
| const CornerRadiusDeltas* m_prev; | |
| }; | |
| float AdjustedCornerRadius(float radius, int corner) { | |
| return std::max(radius + g_cornerRadiusDeltas->px[corner], 0.0f); | |
| } | |
| CornerRadiusDeltas CornerRadiusDeltasForDpi(int dpi) { | |
| CornerRadiusDeltas deltas; | |
| for (int i = 0; i < kCornerCount; i++) { | |
| deltas.px[i] = g_settings.cornerRadiusDelta[i] * dpi / 96.0f; | |
| } | |
| return deltas; | |
| } | |
| // Deltas for the content clip of each CWindowBorder, keyed by the border. The | |
| // clip is rebuilt on every resize from just the border's stored radius and | |
| // DPI, so the deltas are kept from when its parameters were set. | |
| std::mutex g_borderCornerRadiusDeltasMutex; | |
| std::unordered_map g_borderCornerRadiusDeltas; | |
| void SetBorderCornerRadiusDeltas(void* border, | |
| const CornerRadiusDeltas* deltas) { | |
| std::lock_guard lock(g_borderCornerRadiusDeltasMutex); | |
| if (deltas) { | |
| g_borderCornerRadiusDeltas[border] = *deltas; | |
| } else { | |
| g_borderCornerRadiusDeltas.erase(border); | |
| } | |
| } | |
| bool GetBorderCornerRadiusDeltas(void* border, CornerRadiusDeltas* deltas) { | |
| std::lock_guard lock(g_borderCornerRadiusDeltasMutex); | |
| auto it = g_borderCornerRadiusDeltas.find(border); | |
| if (it == g_borderCornerRadiusDeltas.end()) { | |
| return false; | |
| } | |
| *deltas = it->second; | |
| return true; | |
| } | |
| using CWindowBorder_Destructor_t = void(WINAPI*)(void* pThis); | |
| CWindowBorder_Destructor_t CWindowBorder_Destructor_Original; | |
| void WINAPI CWindowBorder_Destructor_Hook(void* pThis) { | |
| SetBorderCornerRadiusDeltas(pThis, nullptr); | |
| CWindowBorder_Destructor_Original(pThis); | |
| } | |
| // The geometry DWM clips window content with. It takes an X and Y radius per | |
| // corner, but every caller passes the same radius in all eight. | |
| using SetRectangle_t = long(WINAPI*)(void* pThis, | |
| float left, | |
| float top, | |
| float right, | |
| float bottom, | |
| float topLeftX, | |
| float topLeftY, | |
| float topRightX, | |
| float topRightY, | |
| float bottomLeftX, | |
| float bottomLeftY, | |
| float bottomRightX, | |
| float bottomRightY, | |
| bool flag); | |
| SetRectangle_t SetRectangle_Original; | |
| long WINAPI SetRectangle_Hook(void* pThis, | |
| float left, | |
| float top, | |
| float right, | |
| float bottom, | |
| float topLeftX, | |
| float topLeftY, | |
| float topRightX, | |
| float topRightY, | |
| float bottomLeftX, | |
| float bottomLeftY, | |
| float bottomRightX, | |
| float bottomRightY, | |
| bool flag) { | |
| if (g_cornerRadiusDeltas) { | |
| Wh_Log(L"> %f", topLeftX); | |
| topLeftX = topLeftY = AdjustedCornerRadius(topLeftX, 0); | |
| topRightX = topRightY = AdjustedCornerRadius(topRightX, 1); | |
| bottomLeftX = bottomLeftY = AdjustedCornerRadius(bottomLeftX, 2); | |
| bottomRightX = bottomRightY = AdjustedCornerRadius(bottomRightX, 3); | |
| } | |
| return SetRectangle_Original(pThis, left, top, right, bottom, topLeftX, | |
| topLeftY, topRightX, topRightY, bottomLeftX, | |
| bottomLeftY, bottomRightX, bottomRightY, | |
| flag); | |
| } | |
| // Sets the content clip from the border rect, the stored radius and DPI, and | |
| // the border thickness. Called on every resize. | |
| using SetClipRectangle_t = void(WINAPI*)(void* pThis, | |
| void* geometry, | |
| const RECT& rect); | |
| SetClipRectangle_t SetClipRectangle_Original; | |
| void WINAPI SetClipRectangle_Hook(void* pThis, | |
| void* geometry, | |
| const RECT& rect) { | |
| CornerRadiusDeltas deltas; | |
| bool perCorner = GetBorderCornerRadiusDeltas(pThis, &deltas); | |
| CornerRadiusDeltasScope scope(perCorner ? &deltas : nullptr); | |
| SetClipRectangle_Original(pThis, geometry, rect); | |
| } | |
| // A rounded rectangle path with the active deltas applied to `radius` at each | |
| // corner. Replaces the D2D rounded rectangles the border surface is drawn | |
| // with, which have a single radius. | |
| HRESULT CreateAdjustedRoundedRectangleGeometry(ID2D1Factory* factory, | |
| const D2D1_RECT_F& rect, | |
| float radius, | |
| ID2D1PathGeometry** geometry) { | |
| float maxRadius = | |
| std::min(rect.right - rect.left, rect.bottom - rect.top) / 2; | |
| float radii[kCornerCount]; | |
| for (int i = 0; i < kCornerCount; i++) { | |
| radii[i] = std::min(AdjustedCornerRadius(radius, i), maxRadius); | |
| } | |
| winrt::com_ptr path; | |
| HRESULT hr = factory->CreatePathGeometry(path.put()); | |
| if (FAILED(hr)) { | |
| return hr; | |
| } | |
| winrt::com_ptr sink; | |
| hr = path->Open(sink.put()); | |
| if (FAILED(hr)) { | |
| return hr; | |
| } | |
| auto arcTo = [&sink](D2D1_POINT_2F point, float r) { | |
| if (r > 0) { | |
| sink->AddArc(D2D1::ArcSegment(point, D2D1::SizeF(r, r), 0.0f, | |
| D2D1_SWEEP_DIRECTION_CLOCKWISE, | |
| D2D1_ARC_SIZE_SMALL)); | |
| } | |
| }; | |
| // Clockwise from the top edge. | |
| sink->BeginFigure(D2D1::Point2F(rect.left + radii[0], rect.top), | |
| D2D1_FIGURE_BEGIN_FILLED); | |
| sink->AddLine(D2D1::Point2F(rect.right - radii[1], rect.top)); | |
| arcTo(D2D1::Point2F(rect.right, rect.top + radii[1]), radii[1]); | |
| sink->AddLine(D2D1::Point2F(rect.right, rect.bottom - radii[3])); | |
| arcTo(D2D1::Point2F(rect.right - radii[3], rect.bottom), radii[3]); | |
| sink->AddLine(D2D1::Point2F(rect.left + radii[2], rect.bottom)); | |
| arcTo(D2D1::Point2F(rect.left, rect.bottom - radii[2]), radii[2]); | |
| sink->AddLine(D2D1::Point2F(rect.left, rect.top + radii[0])); | |
| arcTo(D2D1::Point2F(rect.left + radii[0], rect.top), radii[0]); | |
| sink->EndFigure(D2D1_FIGURE_END_CLOSED); | |
| hr = sink->Close(); | |
| if (FAILED(hr)) { | |
| return hr; | |
| } | |
| *geometry = path.detach(); | |
| return S_OK; | |
| } | |
| using FillRoundedRectangle_t = void(WINAPI*)(ID2D1RenderTarget* pThis, | |
| const D2D1_ROUNDED_RECT* rect, | |
| ID2D1Brush* brush); | |
| void FillRoundedRectangleWithDeltas(FillRoundedRectangle_t original, | |
| ID2D1RenderTarget* pThis, | |
| const D2D1_ROUNDED_RECT* rect, | |
| ID2D1Brush* brush) { | |
| if (g_cornerRadiusDeltas) { | |
| Wh_Log(L"> %f", rect->radiusX); | |
| winrt::com_ptr factory; | |
| pThis->GetFactory(factory.put()); | |
| winrt::com_ptr geometry; | |
| HRESULT hr = CreateAdjustedRoundedRectangleGeometry( | |
| factory.get(), rect->rect, rect->radiusX, geometry.put()); | |
| if (SUCCEEDED(hr)) { | |
| pThis->FillGeometry(geometry.get(), brush); | |
| return; | |
| } | |
| Wh_Log(L"Failed: %08X", hr); | |
| } | |
| original(pThis, rect, brush); | |
| } | |
| // The border ring is filled on the device context DWM gets from the | |
| // composition surface, and the shadow shape on a bitmap render target made | |
| // from it. Those are separate classes in d2d1.dll, whose implementations of | |
| // this method may or may not be folded into one function. | |
| FillRoundedRectangle_t FillRoundedRectangle_Original; | |
| void WINAPI FillRoundedRectangle_Hook(ID2D1RenderTarget* pThis, | |
| const D2D1_ROUNDED_RECT* rect, | |
| ID2D1Brush* brush) { | |
| FillRoundedRectangleWithDeltas(FillRoundedRectangle_Original, pThis, rect, | |
| brush); | |
| } | |
| FillRoundedRectangle_t BitmapTargetFillRoundedRectangle_Original; | |
| void WINAPI BitmapTargetFillRoundedRectangle_Hook(ID2D1RenderTarget* pThis, | |
| const D2D1_ROUNDED_RECT* rect, | |
| ID2D1Brush* brush) { | |
| FillRoundedRectangleWithDeltas(BitmapTargetFillRoundedRectangle_Original, | |
| pThis, rect, brush); | |
| } | |
| using CreateRoundedRectangleGeometry_t = | |
| HRESULT(WINAPI*)(ID2D1Factory* pThis, | |
| const D2D1_ROUNDED_RECT* rect, | |
| ID2D1RoundedRectangleGeometry** geometry); | |
| CreateRoundedRectangleGeometry_t CreateRoundedRectangleGeometry_Original; | |
| HRESULT WINAPI | |
| CreateRoundedRectangleGeometry_Hook(ID2D1Factory* pThis, | |
| const D2D1_ROUNDED_RECT* rect, | |
| ID2D1RoundedRectangleGeometry** geometry) { | |
| if (g_cornerRadiusDeltas) { | |
| Wh_Log(L"> %f", rect->radiusX); | |
| winrt::com_ptr path; | |
| HRESULT hr = CreateAdjustedRoundedRectangleGeometry( | |
| pThis, rect->rect, rect->radiusX, path.put()); | |
| if (SUCCEEDED(hr)) { | |
| // The border code only combines the result with another geometry | |
| // and releases it, which any ID2D1Geometry supports. | |
| *geometry = | |
| reinterpret_cast(path.detach()); | |
| return S_OK; | |
| } | |
| Wh_Log(L"Failed: %08X", hr); | |
| } | |
| return CreateRoundedRectangleGeometry_Original(pThis, rect, geometry); | |
| } | |
| // Draws the border and its shadow into the surface a window's border brush | |
| // stretches as a nine-grid. DWM caches the result by these parameters. | |
| using CreateBorderSurface_t = long(WINAPI*)(float radius, | |
| int dpi, | |
| const void* color, | |
| int borderStyle, | |
| int shadowStyle, | |
| void** surface); | |
| CreateBorderSurface_t CreateBorderSurface_Original; | |
| long WINAPI CreateBorderSurface_Hook(float radius, | |
| int dpi, | |
| const void* color, | |
| int borderStyle, | |
| int shadowStyle, | |
| void** surface) { | |
| Wh_Log(L"> %f dpi=%d", radius, dpi); | |
| CornerRadiusDeltas deltas; | |
| bool perCorner = IsPerCornerRadius(radius); | |
| if (perCorner) { | |
| deltas = CornerRadiusDeltasForDpi(dpi); | |
| } | |
| CornerRadiusDeltasScope scope(perCorner ? &deltas : nullptr); | |
| return CreateBorderSurface_Original(radius, dpi, color, borderStyle, | |
| shadowStyle, surface); | |
| } | |
| struct D2DFunctions { | |
| void* createRoundedRectangleGeometry; | |
| void* fillRoundedRectangle; | |
| void* bitmapTargetFillRoundedRectangle; | |
| }; | |
| // d2d1.dll's implementations of ID2D1Factory::CreateRoundedRectangleGeometry | |
| // and ID2D1RenderTarget::FillRoundedRectangle. d2d1.dll doesn't export them, | |
| // so they're read off the vtables of throwaway objects. A D3D device with the | |
| // null driver is enough to get a device context of the same class as the one | |
| // DWM draws the border surface with. | |
| bool ResolveD2DFunctions(D2DFunctions* functions) { | |
| winrt::com_ptr factory; | |
| HRESULT hr = | |
| D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, | |
| __uuidof(ID2D1Factory1), nullptr, factory.put_void()); | |
| if (FAILED(hr)) { | |
| Wh_Log(L"D2D1CreateFactory failed: %08X", hr); | |
| return false; | |
| } | |
| winrt::com_ptr d3dDevice; | |
| hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_NULL, nullptr, | |
| D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, | |
| D3D11_SDK_VERSION, d3dDevice.put(), nullptr, | |
| nullptr); | |
| if (FAILED(hr)) { | |
| Wh_Log(L"D3D11CreateDevice failed: %08X", hr); | |
| return false; | |
| } | |
| winrt::com_ptr d2dDevice; | |
| hr = factory->CreateDevice(d3dDevice.try_as().get(), | |
| d2dDevice.put()); | |
| if (FAILED(hr)) { | |
| Wh_Log(L"CreateDevice failed: %08X", hr); | |
| return false; | |
| } | |
| winrt::com_ptr deviceContext; | |
| hr = d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, | |
| deviceContext.put()); | |
| if (FAILED(hr)) { | |
| Wh_Log(L"CreateDeviceContext failed: %08X", hr); | |
| return false; | |
| } | |
| // A compatible render target needs a target to be compatible with. | |
| D2D1_BITMAP_PROPERTIES1 bitmapProperties = D2D1::BitmapProperties1( | |
| D2D1_BITMAP_OPTIONS_TARGET, | |
| D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, | |
| D2D1_ALPHA_MODE_PREMULTIPLIED)); | |
| winrt::com_ptr bitmap; | |
| winrt::com_ptr bitmapTarget; | |
| hr = deviceContext->CreateBitmap(D2D1::SizeU(1, 1), nullptr, 0, | |
| &bitmapProperties, bitmap.put()); | |
| if (SUCCEEDED(hr)) { | |
| deviceContext->SetTarget(bitmap.get()); | |
| hr = deviceContext->CreateCompatibleRenderTarget(bitmapTarget.put()); | |
| } | |
| if (FAILED(hr)) { | |
| Wh_Log(L"Bitmap render target creation failed: %08X", hr); | |
| } | |
| // Vtable slots, counted from IUnknown. | |
| constexpr int kCreateRoundedRectangleGeometrySlot = 6; | |
| constexpr int kFillRoundedRectangleSlot = 19; | |
| auto vtableEntry = [](IUnknown* object, int slot) { | |
| return (*reinterpret_cast(object))[slot]; | |
| }; | |
| functions->createRoundedRectangleGeometry = | |
| vtableEntry(factory.get(), kCreateRoundedRectangleGeometrySlot); | |
| functions->fillRoundedRectangle = | |
| vtableEntry(deviceContext.get(), kFillRoundedRectangleSlot); | |
| functions->bitmapTargetFillRoundedRectangle = | |
| bitmapTarget | |
| ? vtableEntry(bitmapTarget.get(), kFillRoundedRectangleSlot) | |
| : nullptr; | |
| return true; | |
| } | |
| bool HookD2DFunctions() { | |
| D2DFunctions functions; | |
| if (!ResolveD2DFunctions(&functions)) { | |
| return false; | |
| } | |
| if (!WindhawkUtils::SetFunctionHook( | |
| (CreateRoundedRectangleGeometry_t) | |
| functions.createRoundedRectangleGeometry, | |
| CreateRoundedRectangleGeometry_Hook, | |
| &CreateRoundedRectangleGeometry_Original) || | |
| !WindhawkUtils::SetFunctionHook( | |
| (FillRoundedRectangle_t)functions.fillRoundedRectangle, | |
| FillRoundedRectangle_Hook, &FillRoundedRectangle_Original)) { | |
| Wh_Log(L"Hooking D2D functions failed"); | |
| return false; | |
| } | |
| if (functions.bitmapTargetFillRoundedRectangle && | |
| functions.bitmapTargetFillRoundedRectangle != | |
| functions.fillRoundedRectangle && | |
| !WindhawkUtils::SetFunctionHook( | |
| (FillRoundedRectangle_t)functions.bitmapTargetFillRoundedRectangle, | |
| BitmapTargetFillRoundedRectangle_Hook, | |
| &BitmapTargetFillRoundedRectangle_Original)) { | |
| Wh_Log(L"Hooking the bitmap render target failed"); | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Forces an empty window region on a SysShadow companion HWND so the | |
| // legacy rectangular drop shadow stops being composited. Idempotent: once | |
| // the window already has the (empty) region we own, the call is a no-op. | |
| void HideSysShadowWindow(HWND hwnd) { | |
| HRGN rgn = CreateRectRgn(0, 0, 0, 0); | |
| if (!rgn) { | |
| return; | |
| } | |
| if (!SetWindowRgn(hwnd, rgn, FALSE)) { | |
| DeleteObject(rgn); | |
| } | |
| // SetWindowRgn takes ownership of the region on success. | |
| } | |
| // The CTopLevelWindow whose UpdateWindowVisuals call is currently running, or | |
| // null outside of one. SetBorderParameters gets a CWindowBorder, which offers | |
| // no way back to the window it belongs to. | |
| thread_local void* g_updateWindowVisualsTarget; | |
| using UpdateWindowVisuals_t = long(WINAPI*)(void* pThis); | |
| UpdateWindowVisuals_t UpdateWindowVisuals_Original; | |
| long WINAPI UpdateWindowVisuals_Hook(void* pThis) { | |
| if (g_settings.tooltipRadius >= 0.0f && !IsTopLevelWindowExcluded(pThis)) { | |
| HWND hwnd = HwndFromTopLevelWindow(pThis); | |
| if (HwndHasClass(hwnd, L"SysShadow")) { | |
| Wh_Log(L"> hiding SysShadow hwnd=%p", hwnd); | |
| HideSysShadowWindow(hwnd); | |
| return 0; | |
| } | |
| } | |
| void* prevTarget = g_updateWindowVisualsTarget; | |
| g_updateWindowVisualsTarget = pThis; | |
| long ret = UpdateWindowVisuals_Original(pThis); | |
| g_updateWindowVisualsTarget = prevTarget; | |
| return ret; | |
| } | |
| using GetEffectiveCornerStyle_t = int(WINAPI*)(void* pThis); | |
| GetEffectiveCornerStyle_t GetEffectiveCornerStyle_Original; | |
| int WINAPI GetEffectiveCornerStyle_Hook(void* pThis) { | |
| int orig = GetEffectiveCornerStyle_Original(pThis); | |
| // Tooltips report DWMWCP_DONOTROUND, meaning DWM won't round them. | |
| // Promote them to DWMWCP_ROUNDSMALL so the rounding pipeline kicks in: | |
| // GetShadowStyle returns a rounded-shadow style, and | |
| // GetRadiusFromCornerStyle returns a non-zero radius that our hooks | |
| // override to the configured tooltipRadius via RadiusForOriginal. | |
| if (orig == DWMWCP_DONOTROUND && g_settings.tooltipRadius >= 0.0f && | |
| !IsTopLevelWindowExcluded(pThis) && IsTopLevelWindowTooltip(pThis)) { | |
| Wh_Log(L"> cornerStyle DONOTROUND -> ROUNDSMALL (tooltip)"); | |
| return DWMWCP_ROUNDSMALL; | |
| } | |
| return orig; | |
| } | |
| bool ShouldRoundMaximizedOrSnapped(void* pThis) { | |
| if (g_settings.roundMaximizedAndSnapped == RoundMaximizedAndSnapped::none || | |
| g_settings.radius < 0.0f || !IsMaximizedOrSnapped_Original || | |
| !IsMaximizedOrSnapped_Original(pThis)) { | |
| return false; | |
| } | |
| // A zero radius can also come from an app asking for DWMWCP_DONOTROUND. | |
| // Only the squaring should be undone, so consult the corner style, which | |
| // the squaring doesn't touch. | |
| if (GetEffectiveCornerStyle_Original && | |
| GetEffectiveCornerStyle_Original(pThis) == DWMWCP_DONOTROUND) { | |
| return false; | |
| } | |
| if (g_settings.roundMaximizedAndSnapped == | |
| RoundMaximizedAndSnapped::snappedAndMaximized) { | |
| return true; | |
| } | |
| // Snapped windows only: a maximized window is zoomed, a snapped one isn't. | |
| // Without an HWND there's no way to tell the two apart, so leave the window | |
| // alone. | |
| HWND hwnd = HwndFromTopLevelWindow(pThis); | |
| return hwnd && !IsZoomed(hwnd); | |
| } | |
| // Only GetFloatCornerRadiusForCurrentStyle squares maximized and snapped | |
| // windows, so only it passes canRoundMaximizedOrSnapped. Elsewhere a zero | |
| // radius comes from the corner style and is left alone. | |
| float AdjustCornerRadius(void* pThis, | |
| float orig, | |
| bool canRoundMaximizedOrSnapped) { | |
| if (IsTopLevelWindowExcluded(pThis)) { | |
| return orig; | |
| } | |
| if (orig > 0) { | |
| bool isTooltip = | |
| g_settings.tooltipRadius >= 0.0f && IsTopLevelWindowTooltip(pThis); | |
| Wh_Log(L"> %f isTooltip=%d", orig, isTooltip); | |
| return RadiusForOriginal(orig, isTooltip); | |
| } | |
| if (canRoundMaximizedOrSnapped && ShouldRoundMaximizedOrSnapped(pThis)) { | |
| Wh_Log(L"> %f -> %f (maximized or snapped)", orig, g_settings.radius); | |
| return g_settings.radius; | |
| } | |
| return orig; | |
| } | |
| // Whether the radius last returned by GetRadiusFromCornerStyle_Hook is the | |
| // window radius. UpdateAnimatedResources scales that radius for DPI and | |
| // animation progress and hands it straight to CreateRectangleGeometry, which | |
| // is where the per-corner radii for the animated clip are applied. | |
| thread_local bool g_radiusFromCornerStylePerCorner; | |
| using GetRadiusFromCornerStyle_t = float(WINAPI*)(void* pThis); | |
| GetRadiusFromCornerStyle_t GetRadiusFromCornerStyle_Original; | |
| float WINAPI GetRadiusFromCornerStyle_Hook(void* pThis) { | |
| float radius = | |
| AdjustCornerRadius(pThis, GetRadiusFromCornerStyle_Original(pThis), | |
| /*canRoundMaximizedOrSnapped=*/false); | |
| g_radiusFromCornerStylePerCorner = IsPerCornerRadius(radius); | |
| return radius; | |
| } | |
| // The clip of a window in a minimize or restore animation. The radius shrinks | |
| // along with the window, so the deltas are scaled with it rather than by DPI. | |
| using CreateRectangleGeometry_t = long(WINAPI*)(const void* rect, | |
| float radius, | |
| void** geometry); | |
| CreateRectangleGeometry_t CreateRectangleGeometry_Original; | |
| long WINAPI CreateRectangleGeometry_Hook(const void* rect, | |
| float radius, | |
| void** geometry) { | |
| CornerRadiusDeltas deltas; | |
| bool perCorner = radius > 0 && g_radiusFromCornerStylePerCorner; | |
| if (perCorner) { | |
| Wh_Log(L"> %f", radius); | |
| for (int i = 0; i < kCornerCount; i++) { | |
| deltas.px[i] = | |
| radius * g_settings.cornerRadiusDelta[i] / g_settings.radius; | |
| } | |
| } | |
| CornerRadiusDeltasScope scope(perCorner ? &deltas : nullptr); | |
| return CreateRectangleGeometry_Original(rect, radius, geometry); | |
| } | |
| using GetFloatCornerRadiusForCurrentStyle_t = float(WINAPI*)(void* pThis); | |
| GetFloatCornerRadiusForCurrentStyle_t | |
| GetFloatCornerRadiusForCurrentStyle_Original; | |
| float WINAPI GetFloatCornerRadiusForCurrentStyle_Hook(void* pThis) { | |
| return AdjustCornerRadius( | |
| pThis, GetFloatCornerRadiusForCurrentStyle_Original(pThis), | |
| /*canRoundMaximizedOrSnapped=*/true); | |
| } | |
| using SetBorderParameters_t = long(WINAPI*)(void* pThis, | |
| const RECT& borderRect, | |
| float cornerRadius, | |
| int dpi, | |
| const void* color, | |
| int borderStyle, | |
| int shadowStyle); | |
| SetBorderParameters_t SetBorderParameters_Original; | |
| long WINAPI SetBorderParameters_Hook(void* pThis, | |
| const RECT& borderRect, | |
| float cornerRadius, | |
| int dpi, | |
| const void* color, | |
| int borderStyle, | |
| int shadowStyle) { | |
| bool perCorner = false; | |
| if (cornerRadius > 0) { | |
| // pThis here is a CWindowBorder, not a CTopLevelWindow, so there's no | |
| // straightforward way to recover the HWND for tooltip detection. This | |
| // path is only used by old builds where the radius is computed inline. | |
| // The enclosing UpdateWindowVisuals call is what identifies the window | |
| // for the exclusion check. | |
| bool excluded = g_updateWindowVisualsTarget && | |
| IsTopLevelWindowExcluded(g_updateWindowVisualsTarget); | |
| if (!excluded) { | |
| Wh_Log(L"> %f", cornerRadius); | |
| cornerRadius = RadiusForOriginal(cornerRadius, false); | |
| perCorner = IsPerCornerRadius(cornerRadius); | |
| } | |
| } | |
| CornerRadiusDeltas deltas; | |
| if (perCorner) { | |
| deltas = CornerRadiusDeltasForDpi(dpi); | |
| } | |
| SetBorderCornerRadiusDeltas(pThis, perCorner ? &deltas : nullptr); | |
| return SetBorderParameters_Original(pThis, borderRect, cornerRadius, dpi, | |
| color, borderStyle, shadowStyle); | |
| } | |
| // The corner radius is only recomputed when DWM refreshes a window's visuals. | |
| // dwm.exe's notification window turns WM_SYSCOLORCHANGE into an internal | |
| // settings-change message that marks every window dirty, so the next frame | |
| // re-runs UpdateWindowVisuals for all of them. | |
| void RequestDwmRefresh() { | |
| HWND hDwm = FindWindow(L"Dwm", nullptr); | |
| if (!hDwm) { | |
| Wh_Log(L"DWM notification window wasn't found"); | |
| return; | |
| } | |
| PostMessage(hDwm, WM_SYSCOLORCHANGE, 0, 0); | |
| } | |
| // Set at init once the hooks per-corner radii depend on are in place. | |
| bool g_perCornerRadiusSupported; | |
| // `value` moved `ulps` floats up. | |
| float FloatAbove(int value, int ulps) { | |
| float result = static_cast(value); | |
| for (int i = 0; i < ulps; i++) { | |
| result = std::nextafter(result, std::numeric_limits::max()); | |
| } | |
| return result; | |
| } | |
| void LoadSettings() { | |
| int radius = Wh_GetIntSetting(L"radius"); | |
| // Per-corner radii are applied as a reduction from the radius DWM works | |
| // with, so that radius is the largest of them. Corners left at -1 follow | |
| // "Corner radius", and all of them are ignored when it's -1 (keep the | |
| // original radius), since there's no known value to reduce from. | |
| PCWSTR cornerNames[kCornerCount] = {L"topLeft", L"topRight", L"bottomLeft", | |
| L"bottomRight"}; | |
| int cornerRadius[kCornerCount]; | |
| int maxRadius = radius; | |
| for (int i = 0; i < kCornerCount; i++) { | |
| cornerRadius[i] = radius >= 0 ? Wh_GetIntSetting(L"perCornerRadius.%s", | |
| cornerNames[i]) | |
| : -1; | |
| if (cornerRadius[i] < 0) { | |
| cornerRadius[i] = radius; | |
| } | |
| maxRadius = std::max(maxRadius, cornerRadius[i]); | |
| } | |
| g_settings.perCornerRadius = false; | |
| for (int i = 0; i < kCornerCount; i++) { | |
| g_settings.cornerRadiusDelta[i] = | |
| static_cast(cornerRadius[i] - maxRadius); | |
| if (g_settings.cornerRadiusDelta[i] != 0) { | |
| g_settings.perCornerRadius = true; | |
| } | |
| } | |
| if (g_settings.perCornerRadius && !g_perCornerRadiusSupported) { | |
| Wh_Log(L"Per-corner radius isn't supported, ignoring"); | |
| g_settings.perCornerRadius = false; | |
| } | |
| // Use `std::nextafter` to get a value that's just slightly above the | |
| // integer, for two reasons: | |
| // 1. The original radius values are integer-based, so if the new value is | |
| // exactly the same as the original value, it's impossible to determine | |
| // whether the mod should override it or not if the custom value is | |
| // identical to one of the original values (see RadiusForOriginal). | |
| // 2. If the zero value is used, some functions may treat it as a special | |
| // case, for example dark mode menus will have a white border. | |
| // | |
| // The window radius gets an extra ulp per settings load on top of that. | |
| // Per-corner radii don't show in the radius DWM sees, so this is what | |
| // keeps the window radius apart from a small or tooltip radius with the | |
| // same integer, and what makes it a new key for DWM's cache of border | |
| // surfaces when only the per-corner radii change. | |
| static int loadCount = 0; | |
| loadCount++; | |
| g_settings.radius = FloatAbove(maxRadius, 1 + loadCount); | |
| g_settings.smallRadius = FloatAbove(Wh_GetIntSetting(L"smallRadius"), 1); | |
| g_settings.tooltipRadius = | |
| FloatAbove(Wh_GetIntSetting(L"tooltipRadius"), 1); | |
| PCWSTR roundMaximizedAndSnapped = | |
| Wh_GetStringSetting(L"roundMaximizedAndSnapped"); | |
| g_settings.roundMaximizedAndSnapped = RoundMaximizedAndSnapped::none; | |
| if (wcscmp(roundMaximizedAndSnapped, L"snapped") == 0) { | |
| g_settings.roundMaximizedAndSnapped = RoundMaximizedAndSnapped::snapped; | |
| } else if (wcscmp(roundMaximizedAndSnapped, L"snappedAndMaximized") == 0) { | |
| g_settings.roundMaximizedAndSnapped = | |
| RoundMaximizedAndSnapped::snappedAndMaximized; | |
| } | |
| Wh_FreeStringSetting(roundMaximizedAndSnapped); | |
| g_settings.excludedPrograms.clear(); | |
| for (int i = 0;; i++) { | |
| PCWSTR program = Wh_GetStringSetting(L"excludedPrograms[%d]", i); | |
| bool hasProgram = *program; | |
| if (hasProgram) { | |
| std::wstring programUpper = program; | |
| LCMapStringEx( | |
| LOCALE_NAME_USER_DEFAULT, LCMAP_UPPERCASE, &programUpper[0], | |
| static_cast(programUpper.length()), &programUpper[0], | |
| static_cast(programUpper.length()), nullptr, nullptr, 0); | |
| Wh_Log(L"Excluded program: [%s]", programUpper.c_str()); | |
| g_settings.excludedPrograms.insert(std::move(programUpper)); | |
| } | |
| Wh_FreeStringSetting(program); | |
| if (!hasProgram) { | |
| break; | |
| } | |
| } | |
| } | |
| // Returns true if at least two Dwminit warnings (Level=3) were logged in the | |
| // Application event log within the last 60 seconds. DWM logs warnings here when | |
| // it crashes and is restarted by the session manager, so repeated warnings are | |
| // a strong signal that something in the desktop pipeline is unstable. | |
| bool HasMultipleDwminitWarningsInLastMinute() { | |
| const WCHAR* queryPath = L"Application"; | |
| const WCHAR* query = | |
| L"*[System[Provider[@Name='Dwminit'] and (Level=3) and " | |
| L"TimeCreated[timediff(@SystemTime) <= 60000]]]"; | |
| EVT_HANDLE queryHandle = EvtQuery(nullptr, // Local machine | |
| queryPath, // Application log | |
| query, EvtQueryChannelPath); | |
| if (!queryHandle) { | |
| Wh_Log(L"EvtQuery failed with error: %u", GetLastError()); | |
| return false; | |
| } | |
| EVT_HANDLE events[2] = {}; | |
| DWORD returned = 0; | |
| constexpr DWORD kTimeout = 1000; | |
| BOOL ok = | |
| EvtNext(queryHandle, ARRAYSIZE(events), events, kTimeout, 0, &returned); | |
| if (!ok && GetLastError() != ERROR_NO_MORE_ITEMS) { | |
| Wh_Log(L"EvtNext failed with error: %u", GetLastError()); | |
| } | |
| for (DWORD i = 0; i < returned; i++) { | |
| EvtClose(events[i]); | |
| } | |
| EvtClose(queryHandle); | |
| return ok && returned >= ARRAYSIZE(events); | |
| } | |
| BOOL Wh_ModInit() { | |
| Wh_Log(L">"); | |
| // Skip the event log query unless the previous init is recent. | |
| FILETIME nowFt; | |
| GetSystemTimeAsFileTime(&nowFt); | |
| ULONGLONG now = | |
| ((ULONGLONG)nowFt.dwHighDateTime << 32) | nowFt.dwLowDateTime; | |
| ULONGLONG lastInitTime = 0; | |
| Wh_GetBinaryValue(L"lastInitTime", &lastInitTime, sizeof(lastInitTime)); | |
| Wh_SetBinaryValue(L"lastInitTime", &now, sizeof(now)); | |
| constexpr ULONGLONG kOneMinute = 60 * 10000000ULL; | |
| if (now - lastInitTime <= kOneMinute && | |
| HasMultipleDwminitWarningsInLastMinute()) { | |
| Wh_Log(L"Refusing to load: multiple recent Dwminit warnings"); | |
| return FALSE; | |
| } | |
| HMODULE udwm = GetModuleHandle(L"udwm.dll"); | |
| if (!udwm) { | |
| Wh_Log(L"udwm.dll isn't loaded"); | |
| return FALSE; | |
| } | |
| // Call tree for corner radius in each version: | |
| // | |
| // Old builds (e.g. 10.0.22621.6199): | |
| // UpdateWindowVisuals | |
| // -> GetEffectiveCornerStyle (inlined radius mapping, 8.0/4.0) | |
| // -> SetBorderParameters (receives radius as param) | |
| // CTopLevelWindow3D::UpdateAnimatedResources | |
| // -> GetRadiusFromCornerStyle (DPI scaling inlined) | |
| // -> ResourceHelper::CreateRectangleGeometry | |
| // | |
| // New builds (e.g. 10.0.26100.7920): | |
| // UpdateWindowVisuals | |
| // -> GetFloatCornerRadiusForCurrentStyle | |
| // -> GetRadiusFromCornerStyle | |
| // -> SetBorderParameters (receives radius as param) | |
| // CTopLevelWindow3D::UpdateAnimatedResources | |
| // -> GetDpiAdjustedFloatCornerRadius | |
| // -> GetRadiusFromCornerStyle | |
| // -> ResourceHelper::CreateRectangleGeometry | |
| // | |
| // In new builds, the squaring of maximized and snapped windows sits in | |
| // GetFloatCornerRadiusForCurrentStyle, which returns zero under the same | |
| // condition IsMaximizedOrSnapped tests, without consulting the corner style | |
| // at all. The animation path isn't squared, so restoring the rounding only | |
| // takes replacing that zero. | |
| // | |
| // Per-corner radii are applied one level down, where the single radius is | |
| // turned into geometry: | |
| // SetBorderParameters | |
| // -> CreateAndAttachBorderBrush | |
| // -> CCachedBorderBrush::CreateBorderSurface (D2D drawing, cached) | |
| // -> SetBorderRect (also called on resize) | |
| // -> SetClipRectangle | |
评论
?
参与讨论