Compare commits

...

1 Commits

Author SHA1 Message Date
Clint Rutkas
49b3795f1a [ZoomIt] Theme-aware system-tray menu + reusable helper
Makes ZoomIt's system-tray context menu follow the OS light/dark theme,
including live theme switches, and factors the logic into a small reusable
helper that any PowerToys system-tray utility can adopt.

src/common/Themes/dark_menu.h (header-only):
  - SetAppMode(bool dark): render this process's popup menus dark or light.
  - IsSystemDarkMode(): current app theme via the documented AppsUseLightTheme
    value, read fresh so a live light<->dark switch is reflected.
Drop-in for any Win32 tray utility:
  theme::dark_menu::SetAppMode(theme::dark_menu::IsSystemDarkMode());
  TrackPopupMenu(menu, ...);

ZoomIt (first adopter): the tray menu now follows Light/Dark and updates live
on a theme switch without restarting. The OS renders the real themed menu, so
native keyboard, accessibility, checkmarks, separators and DPI are preserved --
only the colors change. The standalone (non-PowerToys) build is unchanged,
under the existing __ZOOMIT_POWERTOYS__ guard.

Testing: MSBuild src\modules\ZoomIt\ZoomIt\ZoomIt.vcxproj /p:Configuration=Release
/p:Platform=x64 /m -- 0 warnings, 0 errors. Verified Light, Dark, and a live
theme switch on Windows 11.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-19 16:51:19 -07:00
3 changed files with 115 additions and 1 deletions

View File

@@ -30,6 +30,7 @@
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="dark_menu.h" />
<ClInclude Include="icon_helpers.h" />
<ClInclude Include="theme_listener.h" />
<ClInclude Include="theme_helpers.h" />

View File

@@ -0,0 +1,97 @@
// Theme-aware rendering for classic Win32 popup menus (HMENU / TrackPopupMenu).
//
// Lets a Win32 tray/popup menu follow the OS light/dark theme. It puts the
// process into the matching app theme via uxtheme's preferred-app-mode entry
// points -- the same mechanism the OS uses to render dark menus, as already
// used by ZoomIt and File Explorer -- and then the system draws the real
// themed menu. Native keyboard, accessibility, checkmarks, separators and DPI
// are all preserved; only the colors change.
//
// Theme detection reads the documented AppsUseLightTheme value, fresh on each
// call, so a live light<->dark switch is reflected without restarting.
//
// Drop-in for any PowerToys system-tray utility:
//
// theme::dark_menu::SetAppMode(theme::dark_menu::IsSystemDarkMode());
// TrackPopupMenu(menu, ...);
#pragma once
#include <windows.h>
namespace theme::dark_menu
{
namespace details
{
// uxtheme preferred-app-mode values (order matters).
enum class PreferredAppMode
{
Default = 0,
AllowDark,
ForceDark,
ForceLight,
Max
};
using SetPreferredAppModeFn = PreferredAppMode(WINAPI*)(PreferredAppMode);
using FlushMenuThemesFn = void(WINAPI*)();
struct Ordinals
{
SetPreferredAppModeFn setPreferredAppMode = nullptr;
FlushMenuThemesFn flushMenuThemes = nullptr;
};
// Resolved once per process: an inline function's local static is a single
// shared instance across all translation units that include this header.
inline const Ordinals& GetOrdinals() noexcept
{
static const Ordinals ordinals = []() noexcept {
Ordinals result{};
HMODULE uxtheme = GetModuleHandleW(L"uxtheme.dll");
if (uxtheme == nullptr)
{
uxtheme = LoadLibraryExW(L"uxtheme.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
}
if (uxtheme != nullptr)
{
// Ordinal 135 = SetPreferredAppMode, ordinal 136 = FlushMenuThemes.
result.setPreferredAppMode = reinterpret_cast<SetPreferredAppModeFn>(
GetProcAddress(uxtheme, MAKEINTRESOURCEA(135)));
result.flushMenuThemes = reinterpret_cast<FlushMenuThemesFn>(
GetProcAddress(uxtheme, MAKEINTRESOURCEA(136)));
}
return result;
}();
return ordinals;
}
}
// True if the user's app theme is dark, via the documented AppsUseLightTheme value.
inline bool IsSystemDarkMode() noexcept
{
DWORD value = 1; // default to light if the value is missing
DWORD size = sizeof(value);
RegGetValueW(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &size);
return value == 0;
}
// Make this process's classic popup menus render dark or light. Cheap and
// idempotent -- call right before TrackPopupMenu so the menu always matches
// the current theme, including after a live theme switch. No-op if those
// entry points are unavailable.
inline void SetAppMode(bool dark) noexcept
{
const details::Ordinals& ordinals = details::GetOrdinals();
if (ordinals.setPreferredAppMode != nullptr)
{
ordinals.setPreferredAppMode(dark ? details::PreferredAppMode::ForceDark
: details::PreferredAppMode::ForceLight);
}
if (ordinals.flushMenuThemes != nullptr)
{
ordinals.flushMenuThemes();
}
}
}

View File

@@ -34,6 +34,7 @@
#include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
#include <common/utils/gpo.h>
#include <common/Themes/dark_menu.h>
#include <array>
#include <vector>
#endif // __ZOOMIT_POWERTOYS__
@@ -10163,8 +10164,23 @@ LRESULT APIENTRY MainWndProc(
InsertMenu( hPopupMenu, 0, MF_BYPOSITION|MF_SEPARATOR, 0, NULL );
InsertMenu( hPopupMenu, 0, MF_BYPOSITION, IDC_OPTIONS, L"&Options" );
}
// Apply dark mode theme to the menu
// Make the popup theme-aware (dark/light).
#ifdef __ZOOMIT_POWERTOYS__
// Shared common helper: the OS renders the dark/light menu (native
// chrome, keyboard, accessibility). Reusable by other Win32 modules
// such as the runner tray menu.
//
// Detect the theme fresh at open time (respecting ZoomIt's theme
// override) so a live light<->dark switch is reflected without a
// restart. IsDarkModeEnabled() uses uxtheme's cached state, which a
// runtime theme switch may not refresh, whereas the AppsUseLightTheme
// registry value (read by IsSystemDarkMode) is always current.
const bool useDarkMenu = ( g_ThemeOverride == 1 ) ||
( g_ThemeOverride != 0 && theme::dark_menu::IsSystemDarkMode() );
theme::dark_menu::SetAppMode( useDarkMenu );
#else
ApplyDarkModeToMenu( hPopupMenu );
#endif
TrackPopupMenu( hPopupMenu, 0, pt.x , pt.y, 0, hWnd, NULL );
DestroyMenu( hPopupMenu );
break;