comparison foosdk/sdk/libPPUI/DarkMode.cpp @ 1:20d02a178406 default tip

*: check in everything else yay
author Paper <paper@tflc.us>
date Mon, 05 Jan 2026 02:15:46 -0500
parents
children
comparison
equal deleted inserted replaced
0:e9bb126753e7 1:20d02a178406
1 #include "stdafx.h"
2 #include "DarkMode.h"
3 #include "DarkModeEx.h"
4 #include "win32_utility.h"
5 #include "win32_op.h"
6 #include "PaintUtils.h"
7 #include "ImplementOnFinalMessage.h"
8 #include <vsstyle.h>
9 #include "GDIUtils.h"
10 #include "CListControl.h"
11 #include "CListControl-Subst.h"
12 #include "ReStyleWnd.h"
13 #include <map>
14
15 // Allow scary undocumented ordinal-dll-export functions?
16 #define DARKMODE_ALLOW_HAX 1
17
18 #define DARKMODE_DEBUG 0
19
20 #if DARKMODE_DEBUG
21 #define DARKMODE_DEBUG_PRINT(...) PFC_DEBUG_PRINT("DarkMode: ", __VA_ARGS__)
22 #else
23 #define DARKMODE_DEBUG_PRINT(...)
24 #endif
25
26
27 #include <dwmapi.h>
28 #pragma comment(lib, "dwmapi.lib")
29
30 /*
31
32 ==== DARK MODE KNOWLEDGE BASE ====
33
34 == Generic window ==
35 UpdateTitleBar() to set title bar to black
36
37 == Dialog box ==
38 Use WM_CTLCOLORDLG, background of 0x383838
39 UpdateTitleBar() to set title bar to black
40
41 == Edit box ==
42 Use WM_CTLCOLOREDIT, background of 0x383838
43 ApplyDarkThemeCtrl() with "Explorer"
44
45 == Drop list combo ==
46 Method #1: ::SetWindowTheme(wnd, L"DarkMode_CFD", nullptr);
47 Method #2: ::SetWindowTheme(wnd, L"", L""); to obey WM_CTLCOLOR* but leaves oldstyle classic-colors button and breaks comboboxex
48
49 == Button ==
50 Use WM_CTLCOLORBTN, background of 0x383838
51 ApplyDarkThemeCtrl() with "Explorer"
52
53 == Scroll bar ==
54 ApplyDarkThemeCtrl() with "Explorer"
55 ^^ on either scrollbar or the window that implicitly creates scrollbars
56
57 == Header ==
58 ApplyDarkThemeCtrl() with "ItemsView"
59 Handle custom draw, override text color
60
61 == Tree view ==
62 ApplyDarkThemeCtrl() with "Explorer"
63 Set text/bk colors explicitly
64
65 Text color: 0xdedede
66 Background: 0x191919
67
68 Label-editing:
69 Pass WM_CTLCOLOR* to parent, shim TVM_EDITLABEL to pass theme to the editbox (not really necessary tho)
70
71
72 == Rebar ==
73 Can be beaten into working to some extent with a combination of:
74 * ::SetWindowTheme(toolbar, L"", L""); or else RB_SETTEXTCOLOR & REBARBANDINFO colors are disregarded
75 * Use RB_SETTEXTCOLOR / SetTextColor() + RB_SETBKCOLOR / SetBkColor() to override text/background colors
76 * Override WM_ERASEBKGND to draw band frames, RB_SETBKCOLOR doesn't seem to be thorough
77 * NM_CUSTOMDRAW on parent window to paint band labels & grippers without annoying glitches
78 NM_CUSTOMDRAW is buggy, doesn't hand you band indexes to query text, have to use hit tests to know what text to render
79
80 Solution: full custom draw
81
82
83 == Toolbar ==
84 Source: https://stackoverflow.com/questions/61271578/winapi-toolbar-text-color
85 Respects background color of its parent
86 Override text:
87 ::SetWindowTheme(toolbar, L"", L""); or else NM_CUSTOMDRAW color is disregarded
88 NM_CUSTOMDRAW handler:
89 switch (cd->nmcd.dwDrawStage) {
90 case CDDS_PREPAINT: return CDRF_NOTIFYITEMDRAW;
91 case CDDS_ITEMPREPAINT: cd->clrText = DarkMode::GetSysColor(COLOR_WINDOWTEXT); return CDRF_DODEFAULT;
92 }
93
94 == Tab control ==
95 Full custom draw, see CTabsHook
96
97 == List View ==
98 Dark scrollbars are shown only if using "Explorer" theme, but other stuff needs "ItemsView" theme???
99 Other projects shim Windows functions to bypass the above.
100 Avoid using List View, use libPPUI CListControl instead.
101
102 == List Box ==
103 Use WM_CTLCOLOR*
104
105 == Status Bar ==
106 Full custom draw
107
108 == Check box, radio button ==
109 SetWindowTheme(wnd, L"", L""); works but not 100% pretty, disabled text ugly in particular
110 Full custom draw preferred
111
112 == Group box ===
113 SetWindowTheme(wnd, L"", L""); works but not 100% pretty, disabled text ugly in particular
114 Full custom draw preferred (we don't do this).
115 Avoid disabling groupboxes / use something else.
116
117 ==== NOTES ====
118 AllowDarkModeForWindow() needs SetPreferredAppMode() to take effect, hence we implicitly call it
119 AllowDarkModeForWindow() must be called BEFORE SetWindowTheme() to take effect
120
121 It seems it is interchangeable to do:
122 AllowDarkModeForWindow(wnd, true); SetWindowTheme(wnd, L"foo", nullptr);
123 vs
124 SetWindowTheme(wnd, L"DarkMode_foo", nullptr)
125 But the latter doesn't require undocumented function calls and doesn't infect all menus with dark mode
126 */
127
128 namespace {
129 enum class PreferredAppMode
130 {
131 Default,
132 AllowDark,
133 ForceDark,
134 ForceLight,
135 Max
136 };
137
138 enum WINDOWCOMPOSITIONATTRIB
139 {
140 WCA_UNDEFINED = 0,
141 WCA_NCRENDERING_ENABLED = 1,
142 WCA_NCRENDERING_POLICY = 2,
143 WCA_TRANSITIONS_FORCEDISABLED = 3,
144 WCA_ALLOW_NCPAINT = 4,
145 WCA_CAPTION_BUTTON_BOUNDS = 5,
146 WCA_NONCLIENT_RTL_LAYOUT = 6,
147 WCA_FORCE_ICONIC_REPRESENTATION = 7,
148 WCA_EXTENDED_FRAME_BOUNDS = 8,
149 WCA_HAS_ICONIC_BITMAP = 9,
150 WCA_THEME_ATTRIBUTES = 10,
151 WCA_NCRENDERING_EXILED = 11,
152 WCA_NCADORNMENTINFO = 12,
153 WCA_EXCLUDED_FROM_LIVEPREVIEW = 13,
154 WCA_VIDEO_OVERLAY_ACTIVE = 14,
155 WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15,
156 WCA_DISALLOW_PEEK = 16,
157 WCA_CLOAK = 17,
158 WCA_CLOAKED = 18,
159 WCA_ACCENT_POLICY = 19,
160 WCA_FREEZE_REPRESENTATION = 20,
161 WCA_EVER_UNCLOAKED = 21,
162 WCA_VISUAL_OWNER = 22,
163 WCA_HOLOGRAPHIC = 23,
164 WCA_EXCLUDED_FROM_DDA = 24,
165 WCA_PASSIVEUPDATEMODE = 25,
166 WCA_USEDARKMODECOLORS = 26,
167 WCA_LAST = 27
168 };
169
170 struct WINDOWCOMPOSITIONATTRIBDATA
171 {
172 WINDOWCOMPOSITIONATTRIB Attrib;
173 PVOID pvData;
174 SIZE_T cbData;
175 };
176 #if DARKMODE_ALLOW_HAX
177 using fnAllowDarkModeForWindow = bool (WINAPI*)(HWND hWnd, bool allow); // ordinal 133
178 using fnSetPreferredAppMode = PreferredAppMode(WINAPI*)(PreferredAppMode appMode); // ordinal 135, since 1809
179 using fnFlushMenuThemes = void (WINAPI*)(); // ordinal 136
180 fnAllowDarkModeForWindow _AllowDarkModeForWindow = nullptr;
181 fnSetPreferredAppMode _SetPreferredAppMode = nullptr;
182 fnFlushMenuThemes _FlushMenuThemes = nullptr;
183
184 bool ImportsInited = false;
185
186 void InitImports() {
187 if (ImportsInited) return;
188 if (DarkMode::IsSupportedSystem()) {
189 HMODULE hUxtheme = LoadLibraryEx(L"uxtheme.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
190 if (hUxtheme) {
191 _AllowDarkModeForWindow = reinterpret_cast<fnAllowDarkModeForWindow>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133)));
192 _SetPreferredAppMode = reinterpret_cast<fnSetPreferredAppMode>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135)));
193 _FlushMenuThemes = reinterpret_cast<fnFlushMenuThemes>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(136)));
194 }
195 }
196 ImportsInited = true;
197 }
198 #endif
199 }
200
201
202
203 namespace DarkMode {
204 UINT msgSetDarkMode() {
205 // No need to threadguard this, should be main thread only, not much harm even if it's not
206 static UINT val = 0;
207 if (val == 0) val = RegisterWindowMessage(L"libPPUI:msgSetDarkMode");
208 return val;
209 }
210 bool IsSupportedSystem() {
211 return Win10BuildNumber() >= 17763 && !IsWine(); // require at least Win10 1809 / Server 2019
212 }
213 bool IsWindows11() {
214 return Win10BuildNumber() >= 22000;
215 }
216 bool QueryUserOption() {
217 DWORD v = 0;
218 DWORD cb = sizeof(v);
219 DWORD type = 0;
220 if (RegGetValue(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", RRF_RT_REG_DWORD, &type, &v, &cb) == 0) {
221 if (type == REG_DWORD) {
222 return v == 0;
223 }
224 }
225 return false;
226 }
227 void UpdateTitleBar(HWND hWnd, bool bDark) {
228 if (!IsSupportedSystem()) return;
229
230 CWindow wnd(hWnd);
231 DWORD style = wnd.GetStyle();
232 if ((style & WS_CAPTION) != WS_CAPTION) return;
233
234 #if 0
235 // Some apps do this - no idea why, doesn't work
236 // Kept for future reference
237 AllowDarkModeForWindow(hWnd, bDark);
238 SetProp(hWnd, L"UseImmersiveDarkModeColors", (HANDLE)(INT_PTR)(bDark ? TRUE : FALSE));
239 #endif
240
241 if (IsWindows11()) {
242 // DwmSetWindowAttribute()
243 // Windows 11 : works
244 // Windows 10 @ late 2021 : doesn't work
245 // Server 2019 : as good as SetWindowCompositionAttribute(), needs ModifyStyle() hack for full effect
246 BOOL arg = !!bDark;
247 DwmSetWindowAttribute(hWnd, 19 /*DWMWA_USE_IMMERSIVE_DARK_MODE*/, &arg, sizeof(arg));
248 } else {
249 // Windows 10 mode
250 using fnSetWindowCompositionAttribute = BOOL(WINAPI*)(HWND hWnd, WINDOWCOMPOSITIONATTRIBDATA*);
251 static fnSetWindowCompositionAttribute _SetWindowCompositionAttribute = reinterpret_cast<fnSetWindowCompositionAttribute>(GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetWindowCompositionAttribute"));
252 if (_SetWindowCompositionAttribute != nullptr) {
253 BOOL dark = !!bDark;
254 WINDOWCOMPOSITIONATTRIBDATA data = { WCA_USEDARKMODECOLORS, &dark, sizeof(dark) };
255 _SetWindowCompositionAttribute(hWnd, &data);
256
257 // Neither of these fixes stuck titlebar (kept in here for future reference)
258 // ::RedrawWindow(hWnd, NULL, NULL, RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_UPDATENOW);
259 // ::SetWindowPos(hWnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_DRAWFRAME);
260
261 // Apparently the least painful way to reliably fix stuck titlebar
262 // 2x SWP_FRAMECHANGED needed with actual style changes
263
264 if (style & WS_VISIBLE) { // Only do this if visible
265 wnd.ModifyStyle(WS_BORDER, 0, SWP_FRAMECHANGED);
266 wnd.ModifyStyle(0, WS_BORDER, SWP_FRAMECHANGED);
267 }
268
269 }
270 }
271 }
272
273 void ApplyDarkThemeCtrl2(HWND ctrl, bool bDark, const wchar_t* ThemeID_light, const wchar_t * ThemeID_dark) {
274 if (ctrl == NULL) return;
275 AllowDarkModeForWindow(ctrl, bDark);
276 if (bDark && IsSupportedSystem()) {
277 ::SetWindowTheme(ctrl, ThemeID_dark, NULL);
278 } else {
279 ::SetWindowTheme(ctrl, ThemeID_light, NULL);
280 }
281 }
282
283 void ApplyDarkThemeCtrl(HWND ctrl, bool bDark, const wchar_t* ThemeID) {
284 if ( ctrl == NULL ) return;
285 AllowDarkModeForWindow(ctrl, bDark);
286 if (bDark && IsSupportedSystem()) {
287 std::wstring temp = L"DarkMode_"; temp += ThemeID;
288 ::SetWindowTheme(ctrl, temp.c_str(), NULL);
289 } else {
290 ::SetWindowTheme(ctrl, ThemeID, NULL);
291 }
292 }
293
294 void DarkenEditLite(HWND ctrl) {
295 if (IsSupportedSystem()) {
296 ::SetWindowTheme(ctrl, L"DarkMode_Explorer", NULL);
297 }
298 }
299
300 void DarkenComboLite(HWND ctrl) {
301 if (IsSupportedSystem()) {
302 ::SetWindowTheme(ctrl, L"DarkMode_CFD", NULL);
303 }
304 }
305
306 bool IsDCDark(HDC dc_) {
307 CDCHandle dc(dc_);
308 return IsThemeDark(dc.GetTextColor(), dc.GetBkColor());
309 }
310 bool IsDialogDark(HWND dlg, UINT msgSend) {
311 CWindowDC dc(dlg);
312 dc.SetTextColor(0x000000);
313 dc.SetBkColor(0xFFFFFF);
314 ::SendMessage(dlg, msgSend, (WPARAM)dc.m_hDC, (LPARAM)dlg);
315 return IsDCDark(dc);
316 }
317
318 COLORREF GetSysColor(int idx, bool bDark) {
319 if (!bDark) return ::GetSysColor(idx);
320 switch (idx) {
321 case COLOR_MENU:
322 case COLOR_BTNFACE:
323 case COLOR_WINDOW:
324 case COLOR_MENUBAR:
325 // Explorer:
326 // return 0x383838;
327 return 0x202020;
328 case COLOR_BTNSHADOW:
329 return 0;
330 case COLOR_WINDOWTEXT:
331 case COLOR_MENUTEXT:
332 case COLOR_BTNTEXT:
333 case COLOR_CAPTIONTEXT:
334 // Explorer:
335 // return 0xdedede;
336 return 0xC0C0C0;
337 case COLOR_BTNHIGHLIGHT:
338 case COLOR_MENUHILIGHT:
339 return 0x383838;
340 case COLOR_HIGHLIGHT:
341 return 0x777777;
342 case COLOR_HIGHLIGHTTEXT:
343 return 0x101010;
344 case COLOR_GRAYTEXT:
345 return 0x777777;
346 case COLOR_HOTLIGHT:
347 return 0xd69c56;
348 default:
349 return ::GetSysColor(idx);
350 }
351 }
352 #if DARKMODE_ALLOW_HAX
353 void SetAppDarkMode(bool bDark) {
354 InitImports();
355
356 if (_SetPreferredAppMode != nullptr) {
357 static PreferredAppMode lastMode = PreferredAppMode::Default;
358 PreferredAppMode wantMode = bDark ? PreferredAppMode::ForceDark : PreferredAppMode::ForceLight;
359 if (lastMode != wantMode) {
360 _SetPreferredAppMode(wantMode);
361 lastMode = wantMode;
362 if (_FlushMenuThemes) _FlushMenuThemes();
363 }
364 }
365 }
366 #else
367 void SetAppDarkMode(bool) {}
368 #endif
369 #if DARKMODE_ALLOW_HAX
370 void AllowDarkModeForWindow(HWND wnd, bool bDark) {
371 InitImports();
372
373 if (_AllowDarkModeForWindow) _AllowDarkModeForWindow(wnd, bDark);
374 }
375 #else
376 void AllowDarkModeForWindow(HWND, bool) {}
377 #endif
378
379 bool IsThemeDark(COLORREF text, COLORREF background) {
380 if (!IsSupportedSystem() || IsHighContrast()) return false;
381 auto l_text = PaintUtils::Luminance(text);
382 auto l_bk = PaintUtils::Luminance(background);
383 if (l_text > l_bk) {
384 if (l_bk <= PaintUtils::Luminance(GetSysColor(COLOR_BTNFACE))) {
385 return true;
386 }
387 }
388 return false;
389 }
390
391 bool IsHighContrast() {
392 HIGHCONTRASTW highContrast = { sizeof(highContrast) };
393 if (SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(highContrast), &highContrast, FALSE))
394 return (highContrast.dwFlags & HCF_HIGHCONTRASTON) != 0;
395 return false;
396 }
397
398 static void DrawTab(CTabCtrl& tabs, CDCHandle dc, int iTab, bool selected, bool focused, const RECT * rcPaint) {
399 (void)focused;
400 PFC_ASSERT((tabs.GetStyle() & TCS_VERTICAL) == 0);
401
402 CRect rc;
403 if (!tabs.GetItemRect(iTab, rc)) return;
404
405 if ( rcPaint != nullptr ) {
406 CRect foo;
407 if (!foo.IntersectRect(rc, rcPaint)) return;
408 }
409 const int edgeCX = MulDiv(1, QueryScreenDPI_X(tabs), 120); // note: MulDiv() rounds up from +0.5, this will
410 const auto colorBackground = GetSysColor(selected ? COLOR_HIGHLIGHT : COLOR_BTNFACE);
411 const auto colorFrame = GetSysColor(COLOR_WINDOWFRAME);
412 dc.SetDCBrushColor(colorBackground);
413 dc.FillSolidRect(rc, colorBackground);
414
415 {
416 CPen pen;
417 WIN32_OP_D(pen.CreatePen(PS_SOLID, edgeCX, colorFrame));
418 SelectObjectScope scope(dc, pen);
419 dc.MoveTo(rc.left, rc.bottom);
420 dc.LineTo(rc.left, rc.top);
421 dc.LineTo(rc.right, rc.top);
422 dc.LineTo(rc.right, rc.bottom);
423 }
424
425 wchar_t text[512] = {};
426 TCITEM item = {};
427 item.mask = TCIF_TEXT;
428 item.pszText = text;
429 item.cchTextMax = (int)(std::size(text) - 1);
430 if (tabs.GetItem(iTab, &item)) {
431 SelectObjectScope fontScope(dc, tabs.GetFont());
432 dc.SetBkMode(TRANSPARENT);
433 dc.SetTextColor(GetSysColor(selected ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT));
434 dc.DrawText(text, (int)wcslen(text), rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
435 }
436 }
437
438 void PaintTabs(CTabCtrl tabs, CDCHandle dc, const RECT * rcPaint) {
439 CRect rcClient; tabs.GetClientRect(rcClient);
440 CRect rcArea = rcClient; tabs.AdjustRect(FALSE, rcArea);
441 int dx = rcClient.bottom - rcArea.bottom;
442 int dy = rcClient.right - rcArea.right;
443 CRect rcFrame = rcArea; rcFrame.InflateRect(dx/2, dy/2);
444 dc.SetDCBrushColor(GetSysColor(COLOR_WINDOWFRAME));
445 dc.FrameRect(rcFrame, (HBRUSH)GetStockObject(DC_BRUSH));
446 const int tabCount = tabs.GetItemCount();
447 const int tabSelected = tabs.GetCurSel();
448 const int tabFocused = tabs.GetCurFocus();
449 for (int iTab = 0; iTab < tabCount; ++iTab) {
450 if (iTab != tabSelected) DrawTab(tabs, dc, iTab, false, iTab == tabFocused, rcPaint);
451 }
452 if (tabSelected >= 0) DrawTab(tabs, dc, tabSelected, true, tabSelected == tabFocused, rcPaint);
453 }
454
455 void PaintTabsErase(CTabCtrl tabs, CDCHandle dc) {
456 CRect rcClient; WIN32_OP_D(tabs.GetClientRect(rcClient));
457 dc.FillSolidRect(&rcClient, GetSysColor(COLOR_BTNFACE));
458 }
459
460
461 // =================================================
462 // NM_CUSTOMDRAW handlers
463 // =================================================
464
465 // We keep a global list of HWNDs that require dark rendering, so dialogs can call DarkMode::OnCustomDraw() which deals with this nonsense behind the scenes
466 // This way there's no need to subclass parent windows at random
467
468 enum class whichDark_t {
469 none, toolbar
470 };
471
472 // readWriteLock used in case someone uses off-main-thread UI, though it should not really happen in real life
473 static pfc::readWriteLock lstDarkGuard;
474 static std::map<HWND, whichDark_t> lstDark;
475 static whichDark_t lstDark_query(HWND w) {
476 PFC_INSYNC_READ(lstDarkGuard);
477 auto iter = lstDark.find(w);
478 if (iter == lstDark.end()) return whichDark_t::none;
479 return iter->second;
480 }
481 static void lstDark_set(HWND w, whichDark_t which) {
482 PFC_INSYNC_WRITE(lstDarkGuard);
483 lstDark[w] = which;
484 }
485 static void lstDark_clear(HWND w) {
486 PFC_INSYNC_WRITE(lstDarkGuard);
487 lstDark.erase(w);
488 }
489
490 LRESULT CustomDrawToolbar(NMHDR* hdr) {
491 LPNMTBCUSTOMDRAW cd = reinterpret_cast<LPNMTBCUSTOMDRAW>(hdr);
492 switch (cd->nmcd.dwDrawStage) {
493 case CDDS_PREPAINT: return CDRF_NOTIFYITEMDRAW;
494 case CDDS_ITEMPREPAINT:
495 cd->clrText = DarkMode::GetSysColor(COLOR_WINDOWTEXT);
496 cd->clrBtnFace = DarkMode::GetSysColor(COLOR_BTNFACE);
497 cd->clrBtnHighlight = DarkMode::GetSysColor(COLOR_BTNHIGHLIGHT);
498 return CDRF_DODEFAULT;
499 default:
500 return CDRF_DODEFAULT;
501 }
502 }
503
504 LRESULT OnCustomDraw(int,NMHDR* hdr, BOOL& bHandled) {
505 switch (lstDark_query(hdr->hwndFrom)) {
506 case whichDark_t::toolbar:
507 bHandled = TRUE;
508 return CustomDrawToolbar(hdr);
509 default:
510 bHandled = FALSE; return 0;
511 }
512 }
513
514 namespace {
515
516 class CToolbarHook {
517 bool m_dark = false;
518 const bool m_explorerTheme;
519 CToolBarCtrl m_wnd;
520 public:
521 CToolbarHook(HWND wnd, bool initial, bool bExplorerTheme) : m_wnd(wnd), m_explorerTheme(bExplorerTheme) {
522 SetDark(initial);
523 }
524
525 void SetDark(bool v) {
526 if (m_dark == v) return;
527 m_dark = v;
528 if (v) {
529 lstDark_set(m_wnd, whichDark_t::toolbar);
530 if (m_explorerTheme) ::SetWindowTheme(m_wnd, L"", L""); // if we don't do this, NM_CUSTOMDRAW color overrides get disregarded
531 } else {
532 lstDark_clear(m_wnd);
533 if (m_explorerTheme) ::SetWindowTheme(m_wnd, L"Explorer", NULL);
534 }
535 m_wnd.Invalidate();
536
537 ApplyDarkThemeCtrl(m_wnd.GetToolTips(), v);
538 }
539 ~CToolbarHook() {
540 if (m_dark) lstDark_clear(m_wnd);
541 }
542
543 };
544
545 class CTabsHook : public CWindowImpl<CTabsHook, CTabCtrl> {
546 public:
547 CTabsHook(bool bDark = false) : m_dark(bDark) {}
548 BEGIN_MSG_MAP_EX(CTabsHook)
549 MSG_WM_PAINT(OnPaint)
550 MSG_WM_ERASEBKGND(OnEraseBkgnd)
551 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
552 END_MSG_MAP()
553
554 void SetDark(bool v = false);
555 private:
556 void OnPaint(CDCHandle);
557 BOOL OnEraseBkgnd(CDCHandle);
558
559 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
560 switch (wp) {
561 case 0: SetDark(false); break;
562 case 1: SetDark(true); break;
563 }
564 return 1;
565 }
566
567 bool m_dark = false;
568 };
569
570 void CTabsHook::OnPaint(CDCHandle target) {
571 if (!m_dark) { SetMsgHandled(FALSE); return; }
572 if (target) {
573 PaintTabs(m_hWnd, target);
574 } else {
575 CPaintDC dc(*this);
576 PaintTabs(m_hWnd, dc.m_hDC, &dc.m_ps.rcPaint);
577 }
578 }
579 BOOL CTabsHook::OnEraseBkgnd(CDCHandle dc) {
580 if (m_dark) {
581 PaintTabsErase(*this, dc);
582 return TRUE;
583 }
584 SetMsgHandled(FALSE);
585 return FALSE;
586 }
587
588 void CTabsHook::SetDark(bool v) {
589 m_dark = v;
590 if (m_hWnd != NULL) Invalidate();
591
592 ApplyDarkThemeCtrl(GetToolTips(), v);
593 }
594
595 class CTreeViewHook : public CWindowImpl<CTreeViewHook, CTreeViewCtrl> {
596 bool m_dark;
597 public:
598 CTreeViewHook(bool v) : m_dark(v) {}
599
600 BEGIN_MSG_MAP_EX(CTreeViewHook)
601 MESSAGE_RANGE_HANDLER_EX(WM_CTLCOLORMSGBOX, WM_CTLCOLORSTATIC, OnCtlColor)
602 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
603 MESSAGE_HANDLER_EX(TVM_EDITLABEL, OnEditLabel)
604 END_MSG_MAP()
605
606 LRESULT OnCtlColor(UINT uMsg, WPARAM wParam, LPARAM lParam) {
607 return GetParent().SendMessage(uMsg, wParam, lParam);
608 }
609 LRESULT OnEditLabel(UINT, WPARAM, LPARAM) {
610 LRESULT ret = DefWindowProc();
611 if (ret != 0) {
612 HWND edit = (HWND) ret;
613 PFC_ASSERT( ::IsWindow(edit) );
614 ApplyDarkThemeCtrl( edit, m_dark );
615 }
616 return ret;
617 }
618 void SetDark(bool v) {
619 if (m_dark == v) return;
620 m_dark = v;
621 ApplyDark();
622 }
623 void ApplyDark() {
624 ApplyDarkThemeCtrl(m_hWnd, m_dark);
625 COLORREF bk = m_dark ? GetSysColor(COLOR_WINDOW) : (COLORREF)(-1);
626 COLORREF tx = m_dark ? GetSysColor(COLOR_WINDOWTEXT) : (COLORREF)(-1);
627 this->SetTextColor(tx); this->SetLineColor(tx);
628 this->SetBkColor(bk);
629
630 ApplyDarkThemeCtrl(GetToolTips(), m_dark);
631 }
632
633 void SubclassWindow(HWND wnd) {
634 WIN32_OP_D( __super::SubclassWindow(wnd) );
635 this->ApplyDark();
636 }
637
638 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
639 switch (wp) {
640 case 0: SetDark(false); break;
641 case 1: SetDark(true); break;
642 }
643 return 1;
644 }
645 };
646
647 class CDialogHook : public CWindowImpl<CDialogHook> {
648 bool m_enabled;
649 public:
650 CDialogHook(bool v) : m_enabled(v) {}
651
652 void SetDark(bool v) {
653 if (m_enabled == v) return;
654
655 // Important: PostMessage()'ing this caused bugs
656 SendMessage(WM_THEMECHANGED);
657
658 m_enabled = v;
659
660 // Ensure menu bar redraw with RDW_FRAME
661 RedrawWindow(NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_FRAME);
662 }
663
664 // Undocumented Windows menu drawing API
665 // Source: https://github.com/adzm/win32-custom-menubar-aero-theme
666
667 static constexpr unsigned WM_UAHDRAWMENU = 0x0091;
668 static constexpr unsigned WM_UAHDRAWMENUITEM = 0x0092;
669
670 typedef union tagUAHMENUITEMMETRICS
671 {
672 struct {
673 DWORD cx;
674 DWORD cy;
675 } rgsizeBar[2];
676 struct {
677 DWORD cx;
678 DWORD cy;
679 } rgsizePopup[4];
680 } UAHMENUITEMMETRICS;
681
682 typedef struct tagUAHMENUPOPUPMETRICS
683 {
684 DWORD rgcx[4];
685 DWORD fUpdateMaxWidths : 2;
686 } UAHMENUPOPUPMETRICS;
687
688 typedef struct tagUAHMENU
689 {
690 HMENU hmenu;
691 HDC hdc;
692 DWORD dwFlags;
693 } UAHMENU;
694
695 typedef struct tagUAHMENUITEM
696 {
697 int iPosition;
698 UAHMENUITEMMETRICS umim;
699 UAHMENUPOPUPMETRICS umpm;
700 } UAHMENUITEM;
701
702 typedef struct UAHDRAWMENUITEM
703 {
704 DRAWITEMSTRUCT dis;
705 UAHMENU um;
706 UAHMENUITEM umi;
707 } UAHDRAWMENUITEM;
708
709
710 BEGIN_MSG_MAP_EX(CDialogHook)
711 MSG_WM_CTLCOLORDLG(ctlColorCommon)
712 MSG_WM_CTLCOLORSTATIC(ctlColorCommon)
713 MSG_WM_CTLCOLOREDIT(ctlColorCommon)
714 MSG_WM_CTLCOLORBTN(ctlColorCommon)
715 MSG_WM_CTLCOLORLISTBOX(ctlColorCommon)
716 MSG_WM_CTLCOLORSCROLLBAR(ctlColorCommon)
717 NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, DarkMode::OnCustomDraw)
718 MESSAGE_HANDLER_EX(WM_UAHDRAWMENU, Handle_WM_UAHDRAWMENU)
719 MESSAGE_HANDLER_EX(WM_UAHDRAWMENUITEM, Handle_WM_UAHDRAWMENUITEM)
720 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
721 END_MSG_MAP()
722
723 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
724 switch (wp) {
725 case 0: SetDark(false); break;
726 case 1: SetDark(true); break;
727 }
728 return 1;
729 }
730
731 static COLORREF GetBkColor() { return DarkMode::GetSysColor(COLOR_WINDOW); }
732 static COLORREF GetTextColor() { return DarkMode::GetSysColor(COLOR_WINDOWTEXT); }
733
734 HBRUSH ctlColorDlg(CDCHandle dc, CWindow wnd) {
735 if (m_enabled && ::IsThemeDialogTextureEnabled(*this)) {
736 auto bkColor = DarkMode::GetSysColor(COLOR_HIGHLIGHT);
737 auto txColor = GetTextColor();
738
739 dc.SetTextColor(txColor);
740 dc.SetBkColor(bkColor);
741 dc.SetDCBrushColor(bkColor);
742 return (HBRUSH)GetStockObject(DC_BRUSH);
743 }
744 return ctlColorCommon(dc, wnd);
745 }
746
747 HBRUSH ctlColorCommon(CDCHandle dc, CWindow wnd) {
748 (void)wnd;
749 if (m_enabled) {
750 auto bkColor = GetBkColor();
751 auto txColor = GetTextColor();
752
753 dc.SetTextColor(txColor);
754 dc.SetBkColor(bkColor);
755 dc.SetDCBrushColor(bkColor);
756 return (HBRUSH)GetStockObject(DC_BRUSH);
757 }
758 SetMsgHandled(FALSE);
759 return NULL;
760 }
761 LRESULT Handle_WM_UAHDRAWMENU(UINT, WPARAM wParam, LPARAM lParam) {
762 if (!m_enabled) {
763 SetMsgHandled(FALSE);
764 return 0;
765 }
766 UAHMENU* pUDM = (UAHMENU*)lParam;
767 CRect rc;
768
769 MENUBARINFO mbi = { sizeof(mbi) };
770 WIN32_OP_D(GetMenuBarInfo(m_hWnd, OBJID_MENU, 0, &mbi));
771
772 CRect rcWindow;
773 WIN32_OP_D(GetWindowRect(rcWindow));
774
775 rc = mbi.rcBar;
776 OffsetRect(&rc, -rcWindow.left, -rcWindow.top);
777
778 rc.top -= 1;
779
780 CDCHandle dc(pUDM->hdc);
781 dc.FillSolidRect(rc, DarkMode::GetSysColor(COLOR_MENUBAR));
782 return 0;
783 }
784 LRESULT Handle_WM_UAHDRAWMENUITEM(UINT, WPARAM wParam, LPARAM lParam) {
785 if (!m_enabled) {
786 SetMsgHandled(FALSE);
787 return 0;
788 }
789
790 UAHDRAWMENUITEM* pUDMI = (UAHDRAWMENUITEM*)lParam;
791 CMenuHandle hMenu = pUDMI->um.hmenu;
792
793 CString menuString;
794 WIN32_OP_D(hMenu.GetMenuString(pUDMI->umi.iPosition, menuString, MF_BYPOSITION) > 0);
795
796 DWORD drawTextFlags = DT_CENTER | DT_SINGLELINE | DT_VCENTER;
797
798 int iTextStateID = MPI_NORMAL;
799 int iBackgroundStateID = MPI_NORMAL;
800 if ((pUDMI->dis.itemState & ODS_INACTIVE) | (pUDMI->dis.itemState & ODS_DEFAULT)) {
801 iTextStateID = MPI_NORMAL;
802 iBackgroundStateID = MPI_NORMAL;
803 }
804 if (pUDMI->dis.itemState & (ODS_HOTLIGHT|ODS_SELECTED)) {
805 iTextStateID = MPI_HOT;
806 iBackgroundStateID = MPI_HOT;
807 }
808 if (pUDMI->dis.itemState & (ODS_GRAYED|ODS_DISABLED)) {
809 iTextStateID = MPI_DISABLED;
810 iBackgroundStateID = MPI_DISABLED;
811 }
812 if (pUDMI->dis.itemState & ODS_NOACCEL) {
813 drawTextFlags |= DT_HIDEPREFIX;
814 }
815
816 if (m_menuTheme == NULL) {
817 m_menuTheme.OpenThemeData(m_hWnd, L"Menu");
818 }
819
820 CDCHandle dc(pUDMI->um.hdc);
821 switch (iBackgroundStateID) {
822 case MPI_NORMAL:
823 case MPI_DISABLED:
824 dc.FillSolidRect(&pUDMI->dis.rcItem, DarkMode::GetSysColor(COLOR_MENUBAR));
825 break;
826 case MPI_HOT:
827 case MPI_DISABLEDHOT:
828 dc.FillSolidRect(&pUDMI->dis.rcItem, DarkMode::GetSysColor(COLOR_MENUHILIGHT));
829 break;
830 default:
831 DrawThemeBackground(m_menuTheme, pUDMI->um.hdc, MENU_POPUPITEM, iBackgroundStateID, &pUDMI->dis.rcItem, nullptr);
832 break;
833 }
834 DTTOPTS dttopts = { sizeof(dttopts) };
835 if (iTextStateID == MPI_NORMAL || iTextStateID == MPI_HOT)
836 {
837 dttopts.dwFlags |= DTT_TEXTCOLOR;
838 dttopts.crText = DarkMode::GetSysColor(COLOR_WINDOWTEXT);
839 }
840 DrawThemeTextEx(m_menuTheme, dc, MENU_POPUPITEM, iTextStateID, menuString, menuString.GetLength(), drawTextFlags, &pUDMI->dis.rcItem, &dttopts);
841
842 return 0;
843 }
844 CTheme m_menuTheme;
845 };
846
847
848 class CStatusBarHook : public CWindowImpl<CStatusBarHook, CStatusBarCtrl> {
849 public:
850 CStatusBarHook(bool v = false) : m_dark(v) {}
851
852 BEGIN_MSG_MAP_EX(CStatusBarHook)
853 MSG_WM_ERASEBKGND(OnEraseBkgnd)
854 MSG_WM_PAINT(OnPaint)
855 MESSAGE_HANDLER_EX(SB_SETTEXT, OnSetText)
856 MESSAGE_HANDLER_EX(SB_SETICON, OnSetIcon)
857 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
858 END_MSG_MAP()
859
860 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
861 switch (wp) {
862 case 0: SetDark(false); break;
863 case 1: SetDark(true); break;
864 }
865 return 1;
866 }
867
868 void SetDark(bool v = true) {
869 if (m_dark != v) {
870 m_dark = v;
871 Invalidate();
872 ApplyDarkThemeCtrl(m_hWnd, v);
873 }
874 }
875
876 void SubclassWindow(HWND wnd) {
877 WIN32_OP_D(__super::SubclassWindow(wnd));
878 Invalidate();
879 ApplyDarkThemeCtrl(m_hWnd, m_dark);
880 }
881 LRESULT OnSetIcon(UINT, WPARAM wp, LPARAM lp) {
882 unsigned idx = (unsigned)wp;
883 if (idx < 32) {
884 CSize sz;
885 if (lp != 0) sz = GetIconSize((HICON)lp);
886 m_iconSizeCache[idx] = sz;
887 }
888 SetMsgHandled(FALSE);
889 return 0;
890 }
891 LRESULT OnSetText(UINT, WPARAM wp, LPARAM) {
892 // Status bar won't tell us about ownerdraw from GetText()
893 // Have to listen to relevant messages to know
894 unsigned idx = (unsigned)(wp & 0xFF);
895 if (idx < 32) {
896 uint32_t flag = 1 << idx;
897 if (wp & SBT_OWNERDRAW) {
898 m_ownerDrawMask |= flag;
899 } else {
900 m_ownerDrawMask &= ~flag;
901 }
902 }
903
904 SetMsgHandled(FALSE);
905 return 0;
906 }
907
908 void Paint(CDCHandle dc) {
909 CRect rcClient; WIN32_OP_D(GetClientRect(rcClient));
910 dc.FillSolidRect(rcClient, GetSysColor(COLOR_BTNFACE)); // Wine seems to not call our WM_ERASEBKGND handler, fill the background here too
911
912 dc.SelectFont(GetFont());
913 dc.SetBkMode(TRANSPARENT);
914 dc.SetTextColor(GetSysColor(COLOR_WINDOWTEXT));
915 CPen pen; pen.CreatePen(PS_SOLID, 1, GetSysColor(COLOR_BTNHIGHLIGHT));
916 dc.SelectPen(pen);
917 int count = this->GetParts(0, nullptr);
918 for (int iPart = 0; iPart < count; ++iPart) {
919 CRect rcPart;
920 this->GetRect(iPart, rcPart);
921 if (rcPart.left > 0) {
922 dc.MoveTo(rcPart.left, rcPart.top);
923 dc.LineTo(rcPart.left, rcPart.bottom);
924 }
925 int type = 0;
926 CString text;
927 this->GetText(iPart, text, &type);
928
929 HICON icon = this->GetIcon(iPart);
930 int iconMargin = 0;
931 if (icon != NULL && (unsigned)iPart < std::size(m_iconSizeCache)) {
932
933 auto size = m_iconSizeCache[iPart];
934
935 dc.DrawIconEx(rcPart.left + size.cx / 4, (rcPart.top + rcPart.bottom) / 2 - size.cy / 2, icon, size.cx, size.cy);
936 iconMargin = MulDiv(size.cx, 3, 2);
937 }
938
939 if (m_ownerDrawMask & (1 << iPart)) { // statusbar won't tell us about ownerdraw from GetText()
940 DRAWITEMSTRUCT ds = {};
941 ds.CtlType = ODT_STATIC;
942 ds.CtlID = this->GetDlgCtrlID();
943 ds.itemID = iPart;
944 ds.hwndItem = m_hWnd;
945 ds.hDC = dc;
946 ds.rcItem = rcPart;
947
948 DCStateScope scope(dc);
949 GetParent().SendMessage(WM_DRAWITEM, GetDlgCtrlID(), (LPARAM)&ds);
950 } else {
951 CRect rcText = rcPart;
952 int defMargin = rcText.Height() / 4;
953 int l = iconMargin > 0 ? iconMargin : defMargin;
954 int r = defMargin;
955 rcText.DeflateRect(l, 0, r, 0);
956 dc.DrawText(text, text.GetLength(), rcText, DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX);
957 }
958
959 if (GetStyle() & SBARS_SIZEGRIP) {
960 CSize size;
961 auto theme = OpenThemeData(*this, L"status");
962 PFC_ASSERT(theme != NULL);
963 GetThemePartSize(theme, dc, SP_GRIPPER, 0, &rcClient, TS_DRAW, &size);
964 auto rc = rcClient;
965 rc.left = rc.right - size.cx;
966 rc.top = rc.bottom - size.cy;
967 DrawThemeBackground(theme, dc, SP_GRIPPER, 0, &rc, nullptr);
968 CloseThemeData(theme);
969 }
970 }
971 }
972
973 void OnPaint(CDCHandle target) {
974 if (!m_dark) { SetMsgHandled(FALSE); return; }
975 if (target) {
976 Paint(target);
977 } else {
978 CPaintDC dc(*this);
979 Paint(dc.m_hDC);
980 }
981 }
982
983 BOOL OnEraseBkgnd(CDCHandle dc) {
984 if (m_dark) {
985 CRect rc; WIN32_OP_D(GetClientRect(rc)); dc.FillSolidRect(rc, DarkMode::GetSysColor(COLOR_BTNFACE)); return TRUE;
986 }
987 SetMsgHandled(FALSE); return FALSE;
988 }
989
990 bool m_dark = false;
991
992 uint32_t m_ownerDrawMask = 0;
993 CSize m_iconSizeCache[32];
994 };
995
996 class CCheckBoxHook : public CWindowImpl<CCheckBoxHook, CButton> {
997 public:
998 CCheckBoxHook(bool v = false) : m_dark(v) {}
999
1000 BEGIN_MSG_MAP_EX(CCheckBoxHook)
1001 MSG_WM_PAINT(OnPaint)
1002 MSG_WM_PRINTCLIENT(OnPaint)
1003 MSG_WM_ERASEBKGND(OnEraseBkgnd)
1004 MSG_WM_UPDATEUISTATE(OnUpdateUIState)
1005
1006 // Note that checkbox implementation likes to paint on its own in response to events
1007 // instead of invalidating and handling WM_PAINT
1008 // We have to specifically trigger WM_PAINT to override their rendering with ours
1009 MESSAGE_HANDLER_EX(WM_SETFOCUS, OnMsgRedraw)
1010 MESSAGE_HANDLER_EX(WM_KILLFOCUS, OnMsgRedraw)
1011 MESSAGE_HANDLER_EX(WM_ENABLE, OnMsgRedraw)
1012 MESSAGE_HANDLER_EX(WM_SETTEXT, OnMsgRedraw)
1013
1014 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
1015 END_MSG_MAP()
1016
1017 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
1018 switch (wp) {
1019 case 0: SetDark(false); break;
1020 case 1: SetDark(true); break;
1021 }
1022 return 1;
1023 }
1024
1025 LRESULT OnMsgRedraw(UINT, WPARAM, LPARAM) {
1026 if ( m_dark ) {
1027 // PROBLEM:
1028 // Can't invalidate prior to their handling of the message
1029 // Causes bugs with specific chains of events - EnableWindow() followed immediately SetWindowText()
1030 LRESULT ret = DefWindowProc();
1031 Invalidate();
1032 return ret;
1033 }
1034 SetMsgHandled(FALSE); return 0;
1035 }
1036
1037 void OnUpdateUIState(WORD nAction, WORD nState) {
1038 (void)nAction;
1039 if ( m_dark && (nState & (UISF_HIDEACCEL | UISF_HIDEFOCUS)) != 0) {
1040 // PROBLEM:
1041 // Can't invalidate prior to their handling of the message
1042 // Causes bugs with specific chains of events - EnableWindow() followed immediately SetWindowText()
1043 DefWindowProc();
1044 Invalidate();
1045 return;
1046 }
1047 SetMsgHandled(FALSE);
1048 }
1049 void PaintHandler(CDCHandle dc) {
1050 CRect rcClient; WIN32_OP_D(GetClientRect(rcClient));
1051
1052 const bool bDisabled = !this->IsWindowEnabled();
1053
1054 dc.SetTextColor(DarkMode::GetSysColor(COLOR_BTNTEXT));
1055 dc.SetBkColor(DarkMode::GetSysColor(COLOR_BTNFACE));
1056 dc.SetBkMode(OPAQUE);
1057 dc.SelectFont(GetFont());
1058 GetParent().SendMessage(WM_CTLCOLORBTN, (WPARAM)dc.m_hDC, (LPARAM)m_hWnd);
1059 if (bDisabled) dc.SetTextColor(DarkMode::GetSysColor(COLOR_GRAYTEXT)); // override WM_CTLCOLORBTN
1060
1061 const DWORD btnStyle = GetStyle();
1062 const DWORD btnType = btnStyle & BS_TYPEMASK;
1063 const bool bRadio = (btnType == BS_RADIOBUTTON || btnType == BS_AUTORADIOBUTTON);
1064 const int part = bRadio ? BP_RADIOBUTTON : BP_CHECKBOX;
1065
1066 const auto ctrlState = GetState();
1067 const DWORD uiState = (DWORD)SendMessage(WM_QUERYUISTATE);
1068
1069
1070 const bool bChecked = (ctrlState & BST_CHECKED) != 0;
1071 const bool bMixed = (ctrlState & BST_INDETERMINATE) != 0;
1072 const bool bHot = (ctrlState & BST_HOT) != 0;
1073 const bool bFocus = (ctrlState & BST_FOCUS) != 0 && (uiState & UISF_HIDEFOCUS) == 0;
1074
1075 HTHEME theme = OpenThemeData(m_hWnd, L"button");
1076
1077 CRect rcCheckBox = rcClient;
1078 bool bDrawn = false;
1079 int margin = 0;
1080 if (theme != NULL && IsThemePartDefined(theme, part, 0)) {
1081 int state = 0;
1082 if (bDisabled) {
1083 if ( bChecked ) state = CBS_CHECKEDDISABLED;
1084 else if ( bMixed ) state = CBS_MIXEDDISABLED;
1085 else state = CBS_UNCHECKEDDISABLED;
1086 } else if (bHot) {
1087 if ( bChecked ) state = CBS_CHECKEDHOT;
1088 else if ( bMixed ) state = CBS_MIXEDHOT;
1089 else state = CBS_UNCHECKEDNORMAL;
1090 } else {
1091 if ( bChecked ) state = CBS_CHECKEDNORMAL;
1092 else if ( bMixed ) state = CBS_MIXEDNORMAL;
1093 else state = CBS_UNCHECKEDNORMAL;
1094 }
1095
1096 CSize size;
1097 if (SUCCEEDED(GetThemePartSize(theme, dc, part, state, rcCheckBox, TS_TRUE, &size))) {
1098 if (size.cx <= rcCheckBox.Width() && size.cy <= rcCheckBox.Height()) {
1099 CRect rc = rcCheckBox;
1100 margin = MulDiv(size.cx, 5, 4);
1101 // rc.left += (rc.Width() - size.cx) / 2;
1102 rc.top += (rc.Height() - size.cy) / 2;
1103 rc.right = rc.left + size.cx;
1104 rc.bottom = rc.top + size.cy;
1105 DrawThemeBackground(theme, dc, part, state, rc, &rc);
1106 bDrawn = true;
1107 }
1108 }
1109 }
1110 if (theme != NULL) CloseThemeData(theme);
1111 if (!bDrawn) {
1112 int stateEx = bRadio ? DFCS_BUTTONRADIO : DFCS_BUTTONCHECK;
1113 if (bChecked) stateEx |= DFCS_CHECKED;
1114 // FIX ME bMixed ?
1115 if (bDisabled) stateEx |= DFCS_INACTIVE;
1116 else if (bHot) stateEx |= DFCS_HOT;
1117
1118 const int dpi = GetDeviceCaps(dc, LOGPIXELSX);
1119 int w = MulDiv(16, dpi, 96);
1120
1121 CRect rc = rcCheckBox;
1122 if (rc.Width() > w) rc.right = rc.left + w;;
1123
1124 DrawFrameControl(dc, rc, DFC_BUTTON, stateEx);
1125 margin = MulDiv(20, dpi, 96);
1126 }
1127
1128 CString text;
1129 if (margin < rcClient.Width()) GetWindowText(text);
1130 if (!text.IsEmpty()) {
1131 CRect rcText = rcClient;
1132 rcText.left += margin;
1133 UINT dtFlags = DT_VCENTER;
1134 if (btnStyle & BS_MULTILINE) {
1135 dtFlags |= DT_WORDBREAK;
1136 } else {
1137 dtFlags |= DT_END_ELLIPSIS | DT_SINGLELINE;
1138 }
1139 dc.DrawText(text, text.GetLength(), rcText, dtFlags);
1140 if (bFocus) {
1141 dc.DrawText(text, text.GetLength(), rcText, DT_CALCRECT | dtFlags);
1142 dc.DrawFocusRect(rcText);
1143 }
1144 } else if (bFocus) {
1145 dc.DrawFocusRect(rcClient);
1146 }
1147 }
1148 void OnPaint(CDCHandle userDC, UINT flags = 0) {
1149 (void)flags;
1150 if (!m_dark) { SetMsgHandled(FALSE); return; }
1151 if (userDC) {
1152 PaintHandler(userDC);
1153 } else {
1154 CPaintDC dc(*this);
1155 PaintHandler(dc.m_hDC);
1156 }
1157 }
1158 BOOL OnEraseBkgnd(CDCHandle dc) {
1159 if (m_dark) {
1160 CRect rc; WIN32_OP_D(GetClientRect(rc));
1161
1162 dc.SetTextColor(DarkMode::GetSysColor(COLOR_BTNTEXT));
1163 dc.SetBkColor(DarkMode::GetSysColor(COLOR_BTNFACE));
1164 auto br = (HBRUSH)GetParent().SendMessage(WM_CTLCOLORSTATIC, (WPARAM)dc.m_hDC, (LPARAM)m_hWnd);
1165 if (br != NULL) {
1166 dc.FillRect(rc, br);
1167 } else {
1168 dc.FillSolidRect(rc, DarkMode::GetSysColor(COLOR_BTNFACE));
1169 }
1170 return TRUE;
1171 }
1172 SetMsgHandled(FALSE); return FALSE;
1173 }
1174
1175 void SetDark(bool v = true) {
1176 if (v != m_dark) {
1177 m_dark = v;
1178 Invalidate();
1179 ApplyDarkThemeCtrl(m_hWnd, m_dark);
1180 }
1181 }
1182
1183 void SubclassWindow(HWND wnd) {
1184 WIN32_OP_D(__super::SubclassWindow(wnd));
1185 ApplyDarkThemeCtrl(m_hWnd, m_dark);
1186 }
1187
1188 bool m_dark = false;
1189 };
1190
1191 class CGripperHook : public CWindowImpl<CGripperHook> {
1192 public:
1193 CGripperHook(bool v) : m_dark(v) {}
1194
1195 BEGIN_MSG_MAP_EX(CGRipperHook)
1196 MSG_WM_ERASEBKGND(OnEraseBkgnd)
1197 MSG_WM_PAINT(OnPaint)
1198 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
1199 END_MSG_MAP()
1200
1201 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
1202 switch (wp) {
1203 case 0: SetDark(false); break;
1204 case 1: SetDark(true); break;
1205 }
1206 return 1;
1207 }
1208
1209 void SetDark(bool v) {
1210 if (v != m_dark) {
1211 m_dark = v;
1212 ApplyDarkThemeCtrl(*this, m_dark);
1213 }
1214 }
1215
1216 void SubclassWindow(HWND wnd) {
1217 WIN32_OP_D(__super::SubclassWindow(wnd));
1218 ApplyDarkThemeCtrl(m_hWnd, m_dark);
1219 }
1220
1221 void PaintGripper(CDCHandle dc) {
1222 CRect rcClient; WIN32_OP_D(GetClientRect(rcClient));
1223 CSize size;
1224 auto theme = OpenThemeData(*this, L"status");
1225 PFC_ASSERT(theme != NULL);
1226 GetThemePartSize(theme, dc, SP_GRIPPER, 0, &rcClient, TS_DRAW, &size);
1227 auto rc = rcClient;
1228 rc.left = rc.right - size.cx;
1229 rc.top = rc.bottom - size.cy;
1230 DrawThemeBackground(theme, dc, SP_GRIPPER, 0, &rc, nullptr);
1231 CloseThemeData(theme);
1232 }
1233
1234 void OnPaint(CDCHandle dc) {
1235 if (!m_dark) { SetMsgHandled(FALSE); return; }
1236 if (dc) PaintGripper(dc);
1237 else {CPaintDC pdc(*this); PaintGripper(pdc.m_hDC);}
1238 }
1239
1240 BOOL OnEraseBkgnd(CDCHandle dc) {
1241 if (m_dark) {
1242 CRect rc; GetClientRect(rc);
1243 dc.FillSolidRect(rc, GetSysColor(COLOR_WINDOW));
1244 return TRUE;
1245 }
1246 SetMsgHandled(FALSE); return FALSE;
1247 }
1248 bool m_dark = false;
1249 };
1250
1251 class CReBarHook : public CWindowImpl<CReBarHook, CReBarCtrl> {
1252 bool m_dark;
1253 public:
1254 CReBarHook(bool v) : m_dark(v) {}
1255 BEGIN_MSG_MAP_EX(CReBarHook)
1256 MSG_WM_ERASEBKGND(OnEraseBkgnd)
1257 MSG_WM_DESTROY(OnDestroy)
1258 MSG_WM_PAINT(OnPaint)
1259 MSG_WM_PRINTCLIENT(OnPaint)
1260 NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, DarkMode::OnCustomDraw)
1261 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
1262 END_MSG_MAP()
1263
1264 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
1265 switch (wp) {
1266 case 0: SetDark(false); break;
1267 case 1: SetDark(true); break;
1268 }
1269 return 1;
1270 }
1271
1272 void OnPaint(CDCHandle target, unsigned flags = 0) {
1273 if (!m_dark) { SetMsgHandled(FALSE); return; }
1274 (void)flags;
1275 if (target) {
1276 HandlePaint(target);
1277 } else {
1278 CPaintDC pdc(*this);
1279 HandlePaint(pdc.m_hDC);
1280 }
1281 }
1282
1283 void HandlePaint(CDCHandle dc) {
1284 const int total = this->GetBandCount();
1285 for (int iBand = 0; iBand < total; ++iBand) {
1286 CRect rc;
1287 WIN32_OP_D(this->GetRect(iBand, rc));
1288
1289 wchar_t buffer[256] = {};
1290 REBARBANDINFO info = { sizeof(info) };
1291 info.fMask = RBBIM_TEXT | RBBIM_CHILD | RBBIM_STYLE;
1292 info.lpText = buffer; info.cch = (UINT)std::size(buffer);
1293 WIN32_OP_D(this->GetBandInfo(iBand, &info));
1294
1295 HFONT useFont;
1296 // Sadly overriding the font breaks the layout
1297 // MS implementation disregards fonts too
1298 // useFont = this->GetFont();
1299 useFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
1300 SelectObjectScope fontScope(dc, useFont);
1301 dc.SetTextColor(DarkMode::GetSysColor(COLOR_BTNTEXT));
1302 dc.SetBkMode(TRANSPARENT);
1303
1304 CRect rcText = rc;
1305 if ((info.fStyle & RBBS_NOGRIPPER) == 0) {
1306 auto color = PaintUtils::BlendColor(DarkMode::GetSysColor(COLOR_WINDOWFRAME), DarkMode::GetSysColor(COLOR_BTNFACE));
1307 dc.SetDCPenColor(color);
1308 SelectObjectScope penScope(dc, GetStockObject(DC_PEN));
1309 dc.MoveTo(rcText.TopLeft());
1310 dc.LineTo(CPoint(rcText.left, rcText.bottom));
1311 rcText.left += 6; // this should be DPI-scaled, only it's not because rebar layout isn't either
1312 } else {
1313 rcText.left += 2;
1314 }
1315 dc.DrawText(buffer, (int)wcslen(buffer), &rcText, DT_VCENTER | DT_LEFT | DT_SINGLELINE | DT_NOPREFIX);
1316 }
1317 }
1318
1319
1320 void OnDestroy() {
1321 SetMsgHandled(FALSE);
1322 }
1323 BOOL OnEraseBkgnd(CDCHandle dc) {
1324 if (m_dark) {
1325 CRect rc;
1326 WIN32_OP_D(GetClientRect(rc));
1327 dc.FillSolidRect(rc, DarkMode::GetSysColor(COLOR_BTNFACE));
1328 return TRUE;
1329 }
1330 SetMsgHandled(FALSE); return FALSE;
1331 }
1332 void SetDark(bool v) {
1333 if (v != m_dark) {
1334 m_dark = v; Apply();
1335 }
1336 }
1337 void Apply() {
1338 if (m_dark) {
1339 this->SetTextColor(DarkMode::GetSysColor(COLOR_WINDOWTEXT));
1340 this->SetBkColor(DarkMode::GetSysColor(COLOR_BTNFACE));
1341 } else {
1342 this->SetTextColor((COLORREF)-1);
1343 this->SetBkColor((COLORREF)-1);
1344 }
1345 Invalidate();
1346 }
1347 void SubclassWindow(HWND wnd) {
1348 WIN32_OP_D(__super::SubclassWindow(wnd));
1349 Apply();
1350 }
1351 };
1352
1353 class CStaticHook : public CWindowImpl<CStaticHook, CStatic> {
1354 bool m_dark;
1355 public:
1356 CStaticHook(bool v) : m_dark(v) {}
1357
1358 BEGIN_MSG_MAP_EX(CStaticHook)
1359 MSG_WM_PAINT(OnPaint)
1360 MESSAGE_HANDLER_EX(WM_ENABLE, OnMsgRedraw)
1361 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
1362 END_MSG_MAP()
1363
1364 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
1365 switch (wp) {
1366 case 0: SetDark(false); break;
1367 case 1: SetDark(true); break;
1368 }
1369 return 1;
1370 }
1371
1372 LRESULT OnMsgRedraw(UINT, WPARAM, LPARAM) {
1373 Invalidate();
1374 SetMsgHandled(FALSE);
1375 return 0;
1376 }
1377
1378 void SetDark(bool v) {
1379 if (m_dark != v) {
1380 m_dark = v;
1381 Invalidate();
1382 }
1383 }
1384
1385 void OnPaint(CDCHandle dc) {
1386 // ONLY override the dark+disabled or dark+icon behavior
1387 if (!m_dark || (this->IsWindowEnabled() && this->GetIcon() == NULL) ) {
1388 SetMsgHandled(FALSE); return;
1389 }
1390
1391 if (dc) HandlePaint(dc);
1392 else {
1393 CPaintDC pdc(*this); HandlePaint(pdc.m_hDC);
1394 }
1395 }
1396 void HandlePaint(CDCHandle dc) {
1397 CString str;
1398 CIconHandle icon = this->GetIcon();
1399 this->GetWindowTextW(str);
1400
1401 CRect rcClient;
1402 WIN32_OP_D(GetClientRect(rcClient));
1403
1404 const DWORD style = this->GetStyle();
1405
1406 dc.SelectFont(GetFont());
1407
1408 HBRUSH br = (HBRUSH) GetParent().SendMessage(WM_CTLCOLORSTATIC, (WPARAM)dc.m_hDC, (LPARAM)m_hWnd);;
1409 if (br == NULL) {
1410 dc.FillSolidRect(rcClient, DarkMode::GetSysColor(COLOR_WINDOW));
1411 } else {
1412 WIN32_OP_D(dc.FillRect(rcClient, br));
1413 }
1414
1415 if (icon != NULL) {
1416 dc.DrawIcon(0, 0, icon);
1417 } else {
1418 DWORD flags = 0;
1419 if (style & SS_SIMPLE) flags |= DT_SINGLELINE | DT_WORD_ELLIPSIS;
1420 else flags |= DT_WORDBREAK;
1421 if (style & SS_RIGHT) flags |= DT_RIGHT;
1422 else if (style & SS_CENTER) flags |= DT_CENTER;
1423
1424 dc.SetTextColor(DarkMode::GetSysColor(COLOR_GRAYTEXT));
1425 dc.SetBkMode(TRANSPARENT);
1426 dc.DrawText(str, str.GetLength(), rcClient, flags);
1427 }
1428 }
1429 };
1430
1431 class CUpDownHook : public CWindowImpl<CUpDownHook, CUpDownCtrl> {
1432 bool m_dark;
1433 public:
1434 CUpDownHook(bool v) : m_dark(v) {}
1435
1436 void SetDark(bool v) {
1437 if (v != m_dark) {
1438 m_dark = v; Invalidate();
1439 }
1440 }
1441
1442 BEGIN_MSG_MAP_EX(CUpDownHook)
1443 MSG_WM_PAINT(OnPaint)
1444 MSG_WM_PRINTCLIENT(OnPaint)
1445 MSG_WM_MOUSEMOVE(OnMouseMove)
1446 MSG_WM_LBUTTONDOWN(OnMouseBtn)
1447 MSG_WM_LBUTTONUP(OnMouseBtn)
1448 MSG_WM_MOUSELEAVE(OnMouseLeave)
1449 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
1450 END_MSG_MAP()
1451
1452 private:
1453 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
1454 switch (wp) {
1455 case 0: SetDark(false); break;
1456 case 1: SetDark(true); break;
1457 }
1458 return 1;
1459 }
1460
1461 struct layout_t {
1462 CRect whole, upper, lower;
1463 int yCenter;
1464 };
1465 layout_t Layout(CRect const & rcClient) {
1466 CRect rc = rcClient;
1467 rc.DeflateRect(1, 1);
1468 int yCenter = (rc.top + rc.bottom) / 2;
1469 layout_t ret;
1470 ret.yCenter = yCenter;
1471 ret.whole = rc;
1472 ret.upper = rc;
1473 ret.upper.bottom = yCenter;
1474 ret.lower = rc;
1475 ret.lower.top = yCenter;
1476 return ret;
1477 }
1478 layout_t Layout() {
1479 CRect rcClient; WIN32_OP_D(GetClientRect(rcClient)); return Layout(rcClient);
1480 }
1481 int m_hot = 0;
1482 bool m_btnDown = false;
1483 void SetHot(int iHot) {
1484 if (iHot != m_hot) {
1485 m_hot = iHot; Invalidate();
1486 }
1487 }
1488 void OnMouseLeave() {
1489 SetHot(0);
1490 SetMsgHandled(FALSE);
1491 }
1492 int HitTest(CPoint pt) {
1493 auto layout = Layout();
1494 if (layout.upper.PtInRect(pt)) return 1;
1495 else if (layout.lower.PtInRect(pt)) return 2;
1496 else return 0;
1497 }
1498 void OnMouseBtn(UINT flags, CPoint) {
1499 bool bDown = (flags & MK_LBUTTON) != 0;
1500 if (bDown != m_btnDown) {
1501 m_btnDown = bDown;
1502 Invalidate();
1503 }
1504 SetMsgHandled(FALSE);
1505 }
1506 void OnMouseMove(UINT, CPoint pt) {
1507 SetHot(HitTest(pt));
1508 SetMsgHandled(FALSE);
1509 }
1510 void OnPaint(CDCHandle target, unsigned flags = 0) {
1511 if (!m_dark) { SetMsgHandled(FALSE); return; }
1512 (void)flags;
1513 if (target) {
1514 HandlePaint(target);
1515 } else {
1516 CPaintDC pdc(*this);
1517 HandlePaint(pdc.m_hDC);
1518 }
1519 }
1520 void HandlePaint(CDCHandle dc) {
1521 // no corresponding getsyscolor values for this, hardcoded values taken from actual button control
1522 // + frame trying to fit edit control frame
1523 const COLORREF colorText = 0xFFFFFF;
1524 const COLORREF colorFrame = 0xFFFFFF;
1525 const COLORREF colorBk = 0x333333;
1526 const COLORREF colorHot = 0x454545;
1527 const COLORREF colorPressed = 0x666666;
1528
1529 CRect rcClient; WIN32_OP_D(GetClientRect(rcClient));
1530 auto layout = Layout(rcClient);
1531 dc.FillRect(rcClient, MakeTempBrush(dc, colorBk));
1532
1533 if (m_hot != 0) {
1534 auto color = m_btnDown ? colorPressed : colorHot;
1535 switch (m_hot) {
1536 case 1:
1537 dc.FillSolidRect(layout.upper, color);
1538 break;
1539 case 2:
1540 dc.FillSolidRect(layout.lower, color);
1541 break;
1542 }
1543 }
1544
1545 dc.FrameRect(layout.whole, MakeTempBrush(dc, colorFrame));
1546 dc.SetDCPenColor(colorFrame);
1547 dc.SelectPen((HPEN)GetStockObject(DC_PEN));
1548 dc.MoveTo(layout.whole.left, layout.yCenter);
1549 dc.LineTo(layout.whole.right, layout.yCenter);
1550
1551 CFontHandle f = GetFont();
1552 if (f == NULL) {
1553 auto buddy = this->GetBuddy();
1554 if ( buddy ) f = buddy.GetFont();
1555 }
1556 if (f == NULL) f = (HFONT) GetStockObject(DEFAULT_GUI_FONT);
1557 PFC_ASSERT(f != NULL);
1558 CFont font2;
1559 CreateScaledFont(font2, f, 0.5);
1560 SelectObjectScope selectFont(dc, font2);
1561 dc.SetBkMode(TRANSPARENT);
1562 dc.SetTextColor(colorText);
1563 dc.DrawText(L"˄", 1, layout.upper, DT_SINGLELINE | DT_VCENTER | DT_CENTER | DT_NOPREFIX);
1564 dc.DrawText(L"˅", 1, layout.lower, DT_SINGLELINE | DT_VCENTER | DT_CENTER | DT_NOPREFIX);
1565 }
1566 };
1567
1568 class CNCFrameHook : public CWindowImpl<CNCFrameHook, CWindow> {
1569 public:
1570 CNCFrameHook(bool dark) : m_dark(dark) {}
1571
1572 BEGIN_MSG_MAP_EX(CNCFrameHook)
1573 MESSAGE_HANDLER_EX(msgSetDarkMode(), OnSetDarkMode)
1574 MSG_WM_NCPAINT(OnNCPaint)
1575 END_MSG_MAP()
1576
1577 void SetDark(bool v) {
1578 if (v != m_dark) {
1579 m_dark = v; ApplyDark();
1580 }
1581 }
1582 BOOL SubclassWindow(HWND wnd) {
1583 auto rv = __super::SubclassWindow(wnd);
1584 if (rv) {
1585 ApplyDark();
1586 }
1587 return rv;
1588 }
1589 private:
1590 void OnNCPaint(HRGN rgn) {
1591 if (m_dark) {
1592 NCPaintDarkFrame(m_hWnd, rgn);
1593 return;
1594 }
1595 SetMsgHandled(FALSE);
1596 }
1597 void ApplyDark() {
1598 ApplyDarkThemeCtrl(m_hWnd, m_dark);
1599 Invalidate();
1600 }
1601 LRESULT OnSetDarkMode(UINT, WPARAM wp, LPARAM) {
1602 switch (wp) {
1603 case 0: SetDark(false); break;
1604 case 1: SetDark(true); break;
1605 }
1606 return 1;
1607 }
1608
1609 bool m_dark;
1610 };
1611 }
1612
1613 void CHooks::AddPopup(HWND wnd) {
1614 addOp( [wnd, this] {
1615 UpdateTitleBar(wnd, m_dark);
1616 } );
1617 }
1618
1619 void CHooks::AddDialogWithControls(HWND wnd) {
1620 AddDialog(wnd); AddControls(wnd);
1621 }
1622
1623 void CHooks::AddDialog(HWND wnd) {
1624
1625 {
1626 CWindow w(wnd);
1627 if ((w.GetStyle() & WS_CHILD) == 0) {
1628 AddPopup(wnd);
1629 }
1630 }
1631
1632 auto hook = new ImplementOnFinalMessage< CDialogHook > (m_dark);
1633 hook->SubclassWindow(wnd);
1634 AddCtrlMsg(wnd);
1635 }
1636 void CHooks::AddTabCtrl(HWND wnd) {
1637 auto hook = new ImplementOnFinalMessage<CTabsHook>(m_dark);
1638 hook->SubclassWindow(wnd);
1639 AddCtrlMsg(wnd);
1640 }
1641 void CHooks::AddComboBox(HWND wnd) {
1642 {
1643 CComboBox combo = wnd;
1644 COMBOBOXINFO info = {sizeof(info)};
1645 WIN32_OP_D( combo.GetComboBoxInfo(&info) );
1646 if (info.hwndList != NULL) {
1647 AddListBox( info.hwndList );
1648 }
1649 }
1650
1651 addOp([wnd, this] {
1652 SetWindowTheme(wnd, m_dark ? L"DarkMode_CFD" : L"Explorer", NULL);
1653 });
1654 }
1655 void CHooks::AddComboBoxEx(HWND wnd) {
1656 this->AddControls(wnd); // recurse to add the combo box
1657 }
1658 void CHooks::AddEditBox(HWND wnd) {
1659 #if 0 // Experimental
1660 auto hook = new ImplementOnFinalMessage<CNCFrameHook>(m_dark);
1661 hook->SubclassWindow( wnd );
1662 AddCtrlMsg( wnd );
1663 #else
1664 AddGeneric(wnd);
1665 #endif
1666 }
1667 void CHooks::AddButton(HWND wnd) {
1668 CButton btn(wnd);
1669 auto style = btn.GetButtonStyle();
1670 auto type = style & BS_TYPEMASK;
1671 if ((type == BS_CHECKBOX || type == BS_AUTOCHECKBOX || type == BS_RADIOBUTTON || type == BS_AUTORADIOBUTTON || type == BS_3STATE || type == BS_AUTO3STATE) && (style & BS_PUSHLIKE) == 0) {
1672 // MS checkbox implementation is terminally retarded and won't draw text in correct color
1673 // Subclass it and draw our own content
1674 // Other button types seem OK
1675 auto hook = new ImplementOnFinalMessage<CCheckBoxHook>(m_dark);
1676 hook->SubclassWindow(wnd);
1677 AddCtrlMsg(wnd);
1678 } else if (type == BS_GROUPBOX) {
1679 SetWindowTheme(wnd, L"", L"");
1680 // SNAFU: re-creation of other controls such as list boxes causes repaint bugs due to overlapping
1681 // Even though this is not a fault of the groupbox, fix it here - by defer-pushing all group boxes to the back
1682 // Can't move to back right away, breaks window enumeration
1683 m_lstMoveToBack.push_back(wnd);
1684 } else {
1685 AddGeneric(wnd);
1686 }
1687
1688 }
1689 void CHooks::AddGeneric(HWND wnd, const wchar_t * name) {
1690 this->addOp([wnd, this, name] {ApplyDarkThemeCtrl(wnd, m_dark, name); });
1691 }
1692 void CHooks::AddClassic(HWND wnd, const wchar_t* normalTheme) {
1693 this->addOp([wnd, this, normalTheme] {
1694 if (m_dark) ::SetWindowTheme(wnd, L"", L"");
1695 else ::SetWindowTheme(wnd, normalTheme, nullptr);
1696 });
1697 }
1698 void CHooks::AddStatusBar(HWND wnd) {
1699 auto hook = new ImplementOnFinalMessage<CStatusBarHook>(m_dark);
1700 hook->SubclassWindow(wnd);
1701 this->AddCtrlMsg(wnd);
1702 }
1703 void CHooks::AddScrollBar(HWND wnd) {
1704 CWindow w(wnd);
1705 if (w.GetStyle() & SBS_SIZEGRIP) {
1706 auto hook = new ImplementOnFinalMessage<CGripperHook>(m_dark);
1707 hook->SubclassWindow(wnd);
1708 this->AddCtrlMsg(wnd);
1709 } else {
1710 AddGeneric(wnd);
1711 }
1712 }
1713
1714 void CHooks::AddReBar(HWND wnd) {
1715 auto hook = new ImplementOnFinalMessage<CReBarHook>(m_dark);
1716 hook->SubclassWindow(wnd);
1717 this->AddCtrlMsg(wnd);
1718 }
1719
1720 void CHooks::AddToolBar(HWND wnd, bool bExplorerTheme) {
1721 // Not a subclass
1722 addObj(new CToolbarHook(wnd, m_dark, bExplorerTheme));
1723 }
1724
1725 void CHooks::AddStatic(HWND wnd) {
1726 auto hook = new ImplementOnFinalMessage<CStaticHook>(m_dark);
1727 hook->SubclassWindow(wnd);
1728 this->AddCtrlMsg(wnd);
1729 }
1730
1731 void CHooks::AddUpDown(HWND wnd) {
1732 auto hook = new ImplementOnFinalMessage<CUpDownHook>(m_dark);
1733 hook->SubclassWindow(wnd);
1734 this->AddCtrlMsg(wnd);
1735 }
1736
1737 void CHooks::AddTreeView(HWND wnd) {
1738 auto hook = new ImplementOnFinalMessage<CTreeViewHook>(m_dark);
1739 hook->SubclassWindow(wnd);
1740 this->AddCtrlMsg(wnd);
1741 }
1742 void CHooks::AddListBox(HWND wnd) {
1743 this->AddGeneric( wnd );
1744 #if 0
1745 auto subst = CListControl_ReplaceListBox(wnd);
1746 if (subst) AddPPListControl(subst);
1747 #endif
1748 }
1749
1750 void CHooks::AddListView(HWND wnd) {
1751 auto subst = CListControl_ReplaceListView(wnd);
1752 if (subst) AddPPListControl(subst);
1753 }
1754
1755 void CHooks::AddPPListControl(HWND wnd) {
1756 this->AddCtrlMsg(wnd);
1757 // this->addOp([this, wnd] { CListControl::wndSetDarkMode(wnd, m_dark); });
1758 }
1759
1760 void CHooks::SetDark(bool v) {
1761 // Important: some handlers to ugly things if told to apply when no state change occurred - UpdateTitleBar() stuff in particular
1762 if (m_dark != v) {
1763 m_dark = v;
1764 for (auto& f : m_apply) f();
1765 }
1766 }
1767 void CHooks::flushMoveToBack() {
1768 for (auto w : m_lstMoveToBack) {
1769 ::SetWindowPos(w, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
1770 }
1771 m_lstMoveToBack.clear();
1772 }
1773 void CHooks::AddControls(HWND wndParent) {
1774 for (HWND walk = GetWindow(wndParent, GW_CHILD); walk != NULL; ) {
1775 HWND next = GetWindow(walk, GW_HWNDNEXT);
1776 AddCtrlAuto(walk);
1777 walk = next;
1778 }
1779 this->flushMoveToBack();
1780 // EnumChildWindows(wndParent, [this](HWND ctrl) {this->AddCtrlAuto(ctrl);});
1781 }
1782
1783 void CHooks::AddCtrlMsg(HWND w) {
1784 this->addOp([this, w] {
1785 ::SendMessage(w, msgSetDarkMode(), this->m_dark ? 1 : 0, 0);
1786 });
1787 }
1788
1789 void CHooks::AddCtrlAuto(HWND wnd) {
1790
1791 if (::SendMessage(wnd, msgSetDarkMode(), -1, -1)) {
1792 AddCtrlMsg(wnd); return;
1793 }
1794
1795 wchar_t buffer[128] = {};
1796
1797 ::GetClassName(wnd, buffer, (int)(std::size(buffer) - 1));
1798
1799 const wchar_t* cls = buffer;
1800 if (_wcsnicmp(cls, L"ATL:", 4) == 0) cls += 4;
1801
1802
1803 if (_wcsicmp(cls, CButton::GetWndClassName()) == 0) {
1804 AddButton(wnd);
1805 } else if (_wcsicmp(cls, CComboBox::GetWndClassName()) == 0) {
1806 AddComboBox(wnd);
1807 } else if (_wcsicmp(cls, CComboBoxEx::GetWndClassName()) == 0 ) {
1808 AddComboBoxEx(wnd);
1809 } else if (_wcsicmp(cls, CTabCtrl::GetWndClassName()) == 0) {
1810 AddTabCtrl(wnd);
1811 } else if (_wcsicmp(cls, CStatusBarCtrl::GetWndClassName()) == 0) {
1812 AddStatusBar(wnd);
1813 } else if (_wcsicmp(cls, CEdit::GetWndClassName()) == 0) {
1814 AddEditBox(wnd);
1815 } else if (_wcsicmp(cls, WC_SCROLLBAR) == 0) {
1816 AddScrollBar(wnd);
1817 } else if (_wcsicmp(cls, CToolBarCtrl::GetWndClassName()) == 0) {
1818 AddToolBar(wnd);
1819 } else if (_wcsicmp(cls, CTrackBarCtrl::GetWndClassName()) == 0) {
1820 AddGeneric(wnd);
1821 } else if (_wcsicmp(cls, CTreeViewCtrl::GetWndClassName()) == 0) {
1822 AddTreeView(wnd);
1823 } else if (_wcsicmp(cls, CStatic::GetWndClassName()) == 0) {
1824 AddStatic(wnd);
1825 } else if (_wcsicmp(cls, CUpDownCtrl::GetWndClassName()) == 0) {
1826 AddUpDown(wnd);
1827 } else if (_wcsicmp(cls, CListViewCtrl::GetWndClassName()) == 0) {
1828 AddListView(wnd);
1829 } else if (_wcsicmp(cls, CListBox::GetWndClassName()) == 0) {
1830 AddListBox(wnd);
1831 } else if (_wcsicmp(cls, CReBarCtrl::GetWndClassName()) == 0) {
1832 AddReBar(wnd);
1833 } else {
1834 #if PFC_DEBUG
1835 pfc::outputDebugLine(pfc::format("DarkMode: unknown class - ", buffer));
1836 #endif
1837 }
1838 }
1839
1840 void CHooks::clear() {
1841 m_apply.clear();
1842 for (auto v : m_cleanup) v();
1843 m_cleanup.clear();
1844 }
1845
1846 void CHooks::AddApp() {
1847 addOp([this] {
1848 SetAppDarkMode(this->m_dark);
1849 });
1850 }
1851
1852 void NCPaintDarkFrame(HWND wnd_, HRGN rgn_) {
1853 // rgn is in SCREEN COORDINATES, possibly (HRGN)1 to indicate no clipping / whole nonclient area redraw
1854 // we're working with SCREEN COORDINATES until actual DC painting
1855 CWindow wnd = wnd_;
1856
1857 CRect rcWindow, rcClient;
1858 WIN32_OP_D( wnd.GetWindowRect(rcWindow) );
1859 WIN32_OP_D( wnd.GetClientRect(rcClient) );
1860 WIN32_OP_D( wnd.ClientToScreen( rcClient ) ); // transform all to same coordinate system
1861
1862 CRgn rgnClip;
1863 WIN32_OP_D( rgnClip.CreateRectRgnIndirect(rcWindow) != NULL );
1864 if (rgn_ != NULL && rgn_ != (HRGN)1) {
1865 // we have a valid HRGN from caller?
1866 if (rgnClip.CombineRgn(rgn_, RGN_AND) == NULLREGION) return; // nothing to draw, exit early
1867 }
1868
1869 {
1870 // Have scroll bars? Have DefWindowProc() them then exclude from our rgnClip.
1871 SCROLLBARINFO si = { sizeof(si) };
1872 if (::GetScrollBarInfo(wnd, OBJID_VSCROLL, &si) && (si.rgstate[0] & STATE_SYSTEM_INVISIBLE) == 0 && si.rcScrollBar.left < si.rcScrollBar.right) {
1873 CRect rc = si.rcScrollBar;
1874 // rcClient.right = rc.right;
1875 CRgn rgn; WIN32_OP_D( rgn.CreateRectRgnIndirect(rc) );
1876 int status = SIMPLEREGION;
1877 if (rgnClip) {
1878 status = rgn.CombineRgn(rgn, rgnClip, RGN_AND);
1879 }
1880 if (status != NULLREGION) {
1881 DefWindowProc(wnd, WM_NCPAINT, (WPARAM)rgn.m_hRgn, 0);
1882 rgnClip.CombineRgn(rgn, RGN_DIFF); // exclude from further drawing
1883 }
1884 }
1885 if (::GetScrollBarInfo(wnd, OBJID_HSCROLL, &si) && (si.rgstate[0] & STATE_SYSTEM_INVISIBLE) == 0 && si.rcScrollBar.top < si.rcScrollBar.bottom) {
1886 CRect rc = si.rcScrollBar;
1887 // rcClient.bottom = rc.bottom;
1888 CRgn rgn; WIN32_OP_D(rgn.CreateRectRgnIndirect(rc));
1889 int status = SIMPLEREGION;
1890 if (rgnClip) {
1891 status = rgn.CombineRgn(rgn, rgnClip, RGN_AND);
1892 }
1893 if (status != NULLREGION) {
1894 DefWindowProc(wnd, WM_NCPAINT, (WPARAM)rgn.m_hRgn, 0);
1895 rgnClip.CombineRgn(rgn, RGN_DIFF); // exclude from further drawing
1896 }
1897 }
1898 }
1899
1900 const auto colorLight = DarkMode::GetSysColor(COLOR_BTNHIGHLIGHT);
1901 const auto colorDark = DarkMode::GetSysColor(COLOR_BTNSHADOW);
1902
1903 CWindowDC dc( wnd );
1904 if (dc.IsNull()) {
1905 PFC_ASSERT(!"???");
1906 return;
1907 }
1908
1909
1910 // Window DC has (0,0) in upper-left corner of our window (not screen, not client)
1911 // Turn rcWindow to (0,0), (winWidth, winHeight)
1912 CPoint origin = rcWindow.TopLeft();
1913 rcWindow.OffsetRect(-origin);
1914 rcClient.OffsetRect(-origin);
1915
1916 if (!rgnClip.IsNull()) {
1917 // rgnClip is still in screen coordinates, fix this here
1918 rgnClip.OffsetRgn(-origin);
1919 dc.SelectClipRgn(rgnClip);
1920 }
1921
1922 // bottom
1923 dc.FillSolidRect(CRect(rcClient.left, rcClient.bottom, rcWindow.right, rcWindow.bottom), colorLight);
1924 // right
1925 dc.FillSolidRect(CRect(rcClient.right, rcWindow.top, rcWindow.right, rcClient.bottom), colorLight);
1926 // top
1927 dc.FillSolidRect(CRect(rcWindow.left, rcWindow.top, rcWindow.right, rcClient.top), colorDark);
1928 // left
1929 dc.FillSolidRect(CRect(rcWindow.left, rcClient.top, rcClient.left, rcWindow.bottom), colorDark);
1930 }
1931 }