diff --git a/build/VisualStudio/Notepad4.vcxproj b/build/VisualStudio/Notepad4.vcxproj index 3bb3c4aadd..e7c3e0c85f 100644 --- a/build/VisualStudio/Notepad4.vcxproj +++ b/build/VisualStudio/Notepad4.vcxproj @@ -1,4 +1,4 @@ - + @@ -157,8 +157,8 @@ - ..\..\scintilla\include;..\..\scintilla\lexlib;..\..\scintilla\src;..\..\src;%(AdditionalIncludeDirectories) - _WINDOWS;NOMINMAX;WIN32_LEAN_AND_MEAN;STRICT_TYPED_ITEMIDS;BOOST_REGEX_STANDALONE;NO_CXX11_REGEX;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + ..\..\scintilla\include;..\..\scintilla\lexlib;..\..\scintilla\src;..\..\src;..\..\darkmodelib\include;..\..\darkmodelib\src;%(AdditionalIncludeDirectories) + _WINDOWS;NOMINMAX;WIN32_LEAN_AND_MEAN;STRICT_TYPED_ITEMIDS;BOOST_REGEX_STANDALONE;NO_CXX11_REGEX;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;_DARKMODELIB_NO_INI_CONFIG;%(PreprocessorDefinitions) Level4 stdcpplatest stdc17 @@ -592,6 +592,16 @@ + + + + + + + + + + @@ -758,6 +768,8 @@ + + diff --git a/build/VisualStudio/Notepad4.vcxproj.filters b/build/VisualStudio/Notepad4.vcxproj.filters index 5421458b83..9aa9016ab3 100644 --- a/build/VisualStudio/Notepad4.vcxproj.filters +++ b/build/VisualStudio/Notepad4.vcxproj.filters @@ -34,6 +34,9 @@ {b1934aa4-ee5f-4fb6-a9f8-2b410b6611eb} + + {a1b2c3d4-e5f6-7890-abcd-ef0123456789} + @@ -435,6 +438,36 @@ Source Files + + Source Files + + + DarkModeLib + + + DarkModeLib + + + DarkModeLib + + + DarkModeLib + + + DarkModeLib + + + DarkModeLib + + + DarkModeLib + + + DarkModeLib + + + DarkModeLib + Source Files\EditLexers @@ -929,6 +962,12 @@ Header Files + + Header Files + + + DarkModeLib + Header Files diff --git a/build/mingw/notepad4.mk b/build/mingw/notepad4.mk index 0d91220dc1..b44d98c34f 100644 --- a/build/mingw/notepad4.mk +++ b/build/mingw/notepad4.mk @@ -6,19 +6,27 @@ OBJDIR = $(BINFOLDER)/obj/$(PROJ) SRCDIR = ../../src editlexers_dir = $(SRCDIR)/EditLexers scintilla_dir = ../../scintilla +darkmodelib_dir = ../../darkmodelib INCDIR = \ -I"../../src" \ -I"../../src/EditLexers" \ - -I"$(scintilla_dir)/include" + -I"$(scintilla_dir)/include" \ + -I"$(darkmodelib_dir)/include" \ + -I"$(darkmodelib_dir)/src" + +CPPFLAGS += -D_DARKMODELIB_NO_INI_CONFIG LDFLAGS += -L"$(BINFOLDER)/obj" -LDLIBS += -limm32 +LDLIBS += -limm32 -ldwmapi editlexers_src = $(wildcard $(editlexers_dir)/*.cpp) editlexers_obj = $(patsubst $(editlexers_dir)/%.cpp,$(OBJDIR)/%.obj,$(editlexers_src)) +darkmodelib_src = $(wildcard $(darkmodelib_dir)/src/*.cpp) +darkmodelib_obj = $(patsubst $(darkmodelib_dir)/src/%.cpp,$(OBJDIR)/dmlib_%.obj,$(darkmodelib_src)) + # c_src = $(wildcard $(SRCDIR)/*.c) # c_obj = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.obj,$(c_src)) @@ -30,12 +38,15 @@ rc_obj = $(patsubst $(SRCDIR)/%.rc,$(OBJDIR)/%.res,$(rc_src)) all: $(NAME) -$(NAME): $(editlexers_obj) $(cpp_obj) $(rc_obj) +$(NAME): $(editlexers_obj) $(cpp_obj) $(darkmodelib_obj) $(rc_obj) $(CXX) $^ $(LDFLAGS) -lscintilla $(LDLIBS) -o $@ $(editlexers_obj): $(OBJDIR)/%.obj: $(editlexers_dir)/%.cpp $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(INCDIR) $< -o $(OBJDIR)/$*.obj +$(darkmodelib_obj): $(OBJDIR)/dmlib_%.obj: $(darkmodelib_dir)/src/%.cpp + $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(INCDIR) $< -o $@ + # $(c_obj): $(OBJDIR)/%.obj: $(SRCDIR)/%.c # $(CC) -c $(CPPFLAGS) $(CFLAGS) $(INCDIR) $< -o $(OBJDIR)/$*.obj diff --git a/darkmodelib/include/Darkmodelib.h b/darkmodelib/include/Darkmodelib.h new file mode 100644 index 0000000000..ba99cd9ca8 --- /dev/null +++ b/darkmodelib/include/Darkmodelib.h @@ -0,0 +1,786 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + +// Based on the Notepad++ dark mode code licensed under GPLv3. +// Originally by adzm / Adam D. Walling, with modifications by the Notepad++ team. +// Heavily modified by ozone10 (Notepad++ contributor). +// Used with permission to relicense under the Mozilla Public License, v. 2.0. + + +#pragma once + +#include + +#if (NTDDI_VERSION >= NTDDI_VISTA) /*\ + && (defined(__x86_64__) || defined(_M_X64)\ + || defined(__arm64__) || defined(__arm64) || defined(_M_ARM64))*/ + +#ifdef _MSC_VER +#pragma comment(lib, "dwmapi.lib") +#pragma comment(lib, "uxtheme.lib") +#pragma comment(lib, "Comctl32.lib") +#pragma comment(lib, "Gdi32.lib") +#pragma comment(lib, "Shlwapi.lib") +#endif + +#ifdef DMLIB_DLL + #if defined(DMLIB_EXPORTS) + #define DMLIB_API __declspec(dllexport) + #else + #define DMLIB_API __declspec(dllimport) + #endif +#else + #define DMLIB_API +#endif + +#ifdef __clang__ + #pragma clang diagnostic push + // identifier '_TASKDIALOGCONFIG' is reserved because it starts with '_' followed by a capital letter + #pragma clang diagnostic ignored "-Wreserved-identifier" +#endif + +typedef struct _TASKDIALOGCONFIG TASKDIALOGCONFIG; // NOLINT // forward declaration, from + +#ifdef __clang__ + #pragma clang diagnostic pop +#endif +/** + * @namespace dmlib + * @brief Provides dark mode theming, subclassing, and rendering utilities for most Win32 controls. + */ +namespace dmlib +{ + struct Colors + { + COLORREF background = 0; + COLORREF ctrlBackground = 0; + COLORREF hotBackground = 0; + COLORREF dlgBackground = 0; + COLORREF errorBackground = 0; + COLORREF text = 0; + COLORREF darkerText = 0; + COLORREF disabledText = 0; + COLORREF linkText = 0; + COLORREF edge = 0; + COLORREF hotEdge = 0; + COLORREF disabledEdge = 0; + COLORREF highlight = 0; + }; + + struct ColorsView + { + COLORREF background = 0; + COLORREF text = 0; + COLORREF gridlines = 0; + COLORREF headerBackground = 0; + COLORREF headerHotBackground = 0; + COLORREF headerText = 0; + COLORREF headerEdge = 0; + }; + + /** + * @brief Represents tooltip from different controls. + */ + enum class ToolTipsType : unsigned char + { + tooltip, ///< Standard tooltip control. + toolbar, ///< Tooltips associated with toolbar buttons. + listview, ///< Tooltips associated with list views. + treeview, ///< Tooltips associated with tree views. + tabbar, ///< Tooltips associated with tab controls. + trackbar, ///< Tooltips associated with trackbar (slider) controls. + rebar ///< Tooltips associated with rebar controls. + }; + + /** + * @brief Defines dark mode preset color tones. + * + * Used as preset to choose default colors in dark mode. + * Value `max` is reserved for internal range checking, + * do not use in application code. + */ + enum class ColorTone : unsigned char + { + black = 0, ///< Black + red = 1, ///< Red + green = 2, ///< Green + blue = 3, ///< Blue + purple = 4, ///< Purple + cyan = 5, ///< Cyan + olive = 6, ///< Olive + max = 7 ///< Don't use, for internal checks + }; + + /** + * @brief Defines the available visual styles for TreeView controls. + * + * Used to control theming behavior for TreeViews: + * - `classic`: Legacy style without theming. + * - `light`: Light mode appearance. + * - `dark`: Dark mode appearance. + * + * Set via configuration and used by style evaluators (e.g. @ref dmlib::calculateTreeViewStyle). + * + * @see dmlib::calculateTreeViewStyle() + */ + enum class TreeViewStyle : unsigned char + { + classic, ///< Non-themed legacy appearance. + light, ///< Light mode. + dark ///< Dark mode. + }; + + /** + * @brief Describes metadata fields and compile-time features of the dark mode library. + * + * Values of this enum are used with @ref dmlib::getLibInfo to retrieve version numbers and + * determine whether specific features were enabled during compilation. + * + * @see dmlib::getLibInfo() + */ + enum class LibInfo : unsigned char + { + featureCheck, ///< Returns maxValue to verify enum coverage. + verMajor, ///< Major version number of the library. + verMinor, ///< Minor version number of the library. + verRevision, ///< Revision/patch number of the library. + iniConfigUsed, ///< True if `.ini` file configuration is supported. + allowOldOS, ///< '1' if older Windows 10 versions are allowed, '2' if all older Windows are allowed. + useDlgProcCtl, ///< True if WM_CTLCOLORxxx can be handled directly in dialog procedure. + preferTheme, ///< True if theme is supported and can be used over subclass, e.g. combo box on Windows 10+. + useSBFix, ///< '1' if scroll bar fix is applied to all scroll bars, '2' if scroll bar fix can be limited to specific window. + maxValue ///< Sentinel value for internal validation (not intended for use). + }; + + /** + * @brief Defines the available dark mode types for manual configurations. + * + * Can be used in `dmlib::initDarkModeConfig` and in `dmlib::setDarkModeConfigEx` + * with static_cast(DarkModeType::'value'). + * + * @note Also used internally to distinguish between light, dark, and classic modes. + * + * @see dmlib::initDarkModeConfig() + * @see dmlib::setDarkModeConfigEx() + */ + enum class DarkModeType : unsigned char + { + light = 0, ///< Light mode appearance. + dark = 1, ///< Dark mode appearance. + classic = 3 ///< Classic (non-themed or system) appearance. + }; + +#ifdef __cplusplus + extern "C" { +#endif + + /** + * @brief Returns library version information or compile-time feature flags. + * + * @param libInfoType The type of information to query. + * @return Integer representing the requested value or feature flag. + * + * @see LibInfo + */ + [[nodiscard]] int getLibInfo(int libInfoType); + + // ======================================================================== + // Config + // ======================================================================== + + /** + * @brief Initializes the dark mode configuration based on the selected mode. + * + * For convenience @ref DarkModeType enums values can be used. + * + * @param dmType Configuration mode: + * - 0: Light mode + * - 1: Dark mode + * - 3: Classic mode + * + * @note Values 2 and 4 are reserved for internal use only. + * Using them can cause visual glitches. + */ + DMLIB_API void initDarkModeConfig(UINT dmType); + + /// Sets the preferred window corner style on Windows 11. (DWM_WINDOW_CORNER_PREFERENCE values) + DMLIB_API void setRoundCornerConfig(UINT roundCornerStyle); + + /// Sets the preferred border color for window edge on Windows 11. + DMLIB_API void setBorderColorConfig(COLORREF clr); + + // Sets the Mica effects on Windows 11 setting. (DWM_SYSTEMBACKDROP_TYPE values) + DMLIB_API void setMicaConfig(UINT mica); + + /// Sets Mica effects on the full window setting. + DMLIB_API void setMicaExtendedConfig(bool extendMica); + + /// Sets dialog colors on title bar on Windows 11 setting. + DMLIB_API void setColorizeTitleBarConfig(bool colorize); + + /// Applies dark mode settings based on the given configuration type. (DarkModeType values) + DMLIB_API void setDarkModeConfigEx(UINT dmType); + + /// Applies dark mode settings based on system mode preference. + DMLIB_API void setDarkModeConfig(); + + /// Initializes dark mode experimental features, colors, and other settings. + DMLIB_API void initDarkModeEx(const wchar_t* iniName); + + ///Initializes dark mode without INI settings. + DMLIB_API void initDarkMode(); + + /// Checks if there is config INI file. + [[nodiscard]] DMLIB_API bool doesConfigFileExist(); + + // ======================================================================== + // Basic checks + // ======================================================================== + + /// Checks if non-classic mode is enabled. + [[nodiscard]] DMLIB_API bool isEnabled(); + + /// Checks if experimental dark mode features are currently active. + [[nodiscard]] DMLIB_API bool isExperimentalActive(); + + /// Checks if experimental dark mode features are supported by the system. + [[nodiscard]] DMLIB_API bool isExperimentalSupported(); + + /// Checks if follow the system mode behavior is enabled. + [[nodiscard]] DMLIB_API bool isWindowsModeEnabled(); + + /// Checks if the host OS is at least Windows 10. + [[nodiscard]] DMLIB_API bool isAtLeastWindows10(); + + /// Checks if the host OS is at least Windows 11. + [[nodiscard]] DMLIB_API bool isAtLeastWindows11(); + + /// Retrieves the current Windows build number. + [[nodiscard]] DMLIB_API DWORD getWindowsBuildNumber(); + + // ======================================================================== + // System Events + // ======================================================================== + + /// Handles system setting changes related to dark mode. + DMLIB_API bool handleSettingChange(LPARAM lParam); + + /// Checks if dark mode is enabled in the Windows registry. + [[nodiscard]] DMLIB_API bool isDarkModeReg(); + + // ======================================================================== + // From DarkMode.h + // ======================================================================== + + /** + * @brief Overrides a specific system color with a custom color. + * + * Currently supports: + * - `COLOR_WINDOW`: Background of ComboBoxEx list. + * - `COLOR_WINDOWTEXT`: Text color of ComboBoxEx list. + * - `COLOR_BTNFACE`: Gridline color in ListView (when applicable). + * + * @param[in] nIndex One of the supported system color indices. + * @param[in] color Custom `COLORREF` value to apply. + */ + DMLIB_API void setSysColor(int nIndex, COLORREF color); + + // ======================================================================== + // Enhancements to DarkMode.h + // ======================================================================== + + /// Makes scroll bars on the specified window and all its children consistent. + DMLIB_API void enableDarkScrollBarForWindowAndChildren(HWND hWnd); + + // ======================================================================== + // Colors + // ======================================================================== + + /// Sets the color tone and its color set for the active theme. + DMLIB_API void setColorTone(int colorTone); + + /// Retrieves the currently active color tone for the theme. + [[nodiscard]] DMLIB_API int getColorTone(); + + DMLIB_API COLORREF setBackgroundColor(COLORREF clrNew); + DMLIB_API COLORREF setCtrlBackgroundColor(COLORREF clrNew); + DMLIB_API COLORREF setHotBackgroundColor(COLORREF clrNew); + DMLIB_API COLORREF setDlgBackgroundColor(COLORREF clrNew); + DMLIB_API COLORREF setErrorBackgroundColor(COLORREF clrNew); + + DMLIB_API COLORREF setTextColor(COLORREF clrNew); + DMLIB_API COLORREF setDarkerTextColor(COLORREF clrNew); + DMLIB_API COLORREF setDisabledTextColor(COLORREF clrNew); + DMLIB_API COLORREF setLinkTextColor(COLORREF clrNew); + + DMLIB_API COLORREF setEdgeColor(COLORREF clrNew); + DMLIB_API COLORREF setHotEdgeColor(COLORREF clrNew); + DMLIB_API COLORREF setDisabledEdgeColor(COLORREF clrNew); + DMLIB_API COLORREF setHighlightColor(COLORREF clrNew); + + DMLIB_API void setThemeColors(const Colors* colors); + DMLIB_API void updateThemeBrushesAndPens(); + + [[nodiscard]] DMLIB_API COLORREF getBackgroundColor(); + [[nodiscard]] DMLIB_API COLORREF getCtrlBackgroundColor(); + [[nodiscard]] DMLIB_API COLORREF getHotBackgroundColor(); + [[nodiscard]] DMLIB_API COLORREF getDlgBackgroundColor(); + [[nodiscard]] DMLIB_API COLORREF getErrorBackgroundColor(); + + [[nodiscard]] DMLIB_API COLORREF getTextColor(); + [[nodiscard]] DMLIB_API COLORREF getDarkerTextColor(); + [[nodiscard]] DMLIB_API COLORREF getDisabledTextColor(); + [[nodiscard]] DMLIB_API COLORREF getLinkTextColor(); + + [[nodiscard]] DMLIB_API COLORREF getEdgeColor(); + [[nodiscard]] DMLIB_API COLORREF getHotEdgeColor(); + [[nodiscard]] DMLIB_API COLORREF getDisabledEdgeColor(); + + [[nodiscard]] DMLIB_API COLORREF getHighlightColor(); + + [[nodiscard]] DMLIB_API HBRUSH getBackgroundBrush(); + [[nodiscard]] DMLIB_API HBRUSH getDlgBackgroundBrush(); + [[nodiscard]] DMLIB_API HBRUSH getCtrlBackgroundBrush(); + [[nodiscard]] DMLIB_API HBRUSH getHotBackgroundBrush(); + [[nodiscard]] DMLIB_API HBRUSH getErrorBackgroundBrush(); + + [[nodiscard]] DMLIB_API HBRUSH getEdgeBrush(); + [[nodiscard]] DMLIB_API HBRUSH getHotEdgeBrush(); + [[nodiscard]] DMLIB_API HBRUSH getDisabledEdgeBrush(); + + [[nodiscard]] DMLIB_API HBRUSH getHighlightBrush(); + + [[nodiscard]] DMLIB_API HPEN getDarkerTextPen(); + [[nodiscard]] DMLIB_API HPEN getEdgePen(); + [[nodiscard]] DMLIB_API HPEN getHotEdgePen(); + [[nodiscard]] DMLIB_API HPEN getDisabledEdgePen(); + + [[nodiscard]] DMLIB_API HPEN getHighlightPen(); + + DMLIB_API COLORREF setViewBackgroundColor(COLORREF clrNew); + DMLIB_API COLORREF setViewTextColor(COLORREF clrNew); + DMLIB_API COLORREF setViewGridlinesColor(COLORREF clrNew); + + DMLIB_API COLORREF setHeaderBackgroundColor(COLORREF clrNew); + DMLIB_API COLORREF setHeaderHotBackgroundColor(COLORREF clrNew); + DMLIB_API COLORREF setHeaderTextColor(COLORREF clrNew); + DMLIB_API COLORREF setHeaderEdgeColor(COLORREF clrNew); + + DMLIB_API void setViewColors(const ColorsView* colors); + DMLIB_API void updateViewBrushesAndPens(); + + [[nodiscard]] DMLIB_API COLORREF getViewBackgroundColor(); + [[nodiscard]] DMLIB_API COLORREF getViewTextColor(); + [[nodiscard]] DMLIB_API COLORREF getViewGridlinesColor(); + + [[nodiscard]] DMLIB_API COLORREF getHeaderBackgroundColor(); + [[nodiscard]] DMLIB_API COLORREF getHeaderHotBackgroundColor(); + [[nodiscard]] DMLIB_API COLORREF getHeaderTextColor(); + [[nodiscard]] DMLIB_API COLORREF getHeaderEdgeColor(); + + [[nodiscard]] DMLIB_API HBRUSH getViewBackgroundBrush(); + [[nodiscard]] DMLIB_API HBRUSH getViewGridlinesBrush(); + + [[nodiscard]] DMLIB_API HBRUSH getHeaderBackgroundBrush(); + [[nodiscard]] DMLIB_API HBRUSH getHeaderHotBackgroundBrush(); + + [[nodiscard]] DMLIB_API HPEN getHeaderEdgePen(); + + /// Initializes default color set based on the current mode type. + DMLIB_API void setDefaultColors(bool updateBrushesAndOther); + + // ======================================================================== + // Control Subclassing + // ======================================================================== + + /// Applies themed owner drawn subclassing to a checkbox, radio, or tri-state button control. + DMLIB_API void setCheckboxOrRadioBtnCtrlSubclass(HWND hWnd); + /// Removes the owner drawn subclass from a a checkbox, radio, or tri-state button control. + DMLIB_API void removeCheckboxOrRadioBtnCtrlSubclass(HWND hWnd); + + /// Applies owner drawn subclassing to a groupbox button control. + DMLIB_API void setGroupboxCtrlSubclass(HWND hWnd); + /// Removes the owner drawn subclass from a groupbox button control. + DMLIB_API void removeGroupboxCtrlSubclass(HWND hWnd); + + /// Applies owner drawn subclassing and theming to an up-down (spinner) control. + DMLIB_API void setUpDownCtrlSubclass(HWND hWnd); + /// Removes the owner drawn subclass from a up-down (spinner) control. + DMLIB_API void removeUpDownCtrlSubclass(HWND hWnd); + + /// Applies a subclass to detect and subclass tab control's up-down (spinner) child. + DMLIB_API void setTabCtrlUpDownSubclass(HWND hWnd); + /// Removes the subclass procedure for a tab control's up-down (spinner) child detection. + DMLIB_API void removeTabCtrlUpDownSubclass(HWND hWnd); + /// Applies owner drawn and up-down (spinner) child detection subclassings for a tab control. + DMLIB_API void setTabCtrlSubclass(HWND hWnd); + /// Removes owner drawn and up-down (spinner) child detection subclasses. + DMLIB_API void removeTabCtrlSubclass(HWND hWnd); + + /// Applies owner drawn custom border subclassing to a list box or edit control. + DMLIB_API void setCustomBorderForListBoxOrEditCtrlSubclass(HWND hWnd); + /// Removes the custom border subclass from a list box or edit control. + DMLIB_API void removeCustomBorderForListBoxOrEditCtrlSubclass(HWND hWnd); + + /// Applies owner drawn subclassing to a combo box control. + DMLIB_API void setComboBoxCtrlSubclass(HWND hWnd); + /// Removes the owner drawn subclass from a combo box control. + DMLIB_API void removeComboBoxCtrlSubclass(HWND hWnd); + + /// Applies subclassing to a ComboBoxEx control to handle its child list box and edit controls. + DMLIB_API void setComboBoxExCtrlSubclass(HWND hWnd); + /// Removes the child handling subclass from a ComboBoxEx control. + DMLIB_API void removeComboBoxExCtrlSubclass(HWND hWnd); + + /// Applies subclassing to a list view control to handle custom colors. + DMLIB_API void setListViewCtrlSubclass(HWND hWnd); + /// Removes the custom colors handling subclass from a list view control. + DMLIB_API void removeListViewCtrlSubclass(HWND hWnd); + + /// Applies owner drawn subclassing to a header control. + DMLIB_API void setHeaderCtrlSubclass(HWND hWnd); + /// Removes the owner drawn subclass from a header control. + DMLIB_API void removeHeaderCtrlSubclass(HWND hWnd); + + /// Applies owner drawn subclassing to a status bar control. + DMLIB_API void setStatusBarCtrlSubclass(HWND hWnd); + /// Removes the owner drawn subclass from a status bar control. + DMLIB_API void removeStatusBarCtrlSubclass(HWND hWnd); + + /// Applies owner drawn subclassing to a progress bar control. + DMLIB_API void setProgressBarCtrlSubclass(HWND hWnd); + /// Removes the owner drawn subclass from a progress bar control. + DMLIB_API void removeProgressBarCtrlSubclass(HWND hWnd); + + /// Applies workaround subclassing to a static control to handle visual glitch in disabled state. + DMLIB_API void setStaticTextCtrlSubclass(HWND hWnd); + /// Removes the workaround subclass from a static control. + DMLIB_API void removeStaticTextCtrlSubclass(HWND hWnd); + + /// Applies owner drawn subclassing to a IP address control. + DMLIB_API void setIPAddressCtrlSubclass(HWND hWnd); + /// Removes the owner drawn subclass from a IP address control. + DMLIB_API void removeIPAddressCtrlSubclass(HWND hWnd); + + /// Applies custom color subclassing to a hot key control. + DMLIB_API void setHotKeyCtrlSubclass(HWND hWnd); + /// Removes the custom color subclass from a hot key control. + DMLIB_API void removeHotKeyCtrlSubclass(HWND hWnd); + + /// Applies custom color subclassing to a date time picker control. + DMLIB_API void setDTPCtrlSubclass(HWND hWnd); + /// Removes the custom color subclass from a date time picker control. + DMLIB_API void removeDTPCtrlSubclass(HWND hWnd); + + // ======================================================================== + // Child Subclassing + // ======================================================================== + + /// Applies theming and/or subclassing to all child controls of a parent window. + DMLIB_API void setChildCtrlsSubclassAndThemeEx(HWND hParent, bool subclass, bool theme); + /// Wrapper for `dmlib::setChildCtrlsSubclassAndThemeEx`. + DMLIB_API void setChildCtrlsSubclassAndTheme(HWND hParent); + /// Applies theming to all child controls of a parent window. + DMLIB_API void setChildCtrlsTheme(HWND hParent); + + // ======================================================================== + // Window, Parent, And Other Subclassing + // ======================================================================== + + /// Applies window subclassing to handle `WM_ERASEBKGND` message. + DMLIB_API void setWindowEraseBgSubclass(HWND hWnd); + /// Removes the subclass used for `WM_ERASEBKGND` message handling. + DMLIB_API void removeWindowEraseBgSubclass(HWND hWnd); + + /// Applies window subclassing to handle `WM_CTLCOLOR*` messages. + DMLIB_API void setWindowCtlColorSubclass(HWND hWnd); + /// Removes the subclass used for `WM_CTLCOLOR*` messages handling. + DMLIB_API void removeWindowCtlColorSubclass(HWND hWnd); + + /// Applies window subclassing for handling `NM_CUSTOMDRAW` notifications for custom drawing. + DMLIB_API void setWindowNotifyCustomDrawSubclass(HWND hWnd); + /// Removes the subclass used for handling `NM_CUSTOMDRAW` notifications for custom drawing. + DMLIB_API void removeWindowNotifyCustomDrawSubclass(HWND hWnd); + + /// Applies window subclassing for menu bar themed custom drawing. + DMLIB_API void setWindowMenuBarSubclass(HWND hWnd); + /// Removes the subclass used for menu bar themed custom drawing. + DMLIB_API void removeWindowMenuBarSubclass(HWND hWnd); + + /// Applies window subclassing to handle `WM_SETTINGCHANGE` message. + DMLIB_API void setWindowSettingChangeSubclass(HWND hWnd); + /// Removes the subclass used for `WM_SETTINGCHANGE` message handling. + DMLIB_API void removeWindowSettingChangeSubclass(HWND hWnd); + + // ======================================================================== + // Theme And Helpers + // ======================================================================== + + /// Configures the SysLink control to be affected by `WM_CTLCOLORSTATIC` message. + DMLIB_API void enableSysLinkCtrlCtlColor(HWND hWnd); + + /// Sets dark title bar and optional Windows 11 features. + DMLIB_API void setDarkTitleBarEx(HWND hWnd, bool useWin11Features); + /// Sets dark mode title bar on supported Windows versions. + DMLIB_API void setDarkTitleBar(HWND hWnd); + + /// Get dark mode theme name. + [[nodiscard]] DMLIB_API const wchar_t* getDarkModeThemeName(); + /// Applies an experimental visual style to the specified window, if supported. + DMLIB_API void setDarkThemeExperimentalEx(HWND hWnd, const wchar_t* themeClassName); + /// Applies an experimental Explorer visual style to the specified window, if supported. + DMLIB_API void setDarkThemeExperimental(HWND hWnd); + /// Applies "DarkMode_Explorer" visual style if experimental mode is active. + DMLIB_API void setDarkExplorerTheme(HWND hWnd); + /// Applies "DarkMode_Explorer" visual style to scroll bars. + DMLIB_API void setDarkScrollBar(HWND hWnd); + /// Applies "DarkMode_Explorer" visual style to tooltip controls based on context. + DMLIB_API void setDarkTooltips(HWND hWnd, UINT tooltipType); + /// Applies "DarkMode_DarkTheme" visual style if supported and experimental mode is active. + DMLIB_API void setDarkThemeTheme(HWND hWnd); + + /// Sets the color of line above a toolbar control for non-classic mode. + DMLIB_API void setDarkLineAbovePanelToolbar(HWND hWnd); + /// Applies an experimental Explorer visual style to a list view. + DMLIB_API void setDarkListView(HWND hWnd); + /// Replaces default list view checkboxes with themed dark-mode versions on Windows 11. + DMLIB_API void setDarkListViewCheckboxes(HWND hWnd); + /// Replaces default tree view checkboxes with themed dark-mode versions on Windows 11. + DMLIB_API void setDarkTreeViewCheckboxes(HWND hWnd); + /// Sets colors and edges for a rich edt control. + DMLIB_API void setDarkRichEdit(HWND hWnd); + /// Sets colors for a month calendar control. + DMLIB_API void setDarkMonthCalendar(HWND hWnd); + + /// Applies visual styles; ctl color message and child controls subclassings to a window safely. + DMLIB_API void setDarkWndSafeEx(HWND hWnd, bool useWin11Features); + /// Applies visual styles; ctl color message and child controls subclassings with Windows 11 features. + DMLIB_API void setDarkWndSafe(HWND hWnd); + /// Applies visual styles; ctl color message, child controls, custom drawing, and setting change subclassings to a window safely. + DMLIB_API void setDarkWndNotifySafeEx(HWND hWnd, bool setSettingChangeSubclass, bool useWin11Features); + /// Applies visual styles; ctl color message, child controls, and custom drawing subclassings with Windows 11 features. + DMLIB_API void setDarkWndNotifySafe(HWND hWnd); + + /// Enables or disables theme-based dialog background textures in classic mode. + DMLIB_API void enableThemeDialogTexture(HWND hWnd, bool theme); + + /// Enables or disables visual styles for a window. + DMLIB_API void disableVisualStyle(HWND hWnd, bool doDisable); + + /// Calculates perceptual lightness of a COLORREF color. + [[nodiscard]] DMLIB_API double calculatePerceivedLightness(COLORREF clr); + + /// Retrieves the current TreeView style configuration. + [[nodiscard]] DMLIB_API int getTreeViewStyle(); + + /// Determines appropriate TreeView style based on background perceived lightness. + DMLIB_API void calculateTreeViewStyle(); + + /// (Re)applies the appropriate window theme style to the specified TreeView. + DMLIB_API void setTreeViewWindowThemeEx(HWND hWnd, bool force); + /// Applies the appropriate window theme style to the specified TreeView. + DMLIB_API void setTreeViewWindowTheme(HWND hWnd); + + /// Retrieves the previous TreeView style configuration. + [[nodiscard]] DMLIB_API int getPrevTreeViewStyle(); + + /// Stores the current TreeView style as the previous style for later comparison. + DMLIB_API void setPrevTreeViewStyle(); + + /// Checks whether the current theme is dark. + [[nodiscard]] DMLIB_API bool isThemeDark(); + + /// Checks whether the color is dark. + [[nodiscard]] DMLIB_API bool isColorDark(COLORREF clr); + + /// Forces a window to redraw its non-client frame. + DMLIB_API void redrawWindowFrame(HWND hWnd); + /// Sets a window's standard style flags and redraws window if needed. + DMLIB_API void setWindowStyle(HWND hWnd, bool setStyle, LONG_PTR styleFlag); + /// Sets a window's extended style flags and redraws window if needed. + DMLIB_API void setWindowExStyle(HWND hWnd, bool setExStyle, LONG_PTR exStyleFlag); + /// Replaces an extended edge (e.g. client edge) with a standard window border. + DMLIB_API void replaceExEdgeWithBorder(HWND hWnd, bool replace, LONG_PTR exStyleFlag); + /// Safely toggles `WS_EX_CLIENTEDGE` with `WS_BORDER`. + DMLIB_API void replaceClientEdgeWithBorderSafeEx(HWND hWnd, bool replace); + /// Safely toggles `WS_EX_CLIENTEDGE` with `WS_BORDER` based on dark mode state. + DMLIB_API void replaceClientEdgeWithBorderSafe(HWND hWnd); + + /// Applies classic-themed styling to a progress bar in non-classic mode. + DMLIB_API void setProgressBarClassicTheme(HWND hWnd); + + // ======================================================================== + // Ctl Color + // ======================================================================== + + /// Handles text and background colorizing for read-only controls. + [[nodiscard]] DMLIB_API LRESULT onCtlColor(HDC hdc); + + /// Handles text and background colorizing for interactive controls. + [[nodiscard]] DMLIB_API LRESULT onCtlColorCtrl(HDC hdc); + + /// Handles text and background colorizing for window and disabled non-text controls. + [[nodiscard]] DMLIB_API LRESULT onCtlColorDlg(HDC hdc); + + /// Handles text and background colorizing for error state (for specific usage). + [[nodiscard]] DMLIB_API LRESULT onCtlColorError(HDC hdc); + + /// Handles text and background colorizing for static text controls. + [[nodiscard]] DMLIB_API LRESULT onCtlColorDlgStaticText(HDC hdc, bool isTextEnabled); + + /// Handles text and background colorizing for SysLink controls. + [[nodiscard]] DMLIB_API LRESULT onCtlColorDlgLinkText(HDC hdc, bool isTextEnabled); + + /// Handles text and background colorizing for list box controls. + [[nodiscard]] DMLIB_API LRESULT onCtlColorListbox(WPARAM wParam, LPARAM lParam); + + // ======================================================================== + // Hook Callback Dialog Procedure + // ======================================================================== + + /** + * @brief Hook procedure for customizing common dialogs with dark mode. + * + * This function handles messages for all Windows common dialogs. + * When initialized (`WM_INITDIALOG`), it applies dark mode styling to the dialog. + * + * ## Special Case: Font Dialog Workaround + * - When a hook is used with `ChooseFont`, Windows **automatically falls back** + * to an **older template**, losing modern UI elements. + * - To prevent this forced downgrade, a **modified template** (based on Font.dlg) is used. + * - **CBS_OWNERDRAWFIXED should be removed** from the **Size** and **Script** combo boxes + * to restore proper visualization. + * - **Custom owner-draw visuals remain** for other font combo boxes to allow font preview. + * - Same for the `"AaBbYyZz"` sample text. + * - However **Automatic system translation for captions and static texts is lost** in this workaround. + * + * ## Custom Font Dialog Template (Resource File) + * ```rc + * IDD_DARK_FONT_DIALOG DIALOG 13, 54, 243, 234 + * STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU | + * DS_3DLOOK + * CAPTION "Font" + * FONT 9, "Segoe UI" + * BEGIN + * LTEXT "&Font:", stc1, 7, 7, 98, 9 + * COMBOBOX cmb1, 7, 16, 98, 76, + * CBS_SIMPLE | CBS_AUTOHSCROLL | CBS_DISABLENOSCROLL | + * CBS_SORT | WS_VSCROLL | WS_TABSTOP | CBS_HASSTRINGS | + * CBS_OWNERDRAWFIXED + * + * LTEXT "Font st&yle:", stc2, 114, 7, 74, 9 + * COMBOBOX cmb2, 114, 16, 74, 76, + * CBS_SIMPLE | CBS_AUTOHSCROLL | CBS_DISABLENOSCROLL | + * WS_VSCROLL | WS_TABSTOP | CBS_HASSTRINGS | + * CBS_OWNERDRAWFIXED + * + * LTEXT "&Size:", stc3, 198, 7, 36, 9 + * COMBOBOX cmb3, 198, 16, 36, 76, + * CBS_SIMPLE | CBS_AUTOHSCROLL | CBS_DISABLENOSCROLL | + * CBS_SORT | WS_VSCROLL | WS_TABSTOP | CBS_HASSTRINGS | + * CBS_OWNERDRAWFIXED // remove CBS_OWNERDRAWFIXED + * + * GROUPBOX "Effects", grp1, 7, 97, 98, 76, WS_GROUP + * AUTOCHECKBOX "Stri&keout", chx1, 13, 111, 90, 10, WS_TABSTOP + * AUTOCHECKBOX "&Underline", chx2, 13, 127, 90, 10 + * + * LTEXT "&Color:", stc4, 13, 144, 89, 9 + * COMBOBOX cmb4, 13, 155, 85, 100, + * CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_AUTOHSCROLL | + * CBS_HASSTRINGS | WS_BORDER | WS_VSCROLL | WS_TABSTOP + * + * GROUPBOX "Sample", grp2, 114, 97, 120, 43, WS_GROUP + * CTEXT "AaBbYyZz", stc5, 116, 106, 117, 33, + * SS_NOPREFIX | NOT WS_VISIBLE + * LTEXT "", stc6, 7, 178, 227, 20, SS_NOPREFIX | NOT WS_GROUP + * + * LTEXT "Sc&ript:", stc7, 114, 145, 118, 9 + * COMBOBOX cmb5, 114, 155, 120, 30, CBS_DROPDOWNLIST | + * CBS_OWNERDRAWFIXED | CBS_AUTOHSCROLL | CBS_HASSTRINGS | // remove CBS_OWNERDRAWFIXED + * WS_BORDER | WS_VSCROLL | WS_TABSTOP + * + * CONTROL "Show more fonts", IDC_MANAGE_LINK, "SysLink", + * WS_TABSTOP, 7, 199, 227, 9 + * + * DEFPUSHBUTTON "OK", IDOK, 141, 215, 45, 14, WS_GROUP + * PUSHBUTTON "Cancel", IDCANCEL, 190, 215, 45, 14, WS_GROUP + * PUSHBUTTON "&Apply", psh3, 92, 215, 45, 14, WS_GROUP + * PUSHBUTTON "&Help", pshHelp, 43, 215, 45, 14, WS_GROUP + * END + * ``` + * + * ## Usage Example: + * ```cpp + * #define IDD_DARK_FONT_DIALOG 1000 // usually in resource.h or other header + * + * CHOOSEFONT cf{}; + * cf.Flags |= CF_ENABLEHOOK | CF_ENABLETEMPLATE; + * cf.lpfnHook = static_cast(dmlib::HookDlgProc); + * cf.hInstance = GetModuleHandle(nullptr); + * cf.lpTemplateName = MAKEINTRESOURCE(IDD_DARK_FONT_DIALOG); + * ``` + * + * @param[in] hWnd Handle to the dialog window. + * @param[in] uMsg Message identifier. + * @param[in] wParam First message parameter (unused). + * @param[in] lParam Second message parameter (unused). + * @return UINT_PTR A value defined by the hook procedure. + */ + DMLIB_API UINT_PTR CALLBACK HookDlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + + /// Applies dark mode visual styles to task dialog. + DMLIB_API void setDarkTaskDlg(HWND hWnd); + + /// Simple task dialog callback procedure to enable dark mode support. + DMLIB_API HRESULT CALLBACK DarkTaskDlgCallback(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LONG_PTR lpRefData); + + /** + * @brief Wrapper for `TaskDialogIndirect` with dark mode support. + * + * Parameters are same as for `TaskDialogIndirect`. + * Should be used with `dmlib::setDarkTaskDlg` + * used in task dialog callback procedure. + * + * ## Example of Callback Procedure + * ```cpp + * static HRESULT CALLBACK DarkTaskDlgCallback( + * HWND hWnd, + * UINT uMsg, + * [[maybe_unused]] WPARAM wParam, + * [[maybe_unused]] LPARAM lParam, + * [[maybe_unused]] LONG_PTR lpRefData + * ) + * { + * if (uMsg == TDN_DIALOG_CONSTRUCTED) + * { + * dmlib::setDarkTaskDlg(hWnd); + * } + * return S_OK; + * } + * ``` + * + * @see dmlib::DarkTaskDlgCallback() + * @see dmlib::setDarkTaskDlg() + */ + DMLIB_API HRESULT darkTaskDialogIndirect(const TASKDIALOGCONFIG* pTaskConfig, int* pnButton, int* pnRadioButton, BOOL* pfVerificationFlagChecked); + + /// Displays a message box as task dialog with themed styling. + DMLIB_API int darkMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType); + +#ifdef __cplusplus + } // extern "C" +#endif + +} // namespace dmlib + +#else +#define _DARKMODELIB_NOT_USED +#endif // (NTDDI_VERSION >= NTDDI_VISTA) //&& (x64 or arm64) diff --git a/darkmodelib/src/Darkmodelib.cpp b/darkmodelib/src/Darkmodelib.cpp new file mode 100644 index 0000000000..7d73d1c757 --- /dev/null +++ b/darkmodelib/src/Darkmodelib.cpp @@ -0,0 +1,4358 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + +// Based on the Notepad++ dark mode code licensed under GPLv3. +// Originally by adzm / Adam D. Walling, with modifications by the Notepad++ team. +// Heavily modified by ozone10 (Notepad++ contributor). +// Used with permission to relicense under the Mozilla Public License, v. 2.0. + + +#include "StdAfx.h" + +#include "Darkmodelib.h" + +#ifndef _DARKMODELIB_NOT_USED + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "DmlibColor.h" +#include "DmlibDpi.h" +#include "DmlibHook.h" +#ifndef _DARKMODELIB_NO_INI_CONFIG +#include "DmlibIni.h" +#endif +#include "DmlibSubclass.h" +#include "DmlibSubclassControl.h" +#include "DmlibSubclassWindow.h" +#include "DmlibWinApi.h" + +#include "Version.h" + +/** + * @brief Returns library version information or compile-time feature flags. + * + * Responds to the specified query by returning either: + * - Version numbers (`verMajor`, `verMinor`, `verRevision`) + * - Build configuration flags (returns `TRUE` or `FALSE`) + * - A constant value (`featureCheck`, `maxValue`) used for validation + * + * @param[in] libInfoType Integer with `LibInfo` enum value specifying which piece of information to retrieve. + * @return Integer value: + * - Version: as defined by `DM_VERSION_MAJOR`, etc. + * - Boolean flags: `TRUE` (1) if the feature is enabled, `FALSE` (0) otherwise. + * - `featureCheck`, `maxValue`: returns the numeric max enum value. + * - `-1`: for invalid or unhandled enum cases (should not occur in correct usage). + * + * @see LibInfo + */ +int dmlib::getLibInfo(int libInfoType) +{ + switch (static_cast(libInfoType)) + { + case LibInfo::maxValue: + case LibInfo::featureCheck: + { + return static_cast(LibInfo::maxValue); + } + + case LibInfo::verMajor: + { + return DM_VERSION_MAJOR; + } + + case LibInfo::verMinor: + { + return DM_VERSION_MINOR; + } + + case LibInfo::verRevision: + { + return DM_VERSION_REVISION; + } + + case LibInfo::iniConfigUsed: + { +#ifndef _DARKMODELIB_NO_INI_CONFIG + return TRUE; +#else + return FALSE; +#endif + } + + case LibInfo::allowOldOS: + { +#ifdef _DARKMODELIB_ALLOW_OLD_OS + return _DARKMODELIB_ALLOW_OLD_OS; +#else + return FALSE; +#endif + } + + case LibInfo::useDlgProcCtl: + { +#ifdef _DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS + return _DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS; +#else + return FALSE; +#endif + } + + case LibInfo::preferTheme: + { +#ifdef _DARKMODELIB_PREFER_THEME + return _DARKMODELIB_PREFER_THEME; +#else + return FALSE; +#endif + } + + case LibInfo::useSBFix: + { +#ifdef _DARKMODELIB_USE_SCROLLBAR_FIX + return _DARKMODELIB_USE_SCROLLBAR_FIX; +#else + return FALSE; +#endif + } + } + return -1; // should never happen +} + +/** + * @brief Describes how the application responds to the system theme. + * + * Used to determine behavior when following the system's light/dark mode setting. + * - `disabled`: Do not follow system; use manually selected appearance. + * - `light`: Follow system mode; apply light theme when system is in light mode. + * - `classic`: Follow system mode; apply classic style when system is in light mode. + */ +enum class WinMode : std::uint8_t +{ + disabled, ///< Manual - system mode is ignored. + light, ///< Use light theme if system is in light mode. + classic ///< Use classic style if system is in light mode. +}; + +/// Types of list and tree views checkbox styles +enum class ViewCheckbox : std::uint8_t +{ + listView, ///< List view with LVS_EX_CHECKBOXES extendend style. + tvSimple, ///< Tree view with TVS_CHECKBOXES style. + + /// Tree view with any combination of TVS_EX_PARTIALCHECKBOXES, TVS_EX_EXCLUSIONCHECKBOXES, + /// TVS_EX_DIMMEDCHECKBOXES extendend styles. + tvExtended +}; + +/** + * @struct DarkModeParams + * @brief Defines theming and subclassing parameters for child controls. + * + * Members: + * - `m_themeClassName`: Optional theme class name (e.g. `"DarkMode_Explorer"`), or `nullptr` to skip theming. + * - `m_subclass`: Whether to apply custom subclassing for dark-mode painting and behavior. + * - `m_theme`: Whether to apply a themed visual style to applicable controls. + * + * Used during enumeration to configure dark mode application on a per-control basis. + */ +struct DarkModeParams +{ + const wchar_t* m_themeClassName = nullptr; + bool m_subclass = false; + bool m_theme = false; +}; + +/// Threshold range around 50.0 where TreeView uses classic style instead of light/dark. +static constexpr double kMiddleGrayRange = 2.0; + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wexit-time-destructors" +#pragma clang diagnostic ignored "-Wglobal-constructors" +#endif + +namespace // anonymous +{ + /// Global struct + struct + { + DWM_WINDOW_CORNER_PREFERENCE m_roundCorner = DWMWCP_DEFAULT; + COLORREF m_borderColor = DWMWA_COLOR_DEFAULT; + DWM_SYSTEMBACKDROP_TYPE m_mica = DWMSBT_AUTO; + COLORREF m_tvBackground = RGB(41, 49, 52); + double m_lightness = 50.0; + dmlib::TreeViewStyle m_tvStylePrev = dmlib::TreeViewStyle::classic; + dmlib::TreeViewStyle m_tvStyle = dmlib::TreeViewStyle::classic; + bool m_micaExtend = false; + bool m_colorizeTitleBar = false; + dmlib::DarkModeType m_dmType = dmlib::DarkModeType::dark; + WinMode m_windowsMode = WinMode::disabled; + bool m_isInit = false; + bool m_isInitExperimental = false; + +#ifndef _DARKMODELIB_NO_INI_CONFIG + std::wstring m_iniName; + bool m_isIniNameSet = false; + bool m_iniExist = false; +#endif + } g_dmCfg; +} // anonymous namespace + +static dmlib_color::Theme& getTheme() noexcept +{ + static dmlib_color::Theme tMain{}; + return tMain; +} + +/** + * @brief Sets the color tone and its color set for the active theme. + * + * Applies a color tone (e.g. red, blue, olive) its color set. + * + * @param[in] colorTone The tone to apply (see @ref ColorTone enum). + * + * @see dmlib::getColorTone() + * @see dmlib_color::Theme + */ +void dmlib::setColorTone(int colorTone) +{ + getTheme().setToneColors(static_cast(colorTone)); +} + +/** + * @brief Retrieves the currently active color tone for the theme. + * + * @return The currently selected @ref ColorTone value. + * + * @see dmlib::setColorTone() + */ +int dmlib::getColorTone() +{ + return static_cast(getTheme().getColorTone()); +} + +static dmlib_color::ThemeView& getThemeView() noexcept +{ + static dmlib_color::ThemeView tView{}; + return tView; +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +COLORREF dmlib::setBackgroundColor(COLORREF clrNew) { return getTheme().setColorBackground(clrNew); } +COLORREF dmlib::setCtrlBackgroundColor(COLORREF clrNew) { return getTheme().setColorCtrlBackground(clrNew); } +COLORREF dmlib::setHotBackgroundColor(COLORREF clrNew) { return getTheme().setColorHotBackground(clrNew); } +COLORREF dmlib::setDlgBackgroundColor(COLORREF clrNew) { return getTheme().setColorDlgBackground(clrNew); } +COLORREF dmlib::setErrorBackgroundColor(COLORREF clrNew) { return getTheme().setColorErrorBackground(clrNew); } +COLORREF dmlib::setTextColor(COLORREF clrNew) { return getTheme().setColorText(clrNew); } +COLORREF dmlib::setDarkerTextColor(COLORREF clrNew) { return getTheme().setColorDarkerText(clrNew); } +COLORREF dmlib::setDisabledTextColor(COLORREF clrNew) { return getTheme().setColorDisabledText(clrNew); } +COLORREF dmlib::setLinkTextColor(COLORREF clrNew) { return getTheme().setColorLinkText(clrNew); } +COLORREF dmlib::setEdgeColor(COLORREF clrNew) { return getTheme().setColorEdge(clrNew); } +COLORREF dmlib::setHotEdgeColor(COLORREF clrNew) { return getTheme().setColorHotEdge(clrNew); } +COLORREF dmlib::setDisabledEdgeColor(COLORREF clrNew) { return getTheme().setColorDisabledEdge(clrNew); } +COLORREF dmlib::setHighlightColor(COLORREF clrNew) { return getTheme().setColorHighlight(clrNew); } + +void dmlib::setThemeColors(const Colors* colors) +{ + if (colors != nullptr) + { + getTheme().updateTheme(*colors); + } +} + +void dmlib::updateThemeBrushesAndPens() +{ + getTheme().updateTheme(); +} + +COLORREF dmlib::getBackgroundColor() { return getTheme().getColors().background; } +COLORREF dmlib::getCtrlBackgroundColor() { return getTheme().getColors().ctrlBackground; } +COLORREF dmlib::getHotBackgroundColor() { return getTheme().getColors().hotBackground; } +COLORREF dmlib::getDlgBackgroundColor() { return getTheme().getColors().dlgBackground; } +COLORREF dmlib::getErrorBackgroundColor() { return getTheme().getColors().errorBackground; } +COLORREF dmlib::getTextColor() { return getTheme().getColors().text; } +COLORREF dmlib::getDarkerTextColor() { return getTheme().getColors().darkerText; } +COLORREF dmlib::getDisabledTextColor() { return getTheme().getColors().disabledText; } +COLORREF dmlib::getLinkTextColor() { return getTheme().getColors().linkText; } +COLORREF dmlib::getEdgeColor() { return getTheme().getColors().edge; } +COLORREF dmlib::getHotEdgeColor() { return getTheme().getColors().hotEdge; } +COLORREF dmlib::getDisabledEdgeColor() { return getTheme().getColors().disabledEdge; } +COLORREF dmlib::getHighlightColor() { return getTheme().getColors().highlight; } + +HBRUSH dmlib::getBackgroundBrush() { return getTheme().getBrushes().m_background; } +HBRUSH dmlib::getCtrlBackgroundBrush() { return getTheme().getBrushes().m_ctrlBackground; } +HBRUSH dmlib::getHotBackgroundBrush() { return getTheme().getBrushes().m_hotBackground; } +HBRUSH dmlib::getDlgBackgroundBrush() { return getTheme().getBrushes().m_dlgBackground; } +HBRUSH dmlib::getErrorBackgroundBrush() { return getTheme().getBrushes().m_errorBackground; } + +HBRUSH dmlib::getEdgeBrush() { return getTheme().getBrushes().m_edge; } +HBRUSH dmlib::getHotEdgeBrush() { return getTheme().getBrushes().m_hotEdge; } +HBRUSH dmlib::getDisabledEdgeBrush() { return getTheme().getBrushes().m_disabledEdge; } + +HBRUSH dmlib::getHighlightBrush() { return getTheme().getBrushes().m_highlight; } + +HPEN dmlib::getDarkerTextPen() { return getTheme().getPens().m_darkerText; } +HPEN dmlib::getEdgePen() { return getTheme().getPens().m_edge; } +HPEN dmlib::getHotEdgePen() { return getTheme().getPens().m_hotEdge; } +HPEN dmlib::getDisabledEdgePen() { return getTheme().getPens().m_disabledEdge; } + +HPEN dmlib::getHighlightPen() { return getTheme().getPens().m_highlight; } + +COLORREF dmlib::setViewBackgroundColor(COLORREF clrNew) { return getThemeView().setColorBackground(clrNew); } +COLORREF dmlib::setViewTextColor(COLORREF clrNew) { return getThemeView().setColorText(clrNew); } +COLORREF dmlib::setViewGridlinesColor(COLORREF clrNew) { return getThemeView().setColorGridlines(clrNew); } + +COLORREF dmlib::setHeaderBackgroundColor(COLORREF clrNew) { return getThemeView().setColorHeaderBackground(clrNew); } +COLORREF dmlib::setHeaderHotBackgroundColor(COLORREF clrNew) { return getThemeView().setColorHeaderHotBackground(clrNew); } +COLORREF dmlib::setHeaderTextColor(COLORREF clrNew) { return getThemeView().setColorHeaderText(clrNew); } +COLORREF dmlib::setHeaderEdgeColor(COLORREF clrNew) { return getThemeView().setColorHeaderEdge(clrNew); } + +void dmlib::setViewColors(const ColorsView* colors) +{ + if (colors != nullptr) + { + getThemeView().updateView(*colors); + } +} + +void dmlib::updateViewBrushesAndPens() +{ + getThemeView().updateView(); +} + +COLORREF dmlib::getViewBackgroundColor() { return getThemeView().getColors().background; } +COLORREF dmlib::getViewTextColor() { return getThemeView().getColors().text; } +COLORREF dmlib::getViewGridlinesColor() { return getThemeView().getColors().gridlines; } + +COLORREF dmlib::getHeaderBackgroundColor() { return getThemeView().getColors().headerBackground; } +COLORREF dmlib::getHeaderHotBackgroundColor() { return getThemeView().getColors().headerHotBackground; } +COLORREF dmlib::getHeaderTextColor() { return getThemeView().getColors().headerText; } +COLORREF dmlib::getHeaderEdgeColor() { return getThemeView().getColors().headerEdge; } + +HBRUSH dmlib::getViewBackgroundBrush() { return getThemeView().getViewBrushesAndPens().m_background; } +HBRUSH dmlib::getViewGridlinesBrush() { return getThemeView().getViewBrushesAndPens().m_gridlines; } + +HBRUSH dmlib::getHeaderBackgroundBrush() { return getThemeView().getViewBrushesAndPens().m_headerBackground; } +HBRUSH dmlib::getHeaderHotBackgroundBrush() { return getThemeView().getViewBrushesAndPens().m_headerHotBackground; } + +HPEN dmlib::getHeaderEdgePen() { return getThemeView().getViewBrushesAndPens().m_headerEdge; } + +/** + * @brief Initializes default color set based on the current mode type. + * + * Sets up control and view colors depending on the active theme: + * - `dark`: Applies dark tone color set and view dark color set. + * - `light`: Applies the predefined light color set and view light color set. + * - `classic`: Applies only system color on views, other controls are not affected + * by theme colors. + * + * If `updateBrushesAndOther` is `true`, also updates + * brushes, pens, and view styles (unless in classic mode). + * + * @param[in] updateBrushesAndOther Whether to refresh GDI brushes and pens, and tree view styling. + * + * @see dmlib::updateThemeBrushesAndPens + * @see dmlib::calculateTreeViewStyle + */ +void dmlib::setDefaultColors(bool updateBrushesAndOther) +{ + switch (g_dmCfg.m_dmType) + { + case DarkModeType::dark: + { + getTheme().setToneColors(); + getThemeView().resetColors(true); + break; + } + + case DarkModeType::light: + { + getTheme().setLightColors(); + getThemeView().resetColors(false); + break; + } + + case DarkModeType::classic: + { + dmlib::setViewBackgroundColor(::GetSysColor(COLOR_WINDOW)); + dmlib::setViewTextColor(::GetSysColor(COLOR_WINDOWTEXT)); + break; + } + } + + if (updateBrushesAndOther) + { + if (g_dmCfg.m_dmType != DarkModeType::classic) + { + dmlib::updateThemeBrushesAndPens(); + dmlib::updateViewBrushesAndPens(); + } + } + dmlib::calculateTreeViewStyle(); +} + +/** + * @brief Initializes the dark mode configuration based on the selected mode. + * + * Sets the active dark mode theming and system-following behavior according to the specified `dmType`: + * - `0`: Light mode, do not follow system. + * - `1` or default: Dark mode, do not follow system. + * - `2`: *[Internal]* Follow system - light or dark depending on registry (see `dmlib::isDarkModeReg()`). + * - `3`: Classic mode, do not follow system. + * - `4`: *[Internal]* Follow system - classic or dark depending on registry. + * + * @param[in] dmType Integer representing the desired mode. + * + * @see DarkModeType + * @see WinMode + * @see dmlib::isDarkModeReg() + */ +void dmlib::initDarkModeConfig(UINT dmType) +{ + switch (dmType) + { + case 0: + { + g_dmCfg.m_dmType = DarkModeType::light; + g_dmCfg.m_windowsMode = WinMode::disabled; + break; + } + + case 2: + { + g_dmCfg.m_dmType = dmlib::isDarkModeReg() ? DarkModeType::dark : DarkModeType::light; + g_dmCfg.m_windowsMode = WinMode::light; + break; + } + + case 3: + { + g_dmCfg.m_dmType = DarkModeType::classic; + g_dmCfg.m_windowsMode = WinMode::disabled; + break; + } + + case 4: + { + g_dmCfg.m_dmType = dmlib::isDarkModeReg() ? DarkModeType::dark : DarkModeType::classic; + g_dmCfg.m_windowsMode = WinMode::classic; + break; + } + + case 1: + default: + { + g_dmCfg.m_dmType = DarkModeType::dark; + g_dmCfg.m_windowsMode = WinMode::disabled; + break; + } + } +} + +/** + * @brief Sets the preferred window corner style on Windows 11. + * + * Assigns a valid `DWM_WINDOW_CORNER_PREFERENCE` value to the config, + * falling back to `DWMWCP_DEFAULT` if the input is out of range. + * + * @param[in] roundCornerStyle Integer value representing a `DWM_WINDOW_CORNER_PREFERENCE`. + * + * @see https://learn.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwm_window_corner_preference + * @see dmlib::setDarkTitleBarEx() + */ +void dmlib::setRoundCornerConfig(UINT roundCornerStyle) +{ + const auto cornerStyle = static_cast(roundCornerStyle); + if (cornerStyle > DWMWCP_ROUNDSMALL) // || cornerStyle < DWMWCP_DEFAULT) // should never be < 0 + { + g_dmCfg.m_roundCorner = DWMWCP_DEFAULT; + } + else + { + g_dmCfg.m_roundCorner = cornerStyle; + } +} + +static constexpr DWORD kDwmwaClrDefaultRGBCheck = 0x00FFFFFF; + +/** + * @brief Sets the preferred border color for window edge on Windows 11. + * + * Assigns the given `COLORREF` to the configuration. If the value matches + * `kDwmwaClrDefaultRGBCheck`, the color is reset to `DWMWA_COLOR_DEFAULT`. + * + * @param[in] clr Border color value, or sentinel to reset to system default. + * + * @see DWMWA_BORDER_COLOR + * @see dmlib::setDarkTitleBarEx() + */ +void dmlib::setBorderColorConfig(COLORREF clr) +{ + if (clr == kDwmwaClrDefaultRGBCheck) + { + g_dmCfg.m_borderColor = DWMWA_COLOR_DEFAULT; + } + else + { + g_dmCfg.m_borderColor = clr; + } +} + +/** + * @brief Sets the Mica effects on Windows 11 setting. + * + * Assigns a valid `DWM_SYSTEMBACKDROP_TYPE` to the configuration. If the value exceeds + * `DWMSBT_TABBEDWINDOW`, it falls back to `DWMSBT_AUTO`. + * + * @param[in] mica Integer value representing a `DWM_SYSTEMBACKDROP_TYPE`. + * + * @see DWM_SYSTEMBACKDROP_TYPE + * @see dmlib::setDarkTitleBarEx() + */ +void dmlib::setMicaConfig(UINT mica) +{ + const auto micaType = static_cast(mica); + if (micaType > DWMSBT_TABBEDWINDOW) // || micaType < DWMSBT_AUTO) // should never be < 0 + { + g_dmCfg.m_mica = DWMSBT_AUTO; + } + else + { + g_dmCfg.m_mica = micaType; + } +} + +/** + * @brief Sets Mica effects on the full window setting. + * + * Controls whether Mica should be applied to the entire window + * or limited to the title bar only. + * + * @param[in] extendMica `true` to apply Mica to the full window, `false` for title bar only. + * + * @see dmlib::setDarkTitleBarEx() + */ +void dmlib::setMicaExtendedConfig(bool extendMica) +{ + g_dmCfg.m_micaExtend = extendMica; +} + +/** + * @brief Sets dialog colors on title bar on Windows 11 setting. + * + * Controls whether title bar should have same colors as dialog window. + * + * @param[in] colorize `true` to have title bar to have same colors as dialog window. + * + * @see dmlib::setDarkTitleBarEx() + */ +void dmlib::setColorizeTitleBarConfig(bool colorize) +{ + g_dmCfg.m_colorizeTitleBar = colorize; +} + +#ifndef _DARKMODELIB_NO_INI_CONFIG +/** + * @brief Initializes dark mode configuration and colors from an INI file. + * + * Loads configuration values from the specified INI file path and applies them to the + * current dark mode settings. This includes: + * - Base appearance (`DarkModeType`) and system-following mode (`WinMode`) + * - Optional Mica and rounded corner styling + * - Custom colors for background, text, borders, and headers (if present) + * - Tone settings for dark theme (`ColorTone`) + * + * If the INI file does not exist, default dark mode behavior is applied via + * @ref dmlib::setDarkModeConfigEx. + * + * @param[in] iniName Name of INI file (resolved via @ref getIniPath). + * + * @note When `DarkModeType::classic` is set, system colors are used instead of themed ones. + */ +static void initOptions(const std::wstring& iniName) +{ + if (iniName.empty()) + { + return; + } + + const auto iniPath = dmlib_ini::getIniPath(iniName); + g_dmCfg.m_iniExist = dmlib_ini::fileExists(iniPath); + if (g_dmCfg.m_iniExist) + { + dmlib::initDarkModeConfig(::GetPrivateProfileIntW(L"main", L"mode", 1, iniPath.c_str())); + if (g_dmCfg.m_dmType == dmlib::DarkModeType::classic) + { + dmlib::setDarkModeConfigEx(static_cast(dmlib::DarkModeType::classic)); + dmlib::setDefaultColors(false); + return; + } + + const bool useDark = g_dmCfg.m_dmType == dmlib::DarkModeType::dark; + + const std::wstring sectionBase = useDark ? L"dark" : L"light"; + const std::wstring sectionColorsView = sectionBase + L".colors.view"; + const std::wstring sectionColors = sectionBase + L".colors"; + + dmlib::setMicaConfig(::GetPrivateProfileIntW(sectionBase.c_str(), L"mica", 0, iniPath.c_str())); + dmlib::setRoundCornerConfig(::GetPrivateProfileIntW(sectionBase.c_str(), L"roundCorner", 0, iniPath.c_str())); + dmlib_ini::setClrFromIni(iniPath, sectionBase, L"borderColor", &g_dmCfg.m_borderColor); + if (g_dmCfg.m_borderColor == kDwmwaClrDefaultRGBCheck) + { + g_dmCfg.m_borderColor = DWMWA_COLOR_DEFAULT; + } + + getThemeView().resetColors(useDark); + + if (useDark) + { + UINT tone = ::GetPrivateProfileIntW(sectionBase.c_str(), L"tone", 0, iniPath.c_str()); + if (tone >= static_cast(dmlib::ColorTone::max)) + { + tone = 0; + } + + getTheme().setToneColors(static_cast(tone)); + getThemeView().setColorHeaderBackground(getTheme().getColors().background); + getThemeView().setColorHeaderHotBackground(getTheme().getColors().hotBackground); + getThemeView().setColorHeaderText(getTheme().getColors().darkerText); + + if (!dmlib::isWindowsModeEnabled()) + { + g_dmCfg.m_micaExtend = (::GetPrivateProfileIntW(sectionBase.c_str(), L"micaExtend", 0, iniPath.c_str()) == 1); + } + } + else + { + getTheme().setLightColors(); + } + + struct ColorEntry + { + const wchar_t* key = nullptr; + COLORREF* clr = nullptr; + }; + + static constexpr size_t nColorsViewMembers = 7; + const std::array viewColors{ { + {L"backgroundView", &getThemeView().getToSetColors().background}, + {L"textView", &getThemeView().getToSetColors().text}, + {L"gridlines", &getThemeView().getToSetColors().gridlines}, + {L"backgroundHeader", &getThemeView().getToSetColors().headerBackground}, + {L"backgroundHotHeader", &getThemeView().getToSetColors().headerHotBackground}, + {L"textHeader", &getThemeView().getToSetColors().headerText}, + {L"edgeHeader", &getThemeView().getToSetColors().headerEdge} + } }; + + static constexpr size_t nColorsMembers = 13; + const std::array baseColors{ { + {L"background", &getTheme().getToSetColors().background}, + {L"backgroundCtrl", &getTheme().getToSetColors().ctrlBackground}, + {L"backgroundHot", &getTheme().getToSetColors().hotBackground}, + {L"backgroundDlg", &getTheme().getToSetColors().dlgBackground}, + {L"backgroundError", &getTheme().getToSetColors().errorBackground}, + {L"text", &getTheme().getToSetColors().text}, + {L"textItem", &getTheme().getToSetColors().darkerText}, + {L"textDisabled", &getTheme().getToSetColors().disabledText}, + {L"textLink", &getTheme().getToSetColors().linkText}, + {L"edge", &getTheme().getToSetColors().edge}, + {L"edgeHot", &getTheme().getToSetColors().hotEdge}, + {L"edgeDisabled", &getTheme().getToSetColors().disabledEdge}, + {L"highlight", &getTheme().getToSetColors().highlight} + } }; + + for (const auto& entry : viewColors) + { + dmlib_ini::setClrFromIni(iniPath, sectionColorsView, entry.key, entry.clr); + } + + for (const auto& entry : baseColors) + { + dmlib_ini::setClrFromIni(iniPath, sectionColors, entry.key, entry.clr); + } + + dmlib::updateThemeBrushesAndPens(); + dmlib::updateViewBrushesAndPens(); + dmlib::calculateTreeViewStyle(); + + if (!g_dmCfg.m_micaExtend) + { + g_dmCfg.m_colorizeTitleBar = (::GetPrivateProfileIntW(sectionBase.c_str(), L"colorizeTitleBar", 0, iniPath.c_str()) == 1); + } + + dmlib_win32api::SetDarkMode(g_dmCfg.m_dmType == dmlib::DarkModeType::dark, true); + } + else + { + dmlib::setDarkModeConfigEx(static_cast(dmlib::DarkModeType::dark)); + dmlib::setDefaultColors(true); + } +} +#endif // !defined(_DARKMODELIB_NO_INI_CONFIG) + +/** + * @brief Applies dark mode settings based on the given configuration type. + * + * Initializes the dark mode type settings and system-following behavior. + * Enables or disables dark mode depending on whether `DarkModeType::dark` is selected. + * It is recommended to use together with @ref dmlib::setDefaultColors to also set colors. + * + * @param[in] dmType Dark mode configuration type; see @ref dmlib::initDarkModeConfig for values. + * + * @see dmlib::setDarkModeConfig() + * @see dmlib::initDarkModeConfig() + * @see dmlib::setDefaultColors() + */ +void dmlib::setDarkModeConfigEx(UINT dmType) +{ + dmlib::initDarkModeConfig(dmType); + + const bool useDark = g_dmCfg.m_dmType == DarkModeType::dark; + dmlib_win32api::SetDarkMode(useDark, true); +} + +/** + * @brief Applies dark mode settings based on system mode preference. + * + * Determines the appropriate mode using @ref dmlib::isDarkModeReg and forwards + * the result to @ref dmlib::setDarkModeConfigEx. + * It is recommended to use together with @ref dmlib::setDefaultColors to also set colors. + * + * Uses: + * - `DarkModeType::dark` if registry prefers dark mode. + * - `DarkModeType::classic` otherwise. + * + * @see dmlib::setDarkModeConfigEx() + */ +void dmlib::setDarkModeConfig() +{ + const auto dmType = static_cast(dmlib::isDarkModeReg() ? DarkModeType::dark : DarkModeType::classic); + dmlib::setDarkModeConfigEx(dmType); +} + +/** + * @brief Initializes dark mode experimental features, colors, and other settings. + * + * Performs one-time setup for dark mode, including: + * - Initializing experimental features if not yet done. + * - Optionally loading settings from an INI file (if INI config is enabled). + * - Initializing TreeView style and applying dark mode settings. + * - Preparing system colors (e.g. `COLOR_WINDOW`, `COLOR_WINDOWTEXT`, `COLOR_BTNFACE`) + * for hooking. + * + * @param[in] iniName Optional path to an INI file for dark mode settings (ignored if already set). + * + * @note This function is only run once per session; + * subsequent calls have no effect, unless follow system mode is used, + * then only colors are updated each time system changes mode. + * + * @see dmlib::initDarkMode() + * @see dmlib::calculateTreeViewStyle() + */ +void dmlib::initDarkModeEx([[maybe_unused]] const wchar_t* iniName) +{ + if (!g_dmCfg.m_isInit) + { + if (!g_dmCfg.m_isInitExperimental) + { + dmlib_win32api::InitDarkMode(); + dmlib_dpi::InitDpiAPI(); + g_dmCfg.m_isInitExperimental = true; + } + +#ifndef _DARKMODELIB_NO_INI_CONFIG + if (!g_dmCfg.m_isIniNameSet) + { + g_dmCfg.m_iniName = iniName; + g_dmCfg.m_isIniNameSet = true; + + if (g_dmCfg.m_iniName.empty()) + { + dmlib::setDarkModeConfigEx(static_cast(DarkModeType::dark)); + dmlib::setDefaultColors(true); + } + } + initOptions(g_dmCfg.m_iniName); +#else + dmlib::setDarkModeConfig(); + dmlib::setDefaultColors(true); +#endif + + dmlib::setSysColor(COLOR_WINDOW, dmlib::getCtrlBackgroundColor()); + dmlib::setSysColor(COLOR_WINDOWTEXT, dmlib::getTextColor()); + dmlib::setSysColor(COLOR_BTNFACE, dmlib::getViewGridlinesColor()); + + g_dmCfg.m_isInit = true; + } +} + +/** + * @brief Initializes dark mode without INI settings. + * + * Forwards to @ref dmlib::initDarkModeEx with an empty INI path, effectively disabling INI settings. + * + * @see dmlib::initDarkModeEx() + */ +void dmlib::initDarkMode() +{ + dmlib::initDarkModeEx(L""); +} + +/** + * @brief Checks if there is config INI file. + * + * @return `true` if there is config INI file that can be used. + */ +bool dmlib::doesConfigFileExist() +{ +#ifndef _DARKMODELIB_NO_INI_CONFIG + return g_dmCfg.m_iniExist; +#else + return false; +#endif +} + +/** + * @brief Checks if non-classic mode is enabled. + * + * If `_DARKMODELIB_ALLOW_OLD_OS` is defined with value larger than '1', + * this skips Windows version checks. Otherwise, dark mode is only enabled + * on Windows 10 or newer. + * + * @return `true` if a supported dark mode type is active, otherwise `false`. + */ +bool dmlib::isEnabled() +{ +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 1) + return g_dmCfg.m_dmType != DarkModeType::classic; +#else + return dmlib::isAtLeastWindows10() && g_dmCfg.m_dmType != DarkModeType::classic; +#endif +} + +/** + * @brief Checks if experimental dark mode features are currently active. + * + * @return `true` if experimental dark mode is enabled. + */ +bool dmlib::isExperimentalActive() +{ + return dmlib_win32api::IsDarkModeActive(); +} + +/** + * @brief Checks if experimental dark mode features are supported by the system. + * + * @return `true` if dark mode experimental APIs are available. + */ +bool dmlib::isExperimentalSupported() +{ + return dmlib_win32api::IsDarkModeSupported(); +} + +/** + * @brief Checks if follow the system mode behavior is enabled. + * + * @return `true` if "mode" is not `WinMode::disabled`, i.e. system mode is followed. + */ +bool dmlib::isWindowsModeEnabled() +{ + return g_dmCfg.m_windowsMode != WinMode::disabled; +} + +/** + * @brief Checks if the host OS is at least Windows 10. + * + * @return `true` if running on Windows 10 or newer. + */ +bool dmlib::isAtLeastWindows10() +{ + return dmlib_win32api::IsWindows10(); +} +/** + * @brief Checks if the host OS is at least Windows 11. + * + * @return `true` if running on Windows 11 or newer. + */ +bool dmlib::isAtLeastWindows11() +{ + return dmlib_win32api::IsWindows11(); +} + +/** + * @brief Retrieves the current Windows build number. + * + * @return Windows build number reported by the system. + */ +DWORD dmlib::getWindowsBuildNumber() +{ + return dmlib_win32api::GetWindowsBuildNumber(); +} + +/// Check if OS is Windows 11 version 24H2 build 26100 rev 6899+, +/// Windows 11 version 25H2 build 26200 rev 6899+, or later builds. +static bool doesWin11SupportDarkThemeStyle() noexcept +{ + static const DWORD win11Revision = []() noexcept + { + DWORD revisionReg = 0; + DWORD dwBufSize = sizeof(revisionReg); + static constexpr LPCWSTR lpSubKey = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; + static constexpr LPCWSTR lpValue = L"UBR"; + + if (::RegGetValueW(HKEY_LOCAL_MACHINE, lpSubKey, lpValue, RRF_RT_REG_DWORD, nullptr, &revisionReg, &dwBufSize) == ERROR_SUCCESS) + { + return revisionReg; + } + return 0UL; + }(); + + static constexpr DWORD win11Build24H2 = 26100; + static constexpr DWORD win11Build25H2 = 26200; + static constexpr DWORD minWin11Revision = 6899; + return dmlib_win32api::GetWindowsBuildNumber() > win11Build25H2 + || (dmlib_win32api::GetWindowsBuildNumber() == win11Build25H2 && win11Revision >= minWin11Revision) + || (dmlib_win32api::GetWindowsBuildNumber() == win11Build24H2 && win11Revision >= minWin11Revision); +} + +/** + * @brief Handles system setting changes related to dark mode. + * + * Responds to system messages indicating a color scheme change. If the current + * dark mode state no longer matches the system registry preference, dark mode is + * re-initialized. + * + * - Skips processing if experimental dark mode is unsupported. + * - Relies on @ref dmlib::isDarkModeReg for theme preference and skips during high contrast. + * + * @param[in] lParam Message parameter (typically from `WM_SETTINGCHANGE`). + * @return `true` if a dark mode change was handled; otherwise `false`. + * + * @see dmlib::isDarkModeReg() + * @see dmlib::initDarkMode() + */ +bool dmlib::handleSettingChange(LPARAM lParam) +{ + if (dmlib::isExperimentalSupported() + && dmlib_win32api::IsColorSchemeChangeMessage(lParam)) + { + // fnShouldAppsUseDarkMode (ordinal 132) is not reliable on 1903+, use dmlib::isDarkModeReg() instead + if (const bool isDarkModeUsed = (dmlib::isDarkModeReg() && !dmlib_win32api::IsHighContrast()); + dmlib::isExperimentalActive() != isDarkModeUsed + && g_dmCfg.m_isInit) + { + g_dmCfg.m_isInit = false; + dmlib::initDarkMode(); + } + return true; + } + return false; +} + +/** + * @brief Checks if dark mode is enabled in the Windows registry. + * + * Queries `HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\\AppsUseLightTheme`. + * + * @return `true` if dark mode is preferred (value is `0`); otherwise `false`. + */ +bool dmlib::isDarkModeReg() +{ + DWORD data{}; + DWORD dwBufSize = sizeof(data); + static constexpr LPCWSTR lpSubKey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + static constexpr LPCWSTR lpValue = L"AppsUseLightTheme"; + + if (::RegGetValueW(HKEY_CURRENT_USER, lpSubKey, lpValue, RRF_RT_REG_DWORD, nullptr, &data, &dwBufSize) == ERROR_SUCCESS) + { + // dark mode is 0, light mode is 1 + return data == 0UL; + } + return false; +} + +/** + * @brief Overrides a specific system color with a custom color. + * + * Currently supports: + * - `COLOR_WINDOW`: Background of ComboBoxEx list. + * - `COLOR_WINDOWTEXT`: Text color of ComboBoxEx list. + * - `COLOR_BTNFACE`: Gridline color in ListView (when applicable). + * + * @param[in] nIndex One of the supported system color indices. + * @param[in] color Custom `COLORREF` value to apply. + */ +void dmlib::setSysColor(int nIndex, COLORREF color) +{ + dmlib_hook::setMySysColor(nIndex, color); +} + +/** + * @brief Makes scroll bars on the specified window and all its children consistent. + * + * @note Currently not widely used by default. + * + * @param[in] hWnd Handle to the parent window. + */ +void dmlib::enableDarkScrollBarForWindowAndChildren([[maybe_unused]] HWND hWnd) +{ +#if defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 0) + dmlib_hook::enableDarkScrollBarForWindowAndChildren(hWnd); +#endif +} + +/** + * @brief Checks if current mode is dark type. + */ +bool dmlib::isDarkDmTypeUsed() noexcept +{ + return g_dmCfg.m_dmType == dmlib::DarkModeType::dark; +} + +/** + * @brief Applies themed owner drawn subclassing to a checkbox, radio, or tri-state button control. + * + * Associates a `ButtonData` instance with the control. + * + * @param[in] hWnd Handle to the checkbox, radio, or tri-state button control. + * + * @see dmlib_subclass::ButtonSubclass() + * @see dmlib::removeCheckboxOrRadioBtnCtrlSubclass() + */ +void dmlib::setCheckboxOrRadioBtnCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::ButtonSubclass, dmlib_subclass::SubclassID::button, hWnd); +} + +/** + * @brief Removes the owner drawn subclass from a checkbox, radio, or tri-state button control. + * + * Cleans up the `ButtonData` instance and detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the previously subclassed control. + * + * @see dmlib_subclass::ButtonSubclass() + * @see dmlib::setCheckboxOrRadioBtnCtrlSubclass() + */ +void dmlib::removeCheckboxOrRadioBtnCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::ButtonSubclass, dmlib_subclass::SubclassID::button); +} + +/** + * @brief Applies owner drawn subclassing to a groupbox button control. + * + * Associates a `ButtonData` instance with the control. + * + * @param[in] hWnd Handle to the groupbox button control. + * + * @see dmlib_subclass::GroupboxSubclass() + * @see dmlib::removeGroupboxCtrlSubclass() + */ +void dmlib::setGroupboxCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::GroupboxSubclass, dmlib_subclass::SubclassID::groupbox); +} + +/** + * @brief Removes the owner drawn subclass from a groupbox button control. + * + * Cleans up the `ButtonData` instance and detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the previously subclassed control. + * + * @see dmlib_subclass::GroupboxSubclass() + * @see dmlib::setGroupboxCtrlSubclass() + */ +void dmlib::removeGroupboxCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::GroupboxSubclass, dmlib_subclass::SubclassID::groupbox); +} + +/** + * @brief Applies theming and/or subclassing to a button control based on its style. + * + * Inspects the control's style (`BS_*`) to determine its visual category and applies + * apropriate theming and/or subclassing accordingly. Handles: + * - Checkbox/radio/tri-state buttons: Applies theme (optional) and optional subclassing + * - Group boxes: Applies subclassing for dark mode drawing + * - Push buttons: Applies visual theming if requested + * + * The behavior varies depending on dark mode support, Windows version, and the flags + * provided in @ref DarkModeParams. + * + * @param[in] hWnd Handle to the target button control. + * @param[in] p Parameters defining theming and subclassing behavior. + * + * @see DarkModeParams + * @see dmlib::setCheckboxOrRadioBtnCtrlSubclass() + * @see dmlib::setGroupboxCtrlSubclass() + */ +static void setBtnCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p) noexcept +{ + const auto nBtnStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + switch (nBtnStyle & BS_TYPEMASK) + { + case BS_CHECKBOX: + case BS_AUTOCHECKBOX: + case BS_3STATE: + case BS_AUTO3STATE: + case BS_RADIOBUTTON: + case BS_AUTORADIOBUTTON: + { + if ((nBtnStyle & BS_PUSHLIKE) == BS_PUSHLIKE) + { + if (p.m_theme) + { + ::SetWindowTheme(hWnd, p.m_themeClassName, nullptr); + } + break; + } + + if (dmlib::isAtLeastWindows11() && p.m_theme) + { + ::SetWindowTheme(hWnd, p.m_themeClassName, nullptr); + } + + if (p.m_subclass) + { + dmlib::setCheckboxOrRadioBtnCtrlSubclass(hWnd); + } + break; + } + + case BS_GROUPBOX: + { + if (p.m_subclass) + { + dmlib::setGroupboxCtrlSubclass(hWnd); + } + break; + } + + case BS_PUSHBUTTON: + case BS_DEFPUSHBUTTON: + case BS_SPLITBUTTON: + case BS_DEFSPLITBUTTON: + { + if (p.m_theme) + { + ::SetWindowTheme(hWnd, p.m_themeClassName, nullptr); + } + break; + } + + default: + { + break; + } + } +} + +/** + * @brief Applies owner drawn subclassing and theming to an up-down (spinner) control. + * + * Associates a `UpDownData` instance with the control. + * + * @param[in] hWnd Handle to the up-down (spinner) control. + * + * @see dmlib_subclass::UpDownSubclass() + * @see dmlib::removeUpDownCtrlSubclass() + */ +void dmlib::setUpDownCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::UpDownSubclass, dmlib_subclass::SubclassID::upDown, hWnd); +} + +/** + * @brief Removes the owner drawn subclass from a up-down (spinner) control. + * + * Cleans up the `UpDownData` instance and detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the previously subclassed control. + * + * @see dmlib_subclass::UpDownSubclass() + * @see dmlib::setUpDownCtrlSubclass() + */ +void dmlib::removeUpDownCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::UpDownSubclass, dmlib_subclass::SubclassID::upDown); +} + +/** + * @brief Applies up-down (spinner) control theming and/or subclassing based on specified parameters. + * + * Conditionally applies custom subclassing and/or themed appearance + * depending on `DarkModeParams`. Subclassing takes priority if both are requested. + * + * @param[in] hWnd Handle to the up-down control. + * @param[in] p Parameters controlling whether to apply theming and/or subclassing. + * + * @see DarkModeParams + * @see dmlib::setUpDownCtrlSubclass() + */ +static void setUpDownCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p) +{ + if (p.m_theme) + { + ::SetWindowTheme(hWnd, p.m_themeClassName, nullptr); + } + + if (p.m_subclass) + { + dmlib::setUpDownCtrlSubclass(hWnd); + } +} + +/** + * @brief Applies owner drawn subclassing to a tab control. + * + * @param[in] hWnd Handle to the tab control. + * + * @see dmlib_subclass::TabPaintSubclass() + * @see removeTabCtrlPaintSubclass() + */ +static void setTabCtrlPaintSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::TabPaintSubclass, dmlib_subclass::SubclassID::tabPaint); +} + +/** + * @brief Removes the owner drawn subclass from a tab control. + * + * Cleans up the `TabData` instance and detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the previously subclassed tab control. + * + * @see dmlib_subclass::TabPaintSubclass() + * @see setTabCtrlPaintSubclass() + */ +static void removeTabCtrlPaintSubclass(HWND hWnd) noexcept +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::TabPaintSubclass, dmlib_subclass::SubclassID::tabPaint); +} + +/** + * @brief Applies a subclass to detect and subclass tab control's up-down (spinner) child. + * + * Enable automatic subclassing of the up-down (spinner) control + * when it's created dynamically (for `TCS_SCROLLOPPOSITE` or overflow). + * + * @param[in] hWnd Handle to the tab control. + * + * @see dmlib_subclass::TabUpDownSubclass() + * @see dmlib::removeTabCtrlUpDownSubclass() + */ +void dmlib::setTabCtrlUpDownSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::TabUpDownSubclass, dmlib_subclass::SubclassID::tabUpDown); +} + +/** + * @brief Removes the subclass procedure for a tab control's up-down (spinner) child detection. + * + * Detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the previously subclassed tab control. + * + * @see dmlib_subclass::TabUpDownSubclass() + * @see dmlib::setTabCtrlUpDownSubclass() + */ +void dmlib::removeTabCtrlUpDownSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::TabUpDownSubclass, dmlib_subclass::SubclassID::tabUpDown); +} + +/** + * @brief Applies owner drawn and up-down (spinner) child detection subclassings for a tab control. + * + * Applies both @ref dmlib::TabPaintSubclass() for custom drawing + * and @ref dmlib::TabUpDownSubclass() for detecting and subclassing + * the associated up-down (spinner) control. + * + * @param[in] hWnd Handle to the tab control. + * + * @see dmlib::removeTabCtrlSubclass() + * @see setTabCtrlPaintSubclass() + * @see dmlib::setTabCtrlUpDownSubclass() + */ +void dmlib::setTabCtrlSubclass(HWND hWnd) +{ + setTabCtrlPaintSubclass(hWnd); + dmlib::setTabCtrlUpDownSubclass(hWnd); +} + +/** + * @brief Removes owner drawn and up-down (spinner) child detection subclasses. + * + * Detaches the control's subclass procs. + * + * @param[in] hWnd Handle to the previously subclassed tab control. + * + * @see dmlib::setTabCtrlSubclass() + * @see removeTabCtrlPaintSubclass() + * @see dmlib::removeTabCtrlUpDownSubclass() + */ +void dmlib::removeTabCtrlSubclass(HWND hWnd) +{ + removeTabCtrlPaintSubclass(hWnd); + dmlib::removeTabCtrlUpDownSubclass(hWnd); +} + +/** + * @brief Applies tab control theming and subclassing based on specified parameters. + * + * Conditionally applies tooltip theming and tab control subclassing + * depending on `DarkModeParams`. + * + * @param[in] hWnd Handle to the tab control. + * @param[in] p Parameters controlling whether to apply theming and/or subclassing. + * + * @see DarkModeParams + * @see dmlib::setDarkTooltips() + * @see dmlib::setTabCtrlSubclass() + */ +static void setTabCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_theme) + { + dmlib::setDarkTooltips(hWnd, static_cast(dmlib::ToolTipsType::tabbar)); + if (doesWin11SupportDarkThemeStyle() && dmlib_subclass::isThemePrefered()) + { + dmlib::setDarkThemeTheme(hWnd); + } + } + + if (p.m_subclass && !dmlib_subclass::isThemePrefered()) + { + dmlib::setTabCtrlSubclass(hWnd); + } +} + +/** + * @brief Applies owner drawn custom border subclassing to a list box or edit control. + * + * @param[in] hWnd Handle to the list box or edit control. + * + * @see dmlib_subclass::CustomBorderSubclass() + * @see dmlib::removeCustomBorderForListBoxOrEditCtrlSubclass() + */ +void dmlib::setCustomBorderForListBoxOrEditCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::CustomBorderSubclass, dmlib_subclass::SubclassID::customBorder, hWnd); +} + +/** + * @brief Removes the custom border subclass from a list box or edit control. + * + * Cleans up the `BorderMetricsData` and detaches the control's subclass proc, + * restoring the control's default border drawing. + * + * @param[in] hWnd Handle to the previously subclassed control. + * + * @see dmlib_subclass::CustomBorderSubclass() + * @see dmlib::setCustomBorderForListBoxOrEditCtrlSubclass() + */ +void dmlib::removeCustomBorderForListBoxOrEditCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::CustomBorderSubclass, dmlib_subclass::SubclassID::customBorder); +} + +/** + * @brief Applies theming and optional custom border subclassing to a list box or edit control. + * + * Conditionally configures the visual style of a list box or edit control + * depending on `DarkModeParams`, control type, and window styles. + * Applies a custom border subclass for controls with `WS_EX_CLIENTEDGE` flag. + * Toggle the client edge style depending on dark mode state. + * + * @param[in] hWnd Handle to the target list box or edit control. + * @param[in] p Parameters controlling whether to apply theming and/or subclassing. + * @param[in] isListBox `true` if the control is a list box, `false` if it's an edit control. + * + * @note Custom border subclassing is skipped for combo box list boxes. + * + * @see DarkModeParams + * @see dmlib::setCustomBorderForListBoxOrEditCtrlSubclass() + */ +static void setCustomBorderForListBoxOrEditCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p, bool isListBox) +{ + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + const bool hasScrollBar = ((nStyle & WS_HSCROLL) == WS_HSCROLL) || ((nStyle & WS_VSCROLL) == WS_VSCROLL); + + // edit control without scroll bars + if (dmlib_subclass::isThemePrefered() + && p.m_theme + && !isListBox + && !hasScrollBar) + { + if (doesWin11SupportDarkThemeStyle()) + { + dmlib::setDarkThemeTheme(hWnd); + } + else + { + dmlib::setDarkThemeExperimentalEx(hWnd, L"CFD"); + } + } + else + { + if (p.m_theme && (isListBox || hasScrollBar)) + { + // dark scroll bars for list box or edit control + ::SetWindowTheme(hWnd, p.m_themeClassName, nullptr); + } + + const auto nExStyle = ::GetWindowLongPtr(hWnd, GWL_EXSTYLE); + const bool hasClientEdge = (nExStyle & WS_EX_CLIENTEDGE) == WS_EX_CLIENTEDGE; + + if (const bool isCBoxListBox = isListBox && (nStyle & LBS_COMBOBOX) == LBS_COMBOBOX; + p.m_subclass && hasClientEdge && !isCBoxListBox) + { + dmlib::setCustomBorderForListBoxOrEditCtrlSubclass(hWnd); + } + + if (::GetWindowSubclass(hWnd, dmlib_subclass::CustomBorderSubclass, static_cast(dmlib_subclass::SubclassID::customBorder), nullptr) == TRUE) + { + const bool enableClientEdge = !dmlib::isEnabled(); + dmlib::setWindowExStyle(hWnd, enableClientEdge, WS_EX_CLIENTEDGE); + } + } +} + +/** + * @brief Applies owner drawn subclassing to a combo box control. + * + * Retrieves the combo box style from the window and passes it to the subclass data + * (`ComboBoxData`) so the paint routine can adapt to that combo box style. + * + * @param[in] hWnd Handle to the combo box control. + * + * @note Uses `GetWindowLongPtr` to extract the style bits. + * + * @see dmlib_subclass::ComboBoxSubclass() + * @see dmlib::removeComboBoxCtrlSubclass() + */ +void dmlib::setComboBoxCtrlSubclass(HWND hWnd) +{ + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + const auto cbStyle = nStyle & CBS_DROPDOWNLIST; + if (cbStyle != CBS_DROPDOWNLIST) + { + ::SetWindowLongPtr(hWnd, GWL_STYLE, nStyle | WS_CLIPCHILDREN); + } + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::ComboBoxSubclass, dmlib_subclass::SubclassID::comboBox, cbStyle); +} + +/** + * @brief Removes the owner drawn subclass from a combo box control. + * + * Cleans up the `ComboBoxData` and detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the combo box control. + * + * @see dmlib_subclass::ComboBoxSubclass() + * @see dmlib::setComboBoxCtrlSubclass() + */ +void dmlib::removeComboBoxCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::ComboBoxSubclass, dmlib_subclass::SubclassID::comboBox); +} + +/** + * @brief Applies theming and optional subclassing to a combo box control. + * + * Configures a combo box' appearance and behavior based on its style, + * the provided parameters, and current theme preferences. + * + * Behavior: + * - If theming is enabled (`p.m_theme`) and the combo box has an associated list box: + * - For `CBS_SIMPLE`, replaces the client edge with a custom border for non-classic mode. + * - Applies themed scroll bars. + * - If subclassing is enabled (`p.m_subclass`) and dark mode is not the preferred theme: + * - Applies a combo box subclassing unless the parent is a `ComboBoxEx` control. + * - If theming is enabled (`p.m_theme`): + * - Applies the experimental `"CFD"` dark theme to the combo box for a light drop-down arrow. + * - Clears the edit selection for non-`CBS_DROPDOWNLIST` styles to avoid visual artifacts. + * + * @param[in] hWnd Handle to the combo box control. + * @param[in] p Parameters controlling whether to apply theming and/or subclassing. + * + * @note Skips subclassing for `ComboBoxEx` parents to avoid conflicts. + * + * @see DarkModeParams + * @see dmlib::setComboBoxCtrlSubclass() + */ +static void setComboBoxCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p) +{ + const auto cbStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE) & CBS_DROPDOWNLIST; + const bool isCbList = cbStyle == CBS_DROPDOWNLIST; + const bool isCbSimple = cbStyle == CBS_SIMPLE; + + if (cbStyle == 0) // something wrong happened + { + return; + } + + COMBOBOXINFO cbi{}; + cbi.cbSize = sizeof(COMBOBOXINFO); + if (::GetComboBoxInfo(hWnd, &cbi) == TRUE + && p.m_theme + && cbi.hwndList != nullptr) + { + if (isCbSimple) + { + dmlib::replaceClientEdgeWithBorderSafe(cbi.hwndList); + } + + // dark scroll bar for list box of combo box + ::SetWindowTheme(cbi.hwndList, p.m_themeClassName, nullptr); + } + + if (!dmlib_subclass::isThemePrefered() && p.m_subclass) + { + if (HWND hParent = ::GetParent(hWnd); + hParent == nullptr + || dmlib_subclass::getWndClassName(hParent) != WC_COMBOBOXEX) + { + dmlib::setComboBoxCtrlSubclass(hWnd); + } + } + + if (p.m_theme) // for light dropdown arrow in dark mode + { + if (HWND hParent = ::GetParent(hWnd); + doesWin11SupportDarkThemeStyle() + && (hParent == nullptr + || dmlib_subclass::getWndClassName(hParent) != WC_COMBOBOXEX)) + { + dmlib::setDarkThemeTheme(hWnd); + } + else + { + dmlib::setDarkThemeExperimentalEx(hWnd, L"CFD"); + } + + if (!isCbList) + { + ::SendMessage(hWnd, CB_SETEDITSEL, 0, 0); // clear selection + } + } +} + +/** + * @brief Applies subclassing to a ComboBoxEx control to handle its child list box and edit controls. + * + * @param[in] hWnd Handle to the ComboBoxEx control. + * + * @note Uses IAT hooking for custom colors. + * + * @see dmlib_subclass::ComboBoxSubclass() + * @see dmlib::removeComboBoxExCtrlSubclass() + */ +void dmlib::setComboBoxExCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::ComboBoxExSubclass, dmlib_subclass::SubclassID::comboBoxEx); +} + +/** + * @brief Removes the child handling subclass from a ComboBoxEx control. + * + * Detaches the control's subclass proc and unhooks system color changes. + * + * @param[in] hWnd Handle to the ComboBoxEx control. + * + * @see dmlib_subclass::ComboBoxSubclass() + * @see dmlib::setComboBoxExCtrlSubclass() + */ +void dmlib::removeComboBoxExCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::ComboBoxExSubclass, dmlib_subclass::SubclassID::comboBoxEx); + dmlib_hook::unhookSysColor(); +} + +/** + * @brief Applies subclassing to a ComboBoxEx control to handle its child list box and edit controls. + * + * Overload wrapper that applies the subclass only if `p.m_subclass` is `true`. + * + * @param[in] hWnd Handle to the ComboBoxEx control. + * @param[in] p Parameters controlling whether to apply subclassing. + * + * @see dmlib::setComboBoxExCtrlSubclass() + */ +static void setComboBoxExCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_subclass) + { + dmlib::setComboBoxExCtrlSubclass(hWnd); + } +} + +/** + * @brief Applies subclassing to a list view control to handle custom colors. + * + * Handles custom colors for gridlines, header text, and in-place edit controls. + * + * @param[in] hWnd Handle to the list view control. + * + * @note Uses IAT hooking for gridlines colors. + * + * @see dmlib_subclass::ListViewSubclass() + * @see dmlib::removeListViewCtrlSubclass() + */ +void dmlib::setListViewCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::ListViewSubclass, dmlib_subclass::SubclassID::listView); +} + +/** + * @brief Removes the custom colors handling subclass from a list view control. + * + * Detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the list view control. + * + * @see dmlib_subclass::ListViewSubclass() + * @see dmlib::setListViewCtrlSubclass() + */ +void dmlib::removeListViewCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::ListViewSubclass, dmlib_subclass::SubclassID::listView); +} + +/** + * @brief Applies theming and optional subclassing to a list view control. + * + * Configures colors, header theming, checkbox styling, and tooltips theming. + * Optionally installs the list view and header control subclasses for custom drawing. + * Enables double-buffering via `LVS_EX_DOUBLEBUFFER` flag. + * + * @param[in] hWnd Handle to the list view control. + * @param[in] p Parameters controlling whether to apply theming and/or subclassing. + * + * @see dmlib::setDarkListView() + * @see dmlib::setDarkListViewCheckboxes() + * @see dmlib::setDarkTooltips() + * @see dmlib::setListViewCtrlSubclass() + * @see dmlib::setHeaderCtrlSubclass() + */ +static void setListViewCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p) +{ + auto* hHeader = ListView_GetHeader(hWnd); + + if (p.m_theme) + { + ListView_SetTextColor(hWnd, dmlib::getViewTextColor()); + ListView_SetTextBkColor(hWnd, dmlib::getViewBackgroundColor()); + ListView_SetBkColor(hWnd, dmlib::getViewBackgroundColor()); + + dmlib::setDarkListView(hWnd); + dmlib::setDarkListViewCheckboxes(hWnd); + dmlib::setDarkTooltips(hWnd, static_cast(dmlib::ToolTipsType::listview)); + + if (dmlib_subclass::isThemePrefered()) + { + if (doesWin11SupportDarkThemeStyle()) + { + dmlib::setDarkThemeTheme(hHeader); + } + else + { + dmlib::setDarkThemeExperimentalEx(hHeader, L"ItemsView"); + } + } + + const bool isDisabled = dmlib::isEnabled() && ::IsWindowEnabled(hWnd) == FALSE; + dmlib::replaceClientEdgeWithBorderSafeEx(hWnd, isDisabled); + } + + if (p.m_subclass) + { + if (!dmlib_subclass::isThemePrefered()) + { + dmlib::setHeaderCtrlSubclass(hHeader); + } + + const auto lvExStyle = ListView_GetExtendedListViewStyle(hWnd); + ListView_SetExtendedListViewStyle(hWnd, lvExStyle | LVS_EX_DOUBLEBUFFER); + dmlib::setListViewCtrlSubclass(hWnd); + } +} + +/** + * @brief Applies owner drawn subclassing to a header control. + * + * Retrieves the header button style from the window and passes it to the subclass data + * (`HeaderData`) so the paint routine can adapt to that header style. + * + * @param[in] hWnd Handle to the header control. + * + * @note Uses `GetWindowLongPtr` to extract the style bits. + * + * @see dmlib_subclass::HeaderSubclass() + * @see dmlib::removeHeaderCtrlSubclass() + */ +void dmlib::setHeaderCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::HeaderSubclass, dmlib_subclass::SubclassID::header, hWnd); +} + +/** + * @brief Removes the owner drawn subclass from a header control. + * + * Cleans up the `HeaderData` and detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the header control. + * + * @see dmlib_subclass::HeaderSubclass() + * @see dmlib::setHeaderCtrlSubclass() + */ +void dmlib::removeHeaderCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::HeaderSubclass, dmlib_subclass::SubclassID::header); +} + +/** + * @brief Applies owner drawn subclassing to a status bar control. + * + * Retrieves the status bar system font and passes it to the subclass data + * (`StatusBarData`). + * + * @param[in] hWnd Handle to the status bar control. + * + * @note Uses `SystemParametersInfoW` to extract the `lfStatusFont` font. + * + * @see dmlib_subclass::StatusBarSubclass() + * @see dmlib::removeStatusBarCtrlSubclass() + */ +void dmlib::setStatusBarCtrlSubclass(HWND hWnd) +{ + const auto lf = LOGFONT{ dmlib_dpi::getSysFontForDpi(::GetParent(hWnd), dmlib_dpi::FontType::status) }; + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::StatusBarSubclass, dmlib_subclass::SubclassID::statusBar, ::CreateFontIndirectW(&lf)); +} + +/** + * @brief Removes the owner drawn subclass from a status bar control. + * + * Cleans up the `StatusBarData` and detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the status bar control. + * + * @see dmlib_subclass::StatusBarSubclass() + * @see dmlib::setStatusBarCtrlSubclass() + */ +void dmlib::removeStatusBarCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::StatusBarSubclass, dmlib_subclass::SubclassID::statusBar); +} + +/** + * @brief Applies owner drawn subclassing to a status bar control. + * + * Overload wrapper that applies the subclass only if `p.m_subclass` is `true`. + * + * @param[in] hWnd Handle to the status bar control. + * @param[in] p Parameters controlling whether to apply subclassing. + * + * @see dmlib::setStatusBarCtrlSubclass() + */ +static void setStatusBarCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_theme + && dmlib_subclass::isThemePrefered() + && doesWin11SupportDarkThemeStyle()) + { + dmlib::setDarkThemeTheme(hWnd); + } + else if (p.m_subclass) + { + dmlib::setStatusBarCtrlSubclass(hWnd); + } +} + +/** + * @brief Applies owner drawn subclassing to a progress bar control. + * + * Retrieves the progress bar state information and passes it to the subclass data + * (`ProgressBarData`). + * + * @param[in] hWnd Handle to the progress bar control. + * + * @note Uses `PBM_GETSTATE` to determine the current visual state. + * + * @see dmlib_subclass::ProgressBarSubclass() + * @see dmlib::removeProgressBarCtrlSubclass() + */ +void dmlib::setProgressBarCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::ProgressBarSubclass, dmlib_subclass::SubclassID::progressBar, hWnd); +} + +/** + * @brief Removes the owner drawn subclass from a progress bar control. + * + * Cleans up the `ProgressBarData` and detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the progress bar control. + * + * @see dmlib_subclass::ProgressBarSubclass() + * @see dmlib::setProgressBarCtrlSubclass() + */ +void dmlib::removeProgressBarCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::ProgressBarSubclass, dmlib_subclass::SubclassID::progressBar); +} + +/** + * @brief Applies theming or subclassing to a progress bar control based on style and parameters. + * + * Conditionally applies either the classic theme or applies the owner drawn subclassing + * depending on the control style and `DarkModeParams`. + * + * Behavior: + * - If `p.m_theme` is `true` and the control uses `PBS_MARQUEE`, applies classic theme. + * - Otherwise, if `p.m_subclass` is `true`, applies owner drawn subclassing. + * + * @param[in] hWnd Handle to the progress bar control. + * @param[in] p Parameters controlling whether to apply theming or subclassing. + * + * @see dmlib::setProgressBarClassicTheme() + * @see dmlib::setProgressBarCtrlSubclass() + */ +static void setProgressBarCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept +{ + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + if (p.m_theme) + { + if ((nStyle & PBS_MARQUEE) == PBS_MARQUEE) + { + dmlib::setProgressBarClassicTheme(hWnd); + } + else if (doesWin11SupportDarkThemeStyle()) + { + ::SetWindowTheme(hWnd, dmlib::isExperimentalActive() ? L"DarkMode_CopyEngine" : nullptr, nullptr); + } + } + + if (p.m_subclass && ((nStyle & PBS_MARQUEE) != PBS_MARQUEE)) + { + dmlib::setProgressBarCtrlSubclass(hWnd); + } +} + +/** + * @brief Applies workaround subclassing to a static control to handle visual glitch in disabled state. + * + * Retrieves the static control enabled state information and passes it to the subclass data + * (`StaticTextData`) to handle visual glitch with static text in disabled state + * via handling `WM_ENABLE` message. + * + * @param[in] hWnd Handle to the static control. + * + * @note + * - Uses `IsWindowEnabled` to determine the current enabled state. + * - Works only if `WM_ENABLE` message is sent. + * + * @see dmlib_subclass::StaticTextSubclass() + * @see dmlib::removeStaticTextCtrlSubclass() + */ +void dmlib::setStaticTextCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::StaticTextSubclass, dmlib_subclass::SubclassID::staticText, hWnd); +} + +/** + * @brief Removes the workaround subclass from a static control. + * + * Cleans up the `StaticTextData` and detaches the control's subclass proc. + * + * @param[in] hWnd Handle to the static control. + * + * @see dmlib_subclass::StaticTextSubclass() + * @see dmlib::setStaticTextCtrlSubclass() + */ +void dmlib::removeStaticTextCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::StaticTextSubclass, dmlib_subclass::SubclassID::staticText); +} + +/** + * @brief Applies workaround subclassing to a static control. + * + * Overload wrapper that applies the subclass only if `p.m_subclass` is `true`. + * + * @param[in] hWnd Handle to the static control. + * @param[in] p Parameters controlling whether to apply subclassing. + * + * @see dmlib::setStaticTextCtrlSubclass() + */ +static void setStaticTextCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_subclass) + { + dmlib::setStaticTextCtrlSubclass(hWnd); + } +} + +/** + * @brief Applies owner drawn subclassing to a IP address control. + * + * Handles custom colors for itself and its child edit controls. + * + * @param[in] hWnd Handle to the IP address control. + * + * @see dmlib_subclass::IPAddressSubclass() + * @see dmlib::removeIPAddressCtrlSubclass() + */ +void dmlib::setIPAddressCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::IPAddressSubclass, dmlib_subclass::SubclassID::ipAddress); +} + +/** + * @brief Removes the owner drawn subclass from a IP address control. + * + * @param[in] hWnd Handle to the IP address control. + * + * @see dmlib_subclass::IPAddressSubclass() + * @see dmlib::setIPAddressCtrlSubclass() + */ +void dmlib::removeIPAddressCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::IPAddressSubclass, dmlib_subclass::SubclassID::ipAddress); +} + +/** + * @brief Applies owner drawn subclassing to a IP address control and adjusts its border style. + * + * Overload wrapper that applies the subclass only if `p.m_subclass` is `true`. + * Adjusts border style depending on used dark mode state. + * + * @param[in] hWnd Handle to the IP address control. + * @param[in] p Parameters controlling whether to apply subclassing. + * + * @see dmlib::setIPAddressCtrlSubclass() + */ +static void setIPAddressCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_subclass) + { + dmlib::setIPAddressCtrlSubclass(hWnd); + } + + if (p.m_theme) + { + dmlib::replaceClientEdgeWithBorderSafe(hWnd); + } +} + +/** + * @brief Applies custom color subclassing to a hot key control. + * + * Handles custom colors for hot key control via hooks. + * + * @param[in] hWnd Handle to the hot key control. + * + * @see dmlib_subclass::HotKeySubclass() + * @see dmlib::removeHotKeyCtrlSubclass() + */ +void dmlib::setHotKeyCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::HotKeySubclass, dmlib_subclass::SubclassID::hotKey); +} + +/** + * @brief Removes the custom color subclass from a hot key control. + * + * @param[in] hWnd Handle to the hot key control. + * + * @see dmlib_subclass::HotKeySubclass() + * @see dmlib::setHotKeyCtrlSubclass() + */ +void dmlib::removeHotKeyCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::HotKeySubclass, dmlib_subclass::SubclassID::hotKey); +} + +/** + * @brief Applies custom color subclassing to a hot key control and adjusts its border style. + * + * Overload wrapper that applies the subclass only if `p.m_subclass` is `true`. + * Adjusts border style depending on used dark mode state. + * + * @param[in] hWnd Handle to the hot key control. + * @param[in] p Parameters controlling whether to apply subclassing. + * + * @see dmlib::setHotKeyCtrlSubclass() + */ +static void setHotKeyCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_subclass) + { + dmlib::setHotKeyCtrlSubclass(hWnd); + } + + if (p.m_theme) + { + dmlib::replaceClientEdgeWithBorderSafe(hWnd); + } +} + +/** + * @brief Applies custom color subclassing to a date time picker control. + * + * Handles custom colors for date time picker control via hooks. + * + * @param[in] hWnd Handle to the date time picker control. + * + * @see dmlib_subclass::DTPSubclass() + * @see dmlib::removeDTPCtrlSubclass() + */ +void dmlib::setDTPCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::DTPSubclass, dmlib_subclass::SubclassID::dtp); +} + +/** + * @brief Removes the custom color subclass from a date time picker control. + * + * @param[in] hWnd Handle to the date time picker control. + * + * @see dmlib_subclass::DTPSubclass() + * @see dmlib::setDTPCtrlSubclass() + */ +void dmlib::removeDTPCtrlSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::DTPSubclass, dmlib_subclass::SubclassID::dtp); +} + +/** + * @brief Applies custom color subclassing to a date time picker control. + * + * Applies the subclass only if `p.m_subclass` is `true`. + * Disable visual style if to allow custom colors. + * + * @param[in] hWnd Handle to the date time picker control. + * @param[in] p Parameters controlling whether to apply subclassing. + * + * @see dmlib::setDTPCtrlSubclass() + */ +static void setDTPCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_subclass) + { + dmlib::setDTPCtrlSubclass(hWnd); + } + + if (p.m_theme) + { + dmlib::disableVisualStyle(hWnd, dmlib::isEnabled()); + } +} + +/** + * @brief Applies theming to a tree view control. + * + * Sets custom text and background colors, applies a themed window style, + * and applies themed tooltips for tree view items. + * + * @param[in] hWnd Handle to the tree view control. + * @param[in] p Parameters controlling whether to apply theming. + * + * @see dmlib::setDarkTreeViewCheckboxes(hWnd); + * @see dmlib::setTreeViewWindowTheme() + * @see dmlib::setDarkTooltips() + */ +static void setTreeViewCtrlTheme(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_theme) + { + TreeView_SetTextColor(hWnd, dmlib::getViewTextColor()); + TreeView_SetBkColor(hWnd, dmlib::getViewBackgroundColor()); + + dmlib::setTreeViewWindowThemeEx(hWnd, p.m_theme); + dmlib::setDarkTreeViewCheckboxes(hWnd); + dmlib::setDarkTooltips(hWnd, static_cast(dmlib::ToolTipsType::treeview)); + } +} + +/** + * @brief Applies subclassing to a rebar control. + * + * Applies window subclassing to handle `WM_ERASEBKGND` message. + * + * @param[in] hWnd Handle to the rebar control. + * @param[in] p Parameters controlling whether to apply subclassing. + * + * @see dmlib::setWindowEraseBgSubclass() + */ +static void setRebarCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_subclass) + { + dmlib::setWindowEraseBgSubclass(hWnd); + } +} + +/** + * @brief Applies theming to a toolbar control. + * + * Sets custom colors for line above toolbar panel + * and applies themed tooltips for toolbar buttons. + * + * @param[in] hWnd Handle to the toolbar control. + * @param[in] p Parameters controlling whether to apply theming. + * + * @see dmlib::setDarkLineAbovePanelToolbar() + * @see dmlib::setDarkTooltips() + */ +static void setToolbarCtrlTheme(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_theme) + { + dmlib::setDarkLineAbovePanelToolbar(hWnd); + dmlib::setDarkTooltips(hWnd, static_cast(dmlib::ToolTipsType::toolbar)); + } +} + +/** + * @brief Applies theming to a scroll bar control. + * + * @param[in] hWnd Handle to the scroll bar control. + * @param[in] p Parameters controlling whether to apply theming. + * + * @see dmlib::setDarkScrollBar() + */ +static void setScrollBarCtrlTheme(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_theme) + { + dmlib::setDarkScrollBar(hWnd); + } +} + +/** + * @brief Applies theming to a SysLink control. + * + * Overload that enable `WM_CTLCOLORSTATIC` message handling + * depending on `DarkModeParams` for the syslink control. + * + * @param[in] hWnd Handle to the SysLink control. + * @param[in] p Parameters controlling whether to apply theming. + * + * @see dmlib::enableSysLinkCtrlCtlColor() + */ +static void enableSysLinkCtrlCtlColor(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_theme) + { + dmlib::enableSysLinkCtrlCtlColor(hWnd); + } +} + +/** + * @brief Applies theming to a trackbar control. + * + * Sets transparent background via `TBS_TRANSPARENTBKGND` flag + * and applies themed tooltips for trackbar buttons. + * + * @param[in] hWnd Handle to the trackbar control. + * @param[in] p Parameters controlling whether to apply theming. + * + * @see dmlib::setWindowStyle() + * @see dmlib::setDarkTooltips() + */ +static void setTrackbarCtrlTheme(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_theme) + { + dmlib::setWindowStyle(hWnd, dmlib::isEnabled(), TBS_TRANSPARENTBKGND); + dmlib::setDarkTooltips(hWnd, static_cast(dmlib::ToolTipsType::trackbar)); + } +} + +/** + * @brief Applies theming to a rich edit control. + * + * @param[in] hWnd Handle to the rich edit control. + * @param[in] p Parameters controlling whether to apply theming. + * + * @see dmlib::setDarkRichEdit() + */ +static void setRichEditCtrlTheme(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_theme) + { + dmlib::setDarkRichEdit(hWnd); + } +} + +/** + * @brief Applies theming to a month calendar control. + * + * @param[in] hWnd Handle to the month calendar control. + * @param[in] p Parameters controlling whether to apply theming. + * + * @see dmlib::setDarkMonthCalendar() + */ +static void setMonthCalendarCtrlTheme(HWND hWnd, DarkModeParams p) noexcept +{ + if (p.m_theme) + { + dmlib::setDarkMonthCalendar(hWnd); + } +} + +/** + * @brief Callback function used to enumerate and apply theming/subclassing to child controls. + * + * Called in `setChildCtrlsSubclassAndTheme()` and `setChildCtrlsTheme()` + * to inspect each child window's class name and apply appropriate theming + * and/or subclassing logic based on control type. + * + * @param[in] hWnd Handle to the window being enumerated. + * @param[in] lParam Pointer to a `DarkModeParams` structure containing theming flags and settings. + * @return `TRUE` to continue enumeration. + * + * @note + * - Currently handles these controls: + * `WC_BUTTON`, `WC_STATIC`, `WC_COMBOBOX`, `WC_EDIT`, `WC_LISTBOX`, + * `WC_LISTVIEW`, `WC_TREEVIEW`, `REBARCLASSNAME`, `TOOLBARCLASSNAME`, + * `UPDOWN_CLASS`, `WC_TABCONTROL`, `STATUSCLASSNAME`, `WC_SCROLLBAR`, + * `WC_COMBOBOXEX`, `PROGRESS_CLASS`, `WC_LINK`, `TRACKBAR_CLASS`, + * `RICHEDIT_CLASS`, `MSFTEDIT_CLASS`, `WC_IPADDRESS`, `HOTKEY_CLASS`, + * `DATETIMEPICK_CLASS`, and `MONTHCAL_CLASS` + * - The `#32770` dialog class is commented out for debugging purposes. + * + * @see dmlib::setChildCtrlsSubclassAndTheme() + * @see dmlib::setChildCtrlsTheme() + * @see DarkModeParams + * @see setBtnCtrlSubclassAndTheme() + * @see setStaticTextCtrlSubclass() + * @see setComboBoxCtrlSubclassAndTheme() + * @see setCustomBorderForListBoxOrEditCtrlSubclassAndTheme() + * @see setListViewCtrlSubclassAndTheme() + * @see setTreeViewCtrlTheme() + * @see setRebarCtrlSubclass() + * @see setToolbarCtrlTheme() + * @see setUpDownCtrlSubclassAndTheme() + * @see setTabCtrlSubclassAndTheme() + * @see setStatusBarCtrlSubclass() + * @see setScrollBarCtrlTheme() + * @see setComboBoxExCtrlSubclass() + * @see setProgressBarCtrlSubclass() + * @see enableSysLinkCtrlCtlColor() + * @see setTrackbarCtrlTheme() + * @see setRichEditCtrlTheme() + * @see setIPAddressCtrlSubclass() + * @see setHotKeyCtrlSubclass() + * @see setDTPCtrlSubclassAndTheme() + * @see setMonthCalendarCtrlTheme() + */ +static BOOL CALLBACK DarkEnumChildProc(HWND hWnd, LPARAM lParam) +{ + const auto& p = *reinterpret_cast(lParam); + const std::wstring className = dmlib_subclass::getWndClassName(hWnd); + + if (className == WC_BUTTON) + { + setBtnCtrlSubclassAndTheme(hWnd, p); + return TRUE; + } + + if (className == WC_STATIC) + { + setStaticTextCtrlSubclass(hWnd, p); + return TRUE; + } + + if (className == WC_COMBOBOX) + { + setComboBoxCtrlSubclassAndTheme(hWnd, p); + return TRUE; + } + + if (className == WC_EDIT) + { + setCustomBorderForListBoxOrEditCtrlSubclassAndTheme(hWnd, p, false); + return TRUE; + } + + if (className == WC_LISTBOX) + { + setCustomBorderForListBoxOrEditCtrlSubclassAndTheme(hWnd, p, true); + return TRUE; + } + + if (className == WC_LISTVIEW) + { + setListViewCtrlSubclassAndTheme(hWnd, p); + return TRUE; + } + + if (className == WC_TREEVIEW) + { + setTreeViewCtrlTheme(hWnd, p); + return TRUE; + } + + if (className == REBARCLASSNAME) + { + setRebarCtrlSubclass(hWnd, p); + return TRUE; + } + + if (className == TOOLBARCLASSNAME) + { + setToolbarCtrlTheme(hWnd, p); + return TRUE; + } + + if (className == UPDOWN_CLASS) + { + setUpDownCtrlSubclassAndTheme(hWnd, p); + return TRUE; + } + + if (className == WC_TABCONTROL) + { + setTabCtrlSubclassAndTheme(hWnd, p); + return TRUE; + } + + if (className == STATUSCLASSNAME) + { + setStatusBarCtrlSubclass(hWnd, p); + return TRUE; + } + + if (className == WC_SCROLLBAR) + { + setScrollBarCtrlTheme(hWnd, p); + return TRUE; + } + + if (className == WC_COMBOBOXEX) + { + setComboBoxExCtrlSubclass(hWnd, p); + return TRUE; + } + + if (className == PROGRESS_CLASS) + { + setProgressBarCtrlSubclass(hWnd, p); + return TRUE; + } + + if (className == WC_LINK) + { + enableSysLinkCtrlCtlColor(hWnd, p); + return TRUE; + } + + if (className == TRACKBAR_CLASS) + { + setTrackbarCtrlTheme(hWnd, p); + return TRUE; + } + + if (className == RICHEDIT_CLASS || className == MSFTEDIT_CLASS) // rich edit controls 2.0, 3.0, and 4.1 + { + setRichEditCtrlTheme(hWnd, p); + return TRUE; + } + + if (className == WC_IPADDRESS) + { + setIPAddressCtrlSubclass(hWnd, p); + return TRUE; + } + + if (className == HOTKEY_CLASS) + { + setHotKeyCtrlSubclass(hWnd, p); + return TRUE; + } + + if (className == DATETIMEPICK_CLASS) // date and time picker + { + setDTPCtrlSubclassAndTheme(hWnd, p); + return TRUE; + } + + if (className == MONTHCAL_CLASS) // month calendar + { + setMonthCalendarCtrlTheme(hWnd, p); + return TRUE; + } + +#if 0 // for debugging + if (className == L"#32770") // dialog + { + return TRUE; + } +#endif + return TRUE; +} + +/** + * @brief Applies theming and/or subclassing to all child controls of a parent window. + * + * Enumerates all child windows of the specified parent and dispatches them to + * `DarkEnumChildProc`, which applies control-specific theming and/or subclassing logic + * based on their class name and the provided parameters. + * + * Mainly used when initializing parent control. + * + * @param[in] hParent Handle to the parent window whose child controls will be themed and/or subclassed. + * @param[in] subclass Whether to apply subclassing. + * @param[in] theme Whether to apply theming. + * + * @see dmlib::setChildCtrlsSubclassAndTheme() + * @see dmlib::DarkEnumChildProc() + * @see DarkModeParams + */ +void dmlib::setChildCtrlsSubclassAndThemeEx(HWND hParent, bool subclass, bool theme) +{ + DarkModeParams p{ + dmlib::isExperimentalActive() ? L"DarkMode_Explorer" : nullptr + , subclass + , theme + }; + + ::EnumChildWindows(hParent, DarkEnumChildProc, reinterpret_cast(&p)); +} + +/** + * @brief Wrapper for `dmlib::setChildCtrlsSubclassAndThemeEx`. + * + * Forwards to `dmlib::setChildCtrlsSubclassAndThemeEx` with `subclass` and `theme` parameters set as `true`. + * + * @param[in] hParent Handle to the parent window whose child controls will be themed and/or subclassed. + * + * @see dmlib::setChildCtrlsSubclassAndThemeEx() + */ +void dmlib::setChildCtrlsSubclassAndTheme(HWND hParent) +{ + dmlib::setChildCtrlsSubclassAndThemeEx(hParent, true, true); +} + +/** + * @brief Applies theming to all child controls of a parent window. + * + * Enumerates child windows of the specified parent and applies theming without subclassing. + * The theming behavior adapts based on OS support and compile-time flags. + * If `_DARKMODELIB_ALLOW_OLD_OS > 1` is true, theming is applied unconditionally. + * Otherwise, theming is applied only if the OS is Windows 10 or newer. + * The function delegates to `setChildCtrlsSubclassAndTheme()` with appropriate flags. + * + * Mainly used when changing mode. + * + * @param[in] hParent Handle to the parent window whose child controls will be themed. + * + * @see dmlib::setChildCtrlsSubclassAndTheme() + */ +void dmlib::setChildCtrlsTheme(HWND hParent) +{ +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 1) + dmlib::setChildCtrlsSubclassAndThemeEx(hParent, false, true); +#else + dmlib::setChildCtrlsSubclassAndThemeEx(hParent, false, dmlib::isAtLeastWindows10()); +#endif +} + +/** + * @brief Applies window subclassing to handle `WM_ERASEBKGND` message. + * + * @param[in] hWnd Handle to the control to subclass. + * + * @see dmlib_subclass::WindowEraseBgSubclass() + * @see dmlib::removeWindowEraseBgSubclass() + */ +void dmlib::setWindowEraseBgSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::WindowEraseBgSubclass, dmlib_subclass::SubclassID::windowEraseBg); +} + +/** + * @brief Removes the subclass used for `WM_ERASEBKGND` message handling. + * + * Detaches the window's subclass proc used for `WM_ERASEBKGND` message handling. + * + * @param[in] hWnd Handle to the previously subclassed window. + * + * @see dmlib_subclass::WindowEraseBgSubclass() + * @see dmlib::removeWindowEraseBgSubclass() + */ +void dmlib::removeWindowEraseBgSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::WindowEraseBgSubclass, dmlib_subclass::SubclassID::windowEraseBg); +} + +/** + * @brief Applies window subclassing to handle `WM_CTLCOLOR*` messages. + * + * Enable custom colors for edit, listbox, static, and dialog elements + * via @ref dmlib_subclass::WindowCtlColorSubclass. + * + * @param[in] hWnd Handle to the parent or composite control (dialog, rebar, toolbar, ...) to subclass. + * + * @see dmlib_subclass::WindowCtlColorSubclass() + * @see dmlib::removeWindowCtlColorSubclass() + */ +void dmlib::setWindowCtlColorSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::WindowCtlColorSubclass, dmlib_subclass::SubclassID::windowCtlColor); +} + +/** + * @brief Removes the subclass used for `WM_CTLCOLOR*` messages handling. + * + * Detaches the window's subclass proc used for `WM_CTLCOLOR*` messages handling. + * + * @param[in] hWnd Handle to the previously subclassed window. + * + * @see dmlib_subclass::WindowCtlColorSubclass() + * @see dmlib::setWindowCtlColorSubclass() + */ +void dmlib::removeWindowCtlColorSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::WindowCtlColorSubclass, dmlib_subclass::SubclassID::windowCtlColor); +} + +/** + * @brief Applies window subclassing for handling `NM_CUSTOMDRAW` notifications for custom drawing. + * + * Installs @ref dmlib_subclass::WindowNotifySubclass. + * Enables handling of `WM_NOTIFY` `NM_CUSTOMDRAW` notifications for custom drawing + * behavior for supported controls. + * + * @param[in] hWnd Handle to the window with child which support `NM_CUSTOMDRAW`. + * + * @see dmlib_subclass::WindowNotifySubclass() + * @see dmlib::removeWindowNotifyCustomDrawSubclass() + */ +void dmlib::setWindowNotifyCustomDrawSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::WindowNotifySubclass, dmlib_subclass::SubclassID::windowNotify); +} + +/** + * @brief Removes the subclass used for handling `NM_CUSTOMDRAW` notifications for custom drawing. + * + * Detaches the window's subclass proc used for handling `NM_CUSTOMDRAW` notifications for custom drawing. + * + * @param[in] hWnd Handle to the previously subclassed window. + * + * @see dmlib_subclass::WindowNotifySubclass() + * @see dmlib::setWindowNotifyCustomDrawSubclass() + */ +void dmlib::removeWindowNotifyCustomDrawSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::WindowNotifySubclass, dmlib_subclass::SubclassID::windowNotify); +} + +/** + * @brief Applies window subclassing for menu bar themed custom drawing. + * + * Installs @ref dmlib::WindowMenuBarSubclass with an associated `ThemeData` instance + * for the `VSCLASS_MENU` visual style. Enables custom drawing + * behavior for menu bar. + * + * @param[in] hWnd Handle to the window with a menu bar. + * + * @see dmlib_subclass::WindowMenuBarSubclass() + * @see dmlib::removeWindowMenuBarSubclass() + */ +void dmlib::setWindowMenuBarSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::WindowMenuBarSubclass, dmlib_subclass::SubclassID::windowMenuBar, VSCLASS_MENU); +} + +/** + * @brief Removes the subclass used for menu bar themed custom drawing. + * + * Detaches the window's subclass proc used for menu bar themed custom drawing. + * + * @param[in] hWnd Handle to the previously subclassed window. + * + * @see dmlib_subclass::WindowMenuBarSubclass() + * @see dmlib::setWindowMenuBarSubclass() + */ +void dmlib::removeWindowMenuBarSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::WindowMenuBarSubclass, dmlib_subclass::SubclassID::windowMenuBar); +} + +/** + * @brief Applies window subclassing to handle `WM_SETTINGCHANGE` message. + * + * Enable monitoring WM_SETTINGCHANGE message, + * allowing the app to respond to system-wide dark mode change. + * + * @param[in] hWnd Handle to the main window. + * + * @see dmlib::WindowSettingChangeSubclass() + * @see dmlib::removeWindowSettingChangeSubclass() + */ +void dmlib::setWindowSettingChangeSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, dmlib_subclass::WindowSettingChangeSubclass, dmlib_subclass::SubclassID::windowSettingChange); +} + +/** + * @brief Removes the subclass used for `WM_SETTINGCHANGE` message handling. + * + * Detaches the window's subclass proc used for `WM_SETTINGCHANGE` messages handling. + * + * @param[in] hWnd Handle to the previously subclassed window. + * + * @see dmlib::WindowSettingChangeSubclass() + * @see dmlib::setWindowSettingChangeSubclass() + */ +void dmlib::removeWindowSettingChangeSubclass(HWND hWnd) +{ + dmlib_subclass::RemoveSubclass(hWnd, dmlib_subclass::WindowSettingChangeSubclass, dmlib_subclass::SubclassID::windowSettingChange); +} + +/** + * @brief Configures the SysLink control to be affected by `WM_CTLCOLORSTATIC` message. + * + * Configures all items to either use default system link colors if in classic mode, + * or to be affected by `WM_CTLCOLORSTATIC` message from its parent. + * + * @param[in] hWnd Handle to the SysLink control. + * + * @note Will affect all items, even if it's static (non-clickable). + */ +void dmlib::enableSysLinkCtrlCtlColor(HWND hWnd) +{ + LITEM lItem{}; + lItem.iLink = 0; + lItem.mask = LIF_ITEMINDEX | LIF_STATE; + lItem.state = dmlib::isEnabled() ? LIS_DEFAULTCOLORS : 0; + lItem.stateMask = LIS_DEFAULTCOLORS; + while (::SendMessage(hWnd, LM_SETITEM, 0, reinterpret_cast(&lItem)) == TRUE) + { + ++lItem.iLink; + } +} + +/** + * @brief Sets dark title bar and optional Windows 11 features. + * + * For Windows 10 (2004+) and newer, this function configures the dark title bar using + * `DWMWA_USE_IMMERSIVE_DARK_MODE`. On Windows 11, if `useWin11Features` is `true`, it + * additionally applies: + * - Rounded corners (`DWMWA_WINDOW_CORNER_PREFERENCE`) + * - Border color (`DWMWA_BORDER_COLOR`) + * - Mica backdrop (`DWMWA_SYSTEMBACKDROP_TYPE`) if allowed and compatible + * - Static text color for text and dialog background color for background + * (`DWMWA_CAPTION_COLOR`, `DWMWA_TEXT_COLOR`), + * only when frames are not extended to full window + * + * If `_DARKMODELIB_ALLOW_OLD_OS` is defined with non-zero unsigned value + * and running on pre-2004 builds, fallback behavior will enable dark title bars via undocumented APIs. + * + * @param[in] hWnd Handle to the top-level window. + * @param[in] useWin11Features `true` to enable Windows 11 specific features such as Mica and rounded corners. + * + * @note Requires Windows 10 version 2004 (build 19041) or later. + * + * @see DwmSetWindowAttribute + * @see DwmExtendFrameIntoClientArea + */ +void dmlib::setDarkTitleBarEx(HWND hWnd, bool useWin11Features) +{ + if (static constexpr DWORD win10Build2004 = 19041; + dmlib::getWindowsBuildNumber() >= win10Build2004) + { + const BOOL useDark = dmlib::isExperimentalActive() ? TRUE : FALSE; + ::DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &useDark, sizeof(useDark)); + } +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) + else + { + dmlib_win32api::AllowDarkModeForWindow(hWnd, dmlib::isExperimentalActive()); + dmlib_win32api::RefreshTitleBarThemeColor(hWnd); + } +#endif + + if (!dmlib::isAtLeastWindows11()) + { + // on Windows 10 title bar needs refresh when changing colors + if (dmlib::isAtLeastWindows10()) + { + const bool isActive = (hWnd == ::GetActiveWindow()) && (hWnd == ::GetForegroundWindow()); + ::SendMessage(hWnd, WM_NCACTIVATE, static_cast(!isActive), 0); + ::SendMessage(hWnd, WM_NCACTIVATE, static_cast(isActive), 0); + } + return; + } + + if (!useWin11Features) + { + return; + } + + ::DwmSetWindowAttribute(hWnd, DWMWA_WINDOW_CORNER_PREFERENCE, &g_dmCfg.m_roundCorner, sizeof(g_dmCfg.m_roundCorner)); + ::DwmSetWindowAttribute(hWnd, DWMWA_BORDER_COLOR, &g_dmCfg.m_borderColor, sizeof(g_dmCfg.m_borderColor)); + + bool canColorizeTitleBar = true; + + if (static constexpr DWORD win11Mica = 22621; + dmlib::getWindowsBuildNumber() >= win11Mica) + { + if (g_dmCfg.m_micaExtend && g_dmCfg.m_mica != DWMSBT_AUTO + && !dmlib::isWindowsModeEnabled() + && (g_dmCfg.m_dmType == DarkModeType::dark)) + { + static constexpr MARGINS margins{ -1, 0, 0, 0 }; + ::DwmExtendFrameIntoClientArea(hWnd, &margins); + } + + ::DwmSetWindowAttribute(hWnd, DWMWA_SYSTEMBACKDROP_TYPE, &g_dmCfg.m_mica, sizeof(g_dmCfg.m_mica)); + + canColorizeTitleBar = !g_dmCfg.m_micaExtend; + } + + canColorizeTitleBar = g_dmCfg.m_colorizeTitleBar && canColorizeTitleBar && dmlib::isEnabled(); + const COLORREF clrDlg = canColorizeTitleBar ? dmlib::getDlgBackgroundColor() : DWMWA_COLOR_DEFAULT; + const COLORREF clrText = canColorizeTitleBar ? dmlib::getTextColor() : DWMWA_COLOR_DEFAULT; + ::DwmSetWindowAttribute(hWnd, DWMWA_CAPTION_COLOR, &clrDlg, sizeof(clrDlg)); + ::DwmSetWindowAttribute(hWnd, DWMWA_TEXT_COLOR, &clrText, sizeof(clrText)); +} + +/** + * @brief Sets dark mode title bar on supported Windows versions. + * + * Delegates to @ref setDarkTitleBarEx with `useWin11Features = false`. + * + * @param[in] hWnd Handle to the top-level window. + * + * @see dmlib::setDarkTitleBarEx() + */ +void dmlib::setDarkTitleBar(HWND hWnd) +{ + dmlib::setDarkTitleBarEx(hWnd, false); +} + +/** + * @brief Get dark mode theme name. + * + * @return "DarkMode_DarkTheme" on Windows 11 25H2+ + * @return "DarkMode_Explorer" on Windows 10+ + * @return nullptr if not on supported OS + * + * @see doesWin11SupportDarkThemeStyle() + */ +const wchar_t* dmlib::getDarkModeThemeName() +{ + if (doesWin11SupportDarkThemeStyle()) + { + return L"DarkMode_DarkTheme"; + } + if (dmlib::isAtLeastWindows10()) + { + return L"DarkMode_Explorer"; + } + return nullptr; +} + +/** + * @brief Applies an experimental visual style to the specified window, if supported. + * + * When experimental features are supported and active, + * this function enables dark experimental visual style on the window. + * + * @param[in] hWnd Handle to the target window or control. + * @param[in] themeClassName Name of the theme class to apply (e.g. L"Explorer", "ItemsView"). + * + * @note This function is a no-op if experimental theming is not supported on the current OS. + * + * @see dmlib::isExperimentalSupported() + * @see dmlib::isExperimentalActive() + * @see dmlib_win32api::AllowDarkModeForWindow() + * @see dmlib::setDarkThemeExperimental() + */ +void dmlib::setDarkThemeExperimentalEx(HWND hWnd, const wchar_t* themeClassName) +{ + if (dmlib::isExperimentalSupported()) + { + dmlib_win32api::AllowDarkModeForWindow(hWnd, dmlib::isExperimentalActive()); + ::SetWindowTheme(hWnd, themeClassName, nullptr); + } +} + +/** + * @brief Applies an experimental Explorer visual style to the specified window, if supported. + * + * Forwards to `dmlib::setDarkThemeExperimentalEx` with `themeClassName` as `L"Explorer"`. + * + * @param[in] hWnd Handle to the target window or control. + * + * @see dmlib::setDarkThemeExperimentalEx() + */ +void dmlib::setDarkThemeExperimental(HWND hWnd) +{ + dmlib::setDarkThemeExperimentalEx(hWnd, L"Explorer"); +} + +/** + * @brief Applies "DarkMode_Explorer" visual style if experimental mode is active. + * + * Useful for controls like list views or tree views to use dark scroll bars + * and explorer style theme in supported environments. + * + * @param[in] hWnd Handle to the control or window to theme. + */ +void dmlib::setDarkExplorerTheme(HWND hWnd) +{ + ::SetWindowTheme(hWnd, dmlib::isExperimentalActive() ? L"DarkMode_Explorer" : nullptr, nullptr); +} + +/** + * @brief Applies "DarkMode_Explorer" visual style to scroll bars. + * + * Convenience wrapper that calls @ref dmlib::setDarkExplorerTheme to apply dark scroll bar + * for compatible controls (e.g. list views, tree views). + * + * @param[in] hWnd Handle to the control with scroll bars. + * + * @see dmlib::setDarkExplorerTheme() + */ +void dmlib::setDarkScrollBar(HWND hWnd) +{ + dmlib::setDarkExplorerTheme(hWnd); +} + +/** + * @brief Applies "DarkMode_Explorer" visual style to tooltip controls based on context. + * + * Selects the appropriate `GETTOOLTIPS` message depending on the control type + * (e.g. toolbar, list view, tree view, tab bar) to retrieve the tooltip handle. + * If `ToolTipsType::tooltip` is specified, applies theming directly to `hWnd`. + * + * Internally calls @ref dmlib::setDarkExplorerTheme to set dark tooltip. + * + * @param[in] hWnd Handle to the parent control or tooltip. + * @param[in] tooltipType The tooltip context type (toolbar, list view, etc.). + * + * @see dmlib::setDarkExplorerTheme() + * @see ToolTipsType + */ +void dmlib::setDarkTooltips(HWND hWnd, UINT tooltipType) +{ + if (tooltipType > static_cast(dmlib::ToolTipsType::rebar)) + { + return; + } + + const auto type = static_cast(tooltipType); + UINT msg = 0; + switch (type) + { + case dmlib::ToolTipsType::toolbar: + { + msg = TB_GETTOOLTIPS; + break; + } + + case dmlib::ToolTipsType::listview: + { + msg = LVM_GETTOOLTIPS; + break; + } + + case dmlib::ToolTipsType::treeview: + { + msg = TVM_GETTOOLTIPS; + break; + } + + case dmlib::ToolTipsType::tabbar: + { + msg = TCM_GETTOOLTIPS; + break; + } + + case dmlib::ToolTipsType::trackbar: + { + msg = TBM_GETTOOLTIPS; + break; + } + + case dmlib::ToolTipsType::rebar: + { + msg = RB_GETTOOLTIPS; + break; + } + + case dmlib::ToolTipsType::tooltip: + { + msg = 0; + break; + } + } + + if (msg == 0) + { + dmlib::setDarkExplorerTheme(hWnd); + } + else + { + auto hTips = reinterpret_cast(::SendMessage(hWnd, msg, 0, 0)); + if (hTips != nullptr) + { + dmlib::setDarkExplorerTheme(hTips); + } + } +} + +/** + * @brief Applies "DarkMode_DarkTheme" visual style if supported and experimental mode is active. + * + * Applies "DarkMode_DarkTheme" visual style if supported, + * else applies "DarkMode_Explorer" visual style. + * + * @param[in] hWnd Handle to the control or window to theme. + * + * @see dmlib::getDarkModeThemeName() + */ +void dmlib::setDarkThemeTheme(HWND hWnd) +{ + ::SetWindowTheme(hWnd, dmlib::isExperimentalActive() ? dmlib::getDarkModeThemeName() : nullptr, nullptr); +} + +/** + * @brief Sets the color of line above a toolbar control for non-classic mode. + * + * Sends `TB_SETCOLORSCHEME` to customize the line drawn above the toolbar. + * When non-classic mode is enabled, sets both `clrBtnHighlight` and `clrBtnShadow` + * to the dialog background color, otherwise uses system defaults. + * + * @param[in] hWnd Handle to the toolbar control. + */ +void dmlib::setDarkLineAbovePanelToolbar(HWND hWnd) +{ + COLORSCHEME scheme{}; + scheme.dwSize = sizeof(COLORSCHEME); + + if (dmlib::isEnabled()) + { + scheme.clrBtnHighlight = dmlib::getBackgroundColor(); + scheme.clrBtnShadow = dmlib::getBackgroundColor(); + } + else + { + scheme.clrBtnHighlight = CLR_DEFAULT; + scheme.clrBtnShadow = CLR_DEFAULT; + } + + ::SendMessage(hWnd, TB_SETCOLORSCHEME, 0, reinterpret_cast(&scheme)); +} + +/** + * @brief Applies an experimental Explorer visual style to a list view. + * + * Uses @ref dmlib::setDarkThemeExperimental with the `"Explorer"` theme class to adapt + * list view visuals (e.g. scroll bars, selection color) for dark mode, if supported. + * + * @param[in] hWnd Handle to the list view control. + * + * @see dmlib::setDarkThemeExperimental() + */ +void dmlib::setDarkListView(HWND hWnd) +{ + dmlib::setDarkThemeExperimental(hWnd); +} + +/** + * @brief Replaces list view or tree view image list checkbox state images with themed dark mode versions on Windows 11. + * + * Uses `"DarkMode_Explorer::Button"` as the theme class if experimental dark mode is active; + * otherwise falls back to `VSCLASS_BUTTON`. + * + * @param[in] hWnd Handle to the control to change checkbox style. + * @param[in] hImgList Handle to the image list of control containing checkbox state images. + * @param[in] viewCheckbox Type of checkbox style. + * + * @note Does nothing on pre-Windows 11 systems. + */ +static void setDarkCheckboxes(HWND hWnd, HIMAGELIST hImgList, ViewCheckbox viewCheckbox) noexcept +{ + if (!dmlib::isAtLeastWindows11()) + { + return; + } + + HDC hdc = ::GetDC(nullptr); + + const bool useDark = dmlib::isExperimentalActive() && dmlib::isThemeDark(); + HTHEME hTheme = dmlib_dpi::OpenThemeDataForDpi(nullptr, useDark ? L"DarkMode_Explorer::Button" : VSCLASS_BUTTON, ::GetParent(hWnd)); + + SIZE szBox{}; + ::GetThemePartSize(hTheme, hdc, BP_CHECKBOX, CBS_UNCHECKEDNORMAL, nullptr, TS_DRAW, &szBox); + + const RECT rcBox{ 0, 0, szBox.cx, szBox.cy }; + + if (hImgList == nullptr) + { + ::CloseThemeData(hTheme); + ::ReleaseDC(nullptr, hdc); + return; + } + + HDC hBoxDC = ::CreateCompatibleDC(hdc); + HBITMAP hBoxBmp = ::CreateCompatibleBitmap(hdc, szBox.cx, szBox.cy); + HBITMAP hMaskBmp = ::CreateCompatibleBitmap(hdc, szBox.cx, szBox.cy); + + auto holdBmp = static_cast(::SelectObject(hBoxDC, hBoxBmp)); + ::DrawThemeBackground(hTheme, hBoxDC, BP_CHECKBOX, CBS_UNCHECKEDNORMAL, &rcBox, nullptr); + + ICONINFO ii{}; + ii.fIcon = TRUE; + ii.hbmColor = hBoxBmp; + ii.hbmMask = hMaskBmp; + + int idx = (viewCheckbox == ViewCheckbox::listView) ? 0 : 1; // tree view state images start from index 1 + + HICON hIcon = ::CreateIconIndirect(&ii); + + auto addIcon = [&hImgList, &idx, &hIcon]() noexcept + { + if (hIcon != nullptr) + { + ::ImageList_ReplaceIcon(hImgList, idx, hIcon); + ::DestroyIcon(hIcon); + hIcon = nullptr; + ++idx; + } + }; + + addIcon(); // unchecked state + + auto addIconState = [&](int iStateId) noexcept + { + ::DrawThemeBackground(hTheme, hBoxDC, BP_CHECKBOX, iStateId, &rcBox, nullptr); + ii.hbmColor = hBoxBmp; + + hIcon = ::CreateIconIndirect(&ii); + addIcon(); + }; + + addIconState(CBS_CHECKEDNORMAL); + + if (viewCheckbox == ViewCheckbox::tvExtended) + { + const auto tvExStyle = TreeView_GetExtendedStyle(hWnd); + + // Tree view check boxes: The extended check box states + // https://devblogs.microsoft.com/oldnewthing/20171205-00/?p=97525 + // The image list states are added in the order: Partial, then dimmed, then exclusion. + if ((tvExStyle & TVS_EX_PARTIALCHECKBOXES) != 0) + { + addIconState(CBS_MIXEDNORMAL); + } + + if ((tvExStyle & TVS_EX_DIMMEDCHECKBOXES) != 0) + { + addIconState(CBS_IMPLICITPRESSED); // make it different from CBS_CHECKEDNORMAL + } + + if ((tvExStyle & TVS_EX_EXCLUSIONCHECKBOXES) != 0) + { + addIconState(CBS_EXCLUDEDNORMAL); + } + } + + ::SelectObject(hBoxDC, holdBmp); + ::DeleteObject(hMaskBmp); + ::DeleteObject(hBoxBmp); + ::DeleteDC(hBoxDC); + ::CloseThemeData(hTheme); + ::ReleaseDC(nullptr, hdc); +} + +/** + * @brief Replaces default list view checkboxes with themed dark-mode versions on Windows 11. + * + * If the list view uses `LVS_EX_CHECKBOXES` and is running on Windows 11 or later, + * this function then manually draws the unchecked and checked checkbox visuals using + * themed drawing APIs, then inserts the resulting icons into the state image list. + * + * Uses `"DarkMode_Explorer::Button"` as the theme class if experimental dark mode is active; + * otherwise falls back to `VSCLASS_BUTTON`. + * + * @param[in] hWnd Handle to the list view control with extended checkbox style. + * + * @see setDarkCheckboxes() + * + * @note Does nothing on pre-Windows 11 systems or if checkboxes are not enabled. + */ +void dmlib::setDarkListViewCheckboxes(HWND hWnd) +{ + if (const auto lvExStyle = ListView_GetExtendedListViewStyle(hWnd); + (lvExStyle & LVS_EX_CHECKBOXES) != LVS_EX_CHECKBOXES) + { + return; + } + + setDarkCheckboxes(hWnd, ListView_GetImageList(hWnd, LVSIL_STATE), ViewCheckbox::listView); +} + +/** + * @brief Replaces default tree view checkboxes with themed dark-mode versions on Windows 11. + * + * If the tree view uses `TVS_CHECKBOXES` or any combination of `TVS_EX_PARTIALCHECKBOXES`, + * `TVS_EX_EXCLUSIONCHECKBOXES`, `TVS_EX_DIMMEDCHECKBOXES` extended styles and is running on + * Windows 11 or later, this function then manually draws the checkbox state visuals using + * themed drawing APIs, then inserts the resulting icons into the state image list. + * + * Uses `"DarkMode_Explorer::Button"` as the theme class if experimental dark mode is active; + * otherwise falls back to `VSCLASS_BUTTON`. + * + * @param[in] hWnd Handle to the tree view control with normal or extended checkbox style. + * + * @see setDarkCheckboxes() + * + * @note Does nothing on pre-Windows 11 systems or if checkboxes are not enabled. + */ +void dmlib::setDarkTreeViewCheckboxes(HWND hWnd) +{ + if (hWnd == nullptr) + { + return; + } + + ViewCheckbox tvType = ViewCheckbox::tvSimple; + if (const auto tvStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + (tvStyle & TVS_CHECKBOXES) == TVS_CHECKBOXES) + { + // do nothing tvType already has ViewCheckbox::tvSimple + } + else if (const auto tvExStyle = TreeView_GetExtendedStyle(hWnd); + (tvExStyle & (TVS_EX_PARTIALCHECKBOXES | TVS_EX_EXCLUSIONCHECKBOXES | TVS_EX_DIMMEDCHECKBOXES)) != 0) + { + tvType = ViewCheckbox::tvExtended; + } + else + { + return; + } + + setDarkCheckboxes(hWnd, TreeView_GetImageList(hWnd, TVSIL_STATE), tvType); +} + +/** + * @brief Sets colors and edges for a rich edit control. + * + * Determines if the control has `WS_BORDER` or `WS_EX_STATICEDGE`, and sets the background + * accordingly: uses control background color when edged, otherwise dialog background. + * + * In dark mode: + * - Sets background color via `EM_SETBKGNDCOLOR` + * - Updates default text color via `EM_SETCHARFORMAT` + * - Applies themed scroll bars using `DarkMode_Explorer::ScrollBar` + * + * When not in dark mode, restores default visual styles and coloring. + * Also conditionally swaps `WS_BORDER` and `WS_EX_CLIENTEDGE`. + * + * @param[in] hWnd Handle to the rich edit control. + * + * @see dmlib::setWindowStyle() + * @see dmlib::setWindowExStyle() + */ +void dmlib::setDarkRichEdit(HWND hWnd) +{ + const auto nStyle = ::GetWindowLongPtrW(hWnd, GWL_STYLE); + const bool isReadOnly = (nStyle & ES_READONLY) == ES_READONLY; + + CHARFORMATW cf{}; + cf.cbSize = sizeof(CHARFORMATW); + cf.dwMask = CFM_COLOR; + + if (dmlib::isEnabled()) + { + const COLORREF clrBg = (!isReadOnly ? dmlib::getCtrlBackgroundColor() : dmlib::getDlgBackgroundColor()); + ::SendMessage(hWnd, EM_SETBKGNDCOLOR, 0, static_cast(clrBg)); + + cf.crTextColor = dmlib::getTextColor(); + ::SendMessage(hWnd, EM_SETCHARFORMAT, SCF_DEFAULT, reinterpret_cast(&cf)); + + ::SetWindowTheme(hWnd, nullptr, dmlib::isExperimentalActive() ? L"DarkMode_Explorer::ScrollBar" : nullptr); + } + else + { + cf.dwEffects = CFE_AUTOCOLOR; + ::SendMessage(hWnd, EM_SETBKGNDCOLOR, TRUE, 0); + ::SendMessage(hWnd, EM_SETCHARFORMAT, SCF_DEFAULT, reinterpret_cast(&cf)); + + ::SetWindowTheme(hWnd, nullptr, nullptr); + } + + dmlib::replaceClientEdgeWithBorderSafe(hWnd); +} + +/** + * @brief Sets colors for month calendar control. + * + * To set colors visual style needs to be disabled. + * Title background and header day text use same color #0078D7 Windows accent blue. + * + * @param[in] hWnd Handle to the month calendar control. + * + * @see dmlib::disableVisualStyle() + */ +void dmlib::setDarkMonthCalendar(HWND hWnd) +{ + dmlib::disableVisualStyle(hWnd, dmlib::isEnabled()); + MonthCal_SetColor(hWnd, MCSC_BACKGROUND, dmlib::isEnabled() ? dmlib::getDlgBackgroundColor() : ::GetSysColor(COLOR_3DFACE)); + if (dmlib::isEnabled()) + { + MonthCal_SetColor(hWnd, MCSC_MONTHBK, dmlib::getCtrlBackgroundColor()); + MonthCal_SetColor(hWnd, MCSC_TEXT, dmlib::getTextColor()); + static constexpr COLORREF accentBlue = dmlib_color::HEXRGB(0x0078D7); + MonthCal_SetColor(hWnd, MCSC_TITLEBK, accentBlue); + MonthCal_SetColor(hWnd, MCSC_TITLETEXT, dmlib::getTextColor()); + MonthCal_SetColor(hWnd, MCSC_TRAILINGTEXT, dmlib::getDisabledTextColor()); + } +} + +/** + * @brief Applies visual styles; ctl color message and child controls subclassings to a window safely. + * + * Ensures the specified window is not `nullptr` and then: + * - Enables the dark title bar + * - Subclasses the window for control ctl coloring + * - Applies theming and subclassing to child controls + * + * + * @param[in] hWnd Handle to the window. No action taken if `nullptr`. + * @param[in] useWin11Features `true` to enable Windows 11 specific styling like Mica or rounded corners. + * + * @note Should not be used in combination with @ref dmlib::setDarkWndNotifySafeEx + * and @ref dmlib::setDarkWndNotifySafe to avoid overlapping styling logic. + * + * @see dmlib::setDarkWndNotifySafeEx() + * @see dmlib::setDarkWndNotifySafe() + * @see dmlib::setDarkTitleBarEx() + * @see dmlib::setWindowCtlColorSubclass() + * @see dmlib::setChildCtrlsSubclassAndTheme() + * @see dmlib::setDarkWndSafe() + */ +void dmlib::setDarkWndSafeEx(HWND hWnd, bool useWin11Features) +{ + if (hWnd == nullptr) + { + return; + } + + dmlib::setDarkTitleBarEx(hWnd, useWin11Features); + dmlib::setWindowCtlColorSubclass(hWnd); + dmlib::setChildCtrlsSubclassAndTheme(hWnd); +} + +/** + * @brief Applies visual styles; ctl color message and child controls subclassings with Windows 11 features. + * + * Forwards to `dmlib::setDarkWndSafeEx` with parameter `useWin11Features` as `true`. + * + * @param[in] hWnd Handle to the window. + * + * @see dmlib::setDarkWndSafeEx() + */ +void dmlib::setDarkWndSafe(HWND hWnd) +{ + dmlib::setDarkWndSafeEx(hWnd, true); +} + +/** + * @brief Applies visual styles; ctl color message, child controls, custom drawing, and setting change subclassings to a window safely. + * + * Ensures the specified window is not `nullptr` and then: + * - Enables the dark title bar + * - Subclasses the window for control coloring + * - Applies theming and subclassing to child controls + * - Enables custom draw-based theming via notification subclassing + * - Subclasses the window to handle dark mode change if window mode is enabled. + * + * @param[in] hWnd Handle to the window. No action taken if `nullptr`. + * @param[in] setSettingChangeSubclass `true` to set setting change subclass if applicable. + * @param[in] useWin11Features `true` to enable Windows 11 specific styling like Mica or rounded corners. + * + * @note `setSettingChangeSubclass = true` should be used only on main window. + * For other secondary windows and controls use @ref dmlib::setDarkWndNotifySafe. + * Should not be used in combination with @ref dmlib::setDarkWndSafe + * and @ref dmlib::setDarkWndNotifySafe to avoid overlapping styling logic. + * + * @see dmlib::setDarkWndNotifySafe() + * @see dmlib::setDarkWndSafe() + * @see dmlib::setDarkTitleBarEx() + * @see dmlib::setWindowCtlColorSubclass() + * @see dmlib::setWindowNotifyCustomDrawSubclass() + * @see dmlib::setChildCtrlsSubclassAndTheme() + * @see dmlib::isWindowsModeEnabled() + * @see dmlib::setWindowSettingChangeSubclass() + */ +void dmlib::setDarkWndNotifySafeEx(HWND hWnd, bool setSettingChangeSubclass, bool useWin11Features) +{ + if (hWnd == nullptr) + { + return; + } + + dmlib::setDarkTitleBarEx(hWnd, useWin11Features); + dmlib::setWindowCtlColorSubclass(hWnd); + dmlib::setWindowNotifyCustomDrawSubclass(hWnd); + dmlib::setChildCtrlsSubclassAndTheme(hWnd); + if (setSettingChangeSubclass && dmlib::isWindowsModeEnabled()) + { + dmlib::setWindowSettingChangeSubclass(hWnd); + } +} + +/** + * @brief Applies visual styles; ctl color message, child controls, and custom drawing subclassings with Windows 11 features. + * + * Calls @ref dmlib::setDarkWndNotifySafeEx with `setSettingChangeSubclass = false` + * and `useWin11Features = true`, streamlining dark mode setup for secondary or transient windows + * that don't need to track system dark mode changes. + * + * @param[in] hWnd Handle to the target window. + * + * @note Should not be used in combination with @ref dmlib::setDarkWndSafe + * and @ref dmlib::setDarkWndNotifySafeEx to avoid overlapping styling logic. + * + * @see dmlib::setDarkWndNotifySafeEx() + * @see dmlib::setDarkWndSafe() + */ +void dmlib::setDarkWndNotifySafe(HWND hWnd) +{ + dmlib::setDarkWndNotifySafeEx(hWnd, false, true); +} + +/** + * @brief Enables or disables theme-based dialog background textures in classic mode. + * + * Applies `ETDT_ENABLETAB` only when `theme` is `true` and the current mode is classic. + * This replaces the default classic gray background with a lighter themed texture. + * Otherwise disables themed dialog textures with `ETDT_DISABLE`. + * + * @param[in] hWnd Handle to the target dialog window. + * @param[in] theme `true` to enable themed tab textures in classic mode. + * + * @see EnableThemeDialogTexture + */ +void dmlib::enableThemeDialogTexture(HWND hWnd, bool theme) +{ + ::EnableThemeDialogTexture(hWnd, theme && (g_dmCfg.m_dmType == DarkModeType::classic) ? ETDT_ENABLETAB : ETDT_DISABLE); +} + +/** + * @brief Enables or disables visual styles for a window. + * + * Applies `SetWindowTheme(hWnd, L"", L"")` when `doDisable` is `true`, effectively removing + * the current theme. Restores default theming when `doDisable` is `false`. + * + * @param[in] hWnd Handle to the window. + * @param[in] doDisable `true` to strip visual styles, `false` to re-enable them. + * + * @see SetWindowTheme + */ +void dmlib::disableVisualStyle(HWND hWnd, bool doDisable) +{ + if (doDisable) + { + ::SetWindowTheme(hWnd, L"", L""); + } + else + { + ::SetWindowTheme(hWnd, nullptr, nullptr); + } +} + +/** + * @brief Calculates perceptual lightness of a COLORREF color. + * + * Converts the RGB color to linear space and calculates perceived lightness. + * + * @param[in] clr COLORREF in 0xBBGGRR format. + * @return Lightness value as a double. + */ +double dmlib::calculatePerceivedLightness(COLORREF clr) +{ + return dmlib_color::calculatePerceivedLightness(clr); +} + +/** + * @brief Retrieves the current TreeView style configuration. + * + * @return Integer with enum value corresponding to the current `TreeViewStyle`. + */ +int dmlib::getTreeViewStyle() +{ + return static_cast(g_dmCfg.m_tvStyle); +} + +/** + * @brief Set TreeView style + * + * @param tvStyle TreeView style to set. + */ +static void setTreeViewStyle(dmlib::TreeViewStyle tvStyle) noexcept +{ + g_dmCfg.m_tvStyle = tvStyle; +} + +/** + * @brief Determines appropriate TreeView style based on background perceived lightness. + * + * Checks the perceived lightness of the current view background and + * selects a corresponding style: dark, light, or classic. Style selection + * is based on how far the lightness deviates from the middle gray threshold range + * around the midpoint value (50.0). + * + * @see dmlib::calculatePerceivedLightness() + */ +void dmlib::calculateTreeViewStyle() +{ + static constexpr double middle = 50.0; + + if (const COLORREF bgColor = dmlib::getViewBackgroundColor(); + g_dmCfg.m_tvBackground != bgColor || (g_dmCfg.m_lightness - middle) == 0.0) + { + g_dmCfg.m_lightness = dmlib::calculatePerceivedLightness(bgColor); + g_dmCfg.m_tvBackground = bgColor; + } + + if (g_dmCfg.m_lightness < (middle - kMiddleGrayRange)) + { + setTreeViewStyle(TreeViewStyle::dark); + } + else if (g_dmCfg.m_lightness > (middle + kMiddleGrayRange)) + { + setTreeViewStyle(TreeViewStyle::light); + } + else + { + setTreeViewStyle(TreeViewStyle::classic); + } +} + +/** + * @brief (Re)applies the appropriate window theme style to the specified TreeView . + * + * Updates the TreeView's visual behavior and theme based on the currently selected + * style @ref dmlib::getTreeViewStyle. It conditionally adjusts the `TVS_TRACKSELECT` + * style flag and applies a matching visual theme using `SetWindowTheme()`. + * + * If `force` is `true`, the style is applied regardless of previous state. + * Otherwise, the update occurs only if the style has changed since the last update. + * + * - `light`: Enables `TVS_TRACKSELECT`, applies "Explorer" theme. + * - `dark`: If supported, enables `TVS_TRACKSELECT`, applies "DarkMode_Explorer" theme. + * - `classic`: Disables `TVS_TRACKSELECT`, clears the theme. + * + * @param[in] hWnd Handle to the TreeView control. + * @param[in] force Whether to forcibly reapply the style even if unchanged. + * + * @see TreeViewStyle + * @see dmlib::getTreeViewStyle() + * @see dmlib::getPrevTreeViewStyle() + */ +void dmlib::setTreeViewWindowThemeEx(HWND hWnd, bool force) +{ + if (!force && dmlib::getPrevTreeViewStyle() == dmlib::getTreeViewStyle()) + { + return; + } + + auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + const bool hasHotStyle = (nStyle & TVS_TRACKSELECT) == TVS_TRACKSELECT; + bool change = false; + const wchar_t* strSubAppName = nullptr; + + switch (static_cast(dmlib::getTreeViewStyle())) + { + case TreeViewStyle::light: + { + if (!hasHotStyle) + { + nStyle |= TVS_TRACKSELECT; + change = true; + } + strSubAppName = L"Explorer"; + break; + } + + case TreeViewStyle::dark: + { + if (dmlib::isExperimentalSupported()) + { + if (!hasHotStyle) + { + nStyle |= TVS_TRACKSELECT; + change = true; + } + strSubAppName = L"DarkMode_Explorer"; + break; + } + [[fallthrough]]; + } + + case TreeViewStyle::classic: + { + if (hasHotStyle) + { + nStyle &= ~TVS_TRACKSELECT; + change = true; + } + break; + } + } + + if (change) + { + ::SetWindowLongPtr(hWnd, GWL_STYLE, nStyle); + } + + ::SetWindowTheme(hWnd, strSubAppName, nullptr); +} + +/** + * @brief Applies the appropriate window theme style to the specified TreeView. + * + * Forwards to `dmlib::setTreeViewWindowThemeEx` with `force = false` to change tree view style + * only if needed. + * + * @param[in] hWnd Handle to the TreeView control. + * + * @see dmlib::setTreeViewWindowThemeEx() + */ +void dmlib::setTreeViewWindowTheme(HWND hWnd) +{ + dmlib::setTreeViewWindowThemeEx(hWnd, false); +} + +/** + * @brief Retrieves the previous TreeView style configuration. + * + * @return Reference to the previous `TreeViewStyle`. + */ +int dmlib::getPrevTreeViewStyle() +{ + return static_cast(g_dmCfg.m_tvStylePrev); +} + +/** + * @brief Stores the current TreeView style as the previous style for later comparison. + */ +void dmlib::setPrevTreeViewStyle() +{ + g_dmCfg.m_tvStylePrev = static_cast(dmlib::getTreeViewStyle()); +} + +/** + * @brief Checks whether the current theme is dark. + * + * Internally it use TreeView style to determine if dark theme is used. + * + * @return `true` if the active style is `TreeViewStyle::dark`, otherwise `false`. + * + * @see dmlib::getTreeViewStyle() + */ +bool dmlib::isThemeDark() +{ + return static_cast(dmlib::getTreeViewStyle()) == TreeViewStyle::dark; +} + +/** + * @brief Checks whether the color is dark. + * + * @param clr Color to check. + * + * @return `true` if the perceived lightness of the color + * is less than (50.0 - kMiddleGrayRange), otherwise `false`. + * + * @see dmlib::calculatePerceivedLightness() + */ +bool dmlib::isColorDark(COLORREF clr) +{ + static constexpr double middle = 50.0; + return dmlib::calculatePerceivedLightness(clr) < (middle - kMiddleGrayRange); +} + +/** + * @brief Forces a window to redraw its non-client frame. + * + * Triggers a non-client area update by using `SWP_FRAMECHANGED` without changing + * size, position, or Z-order. + * + * @param[in] hWnd Handle to the target window. + */ +void dmlib::redrawWindowFrame(HWND hWnd) +{ + ::SetWindowPos(hWnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); +} + +/** + * @brief Sets or clears a specific window style or extended style. + * + * Checks if the specified `dwFlag` is already set and toggles it if needed. + * Only valid for `GWL_STYLE` or `GWL_EXSTYLE`. + * + * @param[in] hWnd Handle to the window. + * @param[in] setFlag `true` to set the flag, `false` to clear it. + * @param[in] dwFlag Style bitmask to apply. + * @param[in] gwlIdx Either `GWL_STYLE` or `GWL_EXSTYLE`. + * @return `TRUE` if modified, `FALSE` if unchanged, `-1` if invalid index. + */ +static int setWindowLongPtrStyle(HWND hWnd, bool setFlag, LONG_PTR dwFlag, int gwlIdx) noexcept +{ + if ((gwlIdx != GWL_STYLE) && (gwlIdx != GWL_EXSTYLE)) + { + return -1; + } + + auto nStyle = ::GetWindowLongPtrW(hWnd, gwlIdx); + + if (const bool hasFlag = (nStyle & dwFlag) == dwFlag; + setFlag != hasFlag) + { + nStyle ^= dwFlag; + ::SetWindowLongPtrW(hWnd, gwlIdx, nStyle); + return TRUE; + } + return FALSE; +} + +/** + * @brief Sets a window's standard style flags and redraws window if needed. + * + * Wraps @ref setWindowLongPtrStyle with `GWL_STYLE` + * and calls @ref dmlib::redrawWindowFrame if a change occurs. + * + * @param[in] hWnd Handle to the target window. + * @param[in] setStyle `true` to set the flag, `false` to remove it. + * @param[in] styleFlag Style bit to modify. + */ +void dmlib::setWindowStyle(HWND hWnd, bool setStyle, LONG_PTR styleFlag) +{ + if (setWindowLongPtrStyle(hWnd, setStyle, styleFlag, GWL_STYLE) == TRUE) + { + dmlib::redrawWindowFrame(hWnd); + } +} + +/** + * @brief Sets a window's extended style flags and redraws window if needed. + * + * Wraps @ref setWindowLongPtrStyle with `GWL_EXSTYLE` + * and calls @ref dmlib::redrawWindowFrame if a change occurs. + * + * @param[in] hWnd Handle to the target window. + * @param[in] setExStyle `true` to set the flag, `false` to remove it. + * @param[in] exStyleFlag Extended style bit to modify. + */ +void dmlib::setWindowExStyle(HWND hWnd, bool setExStyle, LONG_PTR exStyleFlag) +{ + if (setWindowLongPtrStyle(hWnd, setExStyle, exStyleFlag, GWL_EXSTYLE) == TRUE) + { + dmlib::redrawWindowFrame(hWnd); + } +} + +/** + * @brief Replaces an extended edge (e.g. client edge) with a standard window border. + * + * The given `exStyleFlag` must be a valid edge-related extended window style: + * - `WS_EX_CLIENTEDGE` + * - `WS_EX_DLGMODALFRAME` + * - `WS_EX_STATICEDGE` + * - `WS_EX_WINDOWEDGE` + * ...or any combination of these. + * + * If `replace` is `true`, the specified extended edge style(s) are removed and + * `WS_BORDER` is applied. If `false`, the edge style(s) are restored and `WS_BORDER` is cleared. + * + * @param[in] hWnd Handle to the target window. + * @param[in] replace `true` to apply standard border; `false` to restore extended edge(s). + * @param[in] exStyleFlag One or more valid edge-related extended styles. + * + * @see dmlib::setWindowExStyle() + * @see dmlib::setWindowStyle() + */ +void dmlib::replaceExEdgeWithBorder(HWND hWnd, bool replace, LONG_PTR exStyleFlag) +{ + dmlib::setWindowStyle(hWnd, replace, WS_BORDER); + dmlib::setWindowExStyle(hWnd, !replace, exStyleFlag); +} + +/** + * @brief Safely toggles `WS_EX_CLIENTEDGE` with `WS_BORDER`. + * + * @param[in] hWnd Handle to the target window. No action is taken if `hWnd` is `nullptr`. + * @param[in] replace `true` to apply `WS_BORDER`; `false` to restore`WS_EX_CLIENTEDGE`. + * + * @note Functions only affects controls, which have `WS_EX_CLIENTEDGE` or `WS_BORDER` (ex)styles. + * + * @see dmlib::replaceExEdgeWithBorder() + */ +void dmlib::replaceClientEdgeWithBorderSafeEx(HWND hWnd, bool replace) +{ + if (hWnd == nullptr) + { + return; + } + + const auto nStyle = ::GetWindowLongPtrW(hWnd, GWL_STYLE); + const bool hasBorder = (nStyle & WS_BORDER) == WS_BORDER; + + const auto nExStyle = ::GetWindowLongPtrW(hWnd, GWL_EXSTYLE); + const bool hasClientEdge = (nExStyle & WS_EX_CLIENTEDGE) == WS_EX_CLIENTEDGE; + + if (hasBorder || hasClientEdge) + { + dmlib::replaceExEdgeWithBorder(hWnd, replace, WS_EX_CLIENTEDGE); + } +} + +/** + * @brief Safely toggles `WS_EX_CLIENTEDGE` with `WS_BORDER` based on dark mode state. + * + * If dark mode is enabled, removes `WS_EX_CLIENTEDGE` and applies `WS_BORDER`. + * Otherwise restores the extended edge style. + * + * @param[in] hWnd Handle to the target window. No action is taken if `hWnd` is `nullptr`. + * + * @note Functions only affects controls, which have `WS_EX_CLIENTEDGE` or `WS_BORDER` (ex)styles. + * + * @see dmlib::replaceClientEdgeWithBorderSafeEx() + */ +void dmlib::replaceClientEdgeWithBorderSafe(HWND hWnd) +{ + dmlib::replaceClientEdgeWithBorderSafeEx(hWnd, dmlib::isEnabled()); +} + +/** + * @brief Applies classic-themed styling to a progress bar in non-classic mode. + * + * When dark mode is enabled, applies `WS_BORDER`, removes visual styles + * to allow to set custom background and fill colors using: + * - Background: `dmlib::getCtrlBackgroundColor()` + * - Fill: Hardcoded light green #06B025, dark green #0F7B0F, + * or azure #60CDFF via `PBM_SETBARCOLOR` + * + * Typically used for marquee style progress bar. + * + * @param[in] hWnd Handle to the progress bar control. + * + * @see dmlib::setWindowStyle() + * @see dmlib::disableVisualStyle() + */ +void dmlib::setProgressBarClassicTheme(HWND hWnd) +{ + dmlib::setWindowStyle(hWnd, dmlib::isEnabled(), WS_BORDER); + dmlib::disableVisualStyle(hWnd, dmlib::isEnabled()); + dmlib::setWindowExStyle(hWnd, false, WS_EX_STATICEDGE); + if (dmlib::isEnabled()) + { + ::SendMessage(hWnd, PBM_SETBKCOLOR, 0, static_cast(dmlib::getCtrlBackgroundColor())); + static constexpr COLORREF greenLight = dmlib_color::HEXRGB(0x06B025); + static constexpr COLORREF greenDark = dmlib_color::HEXRGB(0x0F7B0F); + static constexpr COLORREF azureDark = dmlib_color::HEXRGB(0x60CDFF); + static const auto clrDark = doesWin11SupportDarkThemeStyle() ? azureDark : greenDark; + ::SendMessage(hWnd, PBM_SETBARCOLOR, 0, static_cast(dmlib::isExperimentalActive() ? clrDark : greenLight)); + } +} + +/** + * @brief Handles text and background colorizing for read-only controls. + * + * Sets the text color and background color on the provided HDC. + * Returns the corresponding background brush for painting. + * Typically used for read-only controls (e.g. edit control and combo box' list box). + * Typically used in response to `WM_CTLCOLORSTATIC` or in `WM_CTLCOLORLISTBOX` + * via @ref dmlib::onCtlColorListbox + * + * @param[in] hdc Handle to the device context (HDC) receiving the drawing instructions. + * @return Background brush to use for painting, or `FALSE` (0) if classic mode is enabled + * and `_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS` is defined + * and has non-zero unsigned value. + * + * @see dmlib::WindowCtlColorSubclass() + * @see dmlib::onCtlColorListbox() + */ +LRESULT dmlib::onCtlColor(HDC hdc) +{ +#if defined(_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS) && (_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS > 0) + if (!dmlib::_isEnabled()) + { + return FALSE; + } +#endif + ::SetTextColor(hdc, dmlib::getTextColor()); + ::SetBkColor(hdc, dmlib::getBackgroundColor()); + return reinterpret_cast(dmlib::getBackgroundBrush()); +} + +/** + * @brief Handles text and background colorizing for interactive controls. + * + * Sets the text and background colors on the provided HDC. + * Returns the corresponding brush used to paint the background. + * Typically used in response to `WM_CTLCOLOREDIT` and `WM_CTLCOLORLISTBOX` + * via @ref dmlib::onCtlColorListbox + * + * @param[in] hdc Handle to the device context for the target control. + * @return The background brush, or `FALSE` if dark mode is disabled and + * `_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS` is defined + * and has non-zero unsigned value. + * + * @see dmlib::WindowCtlColorSubclass() + * @see dmlib::onCtlColorListbox() + */ +LRESULT dmlib::onCtlColorCtrl(HDC hdc) +{ +#if defined(_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS) && (_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS > 0) + if (!dmlib::_isEnabled()) + { + return FALSE; + } +#endif + + ::SetTextColor(hdc, dmlib::getTextColor()); + ::SetBkColor(hdc, dmlib::getCtrlBackgroundColor()); + return reinterpret_cast(dmlib::getCtrlBackgroundBrush()); +} + +/** + * @brief Handles text and background colorizing for window and disabled non-text controls. + * + * Sets the text and background colors on the provided HDC. + * Returns the corresponding brush used to paint the background. + * Typically used in response to `WM_CTLCOLORDLG`, `WM_CTLCOLORSTATIC` + * and `WM_CTLCOLORLISTBOX` via @ref dmlib::onCtlColorListbox + * + * @param[in] hdc Handle to the device context for the target control. + * @return The background brush, or `FALSE` if dark mode is disabled and + * `_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS` is defined + * and has non-zero unsigned value. + * + * @see dmlib::WindowCtlColorSubclass() + * @see dmlib::onCtlColorListbox() + */ +LRESULT dmlib::onCtlColorDlg(HDC hdc) +{ +#if defined(_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS) && (_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS > 0) + if (!dmlib::_isEnabled()) + { + return FALSE; + } +#endif + + ::SetTextColor(hdc, dmlib::getTextColor()); + ::SetBkColor(hdc, dmlib::getDlgBackgroundColor()); + return reinterpret_cast(dmlib::getDlgBackgroundBrush()); +} + +/** + * @brief Handles text and background colorizing for error state (for specific usage). + * + * Sets the text and background colors on the provided HDC. + * + * @param[in] hdc Handle to the device context for the target control. + * @return The background brush, or `FALSE` if dark mode is disabled and + * `_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS` is defined + * and has non-zero unsigned value. + * + * @see dmlib::WindowCtlColorSubclass() + */ +LRESULT dmlib::onCtlColorError(HDC hdc) +{ +#if defined(_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS) && (_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS > 0) + if (!dmlib::_isEnabled()) + { + return FALSE; + } +#endif + + ::SetTextColor(hdc, dmlib::getTextColor()); + ::SetBkColor(hdc, dmlib::getErrorBackgroundColor()); + return reinterpret_cast(dmlib::getErrorBackgroundBrush()); +} + +/** + * @brief Handles text and background colorizing for static text controls. + * + * Sets the text and background colors on the provided HDC. + * Colors depend on if control is enabled. + * Returns the corresponding brush used to paint the background. + * Typically used in response to `WM_CTLCOLORSTATIC`. + * + * @param[in] hdc Handle to the device context for the target control. + * @param[in] isTextEnabled `true` if control should use enabled colors. + * @return The background brush, or `FALSE` if dark mode is disabled and + * `_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS` is defined + * and has non-zero unsigned value. + * + * @see dmlib::WindowCtlColorSubclass() + */ +LRESULT dmlib::onCtlColorDlgStaticText(HDC hdc, bool isTextEnabled) +{ +#if defined(_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS) && (_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS > 0) + if (!dmlib::_isEnabled()) + { + ::SetTextColor(hdc, ::GetSysColor(isTextEnabled ? COLOR_WINDOWTEXT : COLOR_GRAYTEXT)); + return FALSE; + } +#endif + ::SetTextColor(hdc, isTextEnabled ? dmlib::getTextColor() : dmlib::getDisabledTextColor()); + ::SetBkColor(hdc, dmlib::getDlgBackgroundColor()); + return reinterpret_cast(dmlib::getDlgBackgroundBrush()); +} + +/** + * @brief Handles text and background colorizing for SysLink controls. + * + * Sets the text and background colors on the provided HDC. + * Colors depend on if control is enabled. + * Returns the corresponding brush used to paint the background. + * Typically used in response to `WM_CTLCOLORSTATIC`. + * + * @param[in] hdc Handle to the device context for the target control. + * @param[in] isTextEnabled `true` if control should use enabled colors. + * @return The background brush, or `FALSE` if dark mode is disabled and + * `_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS` is defined + * and has non-zero unsigned value. + * + * @see dmlib::WindowCtlColorSubclass() + */ +LRESULT dmlib::onCtlColorDlgLinkText(HDC hdc, bool isTextEnabled) +{ +#if defined(_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS) && (_DARKMODELIB_DLG_PROC_CTLCOLOR_RETURNS > 0) + if (!dmlib::_isEnabled()) + { + ::SetTextColor(hdc, ::GetSysColor(isTextEnabled ? COLOR_HOTLIGHT : COLOR_GRAYTEXT)); + return FALSE; + } +#endif + ::SetTextColor(hdc, isTextEnabled ? dmlib::getLinkTextColor() : dmlib::getDisabledTextColor()); + ::SetBkColor(hdc, dmlib::getDlgBackgroundColor()); + return reinterpret_cast(dmlib::getDlgBackgroundBrush()); +} + +/** + * @brief Handles text and background colorizing for list box controls. + * + * Inspects the list box style flags to detect if it's part of a combo box (via `LBS_COMBOBOX`) + * and whether experimental feature is active. Based on the context, delegates to: + * - @ref dmlib::onCtlColorCtrl for standard enabled listboxes + * - @ref dmlib::onCtlColorDlg for disabled ones or when dark mode is disabled + * - @ref dmlib::onCtlColor for combo box' listbox + * + * @param[in] wParam WPARAM from `WM_CTLCOLORLISTBOX`, representing the HDC. + * @param[in] lParam LPARAM from `WM_CTLCOLORLISTBOX`, representing the HWND of the listbox. + * @return The brush handle as LRESULT for background painting, or `FALSE` if not themed. + * + * @see dmlib::WindowCtlColorSubclass() + * @see dmlib::onCtlColor() + * @see dmlib::onCtlColorCtrl() + * @see dmlib::onCtlColorDlg() + */ +LRESULT dmlib::onCtlColorListbox(WPARAM wParam, LPARAM lParam) +{ + auto hdc = reinterpret_cast(wParam); + auto hWnd = reinterpret_cast(lParam); + + if (const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + ((nStyle & LBS_COMBOBOX) != LBS_COMBOBOX) // is not child of combo box + || !dmlib::isExperimentalActive()) + { + if (::IsWindowEnabled(hWnd) == TRUE) + { + return dmlib::onCtlColorCtrl(hdc); + } + return dmlib::onCtlColorDlg(hdc); + } + return dmlib::onCtlColor(hdc); +} + +/** + * @brief Hook procedure for customizing common dialogs with custom colors. + */ +UINT_PTR CALLBACK dmlib::HookDlgProc(HWND hWnd, UINT uMsg, [[maybe_unused]] WPARAM wParam, [[maybe_unused]] LPARAM lParam) +{ + if (uMsg == WM_INITDIALOG) + { + dmlib::setDarkWndSafe(hWnd); + return TRUE; + } + return FALSE; +} + +/** + * @brief Applies dark mode visual styles to task dialog. + * + * @note Currently colors cannot be customized. + * + * @param[in] hWnd Handle to the task dialog. + * + * @see dmlib_subclass::setTaskDlgChildCtrlsSubclassAndTheme() + */ +void dmlib::setDarkTaskDlg(HWND hWnd) +{ + if (dmlib::isExperimentalActive()) + { + dmlib::setDarkTitleBar(hWnd); + dmlib::setDarkExplorerTheme(hWnd); + dmlib_subclass::setTaskDlgChildCtrlsSubclassAndTheme(hWnd); + } +} + +/** + * @brief Simple task dialog callback procedure to enable dark mode support. + * + * @param[in] hWnd Handle to the task dialog. + * @param[in] uMsg Message identifier. + * @param[in] wParam First message parameter (unused). + * @param[in] lParam Second message parameter (unused). + * @param[in] lpRefData Reserved data (unused). + * @return HRESULT A value defined by the hook procedure. + * + * @see dmlib::setDarkTaskDlg() + * @see dmlib::darkTaskDialogIndirect() + */ +HRESULT CALLBACK dmlib::DarkTaskDlgCallback( + HWND hWnd, + UINT uMsg, + [[maybe_unused]] WPARAM wParam, + [[maybe_unused]] LPARAM lParam, + [[maybe_unused]] LONG_PTR lpRefData +) +{ + if (uMsg == TDN_DIALOG_CONSTRUCTED) + { + dmlib::setDarkTaskDlg(hWnd); + } + return S_OK; +} + +/** + * @brief Wrapper for `TaskDialogIndirect` with dark mode support. + */ +HRESULT dmlib::darkTaskDialogIndirect( + const TASKDIALOGCONFIG* pTaskConfig, + int* pnButton, + int* pnRadioButton, + BOOL* pfVerificationFlagChecked +) +{ + dmlib_hook::hookThemeColor(); + const HRESULT retVal = ::TaskDialogIndirect(pTaskConfig, pnButton, pnRadioButton, pfVerificationFlagChecked); + dmlib_hook::unhookThemeColor(); + return retVal; +} + +/** + * @brief Simple task dialog callback procedure for msgBoxParamToTaskDlgConfig. + * + * @param[in] hWnd Handle to the task dialog. + * @param[in] uMsg Message identifier. + * @param[in] wParam First message parameter (unused). + * @param[in] lParam Second message parameter (unused). + * @param[in] lpRefData Message box flags. + * @return HRESULT A value defined by the hook procedure. + * + * @see DarkTaskDlgMsgBoxCallback() + * @see dmlib::darkMessageBoxW() + */ +static HRESULT CALLBACK DarkTaskDlgMsgBoxCallback( + HWND hWnd, + UINT uMsg, + [[maybe_unused]] WPARAM wParam, + [[maybe_unused]] LPARAM lParam, + [[maybe_unused]] LONG_PTR lpRefData +) noexcept +{ + const auto uType = static_cast(lpRefData); + + if (uMsg == TDN_DIALOG_CONSTRUCTED) + { + dmlib::setDarkTaskDlg(hWnd); + if ((uType & (MB_SYSTEMMODAL | MB_TOPMOST)) != 0) + { + ::SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + } + + if ((uType & MB_SETFOREGROUND) == MB_SETFOREGROUND) + { + ::SetForegroundWindow(hWnd); + } + } + return S_OK; +} + +/** + * @brief Translates a message box parameter to task dialog config. + * + * @note Flags MB_HELP, MB_TASKMODAL, MB_DEFAULT_DESKTOP_ONLY, MB_SERVICE_NOTIFICATION are not supported. + * Other flags can have limited support. Check parameter uType for more information. + * + * @param[in] hWnd Handle to the owner window of the message box. + * It can be NULL if the message box has no owner. + * @param[in] lpText Text to be displayed in the message box. + * @param[in] lpCaption Text to be displayed in the title bar of the message box. + * @param[in] uType Specifies the contents and behavior of the message box. + * This parameter can be a allowed combination of the following flags: + * - MB_OK + * - MB_OKCANCEL + * - MB_ABORTRETRYIGNORE + * - MB_YESNOCANCEL + * - MB_YESNO + * - MB_RETRYCANCEL + * - MB_CANCELTRYCONTINUE + * + * - MB_DEFBUTTON1 + * - MB_DEFBUTTON2 + * - MB_DEFBUTTON3 + * - MB_DEFBUTTON4 - has no effect, there is no 4th button + * + * - MB_ICONERROR + * - MB_ICONQUESTION + * - MB_ICONWARNING + * - MB_ICONINFORMATION + * + * - MB_APPLMODAL + * - MB_SYSTEMMODAL + * + * - MB_RIGHT - has no effect, use MB_RTLREADING instead + * - MB_RTLREADING + * - MB_SETFOREGROUND + * - MB_TOPMOST + * + * @return TASKDIALOGCONFIG Returns the translated task dialog config. + * + * @see DarkTaskDlgMsgBoxCallback() + * @see dmlib::darkMessageBoxW() + */ +static TASKDIALOGCONFIG msgBoxParamToTaskDlgConfig(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) +{ + // base config + +#ifdef _MSC_VER + #pragma warning(push) + #pragma warning(disable: 26476) // Expression/symbol 'name' uses a naked union 'union' with multiple type pointers: Use variant instead (type.7) +#endif + TASKDIALOGCONFIG tdc{}; +#ifdef _MSC_VER + #pragma warning(pop) +#endif + tdc.cbSize = sizeof(TASKDIALOGCONFIG); + tdc.hwndParent = hWnd; + tdc.hInstance = nullptr; + tdc.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION | TDF_SIZE_TO_CONTENT; + tdc.pszWindowTitle = lpCaption; + tdc.pszContent = lpText; + tdc.pfCallback = DarkTaskDlgMsgBoxCallback; + tdc.lpCallbackData = static_cast(uType); + + dmlib_win32api::InitMB_GetString(); + + // buttons + + const UINT btnDefMask = uType & MB_DEFMASK; + auto getDefBtn = [&btnDefMask](std::array btnIDs) + { + if (btnDefMask == MB_DEFBUTTON2) + { + return btnIDs.at(1); + } + if (btnDefMask == MB_DEFBUTTON3) + { + return btnIDs.at(2); + } + return btnIDs.at(0); + }; + + switch (uType & MB_TYPEMASK) + { + case MB_OK: + { + tdc.dwCommonButtons = TDCBF_OK_BUTTON; + break; + } + + case MB_OKCANCEL: + { + tdc.dwCommonButtons = TDCBF_OK_BUTTON | TDCBF_CANCEL_BUTTON; + tdc.nDefaultButton = (btnDefMask == MB_DEFBUTTON2) ? IDCANCEL : IDOK; + break; + } + + case MB_ABORTRETRYIGNORE: + { + static const std::array buttons{ { + { IDABORT, dmlib_win32api::MB_GetString(IDABORT) }, + { IDRETRY, dmlib_win32api::MB_GetString(IDRETRY) }, + { IDIGNORE, dmlib_win32api::MB_GetString(IDIGNORE) } + } }; + + tdc.cButtons = static_cast(buttons.size()); + tdc.pButtons = buttons.data(); + tdc.nDefaultButton = getDefBtn({ { buttons.at(0).nButtonID, buttons.at(1).nButtonID, buttons.at(2).nButtonID } }); + + break; + } + + case MB_YESNOCANCEL: + { + tdc.dwCommonButtons = TDCBF_YES_BUTTON | TDCBF_NO_BUTTON | TDCBF_CANCEL_BUTTON; + tdc.nDefaultButton = getDefBtn({ { IDYES, IDNO, IDCANCEL } }); + break; + } + + case MB_YESNO: + { + tdc.dwCommonButtons = TDCBF_YES_BUTTON | TDCBF_NO_BUTTON; + tdc.nDefaultButton = (btnDefMask == MB_DEFBUTTON2) ? IDNO : IDYES; + break; + } + + case MB_RETRYCANCEL: + { + tdc.dwCommonButtons = TDCBF_RETRY_BUTTON | TDCBF_CANCEL_BUTTON; + tdc.nDefaultButton = (btnDefMask == MB_DEFBUTTON2) ? IDCANCEL : IDRETRY; + break; + } + + case MB_CANCELTRYCONTINUE: + { + static const std::array buttons{ { + { IDCANCEL, dmlib_win32api::MB_GetString(IDCANCEL) }, + { IDTRYAGAIN, dmlib_win32api::MB_GetString(IDTRYAGAIN) }, + { IDCONTINUE, dmlib_win32api::MB_GetString(IDCONTINUE) } + } }; + + tdc.cButtons = static_cast(buttons.size()); + tdc.pButtons = buttons.data(); + tdc.nDefaultButton = getDefBtn({ { buttons.at(0).nButtonID, buttons.at(1).nButtonID, buttons.at(2).nButtonID } }); + + break; + } + + default: + { + tdc.dwCommonButtons = TDCBF_OK_BUTTON; + break; + } + } + + // icons + + switch (uType & MB_ICONMASK) + { + case MB_ICONERROR: + { + tdc.pszMainIcon = TD_ERROR_ICON; + break; + } + + case MB_ICONQUESTION: + { + tdc.dwFlags |= TDF_USE_HICON_MAIN; + tdc.hMainIcon = static_cast(::LoadImageW(nullptr, IDI_QUESTION, IMAGE_ICON, 0, 0, LR_SHARED)); + break; + } + + case MB_ICONWARNING: + { + tdc.pszMainIcon = TD_WARNING_ICON; + break; + } + + case MB_ICONINFORMATION: + { + tdc.pszMainIcon = TD_INFORMATION_ICON; + break; + } + + default: + break; + } + + // other + + if ((uType & MB_RTLREADING) == MB_RTLREADING) + { + tdc.dwFlags |= TDF_RTL_LAYOUT; + } + + return tdc; +} + +/** + * @brief Displays a message box as task dialog with themed styling. + * + * Shows a custom task dialog instead of classic message box if @ref dmlib::isExperimentalActive is true. + * Otherwise, it falls back to the standard Windows message box function. + * The message box can present various buttons and icons based on the + * specified parameters. + * + * @param[in] hWnd Handle to the owner window of the message box. + * It can be NULL if the message box has no owner. + * @param[in] lpText Text to be displayed in the message box. + * @param[in] lpCaption Text to be displayed in the title bar of the message box. + * @param[in] uType Specifies the contents and behavior of the message box. + * + * @return int The returned value indicates which button was pressed by the user. + * Or 0 zero if the function has failed. + * + * @see DarkTaskDlgMsgBoxCallback() + * @see msgBoxParamToTaskDlgConfig() + */ +int dmlib::darkMessageBoxW( + HWND hWnd, + LPCWSTR lpText, + LPCWSTR lpCaption, + UINT uType +) +{ + if (!dmlib::isExperimentalActive()) + { + return ::MessageBoxW(hWnd, lpText, lpCaption, uType); + } + + const TASKDIALOGCONFIG tdc = msgBoxParamToTaskDlgConfig(hWnd, lpText, lpCaption, uType); + + int btnPressed = 0; + if (dmlib::darkTaskDialogIndirect(&tdc, &btnPressed, nullptr, nullptr) != S_OK) + { + return ::MessageBoxW(hWnd, lpText, lpCaption, uType); + } + return btnPressed; +} + +#endif // !defined(_DARKMODELIB_NOT_USED) diff --git a/darkmodelib/src/DmlibColor.cpp b/darkmodelib/src/DmlibColor.cpp new file mode 100644 index 0000000000..f769ccabc5 --- /dev/null +++ b/darkmodelib/src/DmlibColor.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#include "StdAfx.h" + +#include "DmlibColor.h" + +#include + +#include +#include + +#include + +#include "Darkmodelib.h" + +namespace dmlib_win32api +{ + [[nodiscard]] bool IsDarkModeActive() noexcept; +} // namespace dmlib_win32api + +dmlib::Colors dmlib_color::getLightColors() noexcept +{ + return dmlib::Colors{ + ::GetSysColor(COLOR_WINDOW), // background + ::GetSysColor(COLOR_WINDOW), // ctrlBackground + dmlib_color::HEXRGB(0xC0DCF3), // hotBackground + ::GetSysColor(COLOR_3DFACE), // dlgBackground + dmlib_color::HEXRGB(0xA01000), // errorBackground + ::GetSysColor(COLOR_WINDOWTEXT), // textColor + ::GetSysColor(COLOR_BTNTEXT), // darkerTextColor + ::GetSysColor(COLOR_GRAYTEXT), // disabledTextColor + ::GetSysColor(COLOR_HOTLIGHT), // linkTextColor + dmlib_color::HEXRGB(0x8D8D8D), // edgeColor + ::GetSysColor(COLOR_HIGHLIGHT), // hotEdgeColor + ::GetSysColor(COLOR_GRAYTEXT), // disabledEdgeColor + ::GetSysColor(COLOR_HOTLIGHT) // highlight + }; +} + +/** + * @brief Calculates perceptual lightness of a COLORREF color. + * + * Converts the RGB color to linear space and calculates perceived lightness. + * + * @param[in] clr COLORREF in 0xBBGGRR format. + * @return Lightness value as a double. + * + * @note Based on: https://stackoverflow.com/a/56678483 + */ +double dmlib_color::calculatePerceivedLightness(COLORREF clr) noexcept +{ + auto linearValue = [](double colorChannel) noexcept + { + colorChannel /= 255.0; + + static constexpr double treshhold = 0.04045; + static constexpr double lowScalingFactor = 12.92; + static constexpr double gammaOffset = 0.055; + static constexpr double gammaScalingFactor = 1.055; + static constexpr double gammaExp = 2.4; + + if (colorChannel <= treshhold) + { + return colorChannel / lowScalingFactor; + } + return std::pow(((colorChannel + gammaOffset) / gammaScalingFactor), gammaExp); + }; + + const double r = linearValue(static_cast(GetRValue(clr))); + const double g = linearValue(static_cast(GetGValue(clr))); + const double b = linearValue(static_cast(GetBValue(clr))); + + static constexpr double rWeight = 0.2126; + static constexpr double gWeight = 0.7152; + static constexpr double bWeight = 0.0722; + + const double luminance = (rWeight * r) + (gWeight * g) + (bWeight * b); + + static constexpr double cieEpsilon = 216.0 / 24389.0; + static constexpr double cieKappa = 24389.0 / 27.0; + static constexpr double oneThird = 1.0 / 3.0; + static constexpr double scalingFactor = 116.0; + static constexpr double offset = 16.0; + + // calculate lightness + + if (luminance <= cieEpsilon) + { + return (luminance * cieKappa); + } + return ((std::pow(luminance, oneThird) * scalingFactor) - offset); +} + +static COLORREF adjustClrLightness(COLORREF clr, bool useDark) noexcept +{ + WORD h = 0; + WORD s = 0; + WORD l = 0; + ::ColorRGBToHLS(clr, &h, &l, &s); + + static constexpr double lightnessThreshold = 50.0 - 3.0; + static constexpr WORD saturationAdjustment = 20; + static constexpr WORD luminanceAdjustment = 50; + + if (dmlib_color::calculatePerceivedLightness(clr) < lightnessThreshold) + { + s -= saturationAdjustment; + l += luminanceAdjustment; + return useDark ? ::ColorHLSToRGB(h, l, s) : clr; + } + + s += saturationAdjustment; + l -= luminanceAdjustment; + return useDark ? clr : ::ColorHLSToRGB(h, l, s); +} + +COLORREF dmlib_color::getAccentColor(bool adjust) noexcept +{ + BOOL opaque = TRUE; + COLORREF clrAccent = 0; + + if (FAILED(::DwmGetColorizationColor(&clrAccent, &opaque))) + { + return CLR_INVALID; + } + + // DwmGetColorizationColor use 0xAARRGGBB format + clrAccent = RGB(GetBValue(clrAccent), GetGValue(clrAccent), GetRValue(clrAccent)); + + if (adjust) + { + clrAccent = adjustClrLightness(clrAccent, dmlib_win32api::IsDarkModeActive()); + } + + return clrAccent; +} diff --git a/darkmodelib/src/DmlibColor.h b/darkmodelib/src/DmlibColor.h new file mode 100644 index 0000000000..a26c816591 --- /dev/null +++ b/darkmodelib/src/DmlibColor.h @@ -0,0 +1,696 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +#include + +#include "Darkmodelib.h" + +namespace dmlib_color +{ + /// Converts 0xRRGGBB to COLORREF (0xBBGGRR) for GDI usage. + constexpr COLORREF HEXRGB(DWORD rrggbb) noexcept + { + return + ((rrggbb & 0xFF0000) >> 16) + | (rrggbb & 0x00FF00) + | ((rrggbb & 0x0000FF) << 16); + } + + /// Black tone (default) + inline constexpr dmlib::Colors kDarkColors{ + dmlib_color::HEXRGB(0x202020), // background + dmlib_color::HEXRGB(0x383838), // ctrlBackground + dmlib_color::HEXRGB(0x454545), // hotBackground + dmlib_color::HEXRGB(0x202020), // dlgBackground + dmlib_color::HEXRGB(0xB00000), // errorBackground + dmlib_color::HEXRGB(0xE0E0E0), // textColor + dmlib_color::HEXRGB(0xC0C0C0), // darkerTextColor + dmlib_color::HEXRGB(0x808080), // disabledTextColor + dmlib_color::HEXRGB(0x60CDFF), // linkTextColor + dmlib_color::HEXRGB(0x646464), // edgeColor + dmlib_color::HEXRGB(0x9B9B9B), // hotEdgeColor + dmlib_color::HEXRGB(0x484848), // disabledEdgeColor + dmlib_color::HEXRGB(0x60CDFF) // highlight + }; + + inline constexpr DWORD kOffsetEdge = dmlib_color::HEXRGB(0x1C1C1C); + + /// Red tone + inline constexpr DWORD kOffsetRed = dmlib_color::HEXRGB(0x100000); + inline constexpr dmlib::Colors kDarkRedColors{ + kDarkColors.background + kOffsetRed, + kDarkColors.ctrlBackground + kOffsetRed, + kDarkColors.hotBackground + kOffsetRed, + kDarkColors.dlgBackground + kOffsetRed, + kDarkColors.errorBackground, + kDarkColors.text, + kDarkColors.darkerText, + kDarkColors.disabledText, + kDarkColors.linkText, + kDarkColors.edge + kOffsetEdge + kOffsetRed, + kDarkColors.hotEdge + kOffsetRed, + kDarkColors.disabledEdge + kOffsetRed, + kDarkColors.highlight + }; + + /// Green tone + inline constexpr DWORD kOffsetGreen = dmlib_color::HEXRGB(0x001000); + inline constexpr dmlib::Colors kDarkGreenColors{ + kDarkColors.background + kOffsetGreen, + kDarkColors.ctrlBackground + kOffsetGreen, + kDarkColors.hotBackground + kOffsetGreen, + kDarkColors.dlgBackground + kOffsetGreen, + kDarkColors.errorBackground, + kDarkColors.text, + kDarkColors.darkerText, + kDarkColors.disabledText, + kDarkColors.linkText, + kDarkColors.edge + kOffsetEdge + kOffsetGreen, + kDarkColors.hotEdge + kOffsetGreen, + kDarkColors.disabledEdge + kOffsetGreen, + kDarkColors.highlight + }; + + /// Blue tone + inline constexpr DWORD kOffsetBlue = dmlib_color::HEXRGB(0x000020); + inline constexpr dmlib::Colors kDarkBlueColors{ + kDarkColors.background + kOffsetBlue, + kDarkColors.ctrlBackground + kOffsetBlue, + kDarkColors.hotBackground + kOffsetBlue, + kDarkColors.dlgBackground + kOffsetBlue, + kDarkColors.errorBackground, + kDarkColors.text, + kDarkColors.darkerText, + kDarkColors.disabledText, + kDarkColors.linkText, + kDarkColors.edge + kOffsetEdge + kOffsetBlue, + kDarkColors.hotEdge + kOffsetBlue, + kDarkColors.disabledEdge + kOffsetBlue, + kDarkColors.highlight + }; + + /// Purple tone + inline constexpr DWORD kOffsetPurple = dmlib_color::HEXRGB(0x100020); + inline constexpr dmlib::Colors kDarkPurpleColors{ + kDarkColors.background + kOffsetPurple, + kDarkColors.ctrlBackground + kOffsetPurple, + kDarkColors.hotBackground + kOffsetPurple, + kDarkColors.dlgBackground + kOffsetPurple, + kDarkColors.errorBackground, + kDarkColors.text, + kDarkColors.darkerText, + kDarkColors.disabledText, + kDarkColors.linkText, + kDarkColors.edge + kOffsetEdge + kOffsetPurple, + kDarkColors.hotEdge + kOffsetPurple, + kDarkColors.disabledEdge + kOffsetPurple, + kDarkColors.highlight + }; + + /// Cyan tone + inline constexpr DWORD kOffsetCyan = dmlib_color::HEXRGB(0x001020); + inline constexpr dmlib::Colors kDarkCyanColors{ + kDarkColors.background + kOffsetCyan, + kDarkColors.ctrlBackground + kOffsetCyan, + kDarkColors.hotBackground + kOffsetCyan, + kDarkColors.dlgBackground + kOffsetCyan, + kDarkColors.errorBackground, + kDarkColors.text, + kDarkColors.darkerText, + kDarkColors.disabledText, + kDarkColors.linkText, + kDarkColors.edge + kOffsetEdge + kOffsetCyan, + kDarkColors.hotEdge + kOffsetCyan, + kDarkColors.disabledEdge + kOffsetCyan, + kDarkColors.highlight + }; + + /// Olive tone + inline constexpr DWORD kOffsetOlive = dmlib_color::HEXRGB(0x101000); + inline constexpr dmlib::Colors kDarkOliveColors{ + kDarkColors.background + kOffsetOlive, + kDarkColors.ctrlBackground + kOffsetOlive, + kDarkColors.hotBackground + kOffsetOlive, + kDarkColors.dlgBackground + kOffsetOlive, + kDarkColors.errorBackground, + kDarkColors.text, + kDarkColors.darkerText, + kDarkColors.disabledText, + kDarkColors.linkText, + kDarkColors.edge + kOffsetEdge + kOffsetOlive, + kDarkColors.hotEdge + kOffsetOlive, + kDarkColors.disabledEdge + kOffsetOlive, + kDarkColors.highlight + }; + + /// Dark views colors + inline constexpr dmlib::ColorsView kDarkColorsView{ + dmlib_color::HEXRGB(0x293134), // background + dmlib_color::HEXRGB(0xE0E2E4), // text + dmlib_color::HEXRGB(0x646464), // gridlines + dmlib_color::HEXRGB(0x202020), // Header background + dmlib_color::HEXRGB(0x454545), // Header hot background + dmlib_color::HEXRGB(0xC0C0C0), // header text + dmlib_color::HEXRGB(0x646464) // header divider + }; + + /// Light views colors + inline constexpr dmlib::ColorsView kLightColorsView{ + dmlib_color::HEXRGB(0xFFFFFF), // background + dmlib_color::HEXRGB(0x000000), // text + dmlib_color::HEXRGB(0xF0F0F0), // gridlines + dmlib_color::HEXRGB(0xFFFFFF), // header background + dmlib_color::HEXRGB(0xD9EBF9), // header hot background + dmlib_color::HEXRGB(0x000000), // header text + dmlib_color::HEXRGB(0xE5E5E5) // header divider + }; + + dmlib::Colors getLightColors() noexcept; + + inline COLORREF setNewColor(COLORREF& clrOld, COLORREF clrNew) noexcept + { + const auto clrTmp = clrOld ; + clrOld = clrNew; + return clrTmp; + } + + struct Brushes + { + HBRUSH m_background = nullptr; + HBRUSH m_ctrlBackground = nullptr; + HBRUSH m_hotBackground = nullptr; + HBRUSH m_dlgBackground = nullptr; + HBRUSH m_errorBackground = nullptr; + + HBRUSH m_edge = nullptr; + HBRUSH m_hotEdge = nullptr; + HBRUSH m_disabledEdge = nullptr; + HBRUSH m_highlight = nullptr; + + Brushes() = delete; + + explicit Brushes(const dmlib::Colors& colors) noexcept + : m_background(::CreateSolidBrush(colors.background)) + , m_ctrlBackground(::CreateSolidBrush(colors.ctrlBackground)) + , m_hotBackground(::CreateSolidBrush(colors.hotBackground)) + , m_dlgBackground(::CreateSolidBrush(colors.dlgBackground)) + , m_errorBackground(::CreateSolidBrush(colors.errorBackground)) + + , m_edge(::CreateSolidBrush(colors.edge)) + , m_hotEdge(::CreateSolidBrush(colors.hotEdge)) + , m_disabledEdge(::CreateSolidBrush(colors.disabledEdge)) + + , m_highlight(::CreateSolidBrush(colors.highlight)) + {} + + Brushes(const Brushes&) = delete; + Brushes& operator=(const Brushes&) = delete; + + Brushes(Brushes&&) = delete; + Brushes& operator=(Brushes&&) = delete; + + ~Brushes() + { + ::DeleteObject(m_background); m_background = nullptr; + ::DeleteObject(m_ctrlBackground); m_ctrlBackground = nullptr; + ::DeleteObject(m_hotBackground); m_hotBackground = nullptr; + ::DeleteObject(m_dlgBackground); m_dlgBackground = nullptr; + ::DeleteObject(m_errorBackground); m_errorBackground = nullptr; + + ::DeleteObject(m_edge); m_edge = nullptr; + ::DeleteObject(m_hotEdge); m_hotEdge = nullptr; + ::DeleteObject(m_disabledEdge); m_disabledEdge = nullptr; + + ::DeleteObject(m_highlight); m_highlight = nullptr; + } + + void updateBrushes(const dmlib::Colors& colors) noexcept + { + ::DeleteObject(m_background); + ::DeleteObject(m_ctrlBackground); + ::DeleteObject(m_hotBackground); + ::DeleteObject(m_dlgBackground); + ::DeleteObject(m_errorBackground); + + ::DeleteObject(m_edge); + ::DeleteObject(m_hotEdge); + ::DeleteObject(m_disabledEdge); + + ::DeleteObject(m_highlight); + + m_background = ::CreateSolidBrush(colors.background); + m_ctrlBackground = ::CreateSolidBrush(colors.ctrlBackground); + m_hotBackground = ::CreateSolidBrush(colors.hotBackground); + m_dlgBackground = ::CreateSolidBrush(colors.dlgBackground); + m_errorBackground = ::CreateSolidBrush(colors.errorBackground); + + m_edge = ::CreateSolidBrush(colors.edge); + m_hotEdge = ::CreateSolidBrush(colors.hotEdge); + m_disabledEdge = ::CreateSolidBrush(colors.disabledEdge); + + m_highlight = ::CreateSolidBrush(colors.highlight); + } + }; + + struct Pens + { + HPEN m_darkerText = nullptr; + HPEN m_edge = nullptr; + HPEN m_hotEdge = nullptr; + HPEN m_disabledEdge = nullptr; + HPEN m_highlight = nullptr; + + Pens() = delete; + + explicit Pens(const dmlib::Colors& colors) noexcept + : m_darkerText(::CreatePen(PS_SOLID, 1, colors.darkerText)) + , m_edge(::CreatePen(PS_SOLID, 1, colors.edge)) + , m_hotEdge(::CreatePen(PS_SOLID, 1, colors.hotEdge)) + , m_disabledEdge(::CreatePen(PS_SOLID, 1, colors.disabledEdge)) + , m_highlight(::CreatePen(PS_SOLID, 1, colors.highlight)) + {} + + Pens(const Pens&) = delete; + Pens& operator=(const Pens&) = delete; + + Pens(Pens&&) = delete; + Pens& operator=(Pens&&) = delete; + + ~Pens() + { + ::DeleteObject(m_darkerText); m_darkerText = nullptr; + ::DeleteObject(m_edge); m_edge = nullptr; + ::DeleteObject(m_hotEdge); m_hotEdge = nullptr; + ::DeleteObject(m_disabledEdge); m_disabledEdge = nullptr; + ::DeleteObject(m_highlight); m_highlight = nullptr; + } + + void updatePens(const dmlib::Colors& colors) noexcept + { + ::DeleteObject(m_darkerText); + ::DeleteObject(m_edge); + ::DeleteObject(m_hotEdge); + ::DeleteObject(m_disabledEdge); + ::DeleteObject(m_highlight); + + m_darkerText = ::CreatePen(PS_SOLID, 1, colors.darkerText); + m_edge = ::CreatePen(PS_SOLID, 1, colors.edge); + m_hotEdge = ::CreatePen(PS_SOLID, 1, colors.hotEdge); + m_disabledEdge = ::CreatePen(PS_SOLID, 1, colors.disabledEdge); + m_highlight = ::CreatePen(PS_SOLID, 1, colors.highlight); + } + }; + + class Theme + { + public: + Theme() noexcept + : m_colors(kDarkColors) + , m_brushes(kDarkColors) + , m_pens(kDarkColors) + {} + + explicit Theme(const dmlib::Colors& colors) noexcept + : m_colors(colors) + , m_brushes(colors) + , m_pens(colors) + {} + + void updateTheme() noexcept + { + m_brushes.updateBrushes(m_colors); + m_pens.updatePens(m_colors); + } + + void updateTheme(const dmlib::Colors& colors, bool update = true) noexcept + { + m_colors = dmlib::Colors{ colors }; + if (update) + { + Theme::updateTheme(); + } + } + + [[nodiscard]] dmlib::Colors getToneColors() const noexcept + { + switch (m_tone) + { + case dmlib::ColorTone::red: + { + return kDarkRedColors; + } + + case dmlib::ColorTone::green: + { + return kDarkGreenColors; + } + + case dmlib::ColorTone::blue: + { + return kDarkBlueColors; + } + + case dmlib::ColorTone::purple: + { + return kDarkPurpleColors; + } + + case dmlib::ColorTone::cyan: + { + return kDarkCyanColors; + } + + case dmlib::ColorTone::olive: + { + return kDarkOliveColors; + } + + case dmlib::ColorTone::black: + case dmlib::ColorTone::max: + { + break; + } + } + return kDarkColors; + } + + void setToneColors(dmlib::ColorTone colorTone) noexcept + { + m_tone = colorTone; + + switch (m_tone) + { + case dmlib::ColorTone::red: + { + m_colors = kDarkRedColors; + break; + } + + case dmlib::ColorTone::green: + { + m_colors = kDarkGreenColors; + break; + } + + case dmlib::ColorTone::blue: + { + m_colors = kDarkBlueColors; + break; + } + + case dmlib::ColorTone::purple: + { + m_colors = kDarkPurpleColors; + break; + } + + case dmlib::ColorTone::cyan: + { + m_colors = kDarkCyanColors; + break; + } + + case dmlib::ColorTone::olive: + { + m_colors = kDarkOliveColors; + break; + } + + case dmlib::ColorTone::black: + case dmlib::ColorTone::max: + { + m_colors = kDarkColors; + break; + } + } + + Theme::updateTheme(); + } + + void setToneColors(bool update = false) noexcept + { + updateTheme(getToneColors(), update); + } + + void setLightColors(bool update = false) noexcept + { + updateTheme(dmlib_color::getLightColors(), update); + } + + COLORREF setColorBackground(COLORREF newClr) noexcept + { + return setNewColor(m_colors.background, newClr); + } + + COLORREF setColorCtrlBackground(COLORREF newClr) noexcept + { + return setNewColor(m_colors.ctrlBackground, newClr); + } + + COLORREF setColorHotBackground(COLORREF newClr) noexcept + { + return setNewColor(m_colors.hotBackground, newClr); + } + + COLORREF setColorDlgBackground(COLORREF newClr) noexcept + { + return setNewColor(m_colors.dlgBackground, newClr); + } + + COLORREF setColorErrorBackground(COLORREF newClr) noexcept + { + return setNewColor(m_colors.errorBackground, newClr); + } + + COLORREF setColorText(COLORREF newClr) noexcept + { + return setNewColor(m_colors.text, newClr); + } + + COLORREF setColorDarkerText(COLORREF newClr) noexcept + { + return setNewColor(m_colors.darkerText, newClr); + } + + COLORREF setColorDisabledText(COLORREF newClr) noexcept + { + return setNewColor(m_colors.disabledText, newClr); + } + + COLORREF setColorLinkText(COLORREF newClr) noexcept + { + return setNewColor(m_colors.linkText, newClr); + } + + COLORREF setColorEdge(COLORREF newClr) noexcept + { + return setNewColor(m_colors.edge, newClr); + } + + COLORREF setColorHotEdge(COLORREF newClr) noexcept + { + return setNewColor(m_colors.hotEdge, newClr); + } + + COLORREF setColorDisabledEdge(COLORREF newClr) noexcept + { + return setNewColor(m_colors.disabledEdge, newClr); + } + + COLORREF setColorHighlight(COLORREF newClr) noexcept + { + return setNewColor(m_colors.highlight, newClr); + } + + [[nodiscard]] const dmlib::Colors& getColors() const noexcept + { + return m_colors; + } + +#ifndef _DARKMODELIB_NO_INI_CONFIG + [[nodiscard]] dmlib::Colors& getToSetColors() noexcept + { + return m_colors; + } +#endif + + [[nodiscard]] const Brushes& getBrushes() const noexcept + { + return m_brushes; + } + + [[nodiscard]] const Pens& getPens() const noexcept + { + return m_pens; + } + + [[nodiscard]] const dmlib::ColorTone& getColorTone() const noexcept + { + return m_tone; + } + + private: + dmlib::Colors m_colors; + Brushes m_brushes; + Pens m_pens; + dmlib::ColorTone m_tone = dmlib::ColorTone::black; + }; + + struct BrushesAndPensView + { + HBRUSH m_background = nullptr; + HBRUSH m_gridlines = nullptr; + HBRUSH m_headerBackground = nullptr; + HBRUSH m_headerHotBackground = nullptr; + + HPEN m_headerEdge = nullptr; + + BrushesAndPensView() = delete; + + explicit BrushesAndPensView(const dmlib::ColorsView& colors) noexcept + : m_background(::CreateSolidBrush(colors.background)) + , m_gridlines(::CreateSolidBrush(colors.gridlines)) + , m_headerBackground(::CreateSolidBrush(colors.headerBackground)) + , m_headerHotBackground(::CreateSolidBrush(colors.headerHotBackground)) + + , m_headerEdge(::CreatePen(PS_SOLID, 1, colors.headerEdge)) + {} + + BrushesAndPensView(const BrushesAndPensView&) = delete; + BrushesAndPensView& operator=(const BrushesAndPensView&) = delete; + + BrushesAndPensView(BrushesAndPensView&&) = delete; + BrushesAndPensView& operator=(BrushesAndPensView&&) = delete; + + ~BrushesAndPensView() + { + ::DeleteObject(m_background); m_background = nullptr; + ::DeleteObject(m_gridlines); m_gridlines = nullptr; + ::DeleteObject(m_headerBackground); m_headerBackground = nullptr; + ::DeleteObject(m_headerHotBackground); m_headerHotBackground = nullptr; + + ::DeleteObject(m_headerEdge); m_headerEdge = nullptr; + } + + void update(const dmlib::ColorsView& colors) noexcept + { + ::DeleteObject(m_background); + ::DeleteObject(m_gridlines); + ::DeleteObject(m_headerBackground); + ::DeleteObject(m_headerHotBackground); + + m_background = ::CreateSolidBrush(colors.background); + m_gridlines = ::CreateSolidBrush(colors.gridlines); + m_headerBackground = ::CreateSolidBrush(colors.headerBackground); + m_headerHotBackground = ::CreateSolidBrush(colors.headerHotBackground); + + ::DeleteObject(m_headerEdge); + + m_headerEdge = ::CreatePen(PS_SOLID, 1, colors.headerEdge); + } + }; + + class ThemeView + { + public: + ThemeView() noexcept + : m_clrView(kDarkColorsView) + , m_hbrPnView(kDarkColorsView) + {} + + explicit ThemeView(const dmlib::ColorsView& colorsView) noexcept + : m_clrView(colorsView) + , m_hbrPnView(colorsView) + {} + + void updateView() noexcept + { + m_hbrPnView.update(m_clrView); + } + + void updateView(const dmlib::ColorsView& colors, bool update = true) noexcept + { + m_clrView = dmlib::ColorsView{ colors }; + if (update) + { + ThemeView::updateView(); + } + } + + [[nodiscard]] const dmlib::ColorsView& getColors() const noexcept + { + return m_clrView; + } + +#ifndef _DARKMODELIB_NO_INI_CONFIG + [[nodiscard]] dmlib::ColorsView& getToSetColors() noexcept + { + return m_clrView; + } +#endif + + [[nodiscard]] const BrushesAndPensView& getViewBrushesAndPens() const noexcept + { + return m_hbrPnView; + } + + void resetColors(bool isDark) noexcept + { + m_clrView = isDark ? dmlib_color::kDarkColorsView : dmlib_color::kLightColorsView; + } + + COLORREF setColorBackground(COLORREF newClr) noexcept + { + return setNewColor(m_clrView.background, newClr); + } + + COLORREF setColorText(COLORREF newClr) noexcept + { + return setNewColor(m_clrView.text, newClr); + } + + COLORREF setColorGridlines(COLORREF newClr) noexcept + { + return setNewColor(m_clrView.gridlines, newClr); + } + + COLORREF setColorHeaderBackground(COLORREF newClr) noexcept + { + return setNewColor(m_clrView.headerBackground, newClr); + } + + COLORREF setColorHeaderHotBackground(COLORREF newClr) noexcept + { + return setNewColor(m_clrView.headerHotBackground, newClr); + } + + COLORREF setColorHeaderText(COLORREF newClr) noexcept + { + return setNewColor(m_clrView.headerText, newClr); + } + + COLORREF setColorHeaderEdge(COLORREF newClr) noexcept + { + return setNewColor(m_clrView.headerEdge, newClr); + } + + private: + dmlib::ColorsView m_clrView; + BrushesAndPensView m_hbrPnView; + }; + + /// Calculates perceptual lightness of a COLORREF color. + [[nodiscard]] double calculatePerceivedLightness(COLORREF clr) noexcept; + [[nodiscard]] COLORREF getAccentColor(bool adjust) noexcept; +} // namespace dmlib_color diff --git a/darkmodelib/src/DmlibDpi.cpp b/darkmodelib/src/DmlibDpi.cpp new file mode 100644 index 0000000000..dc5f5aab24 --- /dev/null +++ b/darkmodelib/src/DmlibDpi.cpp @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + +// Based on parts of the Notepad++ dpi code licensed under GPLv3. +// Originally by ozone10. + + +#include "StdAfx.h" + +#include "DmlibDpi.h" + +#include + +#include + +#include "ModuleHelper.h" + +using fnGetDpiForSystem = auto (WINAPI*)(VOID) -> UINT; +using fnGetDpiForWindow = auto (WINAPI*)(HWND hwnd) -> UINT; +using fnGetSystemMetricsForDpi = auto (WINAPI*)(int nIndex, UINT dpi) -> int; +using fnSystemParametersInfoForDpi = auto (WINAPI*)(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni, UINT dpi) -> BOOL; +using fnIsValidDpiAwarenessContext = auto (WINAPI*)(DPI_AWARENESS_CONTEXT value) -> BOOL; +using fnSetThreadDpiAwarenessContext = auto (WINAPI*)(DPI_AWARENESS_CONTEXT dpiContext) -> DPI_AWARENESS_CONTEXT; +using fnAdjustWindowRectExForDpi = auto (WINAPI*)(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi) -> BOOL; + +using fnOpenThemeDataForDpi = auto (WINAPI*)(HWND hwnd, LPCWSTR pszClassList, UINT dpi) -> HTHEME; + +extern "C" +{ + static UINT WINAPI DummyGetDpiForSystem() noexcept + { + UINT dpi = USER_DEFAULT_SCREEN_DPI; + if (HDC hdc = ::GetDC(nullptr); hdc != nullptr) + { + dpi = static_cast(::GetDeviceCaps(hdc, LOGPIXELSX)); + ::ReleaseDC(nullptr, hdc); + } + return dpi; + } + + static UINT WINAPI DummyGetDpiForWindow([[maybe_unused]] HWND hwnd) noexcept + { + return DummyGetDpiForSystem(); + } + + static int WINAPI DummyGetSystemMetricsForDpi(int nIndex, UINT dpi) noexcept + { + return dmlib_dpi::scale(::GetSystemMetrics(nIndex), dpi); + } + + static BOOL WINAPI DummySystemParametersInfoForDpi(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni, [[maybe_unused]] UINT dpi) noexcept + { + return ::SystemParametersInfoW(uiAction, uiParam, pvParam, fWinIni); + } + + [[nodiscard]] static BOOL WINAPI DummyIsValidDpiAwarenessContext([[maybe_unused]] DPI_AWARENESS_CONTEXT value) noexcept + { + return FALSE; + } + + static DPI_AWARENESS_CONTEXT WINAPI DummySetThreadDpiAwarenessContext([[maybe_unused]] DPI_AWARENESS_CONTEXT dpiContext) noexcept + { + return nullptr; + } + + static HTHEME WINAPI DummyOpenThemeDataForDpi(HWND hwnd, LPCWSTR pszClassList, [[maybe_unused]] UINT dpi) noexcept + { + return ::OpenThemeData(hwnd, pszClassList); + } + + static BOOL WINAPI DummyAdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, [[maybe_unused]] UINT dpi) + { + return ::AdjustWindowRectEx(lpRect, dwStyle, bMenu, dwExStyle); + } +} + +static fnGetDpiForSystem pfGetDpiForSystem = DummyGetDpiForSystem; +static fnGetDpiForWindow pfGetDpiForWindow = DummyGetDpiForWindow; +static fnGetSystemMetricsForDpi pfGetSystemMetricsForDpi = DummyGetSystemMetricsForDpi; +static fnSystemParametersInfoForDpi pfSystemParametersInfoForDpi = DummySystemParametersInfoForDpi; +static fnIsValidDpiAwarenessContext pfIsValidDpiAwarenessContext = DummyIsValidDpiAwarenessContext; +static fnSetThreadDpiAwarenessContext pfSetThreadDpiAwarenessContext = DummySetThreadDpiAwarenessContext; +static fnAdjustWindowRectExForDpi pfAdjustWindowRectExForDpi = DummyAdjustWindowRectExForDpi; +static fnOpenThemeDataForDpi pfOpenThemeDataForDpi = DummyOpenThemeDataForDpi; + +bool dmlib_dpi::InitDpiAPI() noexcept +{ + if (HMODULE hUser32 = ::GetModuleHandleW(L"user32.dll"); hUser32 != nullptr) + { + if (const auto moduleUxtheme = dmlib_module::ModuleHandle{ L"uxtheme.dll" }; + moduleUxtheme.isLoaded()) + { + bool allLoaded = true; + + allLoaded &= dmlib_module::LoadFn(hUser32, pfGetDpiForSystem, "GetDpiForSystem", DummyGetDpiForSystem); + allLoaded &= dmlib_module::LoadFn(hUser32, pfGetDpiForWindow, "GetDpiForWindow", DummyGetDpiForWindow); + allLoaded &= dmlib_module::LoadFn(hUser32, pfGetSystemMetricsForDpi, "GetSystemMetricsForDpi", DummyGetSystemMetricsForDpi); + allLoaded &= dmlib_module::LoadFn(hUser32, pfSystemParametersInfoForDpi, "SystemParametersInfoForDpi", DummySystemParametersInfoForDpi); + allLoaded &= dmlib_module::LoadFn(hUser32, pfIsValidDpiAwarenessContext, "IsValidDpiAwarenessContext", DummyIsValidDpiAwarenessContext); + allLoaded &= dmlib_module::LoadFn(hUser32, pfSetThreadDpiAwarenessContext, "SetThreadDpiAwarenessContext", DummySetThreadDpiAwarenessContext); + allLoaded &= dmlib_module::LoadFn(hUser32, pfAdjustWindowRectExForDpi, "AdjustWindowRectExForDpi", DummyAdjustWindowRectExForDpi); + allLoaded &= dmlib_module::LoadFn(moduleUxtheme.get(), pfOpenThemeDataForDpi, "OpenThemeDataForDpi", DummyOpenThemeDataForDpi); + + return allLoaded; + } + } + return false; +} + +UINT dmlib_dpi::GetDpiForSystem() noexcept +{ + return pfGetDpiForSystem(); +} + +UINT dmlib_dpi::GetDpiForWindow(HWND hWnd) noexcept +{ + if (hWnd != nullptr) + { + const auto dpi = pfGetDpiForWindow(hWnd); + if (dpi > 0) + { + return dpi; + } + } + return dmlib_dpi::GetDpiForSystem(); +} + +int dmlib_dpi::GetSystemMetricsForDpi(int nIndex, UINT dpi) noexcept +{ + return pfGetSystemMetricsForDpi(nIndex, dpi); +} + +LOGFONT dmlib_dpi::getSysFontForDpi(UINT dpi, FontType type) noexcept +{ + LOGFONT lf{}; + NONCLIENTMETRICS ncm{}; + ncm.cbSize = sizeof(NONCLIENTMETRICS); + + if (pfSystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &ncm, 0, dpi) == TRUE) + { + switch (type) + { + case FontType::menu: + { + lf = ncm.lfMenuFont; + break; + } + + case FontType::status: + { + lf = ncm.lfStatusFont; + break; + } + + case FontType::message: + { + lf = ncm.lfMessageFont; + break; + } + + case FontType::caption: + { + lf = ncm.lfCaptionFont; + break; + } + + case FontType::smcaption: + { + lf = ncm.lfSmCaptionFont; + break; + } + } + } + else // should not happen, fallback + { + auto hf = static_cast(::GetStockObject(DEFAULT_GUI_FONT)); + ::GetObjectW(hf, sizeof(LOGFONT), &lf); + lf.lfHeight = scaleFontForDpi(lf.lfHeight, dpi); + } + + return lf; +} + +BOOL dmlib_dpi::IsValidDpiAwarenessContext(DPI_AWARENESS_CONTEXT value) noexcept +{ + return pfIsValidDpiAwarenessContext(value); +} + +DPI_AWARENESS_CONTEXT dmlib_dpi::SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT dpiContext) noexcept +{ + return pfSetThreadDpiAwarenessContext(dpiContext); +} + +BOOL dmlib_dpi::AdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi) noexcept +{ + return pfAdjustWindowRectExForDpi(lpRect, dwStyle, bMenu, dwExStyle, dpi); +} + +void dmlib_dpi::loadIcon(HINSTANCE hinst, const wchar_t* pszName, int cx, int cy, HICON& hicon) noexcept +{ + if (::LoadIconWithScaleDown(hinst, pszName, cx, cy, &hicon) != S_OK) + { + hicon = static_cast(::LoadImageW(hinst, pszName, IMAGE_ICON, cx, cy, LR_DEFAULTCOLOR)); + } +} + +HTHEME dmlib_dpi::OpenThemeDataForDpi(HWND hwnd, LPCWSTR pszClassList, UINT dpi) noexcept +{ + return pfOpenThemeDataForDpi(hwnd, pszClassList, dpi); +} + +/** + * @brief Get text scale factor from the Windows registry. + * + * Queries `HKEY_CURRENT_USER\\Software\\Microsoft\\Accessibility\\TextScaleFactor`. + * + * @return DWORD value 100 if there is no key or TextScaleFactor value. + */ +DWORD dmlib_dpi::getTextScaleFactor() noexcept +{ + static constexpr DWORD defaultVal = 100; + DWORD data = defaultVal; + DWORD dwBufSize = sizeof(data); + static constexpr LPCWSTR lpSubKey = L"Software\\Microsoft\\Accessibility"; + static constexpr LPCWSTR lpValue = L"TextScaleFactor"; + + if (::RegGetValueW(HKEY_CURRENT_USER, lpSubKey, lpValue, RRF_RT_REG_DWORD, nullptr, &data, &dwBufSize) == ERROR_SUCCESS) + { + return data; + } + return defaultVal; +} diff --git a/darkmodelib/src/DmlibDpi.h b/darkmodelib/src/DmlibDpi.h new file mode 100644 index 0000000000..c566b2ff0a --- /dev/null +++ b/darkmodelib/src/DmlibDpi.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + +// Based on parts of the Notepad++ dpi code licensed under GPLv3. +// Originally by ozone10. + + +#pragma once + +#include + +#include + +#if defined(__GNUC__) || (WINVER <= _WIN32_WINNT_WIN7) + #ifndef WM_DPICHANGED + #define WM_DPICHANGED 0x02E0 + #endif + + #ifndef WM_DPICHANGED_BEFOREPARENT + #define WM_DPICHANGED_BEFOREPARENT 0x02E2 + #endif + + #ifndef WM_DPICHANGED_AFTERPARENT + #define WM_DPICHANGED_AFTERPARENT 0x02E3 + #endif + + #ifndef WM_GETDPISCALEDSIZE + #define WM_GETDPISCALEDSIZE 0x02E4 + #endif +#endif + +namespace dmlib_dpi +{ + enum class FontType + { + menu, + status, + message, + caption, + smcaption + }; + + inline constexpr UINT kDefaultFontDpi = 72; + inline constexpr UINT kDefaultFontScaleFactor = 100; + + bool InitDpiAPI() noexcept; + + [[nodiscard]] UINT GetDpiForSystem() noexcept; + [[nodiscard]] UINT GetDpiForWindow(HWND hWnd) noexcept; + [[nodiscard]] inline UINT GetDpiForParent(HWND hWnd) noexcept + { + return dmlib_dpi::GetDpiForWindow(::GetParent(hWnd)); + } + + [[nodiscard]] int GetSystemMetricsForDpi(int nIndex, UINT dpi) noexcept; + + [[nodiscard]] inline int scale(int x, UINT toDpi, UINT fromDpi) noexcept + { + return ::MulDiv(x, static_cast(toDpi), static_cast(fromDpi)); + } + + [[nodiscard]] inline int scale(int x, UINT dpi) noexcept + { + return dmlib_dpi::scale(x, dpi, USER_DEFAULT_SCREEN_DPI); + } + + [[nodiscard]] inline int unscale(int x, UINT dpi) noexcept + { + return dmlib_dpi::scale(x, USER_DEFAULT_SCREEN_DPI, dpi); + } + + [[nodiscard]] inline int scale(int x, HWND hWnd) noexcept + { + return dmlib_dpi::scale(x, dmlib_dpi::GetDpiForWindow(hWnd), USER_DEFAULT_SCREEN_DPI); + } + + [[nodiscard]] inline int unscale(int x, HWND hWnd) noexcept + { + return dmlib_dpi::scale(x, USER_DEFAULT_SCREEN_DPI, dmlib_dpi::GetDpiForWindow(hWnd)); + } + + [[nodiscard]] inline int scaleFontForDpi(int pt, UINT dpi) noexcept + { + return dmlib_dpi::scale(pt, dpi, kDefaultFontDpi); + } + + [[nodiscard]] inline int scaleFontForDpi(int pt, HWND hWnd) noexcept + { + return dmlib_dpi::scale(pt, dmlib_dpi::GetDpiForWindow(hWnd), kDefaultFontDpi); + } + + [[nodiscard]] LOGFONT getSysFontForDpi(UINT dpi, FontType type) noexcept; + [[nodiscard]] inline LOGFONT getSysFontForDpi(HWND hWnd, FontType type) noexcept + { + return dmlib_dpi::getSysFontForDpi(dmlib_dpi::GetDpiForWindow(hWnd), type); + } + + [[nodiscard]] BOOL IsValidDpiAwarenessContext(DPI_AWARENESS_CONTEXT value) noexcept; + + DPI_AWARENESS_CONTEXT SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT dpiContext) noexcept; + + BOOL AdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi) noexcept; + + void loadIcon(HINSTANCE hinst, const wchar_t* pszName, int cx, int cy, HICON& hicon) noexcept; + + [[nodiscard]] HTHEME OpenThemeDataForDpi(HWND hwnd, LPCWSTR pszClassList, UINT dpi) noexcept; + + [[nodiscard]] inline HTHEME OpenThemeDataForDpi(HWND hwnd, LPCWSTR pszClassList, HWND hWndDpi) noexcept + { + return dmlib_dpi::OpenThemeDataForDpi(hwnd, pszClassList, dmlib_dpi::GetDpiForWindow(hWndDpi)); + } + + /// Get text scale factor from the Windows registry. + [[nodiscard]] DWORD getTextScaleFactor() noexcept; + + [[nodiscard]] inline int scaleFontForFactor(int pt, UINT textScaleFactor) noexcept + { + return dmlib_dpi::scale(pt, textScaleFactor, kDefaultFontScaleFactor); + } + + [[nodiscard]] inline int scaleFontForFactor(int pt) noexcept + { + return dmlib_dpi::scale(pt, dmlib_dpi::getTextScaleFactor(), kDefaultFontScaleFactor); + } + +} // namespace dmlib_dpi diff --git a/darkmodelib/src/DmlibGlyph.h b/darkmodelib/src/DmlibGlyph.h new file mode 100644 index 0000000000..9b219cb0ad --- /dev/null +++ b/darkmodelib/src/DmlibGlyph.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +namespace dmlib_glyph +{ + inline constexpr const wchar_t* kArrowLeft = L"<"; + inline constexpr const wchar_t* kArrowRight = L">"; + inline constexpr const wchar_t* kArrowUp = L"˄"; + inline constexpr const wchar_t* kArrowDown = L"˅"; + + inline constexpr const wchar_t* kTriangleDown = L"⏷"; + + inline constexpr const wchar_t* kChevron = L"»"; +} // namespace dmlib_glyph diff --git a/darkmodelib/src/DmlibHook.cpp b/darkmodelib/src/DmlibHook.cpp new file mode 100644 index 0000000000..18bf9b7c68 --- /dev/null +++ b/darkmodelib/src/DmlibHook.cpp @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#include "StdAfx.h" + +#include "DmlibHook.h" + +#include + +#include +#include +#include + +#include +#include + +#if defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 0) +#include +#include +#include +#endif + +#include "ModuleHelper.h" + +#include "IatHook.h" + +namespace dmlib_win32api +{ + [[nodiscard]] bool IsWindows11() noexcept; + [[nodiscard]] bool IsDarkModeActive() noexcept; +} // namespace dmlib_win32api + +using fnFindThunkInModule = auto (*)(void* moduleBase, const char* dllName, const char* funcName) -> PIMAGE_THUNK_DATA; + +using fnGetSysColor = auto (WINAPI*)(int nIndex) -> DWORD; +using fnGetThemeColor = auto (WINAPI*)(HTHEME hTheme, int iPartId, int iStateId, int iPropId, COLORREF* pColor) -> HRESULT; +using fnDrawThemeBackgroundEx = auto (WINAPI*)(HTHEME hTheme, HDC hdc, int iPartId, int iStateId, LPCRECT pRect, const DTBGOPTS* pOptions) -> HRESULT; + +template +static auto ReplaceFunction(IMAGE_THUNK_DATA* addr, const P& newFunction) noexcept -> P +{ + DWORD oldProtect = 0; + if (::VirtualProtect(addr, sizeof(IMAGE_THUNK_DATA), PAGE_READWRITE, &oldProtect) == FALSE) + { + return nullptr; + } + + const ULONGLONG oldFunction = addr->u1.Function; + addr->u1.Function = reinterpret_cast(newFunction); + ::VirtualProtect(addr, sizeof(IMAGE_THUNK_DATA), oldProtect, &oldProtect); + return reinterpret_cast

(oldFunction); +} + +template +struct HookData +{ + T m_trueFn = nullptr; + size_t m_ref = 0; + const char* m_dllName = nullptr; + + const char* m_fnName = nullptr; + fnFindThunkInModule m_findFn = nullptr; + + std::uint16_t m_ord = 0; + + void init(const char* dllName, const char* funcName, const fnFindThunkInModule& findFn) noexcept + { + if (m_dllName == nullptr) + { + m_dllName = dllName; + m_fnName = funcName; + m_findFn = findFn; + + m_ord = 0; + } + } + + void init(const char* dllName, std::uint16_t ord) noexcept + { + if (m_dllName == nullptr) + { + m_dllName = dllName; + m_ord = ord; + + m_fnName = nullptr; + m_findFn = nullptr; + } + } + + [[nodiscard]] IMAGE_THUNK_DATA* findAddr(HMODULE hMod) const noexcept + { + if (m_fnName != nullptr && m_findFn != nullptr) + { + return m_findFn(hMod, m_dllName, m_fnName); + } + + if (m_ord != 0) + { + return iat_hook::FindDelayLoadThunkInModule(hMod, m_dllName, m_ord); + } + + return nullptr; + } +}; + +template +static auto HookFunction(HookData& hookData, T newFn, const char* dllName, InitArgs&&... args) noexcept -> bool +{ + const dmlib_module::ModuleHandle moduleComctl(L"comctl32.dll"); + if (!moduleComctl.isLoaded()) + { + return false; + } + + if (hookData.m_trueFn == nullptr && hookData.m_ref == 0) + { + hookData.init(dllName, std::forward(args)...); + + auto* addr = hookData.findAddr(moduleComctl.get()); + if (addr != nullptr) + { + hookData.m_trueFn = ReplaceFunction(addr, newFn); + } + } + + if (hookData.m_trueFn != nullptr) + { + ++hookData.m_ref; + return true; + } + return false; +} + +template +static void UnhookFunction(HookData& hookData) noexcept +{ + const dmlib_module::ModuleHandle moduleComctl(L"comctl32.dll"); + if (!moduleComctl.isLoaded()) + { + return; + } + + if (hookData.m_ref > 0) + { + --hookData.m_ref; + + if (hookData.m_trueFn != nullptr && hookData.m_ref == 0) + { + auto* addr = hookData.findAddr(moduleComctl.get()); + if (addr != nullptr) + { + ReplaceFunction(addr, hookData.m_trueFn); + hookData.m_trueFn = nullptr; + } + } + } +} + +#if defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 0) +using fnOpenNcThemeData = auto (WINAPI*)(HWND hWnd, LPCWSTR pszClassList) -> HTHEME; // ordinal 49 +static fnOpenNcThemeData pfOpenNcThemeData = nullptr; + +bool dmlib_hook::loadOpenNcThemeData(const HMODULE& hUxtheme) noexcept +{ + return dmlib_module::LoadFn(hUxtheme, pfOpenNcThemeData, 49); +} + +#if defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 1) +// limit dark scroll bar to specific windows and their children +static std::unordered_set g_darkScrollBarWindows; +static std::mutex g_darkScrollBarMutex; + +/** + * @brief Makes scroll bars on the specified window and all its children consistent. + * + * @note Currently not widely used by default. + * If possible, try to use `dmlib::setDarkExplorerTheme` + * or `dmlib::setDarkThemeExperimental` instead. + * + * @param[in] hWnd Handle to the parent window. + * + * @see dmlib::setDarkExplorerTheme() + * @see dmlib::setDarkThemeExperimental() + */ +void dmlib_hook::enableDarkScrollBarForWindowAndChildren(HWND hWnd) +{ + const std::lock_guard lock(g_darkScrollBarMutex); + g_darkScrollBarWindows.insert(hWnd); +} + +static bool isWindowOrParentUsingDarkScrollBar(HWND hWnd) +{ + HWND hRoot = ::GetAncestor(hWnd, GA_ROOT); + + const std::lock_guard lock(g_darkScrollBarMutex); + auto hasElement = [](const auto& container, HWND hWndToCheck) -> bool + { +#if (defined(_MSC_VER) && (_MSVC_LANG >= 202002L)) || (__cplusplus >= 202002L) + return container.contains(hWndToCheck); +#else + return container.count(hWndToCheck) != 0; +#endif + }; + + if (hasElement(g_darkScrollBarWindows, hWnd)) + { + return true; + } + return (hWnd != hRoot && hasElement(g_darkScrollBarWindows, hRoot)); +} +#endif // defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 1) + +static HTHEME WINAPI MyOpenNcThemeData(HWND hWnd, LPCWSTR pszClassList) +{ + static constexpr std::wstring_view scrollBarClassName = WC_SCROLLBAR; + if (scrollBarClassName == pszClassList) + { +#if defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 1) + if (isWindowOrParentUsingDarkScrollBar(hWnd)) +#endif + { + hWnd = nullptr; + pszClassList = L"Explorer::ScrollBar"; + } + } + return pfOpenNcThemeData(hWnd, pszClassList); +} + +void dmlib_hook::fixDarkScrollBar() +{ + const dmlib_module::ModuleHandle moduleComctl(L"comctl32.dll"); + if (moduleComctl.isLoaded()) + { + auto* addr = iat_hook::FindDelayLoadThunkInModule(moduleComctl.get(), "uxtheme.dll", 49); // OpenNcThemeData + if (addr != nullptr) // && pfOpenNcThemeData != nullptr) // checked in InitDarkMode + { + ReplaceFunction(addr, MyOpenNcThemeData); + } + } +} +#endif // defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 0) + +// Hooking GetSysColor for combo box ex' list box and list view's gridlines + + +static HookData g_hookDataGetSysColor{}; + +static COLORREF g_clrWindow = RGB(32, 32, 32); +static COLORREF g_clrText = RGB(224, 224, 224); +static COLORREF g_clrTGridlines = RGB(100, 100, 100); + + +/** + * @brief Overrides a specific system color with a custom color. + * + * Currently supports: + * - `COLOR_WINDOW`: Background of ComboBoxEx list. + * - `COLOR_WINDOWTEXT`: Text color of ComboBoxEx list. + * - `COLOR_BTNFACE`: Gridline color in ListView (when applicable). + * + * @param[in] nIndex One of the supported system color indices. + * @param[in] clr Custom `COLORREF` value to apply. + */ +void dmlib_hook::setMySysColor(int nIndex, COLORREF clr) noexcept +{ + switch (nIndex) + { + case COLOR_WINDOW: + { + g_clrWindow = clr; + break; + } + + case COLOR_WINDOWTEXT: + { + g_clrText = clr; + break; + } + + case COLOR_BTNFACE: + { + g_clrTGridlines = clr; + break; + } + + default: + { + break; + } + } +} + +static DWORD WINAPI MyGetSysColor(int nIndex) noexcept +{ + if (!dmlib_win32api::IsDarkModeActive()) + { + return g_hookDataGetSysColor.m_trueFn(nIndex); + } + + switch (nIndex) + { + case COLOR_WINDOW: + { + return g_clrWindow; + } + + case COLOR_WINDOWTEXT: + { + return g_clrText; + } + + case COLOR_BTNFACE: + { + return g_clrTGridlines; + } + + default: + { + return g_hookDataGetSysColor.m_trueFn(nIndex); + } + } +} + +/** + * @brief Hooks system color to support runtime customization. + * + * @return `true` if the hook was installed successfully. + */ +bool dmlib_hook::hookSysColor() noexcept +{ + return HookFunction( + g_hookDataGetSysColor, + MyGetSysColor, + "user32.dll", + static_cast("GetSysColor"), + iat_hook::FindIatThunkInModule); +} + +/** + * @brief Unhooks system color overrides and restores default color behavior. + * + * This function is safe to call even if no color hook is currently installed. + * It ensures that system colors return to normal without requiring + * prior state checks. + */ +void dmlib_hook::unhookSysColor() noexcept +{ + UnhookFunction(g_hookDataGetSysColor); +} + +// Hooking GetThemeColor for Task Dialog text color + +static HookData g_hookDataGetThemeColor{}; +static HookData g_hookDataDrawThemeBackgroundEx{}; + +static constexpr COLORREF kMainInstructionTextClr = RGB(96, 205, 255); +static constexpr COLORREF kOtherTextClr = RGB(255, 255, 255); + +static HTHEME g_hDarkTheme = nullptr; + +static HRESULT WINAPI MyGetThemeColor( + HTHEME hTheme, + int iPartId, + int iStateId, + int iPropId, + COLORREF* pColor +) noexcept +{ + const HRESULT retVal = g_hookDataGetThemeColor.m_trueFn(hTheme, iPartId, iStateId, iPropId, pColor); + if (!dmlib_win32api::IsDarkModeActive() || pColor == nullptr) + { + return retVal; + } + + if (iPropId == TMT_TEXTCOLOR) + { + switch (iPartId) + { + case TDLG_MAININSTRUCTIONPANE: + { + *pColor = kMainInstructionTextClr; + break; + } + + case TDLG_CONTENTPANE: + case TDLG_EXPANDOTEXT: + case TDLG_VERIFICATIONTEXT: + case TDLG_FOOTNOTEPANE: + case TDLG_EXPANDEDFOOTERAREA: + { + if (g_hDarkTheme != nullptr) + { + g_hookDataGetThemeColor.m_trueFn(g_hDarkTheme, iPartId, iStateId, iPropId, pColor); + } + else + { + *pColor = kOtherTextClr; + } + break; + } + + default: + { + break; + } + } + } + return retVal; +} + +static constexpr std::uint16_t kDrawThemeBackgroundExOrdinal = 47; + +static constexpr COLORREF kMainPaneBgClr = RGB(44, 44, 44); +static constexpr COLORREF kFooterBgClr = RGB(32, 32, 32); + +static HBRUSH g_hBrushBg = nullptr; +static HBRUSH g_hBrushBgFooter = nullptr; + +static HRESULT WINAPI MyDrawThemeBackgroundEx( + HTHEME hTheme, + HDC hdc, + int iPartId, + int iStateId, + LPCRECT pRect, + const DTBGOPTS* pOptions +) noexcept +{ + if (!dmlib_win32api::IsDarkModeActive() || pOptions == nullptr) + { + return g_hookDataDrawThemeBackgroundEx.m_trueFn(hTheme, hdc, iPartId, iStateId, pRect, pOptions); + } + + switch (iPartId) + { + case TDLG_PRIMARYPANEL: + { + ::FillRect(hdc, pRect, g_hBrushBg); + break; + } + + case TDLG_SECONDARYPANEL: + case TDLG_FOOTNOTEPANE: + { + ::FillRect(hdc, &pOptions->rcClip, g_hBrushBgFooter); + break; + } + + default: + { + return g_hookDataDrawThemeBackgroundEx.m_trueFn(hTheme, hdc, iPartId, iStateId, pRect, pOptions); + } + } + return S_OK; +} + +/** + * @brief Hooks `GetThemeColor` and `DrawThemeBackgroundEx` to support dark colors. + * + * @return `true` if the hook was installed successfully. + */ +bool dmlib_hook::hookThemeColor() noexcept +{ + COLORREF clrMain = kMainPaneBgClr; + COLORREF clrFooter = kFooterBgClr; + + if (dmlib_win32api::IsWindows11() && g_hDarkTheme == nullptr) + { + g_hDarkTheme = ::OpenThemeData(nullptr, L"DarkMode_Explorer::TaskDialog"); + if (g_hDarkTheme != nullptr) + { + if (FAILED(::GetThemeColor(g_hDarkTheme, TDLG_PRIMARYPANEL, 0, TMT_FILLCOLOR, &clrMain))) + { + clrMain = kMainPaneBgClr; + } + + if (FAILED(::GetThemeColor(g_hDarkTheme, TDLG_SECONDARYPANEL, 0, TMT_FILLCOLOR, &clrFooter))) + { + clrFooter = kFooterBgClr; + } + } + } + + if (g_hBrushBg == nullptr) + { + g_hBrushBg = ::CreateSolidBrush(clrMain); + } + + if (g_hBrushBgFooter == nullptr) + { + g_hBrushBgFooter = ::CreateSolidBrush(clrFooter); + } + + return + HookFunction(g_hookDataGetThemeColor, + MyGetThemeColor, + "uxtheme.dll", + static_cast("GetThemeColor"), + static_cast(iat_hook::FindDelayLoadThunkInModule)) + && HookFunction(g_hookDataDrawThemeBackgroundEx, + MyDrawThemeBackgroundEx, + "uxtheme.dll", + kDrawThemeBackgroundExOrdinal); +} + + +/** + * @brief Unhooks `GetThemeColor` and `DrawThemeBackgroundEx` overrides and restores default color behavior. + * + * This function is safe to call even if no color hook is currently installed. + * It ensures that theme colors return to normal without requiring + * prior state checks. + */ +void dmlib_hook::unhookThemeColor() noexcept +{ + UnhookFunction(g_hookDataGetThemeColor); + UnhookFunction(g_hookDataDrawThemeBackgroundEx); + if (g_hDarkTheme != nullptr && g_hookDataGetThemeColor.m_ref == 0) + { + ::CloseThemeData(g_hDarkTheme); + g_hDarkTheme = nullptr; + } + + if (g_hBrushBg != nullptr) + { + ::DeleteObject(g_hBrushBg); + g_hBrushBg = nullptr; + } + + if (g_hBrushBgFooter != nullptr) + { + ::DeleteObject(g_hBrushBgFooter); + g_hBrushBgFooter = nullptr; + } +} diff --git a/darkmodelib/src/DmlibHook.h b/darkmodelib/src/DmlibHook.h new file mode 100644 index 0000000000..427ecfa35d --- /dev/null +++ b/darkmodelib/src/DmlibHook.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +#include + +namespace dmlib_hook +{ +#if defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 0) + bool loadOpenNcThemeData(const HMODULE& hUxtheme) noexcept; + /// Makes scroll bars on the specified window and all its children consistent. + void enableDarkScrollBarForWindowAndChildren(HWND hWnd); + void fixDarkScrollBar(); +#endif + + /// Overrides a specific system color with a custom color. + void setMySysColor(int nIndex, COLORREF clr) noexcept; + /// Hooks system color to support runtime customization. + bool hookSysColor() noexcept; + /// Unhooks system color overrides and restores default color behavior. + void unhookSysColor() noexcept; + + /// Hooks `GetThemeColor` and `DrawThemeBackgroundEx` to support dark colors. + bool hookThemeColor() noexcept; + /// Unhooks `GetThemeColor` and `DrawThemeBackgroundEx` overrides and restores default color behavior. + void unhookThemeColor() noexcept; +} // namespace dmlib_hook diff --git a/darkmodelib/src/DmlibPaintHelper.cpp b/darkmodelib/src/DmlibPaintHelper.cpp new file mode 100644 index 0000000000..716f331ec3 --- /dev/null +++ b/darkmodelib/src/DmlibPaintHelper.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#include "StdAfx.h" + +#include "DmlibPaintHelper.h" + +#include + +/** + * @brief Paints a rounded rectangle using the specified pen and brush. + * + * Draws a rounded rectangle defined by `rect`, using the provided pen (`hpen`) and brush (`hBrush`) + * for the edge and fill, respectively. Preserves previous GDI object selections. + * + * @param[in] hdc Handle to the device context. + * @param[in] rect Rectangle bounds for the shape. + * @param[in] hpen Pen used to draw the edge. + * @param[in] hBrush Brush used to inner fill. + * @param[in] width Horizontal corner radius. + * @param[in] height Vertical corner radius. + */ +void dmlib_paint::paintRoundRect( + HDC hdc, + const RECT& rect, + HPEN hpen, + HBRUSH hBrush, + int width, + int height +) noexcept +{ + auto holdBrush = ::SelectObject(hdc, hBrush); + auto holdPen = ::SelectObject(hdc, hpen); + ::RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, width, height); + ::SelectObject(hdc, holdBrush); + ::SelectObject(hdc, holdPen); +} diff --git a/darkmodelib/src/DmlibPaintHelper.h b/darkmodelib/src/DmlibPaintHelper.h new file mode 100644 index 0000000000..f3706f54b6 --- /dev/null +++ b/darkmodelib/src/DmlibPaintHelper.h @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +#include + +#include + +namespace dmlib_paint +{ + /// Base roundness value for various controls, such as toolbar iconic buttons and combo boxes + inline constexpr int kWin11CornerRoundness = 4; + + /** + * @class GdiObject + * @brief RAII wrapper for managing GDI objects in a device context. + * + * Automatically selects a GDI object (e.g., brush, pen, font) into a device context (DC), + * stores the previous object, and restores it upon destruction. Optionally deletes the + * selected object unless marked as shared. + * + * Logic: + * - Prevents resource leaks and ensures proper cleanup of GDI objects. + * - Supports shared objects (e.g., system fonts or brushes) via the `isShared` flag. + * - Uses `SelectObject()` to apply and restore the DC state. + * - Deletes the object via `DeleteObject()` unless shared. + * + * Constructors: + * - `GdiObject(HDC hdc, HGDIOBJ obj, bool isShared)` + * Selects the object into the DC and marks it as shared or owned. + * - `GdiObject(HDC hdc, HGDIOBJ obj)` + * Convenience constructor for non-shared objects. + * - `GdiObject(HDC hdc, HWND hWnd)` + * Convenience constructor for shared HFONT object acquired from WM_GETFONT. + * + * Destructor: + * - Automatically restores the previous object and deletes the selected one if owned. + * + * Methods: + * - `void deleteObj()` + * Manually restores and deletes the object (if not shared). + * + * @note The default constructor is deleted to enforce explicit initialization. + */ + class GdiObject + { + public: + GdiObject() = delete; + explicit GdiObject(HDC hdc, HGDIOBJ obj, bool isShared) noexcept + : m_hdc(hdc) + , m_hObj(obj) + , m_isShared(isShared) + { + if (m_hObj != nullptr) + { + m_holdObj = ::SelectObject(hdc, obj); + } + } + + explicit GdiObject(HDC hdc, HGDIOBJ obj) noexcept + : GdiObject(hdc, obj, false) + {} + + explicit GdiObject(HDC hdc, HWND hWnd) noexcept + : GdiObject(hdc, reinterpret_cast(::SendMessageW(hWnd, WM_GETFONT, 0, 0)), true) + {} + + ~GdiObject() + { + deleteObj(); + } + + void deleteObj() noexcept + { + if (m_hObj != nullptr) + { + ::SelectObject(m_hdc, m_holdObj); + if (!m_isShared) + { + ::DeleteObject(m_hObj); + m_hObj = nullptr; + } + } + } + + GdiObject(const GdiObject&) = delete; + GdiObject& operator=(const GdiObject&) = delete; + + GdiObject(GdiObject&&) = delete; + GdiObject& operator=(GdiObject&&) = delete; + + explicit operator HGDIOBJ() const noexcept + { + return m_hObj; + } + + private: + HDC m_hdc = nullptr; + HGDIOBJ m_hObj = nullptr; + HGDIOBJ m_holdObj = nullptr; + bool m_isShared = false; + }; + + /** + * @brief Performs double-buffered painting using a memory DC and a custom paint function. + * + * Allocates and manages an off-screen buffer via `BufferData`, clips to the paint region, + * executes the provided paint function, and blits the result to the target DC. + * + * @tparam T Control data type containing a `m_bufferData` member. + * @tparam PaintFunc Callable object (lambda or function) that performs painting. + * @param[in] ctrlData Reference to control-specific data (must contain `m_bufferData`). + * @param[in] hdc Target device context. + * @param[in] ps Paint structure from `BeginPaint`. + * @param[in] paintFunc Custom paint routine. + * @param[in] rcClient Client rectangle of the control. + * + * @see BufferData + */ + template + inline void PaintWithBuffer( + T& ctrlData, + HDC hdc, + const PAINTSTRUCT& ps, + PaintFunc&& paintFunc, + const RECT& rcClient + ) + { + auto& bufferData = ctrlData.m_bufferData; + + if (bufferData.ensureBuffer(hdc, rcClient)) + { + const auto& hMemDC = bufferData.getHMemDC(); + const int savedState = ::SaveDC(hMemDC); + + ::IntersectClipRect( + hMemDC, + ps.rcPaint.left, ps.rcPaint.top, + ps.rcPaint.right, ps.rcPaint.bottom + ); + + std::forward(paintFunc)(); + + ::RestoreDC(hMemDC, savedState); + + ::BitBlt( + hdc, + ps.rcPaint.left, ps.rcPaint.top, + ps.rcPaint.right - ps.rcPaint.left, + ps.rcPaint.bottom - ps.rcPaint.top, + hMemDC, + ps.rcPaint.left, ps.rcPaint.top, + SRCCOPY + ); + } + } + + /** + * @brief Overload of `paintWithBuffer` that automatically retrieves the client rect. + * + * Extracts the client rectangle from the window handle, + * then forwards it to the main `paintWithBuffer` implementation. + * + * @tparam T Control data type containing a `m_bufferData` member. + * @tparam PaintFunc Callable object (lambda or function) that performs painting. + * @param[in] ctrlData Reference to control-specific data (must contain `m_bufferData`). + * @param[in] hdc Target device context. + * @param[in] ps Paint structure from `BeginPaint`. + * @param[in] paintFunc Custom paint routine. + * @param[in] hWnd Handle to the control window. + * + * @see dmlib_paint::PaintWithBuffer(const T&, HDC, const PAINTSTRUCT&, PaintFunc&&, const RECT&) + */ + template + inline void PaintWithBuffer( + T& ctrlData, + HDC hdc, + const PAINTSTRUCT& ps, + PaintFunc&& paintFunc, + HWND hWnd + ) + { + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + dmlib_paint::PaintWithBuffer(ctrlData, hdc, ps, std::forward(paintFunc), rcClient); + } + + /// Paints a rounded rectangle using the specified pen and brush. + void paintRoundRect(HDC hdc, const RECT& rect, HPEN hpen, HBRUSH hBrush, int width, int height) noexcept; + + /** + * @brief Paints a rectangle using the specified pen and brush. + * + * Draws a rectangle defined by `rect`, using the provided pen (`hpen`) and brush (`hBrush`) + * for the edge and fill, respectively. Preserves previous GDI object selections. + * Forwards to `dmlib_paint::paintRoundRect` with `width` and `height` parameters with `0` value. + * + * @param[in] hdc Handle to the device context. + * @param[in] rect Rectangle bounds for the shape. + * @param[in] hpen Pen used to draw the edge. + * @param[in] hBrush Brush used to inner fill. + * + * @see dmlib_paint::paintRoundRect() + */ + inline void paintRect( + HDC hdc, + const RECT& rect, + HPEN hpen, + HBRUSH hBrush + ) noexcept + { + dmlib_paint::paintRoundRect(hdc, rect, hpen, hBrush, 0, 0); + } + + /** + * @brief Paints an unfilled rounded rectangle (frame only). + * + * Forwards to `dmlib_paint::paintRoundRect` and uses a `NULL_BRUSH` + * to omit the inner fill, drawing only the rounded frame. + * + * @param[in] hdc Handle to the device context. + * @param[in] rect Rectangle bounds for the frame. + * @param[in] hpen Pen used to draw the edge. + * @param[in] width Horizontal corner radius. + * @param[in] height Vertical corner radius. + * + * @see dmlib_paint::paintRoundRect() + */ + inline void paintRoundFrameRect( + HDC hdc, + const RECT& rect, + HPEN hpen, + int width, + int height + ) noexcept + { + dmlib_paint::paintRoundRect(hdc, rect, hpen, static_cast(::GetStockObject(NULL_BRUSH)), width, height); + } + + /** + * @brief Paints an unfilled rectangle (frame only). + * + * Forwards to `dmlib_paint::paintRoundRect` and uses a `NULL_BRUSH` + * to omit the inner fill with `width` and `height` parameters with `0` value + * to draw only the frame. + * + * @param[in] hdc Handle to the device context. + * @param[in] rect Rectangle bounds for the frame. + * @param[in] hpen Pen used to draw the edge. + * + * @see dmlib_paint::paintRoundRect() + */ + inline void paintFrameRect(HDC hdc, const RECT& rect, HPEN hpen) noexcept + { + dmlib_paint::paintRoundRect(hdc, rect, hpen, static_cast(::GetStockObject(NULL_BRUSH)), 0, 0); + } + + /** + * @brief Checks whether a RECT defines a non-empty, valid area. + * + * @param[in] rc The rectangle to validate. + * @return `true` If rc has positive width and height (right > left and bottom > top). + * @return `false` Otherwise. + */ + [[nodiscard]] inline bool isRectValid(const RECT& rc) noexcept + { + return (rc.right > rc.left && rc.bottom > rc.top); + } + + /** + * @brief Checks whether a animation effects such as transition effect are enabled. + * + * @return `true` If animation effects are enabled. + * @return `false` Otherwise. + */ + [[nodiscard]] inline bool isAnimationEnabled() noexcept + { + if (BOOL isEnabled = FALSE; + ::SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &isEnabled, FALSE) != FALSE) + { + return isEnabled == TRUE; + } + return false; + } +} // namespace dmlib_paint diff --git a/darkmodelib/src/DmlibSubclass.cpp b/darkmodelib/src/DmlibSubclass.cpp new file mode 100644 index 0000000000..3133004c3c --- /dev/null +++ b/darkmodelib/src/DmlibSubclass.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#include "StdAfx.h" + +#include "DmlibSubclass.h" + +#ifdef _DARKMODELIB_PREFER_THEME +namespace dmlib_win32api +{ + [[nodiscard]] bool IsWindows10() noexcept; + [[nodiscard]] bool IsDarkModeSupported() noexcept; +} // namespace dmlib_win32api +#endif + +/** + * @brief Determines if themed styling should be preferred over subclassing. + * + * Requires support for experimental theming and Windows 10 or later. + * + * @return `true` if themed appearance is preferred and supported. + */ +bool dmlib_subclass::isThemePrefered() noexcept +{ +#ifdef _DARKMODELIB_PREFER_THEME + return dmlib_win32api::IsWindows10() && dmlib_win32api::IsDarkModeSupported(); +#else + return false; +#endif +} diff --git a/darkmodelib/src/DmlibSubclass.h b/darkmodelib/src/DmlibSubclass.h new file mode 100644 index 0000000000..390ca914ec --- /dev/null +++ b/darkmodelib/src/DmlibSubclass.h @@ -0,0 +1,414 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace dmlib_subclass +{ + /** + * @brief Defines control subclass ID values. + */ + enum class SubclassID : unsigned char + { + button = 42, + groupbox, + upDown, + tabPaint, + tabUpDown, + customBorder, + comboBox, + comboBoxEx, + listView, + header, + statusBar, + progressBar, + staticText, + ipAddress, + hotKey, + dtp, + windowEraseBg, + windowCtlColor, + windowNotify, + windowMenuBar, + windowSettingChange, + taskDlg + }; + + /** + * @brief Attaches a typed subclass procedure with custom data to a window. + * + * If the subclass ID is not already attached, allocates a `T` instance using the given + * `param` and stores it as subclass reference data. Ownership is transferred to the system. + * + * @tparam T The user-defined data type associated with the subclass. + * @tparam Param Type used to initialize `T`. + * @param[in] hWnd Window handle. + * @param[in] subclassProc Subclass procedure. + * @param[in] subID Identifier for the subclass instance. + * @param[in] param Constructor argument forwarded to `T`. + * @return TRUE on success, FALSE on failure, -1 if subclass already set. + */ + template + inline auto SetSubclass(HWND hWnd, SUBCLASSPROC subclassProc, SubclassID subID, const Param& param) -> int + { + if (const auto subclassID = static_cast(subID); + ::GetWindowSubclass(hWnd, subclassProc, subclassID, nullptr) == FALSE) + { + if (auto pData = std::make_unique(param); + ::SetWindowSubclass(hWnd, subclassProc, subclassID, reinterpret_cast(pData.get())) == TRUE) + { + pData.release(); + return TRUE; + } + return FALSE; + } + return -1; + } + + /** + * @brief Attaches a typed subclass procedure with default-constructed data. + * + * Same logic as the other overload, but constructs `T` using its default constructor. + * + * @tparam T The user-defined data type associated with the subclass. + * @param[in] hWnd Window handle. + * @param[in] subclassProc Subclass procedure. + * @param[in] subID Identifier for the subclass instance. + * @return TRUE on success, FALSE on failure, -1 if already subclassed. + */ + template + inline auto SetSubclass(HWND hWnd, SUBCLASSPROC subclassProc, SubclassID subID) -> int + { + if (const auto subclassID = static_cast(subID); + ::GetWindowSubclass(hWnd, subclassProc, subclassID, nullptr) == FALSE) + { + if (auto pData = std::make_unique(); + ::SetWindowSubclass(hWnd, subclassProc, subclassID, reinterpret_cast(pData.get())) == TRUE) + { + pData.release(); + return TRUE; + } + return FALSE; + } + return -1; + } + + /** + * @brief Attaches an untyped subclass (no reference data). + * + * Sets a subclass with no associated custom data. + * + * @param[in] hWnd Window handle. + * @param[in] subclassProc Subclass procedure. + * @param[in] subID Identifier for the subclass instance. + * @return TRUE on success, FALSE on failure, -1 if already subclassed. + */ + inline int SetSubclass(HWND hWnd, SUBCLASSPROC subclassProc, SubclassID subID) noexcept + { + if (const auto subclassID = static_cast(subID); + ::GetWindowSubclass(hWnd, subclassProc, subclassID, nullptr) == FALSE) + { + return ::SetWindowSubclass(hWnd, subclassProc, subclassID, 0); + } + return -1; + } + + /** + * @brief Removes a subclass and deletes associated user data (if provided). + * + * Retrieves and deletes user-defined `T` data stored in subclass reference + * (unless `T = void`, in which case no delete is performed). Then removes the subclass. + * + * @tparam T Optional type of reference data to delete. + * @param[in] hWnd Window handle. + * @param[in] subclassProc Subclass procedure. + * @param[in] subID Identifier for the subclass instance. + * @return TRUE on success, FALSE on failure, -1 if not present. + */ + template + inline auto RemoveSubclass(HWND hWnd, SUBCLASSPROC subclassProc, SubclassID subID) noexcept -> int + { + T* pData = nullptr; + if (const auto subclassID = static_cast(subID); + ::GetWindowSubclass(hWnd, subclassProc, subclassID, reinterpret_cast(&pData)) == TRUE) + { + if constexpr (!std::is_void_v) + { + if (pData != nullptr) + { + std::unique_ptr u_ptrData(pData); + u_ptrData.reset(nullptr); + } + } + return ::RemoveWindowSubclass(hWnd, subclassProc, subclassID); + } + return -1; + } + + /** + * @class ThemeData + * @brief RAII-style wrapper for `HTHEME` handle tied to a specific theme class. + * + * Prevents leaks by managing the lifecycle of a theme handle opened via `OpenThemeData()`. + * Ensures handles are released properly in the destructor via `CloseThemeData()`. + * + * Usage: + * - Construct with a valid theme class name (e.g. `L"Button"`). + * - Call `ensureTheme(HWND)` before drawing to open the theme handle. + * - Access the active handle via `getHTheme()`. + * + * Copying and moving are explicitly disabled to preserve exclusive ownership. + */ + class ThemeData + { + public: + ThemeData() = delete; + + explicit ThemeData(std::wstring_view themeClass) noexcept + { + const size_t copyLength = std::min(themeClass.length(), m_themeClass.size() - 1); + std::copy_n(themeClass.begin(), copyLength, m_themeClass.begin()); + } + + ThemeData(const ThemeData&) = delete; + ThemeData& operator=(const ThemeData&) = delete; + + ThemeData(ThemeData&&) = delete; + ThemeData& operator=(ThemeData&&) = delete; + + ~ThemeData() + { + closeTheme(); + } + + bool ensureTheme(HWND hWnd) noexcept + { + if (m_hTheme == nullptr && !m_themeClass.empty()) + { + m_hTheme = ::OpenThemeData(hWnd, m_themeClass.data()); + } + return m_hTheme != nullptr; + } + + void closeTheme() noexcept + { + if (m_hTheme != nullptr) + { + ::CloseThemeData(m_hTheme); + m_hTheme = nullptr; + } + } + + [[nodiscard]] const HTHEME& getHTheme() const noexcept + { + return m_hTheme; + } + + private: + std::array m_themeClass{}; + HTHEME m_hTheme = nullptr; + }; + + /** + * @class BufferData + * @brief RAII-style utility for double buffer technique. + * + * Allocates and resizes an offscreen buffer for flicker-free GDI drawing. When + * `ensureBuffer()` is called with a target HDC and client rect, it creates or resizes + * a memory device context and bitmap accordingly. Automatically releases resources + * via `releaseBuffer()` and destructor. + * + * Usage: + * - Call `ensureBuffer()` before painting. + * - Draw to `getHMemDC()`. + * - BitBlt back to screen in WM_PAINT. + * + * Copying and moving are explicitly disabled to preserve exclusive ownership. + */ + class BufferData + { + public: + BufferData() = default; + + BufferData(const BufferData&) = delete; + BufferData& operator=(const BufferData&) = delete; + + BufferData(BufferData&&) = delete; + BufferData& operator=(BufferData&&) = delete; + + ~BufferData() + { + releaseBuffer(); + } + + bool ensureBuffer(HDC hdc, const RECT& rcClient) noexcept + { + const int width = rcClient.right - rcClient.left; + const int height = rcClient.bottom - rcClient.top; + if (m_szBuffer.cx != width || m_szBuffer.cy != height) + { + releaseBuffer(); + m_hMemDC = ::CreateCompatibleDC(hdc); + m_hMemBmp = ::CreateCompatibleBitmap(hdc, width, height); + m_holdBmp = static_cast(::SelectObject(m_hMemDC, m_hMemBmp)); + m_szBuffer = { width, height }; + } + + return m_hMemDC != nullptr && m_hMemBmp != nullptr; + } + + void releaseBuffer() noexcept + { + if (m_hMemDC != nullptr) + { + ::SelectObject(m_hMemDC, m_holdBmp); + ::DeleteObject(m_hMemBmp); + ::DeleteDC(m_hMemDC); + + m_hMemDC = nullptr; + m_hMemBmp = nullptr; + m_holdBmp = nullptr; + m_szBuffer = { 0, 0 }; + } + } + + [[nodiscard]] const HDC& getHMemDC() const noexcept + { + return m_hMemDC; + } + + private: + HDC m_hMemDC = nullptr; + HBITMAP m_hMemBmp = nullptr; + HBITMAP m_holdBmp = nullptr; + SIZE m_szBuffer{}; + }; + + /** + * @class FontData + * @brief RAII-style wrapper for managing a GDI font (`HFONT`) resource. + * + * Ensures safe creation, assignment, and destruction of fonts in GDI-based UI code. + * Automatically deletes the font in the destructor or when replaced via `setFont()`. + * + * Usage: + * - Use `setFont()` to assign a new font, deleting any previous one. + * - `getFont()` provides access to the current `HFONT`. + * - `hasFont()` checks if a valid font is currently held. + * + * Copying and moving are explicitly disabled to preserve exclusive ownership. + */ + class FontData + { + public: + FontData() = default; + + explicit FontData(HFONT hFont) noexcept + : m_hFont(hFont) + {} + + FontData(const FontData&) = delete; + FontData& operator=(const FontData&) = delete; + + FontData(FontData&&) = delete; + FontData& operator=(FontData&&) = delete; + + ~FontData() + { + FontData::destroyFont(); + } + + void setFont(HFONT newFont) noexcept + { + FontData::destroyFont(); + m_hFont = newFont; + } + + [[nodiscard]] const HFONT& getFont() const noexcept + { + return m_hFont; + } + + [[nodiscard]] bool hasFont() const noexcept + { + return m_hFont != nullptr; + } + + void destroyFont() noexcept + { + if (FontData::hasFont()) + { + ::DeleteObject(m_hFont); + m_hFont = nullptr; + } + } + + private: + HFONT m_hFont = nullptr; + }; + + /** + * @brief Retrieves the class name of a given window. + * + * This function wraps the Win32 API `GetClassNameW` to return the class name + * of a window as a wide string (`std::wstring`). + * + * @param[in] hWnd Handle to the target window. + * @return The class name of the window as a `std::wstring`. + * + * @note The maximum length is capped at 32 characters (including the null terminator), + * which suffices for standard Windows window classes. + */ + [[nodiscard]] inline std::wstring getWndClassName(HWND hWnd) + { + static constexpr int strLen = 32; + auto className = std::wstring(strLen, L'\0'); + className.resize(static_cast(::GetClassNameW(hWnd, className.data(), strLen))); + return className; + } + + /** + * @brief Compares the class name of a window with a specified string. + * + * This function retrieves the class name of the given window handle + * and compares it to the provided class name. + * + * @param[in] hWnd Handle to the window whose class name is to be checked. + * @param[in] classNameToCmp Pointer to a null-terminated wide string representing the class name to compare against. + * @return `true` if the window's class name matches the specified string. + * @return `false` otherwise. + * + * @see dmlib_subclass::getWndClassName() + */ + [[nodiscard]] inline bool cmpWndClassName(HWND hWnd, const wchar_t* classNameToCmp) + { + if (hWnd == nullptr) + { + return false; + } + return (dmlib_subclass::getWndClassName(hWnd) == classNameToCmp); + } + + /// Determines if themed styling should be preferred over subclassing. + [[nodiscard]] bool isThemePrefered() noexcept; +} // namespace dmlib_subclass diff --git a/darkmodelib/src/DmlibSubclassControl.cpp b/darkmodelib/src/DmlibSubclassControl.cpp new file mode 100644 index 0000000000..f474ed834f --- /dev/null +++ b/darkmodelib/src/DmlibSubclassControl.cpp @@ -0,0 +1,3790 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#include "StdAfx.h" + +#include "DmlibSubclassControl.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "Darkmodelib.h" + +#include "DmlibDpi.h" +#include "DmlibGlyph.h" +#include "DmlibHook.h" +#include "DmlibPaintHelper.h" +#include "DmlibSubclass.h" + +#ifdef __GNUC__ +static constexpr int CP_DROPDOWNITEM = 9; // for some reason mingw use only enum up to 8 +#endif + +/** + * @brief Draws a themed owner drawn checkbox, radio, or tri-state button (excluding push-like buttons). + * + * Internally used by @ref paintButton to draw visual elements such as checkbox glyphs + * or radio indicators alongside styled text. Not used for buttons with `BS_PUSHLIKE`, + * which require different handling and theming logic. + * + * - Retrieves themed or fallback font for consistent appearance. + * - Handles alignment, word wrapping, and prefix visibility per style flags. + * - Draws themed background and glyph using `DrawThemeBackground`. + * - Uses themed text drawing and applies focus cue when needed. + * + * @param[in] hWnd Handle to the button control. + * @param[in] hdc Device context for drawing. + * @param[in] hTheme Active visual style theme handle. + * @param[in] iPartID Part ID (`BP_CHECKBOX`, `BP_RADIOBUTTON`, etc.). + * @param[in] iStateID State ID (`CBS_CHECKEDHOT`, `RBS_UNCHECKEDNORMAL`, etc.). + * + * @see paintButton() + */ +static void renderButton( + HWND hWnd, + HDC hdc, + HTHEME hTheme, + int iPartID, + int iStateID +) +{ + // Font part + + HFONT hFont = nullptr; + bool isFontCreated = false; + LOGFONT lf{}; + if (SUCCEEDED(::GetThemeFont(hTheme, hdc, iPartID, iStateID, TMT_FONT, &lf))) + { + hFont = ::CreateFontIndirectW(&lf); + isFontCreated = true; + } + + if (hFont == nullptr) + { + hFont = reinterpret_cast(::SendMessage(hWnd, WM_GETFONT, 0, 0)); + isFontCreated = false; + } + + const auto holdFont = dmlib_paint::GdiObject{ hdc, hFont, !isFontCreated }; + + // Style part + + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + const bool isMultiline = (nStyle & BS_MULTILINE) == BS_MULTILINE; + const bool isTop = (nStyle & BS_TOP) == BS_TOP; + const bool isBottom = (nStyle & BS_BOTTOM) == BS_BOTTOM; + const bool isCenter = (nStyle & BS_CENTER) == BS_CENTER; + const bool isRight = (nStyle & BS_RIGHT) == BS_RIGHT; + const bool isVCenter = (nStyle & BS_VCENTER) == BS_VCENTER; + + DWORD dtFlags = DT_LEFT; + if (isMultiline) + { + dtFlags |= DT_WORDBREAK; + } + else + { + dtFlags |= DT_SINGLELINE; + } + + if (isCenter) + { + dtFlags |= DT_CENTER; + } + else if (isRight) + { + dtFlags |= DT_RIGHT; + } + + if (isVCenter || (!isMultiline && !isBottom && !isTop)) + { + dtFlags |= DT_VCENTER; + } + else if (isBottom) + { + dtFlags |= DT_BOTTOM; + } + + const auto uiState = static_cast(::SendMessage(hWnd, WM_QUERYUISTATE, 0, 0)); + + // hide prefix + if ((uiState & UISF_HIDEACCEL) == UISF_HIDEACCEL) + { + dtFlags |= DT_HIDEPREFIX; + } + + // Text and box part + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + const auto bufferLen = static_cast(::GetWindowTextLengthW(hWnd)); + auto buffer = std::wstring(bufferLen + 1, L'\0'); + ::GetWindowTextW(hWnd, buffer.data(), static_cast(buffer.length())); + + SIZE szBox{}; + ::GetThemePartSize(hTheme, hdc, iPartID, iStateID, nullptr, TS_DRAW, &szBox); + + RECT rcText{}; + ::GetThemeBackgroundContentRect(hTheme, hdc, iPartID, iStateID, &rcClient, &rcText); + + RECT rcBackground{ rcClient }; + if (!isMultiline) + { + rcBackground.top += (rcText.bottom - rcText.top - szBox.cy) / 2; + } + rcBackground.bottom = rcBackground.top + szBox.cy; + rcBackground.right = rcBackground.left + szBox.cx; + rcText.left = rcBackground.right + 3; + + ::DrawThemeParentBackground(hWnd, hdc, &rcClient); + ::DrawThemeBackground(hTheme, hdc, iPartID, iStateID, &rcBackground, nullptr); // draw box + + DTTOPTS dtto{}; + dtto.dwSize = sizeof(DTTOPTS); + dtto.dwFlags = DTT_TEXTCOLOR; + dtto.crText = (::IsWindowEnabled(hWnd) == FALSE) ? dmlib::getDisabledTextColor() : dmlib::getTextColor(); + + ::DrawThemeTextEx(hTheme, hdc, iPartID, iStateID, buffer.c_str(), -1, dtFlags, &rcText, &dtto); + + // Focus rect + + const auto nState = static_cast(::SendMessage(hWnd, BM_GETSTATE, 0, 0)); + if (((nState & BST_FOCUS) == BST_FOCUS) && ((uiState & UISF_HIDEFOCUS) != UISF_HIDEFOCUS)) + { + dtto.dwFlags |= DTT_CALCRECT; + ::DrawThemeTextEx(hTheme, hdc, iPartID, iStateID, buffer.c_str(), -1, dtFlags | DT_CALCRECT, &rcText, &dtto); + const RECT rcFocus{ rcText.left - 1, rcText.top, rcText.right + 1, rcText.bottom + 1 }; + ::DrawFocusRect(hdc, &rcFocus); + } +} + +/** + * @brief Paints a checkbox, radio, or tri-state button with state-based visuals. + * + * Determines the appropriate themed part and state ID based on the control's + * style (e.g. `BS_CHECKBOX`, `BS_RADIOBUTTON`) and current button state flags + * such as `BST_CHECKED`, `BST_PUSHED`, or `BST_HOT`. + * + * Paint logic: + * - Uses buffered animation (if available) to smoothly transition between states. + * - Falls back to direct drawing via @ref renderButton if animation is not used. + * - Internally updates the `buttonData.m_iStateID` to preserve the last rendered state. + * - Not used for `BS_PUSHLIKE` buttons. + * + * @param[in] hWnd Handle to the checkbox or radio button control. + * @param[in] hdc Device context used for drawing. + * @param[in,out] buttonData Theming and state info, including current theme and last state. + * + * @see renderButton() + */ +static void paintButton(HWND hWnd, HDC hdc, dmlib_subclass::ButtonData& buttonData) +{ + const auto& hTheme = buttonData.m_themeData.getHTheme(); + + const auto nState = static_cast(::SendMessage(hWnd, BM_GETSTATE, 0, 0)); + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + const auto nBtnStyle = nStyle & BS_TYPEMASK; + + int iPartID = 0; + int iStateID = 0; + + static constexpr int checkedOffset = 4; + static constexpr int mixedOffset = 8; + + // Get style + switch (nBtnStyle) + { + case BS_CHECKBOX: + case BS_AUTOCHECKBOX: + case BS_3STATE: + case BS_AUTO3STATE: + { + iPartID = BP_CHECKBOX; + + if (::IsWindowEnabled(hWnd) == FALSE) + { + iStateID = CBS_UNCHECKEDDISABLED; + } + else if ((nState & BST_PUSHED) == BST_PUSHED) + { + iStateID = CBS_UNCHECKEDPRESSED; + } + else if ((nState & BST_HOT) == BST_HOT) + { + iStateID = CBS_UNCHECKEDHOT; + } + else + { + iStateID = CBS_UNCHECKEDNORMAL; + } + + if ((nState & BST_CHECKED) == BST_CHECKED) + { + iStateID += checkedOffset; + } + else if ((nState & BST_INDETERMINATE) == BST_INDETERMINATE) + { + iStateID += mixedOffset; + } + + break; + } + + case BS_RADIOBUTTON: + case BS_AUTORADIOBUTTON: + { + iPartID = BP_RADIOBUTTON; + + if (::IsWindowEnabled(hWnd) == FALSE) + { + iStateID = RBS_UNCHECKEDDISABLED; + } + else if ((nState & BST_PUSHED) == BST_PUSHED) + { + iStateID = RBS_UNCHECKEDPRESSED; + } + else if ((nState & BST_HOT) == BST_HOT) + { + iStateID = RBS_UNCHECKEDHOT; + } + else + { + iStateID = RBS_UNCHECKEDNORMAL; + } + + if ((nState & BST_CHECKED) == BST_CHECKED) + { + iStateID += 4; + } + + break; + } + + default: // should never happen + { + iPartID = BP_CHECKBOX; + iStateID = CBS_UNCHECKEDDISABLED; + break; + } + } + + if (!dmlib_paint::isAnimationEnabled()) + { + renderButton(hWnd, hdc, hTheme, iPartID, iStateID); + buttonData.m_iStateID = iStateID; + return; + } + + if (::BufferedPaintRenderAnimation(hWnd, hdc) == TRUE) + { + return; + } + + // Animation part - hover transition + + BP_ANIMATIONPARAMS animParams{}; + animParams.cbSize = sizeof(BP_ANIMATIONPARAMS); + animParams.style = BPAS_LINEAR; + if (iStateID != buttonData.m_iStateID) + { + ::GetThemeTransitionDuration(hTheme, iPartID, buttonData.m_iStateID, iStateID, TMT_TRANSITIONDURATIONS, &animParams.dwDuration); + } + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + HDC hdcFrom = nullptr; + HDC hdcTo = nullptr; + if (HANIMATIONBUFFER hbpAnimation = ::BeginBufferedAnimation(hWnd, hdc, &rcClient, BPBF_COMPATIBLEBITMAP, nullptr, &animParams, &hdcFrom, &hdcTo); + hbpAnimation != nullptr) + { + if (hdcFrom != nullptr) + { + renderButton(hWnd, hdcFrom, hTheme, iPartID, buttonData.m_iStateID); + } + if (hdcTo != nullptr) + { + renderButton(hWnd, hdcTo, hTheme, iPartID, iStateID); + } + + buttonData.m_iStateID = iStateID; + ::EndBufferedAnimation(hbpAnimation, TRUE); + } + else + { + renderButton(hWnd, hdc, hTheme, iPartID, iStateID); + buttonData.m_iStateID = iStateID; + } +} + +/** + * @brief Window subclass procedure for themed owner drawn checkbox, radio, and tri-state buttons. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData ButtonData instance. + * @return LRESULT Result of message processing. + * + * @see paintButton() + * @see dmlib::setCheckboxOrRadioBtnCtrlSubclass() + * @see dmlib::removeCheckboxOrRadioBtnCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::ButtonSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) +{ + auto* pButtonData = reinterpret_cast(dwRefData); + auto& themeData = pButtonData->m_themeData; + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, ButtonSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pButtonData); + u_ptrData.reset(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled() || !themeData.ensureTheme(hWnd)) + { + break; + } + return TRUE; + } + + case WM_PRINTCLIENT: + case WM_PAINT: + { + if (!dmlib::isEnabled() || !themeData.ensureTheme(hWnd)) + { + break; + } + + PAINTSTRUCT ps{}; + auto hdc = reinterpret_cast(wParam); + if (hdc == nullptr) + { + hdc = ::BeginPaint(hWnd, &ps); + } + + paintButton(hWnd, hdc, *pButtonData); + + if (ps.hdc != nullptr) + { + ::EndPaint(hWnd, &ps); + } + + return 0; + } + + case WM_DPICHANGED_AFTERPARENT: + { + themeData.closeTheme(); + if (pButtonData->m_isSizeSet) + { + // szBtn is changed in Button_GetIdealSize so const should not be used, + // but it is used to silence C26496 - The variable 'szBtn' is assigned only once, mark it as const. + if (const SIZE szBtn{}; + Button_GetIdealSize(hWnd, &szBtn) == TRUE) + { + const UINT dpi = dmlib_dpi::GetDpiForParent(hWnd); + const int cx = std::min(szBtn.cx, dmlib_dpi::scale(pButtonData->m_szBtn.cx, dpi)); + const int cy = std::min(szBtn.cy, dmlib_dpi::scale(pButtonData->m_szBtn.cy, dpi)); + ::SetWindowPos(hWnd, nullptr, 0, 0, cx, cy, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER); + } + } + return 0; + } + + case WM_THEMECHANGED: + { + themeData.closeTheme(); + break; + } + + case WM_SIZE: + case WM_DESTROY: + { + if (dmlib_paint::isAnimationEnabled()) + { + ::BufferedPaintStopAllAnimations(hWnd); + } + break; + } + + case WM_ENABLE: + { + if (!dmlib::isEnabled()) + { + break; + } + + // Skip the button's normal wndproc so it won't redraw out of wm_paint + const LRESULT retVal = ::DefWindowProcW(hWnd, uMsg, wParam, lParam); + ::InvalidateRect(hWnd, nullptr, FALSE); + return retVal; + } + + case WM_UPDATEUISTATE: + { + if ((HIWORD(wParam) & (UISF_HIDEACCEL | UISF_HIDEFOCUS)) != 0) + { + ::InvalidateRect(hWnd, nullptr, FALSE); + } + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Paints a group box frame and text with custom colors. + * + * Handles drawing a themed group box with optional centered text, styled borders, + * and font fallback. If a caption text is present, the frame is clipped to avoid overdrawing + * behind the text. The function adapts layout for both centered and left-aligned titles. + * + * Paint logic: + * - Determines current visual state (`GBS_DISABLED`, `GBS_NORMAL`). + * - Retrieves themed font via `GetThemeFont` or falls back to dialog font. + * - Measures caption text, computes layout and exclusion for frame clipping. + * - Paints the outer rounded frame via @ref dmlib::paintRoundFrameRect + * using `dmlib::getEdgePen()`. + * - Restores clip region and draws text using `DrawThemeTextEx` with custom colors. + * + * @param[in] hWnd Handle to the group box control. + * @param[in] hdc Device context to draw into. + * @param[in] buttonData Reference to the theming and state info (theme handle). + * + * @note Ensures proper cleanup of temporary GDI objects (font, clip region). + * + * @see dmlib::paintRoundFrameRect() + */ +static void paintGroupbox(HWND hWnd, HDC hdc, const dmlib_subclass::ButtonData& buttonData) +{ + const auto& hTheme = buttonData.m_themeData.getHTheme(); + + // Style part + + const bool isDisabled = ::IsWindowEnabled(hWnd) == FALSE; + static constexpr int iPartID = BP_GROUPBOX; + const int iStateID = isDisabled ? GBS_DISABLED : GBS_NORMAL; + + // Font part + + bool isFontCreated = false; + HFONT hFont = nullptr; + LOGFONT lf{}; + if (SUCCEEDED(::GetThemeFont(hTheme, hdc, iPartID, iStateID, TMT_FONT, &lf))) + { + hFont = ::CreateFontIndirectW(&lf); + isFontCreated = true; + } + + if (hFont == nullptr) + { + hFont = reinterpret_cast(::SendMessage(hWnd, WM_GETFONT, 0, 0)); + isFontCreated = false; + } + + const auto holdFont = dmlib_paint::GdiObject{ hdc, hFont, !isFontCreated }; + + // Text rectangle part + + std::wstring buffer; + const auto bufferLen = static_cast(::GetWindowTextLengthW(hWnd)); + if (bufferLen > 0) + { + buffer.resize(bufferLen + 1, L'\0'); + ::GetWindowTextW(hWnd, buffer.data(), static_cast(buffer.length())); + } + + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + const bool isCenter = (nStyle & BS_CENTER) == BS_CENTER; + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + rcClient.bottom -= 1; + + RECT rcText{ rcClient }; + RECT rcBackground{ rcClient }; + if (!buffer.empty()) + { + SIZE szText{}; + ::GetTextExtentPoint32W(hdc, buffer.c_str(), static_cast(bufferLen), &szText); + + const int centerPosX = isCenter ? ((rcClient.right - rcClient.left - szText.cx) / 2) : 7; + + rcBackground.top += szText.cy / 2; + rcText.left += centerPosX; + rcText.bottom = rcText.top + szText.cy; + rcText.right = rcText.left + szText.cx + 4; + + ::ExcludeClipRect(hdc, rcText.left, rcText.top, rcText.right, rcText.bottom); + } + else // There is no text, use "M" to get metrics to move top edge down + { + SIZE szText{}; + ::GetTextExtentPoint32W(hdc, L"M", 1, &szText); + rcBackground.top += szText.cy / 2; + } + + RECT rcContent = rcBackground; + ::GetThemeBackgroundContentRect(hTheme, hdc, BP_GROUPBOX, iStateID, &rcBackground, &rcContent); + ::ExcludeClipRect(hdc, rcContent.left, rcContent.top, rcContent.right, rcContent.bottom); + + dmlib_paint::paintFrameRect(hdc, rcBackground, dmlib::getEdgePen()); // main frame + + ::SelectClipRgn(hdc, nullptr); + + // Text part + + if (!buffer.empty()) + { + ::InflateRect(&rcText, -2, 0); + + DTTOPTS dtto{}; + dtto.dwSize = sizeof(DTTOPTS); + dtto.dwFlags = DTT_TEXTCOLOR; + dtto.crText = isDisabled ? dmlib::getDisabledTextColor() : dmlib::getTextColor(); + + DWORD dtFlags = isCenter ? DT_CENTER : DT_LEFT; + + if (::SendMessage(hWnd, WM_QUERYUISTATE, 0, 0) != 0) // NULL + { + dtFlags |= DT_HIDEPREFIX; + } + + ::DrawThemeTextEx(hTheme, hdc, BP_GROUPBOX, iStateID, buffer.c_str(), -1, dtFlags | DT_SINGLELINE, &rcText, &dtto); + } +} + +/** + * @brief Window subclass procedure for owner drawn groupbox button control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData ButtonData instance. + * @return LRESULT Result of message processing. + * + * @see paintGroupbox() + * @see dmlib::setGroupboxCtrlSubclass() + * @see dmlib::removeGroupboxCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::GroupboxSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) +{ + auto* pButtonData = reinterpret_cast(dwRefData); + auto& themeData = pButtonData->m_themeData; + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, GroupboxSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pButtonData); + u_ptrData.reset(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled() || !themeData.ensureTheme(hWnd)) + { + break; + } + return TRUE; + } + + case WM_PRINTCLIENT: + case WM_PAINT: + { + if (!dmlib::isEnabled() || !themeData.ensureTheme(hWnd)) + { + break; + } + + PAINTSTRUCT ps{}; + auto hdc = reinterpret_cast(wParam); + if (hdc == nullptr) + { + hdc = ::BeginPaint(hWnd, &ps); + } + + paintGroupbox(hWnd, hdc, *pButtonData); + + if (ps.hdc != nullptr) + { + ::EndPaint(hWnd, &ps); + } + + return 0; + } + + case WM_DPICHANGED_AFTERPARENT: + { + themeData.closeTheme(); + return 0; + } + + case WM_THEMECHANGED: + { + themeData.closeTheme(); + break; + } + + case WM_ENABLE: + { + ::RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE); + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Retrieves the appropriate color based on the control's state. + * + * This function determines the color to be used for a control based on its + * current state. The disabled state takes precedence over the hot state. + * + * @param[in] isDisabled Boolean indicating if the control is in a disabled state. + * If true, the function prioritizes this state when determining + * the returned color. + * @param[in] isHot Boolean indicating if the control is in a hot state. + * This state is considered only if the control is not disabled. + * + * @return COLORREF The color reference corresponding to the control's state: + * - Disabled: color from `dmlib::getDisabledTextColor()`. + * - Hot: color from `dmlib::getTextColor()`. + * - Default: color from `dmlib::getDarkerTextColor()`. + */ +static COLORREF getColorFromState(bool isDisabled, bool isHot) noexcept +{ + if (isDisabled) + { + return dmlib::getDisabledTextColor(); + } + if (isHot) + { + return dmlib::getTextColor(); + } + return dmlib::getDarkerTextColor(); +}; + +/** + * @brief Retrieves the appropriate HBRUSH based on the control's state. + * + * This function determines the HBRUSH to be used for a control based on its + * current state. The disabled state takes precedence over the hot state. + * + * @param[in] isDisabled Boolean indicating if the control is in a disabled state. + * If true, the function prioritizes this state when determining + * the returned HBRUSH. + * @param[in] isHot Boolean indicating if the control is in a hot state. + * This state is considered only if the control is not disabled. + * + * @return HBRUSH The color reference corresponding to the control's state: + * - Disabled: color from `dmlib::getDlgBackgroundBrush()`. + * - Hot: color from `dmlib::getHotBackgroundBrush()`. + * - Default: color from `dmlib::getCtrlBackgroundBrush()`. + */ +static HBRUSH getBrushFromState(bool isDisabled, bool isHot) noexcept +{ + if (isDisabled) + { + return dmlib::getDlgBackgroundBrush(); + } + if (isHot) + { + return dmlib::getHotBackgroundBrush(); + } + return dmlib::getCtrlBackgroundBrush(); +}; + +/** + * @brief Retrieves the appropriate HPEN based on the control's state. + * + * This function determines the HPEN to be used for a control based on its + * current state. The disabled state takes precedence over the hot state. + * + * @param[in] isDisabled Boolean indicating if the control is in a disabled state. + * If true, the function prioritizes this state when determining + * the returned HPEN. + * @param[in] isHot Boolean indicating if the control is in a hot state. + * This state is considered only if the control is not disabled. + * + * @return HPEN The color reference corresponding to the control's state: + * - Disabled: color from `dmlib::getDisabledEdgePen()`. + * - Hot: color from `dmlib::getHotEdgePen()`. + * - Default: color from `dmlib::getEdgePen()`. + */ +static HPEN getEdgePenFromState(bool isDisabled, bool isHot) noexcept +{ + if (isDisabled) + { + return dmlib::getDisabledEdgePen(); + } + if (isHot) + { + return dmlib::getHotEdgePen(); + } + return dmlib::getEdgePen(); +}; + +/** + * @brief Get up-down control state. + * + * @param[in] hWnd Handle to the up-down control. + * @param[in] ptCursor Position of mouse cursor. + * @param[in] rcBtn Rectangle for up-down button. + * + * @see UpDownData + * @see paintUpDown() + * + * @note All 4 variants of up-down control buttons have enums with same values + */ +static int getUpDownBtnState(HWND hWnd, const POINT& ptCursor, const RECT& rcBtn) noexcept +{ + if (::IsWindowEnabled(hWnd) == FALSE) + { + return UPS_DISABLED; + } + if (::PtInRect(&rcBtn, ptCursor) != FALSE) + { + return UPS_HOT; + } + return UPS_NORMAL; +} + +/** + * @brief Paints an up-down button with the appropriate background and edge based on its state. + * + * This function determines the brush and pen to use for painting an up-down button + * based on whether it is disabled, hot, or in normal state. The painting includes + * rounded corners for aesthetics. + * + * @param[in] hdc Handle to the device context used for painting. + * @param[in] rect Rectangle that defines the area in which to paint the button. + * @param[in] isDisabled Boolean indicating if the button is in a disabled state. + * @param[in] isHot Boolean indicating if the button is in a hot state. + * @param[in] roundness Corner radius of rectangle. + */ +static void paintUpDownBtn( + HDC hdc, + const RECT& rect, + bool isDisabled, + bool isHot, + int roundness +) noexcept +{ + HBRUSH hBrush = nullptr; + HPEN hPen = nullptr; + + if (isDisabled) + { + hBrush = dmlib::getDlgBackgroundBrush(); + hPen = dmlib::getDisabledEdgePen(); + } + else if (isHot) + { + hBrush = dmlib::getHotBackgroundBrush(); + hPen = dmlib::getHotEdgePen(); + } + else + { + hBrush = dmlib::getCtrlBackgroundBrush(); + hPen = dmlib::getEdgePen(); + } + + dmlib_paint::paintRoundRect(hdc, rect, hPen, hBrush, roundness, roundness); +} + +/** + * @brief Paints an arrow in a specified direction based on the provided state. + * + * This function calculates the appropriate size and position to draw an arrow + * within the given rectangle based on whether it is in a hot state, its direction, + * and other parameters. + * + * @param[in] hdc Handle to the device context used for painting. + * @param[in] hWnd Handle to the control for dpi calculation. + * @param[in] upDownData Reference to layout and state information (segments, orientation, corner radius). + * @param[in] rect Rectangle that defines the area in which to paint the arrow. + * @param[in] clr Color based on state. + * @param[in] isPrev Boolean indicating the direction of the arrow: + * true for "previous" (left/up) and false for "next" (right/down). + */ +static void paintArrow( + HDC hdc, + HWND hWnd, + const dmlib_subclass::UpDownData& upDownData, + const RECT& rect, + COLORREF clr, + bool isPrev +) noexcept +{ + SIZE size{}; + ::GetThemePartSize(upDownData.m_themeData.getHTheme(), nullptr, SPNP_UP, UPS_NORMAL, nullptr, TS_TRUE, &size); + + static constexpr std::array ptsArrowLeft{ { {1.0F, 0.0F}, {0.0F, 0.5F}, {1.0F, 1.0F} } }; + static constexpr std::array ptsArrowRight{ { {0.0F, 0.0F}, {1.0F, 0.5F}, {0.0F, 1.0F} } }; + static constexpr std::array ptsArrowUp{ { {0.0F, 1.0F}, {0.5F, 0.0F}, {1.0F, 1.0F} } }; + static constexpr std::array ptsArrowDown{ { {0.0F, 0.0F}, {0.5F, 1.0F}, {1.0F, 0.0F} } }; + + static constexpr auto scaleFactor = 3L; + static constexpr auto offsetSize = scaleFactor % 2; + const auto baseSize = static_cast(dmlib_dpi::scale(((size.cy - offsetSize) / scaleFactor) + offsetSize, ::GetParent(hWnd))); + + auto sizeArrow = POINTFLOAT{ baseSize, baseSize }; + auto offsetPosX = 0.0F; + auto offsetPosY = 0.0F; + std::array ptsArrowSelected{}; + if (upDownData.m_isHorizontal) + { + if (isPrev) + { + ptsArrowSelected = ptsArrowLeft; + offsetPosX = 1.0F; + } + else + { + ptsArrowSelected = ptsArrowRight; + offsetPosX = -1.0F; + } + sizeArrow.x *= 0.5F; // ratio adjustment + } + else + { + if (isPrev) + { + ptsArrowSelected = ptsArrowUp; + offsetPosY = 1.0F; + } + else + { + ptsArrowSelected = ptsArrowDown; + } + sizeArrow.y *= 0.5F; + } + + const auto xPos = static_cast(rect.left) + ((static_cast(rect.right - rect.left) - sizeArrow.x - offsetPosX) / 2.0F); + const auto yPos = static_cast(rect.top) + ((static_cast(rect.bottom - rect.top) - sizeArrow.y - offsetPosY) / 2.0F); + + std::array ptsArrow{}; + for (size_t i = 0; i < 3; ++i) + { + ptsArrow.at(i).x = static_cast((ptsArrowSelected.at(i).x * sizeArrow.x) + xPos); + ptsArrow.at(i).y = static_cast((ptsArrowSelected.at(i).y * sizeArrow.y) + yPos); + } + + const auto hBrush = dmlib_paint::GdiObject{ hdc, ::CreateSolidBrush(clr) }; + const auto hPen = dmlib_paint::GdiObject{ hdc, ::CreatePen(PS_SOLID, 1, clr) }; + + ::Polygon(hdc, ptsArrow.data(), static_cast(ptsArrow.size())); +} + +/** + * @brief Draws a up-down control. + * + * Draws the two-button spinner control using either themed drawing or manual + * owner-drawn logic depending on OS version and theme availability. Supports both + * vertical and horizontal orientations and adapts to hover and disabled states. + * + * Paint logic: + * - Background fill with dialog background brush + * - Rounded corners (optional, based on Windows 11 and parent class) + * - Direction-aware layout and glyph placement + * + * @param[in] hWnd Handle to the up-down control. + * @param[in] hdc Device context to draw into. + * @param[in,out] upDownData Reference to layout and state information (segments, orientation, corner radius). + * @param[in] iStateIDPrev State of the up-down previous button. + * @param[in] iStateIDNext State of the up-down next button. + * + * @see UpDownData + * @see paintUpDown() + * @see paintUpDownBtn() + * @see paintUpDownBtn() + */ +static void renderUpDown( + HWND hWnd, + HDC hdc, + dmlib_subclass::UpDownData& upDownData, + int iStateIDPrev, + int iStateIDNext +) noexcept +{ + ::FillRect(hdc, &upDownData.m_rcClient, dmlib::getDlgBackgroundBrush()); + ::SetBkMode(hdc, TRANSPARENT); + + auto& themeData = upDownData.m_themeData; + const bool hasTheme = themeData.ensureTheme(hWnd); + const auto& hTheme = themeData.getHTheme(); + + const bool isHorz = upDownData.m_isHorizontal; + + RECT rcPrev{ upDownData.m_rcPrev }; + RECT rcNext{ upDownData.m_rcNext }; + + const bool isDisabled = iStateIDPrev == UPS_DISABLED; + const bool isHotPrev = iStateIDPrev == UPS_HOT; + const bool isHotNext = iStateIDNext == UPS_HOT; + + if (hasTheme && dmlib::isAtLeastWindows11() && dmlib_subclass::isThemePrefered()) + { + int partIdPrev = SPNP_DOWNHORZ; + int partIdNext = SPNP_UPHORZ; + + if (!isHorz) + { + --rcPrev.left; + --rcNext.left; + + partIdPrev = SPNP_UP; + partIdNext = SPNP_DOWN; + } + + ::DrawThemeBackground(hTheme, hdc, partIdPrev, iStateIDPrev, &rcPrev, nullptr); + ::DrawThemeBackground(hTheme, hdc, partIdNext, iStateIDPrev, &rcNext, nullptr); + return; + } + + // Button part + + paintUpDownBtn(hdc, rcPrev, isDisabled, isHotPrev, upDownData.m_cornerRoundness); + paintUpDownBtn(hdc, rcNext, isDisabled, isHotNext, upDownData.m_cornerRoundness); + + // Glyph part + + const COLORREF clrPrev = getColorFromState(isDisabled, isHotPrev); + const COLORREF clrNext = getColorFromState(isDisabled, isHotNext); + + if (hasTheme) + { + paintArrow(hdc, hWnd, upDownData, rcPrev, clrPrev, true); + paintArrow(hdc, hWnd, upDownData, rcNext, clrNext, false); + return; + } + + const auto hFont = dmlib_paint::GdiObject{ hdc, hWnd }; + + static constexpr UINT dtFlags = DT_NOPREFIX | DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOCLIP; + const LONG offset = isHorz ? 1 : 0; + + RECT rcTextPrev{ rcPrev.left, rcPrev.top, rcPrev.right, rcPrev.bottom - offset }; + ::SetTextColor(hdc, clrPrev); + ::DrawText(hdc, isHorz ? dmlib_glyph::kArrowLeft : dmlib_glyph::kArrowUp, -1, &rcTextPrev, dtFlags); + + RECT rcTextNext{ rcNext.left + offset, rcNext.top, rcNext.right, rcNext.bottom - offset }; + ::SetTextColor(hdc, clrNext); + ::DrawText(hdc, isHorz ? dmlib_glyph::kArrowRight : dmlib_glyph::kArrowDown, -1, &rcTextNext, dtFlags); +} + +/** + * @brief Custom paints an up-down (spinner) control. + * + * Draws the up-down control. + * + * Paint logic: + * - Background fill with dialog background brush + * - Rounded corners (optional, based on Windows 11 and parent class) + * - Direction-aware layout and glyph placement + * - Transition effect + * + * @param[in] hWnd Handle to the up-down control. + * @param[in] hdc Device context to draw into. + * @param[in,out] upDownData Reference to layout and state information (segments, orientation, corner radius). + * + * @see UpDownData + */ +static void paintUpDown( + HWND hWnd, + HDC hdc, + dmlib_subclass::UpDownData& upDownData +) noexcept +{ + POINT ptCursor{}; + ::GetCursorPos(&ptCursor); + ::ScreenToClient(hWnd, &ptCursor); + + const int iStateIDPrev = getUpDownBtnState(hWnd, ptCursor, upDownData.m_rcPrev); + const int iStateIDNext = getUpDownBtnState(hWnd, ptCursor, upDownData.m_rcNext); + + if (!dmlib_paint::isAnimationEnabled()) + { + upDownData.m_wasHotNext = (iStateIDPrev != UPS_HOT) && (::PtInRect(&upDownData.m_rcClient, ptCursor) == TRUE); + renderUpDown(hWnd, hdc, upDownData, iStateIDPrev, iStateIDNext); + return; + } + + if (::BufferedPaintRenderAnimation(hWnd, hdc) == TRUE) + { + return; + } + + upDownData.m_themeData.ensureTheme(hWnd); + const auto& hTheme = upDownData.m_themeData.getHTheme(); + + // Animation part - transition + + BP_ANIMATIONPARAMS animParams{}; + animParams.cbSize = sizeof(BP_ANIMATIONPARAMS); + animParams.style = BPAS_LINEAR; + int oldStatePrev = upDownData.m_iStateIDPrev; + int oldStateNext = upDownData.m_iStateIDNext; + if (iStateIDPrev != upDownData.m_iStateIDPrev && upDownData.m_iStateIDPrev != UPS_NORMAL) + { + ::GetThemeTransitionDuration(hTheme, SPNP_UP, upDownData.m_iStateIDPrev, iStateIDPrev, TMT_TRANSITIONDURATIONS, &animParams.dwDuration); + if (iStateIDPrev == UPS_NORMAL) + { + oldStateNext = iStateIDNext; + } + } + + if (iStateIDNext != upDownData.m_iStateIDNext && upDownData.m_iStateIDNext != UPS_NORMAL) + { + ::GetThemeTransitionDuration(hTheme, SPNP_UP, upDownData.m_iStateIDNext, iStateIDNext, TMT_TRANSITIONDURATIONS, &animParams.dwDuration); + if (iStateIDNext == UPS_NORMAL) + { + oldStatePrev = iStateIDPrev; + } + } + + animParams.dwDuration /= 2; + + RECT rcTmp{ upDownData.m_rcClient }; + if (!upDownData.m_isHorizontal) + { + rcTmp.left += dmlib_subclass::UpDownData::kOffset; + } + + HDC hdcFrom = nullptr; + HDC hdcTo = nullptr; + if (HANIMATIONBUFFER hbpAnimation = ::BeginBufferedAnimation(hWnd, hdc, &rcTmp, BPBF_COMPATIBLEBITMAP, nullptr, &animParams, &hdcFrom, &hdcTo); + hbpAnimation != nullptr) + { + if (hdcFrom != nullptr) + { + renderUpDown(hWnd, hdcFrom, upDownData, oldStatePrev, oldStateNext); + } + + if (hdcTo != nullptr) + { + renderUpDown(hWnd, hdcTo, upDownData, iStateIDPrev, iStateIDNext); + } + + upDownData.m_iStateIDPrev = iStateIDPrev; + upDownData.m_iStateIDNext = iStateIDNext; + ::EndBufferedAnimation(hbpAnimation, TRUE); + } + else + { + renderUpDown(hWnd, hdc, upDownData, iStateIDPrev, iStateIDNext); + upDownData.m_iStateIDPrev = iStateIDPrev; + upDownData.m_iStateIDNext = iStateIDNext; + } +} + +/** + * @brief Window subclass procedure for owner drawn up-down (spinner) control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData UpDownData instance. + * @return LRESULT Result of message processing. + * + * @see paintUpDown() + * @see dmlib::setUpDownCtrlSubclass() + * @see dmlib::removeUpDownCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::UpDownSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) noexcept +{ + auto* pUpDownData = reinterpret_cast(dwRefData); + auto& themeData = pUpDownData->m_themeData; + const auto& hMemDC = pUpDownData->m_bufferData.getHMemDC(); + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, UpDownSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pUpDownData); + u_ptrData.reset(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled() || !themeData.ensureTheme(hWnd)) + { + break; + } + + if (!dmlib_paint::isAnimationEnabled() + && reinterpret_cast(wParam) != hMemDC) + { + return FALSE; + } + return TRUE; + } + + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + PAINTSTRUCT ps{}; + HDC hdc = ::BeginPaint(hWnd, &ps); + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + pUpDownData->updateRect(rcClient); + + if (dmlib_paint::isAnimationEnabled()) + { + paintUpDown(hWnd, hdc, *pUpDownData); + ::EndPaint(hWnd, &ps); + return 0; + } + + if (!dmlib_paint::isRectValid(ps.rcPaint)) + { + ::EndPaint(hWnd, &ps); + return 0; + } + + if (!pUpDownData->m_isHorizontal) + { + ::OffsetRect(&ps.rcPaint, UpDownData::kOffset, 0); + ::OffsetRect(&rcClient, UpDownData::kOffset, 0); + } + + dmlib_paint::PaintWithBuffer(*pUpDownData, hdc, ps, + [&]() noexcept { paintUpDown(hWnd, hMemDC, *pUpDownData); }, + rcClient); + + ::EndPaint(hWnd, &ps); + return 0; + } + + case WM_DPICHANGED_AFTERPARENT: + { + pUpDownData->updateRect(hWnd); + themeData.closeTheme(); + return 0; + } + + case WM_THEMECHANGED: + { + themeData.closeTheme(); + break; + } + + case WM_SIZE: + case WM_DESTROY: + { + if (dmlib_paint::isAnimationEnabled()) + { + ::BufferedPaintStopAllAnimations(hWnd); + } + break; + } + + case WM_MOUSEMOVE: + { + if (!dmlib::isEnabled()) + { + break; + } + + if (pUpDownData->m_wasHotNext) + { + pUpDownData->m_wasHotNext = false; + ::RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE); + } + + break; + } + + case WM_MOUSELEAVE: + { + if (!dmlib::isEnabled()) + { + break; + } + + pUpDownData->m_wasHotNext = false; + ::RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE); + + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Paints a tab item in a tab control. + * + * This function handles the rendering of a tab item, determining its appearance + * based on whether it is selected, hot (hovered), or has an image. It also applies + * appropriate colors and styles depending on the current visual state of the tab. + * + * @param[in] hdc Handle to the device context used for painting. + * @param[in] hWnd Handle to the parent tab control window. + * @param[in] rcItem Rectangle defining the area of the tab item. + * @param[in] i Index of the tab item being painted. + * @param[in] iSelTab Index of the currently selected tab. + * @param[in] nTabs Total number of tabs in the tab control. + * @param[in] ptCursor Point representing the current cursor position. + */ +static void paintTabItem( + HDC hdc, + HWND hWnd, + RECT& rcItem, + int i, + int iSelTab, + int nTabs, + const POINT& ptCursor +) +{ + RECT rcFrame{ rcItem }; + + const bool isHot = ::PtInRect(&rcItem, ptCursor) == TRUE; + const bool isSelectedTab = (i == iSelTab); + const LONG paddingTop = dmlib_dpi::scale(2, hWnd); + + ::InflateRect(&rcItem, -1, -1); + rcItem.right += 1; + + auto buffer = std::wstring(MAX_PATH, L'\0'); // label + TCITEMW tci{}; + tci.mask = TCIF_TEXT | TCIF_IMAGE | TCIF_STATE; + tci.dwStateMask = TCIS_HIGHLIGHTED; + tci.pszText = buffer.data(); + tci.cchTextMax = MAX_PATH - 1; + + TabCtrl_GetItem(hWnd, i, &tci); + + RECT rcText{ rcItem }; + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + if ((nStyle & TCS_BUTTONS) == TCS_BUTTONS) // is button + { + if (const bool isHighlighted = (tci.dwState & TCIS_HIGHLIGHTED) == TCIS_HIGHLIGHTED; + isHighlighted) + { + ::FillRect(hdc, &rcItem, dmlib::getHotBackgroundBrush()); + ::SetTextColor(hdc, dmlib::getLinkTextColor()); + } + else + { + ::FillRect(hdc, &rcItem, dmlib::getDlgBackgroundBrush()); + ::SetTextColor(hdc, dmlib::getDarkerTextColor()); + } + + ::FrameRect(hdc, &rcFrame, dmlib::getEdgeBrush()); + } + else + { + ::OffsetRect(&rcText, 0, 1); + + ::SetTextColor(hdc, (isHot || isSelectedTab) ? dmlib::getTextColor() : dmlib::getDarkerTextColor()); + + if (isSelectedTab) + { + ::OffsetRect(&rcText, 0, -1); + rcFrame.bottom += 1; + rcFrame.left -= 2; + } + else if ((i - 1) == iSelTab) + { + rcFrame.top += paddingTop; + } + else + { + rcFrame.top += paddingTop; + rcFrame.left -= 1; + } + + const bool isMultiLine = ((nStyle & TCS_MULTILINE) == TCS_MULTILINE); + if (const bool isOneLine = isMultiLine ? TabCtrl_GetRowCount(hWnd) == 1 : true; + isOneLine && i != nTabs - 1) + { + rcFrame.right += 1; + } + } + + dmlib_paint::paintRect(hdc, rcFrame, dmlib::getEdgePen(), getBrushFromState(isSelectedTab, isHot)); + + // Draw image + if (tci.iImage != -1) + { + int cx = 0; + int cy = 0; + auto hImagelist = TabCtrl_GetImageList(hWnd); + static constexpr int offset = 2; + ::ImageList_GetIconSize(hImagelist, &cx, &cy); + ::ImageList_Draw(hImagelist, tci.iImage, hdc, rcText.left + offset, rcText.top + (((rcText.bottom - rcText.top) - cy) / 2), ILD_NORMAL); + rcText.left += cx; + } + + if (dmlib::getHighlightColor() != dmlib::getCtrlBackgroundColor() + && dmlib::isAtLeastWindows11() + && isSelectedTab) + { + const RECT rcHighlightLine{ rcFrame.left + 1, rcFrame.top + 1, rcFrame.right - 1, rcFrame.top + paddingTop + 1 }; + dmlib_paint::paintRect(hdc, rcHighlightLine, dmlib::getHighlightPen(), dmlib::getHighlightBrush()); + } + + ::DrawText(hdc, buffer.c_str(), -1, &rcText, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + + // Draw focus keyboard cue + if (!isSelectedTab || ::GetFocus() != hWnd) + { + return; + } + + if (const auto uiState = static_cast(::SendMessage(hWnd, WM_QUERYUISTATE, 0, 0)); + (uiState & UISF_HIDEFOCUS) != UISF_HIDEFOCUS) + { + if (dmlib::isAtLeastWindows11()) + { + rcFrame.top += (paddingTop + 1); + } + ::InflateRect(&rcFrame, -2, -1); + ::DrawFocusRect(hdc, &rcFrame); + } +} + +/** + * @brief Custom paints tab items. + * + * Iterates through all tabs in a `SysTabControl32`, applying customized backgrounds, + * text colors, focus indicators, and optional icon drawing. Handles both button-style + * (`TCS_BUTTONS`) and standard tab layouts, adapting based on hover state, selection, + * and focus cue. + * + * Paint logic includes: + * - Retrieves label and optional image via `TCITEM` and `ImageList_Draw` + * - Applies coloring based on selection, hover, and tab style + * - Clips each tab to avoid flickering during overlapping redraw + * - Draws optional focus rectangle if control has input focus via keyboard + * + * @note Currently only works for horizontal style. + * + * @param[in] hWnd Handle to the tab control. + * @param[in] hdc Device context to draw into. + * @param[in] rect Tab control rectangle. + */ +static void paintTab(HWND hWnd, HDC hdc, const RECT& rect) +{ + ::FillRect(hdc, &rect, dmlib::getDlgBackgroundBrush()); + + const auto iSelTab = TabCtrl_GetCurSel(hWnd); + const auto nTabs = TabCtrl_GetItemCount(hWnd); + + int iTab = iSelTab; + + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + + if ((nStyle & TCS_BUTTONS) == TCS_BUTTONS) + { + iTab = nTabs - 1; + } + + RECT rcSelTab{}; + TabCtrl_GetItemRect(hWnd, iTab, &rcSelTab); + + ::ExcludeClipRect(hdc, rcSelTab.left, rcSelTab.top, rcSelTab.right, rcSelTab.bottom); + rcSelTab.bottom -= 1; + + static const int roundness = dmlib::isAtLeastWindows11() ? dmlib_paint::kWin11CornerRoundness : 0; + const RECT rcCont{ rect.left, rcSelTab.bottom, rect.right, rect.bottom }; + dmlib_paint::paintRoundFrameRect(hdc, rcCont, dmlib::getEdgePen(), roundness, roundness); + + const auto hPen = dmlib_paint::GdiObject{ hdc, dmlib::getEdgePen(), true }; + const auto hFont = dmlib_paint::GdiObject{ hdc, hWnd }; + + auto holdClip = ::CreateRectRgn(0, 0, 0, 0); + if (::GetClipRgn(hdc, holdClip) != 1) + { + ::DeleteObject(holdClip); + holdClip = nullptr; + } + + POINT ptCursor{}; + ::GetCursorPos(&ptCursor); + ::ScreenToClient(hWnd, &ptCursor); + + ::SetBkMode(hdc, TRANSPARENT); + + const bool isOwnerDraw = (nStyle & TCS_OWNERDRAWFIXED) != 0; + + for (int i = 0; i < nTabs; ++i) + { + RECT rcItem{}; + TabCtrl_GetItemRect(hWnd, i, &rcItem); + + if (isOwnerDraw) + { + const DRAWITEMSTRUCT dis{ + ODT_TAB + , 0 + , static_cast(i) + , ODA_DRAWENTIRE + , ODS_DEFAULT + , hWnd + , hdc + , rcItem + , 0 + }; + + ::SetTextColor(hdc, (iSelTab == i) ? dmlib::getTextColor() : dmlib::getDarkerTextColor()); + ::SendMessage(::GetParent(hWnd), WM_DRAWITEM, 0, reinterpret_cast(&dis)); + continue; + } + + if (RECT rcIntersect{}; + ::IntersectRect(&rcIntersect, &rect, &rcItem) == FALSE) + { + continue; // Skip to the next iteration when there is no intersection + } + + const RECT rcTmp{ rcItem.left - 2, rcItem.top, rcItem.right + 1, rcItem.bottom }; + HRGN hClip = ::CreateRectRgnIndirect(&rcTmp); + ::SelectClipRgn(hdc, hClip); + + paintTabItem(hdc, hWnd, rcItem, i, iSelTab, nTabs, ptCursor); + + ::SelectClipRgn(hdc, holdClip); + ::DeleteObject(hClip); + } + + ::SelectClipRgn(hdc, holdClip); + if (holdClip != nullptr) + { + ::DeleteObject(holdClip); + holdClip = nullptr; + } +} + +/** + * @brief Window subclass procedure for owner drawn tab control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData TabData instance. + * @return LRESULT Result of message processing. + * + * @see paintTab() + * @see dmlib::setTabCtrlPaintSubclass() + * @see dmlib::removeTabCtrlPaintSubclass() + */ +LRESULT CALLBACK dmlib_subclass::TabPaintSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) +{ + auto* pTabData = reinterpret_cast(dwRefData); + const auto& hMemDC = pTabData->m_bufferData.getHMemDC(); + + if (const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + ((nStyle & TCS_VERTICAL) != 0) + && (uMsg != WM_NCDESTROY)) + { + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + } + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, TabPaintSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pTabData); + u_ptrData.reset(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled()) + { + break; + } + + if (reinterpret_cast(wParam) != hMemDC) + { + return FALSE; + } + return TRUE; + } + + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + PAINTSTRUCT ps{}; + HDC hdc = ::BeginPaint(hWnd, &ps); + + if (!dmlib_paint::isRectValid(ps.rcPaint)) + { + ::EndPaint(hWnd, &ps); + return 0; + } + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + dmlib_paint::PaintWithBuffer(*pTabData, hdc, ps, + [&]() { paintTab(hWnd, hMemDC, rcClient); }, + hWnd); + + ::EndPaint(hWnd, &ps); + return 0; + } + + case WM_UPDATEUISTATE: + { + if ((HIWORD(wParam) & (UISF_HIDEACCEL | UISF_HIDEFOCUS)) != 0) + { + ::InvalidateRect(hWnd, nullptr, FALSE); + } + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Window subclass procedure for tab control's up-down control subclassing. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see dmlib::setUpDownCtrlSubclass() + * @see dmlib::setTabCtrlUpDownSubclass() + * @see dmlib::removeTabCtrlUpDownSubclass() + */ +LRESULT CALLBACK dmlib_subclass::TabUpDownSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, TabUpDownSubclass, uIdSubclass); + break; + } + + case WM_PARENTNOTIFY: + { + if (LOWORD(wParam) == WM_CREATE) + { + auto hUpDown = reinterpret_cast(lParam); + if (dmlib_subclass::cmpWndClassName(hUpDown, UPDOWN_CLASS)) + { + dmlib::setUpDownCtrlSubclass(hUpDown); + return 0; + } + } + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Paints a custom non-client border for list box and edit controls. + * + * Paints an inner and outer border using custom colors. + * The outer border highlights when the window is hot (hovered) or focused. + * + * @param[in] hWnd Handle to the target list box or edit control. + * @param[in] borderMetricsData Precomputed system metrics and hot state. + */ +static void ncPaintCustomBorder(HWND hWnd, const dmlib_subclass::BorderMetricsData& borderMetricsData) noexcept +{ + HDC hdc = ::GetWindowDC(hWnd); + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + rcClient.right += (2 * borderMetricsData.m_xEdge); + + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + if ((nStyle & WS_VSCROLL) == WS_VSCROLL) + { + rcClient.right += borderMetricsData.m_xScroll; + } + + rcClient.bottom += (2 * borderMetricsData.m_yEdge); + + if ((nStyle & WS_HSCROLL) == WS_HSCROLL) + { + rcClient.bottom += borderMetricsData.m_yScroll; + } + + const HPEN hPen = ::CreatePen(PS_SOLID, 1, (::IsWindowEnabled(hWnd) == TRUE) ? dmlib::getBackgroundColor() : dmlib::getDlgBackgroundColor()); + RECT rcInner{ rcClient }; + ::InflateRect(&rcInner, -1, -1); + dmlib_paint::paintFrameRect(hdc, rcInner, hPen); + ::DeleteObject(hPen); + + POINT ptCursor{}; + ::GetCursorPos(&ptCursor); + ::ScreenToClient(hWnd, &ptCursor); + + const bool isHot = ::PtInRect(&rcClient, ptCursor) == TRUE; + const bool hasFocus = ::GetFocus() == hWnd; + + const HPEN hEnabledPen = ((borderMetricsData.m_isHot && isHot) || hasFocus ? dmlib::getHotEdgePen() : dmlib::getEdgePen()); + + static const int roundness = dmlib::isAtLeastWindows11() ? dmlib_paint::kWin11CornerRoundness : 0; + dmlib_paint::paintRoundRect( + hdc, + rcClient, + (::IsWindowEnabled(hWnd) == TRUE) ? hEnabledPen : dmlib::getDisabledEdgePen(), + static_cast(::GetStockObject(NULL_BRUSH)), + roundness, + roundness + ); + + if (dmlib::getHighlightColor() != dmlib::getCtrlBackgroundColor() + && dmlib::isAtLeastWindows11() + && hasFocus + && borderMetricsData.m_isEdit) + { + const RECT rcHighlightBottomLine{ rcClient.left, rcClient.bottom - dmlib_dpi::scale(2, hWnd), rcClient.right, rcClient.bottom}; + dmlib_paint::paintRoundRect(hdc, rcHighlightBottomLine, dmlib::getHighlightPen(), dmlib::getHighlightBrush(), roundness, roundness); + } + + ::ReleaseDC(hWnd, hdc); +} + +/** + * @brief Window subclass procedure for owner drawn border for list box and edit controls. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData BorderMetricsData instance. + * @return LRESULT Result of message processing. + * + * @see dmlib::setCustomBorderForListBoxOrEditCtrlSubclass() + * @see dmlib::removeCustomBorderForListBoxOrEditCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::CustomBorderSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) noexcept +{ + auto* pBorderMetricsData = reinterpret_cast(dwRefData); + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, CustomBorderSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pBorderMetricsData); + u_ptrData.reset(); + break; + } + + case WM_NCPAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + + ncPaintCustomBorder(hWnd, *pBorderMetricsData); + + return 0; + } + + case WM_NCCALCSIZE: + { + if (!dmlib::isEnabled()) + { + break; + } + + auto* lpRect = reinterpret_cast(lParam); + ::InflateRect(lpRect, -(pBorderMetricsData->m_xEdge), -(pBorderMetricsData->m_yEdge)); + + break; + } + + case WM_DPICHANGED_AFTERPARENT: + { + pBorderMetricsData->setMetricsForDpi(dmlib_dpi::GetDpiForParent(hWnd)); + dmlib::redrawWindowFrame(hWnd); + return 0; + } + + case WM_MOUSEMOVE: + { + if (!dmlib::isEnabled()) + { + break; + } + + if (::GetFocus() == hWnd) + { + break; + } + + TRACKMOUSEEVENT tme{}; + tme.cbSize = sizeof(TRACKMOUSEEVENT); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = hWnd; + tme.dwHoverTime = HOVER_DEFAULT; + ::TrackMouseEvent(&tme); + + if (!pBorderMetricsData->m_isHot) + { + pBorderMetricsData->m_isHot = true; + dmlib::redrawWindowFrame(hWnd); + } + break; + } + + case WM_MOUSELEAVE: + { + if (!dmlib::isEnabled()) + { + break; + } + + if (pBorderMetricsData->m_isHot) + { + pBorderMetricsData->m_isHot = false; + dmlib::redrawWindowFrame(hWnd); + } + + TRACKMOUSEEVENT tme{}; + tme.cbSize = sizeof(TRACKMOUSEEVENT); + tme.dwFlags = TME_LEAVE | TME_CANCEL; + tme.hwndTrack = hWnd; + tme.dwHoverTime = HOVER_DEFAULT; + ::TrackMouseEvent(&tme); + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Get combo box state and client rectangle. + * + * @param[in] hWnd Handle to the combo box control. + * @param[in,out] rcClient Rectangle for combo box. + * + * @see ComboBoxData + * @see paintComboBox() + */ +static int getComboBoxStateAndRect(HWND hWnd, RECT& rcClient) noexcept +{ + ::GetClientRect(hWnd, &rcClient); + + POINT ptCursor{}; + ::GetCursorPos(&ptCursor); + ::ScreenToClient(hWnd, &ptCursor); + + if (::IsWindowEnabled(hWnd) == FALSE) + { + return CBXSR_DISABLED; + } + if (::PtInRect(&rcClient, ptCursor) != FALSE) + { + return CBXSR_HOT; + } + return CBXSR_NORMAL; +} + +/** + * @brief Draws a combo box control with `CBS_SIMPLE` or `CBS_DROPDOWN` style. + * + * This function handles owner-drawn drawing of a combo box, adapting its + * appearance based on: + * - Control style (`CBS_SIMPLE`, `CBS_DROPDOWN`) + * - Enabled/disabled state + * - Hot (hover) state + * - Focus state + * - Dark mode theme availability + * + * Paint logic: + * - Draws background with different brushes for normal, hot, and disabled states + * - Uses `COMBOBOXINFO` to retrieve subcomponent rectangles. + * - Draws text using theme APIs if available, otherwise GDI + * - For `CBS_SIMPLE` and `CBS_DROPDOWN`, text is handled by the child edit control. + * - The drop-down arrow is drawn either via `DrawThemeBackground` or a manual glyph. + * - Borders are drawn with pens with custom colors depending on state (rounded corners on Windows 11+). + * - Uses `ExcludeClipRect` to avoid overpainting the text/edit area. + * + * @param[in] hWnd Handle to the combo box control. + * @param[in] hdc Device context to draw into. + * @param[in,out] comboBoxData Reference to the combo box' theme and style data. + * @param[in] iStateID State of the combo box. + * + * @see ComboBoxData + * @see paintComboBox() + * @see renderComboBoxList() + */ +static void renderComboBoxEdit(HWND hWnd, HDC hdc, dmlib_subclass::ComboBoxData& comboBoxData, int iStateID) noexcept +{ + auto& themeData = comboBoxData.m_themeData; + const auto& hTheme = themeData.getHTheme(); + const bool hasTheme = themeData.ensureTheme(hWnd); + + const bool isSimple = comboBoxData.m_cbStyle == CBS_SIMPLE; + + COMBOBOXINFO cbi{}; + cbi.cbSize = sizeof(COMBOBOXINFO); + ::GetComboBoxInfo(hWnd, &cbi); + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + const bool isDisabled = iStateID == CBXSR_DISABLED; + const bool isHot = iStateID == CBXSR_HOT; + + bool hasFocus = false; + + RECT rcArrow{ cbi.rcButton }; + rcArrow.left -= 1; + + // Text part + + if (comboBoxData.m_cbStyle == CBS_DROPDOWN && cbi.hwndItem != nullptr) + { + hasFocus = ::GetFocus() == cbi.hwndItem; + const HBRUSH hBrush = getBrushFromState(isDisabled, isHot); + ::FillRect(hdc, &rcArrow, hBrush); + } + + const HPEN hPen = getEdgePenFromState(isDisabled, isHot || hasFocus || isSimple); + const auto holdPen = dmlib_paint::GdiObject{ hdc, hPen, true}; + + // Drop down arrow part + if (comboBoxData.m_cbStyle == CBS_DROPDOWN) + { + if (hasTheme + && (dmlib::isExperimentalSupported() + || !dmlib::isDarkDmTypeUsed())) + { + const RECT rcThemedArrow{ rcArrow.left, rcArrow.top - 1, rcArrow.right, rcArrow.bottom - 1 }; + ::DrawThemeBackground(hTheme, hdc, CP_DROPDOWNBUTTONRIGHT, isDisabled ? CBXSR_DISABLED : CBXSR_NORMAL, &rcThemedArrow, nullptr); + } + else + { + const auto holdFont = dmlib_paint::GdiObject{ hdc, hWnd }; + + ::SetTextColor(hdc, getColorFromState(isDisabled, isHot)); + static constexpr UINT dtFlags = DT_NOPREFIX | DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOCLIP; + ::SetBkMode(hdc, TRANSPARENT); + ::DrawText(hdc, dmlib_glyph::kArrowDown, -1, &rcArrow, dtFlags); + } + } + + // Frame part + ::ExcludeClipRect(hdc, cbi.rcItem.left, cbi.rcItem.top, cbi.rcItem.right, cbi.rcItem.bottom); + + if (isSimple && cbi.hwndList != nullptr) + { + RECT rcItem{ cbi.rcItem }; + ::MapWindowPoints(cbi.hwndItem, hWnd, reinterpret_cast(&rcItem), 2); + rcClient.bottom = rcItem.bottom; + } + + RECT rcInner{ rcClient }; + ::InflateRect(&rcInner, -1, -1); + + if (comboBoxData.m_cbStyle == CBS_DROPDOWN) + { + const std::array edge{ { + { rcArrow.left - 1, rcArrow.top }, + { rcArrow.left - 1, rcArrow.bottom } + } }; + ::Polyline(hdc, edge.data(), static_cast(edge.size())); + + ::ExcludeClipRect(hdc, rcArrow.left - 1, rcArrow.top, rcArrow.right, rcArrow.bottom); + + rcInner.right = rcArrow.left - 1; + } + + HPEN hInnerPen = ::CreatePen(PS_SOLID, 1, isDisabled ? dmlib::getDlgBackgroundColor() : dmlib::getBackgroundColor()); + dmlib_paint::paintFrameRect(hdc, rcInner, hInnerPen); + ::DeleteObject(hInnerPen); + ::InflateRect(&rcInner, -1, -1); + ::FillRect(hdc, &rcInner, isDisabled ? dmlib::getDlgBackgroundBrush() : dmlib::getCtrlBackgroundBrush()); + + static const int roundness = dmlib::isAtLeastWindows11() ? dmlib_paint::kWin11CornerRoundness : 0; + dmlib_paint::paintRoundFrameRect(hdc, rcClient, hPen, roundness, roundness); +} + +/** + * @brief Draws a combo box control with `CBS_DROPDOWNLIST` style. + * + * This function handles owner-drawn drawing of a combo box, adapting its + * appearance based on: + * - Enabled/disabled state + * - Hot (hover) state + * - Focus state + * - Dark mode theme availability + * - `CBS_OWNERDRAWFIXED` and `CBS_OWNERDRAWVARIABLE` flags + * + * Paint logic: + * - Draws background with different brushes for normal, hot, and disabled states + * - Uses `COMBOBOXINFO` to retrieve subcomponent rectangles. + * - Draws text using theme APIs if available, otherwise GDI + * - For `CBS_DROPDOWNLIST`, draws the selected item text directly. + * - The drop-down arrow is drawn either via `DrawThemeBackground` or a manual glyph. + * - Borders are drawn with pens with custom colors depending on state (rounded corners on Windows 11+). + * - Uses `ExcludeClipRect` to avoid overpainting the text/edit area. + * + * @param[in] hWnd Handle to the combo box control. + * @param[in] hdc Device context to draw into. + * @param[in,out] comboBoxData Reference to the combo box' theme and style data. + * @param[in] iStateID State of the combo box. + * + * @see ComboBoxData + * @see paintComboBox() + * @see renderComboBoxEdit() + */ +static void renderComboBoxList( + HWND hWnd, + HDC hdc, + dmlib_subclass::ComboBoxData& comboBoxData, + int iStateID +) +{ + auto& themeData = comboBoxData.m_themeData; + const auto& hTheme = themeData.getHTheme(); + + const bool hasTheme = themeData.ensureTheme(hWnd); + + COMBOBOXINFO cbi{}; + cbi.cbSize = sizeof(COMBOBOXINFO); + ::GetComboBoxInfo(hWnd, &cbi); + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + const auto holdFont = dmlib_paint::GdiObject{ hdc, hWnd }; + + RECT rcArrow{ cbi.rcButton }; + rcArrow.left -= 1; + + const bool isDisabled = iStateID == CBXSR_DISABLED; + const bool isHot = iStateID == CBXSR_HOT; + + const HBRUSH hBrush = getBrushFromState(isDisabled, isHot); + const COLORREF clrText = isDisabled ? dmlib::getDisabledTextColor() : dmlib::getTextColor(); + + // Text part + + // erase background on item change + ::FillRect(hdc, &rcClient, hBrush); + + if (const auto index = static_cast(::SendMessage(hWnd, CB_GETCURSEL, 0, 0)); + index != CB_ERR) + { + if (const bool isOwnerDraw = (::GetWindowLongPtr(hWnd, GWL_STYLE) & (CBS_OWNERDRAWFIXED | CBS_OWNERDRAWVARIABLE)) != 0; + isOwnerDraw) + { + const auto itemData = ::SendMessage(hWnd, CB_GETITEMDATA, static_cast(index), 0); + const int id = ::GetDlgCtrlID(hWnd); + + const DRAWITEMSTRUCT dis{ + ODT_COMBOBOX + , static_cast(id) + , static_cast(index) + , ODA_DRAWENTIRE + , ODS_DEFAULT + , hWnd + , hdc + , cbi.rcItem + , static_cast(itemData) + }; + + ::SetTextColor(hdc, clrText); + ::SendMessage(::GetParent(hWnd), WM_DRAWITEM, static_cast(id), reinterpret_cast(&dis)); + } + else + { + const auto bufferLen = static_cast(::SendMessage(hWnd, CB_GETLBTEXTLEN, static_cast(index), 0)); + auto buffer = std::wstring(bufferLen + 1, L'\0'); + ::SendMessage(hWnd, CB_GETLBTEXT, static_cast(index), reinterpret_cast(buffer.data())); + + RECT rcText{ cbi.rcItem }; + ::InflateRect(&rcText, -2, 0); + + static constexpr DWORD dtFlags = DT_NOPREFIX | DT_LEFT | DT_VCENTER | DT_SINGLELINE; + if (hasTheme) + { + DTTOPTS dtto{}; + dtto.dwSize = sizeof(DTTOPTS); + dtto.dwFlags = DTT_TEXTCOLOR; + dtto.crText = clrText; + + ::DrawThemeTextEx(hTheme, hdc, CP_DROPDOWNITEM, iStateID, buffer.c_str(), -1, dtFlags, &rcText, &dtto); + } + else + { + ::SetTextColor(hdc, clrText); + ::SetBkMode(hdc, TRANSPARENT); + ::DrawText(hdc, buffer.c_str(), -1, &rcText, dtFlags); + } + } + } + + const bool hasFocus = ::GetFocus() == hWnd; + if (!isDisabled && hasFocus && ::SendMessage(hWnd, CB_GETDROPPEDSTATE, 0, 0) == FALSE) + { + ::DrawFocusRect(hdc, &cbi.rcItem); + } + + const HPEN hPen = getEdgePenFromState(isDisabled, isHot || hasFocus); + const auto holdPen = dmlib_paint::GdiObject{ hdc, hPen, true }; + + // Drop down arrow part + if (hasTheme + && (dmlib::isExperimentalSupported() + || !dmlib::isDarkDmTypeUsed())) + { + const RECT rcThemedArrow{ rcArrow.left, rcArrow.top - 1, rcArrow.right, rcArrow.bottom - 1 }; + ::DrawThemeBackground(hTheme, hdc, CP_DROPDOWNBUTTONRIGHT, isDisabled ? CBXSR_DISABLED : CBXSR_NORMAL, &rcThemedArrow, nullptr); + } + else + { + ::SetTextColor(hdc, getColorFromState(isDisabled, isHot)); + static constexpr UINT dtFlags = DT_NOPREFIX | DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOCLIP; + ::DrawText(hdc, dmlib_glyph::kArrowDown, -1, &rcArrow, dtFlags); + } + + // Frame part + ::ExcludeClipRect(hdc, rcClient.left + 1, rcClient.top + 1, rcClient.right - 1, rcClient.bottom - 1); + + static const int roundness = dmlib::isAtLeastWindows11() ? dmlib_paint::kWin11CornerRoundness : 0; + dmlib_paint::paintRoundFrameRect(hdc, rcClient, hPen, roundness, roundness); +} + +/** + * @brief Draws a combo box control. + * + * This wrapper draws appropriate combo box based on its style. + * + * @param[in] hWnd Handle to the combo box control. + * @param[in] hdc Device context to draw into. + * @param[in,out] comboBoxData Reference to the combo box' theme and style data. + * @param[in] iStateID State of the combo box. + * + * @see ComboBoxData + * @see paintComboBox() + * @see renderComboBoxEdit() + * @see renderComboBoxList() + */ +static void renderComboBox(HWND hWnd, HDC hdc, dmlib_subclass::ComboBoxData& comboBoxData, int iStateID) +{ + if (comboBoxData.m_cbStyle == CBS_DROPDOWNLIST) + { + renderComboBoxList(hWnd, hdc, comboBoxData, iStateID); + } + else + { + renderComboBoxEdit(hWnd, hdc, comboBoxData, iStateID); + } +} + +/** + * @brief Custom paints a combo box control. + * + * This function handles owner-drawn drawing of a combo box, + * adapting its appearance based on: + * - Control style (`CBS_SIMPLE`, `CBS_DROPDOWN`, `CBS_DROPDOWNLIST`) + * - Transition effect for `CBS_DROPDOWNLIST` + * - Enabled/disabled state + * - Hot (hover) state + * + * @param[in] hWnd Handle to the combo box control. + * @param[in] hdc Device context to draw into. + * @param[in,out] comboBoxData Reference to the combo box' theme and style data. + * + * @see ComboBoxData + * @see renderComboBoxEdit() + * @see renderComboBoxList() + */ +static void paintComboBox(HWND hWnd, HDC hdc, dmlib_subclass::ComboBoxData& comboBoxData) +{ + RECT rcClient{}; + const int iStateID = getComboBoxStateAndRect(hWnd, rcClient); + + if (comboBoxData.m_cbStyle == CBS_SIMPLE) + { + renderComboBoxEdit(hWnd, hdc, comboBoxData, iStateID); + return; + } + + if (!dmlib_paint::isAnimationEnabled()) + { + renderComboBox(hWnd, hdc, comboBoxData, iStateID); + return; + } + + if (::BufferedPaintRenderAnimation(hWnd, hdc) == TRUE) + { + return; + } + + comboBoxData.m_themeData.ensureTheme(hWnd); + const auto& hTheme = comboBoxData.m_themeData.getHTheme(); + + // Animation part - transition + + BP_ANIMATIONPARAMS animParams{}; + animParams.cbSize = sizeof(BP_ANIMATIONPARAMS); + animParams.style = BPAS_LINEAR; + if (iStateID != comboBoxData.m_iStateID) + { + ::GetThemeTransitionDuration(hTheme, CP_DROPDOWNBUTTONRIGHT, comboBoxData.m_iStateID, iStateID, TMT_TRANSITIONDURATIONS, &animParams.dwDuration); + } + + HDC hdcFrom = nullptr; + HDC hdcTo = nullptr; + if (HANIMATIONBUFFER hbpAnimation = ::BeginBufferedAnimation(hWnd, hdc, &rcClient, BPBF_COMPATIBLEBITMAP, nullptr, &animParams, &hdcFrom, &hdcTo); + hbpAnimation != nullptr) + { + if (hdcFrom != nullptr) + { + renderComboBox(hWnd, hdcFrom, comboBoxData, comboBoxData.m_iStateID); + } + + if (hdcTo != nullptr) + { + renderComboBox(hWnd, hdcTo, comboBoxData, iStateID); + } + + comboBoxData.m_iStateID = iStateID; + ::EndBufferedAnimation(hbpAnimation, TRUE); + } + else + { + renderComboBox(hWnd, hdc, comboBoxData, iStateID); + comboBoxData.m_iStateID = iStateID; + } +} + +/** + * @brief Window subclass procedure for owner drawn combo box control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData ComboBoxData instance. + * @return LRESULT Result of message processing. + * + * @see dmlib::setComboBoxCtrlSubclass() + * @see dmlib::removeComboBoxCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::ComboBoxSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) +{ + auto* pComboboxData = reinterpret_cast(dwRefData); + auto& themeData = pComboboxData->m_themeData; + const auto& hMemDC = pComboboxData->m_bufferData.getHMemDC(); + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, ComboBoxSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pComboboxData); + u_ptrData.reset(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled() || !themeData.ensureTheme(hWnd)) + { + break; + } + + if (!dmlib_paint::isAnimationEnabled() + && pComboboxData->m_cbStyle == CBS_DROPDOWNLIST + && reinterpret_cast(wParam) != hMemDC) + { + return FALSE; + } + return TRUE; + } + + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + PAINTSTRUCT ps{}; + HDC hdc = ::BeginPaint(hWnd, &ps); + + if (!dmlib_paint::isAnimationEnabled() + && pComboboxData->m_cbStyle == CBS_DROPDOWNLIST) + { + if (!dmlib_paint::isRectValid(ps.rcPaint)) + { + ::EndPaint(hWnd, &ps); + return 0; + } + + dmlib_paint::PaintWithBuffer(*pComboboxData, hdc, ps, + [&]() { paintComboBox(hWnd, hMemDC, *pComboboxData); }, + hWnd); + } + else + { + paintComboBox(hWnd, hdc, *pComboboxData); + } + + ::EndPaint(hWnd, &ps); + return 0; + } + + case WM_DPICHANGED_AFTERPARENT: + { + themeData.closeTheme(); + return 0; + } + + case WM_THEMECHANGED: + { + themeData.closeTheme(); + break; + } + + case WM_SIZE: + case WM_DESTROY: + { + if (dmlib_paint::isAnimationEnabled() + && pComboboxData->m_cbStyle != CBS_SIMPLE) + { + ::BufferedPaintStopAllAnimations(hWnd); + } + break; + } + + case WM_ENABLE: + { + if (!dmlib::isEnabled()) + { + break; + } + + const LRESULT retVal = ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + ::RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE); + return retVal; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Window subclass procedure for custom color for ComboBoxEx' list box and edit control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see dmlib::setComboBoxExCtrlSubclass() + * @see dmlib::removeComboBoxExCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::ComboBoxExSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) noexcept +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, ComboBoxExSubclass, uIdSubclass); + dmlib_hook::unhookSysColor(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled()) + { + break; + } + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + ::FillRect(reinterpret_cast(wParam), &rcClient, dmlib::getDlgBackgroundBrush()); + return TRUE; + } + + case WM_CTLCOLOREDIT: + { + if (!dmlib::isEnabled()) + { + break; + } + return dmlib::onCtlColorCtrl(reinterpret_cast(wParam)); + } + + case WM_CTLCOLORLISTBOX: + { + if (!dmlib::isEnabled()) + { + break; + } + return dmlib::onCtlColorListbox(wParam, lParam); + } + + // ComboBoxEx has only one child combo box, so only control-defined notification code is checked. + // Hooking is done only when list box is about to show. And unhook when list box is closed. + // This process is used to avoid visual glitches in other GUI. + case WM_COMMAND: + { + if (!dmlib::isEnabled()) + { + break; + } + + switch (HIWORD(wParam)) + { + case CBN_DROPDOWN: + { + dmlib_hook::hookSysColor(); + break; + } + + case CBN_CLOSEUP: + { + dmlib_hook::unhookSysColor(); + break; + } + + default: + { + break; + } + } + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Handles custom draw notifications for a list view's header control. + * + * Processes `NM_CUSTOMDRAW` message to provide custom color for header text. + * + * @param[in] lParam Pointer to `LPNMCUSTOMDRAW`. + * @return `LRESULT` containing draw flags. + */ +[[nodiscard]] static LRESULT onCustomDrawLVHeader(LPARAM lParam) noexcept +{ + auto* lpnmcd = reinterpret_cast(lParam); + switch (lpnmcd->dwDrawStage) + { + case CDDS_PREPAINT: + { + if (dmlib::isExperimentalActive()) + { + return CDRF_NOTIFYITEMDRAW; + } + return CDRF_DODEFAULT; + } + + case CDDS_ITEMPREPAINT: + { + ::SetTextColor(lpnmcd->hdc, dmlib::getDarkerTextColor()); + + return CDRF_NEWFONT; + } + + default: + { + return CDRF_DODEFAULT; + } + } +} + +/** + * @brief Window subclass procedure for custom color for list view's gridlines and edit control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see dmlib::setListViewCtrlSubclass() + * @see dmlib::removeListViewCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::ListViewSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) noexcept +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, ListViewSubclass, uIdSubclass); + dmlib_hook::unhookSysColor(); + break; + } + + // For gridlines + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + const auto lvStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE) & LVS_TYPEMASK; + const bool isReport = (lvStyle == LVS_REPORT); + bool hasGridlines = false; + if (isReport) + { + const auto lvExStyle = ListView_GetExtendedListViewStyle(hWnd); + hasGridlines = (lvExStyle & LVS_EX_GRIDLINES) == LVS_EX_GRIDLINES; + } + + if (hasGridlines) + { + dmlib_hook::hookSysColor(); + const LRESULT retVal = ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + dmlib_hook::unhookSysColor(); + return retVal; + } + break; + } + + case WM_DPICHANGED_AFTERPARENT: + { + dmlib::setDarkListViewCheckboxes(hWnd); + return 0; + } + + // For edit control, which is created when renaming/editing items + case WM_CTLCOLOREDIT: + { + if (!dmlib::isEnabled()) + { + break; + } + return dmlib::onCtlColorCtrl(reinterpret_cast(wParam)); + } + + case WM_ENABLE: + { + if (!dmlib::isEnabled()) + { + break; + } + + const bool isDisabled = (wParam == FALSE); + dmlib::replaceClientEdgeWithBorderSafeEx(hWnd, isDisabled); + + break; + } + + case WM_NOTIFY: + { + if (!dmlib::isEnabled()) + { + break; + } + + if (reinterpret_cast(lParam)->code == NM_CUSTOMDRAW) + { + return onCustomDrawLVHeader(lParam); + } + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Paints a single header control item (column header). + * + * Draws the item background (including hot/pressed visuals), optional sort arrow, + * vertical separator edge, and item text. Uses visual styles (HTHEME) when available; + * falls back to classic GDI text drawing otherwise. + * + * Paint logic: + * - Draws sort arrows if `HDF_SORTUP` or `HDF_SORTDOWN` is set. + * - Draws a vertical separator line with alignment between items. + * - Draws the item text with alignment and pressed offset adjustments. + * - Uses `DrawThemeTextEx` for themed text drawing, or `DrawText` otherwise. + * + * @param[in] hWnd Handle to the header control. + * @param[in] hdc Device context to draw into. + * @param[in] headerData Reference to the header's theme, state, and style data. + * @param[in] i Zero-based index of the header item to paint. + * @param[in] rcItem Rect used by Header_GetItemRect. + * @param[in] hasGridlines True when parent ListView displays gridlines. + * @param[in] dtto DTTOPTS for DrawThemeTextEx. + */ +static void paintHeaderItem( + HWND hWnd, + HDC hdc, + const dmlib_subclass::HeaderData& headerData, + int i, + RECT& rcItem, + bool hasGridlines, + const DTTOPTS& dtto +) +{ + const HTHEME& hTheme = headerData.m_themeData.getHTheme(); + + Header_GetItemRect(hWnd, i, &rcItem); + const bool isOnItem = ::PtInRect(&rcItem, headerData.m_pt) == TRUE; + + // Different visual styles have different vertical alignments. + // This part is for header item rectangle. + if (headerData.m_hasBtnStyle && isOnItem) + { + RECT rcTmp{ rcItem }; + if (hasGridlines) + { + ::OffsetRect(&rcTmp, 1, 0); + } + else if (dmlib::isExperimentalActive()) + { + ::OffsetRect(&rcTmp, -1, 0); + } + ::FillRect(hdc, &rcTmp, dmlib::getHeaderHotBackgroundBrush()); + } + + auto buffer = std::wstring(MAX_PATH, L'\0'); + HDITEM hdi{}; + hdi.mask = HDI_TEXT | HDI_FORMAT; + hdi.pszText = buffer.data(); + hdi.cchTextMax = MAX_PATH - 1; + + Header_GetItem(hWnd, i, &hdi); + + // Sort arrows + if (hTheme != nullptr + && ((hdi.fmt & HDF_SORTUP) == HDF_SORTUP + || (hdi.fmt & HDF_SORTDOWN) == HDF_SORTDOWN)) + { + const int iStateID = ((hdi.fmt & HDF_SORTUP) == HDF_SORTUP) ? HSAS_SORTEDUP : HSAS_SORTEDDOWN; + RECT rcArrow{ rcItem }; + SIZE szArrow{}; + if (SUCCEEDED(::GetThemePartSize(hTheme, hdc, HP_HEADERSORTARROW, iStateID, nullptr, TS_DRAW, &szArrow))) + { + rcArrow.bottom = szArrow.cy; + } + + ::DrawThemeBackground(hTheme, hdc, HP_HEADERSORTARROW, iStateID, &rcArrow, nullptr); + } + + // Aligment for border + LONG edgeX = rcItem.right; + if (!hasGridlines) + { + --edgeX; + if (dmlib::isExperimentalActive()) + { + --edgeX; + } + } + + const std::array edge{ { + { edgeX, rcItem.top }, + { edgeX, rcItem.bottom } + } }; + ::Polyline(hdc, edge.data(), static_cast(edge.size())); + + // Text draw part + + DWORD dtFlags = DT_VCENTER | DT_SINGLELINE | DT_WORD_ELLIPSIS | DT_HIDEPREFIX; + if ((hdi.fmt & HDF_RIGHT) == HDF_RIGHT) + { + dtFlags |= DT_RIGHT; + } + else if ((hdi.fmt & HDF_CENTER) == HDF_CENTER) + { + dtFlags |= DT_CENTER; + } + + static constexpr LONG lOffset = 6; + static constexpr LONG rOffset = 8; + + rcItem.left += lOffset; + rcItem.right -= rOffset; + + if (headerData.m_isPressed && isOnItem) + { + ::OffsetRect(&rcItem, 1, 1); + } + + if (hTheme != nullptr) + { + ::DrawThemeTextEx(hTheme, hdc, HP_HEADERITEM, HIS_NORMAL, hdi.pszText, -1, dtFlags, &rcItem, &dtto); + } + else + { + ::DrawText(hdc, hdi.pszText, -1, &rcItem, dtFlags); + } +} + +/** + * @brief Custom paints a header control. + * + * Initializes variables for @ref paintHeaderItem. + * + * Paint logic: + * - Determines if the parent list view is in report mode and has gridlines. + * - Iterates over all header items + * + * @param[in] hWnd Handle to the header control. + * @param[in] hdc Device context to draw into. + * @param[in,out] headerData Reference to the header's theme, state, and style data. + * + * @see HeaderData + * @see paintHeaderItem() + */ +static void paintHeader(HWND hWnd, HDC hdc, dmlib_subclass::HeaderData& headerData) +{ + auto& themeData = headerData.m_themeData; + const auto& hTheme = themeData.getHTheme(); + const bool hasTheme = themeData.ensureTheme(hWnd); + auto& fontData = headerData.m_fontData; + + HWND hList = ::GetParent(hWnd); + + HBRUSH hBrush = dmlib::getHeaderBackgroundBrush(); + HPEN hPen = dmlib::getHeaderEdgePen(); + COLORREF clrText = dmlib::getHeaderTextColor(); + + const bool isDisabled = (::IsWindowEnabled(hWnd) == FALSE) + || (headerData.m_isLVChild && (::IsWindowEnabled(hList) == FALSE)); + + if (isDisabled) + { + hBrush = dmlib::getDlgBackgroundBrush(); + hPen = dmlib::getDisabledEdgePen(); + clrText = dmlib::getDisabledTextColor(); + } + + ::SetBkMode(hdc, TRANSPARENT); + const auto holdPen = dmlib_paint::GdiObject{ hdc, hPen, true }; + + RECT rcHeader{}; + ::GetClientRect(hWnd, &rcHeader); + ::FillRect(hdc, &rcHeader, hBrush); + + // Font part + + if (LOGFONT lf{}; + !fontData.hasFont() + && hasTheme + && SUCCEEDED(::GetThemeFont(hTheme, hdc, HP_HEADERITEM, HIS_NORMAL, TMT_FONT, &lf))) + { + fontData.setFont(::CreateFontIndirectW(&lf)); + } + + const auto holdFont = dmlib_paint::GdiObject{ + hdc, + (fontData.hasFont()) ? fontData.getFont() : reinterpret_cast(::SendMessage(hWnd, WM_GETFONT, 0, 0)), + true + }; + + DTTOPTS dtto{}; + dtto.dwSize = sizeof(DTTOPTS); + if (hasTheme) + { + dtto.dwFlags = DTT_TEXTCOLOR; + dtto.crText = clrText; + } + else + { + ::SetTextColor(hdc, clrText); + } + + // Special handling with gridlines + + bool hasGridlines = false; + if (headerData.m_isLVChild + && (::GetWindowLongPtr(hList, GWL_STYLE) & LVS_TYPEMASK) == LVS_REPORT) + { + const auto lvExStyle = ListView_GetExtendedListViewStyle(hList); + hasGridlines = (lvExStyle & LVS_EX_GRIDLINES) == LVS_EX_GRIDLINES; + } + + const auto count = Header_GetItemCount(hWnd); + RECT rcItem{}; + for (int i = 0; i < count; i++) + { + paintHeaderItem(hWnd, hdc, headerData, i, rcItem, hasGridlines, dtto); + } +} + +/** + * @brief Window subclass procedure for owner drawn header control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData HeaderData instance. + * @return LRESULT Result of message processing. + * + * @see dmlib::setHeaderCtrlSubclass() + * @see dmlib::removeHeaderCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::HeaderSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) +{ + auto* pHeaderData = reinterpret_cast(dwRefData); + auto& themeData = pHeaderData->m_themeData; + const auto& hMemDC = pHeaderData->m_bufferData.getHMemDC(); + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, HeaderSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pHeaderData); + u_ptrData.reset(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled() || !themeData.ensureTheme(hWnd)) + { + break; + } + + if (reinterpret_cast(wParam) != hMemDC) + { + return FALSE; + } + return TRUE; + } + + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + PAINTSTRUCT ps{}; + HDC hdc = ::BeginPaint(hWnd, &ps); + + if (!dmlib_paint::isRectValid(ps.rcPaint)) + { + ::EndPaint(hWnd, &ps); + return 0; + } + + dmlib_paint::PaintWithBuffer(*pHeaderData, hdc, ps, + [&]() { paintHeader(hWnd, hMemDC, *pHeaderData); }, + hWnd); + + ::EndPaint(hWnd, &ps); + return 0; + } + + case WM_DPICHANGED_AFTERPARENT: + { + themeData.closeTheme(); + return 0; + } + + case WM_THEMECHANGED: + { + themeData.closeTheme(); + break; + } + + case WM_LBUTTONDOWN: + { + if (!pHeaderData->m_hasBtnStyle) + { + break; + } + + pHeaderData->m_isPressed = true; + break; + } + + case WM_LBUTTONUP: + { + if (!pHeaderData->m_hasBtnStyle) + { + break; + } + + pHeaderData->m_isPressed = false; + break; + } + + case WM_MOUSEMOVE: + { + if (!pHeaderData->m_hasBtnStyle || pHeaderData->m_isPressed) + { + break; + } + + TRACKMOUSEEVENT tme{}; + + if (!pHeaderData->m_isHot) + { + tme.cbSize = sizeof(TRACKMOUSEEVENT); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = hWnd; + + ::TrackMouseEvent(&tme); + + pHeaderData->m_isHot = true; + } + + pHeaderData->m_pt.x = GET_X_LPARAM(lParam); + pHeaderData->m_pt.y = GET_Y_LPARAM(lParam); + + ::InvalidateRect(hWnd, nullptr, FALSE); + break; + } + + case WM_MOUSELEAVE: + { + if (!pHeaderData->m_hasBtnStyle) + { + break; + } + + const LRESULT retVal = ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + + pHeaderData->m_isHot = false; + pHeaderData->m_pt.x = LONG_MIN; + pHeaderData->m_pt.y = LONG_MIN; + + ::InvalidateRect(hWnd, nullptr, TRUE); + + return retVal; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Custom paints a status bar control. + * + * Draws the background, text, part separators, and optional size grip using + * custom brushes, pens, and fonts. Supports owner-drawn parts and adapts + * to the control's style flags and part configuration. + * + * @param[in] hWnd Handle to the status bar control. + * @param[in] hdc Device context to paint into. + * @param[in,out] statusBarData Reference to the control's theme, buffer, and font data. + * + * @see StatusBarData + */ +static void paintStatusBar(HWND hWnd, HDC hdc, dmlib_subclass::StatusBarData& statusBarData) +{ + struct + { + int : sizeof(int) * CHAR_BIT; // horizontal not used + int vertical = 0; + int between = 0; + } borders{}; + + ::SendMessage(hWnd, SB_GETBORDERS, 0, reinterpret_cast(&borders)); + + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + const bool hasSizeGrip = (nStyle & SBARS_SIZEGRIP) == SBARS_SIZEGRIP; + + const auto holdPen = dmlib_paint::GdiObject{ hdc, dmlib::getEdgePen(), true }; + const auto holdFont = dmlib_paint::GdiObject{ hdc, statusBarData.m_fontData.getFont(), true }; + + ::SetBkMode(hdc, TRANSPARENT); + ::SetTextColor(hdc, dmlib::getTextColor()); + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + ::FillRect(hdc, &rcClient, dmlib::getBackgroundBrush()); + + const auto nParts = static_cast(::SendMessage(hWnd, SB_GETPARTS, 0, 0)); + std::wstring str; + RECT rcPart{}; + RECT rcIntersect{}; + // no edge before size grip + const int iLastDiv = nParts - (hasSizeGrip ? 1 : 0); + // Don't draw edge if there is only one part without size grip. + const bool drawEdge = (nParts >= 2 || !hasSizeGrip); + for (int i = 0; i < nParts; ++i) + { + ::SendMessage(hWnd, SB_GETRECT, static_cast(i), reinterpret_cast(&rcPart)); + if (::IntersectRect(&rcIntersect, &rcPart, &rcClient) == FALSE) + { + continue; + } + + if (drawEdge && (i < iLastDiv)) + { + const std::array edges{ { + { rcPart.right - borders.between, rcPart.top + 1 }, + { rcPart.right - borders.between, rcPart.bottom - 3 } + } }; + ::Polyline(hdc, edges.data(), static_cast(edges.size())); + } + + rcPart.left += borders.between; + rcPart.right -= borders.vertical; + + const LRESULT retValLen = ::SendMessage(hWnd, SB_GETTEXTLENGTH, static_cast(i), 0); + const DWORD cchText = LOWORD(retValLen); + + str.resize(static_cast(cchText) + 1); + const LRESULT retValText = ::SendMessage(hWnd, SB_GETTEXT, static_cast(i), reinterpret_cast(str.data())); + + // With `SBT_OWNERDRAW` flag parent will draw status bar. + if (cchText == 0 && (HIWORD(retValLen) & SBT_OWNERDRAW) != 0) + { + const auto id = static_cast(::GetDlgCtrlID(hWnd)); + DRAWITEMSTRUCT dis{ + 0 + , 0 + , static_cast(i) + , ODA_DRAWENTIRE + , id + , hWnd + , hdc + , rcPart + , static_cast(retValText) + }; + + ::SendMessage(::GetParent(hWnd), WM_DRAWITEM, id, reinterpret_cast(&dis)); + } + else + { + ::DrawText(hdc, str.c_str(), -1, &rcPart, DT_SINGLELINE | DT_VCENTER | DT_LEFT); + } + } + +#if 0 // for horizontal edge + POINT edgeHor[]{ + {rcClient.left, rcClient.top}, + {rcClient.right, rcClient.top} + }; + Polyline(hdc, edgeHor, _countof(edgeHor)); +#endif + + // draw optional size grip + if (hasSizeGrip) + { + auto& themeData = statusBarData.m_themeData; + if (themeData.ensureTheme(hWnd)) + { + const auto& hTheme = themeData.getHTheme(); + SIZE szGrip{}; + ::GetThemePartSize(hTheme, hdc, SP_GRIPPER, 0, &rcClient, TS_DRAW, &szGrip); + RECT rcGrip{ rcClient }; + rcGrip.left = rcGrip.right - szGrip.cx; + rcGrip.top = rcGrip.bottom - szGrip.cy; + ::DrawThemeBackground(hTheme, hdc, SP_GRIPPER, 0, &rcGrip, nullptr); + } + } +} + +/** + * @brief Window subclass procedure for owner drawn status bar control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData StatusBarData instance. + * @return LRESULT Result of message processing. + * + * @see dmlib::setStatusBarCtrlSubclass() + * @see dmlib::removeStatusBarCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::StatusBarSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) +{ + auto* pStatusBarData = reinterpret_cast(dwRefData); + auto& themeData = pStatusBarData->m_themeData; + const auto& hMemDC = pStatusBarData->m_bufferData.getHMemDC(); + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, StatusBarSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pStatusBarData); + u_ptrData.reset(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled() || !themeData.ensureTheme(hWnd)) + { + break; + } + + if (reinterpret_cast(wParam) != hMemDC) + { + return FALSE; + } + return TRUE; + } + + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + PAINTSTRUCT ps{}; + HDC hdc = ::BeginPaint(hWnd, &ps); + + if (!dmlib_paint::isRectValid(ps.rcPaint)) + { + ::EndPaint(hWnd, &ps); + return 0; + } + + dmlib_paint::PaintWithBuffer(*pStatusBarData, hdc, ps, + [&]() { paintStatusBar(hWnd, hMemDC, *pStatusBarData); }, + hWnd); + + ::EndPaint(hWnd, &ps); + return 0; + } + + case WM_DPICHANGED_AFTERPARENT: + case WM_THEMECHANGED: + { + themeData.closeTheme(); + + const auto lf = LOGFONT{ dmlib_dpi::getSysFontForDpi(::GetParent(hWnd), dmlib_dpi::FontType::status) }; + pStatusBarData->m_fontData.setFont(::CreateFontIndirectW(&lf)); + + if (uMsg != WM_THEMECHANGED) + { + return 0; + } + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Calculates the filled and empty portions of a progress bar based on its current position. + * + * Retrieves the current progress position and range using `PBM_GETPOS` and `PBM_GETRANGE`, + * then computes two rectangles: + * - `rcFilled`: the portion of the progress bar that is filled. + * - `rcEmpty`: the remaining portion that is unfilled. + * + * The function modifies `rcEmpty->left` to avoid overpainting the filled area. + * + * @param[in] hWnd Handle to the progress bar control. + * @param[in] rcEmpty Pointer to the full client rectangle of the progress bar (in/out). + * @param[in] rcFilled Pointer to a rectangle that will receive the filled portion (out). + * + * @note This function assumes horizontal progress bars. + */ +static void getProgressBarRects(HWND hWnd, RECT* rcEmpty, RECT* rcFilled) noexcept +{ + const auto pos = static_cast(::SendMessage(hWnd, PBM_GETPOS, 0, 0)); + + PBRANGE range{}; + ::SendMessage(hWnd, PBM_GETRANGE, TRUE, reinterpret_cast(&range)); + const int iMin = range.iLow; + + const int currPos = pos - iMin; + if (currPos != 0) + { + const int totalWidth = rcEmpty->right - rcEmpty->left; + rcFilled->left = rcEmpty->left; + rcFilled->top = rcEmpty->top; + rcFilled->bottom = rcEmpty->bottom; + rcFilled->right = rcEmpty->left + static_cast(static_cast(currPos) / (range.iHigh - iMin) * totalWidth); + + rcEmpty->left = rcFilled->right; // to avoid painting under filled part + } +} + +/** + * @brief Custom paints a progress bar control with dark mode styling. + * + * Draws the progress bar frame, filled portion, and background using custom + * brushes and themed drawing. Uses the current progress state to determine the + * visual style (e.g., normal, paused, error). + * + * @param[in] hWnd Handle to the progress bar control. + * @param[in] hdc Device context to paint into. + * @param[in] progressBarData Reference to the control's theme and state data. + * + * @see ProgressBarData + * @see dmlib::getProgressBarRects() + */ +static void paintProgressBar(HWND hWnd, HDC hdc, const dmlib_subclass::ProgressBarData& progressBarData) noexcept +{ + const auto& hTheme = progressBarData.m_themeData.getHTheme(); + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + dmlib_paint::paintRoundFrameRect(hdc, rcClient, dmlib::getEdgePen(), 0, 0); + + ::InflateRect(&rcClient, -1, -1); + rcClient.left = 1; + + RECT rcFill{}; + getProgressBarRects(hWnd, &rcClient, &rcFill); + ::DrawThemeBackground(hTheme, hdc, PP_FILL, progressBarData.m_iStateID, &rcFill, nullptr); + ::FillRect(hdc, &rcClient, dmlib::getCtrlBackgroundBrush()); +} + +/** + * @brief Get progress bar state when handling `PBM_SETSTATE` message. + * + * @param[in] wParam State of the progress bar. + * @return int Fill state enum of the progress bar. + * + * @see dmlib_subclass::ProgressBarSubclass() + */ +[[nodiscard]] static int getProgressBarState(WPARAM wParam) noexcept +{ + switch (wParam) + { + case PBST_NORMAL: + { + return PBFS_NORMAL; // green + } + + case PBST_ERROR: + { + return PBFS_ERROR; // red + } + + case PBST_PAUSED: + { + return PBFS_PAUSED; // yellow + } + + default: + { + return PBFS_PARTIAL; // cyan + } + } +} + +/** + * @brief Window subclass procedure for owner drawn progress bar control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData ProgressBarData instance. + * @return LRESULT Result of message processing. + * + * @see dmlib::setProgressBarCtrlSubclass() + * @see dmlib::removeProgressBarCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::ProgressBarSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) noexcept +{ + auto* pProgressBarData = reinterpret_cast(dwRefData); + auto& themeData = pProgressBarData->m_themeData; + const auto& hMemDC = pProgressBarData->m_bufferData.getHMemDC(); + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, ProgressBarSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pProgressBarData); + u_ptrData.reset(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled() || !themeData.ensureTheme(hWnd)) + { + break; + } + + if (reinterpret_cast(wParam) != hMemDC) + { + return FALSE; + } + return TRUE; + } + + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + PAINTSTRUCT ps{}; + HDC hdc = ::BeginPaint(hWnd, &ps); + + if (!dmlib_paint::isRectValid(ps.rcPaint)) + { + ::EndPaint(hWnd, &ps); + return 0; + } + + dmlib_paint::PaintWithBuffer(*pProgressBarData, hdc, ps, + [&]() noexcept { paintProgressBar(hWnd, hMemDC, *pProgressBarData); }, + hWnd); + + ::EndPaint(hWnd, &ps); + return 0; + } + + case WM_DPICHANGED_AFTERPARENT: + { + themeData.closeTheme(); + return 0; + } + + case WM_THEMECHANGED: + { + themeData.closeTheme(); + break; + } + + case PBM_SETSTATE: + { + pProgressBarData->m_iStateID = getProgressBarState(wParam); + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Window subclass procedure for better disabled state appearence for static control with text. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData StaticTextData instance. + * @return LRESULT Result of message processing. + * + * @see dmlib::setStaticTextCtrlSubclass() + * @see dmlib::removeStaticTextCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::StaticTextSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) noexcept +{ + auto* pStaticTextData = reinterpret_cast(dwRefData); + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, StaticTextSubclass, uIdSubclass); + std::unique_ptr u_ptrData(pStaticTextData); + u_ptrData.reset(); + break; + } + + case WM_ENABLE: + { + pStaticTextData->m_isEnabled = (wParam == TRUE); + + const auto nStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE); + if (!pStaticTextData->m_isEnabled) + { + ::SetWindowLongPtr(hWnd, GWL_STYLE, nStyle & ~WS_DISABLED); + } + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + ::MapWindowPoints(hWnd, ::GetParent(hWnd), reinterpret_cast(&rcClient), 2); + ::RedrawWindow(::GetParent(hWnd), &rcClient, nullptr, RDW_INVALIDATE | RDW_UPDATENOW); + + if (!pStaticTextData->m_isEnabled) + { + ::SetWindowLongPtr(hWnd, GWL_STYLE, nStyle | WS_DISABLED); + } + + return 0; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Custom paints a IP address control. + * + * Draws the IP address background and dot separators. + * + * @param[in] hWnd Handle to the IP address control. + * @param[in] hdc Device context to paint into. + * + * @see dmlib_subclass::IPAddressSubclass() + */ +static void paintIPAddress(HWND hWnd, HDC hdc) noexcept +{ + const bool isEnabled = ::IsWindowEnabled(hWnd) == TRUE; + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + if (isEnabled) + { + ::FillRect(hdc, &rcClient, dmlib::getCtrlBackgroundBrush()); + ::SetTextColor(hdc, dmlib::getDarkerTextColor()); + ::SetBkColor(hdc, dmlib::getCtrlBackgroundColor()); + } + else + { + ::FillRect(hdc, &rcClient, dmlib::getDlgBackgroundBrush()); + ::SetTextColor(hdc, dmlib::getDisabledTextColor()); + ::SetBkColor(hdc, dmlib::getDlgBackgroundColor()); + } + + RECT rcDot{ rcClient }; + ::InflateRect(&rcDot, -1, 0); + const LONG wSection = ((rcDot.right - rcDot.left) / 4); + rcDot.right = rcDot.left + (2 * wSection); + ::OffsetRect(&rcDot, 0, -1); + + const auto holdFont = dmlib_paint::GdiObject{ hdc, reinterpret_cast(::SendMessage(hWnd, WM_GETFONT, 0, 0)), true }; + static constexpr UINT dtFlags = DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX; + + for (int i = 0; i < 3; ++i) + { + ::DrawText(hdc, L".", -1, &rcDot, dtFlags); + rcDot.left += wSection; + rcDot.right += wSection; + } +} + +/** + * @brief Window subclass procedure for custom color for IP address control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see dmlib_subclass::paintIPAddress() + * @see dmlib::setIPAddressCtrlSubclass() + * @see dmlib::removeIPAddressCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::IPAddressSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) noexcept +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, IPAddressSubclass, uIdSubclass); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled()) + { + break; + } + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + ::FillRect( + reinterpret_cast(wParam), + &rcClient, + (::IsWindowEnabled(hWnd) == TRUE) + ? dmlib::getCtrlBackgroundBrush() + : dmlib::getDlgBackgroundBrush()); + return TRUE; + } + + case WM_CTLCOLOREDIT: + { + if (!dmlib::isEnabled()) + { + break; + } + return dmlib::onCtlColorCtrl(reinterpret_cast(wParam)); + } + + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + PAINTSTRUCT ps{}; + HDC hdc = ::BeginPaint(hWnd, &ps); + + paintIPAddress(hWnd, hdc); + + ::EndPaint(hWnd, &ps); + return 0; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Window subclass procedure for custom color for hot key control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see dmlib::setHotKeyCtrlSubclass() + * @see dmlib::removeHotKeyCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::HotKeySubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) noexcept +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, HotKeySubclass, uIdSubclass); + dmlib_hook::unhookSysColor(); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled()) + { + break; + } + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + ::FillRect(reinterpret_cast(wParam), &rcClient, dmlib::getCtrlBackgroundBrush()); + return TRUE; + } + + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + dmlib_hook::hookSysColor(); + const LRESULT resVal = ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + dmlib_hook::unhookSysColor(); + return resVal; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Window subclass procedure for custom color for date time picker control. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see dmlib::setDTPCtrlSubclass() + * @see dmlib::removeDTPCtrlSubclass() + */ +LRESULT CALLBACK dmlib_subclass::DTPSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) noexcept +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, DTPSubclass, uIdSubclass); + dmlib_hook::unhookSysColor(); + break; + } + + case WM_PAINT: + { + if (!dmlib::isEnabled()) + { + break; + } + + dmlib_hook::hookSysColor(); + const LRESULT resVal = ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + dmlib_hook::unhookSysColor(); + return resVal; + } + + // for DTS_APPCANPARSE style + case WM_CTLCOLOREDIT: + { + if (!dmlib::isEnabled()) + { + break; + } + return dmlib::onCtlColorCtrl(reinterpret_cast(wParam)); + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} diff --git a/darkmodelib/src/DmlibSubclassControl.h b/darkmodelib/src/DmlibSubclassControl.h new file mode 100644 index 0000000000..7819f9a4db --- /dev/null +++ b/darkmodelib/src/DmlibSubclassControl.h @@ -0,0 +1,454 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +#include + +#include + +#include + +#include "DmlibDpi.h" +#include "DmlibPaintHelper.h" +#include "DmlibSubclass.h" +#include "DmlibWinApi.h" + +namespace dmlib +{ + /// Checks if current mode is dark type. + [[nodiscard]] bool isDarkDmTypeUsed() noexcept; +} // namespace dmlib + +namespace dmlib_subclass +{ + /** + * @struct ButtonData + * @brief Stores button theming state and original size metadata. + * + * Used for checkbox, radio, tri-state, or group box buttons. Used in conjunction + * with subclassing of button controls to preserve original layout dimensions + * and apply consistent visual styling. Captures the control's client size + * for checkbox, radio, or tri-state buttons. + * + * Members: + * - `m_themeData` : RAII-managed theme handle for `VSCLASS_BUTTON`. + * - `m_szBtn` : Original size extracted from the button rectangle. + * - `m_iStateID` : Current visual state ID (e.g. pressed, disabled, ...). + * - `m_isSizeSet` : Indicates whether `m_szBtn` holds a valid measurement. + * + * Constructor behavior: + * - When constructed with an `HWND`, attempts to extract the initial size if the button + * is a checkbox/radio/tri-state type without `BS_MULTILINE`. + * + * @see ThemeData + */ + struct ButtonData + { + ThemeData m_themeData{ VSCLASS_BUTTON }; + SIZE m_szBtn{}; + + int m_iStateID = 0; + bool m_isSizeSet = false; + + ButtonData() = default; + + // Saves width and height from the resource file for use as restrictions. + // Currently unused / have no effect. + explicit ButtonData(HWND hWnd) noexcept + { + const auto nBtnStyle = ::GetWindowLongPtrW(hWnd, GWL_STYLE); + switch (nBtnStyle & BS_TYPEMASK) + { + case BS_CHECKBOX: + case BS_AUTOCHECKBOX: + case BS_3STATE: + case BS_AUTO3STATE: + case BS_RADIOBUTTON: + case BS_AUTORADIOBUTTON: + { + if ((nBtnStyle & BS_MULTILINE) != BS_MULTILINE) + { + RECT rcBtn{}; + ::GetClientRect(hWnd, &rcBtn); + const UINT dpi = dmlib_dpi::GetDpiForParent(hWnd); + m_szBtn.cx = dmlib_dpi::unscale(rcBtn.right - rcBtn.left, dpi); + m_szBtn.cy = dmlib_dpi::unscale(rcBtn.bottom - rcBtn.top, dpi); + m_isSizeSet = (m_szBtn.cx != 0 && m_szBtn.cy != 0); + } + break; + } + + default: + { + break; + } + } + } + }; + + /** + * @struct UpDownData + * @brief Stores layout and state for a owner drawn up-down (spinner) control. + * + * Used to manage rectangle, buffer, and hit-test regions for owner-drawn subclassed + * up-down controls, supporting both vertical and horizontal layouts. + * + * Members: + * - `m_bufferData`: Buffer wrapper for flicker-free custom painting. + * - `m_rcClient`: Current client rectangle of the control. + * - `m_rcPrev`, `m_rcNext`: Rectangles for the up/down or left/right arrow buttons. + * - `m_cornerRoundness`: Optional roundness for corners (used in Windows 11+ with tabs). + * - `m_isHorizontal`: `true` if the control is horizontal (`UDS_HORZ` style). + * - `m_wasHotNext`: Last hover state (used for hover feedback). + * + * Constructor behavior: + * - Detects orientation from `GWL_STYLE`. + * - Initializes corner styling based on OS and parent class. + * - Extracts rectangles for arrow segments immediately. + * + * Usage: + * - `updateRect(HWND)`: Refreshes rectangle from control handle. + * - `updateRect(RECT)`: Checks for rectangle change and updates it. + * + * @see ThemeData + * @see BufferData + */ + struct UpDownData + { + ThemeData m_themeData{ VSCLASS_SPIN }; + BufferData m_bufferData; + + RECT m_rcClient{}; + RECT m_rcPrev{}; + RECT m_rcNext{}; + int m_cornerRoundness = 0; + int m_iStateIDPrev = UPS_NORMAL; + int m_iStateIDNext = UPS_NORMAL; + bool m_isHorizontal = false; + bool m_wasHotNext = false; + + static constexpr LONG kOffset = 2; + + UpDownData() = delete; + + explicit UpDownData(HWND hWnd) + : m_cornerRoundness( + (dmlib_win32api::IsWindows11() + && dmlib_subclass::cmpWndClassName(::GetParent(hWnd), WC_TABCONTROL)) + ? (dmlib_paint::kWin11CornerRoundness + 1) + : 0) + , m_isHorizontal((::GetWindowLongPtrW(hWnd, GWL_STYLE)& UDS_HORZ) == UDS_HORZ) + { + updateRect(hWnd); + } + + void updateRectUpDown() noexcept + { + if (m_isHorizontal) + { + const RECT rcArrowLeft{ + m_rcClient.left, m_rcClient.top, + m_rcClient.right - ((m_rcClient.right - m_rcClient.left) / 2), m_rcClient.bottom + }; + + const RECT rcArrowRight{ + rcArrowLeft.right, m_rcClient.top, + m_rcClient.right, m_rcClient.bottom + }; + + m_rcPrev = rcArrowLeft; + m_rcNext = rcArrowRight; + } + else + { + const RECT rcArrowTop{ + m_rcClient.left + kOffset, m_rcClient.top, + m_rcClient.right, m_rcClient.bottom - ((m_rcClient.bottom - m_rcClient.top) / 2) + }; + + const RECT rcArrowBottom{ + m_rcClient.left + kOffset, rcArrowTop.bottom, + m_rcClient.right, m_rcClient.bottom + }; + + m_rcPrev = rcArrowTop; + m_rcNext = rcArrowBottom; + } + } + + void updateRect(HWND hWnd) noexcept + { + ::GetClientRect(hWnd, &m_rcClient); + updateRectUpDown(); + } + + bool updateRect(RECT rcClientNew) noexcept + { + if (::EqualRect(&m_rcClient, &rcClientNew) == FALSE) + { + m_rcClient = rcClientNew; + updateRectUpDown(); + return true; + } + return false; + } + }; + + /** + * @struct TabData + * @brief Simple wrapper for `BufferData`. + * + * Members: + * - `m_bufferData` : Buffer wrapper for flicker-free custom painting. + * + * @see BufferData + */ + struct TabData + { + BufferData m_bufferData; + }; + + /** + * @struct BorderMetricsData + * @brief Stores system border and scroll bar metrics. + * + * Captures system metrics related to edit or list box control borders and scroll bars, + * along with the current DPI setting and a hot state flag. + * + * Members: + * - `m_dpi` : Current DPI value (defaults to `USER_DEFAULT_SCREEN_DPI`). + * - `m_xEdge` : Width of a border (`SM_CXEDGE`). + * - `m_yEdge` : Height of a border (`SM_CYEDGE`). + * - `m_xScroll` : Width of a vertical scroll bar (`SM_CXVSCROLL`). + * - `m_yScroll` : Height of a horizontal scroll bar (`SM_CYVSCROLL`). + * - `m_isHot` : Indicates whether the border is in a "hot" (hovered) state. + * + * @note Values are initialized from `GetSystemMetrics()` at construction time. + * Currently there is no dynamic handling for dpi changes. + */ + struct BorderMetricsData + { + UINT m_dpi = USER_DEFAULT_SCREEN_DPI; + LONG m_xEdge = ::GetSystemMetrics(SM_CXEDGE); + LONG m_yEdge = ::GetSystemMetrics(SM_CYEDGE); + LONG m_xScroll = ::GetSystemMetrics(SM_CXVSCROLL); + LONG m_yScroll = ::GetSystemMetrics(SM_CYVSCROLL); + bool m_isHot = false; + bool m_isEdit = false; + + BorderMetricsData() = delete; + + explicit BorderMetricsData(HWND hWnd) + : m_isEdit(dmlib_subclass::cmpWndClassName(hWnd, WC_EDIT)) + { + setMetricsForDpi(dmlib_dpi::GetDpiForParent(hWnd)); + } + + void setMetricsForDpi(UINT dpi) noexcept + { + m_dpi = dpi; + m_xEdge = dmlib_dpi::GetSystemMetricsForDpi(SM_CXEDGE, m_dpi); + m_yEdge = dmlib_dpi::GetSystemMetricsForDpi(SM_CYEDGE, m_dpi); + m_xScroll = dmlib_dpi::GetSystemMetricsForDpi(SM_CXVSCROLL, m_dpi); + m_yScroll = dmlib_dpi::GetSystemMetricsForDpi(SM_CYVSCROLL, m_dpi); + } + }; + + /** + * @struct ComboBoxData + * @brief Stores theme and buffer data for a combo box control, along with its style. + * + * Used to manage theming and double-buffered painting for combo box controls. + * Holds both the visual style information and the control's creation style for + * conditional drawing logic. + * + * Members: + * - `m_themeData` : RAII-managed theme handle for `VSCLASS_COMBOBOX`. + * - `m_bufferData` : Buffer wrapper for flicker-free custom painting. + * - `m_cbStyle` : Combo box style flags (`CBS_SIMPLE`, `CBS_DROPDOWN`, `CBS_DROPDOWNLIST`). + * - `m_iStateID` : Combo box state (normal, hot, disabled). + * + * Constructor behavior: + * - Deleted default constructor to enforce explicit style initialization. + * - Explicit constructor taking `cbStyle` to set `m_cbStyle`. + * + * @note The style value is typically retrieved via `GetWindowLongPtr(hWnd, GWL_STYLE)` + * when subclassing the combo box. + * + * @see ThemeData + * @see BufferData + */ + struct ComboBoxData + { + ThemeData m_themeData{ VSCLASS_COMBOBOX }; + BufferData m_bufferData; + + LONG_PTR m_cbStyle = CBS_SIMPLE; + int m_iStateID = CBXSR_NORMAL; + + ComboBoxData() = delete; + + explicit ComboBoxData(LONG_PTR cbStyle) noexcept + : m_cbStyle(cbStyle) + {} + }; + + /** + * @struct HeaderData + * @brief Stores theme, buffer, and font data for a header control, along with its style and state information. + * + * Used to manage theming and double-buffered painting for header controls. + * Holds the button visual style information and the control's state for + * conditional drawing logic. + * + * Members: + * - `m_themeData` : RAII-managed theme handle for `VSCLASS_HEADER`. + * - `m_bufferData` : Buffer wrapper for flicker-free custom painting. + * - `m_fontData` : Font resource wrapper for text drawing. + * - `m_pt` : Last known mouse position in client coordinates (LONG_MIN if uninitialized). + * - `m_isHot` : True if the mouse is currently over a header item. + * - `m_hasBtnStyle` : True if the header uses button-style items (`HDF_BUTTON`). + * - `m_isPressed` : True if a header item is currently pressed. + * + * Constructor behavior: + * - Deleted default constructor to enforce explicit initialization. + * - Explicit constructor taking `hasBtnStyle` to set `m_hasBtnStyle`. + * + * @see ThemeData + * @see BufferData + * @see FontData + */ + struct HeaderData + { + ThemeData m_themeData{ VSCLASS_HEADER }; + BufferData m_bufferData; + FontData m_fontData{ nullptr }; + + POINT m_pt{ LONG_MIN, LONG_MIN }; + bool m_isHot = false; + bool m_hasBtnStyle = true; + bool m_isPressed = false; + bool m_isLVChild = false; + + HeaderData() = delete; + + explicit HeaderData(HWND hWnd) + : m_hasBtnStyle((::GetWindowLongPtr(hWnd, GWL_STYLE) & HDS_BUTTONS) == HDS_BUTTONS) + , m_isLVChild(dmlib_subclass::cmpWndClassName(::GetParent(hWnd), WC_LISTVIEW)) + {} + }; + + /** + * @struct StatusBarData + * @brief Stores theme, buffer, and font data for a status bar control. + * + * Used to manage theming and double-buffered painting for status bar controls. + * + * Members: + * - `m_themeData` : RAII-managed theme handle for `VSCLASS_HEADER`. + * - `m_bufferData` : Buffer wrapper for flicker-free custom painting. + * - `m_fontData` : Font resource wrapper for text drawing. + * + * Constructor behavior: + * - Deleted default constructor to enforce explicit font initialization. + * - Explicit constructor taking `HFONT` to initialize `m_fontData`. + * + * @see ThemeData + * @see BufferData + * @see FontData + */ + struct StatusBarData + { + ThemeData m_themeData{ VSCLASS_STATUS }; + BufferData m_bufferData; + FontData m_fontData; + + StatusBarData() = delete; + + explicit StatusBarData(const HFONT& hFont) noexcept + : m_fontData(hFont) + {} + }; + + /** + * @struct ProgressBarData + * @brief Stores theme and buffer data for a progress bar control, along with its current state. + * + * Used to manage theming and double-buffered painting for progress bar controls. + * Captures the current visual state (normal, paused, error) via `PBM_GETSTATE`. + * + * Members: + * - `m_themeData` : RAII-managed theme handle for `VSCLASS_PROGRESS`. + * - `m_bufferData` : Buffer wrapper for flicker-free custom painting. + * - `m_iStateID` : Current progress bar state (e.g., `PBFS_NORMAL`, `PBFS_PAUSED`, `PBFS_ERROR`, `PBFS_PARTIAL`). + * + * Constructor behavior: + * - Initializes `m_iStateID` by querying the control with `PBM_GETSTATE`. + * + * @see ThemeData + * @see BufferData + */ + struct ProgressBarData + { + ThemeData m_themeData{ VSCLASS_PROGRESS }; + BufferData m_bufferData; + + int m_iStateID = PBFS_PARTIAL; + + explicit ProgressBarData(HWND hWnd) noexcept + : m_iStateID(static_cast(::SendMessage(hWnd, PBM_GETSTATE, 0, 0))) + {} + }; + + /** + * @struct StaticTextData + * @brief Stores enabled status information for a static text control. + * + * Used to determine whether a static control (e.g., label or caption) should be drawn + * using enabled or disabled colors. + * + * Members: + * - `m_isEnabled` : Indicates whether the control is currently enabled (`true`) or disabled (`false`). + * + * Constructor behavior: + * - Default constructor initializes `m_isEnabled` to `true`. + * - Explicit constructor queries the control's enabled state via `IsWindowEnabled(hWnd)`. + */ + struct StaticTextData + { + bool m_isEnabled = true; + + StaticTextData() = default; + + explicit StaticTextData(HWND hWnd) noexcept + : m_isEnabled(::IsWindowEnabled(hWnd) == TRUE) + {} + }; + + LRESULT CALLBACK ButtonSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK GroupboxSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK UpDownSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + LRESULT CALLBACK TabPaintSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK TabUpDownSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK CustomBorderSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + LRESULT CALLBACK ComboBoxSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK ComboBoxExSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + LRESULT CALLBACK ListViewSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + LRESULT CALLBACK HeaderSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK StatusBarSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK ProgressBarSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + LRESULT CALLBACK StaticTextSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + LRESULT CALLBACK IPAddressSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + LRESULT CALLBACK HotKeySubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + LRESULT CALLBACK DTPSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; +} // namespace dmlib_subclass diff --git a/darkmodelib/src/DmlibSubclassWindow.cpp b/darkmodelib/src/DmlibSubclassWindow.cpp new file mode 100644 index 0000000000..3a1c296804 --- /dev/null +++ b/darkmodelib/src/DmlibSubclassWindow.cpp @@ -0,0 +1,1656 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#include "StdAfx.h" + +#include "DmlibSubclassWindow.h" + +#include + +#include +#include +#include + +#include +#include +#include + +#include "Darkmodelib.h" + +#include "DmlibDpi.h" +#include "DmlibGlyph.h" +#include "DmlibPaintHelper.h" +#include "DmlibSubclass.h" +#include "DmlibSubclassControl.h" + +#include "UAHMenuBar.h" + +/** + * @brief Window subclass procedure for handling `WM_ERASEBKGND` message. + * + * Handles `WM_ERASEBKGND` to fill the window's client area with the custom color brush, + * preventing default light gray flicker or mismatched fill. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see dmlib::setWindowEraseBgSubclass() + * @see dmlib::removeWindowEraseBgSubclass() + */ +LRESULT CALLBACK dmlib_subclass::WindowEraseBgSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) noexcept +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, WindowEraseBgSubclass, uIdSubclass); + break; + } + + case WM_ERASEBKGND: + { + if (!dmlib::isEnabled()) + { + break; + } + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + ::FillRect(reinterpret_cast(wParam), &rcClient, dmlib::getBackgroundBrush()); + return TRUE; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Helper function to get correct colors depending on control's classname and state. + * + * @param[in] wParam Message-specific data to get HDC. + * @param[in] lParam Message-specific data to get child HWND. + * @return The brush handle as LRESULT for background painting. + */ +static LRESULT onCtlColorStaticHelper(LPARAM lParam, WPARAM wParam) +{ + auto* hdc = reinterpret_cast(wParam); + auto hChild = reinterpret_cast(lParam); + + const bool isChildEnabled = ::IsWindowEnabled(hChild) == TRUE; + const std::wstring className = dmlib_subclass::getWndClassName(hChild); + + if (className == WC_EDIT) + { + return isChildEnabled ? dmlib::onCtlColor(hdc) : dmlib::onCtlColorDlg(hdc); + } + + if (className == WC_LINK) + { + return dmlib::onCtlColorDlgLinkText(hdc, isChildEnabled); + } + + if (DWORD_PTR dwRefDataStaticText = 0; + ::GetWindowSubclass(hChild, dmlib_subclass::StaticTextSubclass, static_cast(dmlib_subclass::SubclassID::staticText), &dwRefDataStaticText) == TRUE) + { + const bool isTextEnabled = (reinterpret_cast(dwRefDataStaticText))->m_isEnabled; + return dmlib::onCtlColorDlgStaticText(hdc, isTextEnabled); + } + return dmlib::onCtlColorDlg(hdc); +} + +/** + * @brief Window subclass procedure for handling `WM_CTLCOLOR*` messages. + * + * Handles control drawing messages to apply foreground and background + * styling based on control type and class. + * + * Handles: + * - `WM_CTLCOLOREDIT`, `WM_CTLCOLORLISTBOX`, `WM_CTLCOLORDLG`, `WM_CTLCOLORSTATIC` + * - `WM_PRINTCLIENT` for removing light border for push buttons in dark mode + * + * Cleans up subclass on `WM_NCDESTROY` + * + * Uses `dmlib::onCtlColor*` utilities. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see dmlib::onCtlColor() + * @see dmlib::onCtlColorDlg() + * @see dmlib::onCtlColorDlgStaticText() + * @see dmlib::onCtlColorDlgLinkText() + * @see dmlib::onCtlColorListbox() + */ +LRESULT CALLBACK dmlib_subclass::WindowCtlColorSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, WindowCtlColorSubclass, uIdSubclass); + break; + } + + case WM_CTLCOLOREDIT: + { + if (!dmlib::isEnabled()) + { + break; + } + return dmlib::onCtlColorCtrl(reinterpret_cast(wParam)); + } + + case WM_CTLCOLORLISTBOX: + { + if (!dmlib::isEnabled()) + { + break; + } + return dmlib::onCtlColorListbox(wParam, lParam); + } + + case WM_CTLCOLORDLG: + { + + if (!dmlib::isEnabled()) + { + break; + } + return dmlib::onCtlColorDlg(reinterpret_cast(wParam)); + } + + case WM_CTLCOLORSTATIC: + { + if (!dmlib::isEnabled()) + { + break; + } + + return onCtlColorStaticHelper(lParam, wParam); + } + + case WM_PRINTCLIENT: + { + if (!dmlib::isEnabled()) + { + break; + } + return TRUE; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Applies custom drawing to a toolbar items (buttons) during `CDDS_ITEMPREPAINT` + * + * Handles color assignment and background painting for toolbar buttons during the + * `CDDS_ITEMPREPAINT` stage of `NMTBCUSTOMDRAW`. Applies appropriate brushes, pens, + * and background drawing depending on the button state: + * - **Hot**: Uses hot background and edge styling. + * - **Checked**: Uses control background and standard edge styling. + * - **Drop-down**: Calculates and paints iconic split-button drop arrow. + * + * Also configures transparency and color usage for text, hot-tracking, and background fills. + * Ensures hot/checked states are visually overridden by custom color highlights. + * + * @param[in,out] lptbcd Reference to the toolbar's custom draw structure. + * @return Flags to control draw behavior (`TBCDRF_USECDCOLORS`, `TBCDRF_NOBACKGROUND`, `CDRF_NOTIFYPOSTPAINT`). + * + * @note This function clears `CDIS_HOT`/`CDIS_CHECKED` to allow manual visual overrides. + * + * @see postpaintToolbarItem() + * @see darkToolbarNotifyCustomDraw() + */ +[[nodiscard]] static LRESULT prepaintToolbarItem(LPNMTBCUSTOMDRAW& lptbcd) noexcept +{ + // Set colors + + lptbcd->hbrMonoDither = dmlib::getBackgroundBrush(); + lptbcd->hbrLines = dmlib::getEdgeBrush(); + lptbcd->hpenLines = dmlib::getEdgePen(); + lptbcd->clrText = dmlib::getDarkerTextColor(); + lptbcd->clrTextHighlight = dmlib::getTextColor(); + lptbcd->clrBtnFace = dmlib::getBackgroundColor(); + lptbcd->clrBtnHighlight = dmlib::getCtrlBackgroundColor(); + lptbcd->clrHighlightHotTrack = dmlib::getHotBackgroundColor(); + lptbcd->nStringBkMode = TRANSPARENT; + lptbcd->nHLStringBkMode = TRANSPARENT; + + // Get styles and rectangles + + const bool isHot = (lptbcd->nmcd.uItemState & CDIS_HOT) == CDIS_HOT; + const bool isChecked = (lptbcd->nmcd.uItemState & CDIS_CHECKED) == CDIS_CHECKED; + + RECT rcItem{ lptbcd->nmcd.rc }; + RECT rcDrop{}; + + TBBUTTONINFOW tbi{}; + tbi.cbSize = sizeof(TBBUTTONINFOW); + tbi.dwMask = TBIF_IMAGE | TBIF_STYLE; + ::SendMessage(lptbcd->nmcd.hdr.hwndFrom, TB_GETBUTTONINFO, lptbcd->nmcd.dwItemSpec, reinterpret_cast(&tbi)); + + const bool isIcon = tbi.iImage != I_IMAGENONE; + const bool isDropDown = ((static_cast(tbi.fsStyle) & BTNS_DROPDOWN) == BTNS_DROPDOWN) && isIcon; // has 2 "buttons" + if (isDropDown) + { + const auto idx = ::SendMessage(lptbcd->nmcd.hdr.hwndFrom, TB_COMMANDTOINDEX, lptbcd->nmcd.dwItemSpec, 0); + ::SendMessage(lptbcd->nmcd.hdr.hwndFrom, TB_GETITEMDROPDOWNRECT, static_cast(idx), reinterpret_cast(&rcDrop)); + + rcItem.right = rcDrop.left; + } + + static const int roundness = dmlib::isAtLeastWindows11() ? dmlib_paint::kWin11CornerRoundness + 1 : 0; + + // Paint part + + if (isHot) // hot must have higher priority to overwrite checked state + { + if (!isIcon) + { + ::FillRect(lptbcd->nmcd.hdc, &rcItem, dmlib::getHotBackgroundBrush()); + } + else + { + dmlib_paint::paintRoundRect(lptbcd->nmcd.hdc, rcItem, dmlib::getHotEdgePen(), dmlib::getHotBackgroundBrush(), roundness, roundness); + if (isDropDown) + { + dmlib_paint::paintRoundRect(lptbcd->nmcd.hdc, rcDrop, dmlib::getHotEdgePen(), dmlib::getHotBackgroundBrush(), roundness, roundness); + } + } + + lptbcd->nmcd.uItemState &= ~static_cast(CDIS_CHECKED | CDIS_HOT); // clears states to use custom highlight + } + else if (isChecked) + { + if (!isIcon) + { + ::FillRect(lptbcd->nmcd.hdc, &rcItem, dmlib::getCtrlBackgroundBrush()); + } + else + { + dmlib_paint::paintRoundRect(lptbcd->nmcd.hdc, rcItem, dmlib::getEdgePen(), dmlib::getCtrlBackgroundBrush(), roundness, roundness); + if (isDropDown) + { + dmlib_paint::paintRoundRect(lptbcd->nmcd.hdc, rcDrop, dmlib::getEdgePen(), dmlib::getCtrlBackgroundBrush(), roundness, roundness); + } + } + + lptbcd->nmcd.uItemState &= ~static_cast(CDIS_CHECKED); // clears state to use custom highlight + } + + LRESULT retVal = TBCDRF_USECDCOLORS; + if ((lptbcd->nmcd.uItemState & CDIS_SELECTED) == CDIS_SELECTED) + { + retVal |= TBCDRF_NOBACKGROUND; + } + + if (isDropDown) + { + retVal |= CDRF_NOTIFYPOSTPAINT; + } + + return retVal; +} + +/** + * @brief Applies custom drawing to a toolbar items (buttons) during `CDDS_ITEMPOSTPAINT. + * + * Paints arrow glyph with custom color over system black "down triangle" for button with style `BTNS_DROPDOWN`. + * Triggered by `CDRF_NOTIFYPOSTPAINT` from @ref prepaintToolbarItem. + * + * Logic: + * - Retrieves the drop-down rectangle via `TB_GETITEMDROPDOWNRECT`. + * - Selects the toolbar font and draws a centered arrow glyph with custom text color. + * + * @param[in] lptbcd Reference to `LPNMTBCUSTOMDRAW`. + * @return `CDRF_DODEFAULT` to let default text/icon drawing proceed normally. + * + * @note Only applies to iconic buttons. + * + * @see prepaintToolbarItem() + * @see darkToolbarNotifyCustomDraw() + */ +[[nodiscard]] static LRESULT postpaintToolbarItem(const LPNMTBCUSTOMDRAW& lptbcd) noexcept +{ + TBBUTTONINFOW tbi{}; + tbi.cbSize = sizeof(TBBUTTONINFOW); + tbi.dwMask = TBIF_IMAGE; + ::SendMessage(lptbcd->nmcd.hdr.hwndFrom, TB_GETBUTTONINFO, lptbcd->nmcd.dwItemSpec, reinterpret_cast(&tbi)); + + if (tbi.iImage == I_IMAGENONE) + { + return CDRF_DODEFAULT; + } + + RECT rcArrow{}; + const auto idx = ::SendMessage(lptbcd->nmcd.hdr.hwndFrom, TB_COMMANDTOINDEX, lptbcd->nmcd.dwItemSpec, 0); + ::SendMessage(lptbcd->nmcd.hdr.hwndFrom, TB_GETITEMDROPDOWNRECT, static_cast(idx), reinterpret_cast(&rcArrow)); + rcArrow.left += 1; + rcArrow.bottom -= dmlib_dpi::scale(3, lptbcd->nmcd.hdr.hwndFrom); + + ::SetBkMode(lptbcd->nmcd.hdc, TRANSPARENT); + ::SetTextColor(lptbcd->nmcd.hdc, dmlib::getTextColor()); + + const auto hFont = dmlib_paint::GdiObject{ lptbcd->nmcd.hdc, lptbcd->nmcd.hdr.hwndFrom }; + static constexpr UINT dtFlags = DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOCLIP | DT_NOPREFIX; + ::DrawText(lptbcd->nmcd.hdc, dmlib_glyph::kTriangleDown, -1, &rcArrow, dtFlags); + + return CDRF_DODEFAULT; +} + +/** + * @brief Handles custom draw notifications for a toolbar control. + * + * Processes `NMTBCUSTOMDRAW` messages to provide custom color painting + * at each stage of the custom draw cycle: + * - **CDDS_PREPAINT**: Fills the toolbar background and requests item-level drawing. + * - **CDDS_ITEMPREPAINT**: Applies custom item painting via @ref prepaintToolbarItem. + * - **CDDS_ITEMPOSTPAINT**: Paints dropdown arrows glyphs via @ref postpaintToolbarItem. + * + * @param[in] hWnd Handle to the toolbar control. + * @param[in] uMsg Should be `WM_NOTIFY` with custom draw type (forwarded to default subclass processing). + * @param[in] wParam Message parameter (forwarded to default subclass processing). + * @param[in] lParam Pointer to `NMTBCUSTOMDRAW`. + * @return `LRESULT` containing draw flags or the result of default subclass processing. + * + * @see prepaintToolbarItem() + * @see postpaintToolbarItem() + */ +[[nodiscard]] static LRESULT darkToolbarNotifyCustomDraw( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam +) noexcept +{ + switch (auto* lptbcd = reinterpret_cast(lParam); + lptbcd->nmcd.dwDrawStage) + { + case CDDS_PREPAINT: + { + ::FillRect(lptbcd->nmcd.hdc, &lptbcd->nmcd.rc, dmlib::getBackgroundBrush()); + return CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYPOSTPAINT; + } + + case CDDS_ITEMPREPAINT: + { + return prepaintToolbarItem(lptbcd); + } + + case CDDS_ITEMPOSTPAINT: + { + return postpaintToolbarItem(lptbcd); + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Applies custom drawing to a list view item during `CDDS_ITEMPREPAINT`. + * + * Sets text/background colors and fills the item rectangle based on state and style. + * Handles list view custom colors and styles, and adapts to grid line configuration. + * + * Behavior: + * - **Selected**: Uses `dmlib::getCtrlBackground*()` colors and text brush. + * - **Hot**: Uses `dmlib::getHotBackground()` colors with optional hover frame. + * - **Gridlines active**: Fills the entire row background, column by column. + * + * @param[in,out] lplvcd Reference to `LPNMLVCUSTOMDRAW`. + * @param[in] isReport Whether list view is in `LVS_REPORT` mode. + * @param[in] hasGridLines Whether grid lines are enabled (`LVS_EX_GRIDLINES`). + * + * @see darkListViewNotifyCustomDraw() + */ +static void prepaintListViewItem(LPNMLVCUSTOMDRAW& lplvcd, bool isReport, bool hasGridLines) noexcept +{ + const auto& hList = lplvcd->nmcd.hdr.hwndFrom; + const bool isSelected = ListView_GetItemState(hList, lplvcd->nmcd.dwItemSpec, LVIS_SELECTED) == LVIS_SELECTED; + const bool isFocused = ListView_GetItemState(hList, lplvcd->nmcd.dwItemSpec, LVIS_FOCUSED) == LVIS_FOCUSED; + const bool isHot = (lplvcd->nmcd.uItemState & CDIS_HOT) == CDIS_HOT; + + HBRUSH hBrush = nullptr; + + if (isSelected) + { + lplvcd->clrText = dmlib::getTextColor(); + lplvcd->clrTextBk = dmlib::getCtrlBackgroundColor(); + hBrush = dmlib::getCtrlBackgroundBrush(); + } + else if (isHot) + { + lplvcd->clrText = dmlib::getTextColor(); + lplvcd->clrTextBk = dmlib::getHotBackgroundColor(); + hBrush = dmlib::getHotBackgroundBrush(); + } + + if (hBrush != nullptr) + { + if (!isReport || hasGridLines) + { + ::FillRect(lplvcd->nmcd.hdc, &lplvcd->nmcd.rc, hBrush); + } + else + { + auto* hHeader = ListView_GetHeader(hList); + const auto nCol = Header_GetItemCount(hHeader); + const LONG paddingLeft = dmlib::isThemeDark() ? 1 : 0; + const LONG paddingRight = dmlib::isThemeDark() ? 2 : 1; + + const auto lvii = LVITEMINDEX{ static_cast(lplvcd->nmcd.dwItemSpec), 0 }; + RECT rcSubitem{ + lplvcd->nmcd.rc.left + , lplvcd->nmcd.rc.top + , lplvcd->nmcd.rc.left + ListView_GetColumnWidth(hList, 0) - paddingRight + , lplvcd->nmcd.rc.bottom + }; + ::FillRect(lplvcd->nmcd.hdc, &rcSubitem, hBrush); + + for (int i = 1; i < nCol; ++i) + { +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" // cast from 'const type *' to 'type *' drops const +#endif + ListView_GetItemIndexRect(hList, &lvii, i, LVIR_BOUNDS, &rcSubitem); +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + rcSubitem.left -= paddingLeft; + rcSubitem.right -= paddingRight; + ::FillRect(lplvcd->nmcd.hdc, &rcSubitem, hBrush); + } + } + } + else if (hasGridLines) + { + ::FillRect(lplvcd->nmcd.hdc, &lplvcd->nmcd.rc, dmlib::getViewBackgroundBrush()); + } + + if (isFocused) + { +#if 0 // for testing + ::DrawFocusRect(lplvcd->nmcd.hdc, &lplvcd->nmcd.rc); +#endif + } + else if (!isSelected && isHot && !hasGridLines) + { + ::FrameRect(lplvcd->nmcd.hdc, &lplvcd->nmcd.rc, dmlib::getHotEdgeBrush()); + } +} + +/** + * @brief Handles custom draw notifications for a list view control. + * + * Processes `NMLVCUSTOMDRAW` messages to provide custom color painting + * at each stage of the custom draw cycle: + * - **CDDS_PREPAINT**: Optionally fills the list view with grid lines + * with custom background color and requests item-level drawing. + * - **CDDS_ITEMPREPAINT**: Applies custom item painting via @ref prepaintListViewItem. + * + * @param[in] hWnd Handle to the list view control. + * @param[in] uMsg Should be `WM_NOTIFY` with custom draw type (forwarded to default subclass processing). + * @param[in] wParam Message parameter (forwarded to default subclass processing). + * @param[in] lParam Pointer to `NMLVCUSTOMDRAW`. + * @return `LRESULT` containing draw flags or the result of default subclass processing. + * + * @see prepaintListViewItem() + */ +[[nodiscard]] static LRESULT darkListViewNotifyCustomDraw( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam +) noexcept +{ + auto* lplvcd = reinterpret_cast(lParam); + const auto& hList = lplvcd->nmcd.hdr.hwndFrom; + const bool isDisabled = ::IsWindowEnabled(hList) == FALSE; + // makes sense only in enabled state + const bool isReport = !isDisabled && ((::GetWindowLongPtr(hList, GWL_STYLE) & LVS_TYPEMASK) == LVS_REPORT); + // makes sense only if list view has LVS_REPORT style + const bool hasGridlines = isReport && ((ListView_GetExtendedListViewStyle(hList) & LVS_EX_GRIDLINES) == LVS_EX_GRIDLINES); + + switch (lplvcd->nmcd.dwDrawStage) + { + case CDDS_PREPAINT: + { + const auto hBrush = [&isDisabled, &hasGridlines]() -> HBRUSH + { + if (isDisabled) + { + return dmlib::getDlgBackgroundBrush(); + } + + if (hasGridlines) + { + return dmlib::getViewBackgroundBrush(); + } + return nullptr; + }(); + + if (hBrush != nullptr) + { + ::FillRect(lplvcd->nmcd.hdc, &lplvcd->nmcd.rc, hBrush); + } + + return CDRF_NOTIFYITEMDRAW; + } + + case CDDS_ITEMPREPAINT: + { + if (isDisabled) + { + lplvcd->clrFace = dmlib::getDlgBackgroundColor(); + lplvcd->clrText = dmlib::getDisabledTextColor(); + lplvcd->clrTextBk = dmlib::getDlgBackgroundColor(); + } + else + { + prepaintListViewItem(lplvcd, isReport, hasGridlines); + } + + return CDRF_NEWFONT; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Applies custom drawing to a tree view node during `CDDS_ITEMPREPAINT`. + * + * Colors the node background for selection/hot states, assigns text color, + * and requests optional post-paint framing. + * + * @param[in,out] lptvcd Reference to `LPNMTVCUSTOMDRAW`. + * @return Bitmask with `CDRF_NEWFONT`, `CDRF_NOTIFYPOSTPAINT` if drawing was applied. + * + * @see postpaintTreeViewItem() + * @see darkTreeViewNotifyCustomDraw() + */ +[[nodiscard]] static LRESULT prepaintTreeViewItem(LPNMTVCUSTOMDRAW& lptvcd) noexcept +{ + LRESULT retVal = CDRF_DODEFAULT; + + if ((lptvcd->nmcd.uItemState & CDIS_SELECTED) == CDIS_SELECTED) + { + lptvcd->clrText = dmlib::getTextColor(); + lptvcd->clrTextBk = dmlib::getCtrlBackgroundColor(); + ::FillRect(lptvcd->nmcd.hdc, &lptvcd->nmcd.rc, dmlib::getCtrlBackgroundBrush()); + + retVal |= CDRF_NEWFONT | CDRF_NOTIFYPOSTPAINT; + } + else if ((lptvcd->nmcd.uItemState & CDIS_HOT) == CDIS_HOT) + { + lptvcd->clrText = dmlib::getTextColor(); + lptvcd->clrTextBk = dmlib::getHotBackgroundColor(); + + if (dmlib::isAtLeastWindows10() + || static_cast(dmlib::getTreeViewStyle()) == dmlib::TreeViewStyle::light) + { + ::FillRect(lptvcd->nmcd.hdc, &lptvcd->nmcd.rc, dmlib::getHotBackgroundBrush()); + retVal |= CDRF_NOTIFYPOSTPAINT; + } + retVal |= CDRF_NEWFONT; + } + + return retVal; +} + +/** + * @brief Applies custom drawing to a tree view node during `CDDS_ITEMPOSTPAINT`. + * + * Paints a frame around a tree view node after painting based on state. + * + * @param[in] lptvcd Reference to `LPNMTVCUSTOMDRAW`. + * + * @see prepaintTreeViewItem() + * @see darkTreeViewNotifyCustomDraw() + */ +static void postpaintTreeViewItem(const LPNMTVCUSTOMDRAW& lptvcd) noexcept +{ + RECT rcFrame{ lptvcd->nmcd.rc }; + ::InflateRect(&rcFrame, 1, 0); + + if ((lptvcd->nmcd.uItemState & CDIS_HOT) == CDIS_HOT) + { + dmlib_paint::paintRoundFrameRect(lptvcd->nmcd.hdc, rcFrame, dmlib::getHotEdgePen(), 0, 0); + } + else if ((lptvcd->nmcd.uItemState & CDIS_SELECTED) == CDIS_SELECTED) + { + dmlib_paint::paintRoundFrameRect(lptvcd->nmcd.hdc, rcFrame, dmlib::getEdgePen(), 0, 0); + } +} + +/** + * @brief Handles custom draw notifications for a tree view control. + * + * Processes `NMTVCUSTOMDRAW` messages to provide custom color painting + * at each stage of the custom draw cycle: + * - **CDDS_PREPAINT**: Requests item-level drawing. + * - **CDDS_ITEMPREPAINT**: Applies custom item painting based on state via @ref prepaintTreeViewItem. + * - **CDDS_ITEMPOSTPAINT**: Paints frames based on state via @ref postpaintTreeViewItem. + * + * @param[in] hWnd Handle to the tree view control. + * @param[in] uMsg Should be `WM_NOTIFY` with custom draw type (forwarded to default subclass processing). + * @param[in] wParam Message parameter (forwarded to default subclass processing). + * @param[in] lParam Pointer to `NMTVCUSTOMDRAW`. + * @return `LRESULT` containing draw flags or the result of default subclass processing. + * + * @see prepaintTreeViewItem() + * @see postpaintTreeViewItem() + */ +[[nodiscard]] static LRESULT darkTreeViewNotifyCustomDraw( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam +) noexcept +{ + switch (auto* lptvcd = reinterpret_cast(lParam); + lptvcd->nmcd.dwDrawStage) + { + case CDDS_PREPAINT: + { + return CDRF_NOTIFYITEMDRAW; + } + + case CDDS_ITEMPREPAINT: + { + const LRESULT retVal = prepaintTreeViewItem(lptvcd); + if (retVal == CDRF_DODEFAULT) + { + break; + } + return retVal; + } + + case CDDS_ITEMPOSTPAINT: + { + postpaintTreeViewItem(lptvcd); + return CDRF_DODEFAULT; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Applies custom drawing to a trackbar items during `CDDS_ITEMPREPAINT`. + * + * Colors the trackbar thumb background for selection state, + * and colors the trackbar slider based on if tracbar is enabled. + * For trackbar with style `TBS_AUTOTICKS` default handling is used. + * + * @param[in] lpnmcd Reference to `LPNMCUSTOMDRAW`. + * @return `CDRF_SKIPDEFAULT` if drawing was applied. + * + * @see darkTrackbarNotifyCustomDraw() + */ +[[nodiscard]] static LRESULT prepaintTrackbarItem(const LPNMCUSTOMDRAW& lpnmcd) noexcept +{ + LRESULT retVal = CDRF_DODEFAULT; + + switch (lpnmcd->dwItemSpec) + { + case TBCD_TICS: + { + break; + } + + case TBCD_THUMB: + { + if ((lpnmcd->uItemState & CDIS_SELECTED) == CDIS_SELECTED) + { + ::FillRect(lpnmcd->hdc, &lpnmcd->rc, dmlib::getCtrlBackgroundBrush()); + retVal = CDRF_SKIPDEFAULT; + } + break; + } + + case TBCD_CHANNEL: // slider + { + if (::IsWindowEnabled(lpnmcd->hdr.hwndFrom) == FALSE) + { + ::FillRect(lpnmcd->hdc, &lpnmcd->rc, dmlib::getBackgroundBrush()); + dmlib_paint::paintRoundFrameRect(lpnmcd->hdc, lpnmcd->rc, dmlib::getEdgePen(), 0, 0); + } + else + { + ::FillRect(lpnmcd->hdc, &lpnmcd->rc, dmlib::getCtrlBackgroundBrush()); + } + + retVal = CDRF_SKIPDEFAULT; + break; + } + + default: + { + break; + } + } + + return retVal; +} + +/** + * @brief Handles custom draw notifications for a trackbar control. + * + * Processes `NMCUSTOMDRAW` messages to provide custom color painting + * at each stage of the custom draw cycle: + * - **CDDS_PREPAINT**: Requests item-level drawing. + * - **CDDS_ITEMPREPAINT**: Applies custom item painting based on item type via @ref prepaintTrackbarItem. + * + * @param[in] hWnd Handle to the trackbar control. + * @param[in] uMsg Should be `WM_NOTIFY` with custom draw type (forwarded to default subclass processing). + * @param[in] wParam Message parameter (forwarded to default subclass processing). + * @param[in] lParam Pointer to `NMCUSTOMDRAW`. + * @return `LRESULT` containing draw flags or the result of default subclass processing. + * + * @see prepaintTrackbarItem() + */ +[[nodiscard]] static LRESULT darkTrackbarNotifyCustomDraw( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam +) noexcept +{ + switch (auto* lpnmcd = reinterpret_cast(lParam); + lpnmcd->dwDrawStage) + { + case CDDS_PREPAINT: + { + return CDRF_NOTIFYITEMDRAW; + } + + case CDDS_ITEMPREPAINT: + { + const LRESULT retVal = prepaintTrackbarItem(lpnmcd); + if (retVal == CDRF_DODEFAULT) + { + break; + } + return retVal; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Applies custom drawing to a rebar control during `CDDS_PREPAINT`. + * + * Paints chevrons and 'gripper' edges for all bands if applicable. + * + * @param[in] lpnmcd Reference to `LPNMCUSTOMDRAW`. + * @return `CDRF_SKIPDEFAULT` if drawing was applied. + * + * @see darkRebarNotifyCustomDraw() + */ +[[nodiscard]] static LRESULT prepaintRebar(const LPNMCUSTOMDRAW& lpnmcd) noexcept +{ + ::FillRect(lpnmcd->hdc, &lpnmcd->rc, dmlib::getDlgBackgroundBrush()); + + REBARBANDINFO rbBand{}; + rbBand.cbSize = sizeof(REBARBANDINFO); + rbBand.fMask = RBBIM_STYLE | RBBIM_CHEVRONLOCATION | RBBIM_CHEVRONSTATE; + + const auto nBands = static_cast(::SendMessage(lpnmcd->hdr.hwndFrom, RB_GETBANDCOUNT, 0, 0)); + for (UINT i = 0; i < nBands; ++i) + { + ::SendMessage(lpnmcd->hdr.hwndFrom, RB_GETBANDINFO, static_cast(i), reinterpret_cast(&rbBand)); + + // paints chevron + if ((rbBand.fStyle & RBBS_USECHEVRON) == RBBS_USECHEVRON + && (rbBand.rcChevronLocation.right - rbBand.rcChevronLocation.left) > 0) + { + static const int roundness = dmlib::isAtLeastWindows11() ? dmlib_paint::kWin11CornerRoundness + 1 : 0; + + const bool isHot = (rbBand.uChevronState & STATE_SYSTEM_HOTTRACKED) == STATE_SYSTEM_HOTTRACKED; + const bool isPressed = (rbBand.uChevronState & STATE_SYSTEM_PRESSED) == STATE_SYSTEM_PRESSED; + + if (isHot) + { + dmlib_paint::paintRoundRect(lpnmcd->hdc, rbBand.rcChevronLocation, dmlib::getHotEdgePen(), dmlib::getHotBackgroundBrush(), roundness, roundness); + } + else if (isPressed) + { + dmlib_paint::paintRoundRect(lpnmcd->hdc, rbBand.rcChevronLocation, dmlib::getEdgePen(), dmlib::getCtrlBackgroundBrush(), roundness, roundness); + } + + ::SetTextColor(lpnmcd->hdc, isHot ? dmlib::getTextColor() : dmlib::getDarkerTextColor()); + ::SetBkMode(lpnmcd->hdc, TRANSPARENT); + + const auto hFont = dmlib_paint::GdiObject{ lpnmcd->hdc, lpnmcd->hdr.hwndFrom }; + static constexpr UINT dtFlags = DT_CENTER | DT_TOP | DT_SINGLELINE | DT_NOCLIP | DT_NOPREFIX; + ::DrawText(lpnmcd->hdc, dmlib_glyph::kChevron, -1, &rbBand.rcChevronLocation, dtFlags); + } + + // paints gripper edge + if ((rbBand.fStyle & RBBS_GRIPPERALWAYS) == RBBS_GRIPPERALWAYS + && ((rbBand.fStyle & RBBS_FIXEDSIZE) != RBBS_FIXEDSIZE + || (rbBand.fStyle & RBBS_NOGRIPPER) != RBBS_NOGRIPPER)) + { + auto holdPen = static_cast(::SelectObject(lpnmcd->hdc, dmlib::getDarkerTextPen())); + + RECT rcBand{}; + ::SendMessage(lpnmcd->hdr.hwndFrom, RB_GETRECT, static_cast(i), reinterpret_cast(&rcBand)); + + static constexpr LONG offset = 5; + const std::array edges{ { + { rcBand.left, rcBand.top + offset}, + { rcBand.left, rcBand.bottom - offset} + } }; + ::Polyline(lpnmcd->hdc, edges.data(), static_cast(edges.size())); + + ::SelectObject(lpnmcd->hdc, holdPen); + } + } + return CDRF_SKIPDEFAULT; +} + +/** + * @brief Handles custom draw notifications for a rebar control. + * + * Processes `NMCUSTOMDRAW` messages to provide custom color painting + * at each stage of the custom draw cycle: + * - **CDDS_PREPAINT**: Applies custom painting based on item type via @ref prepaintRebar. + * + * @param[in] hWnd Handle to the rebar control. + * @param[in] uMsg Should be `WM_NOTIFY` with custom draw type (forwarded to default subclass processing). + * @param[in] wParam Message parameter (forwarded to default subclass processing). + * @param[in] lParam Pointer to `NMCUSTOMDRAW`. + * @return `LRESULT` containing draw flags or the result of default subclass processing. + * + * @see prepaintRebar() + */ +[[nodiscard]] static LRESULT darkRebarNotifyCustomDraw( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam +) noexcept +{ + if (auto* lpnmcd = reinterpret_cast(lParam); + lpnmcd->dwDrawStage == CDDS_PREPAINT) + { + return prepaintRebar(lpnmcd); + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Helper for handling `NM_CUSTOMDRAW` or `DTN_DROPDOWN` notification code. + * + * Handles `NM_CUSTOMDRAW` for custom draw for supported controls: + * - toolbar, list view, tree view, trackbar, and rebar. + * Handles `DTN_DROPDOWN` for date time picker control. + * + * @param[in] hWnd Window handle for specific control. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @return LRESULT Result of message processing. + * + * @see dmlib_subclass::WindowNotifySubclass() + */ +static LRESULT onNotifyCustomDrawOrDTPDropDown( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam +) +{ + auto* lpnmhdr = reinterpret_cast(lParam); + if (lpnmhdr->code == NM_CUSTOMDRAW) + { + const std::wstring className = dmlib_subclass::getWndClassName(lpnmhdr->hwndFrom); + + if (className == TOOLBARCLASSNAME) + { + return darkToolbarNotifyCustomDraw(hWnd, uMsg, wParam, lParam); + } + + if (className == WC_LISTVIEW) + { + return darkListViewNotifyCustomDraw(hWnd, uMsg, wParam, lParam); + } + + if (className == WC_TREEVIEW) + { + return darkTreeViewNotifyCustomDraw(hWnd, uMsg, wParam, lParam); + } + + if (className == TRACKBAR_CLASS) + { + return darkTrackbarNotifyCustomDraw(hWnd, uMsg, wParam, lParam); + } + + if (className == REBARCLASSNAME) + { + return darkRebarNotifyCustomDraw(hWnd, uMsg, wParam, lParam); + } + } + else if (lpnmhdr->code == DTN_DROPDOWN + && dmlib_subclass::cmpWndClassName(lpnmhdr->hwndFrom, DATETIMEPICK_CLASS)) + { + HWND hCal = DateTime_GetMonthCal(lpnmhdr->hwndFrom); + dmlib::setDarkMonthCalendar(hCal); + + // Drop down container needs resizing to not clip month calendar + { + const UINT dpi = dmlib_dpi::GetDpiForWindow(hWnd); + + RECT rcIdeal{}; + MonthCal_GetMinReqRect(hCal, &rcIdeal); + // border + padding + const auto borderSize = MonthCal_GetCalendarBorder(hCal) + dmlib_dpi::scale(8, dpi); + HWND hCalContainer = ::GetParent(hCal); + const auto nStyle = static_cast(::GetWindowLongPtr(hCalContainer, GWL_STYLE)); + const auto nExStyle = static_cast(::GetWindowLongPtr(hCalContainer, GWL_EXSTYLE)); + dmlib_dpi::AdjustWindowRectExForDpi(&rcIdeal, nStyle, FALSE, nExStyle, dpi); + ::SetWindowPos(hCalContainer, nullptr, 0, 0, + rcIdeal.right - rcIdeal.left + borderSize, + rcIdeal.bottom - rcIdeal.top + borderSize, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER); + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Window subclass procedure for handling `WM_NOTIFY` message for custom draw for supported controls. + * + * Handles `WM_NOTIFY` for custom draw for supported controls: + * - toolbar, list view, tree view, trackbar, and rebar. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see onNotifyCustomDrawOrDTPDropDown() + * @see dmlib::setWindowNotifyCustomDrawSubclass() + * @see dmlib::removeWindowNotifyCustomDrawSubclass() + */ +LRESULT CALLBACK dmlib_subclass::WindowNotifySubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, WindowNotifySubclass, uIdSubclass); + break; + } + + case WM_NOTIFY: + { + if (!dmlib::isEnabled()) + { + break; + } + + return onNotifyCustomDrawOrDTPDropDown(hWnd, uMsg, wParam, lParam); + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + + +/** + * @brief Fills the menu bar background custom color. + * + * Uses `GetMenuBarInfo` and `GetWindowRect` to compute the menu bar rectangle + * in client-relative coordinates, then fills it with @ref dmlib::getDlgBackgroundBrush. + * + * @param[in] hWnd Handle to the window with a menu bar. + * @param[in] hdc Target device context for painting. + * + * @note Offsets top slightly to account for non-client overlap. + * + * @see dmlib_subclass::WindowMenuBarSubclass() + */ +static void paintMenuBar(HWND hWnd, HDC hdc) noexcept +{ + // get the menubar rect + MENUBARINFO mbi{}; + mbi.cbSize = sizeof(MENUBARINFO); + ::GetMenuBarInfo(hWnd, OBJID_MENU, 0, &mbi); + + RECT rcWindow{}; + ::GetWindowRect(hWnd, &rcWindow); + + // the rcBar is offset by the window rect + RECT rcBar{ mbi.rcBar }; + ::OffsetRect(&rcBar, -rcWindow.left, -rcWindow.top); + + rcBar.top -= 1; + + ::FillRect(hdc, &rcBar, dmlib::getBackgroundBrush()); +} + +/** + * @brief Paints a single menu bar item with custom colors based on state. + * + * Measures and draws menu item text using `DrawThemeTextEx`, and + * fills background using appropriate brush based on `ODS_*` item state. + * + * @param[in,out] UDMI Reference to `UAHDRAWMENUITEM` struct from `WM_UAHDRAWMENUITEM`. + * @param[in] hTheme The themed handle to `VSCLASS_MENU` (via @ref ThemeData). + * + * @see dmlib_subclass::WindowMenuBarSubclass() + */ +static void paintMenuBarItems(UAHDRAWMENUITEM& UDMI, const HTHEME& hTheme) +{ + // get the menu item string + auto buffer = std::wstring(MAX_PATH, L'\0'); + MENUITEMINFO mii{}; + mii.cbSize = sizeof(MENUITEMINFO); + mii.fMask = MIIM_STRING; + mii.dwTypeData = buffer.data(); + mii.cch = MAX_PATH - 1; + + ::GetMenuItemInfoW(UDMI.um.hmenu, static_cast(UDMI.umi.iPosition), TRUE, &mii); + + // get the item state for drawing + + DWORD dwFlags = DT_CENTER | DT_SINGLELINE | DT_VCENTER; + + int iTextStateID = MBI_NORMAL; + int iBackgroundStateID = MBI_NORMAL; + if ((UDMI.dis.itemState & ODS_SELECTED) == ODS_SELECTED) + { + // clicked + iTextStateID = MBI_PUSHED; + iBackgroundStateID = MBI_PUSHED; + } + else if ((UDMI.dis.itemState & ODS_HOTLIGHT) == ODS_HOTLIGHT) + { + // hot tracking + iTextStateID = ((UDMI.dis.itemState & ODS_INACTIVE) == ODS_INACTIVE) ? MBI_DISABLEDHOT : MBI_HOT; + iBackgroundStateID = MBI_HOT; + } + else if (((UDMI.dis.itemState & ODS_GRAYED) == ODS_GRAYED) + || ((UDMI.dis.itemState & ODS_DISABLED) == ODS_DISABLED) + || ((UDMI.dis.itemState & ODS_INACTIVE) == ODS_INACTIVE)) + { + // disabled / grey text / inactive + iTextStateID = MBI_DISABLED; + iBackgroundStateID = MBI_DISABLED; + } + else if ((UDMI.dis.itemState & ODS_DEFAULT) == ODS_DEFAULT) + { + // normal display + iTextStateID = MBI_NORMAL; + iBackgroundStateID = MBI_NORMAL; + } + + if ((UDMI.dis.itemState & ODS_NOACCEL) == ODS_NOACCEL) + { + dwFlags |= DT_HIDEPREFIX; + } + + switch (iBackgroundStateID) + { + case MBI_NORMAL: + case MBI_DISABLED: + { + ::FillRect(UDMI.um.hdc, &UDMI.dis.rcItem, dmlib::getBackgroundBrush()); + break; + } + + case MBI_HOT: + case MBI_DISABLEDHOT: + { + ::FillRect(UDMI.um.hdc, &UDMI.dis.rcItem, dmlib::getHotBackgroundBrush()); + break; + } + + case MBI_PUSHED: + case MBI_DISABLEDPUSHED: + { + ::FillRect(UDMI.um.hdc, &UDMI.dis.rcItem, dmlib::getCtrlBackgroundBrush()); + break; + } + + default: + { + ::DrawThemeBackground(hTheme, UDMI.um.hdc, MENU_BARITEM, iBackgroundStateID, &UDMI.dis.rcItem, nullptr); + break; + } + } + + DTTOPTS dttopts{}; + dttopts.dwSize = sizeof(DTTOPTS); + dttopts.dwFlags = DTT_TEXTCOLOR; + switch (iTextStateID) + { + case MBI_NORMAL: + case MBI_HOT: + case MBI_PUSHED: + { + dttopts.crText = dmlib::getTextColor(); + break; + } + + case MBI_DISABLED: + case MBI_DISABLEDHOT: + case MBI_DISABLEDPUSHED: + { + dttopts.crText = dmlib::getDisabledTextColor(); + break; + } + + default: + { + break; + } + } + + ::DrawThemeTextEx(hTheme, UDMI.um.hdc, MENU_BARITEM, iTextStateID, buffer.c_str(), static_cast(mii.cch), dwFlags, &UDMI.dis.rcItem, &dttopts); +} + +/** + * @brief Over-paints the 1-pixel light line under a menu bar with custom color. + * + * Called post-paint to overwrite non-client leftovers that break custom color styling. + * Computes exact line position based on `MenuBarInfo`, and fills with custom color. + * + * @param[in] hWnd Handle to the window with a menu bar. + * + * @see dmlib_subclass::WindowMenuBarSubclass() + */ +static void drawUAHMenuNCBottomLine(HWND hWnd) noexcept +{ + MENUBARINFO mbi{}; + mbi.cbSize = sizeof(MENUBARINFO); + if (::GetMenuBarInfo(hWnd, OBJID_MENU, 0, &mbi) == FALSE) + { + return; + } + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + ::MapWindowPoints(hWnd, nullptr, reinterpret_cast(&rcClient), 2); + + RECT rcWindow{}; + ::GetWindowRect(hWnd, &rcWindow); + + ::OffsetRect(&rcClient, -rcWindow.left, -rcWindow.top); + + // the rcBar is offset by the window rect + RECT rcAnnoyingLine{ rcClient }; + rcAnnoyingLine.bottom = rcAnnoyingLine.top; + rcAnnoyingLine.top--; + + + HDC hdc = ::GetWindowDC(hWnd); + ::FillRect(hdc, &rcAnnoyingLine, dmlib::getBackgroundBrush()); + ::ReleaseDC(hWnd, hdc); +} + +/** + * @brief Window subclass procedure for custom color for themed menu bar. + * + * Applies custom colors for menu bar, but not for popup menus. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData ThemeData instance. + * @return LRESULT Result of message processing. + * + * @see dmlib::setWindowMenuBarSubclass() + * @see dmlib::removeWindowMenuBarSubclass() + */ +LRESULT CALLBACK dmlib_subclass::WindowMenuBarSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) +{ + auto* pMenuThemeData = reinterpret_cast(dwRefData); + + if (uMsg != WM_NCDESTROY && (!dmlib::isEnabled() || !pMenuThemeData->ensureTheme(hWnd))) + { + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + } + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, WindowMenuBarSubclass, uIdSubclass); + const std::unique_ptr ptrData(pMenuThemeData); + break; + } + + case WM_UAHDRAWMENU: + { + auto* pUDM = reinterpret_cast(lParam); + paintMenuBar(hWnd, pUDM->hdc); + + return 0; + } + + case WM_UAHDRAWMENUITEM: + { + const auto& hTheme = pMenuThemeData->getHTheme(); + auto* pUDMI = reinterpret_cast(lParam); + paintMenuBarItems(*pUDMI, hTheme); + + return 0; + } + +#if 0 // for debugging + case WM_UAHMEASUREMENUITEM: + { + auto* pMMI = reinterpret_cast(lParam); + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + } +#endif + + case WM_DPICHANGED: + case WM_DPICHANGED_AFTERPARENT: + case WM_THEMECHANGED: + { + pMenuThemeData->closeTheme(); + break; + } + + case WM_NCACTIVATE: + case WM_NCPAINT: + { + const LRESULT retVal = ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + drawUAHMenuNCBottomLine(hWnd); + return retVal; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Window subclass procedure for handling `WM_SETTINGCHANGE` message. + * + * Handles `WM_SETTINGCHANGE` to perform changes for dark mode based on system setting. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData Reserved data (unused). + * @return LRESULT Result of message processing. + * + * @see dmlib::setWindowSettingChangeSubclass() + * @see dmlib::removeWindowSettingChangeSubclass() + */ +LRESULT CALLBACK dmlib_subclass::WindowSettingChangeSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + [[maybe_unused]] DWORD_PTR dwRefData +) noexcept +{ + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, WindowSettingChangeSubclass, uIdSubclass); + break; + } + + case WM_SETTINGCHANGE: + { + if (dmlib::handleSettingChange(lParam)) + { + dmlib::setDarkTitleBarEx(hWnd, true); + dmlib::setChildCtrlsTheme(hWnd); + ::RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW | RDW_FRAME); + } + break; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @class TaskDlgData + * @brief Class to handle colors for task dialog. + * + * Members: + * - `m_themeData`: Theme data with "DarkMode_Explorer::TaskDialog" theme to get colors. + * - `m_clrText`: Color for text. + * - `m_clrBg`: Color for background. + * - `m_hBrushBg`: Brush for background. + * + * Copying and moving are explicitly disabled to preserve exclusive ownership. + */ +class TaskDlgData +{ +public: + TaskDlgData() noexcept + { + if (m_themeData.ensureTheme(nullptr)) + { + COLORREF clrTmp = 0; + if (SUCCEEDED(::GetThemeColor(m_themeData.getHTheme(), TDLG_PRIMARYPANEL, 0, TMT_TEXTCOLOR, &clrTmp))) + { + m_clrText = clrTmp; + } + + if (SUCCEEDED(::GetThemeColor(m_themeData.getHTheme(), TDLG_PRIMARYPANEL, 0, TMT_FILLCOLOR, &clrTmp))) + { + m_clrBg = clrTmp; + } + } + + m_hBrushBg = ::CreateSolidBrush(m_clrBg); + } + + TaskDlgData(const TaskDlgData&) = delete; + TaskDlgData& operator=(const TaskDlgData&) = delete; + + TaskDlgData(TaskDlgData&&) = delete; + TaskDlgData& operator=(TaskDlgData&&) = delete; + + ~TaskDlgData() + { + ::DeleteObject(m_hBrushBg); + } + + [[nodiscard]] COLORREF getTextColor() const noexcept + { + return m_clrText; + } + + [[nodiscard]] COLORREF getBgColor() const noexcept + { + return m_clrBg; + } + + [[nodiscard]] const HBRUSH& getBgBrush() const noexcept + { + return m_hBrushBg; + } + + [[nodiscard]] bool shouldErase() const noexcept + { + return m_needErase; + } + + void stopErase() noexcept + { + m_needErase = false; + } + +private: + dmlib_subclass::ThemeData m_themeData{ L"DarkMode_Explorer::TaskDialog" }; + COLORREF m_clrText = RGB(255, 255, 255); + COLORREF m_clrBg = RGB(44, 44, 44); + HBRUSH m_hBrushBg = nullptr; + bool m_needErase = true; +}; + +/** + * @brief Window subclass procedure for handling dark mode for task dialog and its children. + * + * @param[in] hWnd Window handle being subclassed. + * @param[in] uMsg Message identifier. + * @param[in] wParam Message-specific data. + * @param[in] lParam Message-specific data. + * @param[in] uIdSubclass Subclass identifier. + * @param[in] dwRefData TaskDlgData instance. + * @return LRESULT Result of message processing. + * + * @see setDarkTaskDlgSubclass() + */ +static LRESULT CALLBACK DarkTaskDlgSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData +) +{ + auto* pTaskDlgData = reinterpret_cast(dwRefData); + + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, DarkTaskDlgSubclass, uIdSubclass); + const std::unique_ptr ptrData(pTaskDlgData); + break; + } + + case WM_ERASEBKGND: + { + const std::wstring className = dmlib_subclass::getWndClassName(hWnd); + + if (className == L"CtrlNotifySink") + { + break; + } + + if ((className == L"DirectUIHWND") && pTaskDlgData->shouldErase()) + { + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + ::FillRect(reinterpret_cast(wParam), &rcClient, pTaskDlgData->getBgBrush()); + pTaskDlgData->stopErase(); + } + return TRUE; + } + + case WM_CTLCOLORDLG: + case WM_CTLCOLORSTATIC: + { + auto hdc = reinterpret_cast(wParam); + ::SetTextColor(hdc, pTaskDlgData->getTextColor()); + ::SetBkColor(hdc, pTaskDlgData->getBgColor()); + return reinterpret_cast(pTaskDlgData->getBgBrush()); + } + + case WM_PRINTCLIENT: + { + return TRUE; + } + + default: + { + break; + } + } + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +/** + * @brief Applies a subclass to task dialog to handle dark mode. + * + * @param[in] hWnd Handle to the task dialog. + * + * @see DarkTaskDlgSubclass() + */ +static void setDarkTaskDlgSubclass(HWND hWnd) +{ + dmlib_subclass::SetSubclass(hWnd, DarkTaskDlgSubclass, dmlib_subclass::SubclassID::taskDlg); +} + +/** + * @brief Callback function used to enumerate and apply theming/subclassing to task dialog child controls. + * + * @param[in] hWnd Handle to the window being enumerated. + * @param[in] lParam LPARAM data (unused). + * @return `TRUE` to continue enumeration. + */ +static BOOL CALLBACK DarkTaskEnumChildProc(HWND hWnd, [[maybe_unused]] LPARAM lParam) +{ + const std::wstring className = dmlib_subclass::getWndClassName(hWnd); + + if (className == L"CtrlNotifySink") + { + setDarkTaskDlgSubclass(hWnd); + return TRUE; + } + + if (className == WC_BUTTON) + { + switch (::GetWindowLongPtr(hWnd, GWL_STYLE) & BS_TYPEMASK) // button style + { + case BS_RADIOBUTTON: + case BS_AUTORADIOBUTTON: + { + dmlib::setCheckboxOrRadioBtnCtrlSubclass(hWnd); + break; + } + + default: + { + break; + } + } + + dmlib::setDarkExplorerTheme(hWnd); + + return TRUE; + } + + if (className == WC_LINK) + { + dmlib::enableSysLinkCtrlCtlColor(hWnd); + setDarkTaskDlgSubclass(hWnd); + return TRUE; + } + + if (className == WC_SCROLLBAR) + { + dmlib::setDarkScrollBar(hWnd); + return TRUE; + } + + if (className == PROGRESS_CLASS) + { + dmlib::setProgressBarClassicTheme(hWnd); + return TRUE; + } + + if (className == L"DirectUIHWND") + { + ::EnumChildWindows(hWnd, DarkTaskEnumChildProc, 0); + setDarkTaskDlgSubclass(hWnd); + dmlib::setDarkExplorerTheme(hWnd); + return TRUE; + } + + return TRUE; +} + +/** + * @brief Applies a subclass to task dialog and its children to handle dark mode. + * + * @param[in] hWnd Handle to the task dialog. + * + * @see DarkTaskDlgSubclass() + * @see DarkTaskEnumChildProc() + */ +void dmlib_subclass::setTaskDlgChildCtrlsSubclassAndTheme(HWND hWnd) +{ + setDarkTaskDlgSubclass(hWnd); + ::EnumChildWindows(hWnd, DarkTaskEnumChildProc, 0); +} diff --git a/darkmodelib/src/DmlibSubclassWindow.h b/darkmodelib/src/DmlibSubclassWindow.h new file mode 100644 index 0000000000..27b885093d --- /dev/null +++ b/darkmodelib/src/DmlibSubclassWindow.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +#include + +namespace dmlib_subclass +{ + LRESULT CALLBACK WindowEraseBgSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + LRESULT CALLBACK WindowCtlColorSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK WindowNotifySubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK WindowMenuBarSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + LRESULT CALLBACK WindowSettingChangeSubclass(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) noexcept; + + /// Applies a subclass to task dialog and its children to handle dark mode. + void setTaskDlgChildCtrlsSubclassAndTheme(HWND hWnd); + +} // namespace dmlib_subclass diff --git a/darkmodelib/src/DmlibWinApi.cpp b/darkmodelib/src/DmlibWinApi.cpp new file mode 100644 index 0000000000..41736c5e76 --- /dev/null +++ b/darkmodelib/src/DmlibWinApi.cpp @@ -0,0 +1,578 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * This file incorporates work from the win32-darkmode project: + * https://github.com/ysc3839/win32-darkmode + * which is covered by the MIT License. + * See LICENSE-win32-darkmode for more information. + */ + +// This file is part of darkmodelib library. + + +#include "StdAfx.h" + +#include "DmlibWinApi.h" + +#include + +#include + +#include "ModuleHelper.h" + +#if defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 0) +namespace dmlib_hook +{ + bool loadOpenNcThemeData(const HMODULE& hUxtheme); + void fixDarkScrollBar(); +} +#endif + + +enum class IMMERSIVE_HC_CACHE_MODE +{ + IHCM_USE_CACHED_VALUE, + IHCM_REFRESH +}; + +// 1903 18362 +enum class PreferredAppMode +{ + Default, + AllowDark, + ForceDark, + ForceLight, + Max +}; + +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) +static constexpr DWORD g_win10Build1903 = 18362; + +enum WINDOWCOMPOSITIONATTRIB +{ + WCA_UNDEFINED = 0, + WCA_NCRENDERING_ENABLED = 1, + WCA_NCRENDERING_POLICY = 2, + WCA_TRANSITIONS_FORCEDISABLED = 3, + WCA_ALLOW_NCPAINT = 4, + WCA_CAPTION_BUTTON_BOUNDS = 5, + WCA_NONCLIENT_RTL_LAYOUT = 6, + WCA_FORCE_ICONIC_REPRESENTATION = 7, + WCA_EXTENDED_FRAME_BOUNDS = 8, + WCA_HAS_ICONIC_BITMAP = 9, + WCA_THEME_ATTRIBUTES = 10, + WCA_NCRENDERING_EXILED = 11, + WCA_NCADORNMENTINFO = 12, + WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, + WCA_VIDEO_OVERLAY_ACTIVE = 14, + WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, + WCA_DISALLOW_PEEK = 16, + WCA_CLOAK = 17, + WCA_CLOAKED = 18, + WCA_ACCENT_POLICY = 19, + WCA_FREEZE_REPRESENTATION = 20, + WCA_EVER_UNCLOAKED = 21, + WCA_VISUAL_OWNER = 22, + WCA_HOLOGRAPHIC = 23, + WCA_EXCLUDED_FROM_DDA = 24, + WCA_PASSIVEUPDATEMODE = 25, + WCA_USEDARKMODECOLORS = 26, + WCA_LAST = 27 +}; + +struct WINDOWCOMPOSITIONATTRIBDATA +{ + WINDOWCOMPOSITIONATTRIB Attrib; + PVOID pvData; + SIZE_T cbData; +}; +#endif + +using fnRtlGetNtVersionNumbers = void (WINAPI*)(LPDWORD major, LPDWORD minor, LPDWORD build); +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) +using fnSetWindowCompositionAttribute = auto (WINAPI*)(HWND hWnd, WINDOWCOMPOSITIONATTRIBDATA*) -> BOOL; +#endif + +// 1809 17763 +using fnAllowDarkModeForWindow = auto (WINAPI*)(HWND hWnd, bool allow) -> bool; // ordinal 133 +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) +using fnAllowDarkModeForApp = auto (WINAPI*)(bool allow) -> bool; // ordinal 135, in 1809 +#endif +using fnFlushMenuThemes = void (WINAPI*)(); // ordinal 136 +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) +using fnIsDarkModeAllowedForWindow = auto (WINAPI*)(HWND hWnd) -> bool; // ordinal 137 +#endif +using fnRefreshImmersiveColorPolicyState = void (WINAPI*)(); // ordinal 104 +using fnGetIsImmersiveColorUsingHighContrast = auto (WINAPI*)(IMMERSIVE_HC_CACHE_MODE mode) -> bool; // ordinal 106 + +// 1903 18362 +using fnSetPreferredAppMode = auto (WINAPI*)(PreferredAppMode appMode)->PreferredAppMode; // ordinal 135, in 1903 + +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) +static fnSetWindowCompositionAttribute pfSetWindowCompositionAttribute = nullptr; +#endif +static fnAllowDarkModeForWindow pfAllowDarkModeForWindow = nullptr; +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) +static fnAllowDarkModeForApp pfAllowDarkModeForApp = nullptr; +#endif +static fnFlushMenuThemes pfFlushMenuThemes = nullptr; +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) +static fnIsDarkModeAllowedForWindow pfIsDarkModeAllowedForWindow = nullptr; +#endif +static fnRefreshImmersiveColorPolicyState pfRefreshImmersiveColorPolicyState = nullptr; +static fnGetIsImmersiveColorUsingHighContrast pfGetIsImmersiveColorUsingHighContrast = nullptr; + +// 1903 18362 +static fnSetPreferredAppMode pfSetPreferredAppMode = nullptr; + +static bool g_darkModeSupported = false; +static bool g_darkModeActive = false; +static DWORD g_buildNumber = 0; + +/** + * @brief Enables or disables dark mode support for a specific window. + * + * @param[in] hWnd Window handle to apply dark mode. + * @param[in] allow Whether to allow (`true`) or disallow (`false`) dark mode. + * @return `true` if successfully applied. + */ +bool dmlib_win32api::AllowDarkModeForWindow(HWND hWnd, bool allow) noexcept +{ + if (g_darkModeSupported && (pfAllowDarkModeForWindow != nullptr)) + { + return pfAllowDarkModeForWindow(hWnd, allow); + } + return false; +} + +/** + * @brief Determines if high contrast mode is currently active. + * + * @return `true` if high contrast is enabled via system accessibility settings. + */ +bool dmlib_win32api::IsHighContrast() noexcept +{ + HIGHCONTRASTW highContrast{}; + highContrast.cbSize = sizeof(HIGHCONTRASTW); + if (::SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(HIGHCONTRASTW), &highContrast, FALSE) == TRUE) + { + return (highContrast.dwFlags & HCF_HIGHCONTRASTON) == HCF_HIGHCONTRASTON; + } + return false; +} + +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) +static void SetTitleBarThemeColor(HWND hWnd, BOOL dark) +{ + if (g_buildNumber < g_win10Build1903) + { + ::SetPropW(hWnd, L"UseImmersiveDarkModeColors", reinterpret_cast(static_cast(dark))); + } + else if (pfSetWindowCompositionAttribute != nullptr) + { + WINDOWCOMPOSITIONATTRIBDATA data{ WINDOWCOMPOSITIONATTRIB::WCA_USEDARKMODECOLORS, &dark, sizeof(dark) }; + pfSetWindowCompositionAttribute(hWnd, &data); + } +} + +/** + * @brief Refreshes the title bar theme color for legacy systems. + * + * Used only on old Windows 10 systems when `_DARKMODELIB_ALLOW_OLD_OS` + * is defined with non-zero unsigned value. + * + * @param[in] hWnd Handle to the window to update. + */ +void dmlib_win32api::RefreshTitleBarThemeColor(HWND hWnd) +{ + BOOL dark = FALSE; + if (pfIsDarkModeAllowedForWindow != nullptr) + { + if (pfIsDarkModeAllowedForWindow(hWnd) && !IsHighContrast()) + { + dark = TRUE; + } + } + + SetTitleBarThemeColor(hWnd, dark); +} +#endif + +/** + * @brief Checks whether a `WM_SETTINGCHANGE` message indicates a color scheme switch. + * + * @param[in] lParam LPARAM from a system message. + * @return `true` if the message signals a theme mode change. + */ +bool dmlib_win32api::IsColorSchemeChangeMessage(LPARAM lParam) noexcept +{ + const bool isMsg = + (lParam != 0) // NULL + && (_wcsicmp(reinterpret_cast(lParam), L"ImmersiveColorSet") == 0); + + if (isMsg) + { + if (pfRefreshImmersiveColorPolicyState != nullptr) + { + pfRefreshImmersiveColorPolicyState(); + } + + if (pfGetIsImmersiveColorUsingHighContrast != nullptr) + { + pfGetIsImmersiveColorUsingHighContrast(IMMERSIVE_HC_CACHE_MODE::IHCM_REFRESH); + } + } + + return isMsg; +} + +/** + * @brief Checks whether a message indicates a color scheme switch. + * + * Overload that takes uMsg parameter and checks if it is a `WM_SETTINGCHANGE` + * + * @param[in] lParam LPARAM from a system message. + * @param[in] uMsg System message to check. + * @return `true` if the message signals a theme mode change. + */ +bool dmlib_win32api::IsColorSchemeChangeMessage(UINT uMsg, LPARAM lParam) noexcept +{ + if (uMsg == WM_SETTINGCHANGE) + { + return dmlib_win32api::IsColorSchemeChangeMessage(lParam); + } + return false; +} + +static void AllowDarkModeForApp(bool allow) noexcept +{ + if (pfSetPreferredAppMode != nullptr) + { + pfSetPreferredAppMode(allow ? PreferredAppMode::ForceDark : PreferredAppMode::Default); + } +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) + else if (pfAllowDarkModeForApp != nullptr) + { + pfAllowDarkModeForApp(allow); + } +#endif +} + +static void FlushMenuThemes() noexcept +{ + if (pfFlushMenuThemes != nullptr) + { + pfFlushMenuThemes(); + } +} + +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) +static constexpr DWORD g_win10Build = 17763; +#else +static constexpr DWORD g_win10Build = 19044; // 21H2 latest LTSC, 22H2 19045 latest GA +#endif +static constexpr DWORD g_win11Build = 22000; + +/** + * @brief Checks if the host OS is at least Windows 10. + * + * @return `true` if running on Windows 10 or newer. + */ +bool dmlib_win32api::IsWindows10() noexcept +{ + return (g_buildNumber >= g_win10Build); +} + +/** + * @brief Checks if the host OS is at least Windows 11. + * + * @return `true` if running on Windows 11 or newer. + */ +bool dmlib_win32api::IsWindows11() noexcept +{ + return (g_buildNumber >= g_win11Build); +} + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 26497) // This function function-name could be marked constexpr if compile-time evaluation is desired (f.4). // Used only in runtime. +#endif +[[nodiscard]] static bool CheckBuildNumber(DWORD buildNumber) noexcept +#ifdef _MSC_VER +#pragma warning(pop) +#endif +{ +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) + static constexpr size_t nWin10Builds = 8; + // Windows 10 builds { 1809, 1903, 1909, 2004, 20H2, 21H1, 21H2, 22H2 } + static constexpr DWORD win10Builds[nWin10Builds] = { 17763, 18362, 18363, 19041, 19042, 19043, 19044, 19045 }; + + // Windows 10 any version >= 22H2 and Windows 11 + if ((buildNumber >= win10Builds[nWin10Builds - 1])) // || buildNumber > g_win11Build + { + return true; + } + + for (size_t i = 0; i < nWin10Builds; ++i) + { + if (buildNumber == win10Builds[i]) + { + return true; + } + } + return false; +#else + return (buildNumber >= g_win10Build); // || buildNumber > g_win11Build +#endif +} + + +/** + * @brief Retrieves the current Windows build number. + * + * @return Windows build number reported by the system. + */ +DWORD dmlib_win32api::GetWindowsBuildNumber() noexcept +{ + return g_buildNumber; +} + +/** + * @brief Checks if dark mode API is supported. + * + * @return `true` if dark mode API is supported. + */ +bool dmlib_win32api::IsDarkModeSupported() noexcept +{ + return g_darkModeSupported; +} + +/** + * @brief Checks if dark mode is active. + * + * @return `true` if dark mode is active. + */ +bool dmlib_win32api::IsDarkModeActive() noexcept +{ + return g_darkModeActive; +} + +/** + * @brief Initializes undocumented dark mode API. + */ +void dmlib_win32api::InitDarkMode() noexcept +{ + static bool isInit = false; + if (isInit) + { + return; + } +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 26429) // Symbol is never tested for nullness, it can be marked as not_null. // Already checked in dmlib_module::LoadFn. +#endif + fnRtlGetNtVersionNumbers RtlGetNtVersionNumbers = nullptr; +#ifdef _MSC_VER +#pragma warning(pop) +#endif + if (HMODULE hNtdll = ::GetModuleHandleW(L"ntdll.dll"); + hNtdll == nullptr + || !dmlib_module::LoadFn(hNtdll, RtlGetNtVersionNumbers, "RtlGetNtVersionNumbers")) + { + return; + } + + DWORD major = 0; + DWORD minor = 0; + RtlGetNtVersionNumbers(&major, &minor, &g_buildNumber); + g_buildNumber &= ~0xF0000000; + if (major != 10 || minor != 0 || !CheckBuildNumber(g_buildNumber)) + { + return; + } + + if (const dmlib_module::ModuleHandle moduleUxtheme(L"uxtheme.dll"); + moduleUxtheme.isLoaded()) + { + const HMODULE& hUxtheme = moduleUxtheme.get(); + + bool ptrFnOrd135NotNullptr = false; +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) + if (g_buildNumber < g_win10Build1903) + { + ptrFnOrd135NotNullptr = LoadFn(hUxtheme, pfAllowDarkModeForApp, 135); + } + else +#endif + { + ptrFnOrd135NotNullptr = dmlib_module::LoadFn(hUxtheme, pfSetPreferredAppMode, 135); + } + + if (ptrFnOrd135NotNullptr +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) + && ptrFnOrd132NotNullptr +#endif +#if defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 0) + && dmlib_hook::loadOpenNcThemeData(hUxtheme) +#endif + && dmlib_module::LoadFn(hUxtheme, pfRefreshImmersiveColorPolicyState, 104) + && dmlib_module::LoadFn(hUxtheme, pfAllowDarkModeForWindow, 133) + && dmlib_module::LoadFn(hUxtheme, pfFlushMenuThemes, 136)) + { + g_darkModeSupported = true; + } + + dmlib_module::LoadFn(hUxtheme, pfGetIsImmersiveColorUsingHighContrast, 106); +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) + if (static constexpr DWORD build2004 = 19041; + g_buildNumber < build2004 + && g_darkModeSupported + && dmlib_module::LoadFn(hUxtheme, pfIsDarkModeAllowedForWindow, 137)) + { + if (HMODULE hUser32 = ::GetModuleHandleW(L"user32.dll"); + hUser32 != nullptr) + { + dmlib_module::LoadFn(hUser32, pfSetWindowCompositionAttribute, "SetWindowCompositionAttribute"); + } + } +#endif + isInit = true; + } +} + +/** + * @brief Enables or disables dark mode using undocumented API. + * + * Optionally applies a scroll bar fix for dark mode inconsistencies. + * + * @param[in] useDark Enable dark mode when `true`, disable when `false`. + * @param[in] applyScrollBarFix Apply scroll bar fix if `true`. + */ +void dmlib_win32api::SetDarkMode(bool useDark, [[maybe_unused]] bool applyScrollBarFix) noexcept +{ + if (g_darkModeSupported) + { + AllowDarkModeForApp(useDark); + FlushMenuThemes(); +#if defined(_DARKMODELIB_USE_SCROLLBAR_FIX) && (_DARKMODELIB_USE_SCROLLBAR_FIX > 0) + if (applyScrollBarFix) + { + dmlib_hook::fixDarkScrollBar(); + } +#endif + g_darkModeActive = useDark && !dmlib_win32api::IsHighContrast(); + } +} + +static LPCWSTR WINAPI DummyMB_GetString([[maybe_unused]] UINT wBtn) noexcept +{ + return nullptr; +} + +using fnMB_GetString = auto (WINAPI*)(UINT) -> LPCWSTR; +static fnMB_GetString pfMB_GetString = DummyMB_GetString; + +/** + * @brief Initializes undocumented MB_GetString. + */ +void dmlib_win32api::InitMB_GetString() noexcept +{ + static bool isInit = false; + if (isInit) + { + return; + } + + if (HMODULE hUser32 = ::GetModuleHandleW(L"user32.dll"); + hUser32 != nullptr) + { + dmlib_module::LoadFn(hUser32, pfMB_GetString, "MB_GetString"); + isInit = true; + } +} + +/** + * @brief Returns strings for standard message box buttons. + * + * @param[in] wBtn The id of the string to return. + * These are identified by the Dialog Box Command ID values listed in winuser.h. + * https://learn.microsoft.com/en-us/windows/win32/dlgbox/mb-getstring + * + * @return LPCWSTR The string, or nullptr if not found. + */ +LPCWSTR dmlib_win32api::MB_GetString(UINT wBtn) noexcept +{ + if (auto str = pfMB_GetString(wBtn - 1); + str != nullptr) + { + return str; + } + + switch (wBtn) + { + case IDOK: + { + return L"OK"; + } + + case IDCANCEL: + { + return L"Cancel"; + } + + case IDABORT: + { + return L"&Abort"; + } + + case IDRETRY: + { + return L"&Retry"; + } + + case IDIGNORE: + { + return L"&Ignore"; + } + + case IDYES: + { + return L"&Yes"; + } + + case IDNO: + { + return L"&No"; + } + + case IDCLOSE: + { + return L"&Close"; + } + + case IDHELP: + { + return L"Help"; + } + + case IDTRYAGAIN: + { + return L"&Try Again"; + } + + case IDCONTINUE: + { + return L"&Continue"; + } + + default: + { + return nullptr; + } + } +} diff --git a/darkmodelib/src/DmlibWinApi.h b/darkmodelib/src/DmlibWinApi.h new file mode 100644 index 0000000000..809c268d45 --- /dev/null +++ b/darkmodelib/src/DmlibWinApi.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * This file incorporates work from the win32-darkmode project: + * https://github.com/ysc3839/win32-darkmode + * which is covered by the MIT License. + * See LICENSE-win32-darkmode for more information. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +#include + +namespace dmlib_win32api +{ + /// Enables or disables dark mode support for a specific window. + bool AllowDarkModeForWindow(HWND hWnd, bool allow) noexcept; + + /// Determines if high contrast mode is currently active. + [[nodiscard]] bool IsHighContrast() noexcept; + +#if defined(_DARKMODELIB_ALLOW_OLD_OS) && (_DARKMODELIB_ALLOW_OLD_OS > 0) + /// Refreshes the title bar theme color for legacy systems. + void RefreshTitleBarThemeColor(HWND hWnd); +#endif + + /// Checks whether a `WM_SETTINGCHANGE` message indicates a color scheme switch. + [[nodiscard]] bool IsColorSchemeChangeMessage(LPARAM lParam) noexcept; + /// Checks whether a message indicates a color scheme switch. + [[nodiscard]] bool IsColorSchemeChangeMessage(UINT uMsg, LPARAM lParam) noexcept; + + /// Initializes undocumented dark mode API. + void InitDarkMode() noexcept; + /// Enables or disables dark mode using undocumented API. + void SetDarkMode(bool useDark, bool applyScrollBarFix) noexcept; + + /// Checks if the host OS is at least Windows 10. + [[nodiscard]] bool IsWindows10() noexcept; + /// Checks if the host OS is at least Windows 11. + [[nodiscard]] bool IsWindows11() noexcept; + /// Retrieves the current Windows build number. + [[nodiscard]] DWORD GetWindowsBuildNumber() noexcept; + + /// Checks if dark mode API is supported. + [[nodiscard]] bool IsDarkModeSupported() noexcept; + /// Checks if dark mode is active. + [[nodiscard]] bool IsDarkModeActive() noexcept; + + /// Initializes undocumented MB_GetString. + void InitMB_GetString() noexcept; + /// Returns strings for standard message box buttons. + [[nodiscard]] LPCWSTR MB_GetString(UINT wBtn) noexcept; +} // namespace dmlib_win32api diff --git a/darkmodelib/src/IatHook.h b/darkmodelib/src/IatHook.h new file mode 100644 index 0000000000..f336663c0e --- /dev/null +++ b/darkmodelib/src/IatHook.h @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2025 ozone10 +// Licensed under the MIT license. + +// This file is part of darkmodelib library. + +// This file is a modified version of IatHook.h from the win32-darkmode project +// https://github.com/ysc3839/win32-darkmode + +// This file contains code from +// https://github.com/stevemk14ebr/PolyHook_2_0/blob/master/sources/IatHook.cpp +// which is licensed under the MIT license. +// See LICENSE-PolyHook_2_0 for more information. + +#pragma once + +#include + +#include +#include + +// NOLINTBEGIN(cppcoreguidelines-*) + +#ifdef _MSC_VER + # pragma warning(push) + # pragma warning(disable: 26429) // Symbol is never tested for nullness. + # pragma warning(disable: 26472) // Don't use a static_cast for arithmetic conversions. + # pragma warning(disable: 26481) // Don't use pointer arithmetic. + # pragma warning(disable: 26485) // No array to pointer decay. +#elif defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" +#endif + +namespace iat_hook +{ + template + constexpr auto RVA2VA(T1 base, T2 rva) noexcept -> T + { + return reinterpret_cast(reinterpret_cast(base) + rva); + } + + template + constexpr auto DataDirectoryFromModuleBase(void* moduleBase, size_t entryID) noexcept -> T + { + const auto* dosHdr = static_cast(moduleBase); + const auto* ntHdr = RVA2VA(moduleBase, static_cast(dosHdr->e_lfanew)); + const auto* dataDir = ntHdr->OptionalHeader.DataDirectory; + return RVA2VA(moduleBase, dataDir[entryID].VirtualAddress); + } + + inline PIMAGE_THUNK_DATA FindAddressByName(void* moduleBase, PIMAGE_THUNK_DATA impName, PIMAGE_THUNK_DATA impAddr, const char* funcName) noexcept + { + for (; impName->u1.Ordinal != 0; ++impName, ++impAddr) + { + if (IMAGE_SNAP_BY_ORDINAL(impName->u1.Ordinal)) + { + continue; + } + + if (const auto* imgImport = RVA2VA(moduleBase, impName->u1.AddressOfData); + std::strcmp(reinterpret_cast(imgImport->Name), funcName) != 0) + { + continue; + } + return impAddr; + } + return nullptr; + } + + inline PIMAGE_THUNK_DATA FindAddressByOrdinal([[maybe_unused]] void* /*moduleBase*/, PIMAGE_THUNK_DATA impName, PIMAGE_THUNK_DATA impAddr, uint16_t ordinal) noexcept + { + for (; impName->u1.Ordinal != 0; ++impName, ++impAddr) + { + if (IMAGE_SNAP_BY_ORDINAL(impName->u1.Ordinal) && IMAGE_ORDINAL(impName->u1.Ordinal) == ordinal) + { + return impAddr; + } + } + return nullptr; + } + + inline PIMAGE_THUNK_DATA FindIatThunkInModule(void* moduleBase, const char* dllName, const char* funcName) noexcept + { + const auto* imports = DataDirectoryFromModuleBase(moduleBase, IMAGE_DIRECTORY_ENTRY_IMPORT); + for (; imports->Name != 0; ++imports) + { + if (_stricmp(RVA2VA(moduleBase, imports->Name), dllName) != 0) + { + continue; + } + + auto* origThunk = RVA2VA(moduleBase, imports->OriginalFirstThunk); + auto* thunk = RVA2VA(moduleBase, imports->FirstThunk); + return FindAddressByName(moduleBase, origThunk, thunk, funcName); + } + return nullptr; + } + + inline PIMAGE_THUNK_DATA FindDelayLoadThunkInModule(void* moduleBase, const char* dllName, const char* funcName) noexcept + { + const auto* imports = DataDirectoryFromModuleBase(moduleBase, IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT); + for (; imports->DllNameRVA != 0; ++imports) + { + if (_stricmp(RVA2VA(moduleBase, imports->DllNameRVA), dllName) != 0) + { + continue; + } + + auto* impName = RVA2VA(moduleBase, imports->ImportNameTableRVA); + auto* impAddr = RVA2VA(moduleBase, imports->ImportAddressTableRVA); + return FindAddressByName(moduleBase, impName, impAddr, funcName); + } + return nullptr; + } + + inline PIMAGE_THUNK_DATA FindDelayLoadThunkInModule(void* moduleBase, const char* dllName, uint16_t ordinal) noexcept + { + const auto* imports = DataDirectoryFromModuleBase(moduleBase, IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT); + for (; imports->DllNameRVA != 0; ++imports) + { + if (_stricmp(RVA2VA(moduleBase, imports->DllNameRVA), dllName) != 0) + { + continue; + } + + auto* impName = RVA2VA(moduleBase, imports->ImportNameTableRVA); + auto* impAddr = RVA2VA(moduleBase, imports->ImportAddressTableRVA); + return FindAddressByOrdinal(moduleBase, impName, impAddr, ordinal); + } + return nullptr; + } +} // namespace iat_hook + +#ifdef _MSC_VER +# pragma warning(pop) +#elif defined(__clang__) +#pragma clang diagnostic pop +#endif + +// NOLINTEND(cppcoreguidelines-*) diff --git a/darkmodelib/src/ModuleHelper.h b/darkmodelib/src/ModuleHelper.h new file mode 100644 index 0000000000..23b41093a0 --- /dev/null +++ b/darkmodelib/src/ModuleHelper.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +#include + +namespace dmlib_module +{ + template + inline auto LoadFn(HMODULE handle, P& pointer, const char* name) noexcept -> bool + { + if (auto proc = ::GetProcAddress(handle, name); + proc != nullptr) + { + pointer = reinterpret_cast

(reinterpret_cast(proc)); + return true; + } + return false; + } + + template + inline auto LoadFn(HMODULE handle, P& pointer, WORD index) noexcept -> bool + { + return dmlib_module::LoadFn(handle, pointer, MAKEINTRESOURCEA(index)); + } + + template + inline auto LoadFn(HMODULE handle, P& pointer, const char* name, D& dummy) noexcept -> bool + { + const bool retVal = dmlib_module::LoadFn(handle, pointer, name); + if (!retVal) + { + pointer = static_cast

(dummy); + } + return retVal; + } + + class ModuleHandle + { + public: + ModuleHandle() = delete; + + explicit ModuleHandle(const wchar_t* moduleName) noexcept + : m_hModule(::LoadLibraryExW(moduleName, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)) + {} + + ModuleHandle(const ModuleHandle&) = delete; + ModuleHandle& operator=(const ModuleHandle&) = delete; + + ModuleHandle(ModuleHandle&&) = delete; + ModuleHandle& operator=(ModuleHandle&&) = delete; + + ~ModuleHandle() + { + if (m_hModule != nullptr) + { + ::FreeLibrary(m_hModule); + m_hModule = nullptr; + } + } + + [[nodiscard]] HMODULE get() const noexcept + { + return m_hModule; + } + + [[nodiscard]] bool isLoaded() const noexcept + { + return m_hModule != nullptr; + } + + private: + HMODULE m_hModule = nullptr; + }; +} // namespace dmlib_module diff --git a/darkmodelib/src/StdAfx.h b/darkmodelib/src/StdAfx.h new file mode 100644 index 0000000000..b5a3e040fd --- /dev/null +++ b/darkmodelib/src/StdAfx.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + + +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef VC_EXTRALEAN +#define VC_EXTRALEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif diff --git a/darkmodelib/src/UAHMenuBar.h b/darkmodelib/src/UAHMenuBar.h new file mode 100644 index 0000000000..5d3e5c8216 --- /dev/null +++ b/darkmodelib/src/UAHMenuBar.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2021 adzm / Adam D. Walling +// MIT license, see LICENSE-UAHMenuBar + +#pragma once +#include + +// NOLINTBEGIN(cppcoreguidelines-*, modernize-*) + +// processes messages related to UAH / custom menubar drawing. +// return true if handled, false to continue with normal processing in your wndproc +//bool UAHDarkModeWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT* lr); + +// window messages related to menu bar drawing +#define WM_UAHDESTROYWINDOW 0x0090 // handled by DefWindowProc +#define WM_UAHDRAWMENU 0x0091 // lParam is UAHMENU +#define WM_UAHDRAWMENUITEM 0x0092 // lParam is UAHDRAWMENUITEM +#define WM_UAHINITMENU 0x0093 // handled by DefWindowProc +#define WM_UAHMEASUREMENUITEM 0x0094 // lParam is UAHMEASUREMENUITEM +#define WM_UAHNCPAINTMENUPOPUP 0x0095 // handled by DefWindowProc + +// describes the sizes of the menu bar or menu item +typedef union tagUAHMENUITEMMETRICS +{ + // cx appears to be 14 / 0xE less than rcItem's width! + // cy 0x14 seems stable, i wonder if it is 4 less than rcItem's height which is always 24 atm + struct + { + DWORD cx; + DWORD cy; + } rgsizeBar[2]; + struct + { + DWORD cx; + DWORD cy; + } rgsizePopup[4]; +} UAHMENUITEMMETRICS; + +// not really used in our case but part of the other structures +typedef struct tagUAHMENUPOPUPMETRICS +{ + DWORD rgcx[4]; + DWORD fUpdateMaxWidths : 2; // from kernel symbols, padded to full dword +} UAHMENUPOPUPMETRICS; + +// hmenu is the main window menu; hdc is the context to draw in +typedef struct tagUAHMENU +{ + HMENU hmenu; + HDC hdc; + DWORD dwFlags; // no idea what these mean, in my testing it's either 0x00000a00 or sometimes 0x00000a10 +} UAHMENU; + +// menu items are always referred to by iPosition here +typedef struct tagUAHMENUITEM +{ + [[maybe_unused]] int iPosition; // 0-based position of menu item in menubar + UAHMENUITEMMETRICS umim; + UAHMENUPOPUPMETRICS umpm; +} UAHMENUITEM; + +// the DRAWITEMSTRUCT contains the states of the menu items, as well as +// the position index of the item in the menu, which is duplicated in +// the UAHMENUITEM's iPosition as well +typedef struct UAHDRAWMENUITEM +{ + DRAWITEMSTRUCT dis; // itemID looks uninitialized + UAHMENU um; + UAHMENUITEM umi; +} UAHDRAWMENUITEM; + +// the MEASUREITEMSTRUCT is intended to be filled with the size of the item +// height appears to be ignored, but width can be modified +typedef struct tagUAHMEASUREMENUITEM +{ + MEASUREITEMSTRUCT mis; + UAHMENU um; + UAHMENUITEM umi; +} UAHMEASUREMENUITEM; + +// NOLINTEND(cppcoreguidelines-*, modernize-*) diff --git a/darkmodelib/src/Version.h b/darkmodelib/src/Version.h new file mode 100644 index 0000000000..277b91f9c7 --- /dev/null +++ b/darkmodelib/src/Version.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MPL-2.0 + +/* + * Copyright (c) 2025-2026 ozone10 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is part of darkmodelib library. + +// NOLINTBEGIN(cppcoreguidelines-*, modernize-*) + +#define DM_VERSION_MAJOR 0 +#define DM_VERSION_MINOR 66 +#define DM_VERSION_REVISION 0 + +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) + +#define DM_VERSION "Darkmodelib v0.66.0" +#define DM_COPYRIGHT "Copyright (c) 2024-2026 ozone10" + +// NOLINTEND(cppcoreguidelines-*, modernize-*) diff --git a/src/DarkMode.cpp b/src/DarkMode.cpp new file mode 100644 index 0000000000..e102ac7ffe --- /dev/null +++ b/src/DarkMode.cpp @@ -0,0 +1,176 @@ +// This file is part of Notepad4. +// See License.txt for details about distribution and modification. +// +// Dark mode integration using darkmodelib (https://github.com/ozone10/win32-darkmodelib) +// Licensed under MPL-2.0. + +#include "Darkmodelib.h" +#include "DarkMode.h" +#include "EditLexer.h" + +// Declared in Styles.cpp +extern int np2StyleTheme; +// Declared in Notepad4.cpp - this app's module handle +extern HINSTANCE g_hInstance; +// Declared in Notepad4.cpp - the Scintilla edit window +extern HWND hwndEdit; + +// Thread-level hook to intercept WM_INITDIALOG and apply dark mode to dialogs +static HHOOK s_hCallWndProcRetHook = nullptr; + +static constexpr UINT DarkMode_TypeForStyleTheme(int theme) noexcept { + return static_cast((theme == StyleTheme_Dark) ? dmlib::DarkModeType::dark : dmlib::DarkModeType::light); +} + +static LRESULT CALLBACK DarkMode_CallWndRetProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept { + if (nCode == HC_ACTION) { + const CWPRETSTRUCT *cwpret = reinterpret_cast(lParam); + // Only theme dialogs created by this app while dark mode is active. + // This avoids touching system dialogs (e.g. the common file open/save + // dialog) that are hosted in our process but owned by comdlg32/shell. + if (cwpret->message == WM_INITDIALOG + && dmlib::isExperimentalActive() + && reinterpret_cast(GetWindowLongPtr(cwpret->hwnd, GWLP_HINSTANCE)) == g_hInstance) { + DarkMode_ApplyToDialog(cwpret->hwnd); + } + } + return CallNextHookEx(s_hCallWndProcRetHook, nCode, wParam, lParam); +} + +// Custom dark colors for Notepad4 +// Creates visual hierarchy: editor (#1E1E1E) < toolbar/statusbar (#2D2D2D) < controls (#383838) +static constexpr COLORREF HEXRGB(DWORD rrggbb) noexcept { + return ((rrggbb & 0xFF0000) >> 16) | (rrggbb & 0x00FF00) | ((rrggbb & 0x0000FF) << 16); +} + +static void DarkMode_SetCustomColors() noexcept { + dmlib::setBackgroundColor(HEXRGB(0x2D2D2D)); // toolbar, rebar + dmlib::setCtrlBackgroundColor(HEXRGB(0x383838)); // interactive controls (edit, combo) + dmlib::setHotBackgroundColor(HEXRGB(0x454545)); // hover states + dmlib::setDlgBackgroundColor(HEXRGB(0x252526)); // dialog backgrounds + dmlib::setTextColor(HEXRGB(0xD4D4D4)); // match editor text + dmlib::setDarkerTextColor(HEXRGB(0xC0C0C0)); + dmlib::setDisabledTextColor(HEXRGB(0x808080)); + dmlib::setEdgeColor(HEXRGB(0x555555)); + dmlib::setHotEdgeColor(HEXRGB(0x9B9B9B)); + dmlib::setDisabledEdgeColor(HEXRGB(0x484848)); + dmlib::updateThemeBrushesAndPens(); + + // View colors for list boxes, list views, tree views + dmlib::setViewBackgroundColor(HEXRGB(0x1E1E1E)); + dmlib::setViewTextColor(HEXRGB(0xD4D4D4)); + dmlib::setViewGridlinesColor(HEXRGB(0x3C3C3C)); + dmlib::setHeaderBackgroundColor(HEXRGB(0x2D2D2D)); + dmlib::setHeaderTextColor(HEXRGB(0xD4D4D4)); + dmlib::setHeaderEdgeColor(HEXRGB(0x555555)); + dmlib::updateViewBrushesAndPens(); + + // Override system colors used by system-drawn list boxes (e.g. toolbar customize dialog) + dmlib::setSysColor(COLOR_WINDOW, HEXRGB(0x1E1E1E)); + dmlib::setSysColor(COLOR_WINDOWTEXT, HEXRGB(0xD4D4D4)); + dmlib::setSysColor(COLOR_BTNFACE, HEXRGB(0x3C3C3C)); +} + +void DarkMode_Init() noexcept { + dmlib::initDarkMode(); + + if (np2StyleTheme == StyleTheme_Dark) { + dmlib::setDarkModeConfigEx(DarkMode_TypeForStyleTheme(np2StyleTheme)); + } else { + dmlib::setDarkModeConfigEx(DarkMode_TypeForStyleTheme(np2StyleTheme)); + } + dmlib::setDefaultColors(true); + + if (np2StyleTheme == StyleTheme_Dark) { + DarkMode_SetCustomColors(); + } + + // Install a thread-level hook to automatically apply dark mode to all dialogs + s_hCallWndProcRetHook = SetWindowsHookEx(WH_CALLWNDPROCRET, DarkMode_CallWndRetProc, + nullptr, GetCurrentThreadId()); + // Hook failure is non-fatal: dialogs won't get automatic dark mode theming +} + +void DarkMode_Cleanup() noexcept { + if (s_hCallWndProcRetHook) { + UnhookWindowsHookEx(s_hCallWndProcRetHook); + s_hCallWndProcRetHook = nullptr; + } +} + +void DarkMode_ApplyToWindow(HWND hwnd, bool useWin11Features) noexcept { + dmlib::setDarkTitleBarEx(hwnd, useWin11Features); + dmlib::setWindowMenuBarSubclass(hwnd); + dmlib::setWindowSettingChangeSubclass(hwnd); +} + +void DarkMode_ApplyToBars(HWND hwnd, HWND hwndToolbar, HWND hwndReBar, HWND hwndStatus) noexcept { + dmlib::setDarkLineAbovePanelToolbar(hwndToolbar); + dmlib::setStatusBarCtrlSubclass(hwndStatus); + dmlib::setWindowEraseBgSubclass(hwndReBar); + dmlib::setWindowNotifyCustomDrawSubclass(hwnd); + + // Apply the dark/light "DarkMode_Explorer" theme directly to the Scintilla + // edit window so its scroll bars follow the current mode. setDarkScrollBar() + // sets the dark theme when dark mode is active and resets it otherwise, so it + // works for both directions without any per-window hook state tracking. + dmlib::setDarkScrollBar(hwndEdit); + + dmlib::setDarkTooltips(hwndToolbar, static_cast(dmlib::ToolTipsType::toolbar)); +} + +void DarkMode_ApplyToDialog(HWND hDlg) noexcept { + dmlib::setDarkWndNotifySafeEx(hDlg, false, true); +} + +void DarkMode_OnThemeChanged(int newTheme) noexcept { + if (newTheme == StyleTheme_Dark) { + dmlib::setDarkModeConfigEx(DarkMode_TypeForStyleTheme(newTheme)); + } else { + dmlib::setDarkModeConfigEx(DarkMode_TypeForStyleTheme(newTheme)); + } + dmlib::setDefaultColors(true); + + if (newTheme == StyleTheme_Dark) { + DarkMode_SetCustomColors(); + } else { + // Reset overridden system colors to actual system values + dmlib::setSysColor(COLOR_WINDOW, GetSysColor(COLOR_WINDOW)); + dmlib::setSysColor(COLOR_WINDOWTEXT, GetSysColor(COLOR_WINDOWTEXT)); + dmlib::setSysColor(COLOR_BTNFACE, GetSysColor(COLOR_BTNFACE)); + } +} + +// Broadcast WM_THEMECHANGED to a window and all its descendants so that +// themed controls (including scroll bars) re-open their theme handles. +static BOOL CALLBACK DarkMode_SendThemeChangedProc(HWND hwnd, LPARAM /*lParam*/) noexcept { + SendMessage(hwnd, WM_THEMECHANGED, 0, 0); + return TRUE; +} + +void DarkMode_BroadcastThemeChanged(HWND hwnd) noexcept { + if (hwnd == nullptr) { + return; + } + SendMessage(hwnd, WM_THEMECHANGED, 0, 0); + EnumChildWindows(hwnd, DarkMode_SendThemeChangedProc, 0); +} + +bool DarkMode_HandleSettingChange([[maybe_unused]] HWND hwnd, LPARAM lParam) noexcept { + return dmlib::handleSettingChange(lParam); +} + +bool DarkMode_IsEnabled() noexcept { + return dmlib::isExperimentalActive(); +} + +int DarkMode_MessageBox(HWND hwnd, LPCWSTR text, LPCWSTR caption, UINT uType, WORD wLanguageId) noexcept { + if (dmlib::isExperimentalActive()) { + const HRESULT hr = dmlib::darkMessageBoxW(hwnd, text, caption, uType); + if (hr > 0) { + return static_cast(hr); + } + // Fall through to MessageBoxEx on failure. + } + return MessageBoxEx(hwnd, text, caption, uType, wLanguageId); +} diff --git a/src/DarkMode.h b/src/DarkMode.h new file mode 100644 index 0000000000..5a78666ae7 --- /dev/null +++ b/src/DarkMode.h @@ -0,0 +1,41 @@ +// This file is part of Notepad4. +// See License.txt for details about distribution and modification. +// +// Dark mode integration using darkmodelib (https://github.com/ozone10/win32-darkmodelib) +// Licensed under MPL-2.0. +#pragma once + +#include + +// Initialize dark mode support. Call once at startup after LoadSettings(). +void DarkMode_Init() noexcept; + +// Clean up dark mode resources. Call during WM_DESTROY. +void DarkMode_Cleanup() noexcept; + +// Apply dark mode to a top-level window (title bar, menu bar). +void DarkMode_ApplyToWindow(HWND hwnd, bool useWin11Features = true) noexcept; + +// Apply dark mode to the main window bars. +void DarkMode_ApplyToBars(HWND hwnd, HWND hwndToolbar, HWND hwndReBar, HWND hwndStatus) noexcept; + +// Apply dark mode to a dialog (WM_INITDIALOG handler). +void DarkMode_ApplyToDialog(HWND hDlg) noexcept; + +// Update dark mode after style theme change. Call from Style_OnStyleThemeChanged(). +void DarkMode_OnThemeChanged(int newTheme) noexcept; + +// Handle WM_SETTINGCHANGE for system dark/light mode changes. +// Returns true if the message was handled. +bool DarkMode_HandleSettingChange(HWND hwnd, LPARAM lParam) noexcept; + +// Check if dark mode UI is currently active. +bool DarkMode_IsEnabled() noexcept; + +// Show a message box that respects dark mode (uses Task Dialog when dark mode is active). +// Returns the same kind of value as MessageBoxEx (IDOK, IDYES, IDNO, IDCANCEL, ...). +int DarkMode_MessageBox(HWND hwnd, LPCWSTR text, LPCWSTR caption, UINT uType, WORD wLanguageId) noexcept; + +// Broadcast WM_THEMECHANGED to the window and all its descendants so that +// themed controls (including scroll bars) re-open their theme handles. +void DarkMode_BroadcastThemeChanged(HWND hwnd) noexcept; diff --git a/src/Dialogs.cpp b/src/Dialogs.cpp index 453062b1a4..ed6891fa15 100644 --- a/src/Dialogs.cpp +++ b/src/Dialogs.cpp @@ -34,6 +34,7 @@ #include "Styles.h" #include "Dlapi.h" #include "Dialogs.h" +#include "DarkMode.h" #include "resource.h" #include "Version.h" @@ -114,7 +115,7 @@ int MsgBox(UINT uType, UINT uIdMsg, ...) noexcept { HWND hwnd = GetMsgBoxParent(); PostMessage(hwndMain, APPM_CENTER_MESSAGE_BOX, AsInteger(hwnd), 0); - return MessageBoxEx(hwnd, szText, szTitle, uType, lang); + return DarkMode_MessageBox(hwnd, szText, szTitle, uType, lang); } //============================================================================= diff --git a/src/Notepad4.cpp b/src/Notepad4.cpp index 4c0f1bcce5..7885122350 100644 --- a/src/Notepad4.cpp +++ b/src/Notepad4.cpp @@ -36,6 +36,8 @@ #include "Edit.h" #include "Styles.h" #include "Dialogs.h" +#include "DarkMode.h" +#include "Darkmodelib.h" #include "resource.h" //! show code folding level and state on line number margin @@ -590,6 +592,9 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLi // Load Settings LoadSettings(); + // Initialize dark mode support + DarkMode_Init(); + if (!InitApplication(hInstance)) { CleanUpResources(false); return FALSE; @@ -1103,6 +1108,9 @@ LRESULT CALLBACK MainWndProc(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM lParam) // Remove tray icon if necessary ShowNotifyIcon(hwnd, false); + // Clean up dark mode resources + DarkMode_Cleanup(); + bShutdownOK = true; } if (umsg == WM_DESTROY) { @@ -1148,7 +1156,7 @@ LRESULT CALLBACK MainWndProc(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM lParam) case WM_SETTINGCHANGE: bitmapCache.Invalidate(); - // TODO: detect system theme and high contrast mode changes + DarkMode_HandleSettingChange(hwnd, lParam); SendMessage(hwndEdit, WM_SETTINGCHANGE, wParam, lParam); Style_SetLexer(pLexCurrent, false); // override base elements break; @@ -1836,6 +1844,9 @@ LRESULT MsgCreate(HWND hwnd, WPARAM wParam, LPARAM lParam) noexcept { #endif DragAcceptFiles(hwnd, TRUE); + // Apply dark mode to main window + DarkMode_ApplyToWindow(hwnd); + // File MRU const int flags = MRUFlags_FilePath | (static_cast(flagRelativeFileMRU) * MRUFlags_RelativePath) | (static_cast(flagPortableMyDocs) * MRUFlags_PortableMyDocs); mruFile.Init(MRU_KEY_RECENT_FILES, iMaxRecentFiles, flags); @@ -1975,6 +1986,8 @@ void CreateBars(HWND hwnd, HINSTANCE hInstance) noexcept { GetWindowRect(hwndReBar, &rc); cyReBar = rc.bottom - rc.top; cyReBarFrame = bIsAppThemed ? 0 : 2; + + DarkMode_ApplyToBars(hwnd, hwndToolbar, hwndReBar, hwndStatus); } void RecreateBars(HWND hwnd, HINSTANCE hInstance) noexcept { @@ -2040,6 +2053,9 @@ void MsgThemeChanged(HWND hwnd, WPARAM wParam, LPARAM lParam) noexcept { SetWindowExStyle(hwndEdit, dwExStyle); SetWindowPos(hwndEdit, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE); + // refresh dark mode on theme change + DarkMode_ApplyToWindow(hwnd); + // recreate toolbar and statusbar HINSTANCE hInstance = GetWindowInstance(hwnd); RecreateBars(hwnd, hInstance); @@ -3794,6 +3810,11 @@ LRESULT MsgCommand(HWND hwnd, WPARAM wParam, LPARAM lParam) { case IDM_VIEW_STYLE_THEME_DEFAULT: case IDM_VIEW_STYLE_THEME_DARK: Style_OnStyleThemeChanged(LOWORD(wParam) - IDM_VIEW_STYLE_THEME_DEFAULT); + DarkMode_ApplyToBars(hwnd, hwndToolbar, hwndReBar, hwndStatus); + // Make controls (including scroll bars) re-open theme handles after + // updating the dark/light scroll bar theme. + DarkMode_BroadcastThemeChanged(hwnd); + RedrawWindow(hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW | RDW_FRAME); break; case IDM_VIEW_DEFAULT_CODE_FONT: @@ -4080,7 +4101,20 @@ LRESULT MsgCommand(HWND hwnd, WPARAM wParam, LPARAM lParam) { break; case IDM_VIEW_CUSTOMIZE_TOOLBAR: - SendMessage(hwndToolbar, TB_CUSTOMIZE, 0, 0); + if (DarkMode_IsEnabled()) { + // Temporarily override system colors so comctl32's owner-drawn + // list boxes in the Customize Toolbar dialog use dark colors + const int indices[] = { COLOR_WINDOW, COLOR_WINDOWTEXT, COLOR_BTNFACE, COLOR_BTNTEXT }; + const COLORREF oldColors[] = { GetSysColor(COLOR_WINDOW), GetSysColor(COLOR_WINDOWTEXT), + GetSysColor(COLOR_BTNFACE), GetSysColor(COLOR_BTNTEXT) }; + const COLORREF darkColors[] = { RGB(0x1E, 0x1E, 0x1E), RGB(0xD4, 0xD4, 0xD4), + RGB(0x25, 0x25, 0x26), RGB(0xD4, 0xD4, 0xD4) }; + SetSysColors(COUNTOF(indices), indices, darkColors); + SendMessage(hwndToolbar, TB_CUSTOMIZE, 0, 0); + SetSysColors(COUNTOF(indices), indices, oldColors); + } else { + SendMessage(hwndToolbar, TB_CUSTOMIZE, 0, 0); + } break; case IDM_VIEW_AUTO_SCALE_TOOLBAR: diff --git a/src/Styles.cpp b/src/Styles.cpp index fdcdd5e8de..0d47bb020f 100644 --- a/src/Styles.cpp +++ b/src/Styles.cpp @@ -37,6 +37,7 @@ #include "Edit.h" #include "Styles.h" #include "Dialogs.h" +#include "DarkMode.h" #include "resource.h" extern EDITLEXER lexGlobal; @@ -1195,6 +1196,13 @@ void Style_OnStyleThemeChanged(int theme) noexcept { } np2StyleTheme = theme; bCustomColorLoaded = false; + + // Toggle dark mode UI to match the new style theme + DarkMode_OnThemeChanged(theme); + if (hwndMain) { + DarkMode_ApplyToWindow(hwndMain); + } + Style_SetLexer(pLexCurrent, false); }