From e8ff12a83a2aed251b052d9d0eb74b478566b44b Mon Sep 17 00:00:00 2001 From: hy Date: Fri, 27 Feb 2026 21:59:44 +0900 Subject: [PATCH 1/2] fix: prevent file watcher handle leaks --- include/file_watcher.h | 21 ++++- include/unity_focus_detector.h | 6 +- src/file_watcher.cpp | 158 ++++++++++++++++++++------------- src/wakatime_client.cpp | 12 ++- 4 files changed, 128 insertions(+), 69 deletions(-) diff --git a/include/file_watcher.h b/include/file_watcher.h index e40fe85..55e3140 100644 --- a/include/file_watcher.h +++ b/include/file_watcher.h @@ -19,16 +19,33 @@ class FileWatcher { OVERLAPPED overlapped; // 비동기 I/O용 구조체 char buffer[4096]; // 변경 정보를 받을 버퍼 HANDLE stopEvent; + HANDLE ioEvent; - WatchedProject() : shouldStop(false) { + WatchedProject() : + directoryHandle(INVALID_HANDLE_VALUE), + shouldStop(false), + stopEvent(nullptr), + ioEvent(nullptr) + { stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + ioEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); ZeroMemory(&overlapped, sizeof(OVERLAPPED)); + overlapped.hEvent = ioEvent; ZeroMemory(buffer, sizeof(buffer)); } ~WatchedProject() { - if (stopEvent != INVALID_HANDLE_VALUE) { + if (stopEvent != nullptr) { CloseHandle(stopEvent); + stopEvent = nullptr; + } + if (ioEvent != nullptr) { + CloseHandle(ioEvent); + ioEvent = nullptr; + } + if (directoryHandle != nullptr && directoryHandle != INVALID_HANDLE_VALUE) { + CloseHandle(directoryHandle); + directoryHandle = INVALID_HANDLE_VALUE; } } }; diff --git a/include/unity_focus_detector.h b/include/unity_focus_detector.h index 0d3c15b..1935ad8 100644 --- a/include/unity_focus_detector.h +++ b/include/unity_focus_detector.h @@ -5,7 +5,7 @@ class UnityFocusDetector { private: bool isUnityFocused = false; std::chrono::steady_clock::time_point lastHeartbeat; - std::chrono::seconds heartbeatInterval{120}; // 5분 + std::chrono::seconds heartbeatInterval{120}; // 2분 std::function focusCallback; std::function unfocusCallback; @@ -18,7 +18,7 @@ class UnityFocusDetector { void CheckFocused(); /** - * 5분마다 호출 + * 2분마다 호출 */ void SendPeriodicHeartbeat(); @@ -41,4 +41,4 @@ class UnityFocusDetector { * @param callback 이전 하트비트 보낼때 호출될 함수 */ void SetPeriodicHeartbeatCallback(std::function callback); -}; \ No newline at end of file +}; diff --git a/src/file_watcher.cpp b/src/file_watcher.cpp index d95cff0..b93e5d2 100644 --- a/src/file_watcher.cpp +++ b/src/file_watcher.cpp @@ -1,5 +1,6 @@ #include "file_watcher.h" #include +#include FileWatcher::FileWatcher() { @@ -37,6 +38,12 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin // 새로운 WatchedProject 생성 auto project = std::make_unique(); + if (project->stopEvent == nullptr || project->ioEvent == nullptr) + { + std::cerr << "[FileWatcher] Failed to create watcher events for: " << projectPath << std::endl; + return false; + } + project->projectPath = projectPath; project->projectName = projectName; project->unityVersion = unityVersion; @@ -44,6 +51,7 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin // ZeroMemory: 메모리를 0으로 초기화 (Windows API 함수) ZeroMemory(&project->overlapped, sizeof(OVERLAPPED)); + project->overlapped.hEvent = project->ioEvent; ZeroMemory(project->buffer, sizeof(project->buffer)); // CreateFile: 디렉토리 핸들 열기 @@ -72,7 +80,16 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin // 감시 스레드 시작 WatchedProject *projectPtr = project.get(); - project->watchThread = std::thread(&FileWatcher::WatchProjectThread, this, projectPtr); + try + { + project->watchThread = std::thread(&FileWatcher::WatchProjectThread, this, projectPtr); + } + catch (const std::system_error &e) + { + std::cerr << "[FileWatcher] Failed to start watch thread for " << projectName << ": " << e.what() << std::endl; + return false; + } + watchedProjects.push_back(std::move(project)); return true; @@ -80,11 +97,24 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin void FileWatcher::WatchProjectThread(WatchedProject *project) { + if (project == nullptr || + project->directoryHandle == nullptr || + project->directoryHandle == INVALID_HANDLE_VALUE || + project->stopEvent == nullptr || + project->ioEvent == nullptr) + { + std::cerr << "[FileWatcher] Invalid watch project state, thread exit" << std::endl; + return; + } + std::cout << "[FileWatcher] Watch thread started for: " << project->projectName << std::endl; while (!project->shouldStop) { DWORD bytesReturned = 0; + ZeroMemory(&project->overlapped, sizeof(OVERLAPPED)); + project->overlapped.hEvent = project->ioEvent; + ResetEvent(project->ioEvent); // ReadDirectoryChangesW: 디렉토리 변경사항을 감지하는 핵심 함수 // TRUE: 하위 폴더도 감시 @@ -107,9 +137,18 @@ void FileWatcher::WatchProjectThread(WatchedProject *project) break; } } + else + { + // 드물게 동기 완료될 수 있으므로 즉시 처리 + if (bytesReturned > 0) + { + ProcessFileChanges(project->buffer, bytesReturned, project); + } + continue; + } const HANDLE waitHandles[2] = { - project->directoryHandle, // I/O 완료 대기 + project->ioEvent, // I/O 완료 대기 project->stopEvent // 종료 신호 대기 }; @@ -123,7 +162,7 @@ void FileWatcher::WatchProjectThread(WatchedProject *project) switch (waitResult) { - case WAIT_OBJECT_0: // directoryHandle 신호 (I/O 완료) + case WAIT_OBJECT_0: // ioEvent 신호 (I/O 완료) { // GetOverlappedResult로 결과 확인 if (GetOverlappedResult(project->directoryHandle, &project->overlapped, &bytesReturned, FALSE)) @@ -133,11 +172,14 @@ void FileWatcher::WatchProjectThread(WatchedProject *project) } else { - if (const DWORD error = GetLastError(); error != ERROR_OPERATION_ABORTED) + if (const DWORD error = GetLastError(); error != ERROR_OPERATION_ABORTED && error != ERROR_IO_INCOMPLETE) { std::cerr << "[FileWatcher] GetOverlappedResult failed for " << project->projectName << " (Error: " << error << ")" << std::endl; } - break; + if (project->shouldStop) + { + goto thread_exit; + } } break; } @@ -158,8 +200,6 @@ void FileWatcher::WatchProjectThread(WatchedProject *project) std::cerr << "[FileWatcher] WaitForMultipleObjects failed for " << project->projectName << " (Error: " << GetLastError() << ")" << std::endl; goto thread_exit; } - - ZeroMemory(&project->overlapped, sizeof(OVERLAPPED)); } thread_exit: @@ -260,84 +300,76 @@ bool FileWatcher::ShouldIgnoreFolder(const std::string &folderName) const void FileWatcher::StopWatching(const std::string &projectPath) { - std::lock_guard lock(projectsMutex); + std::unique_ptr projectToStop; + + { + std::lock_guard lock(projectsMutex); + + const auto it = std::find_if(watchedProjects.begin(), watchedProjects.end(), + [&projectPath](const std::unique_ptr &project) + { + return project->projectPath == projectPath; + }); - const auto it = std::remove_if(watchedProjects.begin(), watchedProjects.end(), - [&projectPath](const std::unique_ptr &project) - { - if (project->projectPath == projectPath) - { - std::cout << "[FileWatcher] Stopping watch for: " << project->projectName << std::endl; + if (it == watchedProjects.end()) + { + return; + } - project->shouldStop = true; + projectToStop = std::move(*it); + watchedProjects.erase(it); + } - SetEvent(project->stopEvent); - CancelIo(project->directoryHandle); // I/O 작업 취소 + std::cout << "[FileWatcher] Stopping watch for: " << projectToStop->projectName << std::endl; + projectToStop->shouldStop = true; - // 스레드 종료 대기 - if (project->watchThread.joinable()) - { - const auto future = std::async(std::launch::async, [&project]() - { - project->watchThread.join(); - }); - - // 5초 타임아웃으로 join 대기 - if (future.wait_for(std::chrono::seconds(5)) == std::future_status::timeout) - { - std::cout << "[FileWatcher] Thread join timeout, forcing termination" << std::endl; - project->watchThread.detach(); // 강제 종료 - } - } - - CloseHandle(project->directoryHandle); - return true; - } - return false; - }); - - watchedProjects.erase(it, watchedProjects.end()); + if (projectToStop->stopEvent != nullptr) + { + SetEvent(projectToStop->stopEvent); + } + if (projectToStop->directoryHandle != nullptr && projectToStop->directoryHandle != INVALID_HANDLE_VALUE) + { + CancelIoEx(projectToStop->directoryHandle, nullptr); + } + if (projectToStop->watchThread.joinable()) + { + projectToStop->watchThread.join(); + } } void FileWatcher::StopAllWatching() { - std::lock_guard lock(projectsMutex); + std::vector> projectsToStop; + + { + std::lock_guard lock(projectsMutex); + projectsToStop.swap(watchedProjects); + } std::cout << "[FileWatcher] Stopping all watches..." << std::endl; - for (const auto &project: watchedProjects) + for (const auto &project: projectsToStop) { project->shouldStop = true; - SetEvent(project->stopEvent); - CancelIo(project->directoryHandle); + if (project->stopEvent != nullptr) + { + SetEvent(project->stopEvent); + } + if (project->directoryHandle != nullptr && project->directoryHandle != INVALID_HANDLE_VALUE) + { + CancelIoEx(project->directoryHandle, nullptr); + } } // 모든 스레드 종료 대기 - int joinedCount = 0; - for (auto &project: watchedProjects) + for (auto &project: projectsToStop) { if (project->watchThread.joinable()) { - auto future = std::async(std::launch::async, [&project]() - { - project->watchThread.join(); - }); - - if (future.wait_for(std::chrono::seconds(3)) == std::future_status::ready) - { - joinedCount++; - } - else - { - std::cout << "[FileWatcher] Thread join timeout: " << project->projectName << std::endl; - project->watchThread.detach(); - } + project->watchThread.join(); } - - CloseHandle(project->directoryHandle); } - watchedProjects.clear(); std::cout << "[FileWatcher] All watches stopped" << std::endl; } diff --git a/src/wakatime_client.cpp b/src/wakatime_client.cpp index 3c2e280..b65f5fd 100644 --- a/src/wakatime_client.cpp +++ b/src/wakatime_client.cpp @@ -1,4 +1,9 @@ -#include "wakatime_client.h" +#include "wakatime_client.h" + +namespace +{ + constexpr size_t kMaxHeartbeatQueueSize = 1024; +} WakaTimeClient::WakaTimeClient() : hSession(nullptr), initialized(false), @@ -429,6 +434,10 @@ void WakaTimeClient::SendHeartbeat(const std::string &filePath, const std::strin // 큐에 추가 (비동기 전송) { std::lock_guard lock(queueMutex); + while (heartbeatQueue.size() >= kMaxHeartbeatQueueSize) + { + heartbeatQueue.pop(); // 메모리 폭주 방지를 위해 가장 오래된 heartbeat 제거 + } heartbeatQueue.push(heartbeat); } } @@ -529,3 +538,4 @@ void WakaTimeClient::FlushQueue() std::cout << "[WakaTimeClient] Queue flushed" << std::endl; } + From fb75797db6b28258cd9d07583093d1b44550a0f9 Mon Sep 17 00:00:00 2001 From: hy Date: Fri, 27 Feb 2026 22:28:47 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20ui=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 ++ include/windows_dark_mode.h | 18 ++++++ main.cpp | 4 ++ src/tray_icon.cpp | 115 +++++++++--------------------------- src/windows_dark_mode.cpp | 101 +++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 88 deletions(-) create mode 100644 include/windows_dark_mode.h create mode 100644 src/windows_dark_mode.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fd10ae5..00330b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ set(SOURCES src/file_watcher.cpp src/wakatime_client.cpp src/tray_icon.cpp + src/windows_dark_mode.cpp src/unity_focus_detector.cpp ) @@ -21,6 +22,7 @@ set(HEADERS include/file_watcher.h include/wakatime_client.h include/tray_icon.h + include/windows_dark_mode.h include/unity_focus_detector.h ) @@ -52,6 +54,7 @@ if(WIN32) ole32 oleaut32 windowscodecs + dwmapi user32 ) else() @@ -64,6 +67,7 @@ if(WIN32) ole32 oleaut32 windowscodecs + dwmapi user32 ) endif() diff --git a/include/windows_dark_mode.h b/include/windows_dark_mode.h new file mode 100644 index 0000000..90242c4 --- /dev/null +++ b/include/windows_dark_mode.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace WindowsDarkMode +{ + /** + * Enable dark-mode preference for Win32 menus on supported Windows versions. + * Returns true when the API path is available and invoked. + */ + bool EnableForApp(); + + /** + * Apply dark-mode attributes to a specific window when supported. + * Safe to call on unsupported versions (no-op fallback). + */ + void ApplyToWindow(HWND hwnd); +} diff --git a/main.cpp b/main.cpp index 2fe772a..d1c813c 100644 --- a/main.cpp +++ b/main.cpp @@ -4,6 +4,7 @@ #include "wakatime_client.h" #include "tray_icon.h" #include "unity_focus_detector.h" +#include "windows_dark_mode.h" WakaTimeClient *g_wakatimeClient = nullptr; FileWatcher *g_fileWatcher = nullptr; @@ -243,6 +244,9 @@ void InitialUnityProjectScan() int main() { std::cout << "[Main] Unity WakaTime Monitor Starting..." << std::endl; + const bool darkModeAvailable = WindowsDarkMode::EnableForApp(); + std::cout << "[Main] Dark mode menu opt-in: " + << (darkModeAvailable ? "enabled" : "not available") << std::endl; TrayIcon trayIcon; g_trayIcon = &trayIcon; diff --git a/src/tray_icon.cpp b/src/tray_icon.cpp index 7172f04..3e57333 100644 --- a/src/tray_icon.cpp +++ b/src/tray_icon.cpp @@ -1,6 +1,7 @@ -#include "tray_icon.h" +#include "tray_icon.h" #include "file_watcher.h" #include "wakatime_client.h" +#include "windows_dark_mode.h" #include #include @@ -101,6 +102,8 @@ bool TrayIcon::CreateHiddenWindow() return false; } + WindowsDarkMode::ApplyToWindow(hwnd); + std::cout << "[TrayIcon] Hidden window created" << std::endl; return true; } @@ -317,17 +320,12 @@ HMENU TrayIcon::CreateContextMenu() const HMENU menu = CreatePopupMenu(); HMENU statusSubMenu = CreateStatusSubMenu(); - AppendMenuW(menu, MF_STRING | MF_POPUP, (UINT_PTR) statusSubMenu, L"📊 Status"); + AppendMenuW(menu, MF_STRING | MF_POPUP, (UINT_PTR) statusSubMenu, L"Status"); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); AppendMenuW(menu, MF_STRING, IDM_TOGGLE_MONITORING, L"Pause Monitoring"); - AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); - - AppendMenuW(menu, MF_STRING, IDM_OPEN_DASHBOARD, L"Open WakaTime Dashboard"); - AppendMenuW(menu, MF_STRING, IDM_SETTINGS, L"🔑 Setup API Key"); - AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); - - AppendMenuW(menu, MF_STRING, IDM_GITHUB, L"ℹ️ Unity WakaTime v1.0.2"); + AppendMenuW(menu, MF_STRING, IDM_OPEN_DASHBOARD, L"Open Dashboard"); + AppendMenuW(menu, MF_STRING, IDM_SETTINGS, L"Setup API Key"); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); AppendMenuW(menu, MF_STRING, IDM_EXIT, L"Exit"); @@ -340,96 +338,37 @@ HMENU TrayIcon::CreateStatusSubMenu() { const HMENU subMenu = CreatePopupMenu(); - // === API Key 상태 === - if (Globals::GetWakaTimeClient()) - { - std::string maskedKey = Globals::GetWakaTimeClient()->GetMaskedApiKey(); - const std::wstring apiKeyInfo = L"🔑 API Key: " + std::wstring(maskedKey.begin(), maskedKey.end()); - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, apiKeyInfo.c_str()); - } - else - { - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, L"🔑 API Key: Not configured"); - } - - AppendMenuW(subMenu, MF_SEPARATOR, 0, nullptr); - - // === 모니터링 상태 === - const std::wstring monitoringStatus = isMonitoring ? L"✅ Monitoring: Active" : L"⏸️ Monitoring: Paused"; + // Monitoring state + const std::wstring monitoringStatus = isMonitoring ? L"Monitoring: Active" : L"Monitoring: Paused"; AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, monitoringStatus.c_str()); - // === 현재 프로젝트 === + // Current project if (!currentProject.empty()) { - const std::wstring projectInfo = L"🎮 Current: " + std::wstring(currentProject.begin(), currentProject.end()); + const std::wstring projectInfo = L"Current Project: " + std::wstring(currentProject.begin(), currentProject.end()); AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, projectInfo.c_str()); } else { - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, L"🎮 No Unity project detected"); + AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, L"No Unity project detected"); } - // === Heartbeat 통계 === - const std::wstring heartbeatInfo = L"💓 Total Heartbeats: " + std::to_wstring(totalHeartbeats); - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, heartbeatInfo.c_str()); - AppendMenuW(subMenu, MF_SEPARATOR, 0, nullptr); - - // === WakaTime 통계 === - if (Globals::GetWakaTimeClient()) + // Heartbeat summary + int sent = 0; + int failed = 0; + if (const auto *client = Globals::GetWakaTimeClient()) { - int sent, failed; - Globals::GetWakaTimeClient()->GetStats(sent, failed); - - const std::wstring sentInfo = L"📤 Sent: " + std::to_wstring(sent); - const std::wstring failedInfo = L"❌ Failed: " + std::to_wstring(failed); - - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, sentInfo.c_str()); - if (failed > 0) - { - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, failedInfo.c_str()); - } - - // 성공률 계산 - if (sent + failed > 0) - { - const int successRate = (sent * 100) / (sent + failed); - const std::wstring rateInfo = L"📊 Success Rate: " + std::to_wstring(successRate) + L"%"; - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, rateInfo.c_str()); - } + client->GetStats(sent, failed); } - else - { - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, L"⚠️ WakaTime client not initialized"); - } - - AppendMenuW(subMenu, MF_SEPARATOR, 0, nullptr); - // === 파일 감시 상태 === - if (Globals::GetFileWatcher()) - { - const size_t watchedCount = Globals::GetFileWatcher()->GetWatchedProjectCount(); - const std::wstring watchInfo = L"👁️ Watching: " + std::to_wstring(watchedCount) + L" projects"; - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, watchInfo.c_str()); - - // 감시 중인 프로젝트 목록 (최대 3개) - const auto& watchedProjects = Globals::GetFileWatcher()->GetWatchedProjects(); - for (size_t i = 0; i < std::min((size_t) 3, watchedProjects.size()); i++) - { - const auto& projectName = watchedProjects[i].projectName; - std::wstring projectItem = L" 📁 " + std::wstring(projectName.begin(), projectName.end()); - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, projectItem.c_str()); - } - - if (watchedProjects.size() > 3) - { - const std::wstring moreInfo = L" ... and " + std::to_wstring(watchedProjects.size() - 3) + L" more"; - AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, moreInfo.c_str()); - } - } + std::wstring heartbeatInfo = L"Heartbeats: " + std::to_wstring(totalHeartbeats) + + L" (Sent: " + std::to_wstring(sent) + + L", Failed: " + std::to_wstring(failed) + L")"; + AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, heartbeatInfo.c_str()); AppendMenuW(subMenu, MF_SEPARATOR, 0, nullptr); - // === 액션 항목들 === - AppendMenuW(subMenu, MF_STRING, IDM_SHOW_STATUS, L"🔄 Refresh Status"); + // Actions + AppendMenuW(subMenu, MF_STRING, IDM_SHOW_STATUS, L"Refresh Status"); return subMenu; } @@ -449,7 +388,7 @@ void TrayIcon::UpdateContextMenu() // 새로운 서브메뉴 생성 및 삽입 HMENU newStatusSubMenu = CreateStatusSubMenu(); InsertMenuW(hMenu, 0, MF_BYPOSITION | MF_STRING | MF_POPUP, - (UINT_PTR)newStatusSubMenu, L"📊 Status"); + (UINT_PTR)newStatusSubMenu, L"Status"); } void TrayIcon::OpenGitHubRepository() { @@ -598,7 +537,7 @@ std::string TrayIcon::ShowApiKeyInputDialog() const std::wstring wMessage(message.begin(), message.end()); - if (const int result = MessageBoxW(hwnd, wMessage.c_str(), L"🔑 WakaTime API Key Setup", + if (const int result = MessageBoxW(hwnd, wMessage.c_str(), L"WakaTime API Key Setup", MB_OKCANCEL | MB_ICONINFORMATION | MB_TOPMOST); result == IDOK) { if (const std::string clipboardText = GetClipboardText(); !clipboardText.empty()) @@ -607,13 +546,13 @@ std::string TrayIcon::ShowApiKeyInputDialog() } else { - std::wstring retryMessage = L"❌ No valid API key found in clipboard!\n\n"; + std::wstring retryMessage = L"No valid API key found in clipboard.\n\n"; retryMessage += L"Please:\n"; retryMessage += L"1. Go to the opened WakaTime page\n"; retryMessage += L"2. Copy your API key\n"; retryMessage += L"3. Try again from the tray menu\n\n"; - MessageBoxW(hwnd, retryMessage.c_str(), L"⚠️ API Key Not Found", + MessageBoxW(hwnd, retryMessage.c_str(), L"API Key Not Found", MB_OK | MB_ICONWARNING | MB_TOPMOST); } } diff --git a/src/windows_dark_mode.cpp b/src/windows_dark_mode.cpp new file mode 100644 index 0000000..105c152 --- /dev/null +++ b/src/windows_dark_mode.cpp @@ -0,0 +1,101 @@ +#include "windows_dark_mode.h" + +#include + +namespace +{ + enum class PreferredAppMode + { + Default, + AllowDark, + ForceDark, + ForceLight, + Max + }; + + using AllowDarkModeForWindowFn = BOOL(WINAPI *)(HWND, BOOL); + using SetPreferredAppModeFn = PreferredAppMode(WINAPI *)(PreferredAppMode); + using FlushMenuThemesFn = void(WINAPI *)(); + + struct DarkModeApi + { + HMODULE uxTheme = nullptr; + AllowDarkModeForWindowFn allowDarkModeForWindow = nullptr; + SetPreferredAppModeFn setPreferredAppMode = nullptr; + FlushMenuThemesFn flushMenuThemes = nullptr; + bool loaded = false; + }; + + DarkModeApi &GetDarkModeApi() + { + static DarkModeApi api; + if (api.loaded) + { + return api; + } + + api.loaded = true; + api.uxTheme = LoadLibraryW(L"uxtheme.dll"); + if (!api.uxTheme) + { + return api; + } + + api.allowDarkModeForWindow = reinterpret_cast( + GetProcAddress(api.uxTheme, MAKEINTRESOURCEA(133))); + api.setPreferredAppMode = reinterpret_cast( + GetProcAddress(api.uxTheme, MAKEINTRESOURCEA(135))); + api.flushMenuThemes = reinterpret_cast( + GetProcAddress(api.uxTheme, MAKEINTRESOURCEA(136))); + + return api; + } +} + +bool WindowsDarkMode::EnableForApp() +{ + auto &api = GetDarkModeApi(); + if (!api.setPreferredAppMode) + { + return false; + } + + api.setPreferredAppMode(PreferredAppMode::AllowDark); + if (api.flushMenuThemes) + { + api.flushMenuThemes(); + } + + return true; +} + +void WindowsDarkMode::ApplyToWindow(const HWND hwnd) +{ + if (!hwnd) + { + return; + } + + auto &api = GetDarkModeApi(); + if (api.allowDarkModeForWindow) + { + api.allowDarkModeForWindow(hwnd, TRUE); + } + + BOOL useDark = TRUE; + + constexpr DWORD kDwmwaUseImmersiveDarkMode = 20; + HRESULT hr = DwmSetWindowAttribute(hwnd, + kDwmwaUseImmersiveDarkMode, + &useDark, + sizeof(useDark)); + if (FAILED(hr)) + { + // Older Windows 10 builds used attribute id 19 for immersive dark mode. + constexpr DWORD kDwmwaUseImmersiveDarkModeLegacy = 19; + DwmSetWindowAttribute(hwnd, + kDwmwaUseImmersiveDarkModeLegacy, + &useDark, + sizeof(useDark)); + } +}