#include "ccm/ui/Theme.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __WXMSW__ #include #include #endif namespace ccm::ui { namespace { std::unordered_set gButtonHoverBound; std::unordered_set gDialogGripLayoutBound; struct ButtonVisualState { wxColour normalBg; wxColour hoverBg; wxColour pressedBg; wxColour text; bool darkLike{false}; bool hovered{false}; bool pressed{false}; bool focused{false}; }; std::unordered_map gButtonVisualStates; struct GripVisualState { wxColour bg; wxColour line; }; std::unordered_map gGripVisualStates; wxColour lightenTowardWhite(const wxColour& c, int amount) { auto lift = [amount](unsigned char channel) -> unsigned char { const int raised = static_cast(channel) + amount; return static_cast(raised > 255 ? 255 : raised); }; return wxColour(lift(c.Red()), lift(c.Green()), lift(c.Blue())); } bool isDarkLikeTheme(Theme theme) { return theme == Theme::Dark; } void ensureDarkDialogResizeGrip(wxWindow* window, const ThemePalette& palette, Theme theme) { auto* dialog = dynamic_cast(window); if (dialog == nullptr) return; if ((dialog->GetWindowStyleFlag() & wxRESIZE_BORDER) == 0) return; constexpr int kGripSize = 16; const wxString kGripName = "ccm_dark_resize_grip_overlay"; wxWindow* grip = wxWindow::FindWindowByName(kGripName, dialog); if (!isDarkLikeTheme(theme)) { if (grip != nullptr) { gGripVisualStates.erase(grip); grip->Destroy(); } return; } if (grip == nullptr) { grip = new wxWindow(dialog, wxID_ANY, wxDefaultPosition, wxSize(kGripSize, kGripSize), wxBORDER_NONE); grip->SetName(kGripName); grip->SetCursor(wxCursor(wxCURSOR_SIZENWSE)); grip->SetBackgroundStyle(wxBG_STYLE_PAINT); grip->Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {}); grip->Bind(wxEVT_PAINT, [grip](wxPaintEvent&) { wxAutoBufferedPaintDC dc(grip); const auto it = gGripVisualStates.find(grip); const wxColour bg = (it != gGripVisualStates.end()) ? it->second.bg : wxColour(45, 45, 45); const wxColour line = (it != gGripVisualStates.end()) ? it->second.line : wxColour(110, 110, 110); const wxRect rect = grip->GetClientRect(); dc.SetPen(*wxTRANSPARENT_PEN); dc.SetBrush(wxBrush(bg)); dc.DrawRectangle(rect); dc.SetPen(wxPen(line, 1)); const int r = rect.GetRight(); const int b = rect.GetBottom(); dc.DrawLine(r - 11, b, r, b - 11); dc.DrawLine(r - 7, b, r, b - 7); dc.DrawLine(r - 3, b, r, b - 3); }); #ifdef __WXMSW__ grip->Bind(wxEVT_LEFT_DOWN, [dialog](wxMouseEvent&) { const HWND hwnd = reinterpret_cast(dialog->GetHandle()); if (hwnd == nullptr) return; ::ReleaseCapture(); ::SendMessageW(hwnd, WM_NCLBUTTONDOWN, HTBOTTOMRIGHT, 0); }); #endif grip->Bind(wxEVT_DESTROY, [grip](wxWindowDestroyEvent& ev) { gGripVisualStates.erase(grip); ev.Skip(); }); } gGripVisualStates[grip] = GripVisualState{ palette.panelBg, lightenTowardWhite(palette.panelBg, 48), }; auto placeGrip = [dialog, grip]() { const wxSize cs = dialog->GetClientSize(); const int w = kGripSize; const int h = kGripSize; grip->SetSize(std::max(0, cs.GetWidth() - w), std::max(0, cs.GetHeight() - h), w, h); grip->Raise(); }; placeGrip(); grip->Show(); grip->Refresh(); if (!gDialogGripLayoutBound.count(dialog)) { gDialogGripLayoutBound.insert(dialog); dialog->Bind(wxEVT_SIZE, [dialog](wxSizeEvent& ev) { if (wxWindow* w = wxWindow::FindWindowByName("ccm_dark_resize_grip_overlay", dialog)) { constexpr int kSize = 16; const wxSize cs = dialog->GetClientSize(); w->SetSize(std::max(0, cs.GetWidth() - kSize), std::max(0, cs.GetHeight() - kSize), kSize, kSize); w->Raise(); } ev.Skip(); }); dialog->Bind(wxEVT_DESTROY, [dialog](wxWindowDestroyEvent& ev) { gDialogGripLayoutBound.erase(dialog); ev.Skip(); }); } } } #ifdef __WXMSW__ namespace { using SetWindowThemeFn = HRESULT(WINAPI*)(HWND, LPCWSTR, LPCWSTR); using DwmSetWindowAttributeFn = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD); using AllowDarkModeForWindowFn = BOOL(WINAPI*)(HWND, BOOL); enum class PreferredAppMode : int { Default = 0, AllowDark = 1, ForceDark = 2, ForceLight = 3, Max = 4 }; using SetPreferredAppModeFn = PreferredAppMode(WINAPI*)(PreferredAppMode); using FlushMenuThemesFn = VOID(WINAPI*)(); #ifndef HDM_SETBKCOLOR #define HDM_SETBKCOLOR (HDM_FIRST + 19) #endif #ifndef HDM_SETTEXTCOLOR #define HDM_SETTEXTCOLOR (HDM_FIRST + 20) #endif SetWindowThemeFn resolveSetWindowTheme() { static HMODULE uxthemeModule = ::LoadLibraryW(L"uxtheme.dll"); static auto setWindowTheme = reinterpret_cast( uxthemeModule ? ::GetProcAddress(uxthemeModule, "SetWindowTheme") : nullptr); return setWindowTheme; } AllowDarkModeForWindowFn resolveAllowDarkModeForWindow() { static HMODULE uxthemeModule = ::LoadLibraryW(L"uxtheme.dll"); static auto fn = reinterpret_cast( uxthemeModule ? ::GetProcAddress(uxthemeModule, MAKEINTRESOURCEA(133)) : nullptr); return fn; } SetPreferredAppModeFn resolveSetPreferredAppMode() { static HMODULE uxthemeModule = ::LoadLibraryW(L"uxtheme.dll"); static auto fn = reinterpret_cast( uxthemeModule ? ::GetProcAddress(uxthemeModule, MAKEINTRESOURCEA(135)) : nullptr); return fn; } FlushMenuThemesFn resolveFlushMenuThemes() { static HMODULE uxthemeModule = ::LoadLibraryW(L"uxtheme.dll"); static auto fn = reinterpret_cast( uxthemeModule ? ::GetProcAddress(uxthemeModule, MAKEINTRESOURCEA(136)) : nullptr); return fn; } void applyNativeClassTheme(wxWindow* window, Theme theme, const wchar_t* darkClass, const wchar_t* lightClass) { if (window == nullptr) return; const HWND hwnd = reinterpret_cast(window->GetHandle()); if (hwnd == nullptr) return; const auto setWindowTheme = resolveSetWindowTheme(); if (setWindowTheme == nullptr) return; const bool dark = (theme == Theme::Dark); setWindowTheme(hwnd, dark ? darkClass : lightClass, nullptr); } COLORREF toColorRef(const wxColour& c) { return RGB(c.Red(), c.Green(), c.Blue()); } void applyListHeaderTheme(wxWindow* window, Theme theme, const ThemePalette& palette) { auto* list = dynamic_cast(window); if (list == nullptr) return; const HWND listHwnd = reinterpret_cast(list->GetHandle()); if (listHwnd == nullptr) return; const auto setWindowTheme = resolveSetWindowTheme(); if (setWindowTheme == nullptr) return; const HWND header = ListView_GetHeader(listHwnd); if (header == nullptr) return; const bool dark = (theme == Theme::Dark); if (auto allowDarkModeForWindow = resolveAllowDarkModeForWindow()) { allowDarkModeForWindow(header, dark ? TRUE : FALSE); } if (dark) { // Different Windows builds react to different class tokens. setWindowTheme(header, L"DarkMode_ItemsView", nullptr); setWindowTheme(header, L"DarkMode_Explorer", nullptr); setWindowTheme(header, L"ItemsView", nullptr); } else { setWindowTheme(header, L"Header", nullptr); setWindowTheme(header, L"ItemsView", nullptr); } // Force the native header colors to match the selected app theme. ::SendMessageW(header, HDM_SETBKCOLOR, 0, static_cast(toColorRef(palette.inputBg))); ::SendMessageW(header, HDM_SETTEXTCOLOR, 0, static_cast(toColorRef(palette.inputText))); InvalidateRect(header, nullptr, TRUE); } void applyFrameTitlebarTheme(wxWindow* window, Theme theme) { if (dynamic_cast(window) == nullptr) return; const HWND hwnd = reinterpret_cast(window->GetHandle()); if (hwnd == nullptr) return; static HMODULE dwmModule = ::LoadLibraryW(L"dwmapi.dll"); static auto dwmSetWindowAttribute = reinterpret_cast( dwmModule ? ::GetProcAddress(dwmModule, "DwmSetWindowAttribute") : nullptr); if (dwmSetWindowAttribute == nullptr) return; const bool darkLike = (theme == Theme::Dark); const BOOL useDark = darkLike ? TRUE : FALSE; constexpr DWORD kDwmUseImmersiveDarkModeOld = 19; constexpr DWORD kDwmUseImmersiveDarkModeNew = 20; dwmSetWindowAttribute(hwnd, kDwmUseImmersiveDarkModeOld, &useDark, sizeof(useDark)); dwmSetWindowAttribute(hwnd, kDwmUseImmersiveDarkModeNew, &useDark, sizeof(useDark)); // Ask uxtheme to use dark menu rendering for the top menu strip. if (auto setPreferredAppMode = resolveSetPreferredAppMode()) { setPreferredAppMode(darkLike ? PreferredAppMode::ForceDark : PreferredAppMode::Default); } if (auto allowDarkModeForWindow = resolveAllowDarkModeForWindow()) { allowDarkModeForWindow(hwnd, useDark); } if (auto setWindowTheme = resolveSetWindowTheme()) { // Ensure top-level non-client rendering (including resize grip/corner) // uses a dark-capable class theme when the app is in dark mode. setWindowTheme(hwnd, darkLike ? L"DarkMode_Explorer" : L"Explorer", nullptr); } if (auto flushMenuThemes = resolveFlushMenuThemes()) { flushMenuThemes(); } DrawMenuBar(hwnd); } void applyTopLevelSizeGripTheme(wxWindow* window, Theme theme) { if (dynamic_cast(window) == nullptr) return; const HWND top = reinterpret_cast(window->GetHandle()); if (top == nullptr) return; const auto setWindowTheme = resolveSetWindowTheme(); if (setWindowTheme == nullptr) return; const bool dark = (theme == Theme::Dark); const BOOL useDark = dark ? TRUE : FALSE; std::pair enumCtx{theme, setWindowTheme}; ::EnumChildWindows( top, [](HWND child, LPARAM lParam) -> BOOL { auto* ctx = reinterpret_cast*>(lParam); if (ctx == nullptr || ctx->second == nullptr) return TRUE; wchar_t className[64] = {}; if (::GetClassNameW(child, className, static_cast(sizeof(className) / sizeof(className[0]))) <= 0) { return TRUE; } const LONG_PTR style = ::GetWindowLongPtrW(child, GWL_STYLE); const bool isScrollbarClass = (::wcscmp(className, L"SCROLLBAR") == 0); const bool isStatusbarClass = (::wcscmp(className, STATUSCLASSNAMEW) == 0); const bool isSizeGrip = (style & SBS_SIZEGRIP) != 0 || (style & SBS_SIZEBOX) != 0 || (style & SBS_SIZEBOXBOTTOMRIGHTALIGN) != 0 || (style & SBS_SIZEBOXTOPLEFTALIGN) != 0 || (style & SBARS_SIZEGRIP) != 0; if (!isSizeGrip) return TRUE; if (!isScrollbarClass && !isStatusbarClass) return TRUE; const bool darkLocal = (ctx->first == Theme::Dark); if (auto allowDarkModeForWindow = resolveAllowDarkModeForWindow()) { allowDarkModeForWindow(child, darkLocal ? TRUE : FALSE); } const wchar_t* darkClass = isStatusbarClass ? L"DarkMode_StatusBar" : L"DarkMode_Explorer"; const wchar_t* lightClass = isStatusbarClass ? L"Status" : L"Explorer"; ctx->second(child, darkLocal ? darkClass : lightClass, nullptr); ::InvalidateRect(child, nullptr, TRUE); return TRUE; }, reinterpret_cast(&enumCtx)); if (auto allowDarkModeForWindow = resolveAllowDarkModeForWindow()) { allowDarkModeForWindow(top, useDark); } } } // namespace #endif ThemePalette paletteForTheme(Theme theme) { switch (theme) { case Theme::Dark: return ThemePalette{ wxColour(30, 30, 30), wxColour(45, 45, 45), wxColour(230, 230, 230), wxColour(60, 60, 60), wxColour(230, 230, 230), wxColour(75, 75, 75), wxColour(240, 240, 240), }; case Theme::Light: default: return ThemePalette{ wxColour(248, 248, 248), wxColour(255, 255, 255), wxColour(20, 20, 20), wxColour(255, 255, 255), wxColour(20, 20, 20), wxColour(245, 245, 245), wxColour(20, 20, 20), }; } } Theme inferThemeFromWindow(const wxWindow* window) { if (window == nullptr) return Theme::Light; const wxWindow* probe = window; wxColour bg; while (probe != nullptr) { bg = probe->GetBackgroundColour(); if (bg.IsOk()) break; probe = probe->GetParent(); } if (!bg.IsOk()) { bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); } const int luminance = (299 * bg.Red() + 587 * bg.Green() + 114 * bg.Blue()) / 1000; return luminance < 128 ? Theme::Dark : Theme::Light; } void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme theme) { if (root == nullptr) return; root->SetForegroundColour(palette.text); root->SetBackgroundColour(palette.panelBg); root->SetOwnForegroundColour(palette.text); root->SetOwnBackgroundColour(palette.panelBg); #ifdef __WXMSW__ applyFrameTitlebarTheme(root, theme); applyTopLevelSizeGripTheme(root, theme); #endif ensureDarkDialogResizeGrip(root, palette, theme); if (dynamic_cast(root) != nullptr || dynamic_cast(root) != nullptr || dynamic_cast(root) != nullptr || dynamic_cast(root) != nullptr || dynamic_cast(root) != nullptr) { if (auto* text = dynamic_cast(root)) { // On Windows, themed EDIT controls can ignore wx foreground color // while typing in dark mode; disable native theming there so the // control consistently uses palette-driven text/background colors. text->SetThemeEnabled(!isDarkLikeTheme(theme)); } root->SetBackgroundColour(palette.inputBg); root->SetForegroundColour(palette.inputText); root->SetOwnBackgroundColour(palette.inputBg); root->SetOwnForegroundColour(palette.inputText); #ifdef __WXMSW__ if (dynamic_cast(root) != nullptr) { // Keep both native list scrollbars and the SysHeader32 control themed. applyNativeClassTheme(root, theme, L"DarkMode_Explorer", L"Explorer"); applyListHeaderTheme(root, theme, palette); } else if (dynamic_cast(root) != nullptr) { // Do not apply Explorer class theming to edit controls: on some // Windows builds it forces black typed text in dark mode. // Keep text fields palette-driven via wx colors instead. } else { applyNativeClassTheme(root, theme, L"DarkMode_Explorer", L"Explorer"); } #endif } if (dynamic_cast(root) != nullptr) { root->SetBackgroundColour(palette.panelBg); root->SetForegroundColour(palette.text); root->SetOwnBackgroundColour(palette.panelBg); root->SetOwnForegroundColour(palette.text); #ifdef __WXMSW__ applyNativeClassTheme(root, theme, L"DarkMode_StatusBar", L"Status"); #endif } if (dynamic_cast(root) != nullptr || dynamic_cast(root) != nullptr) { const bool darkLike = isDarkLikeTheme(theme); root->SetThemeEnabled(!darkLike); root->SetBackgroundColour(palette.buttonBg); root->SetForegroundColour(palette.buttonText); root->SetOwnBackgroundColour(palette.buttonBg); root->SetOwnForegroundColour(palette.buttonText); const wxColour normalBg = palette.buttonBg; const int hoverLift = 18; const int pressedLift = 30; const wxColour hoverBg = darkLike ? lightenTowardWhite(normalBg, hoverLift) : normalBg; const wxColour pressedBg = darkLike ? lightenTowardWhite(normalBg, pressedLift) : normalBg; const wxColour btnFg = palette.buttonText; gButtonVisualStates[root] = ButtonVisualState{ normalBg, hoverBg, pressedBg, btnFg, darkLike, false, false, false }; if (!gButtonHoverBound.count(root)) { gButtonHoverBound.insert(root); root->Bind(wxEVT_ENTER_WINDOW, [root](wxMouseEvent& event) { auto it = gButtonVisualStates.find(root); if (it == gButtonVisualStates.end() || !it->second.darkLike) { event.Skip(); return; } it->second.hovered = true; const wxColour bg = it->second.pressed ? it->second.pressedBg : it->second.hoverBg; root->SetBackgroundColour(bg); root->SetForegroundColour(it->second.text); root->Refresh(); }); root->Bind(wxEVT_LEAVE_WINDOW, [root](wxMouseEvent& event) { auto it = gButtonVisualStates.find(root); if (it == gButtonVisualStates.end() || !it->second.darkLike) { event.Skip(); return; } it->second.hovered = false; const wxColour bg = it->second.focused ? it->second.hoverBg : it->second.normalBg; root->SetBackgroundColour(bg); root->SetForegroundColour(it->second.text); root->Refresh(); }); root->Bind(wxEVT_LEFT_DOWN, [root](wxMouseEvent& event) { auto it = gButtonVisualStates.find(root); if (it == gButtonVisualStates.end() || !it->second.darkLike) { event.Skip(); return; } it->second.pressed = true; root->SetBackgroundColour(it->second.pressedBg); root->SetForegroundColour(it->second.text); root->Refresh(); event.Skip(); }); root->Bind(wxEVT_LEFT_UP, [root](wxMouseEvent& event) { auto it = gButtonVisualStates.find(root); if (it == gButtonVisualStates.end() || !it->second.darkLike) { event.Skip(); return; } it->second.pressed = false; const wxPoint mousePos = wxGetMousePosition(); const wxPoint localPos = root->ScreenToClient(mousePos); const bool inside = root->GetClientRect().Contains(localPos); it->second.hovered = inside; const wxColour bg = (inside || it->second.focused) ? it->second.hoverBg : it->second.normalBg; root->SetBackgroundColour(bg); root->SetForegroundColour(it->second.text); root->Refresh(); event.Skip(); }); root->Bind(wxEVT_SET_FOCUS, [root](wxFocusEvent& event) { auto it = gButtonVisualStates.find(root); if (it == gButtonVisualStates.end() || !it->second.darkLike) { event.Skip(); return; } it->second.focused = true; root->SetBackgroundColour(it->second.hoverBg); root->SetForegroundColour(it->second.text); root->Refresh(); event.Skip(); }); root->Bind(wxEVT_KILL_FOCUS, [root](wxFocusEvent& event) { auto it = gButtonVisualStates.find(root); if (it == gButtonVisualStates.end() || !it->second.darkLike) { event.Skip(); return; } it->second.focused = false; it->second.pressed = false; const wxColour bg = it->second.hovered ? it->second.hoverBg : it->second.normalBg; root->SetBackgroundColour(bg); root->SetForegroundColour(it->second.text); root->Refresh(); event.Skip(); }); root->SetBackgroundStyle(wxBG_STYLE_PAINT); root->Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {}); root->Bind(wxEVT_PAINT, [root](wxPaintEvent& event) { const auto it = gButtonVisualStates.find(root); if (it == gButtonVisualStates.end() || !it->second.darkLike) { event.Skip(); return; } wxAutoBufferedPaintDC dc(root); const wxRect rect = root->GetClientRect(); wxColour bg = it->second.normalBg; if (it->second.pressed) { bg = it->second.pressedBg; } else if (it->second.hovered || it->second.focused) { bg = it->second.hoverBg; } const wxColour fg = it->second.text; dc.SetBrush(wxBrush(bg)); dc.SetPen(wxPen(lightenTowardWhite(bg, 28))); dc.DrawRectangle(rect); if (auto* bmpBtn = dynamic_cast(root)) { const wxBitmap bmp = bmpBtn->GetBitmap(); if (bmp.IsOk()) { const int x = (rect.GetWidth() - bmp.GetWidth()) / 2; const int y = (rect.GetHeight() - bmp.GetHeight()) / 2; dc.DrawBitmap(bmp, x, y, true); } } else { dc.SetTextForeground(fg); const wxString label = root->GetLabel(); dc.DrawLabel(label, rect, wxALIGN_CENTER); } }); root->Bind(wxEVT_DESTROY, [root](wxWindowDestroyEvent& event) { gButtonHoverBound.erase(root); gButtonVisualStates.erase(root); event.Skip(); }); } #ifdef __WXMSW__ if (darkLike) { // Disable native visual-style painting in dark mode only, // otherwise light theme buttons should stay fully native. applyNativeClassTheme(root, theme, L"", L""); } #endif } if (dynamic_cast(root) != nullptr || dynamic_cast(root) != nullptr) { root->SetForegroundColour(palette.text); root->SetBackgroundColour(palette.panelBg); root->SetOwnForegroundColour(palette.text); root->SetOwnBackgroundColour(palette.panelBg); } const wxWindowList& children = root->GetChildren(); for (wxWindowList::compatibility_iterator it = children.GetFirst(); it; it = it->GetNext()) { applyThemeToWindowTree(it->GetData(), palette, theme); } } void themeModalDialog(wxDialog* dlg, Theme theme) { if (dlg == nullptr) return; const ThemePalette palette = paletteForTheme(theme); applyThemeToWindowTree(dlg, palette, theme); dlg->SetBackgroundColour(palette.panelBg); dlg->SetForegroundColour(palette.text); } int showThemedMessageDialog(wxWindow* parent, const wxString& message, const wxString& caption, long style) { wxDialog dlg(parent, wxID_ANY, caption, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); auto* root = new wxBoxSizer(wxVERTICAL); auto* label = new wxStaticText(&dlg, wxID_ANY, message); root->Add(label, 0, wxALL | wxEXPAND, 12); const bool yesNo = (style & wxYES_NO) != 0; if (yesNo) { auto* buttons = new wxStdDialogButtonSizer(); auto* yesBtn = new wxButton(&dlg, wxID_YES); auto* noBtn = new wxButton(&dlg, wxID_NO); yesBtn->SetLabelText("Yes"); noBtn->SetLabelText("No"); yesBtn->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_YES); }); noBtn->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_NO); }); yesBtn->SetDefault(); buttons->AddButton(yesBtn); buttons->AddButton(noBtn); buttons->Realize(); root->Add(buttons, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 12); } else { if (auto* buttons = dlg.CreateButtonSizer(wxOK)) { root->Add(buttons, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 12); } } dlg.SetSizerAndFit(root); const wxSize fitSize = dlg.GetSize(); dlg.SetSize(fitSize.GetWidth(), static_cast(fitSize.GetHeight() * 1.10)); const Theme theme = inferThemeFromWindow(parent); const ThemePalette palette = paletteForTheme(theme); applyThemeToWindowTree(&dlg, palette, theme); dlg.SetBackgroundColour(palette.panelBg); dlg.SetForegroundColour(palette.text); dlg.CentreOnParent(); return dlg.ShowModal(); } int showThemedConfirmDialog(wxWindow* parent, const wxString& message, const wxString& caption) { wxDialog dlg(parent, wxID_ANY, caption, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); auto* root = new wxBoxSizer(wxVERTICAL); auto* label = new wxStaticText(&dlg, wxID_ANY, message); root->Add(label, 0, wxALL | wxEXPAND, 12); auto* buttons = new wxStdDialogButtonSizer(); auto* yesBtn = new wxButton(&dlg, wxID_YES); auto* noBtn = new wxButton(&dlg, wxID_NO); yesBtn->SetLabelText("Yes"); noBtn->SetLabelText("No"); yesBtn->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_YES); }); noBtn->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_NO); }); yesBtn->SetDefault(); buttons->AddButton(yesBtn); buttons->AddButton(noBtn); buttons->Realize(); root->Add(buttons, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 12); dlg.SetSizerAndFit(root); const wxSize fitSize = dlg.GetSize(); dlg.SetSize(fitSize.GetWidth(), static_cast(fitSize.GetHeight() * 1.10)); const Theme theme = inferThemeFromWindow(parent); const ThemePalette palette = paletteForTheme(theme); applyThemeToWindowTree(&dlg, palette, theme); dlg.SetBackgroundColour(palette.panelBg); dlg.SetForegroundColour(palette.text); dlg.CentreOnParent(); return dlg.ShowModal(); } } // namespace ccm::ui