From 7ba69068f427bad765097676f8526305e1695b5a Mon Sep 17 00:00:00 2001 From: Anthony West Date: Thu, 29 Oct 2026 22:37:13 -0700 Subject: [PATCH 1/3] Add four Carbon-tinted dark themes to the Win32 GUI: Ember, Forest, Midnight, Plum --- CHANGELOG.md | 4 + README.md | 14 +- gui/src/ASWUnitTests_GUI_Theme.cpp | 12 +- gui/src/ASWUnitTests_GUI_Theme.h | 10 +- gui/tests/Test_ASWUnitTests_GUI_Theme.cpp | 15 +- vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp | 3 +- .../src/ASWUnitTests_Win32GUI_MainWindow.cpp | 36 ++-- .../gui/src/ASWUnitTests_Win32GUI_Palette.cpp | 187 ++++++++++++++++++ win32/gui/src/ASWUnitTests_Win32GUI_Palette.h | 27 +++ win32/gui/src/ASWUnitTests_Win32GUI_Theme.cpp | 10 +- win32/gui/src/ASWUnitTests_Win32GUI_Theme.h | 8 +- .../Test_ASWUnitTests_Win32GUI_Palette.cpp | 177 +++++++++++++++++ .../Test_ASWUnitTests_Win32GUI_Palette.h | 4 + 13 files changed, 468 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5677b2b..94ac846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,10 @@ versioned baseline. GUI runner's are INI files that set any of its colors by role, and **Save Theme Template...** saves Carbon's as one to start from; the VCL GUI runner's are VCL style files (`.vsf`). The choice is saved in the layout file, and a theme file's problems are reported in the log. +- Four more built-in dark themes in the Win32 GUI runner, tints of Carbon: Ember (warm brown), + Forest (green), Midnight (blue) and Plum (purple), listed in **View > Theme > Dark Theme** above + the theme files. The colors that show how tests did (the tree's status dots, failed and skipped + text, a failed run's progress bar) stay Carbon's. - An ASWUnitTests icon, `resource\ASWUnitTests.ico`, which this repository's console and GUI runners now have on Windows, with the PNGs it's made from and `resource\make_icon.ps1`, which draws them. `ASWUNITTESTS_WIN32_GUI_SOURCES` includes `resource\ASWUnitTests_Icon.rc`, so a Win32 diff --git a/README.md b/README.md index 42c343f..edcd4b0 100644 --- a/README.md +++ b/README.md @@ -393,13 +393,15 @@ the Win32 GUI runner's dark look, the title bar is dark unless Windows shows the title bars, which it then keeps while the window is active, and the Export JUnit Report dialog, Windows' own, stays light. -**View > Theme > Dark Theme** chooses the dark look's colors: Carbon, built in, or a theme file of +**View > Theme > Dark Theme** chooses the dark look's colors: a built-in theme or a theme file of your own, used whenever the look is dark, with Dark or with Use Windows Setting (high contrast keeps -its own colors). Theme files go in `%APPDATA%\ASWUnitTests\Themes`, which **Open Themes Folder** -opens, and are shared by every test app; the menu lists each one by its file name. The choice is -saved in the layout file with the theme, and a problem with a theme file, such as a misspelled role, -a color that isn't one or a missing file, is reported in the Log tab, with Carbon's colors used in -its place. +its own colors). Both runners have Carbon built in, and the Win32 GUI runner also has four tints of +it: Ember (warm brown), Forest (green), Midnight (blue) and Plum (purple), which keep Carbon's +colors for how tests did. Theme files go in `%APPDATA%\ASWUnitTests\Themes`, which **Open Themes +Folder** opens, and are shared by every test app; the menu lists each one by its file name, below +the built-in themes, and leaves out a file named like one of them. The choice is saved in the layout +file with the theme, and a problem with a theme file, such as a misspelled role, a color that isn't +one or a missing file, is reported in the Log tab, with Carbon's colors used in its place. The Win32 GUI runner's theme files are INI files, `.ini`, and **Save Theme Template...** saves Carbon's colors as one to start from, every role listed under a comment on what its group colors: diff --git a/gui/src/ASWUnitTests_GUI_Theme.cpp b/gui/src/ASWUnitTests_GUI_Theme.cpp index 5592b2d..3ebb5e0 100644 --- a/gui/src/ASWUnitTests_GUI_Theme.cpp +++ b/gui/src/ASWUnitTests_GUI_Theme.cpp @@ -141,7 +141,8 @@ bool GUIThemeIsDark(EGUITheme theme, bool windowsAppsUseDark, bool highContrast) return windowsAppsUseDark && !highContrast; } //--------------------------------------------------------------------------- -std::vector GUIThemeNames(std::vector const& fileNames, std::string const& extension) +std::vector GUIThemeNames(std::vector const& fileNames, std::string const& extension, + std::vector const& builtInNames) { std::vector names; @@ -154,13 +155,16 @@ std::vector GUIThemeNames(std::vector const& fileNames } std::string const name = fileName.substr(0, fileName.size() - extension.size()); - bool const listed = std::any_of(names.begin(), names.end(), [&name](std::string const& other) + auto const sameName = [&name](std::string const& other) { return SameText(other, name); - }); + }; - if (!listed && !SameText(name, GUIDefaultDarkThemeName())) + if (std::none_of(names.begin(), names.end(), sameName) && + std::none_of(builtInNames.begin(), builtInNames.end(), sameName)) + { names.push_back(name); + } } std::stable_sort(names.begin(), names.end(), &LessText); diff --git a/gui/src/ASWUnitTests_GUI_Theme.h b/gui/src/ASWUnitTests_GUI_Theme.h index 5b30429..fcf3896 100644 --- a/gui/src/ASWUnitTests_GUI_Theme.h +++ b/gui/src/ASWUnitTests_GUI_Theme.h @@ -92,8 +92,8 @@ struct TGUIThemeFile // 'rgb', 0xRRGGBB, as a theme file writes a color: "#RRGGBB", in uppercase. std::string FormatGUIThemeColor(std::uint32_t rgb); -// The built-in dark theme's name, "Carbon": the default for LoadGUIDarkTheme(), and a name GUIThemeNames() -// leaves out, so a theme file can't take it. +// The name of the built-in dark theme both runners have, "Carbon": the default for LoadGUIDarkTheme(), and one +// of the built-in names GUIThemeNames() leaves out, so a theme file can't take it. std::string GUIDefaultDarkThemeName(); // Whether 'theme' is dark: Light and Dark as chosen, and Windows as 'windowsAppsUseDark' (Windows' app mode // setting) says, except that it's light while 'highContrast' is on, so the window shows high contrast's own @@ -101,8 +101,10 @@ std::string GUIDefaultDarkThemeName(); bool GUIThemeIsDark(EGUITheme theme, bool windowsAppsUseDark, bool highContrast); // The names of the themes among 'fileNames', the files in the Themes folder: each one that ends in // 'extension' (e.g. ".ini"), ignoring (ASCII) case, without it, sorted ignoring case, and each name once. -// Leaves out GUIDefaultDarkThemeName(), ignoring case, and a file named just 'extension'. -std::vector GUIThemeNames(std::vector const& fileNames, std::string const& extension); +// Leaves out 'builtInNames', the runner's built-in dark themes (GUIDefaultDarkThemeName() among them), ignoring +// case, which a file can't replace, and a file named just 'extension'. +std::vector GUIThemeNames(std::vector const& fileNames, std::string const& extension, + std::vector const& builtInNames); // A line for the log about one of the dark theme 'themeName''s problems (e.g. one of TGUIThemeFile's), as both // GUI runners word it: Dark theme "": std::string GUIThemeProblemLine(std::string const& themeName, std::string const& problem); diff --git a/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp b/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp index 89b9347..cf610e1 100644 --- a/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp +++ b/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp @@ -177,15 +177,20 @@ void TTest_ASWUnitTests_GUI_Theme::Test_GUIThemeNames_ListsTheFilesOfOneKindSort // Arrange: the Themes folder's files, in the order a listing might give them. std::vector const files = { "ocean.ini", "Zest.ini", "Notes.txt", "Carbon.ini", "OCEAN.INI", ".ini", "apple.Ini", "carbon.INI", - "Ocean.ini.bak", "Forest.vsf", "ini", "Two Words.ini", + "Ocean.ini.bak", "Forest.vsf", "ini", "Two Words.ini", "plum.ini", }; + std::vector const carbon = { "Carbon" }; + std::vector const builtIns = { "Carbon", "Plum", "Zest" }; // Act and Assert - CheckEquals(std::string("apple\nocean\nTwo Words\nZest\n"), JoinLines(GUIThemeNames(files, ".ini")), __func__, - __LINE__, "the .ini files' names, sorted ignoring case, a name once, without Carbon or a bare extension"); - CheckEquals(std::string("Forest\n"), JoinLines(GUIThemeNames(files, ".vsf")), __func__, __LINE__, + CheckEquals(std::string("apple\nocean\nplum\nTwo Words\nZest\n"), JoinLines(GUIThemeNames(files, ".ini", carbon)), + __func__, __LINE__, "the .ini files' names, sorted ignoring case, a name once, without Carbon or a bare " + "extension"); + CheckEquals(std::string("apple\nocean\nTwo Words\n"), JoinLines(GUIThemeNames(files, ".ini", builtIns)), __func__, + __LINE__, "without any built-in theme's name, ignoring case"); + CheckEquals(std::string("Forest\n"), JoinLines(GUIThemeNames(files, ".vsf", carbon)), __func__, __LINE__, "the VCL GUI runner's .vsf files"); - CheckEmpty(JoinLines(GUIThemeNames(std::vector(), ".ini")), __func__, __LINE__, + CheckEmpty(JoinLines(GUIThemeNames(std::vector(), ".ini", carbon)), __func__, __LINE__, "an empty or missing folder"); } //--------------------------------------------------------------------------- diff --git a/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp b/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp index 14588aa..3a20f78 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp +++ b/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp @@ -900,7 +900,8 @@ void TFormASWUnitTestsGUIMain::FillDarkThemeMenu() m_DarkThemeItems.clear(); - std::vector names = GUIThemeNames(FileNamesInFolder(GUIThemesFolderPath()), ".vsf"); + std::vector names = GUIThemeNames(FileNamesInFolder(GUIThemesFolderPath()), ".vsf", + { GUIDefaultDarkThemeName() }); names.insert(names.begin(), GUIDefaultDarkThemeName()); // Windows' file names ignore case, so a name saved in another case is still that file's. diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp index 5f524ea..6695642 100644 --- a/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp +++ b/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp @@ -1519,14 +1519,21 @@ void TWin32GUIMainWindow::FailRunOnUnhandledException(std::string const& descrip /* TWin32GUIMainWindow::FillDarkThemeMenu - Carbon first, then the theme files in GUIThemesFolderPath() (see GUIThemeNames()), so a file added or removed - since shows at once, then Save Theme Template and Open Themes Folder; the chosen one has the radio check. A - chosen theme whose file is missing is still listed, so choosing it again reports why it isn't used. + The built-in themes first, Carbon at the top, then the theme files in GUIThemesFolderPath() (see + GUIThemeNames()), so a file added or removed since shows at once, then Save Theme Template and Open Themes + Folder; the chosen one has the radio check. A chosen theme whose file is missing is still listed, so choosing + it again reports why it isn't used. */ void TWin32GUIMainWindow::FillDarkThemeMenu() { - std::vector names = GUIThemeNames(FileNamesInFolder(GUIThemesFolderPath()), ".ini"); - names.insert(names.begin(), GUIDefaultDarkThemeName()); + std::vector names; + + for (TWin32GUIBuiltInTheme const& theme : BuiltInDarkThemes()) + names.push_back(theme.Name); + + std::size_t const builtInCount = names.size(); + std::vector const files = GUIThemeNames(FileNamesInFolder(GUIThemesFolderPath()), ".ini", names); + names.insert(names.end(), files.begin(), files.end()); // Windows' file names ignore case, so a name saved in another case is still that file's. auto const sameName = [](std::string const& a, std::string const& b) @@ -1559,15 +1566,18 @@ void TWin32GUIMainWindow::FillDarkThemeMenu() for (std::size_t i = 0; i < names.size(); ++i) { - // Carbon's C is its accelerator; a theme file's name has none. - std::wstring const caption = UTF8ToWide((i == 0 ? "&" : "") + GUIMenuItemText(names[i])); - AppendMenuW(m_MI_ViewDarkTheme, MF_STRING | enabled, FirstDarkThemeCommand + i, caption.c_str()); - } + // A line between the built-in themes and the files. + if (i == builtInCount) + AppendMenuW(m_MI_ViewDarkTheme, MF_SEPARATOR, 0, nullptr); - if (chosenIndex < names.size()) - { - CheckMenuRadioItem(m_MI_ViewDarkTheme, FirstDarkThemeCommand, static_cast(FirstDarkThemeCommand + - names.size() - 1), static_cast(FirstDarkThemeCommand + chosenIndex), MF_BYCOMMAND); + // A built-in theme's first letter is its accelerator; a theme file's name has none. + UINT const id = static_cast(FirstDarkThemeCommand + i); + std::wstring const caption = UTF8ToWide((i < builtInCount ? "&" : "") + GUIMenuItemText(names[i])); + AppendMenuW(m_MI_ViewDarkTheme, MF_STRING | enabled, id, caption.c_str()); + + // Only it, as nothing else is checked yet, so the range needn't span the line above the files. + if (i == chosenIndex) + CheckMenuRadioItem(m_MI_ViewDarkTheme, id, id, id, MF_BYCOMMAND); } AppendMenuW(m_MI_ViewDarkTheme, MF_SEPARATOR, 0, nullptr); diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp index 1efe2ce..7aec232 100644 --- a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp @@ -25,7 +25,10 @@ limitations under the License. #include "ASWUnitTests_Win32GUI_Palette.h" //--------------------------------------------------------------------------- #include +#include +#include #include +#include #include #include //--------------------------------------------------------------------------- @@ -35,9 +38,69 @@ namespace //--------------------------------------------------------------------------- +COLORREF FromHSL(double hue, double saturation, double lightness); COLORREF FromRGB(std::uint32_t rgb); +bool SameText(std::string const& a, std::string const& b); +void ToHSL(COLORREF color, double& hue, double& saturation, double& lightness); std::uint32_t ToRGB(COLORREF color); +//--------------------------------------------------------------------------- +/* + FromHSL + + The color with 'hue' (in degrees, any number of turns), 'saturation' and 'lightness' (0 to 1 each), each + channel rounded to the nearest. +*/ +COLORREF FromHSL(double hue, double saturation, double lightness) +{ + double const chroma = (1.0 - std::fabs(2.0 * lightness - 1.0)) * saturation; + double const sector = std::fmod(std::fmod(hue, 360.0) + 360.0, 360.0) / 60.0; + double const second = chroma * (1.0 - std::fabs(std::fmod(sector, 2.0) - 1.0)); + double red = 0.0; + double green = 0.0; + double blue = 0.0; + + switch (static_cast(sector)) + { + case 0: + red = chroma; + green = second; + break; + + case 1: + red = second; + green = chroma; + break; + + case 2: + green = chroma; + blue = second; + break; + + case 3: + green = second; + blue = chroma; + break; + + case 4: + red = second; + blue = chroma; + break; + + default: + red = chroma; + blue = second; + break; + } + + double const lowest = lightness - chroma / 2.0; + auto const channel = [lowest](double value) + { + return static_cast(std::lround((std::min)(1.0, (std::max)(0.0, value + lowest)) * 255.0)); + }; + + return RGB(channel(red), channel(green), channel(blue)); +} //--------------------------------------------------------------------------- /* FromRGB @@ -49,6 +112,57 @@ COLORREF FromRGB(std::uint32_t rgb) return RGB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); } //--------------------------------------------------------------------------- +/* + SameText + + Whether 'a' and 'b' are the same, ignoring the case of ASCII letters. +*/ +bool SameText(std::string const& a, std::string const& b) +{ + auto const lower = [](char c) + { + return c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; + }; + + if (a.size() != b.size()) + return false; + + for (std::size_t i = 0; i < a.size(); ++i) + { + if (lower(a[i]) != lower(b[i])) + return false; + } + + return true; +} +//--------------------------------------------------------------------------- +/* + ToHSL + + The color's hue (in degrees, 0 up to 360; 0 for a gray), saturation and lightness (0 to 1 each). +*/ +void ToHSL(COLORREF color, double& hue, double& saturation, double& lightness) +{ + double const red = GetRValue(color) / 255.0; + double const green = GetGValue(color) / 255.0; + double const blue = GetBValue(color) / 255.0; + double const highest = (std::max)(red, (std::max)(green, blue)); + double const lowest = (std::min)(red, (std::min)(green, blue)); + double const chroma = highest - lowest; + + lightness = (highest + lowest) / 2.0; + saturation = chroma <= 0.0 ? 0.0 : chroma / (1.0 - std::fabs(2.0 * lightness - 1.0)); + + if (chroma <= 0.0) + hue = 0.0; + else if (highest == red) + hue = 60.0 * std::fmod((green - blue) / chroma + 6.0, 6.0); + else if (highest == green) + hue = 60.0 * ((blue - red) / chroma + 2.0); + else + hue = 60.0 * ((red - green) / chroma + 4.0); +} +//--------------------------------------------------------------------------- /* ToRGB @@ -84,6 +198,24 @@ COLORREF BlendColors(COLORREF from, COLORREF to, int numerator, int denominator) channel(GetBValue(from), GetBValue(to))); } //--------------------------------------------------------------------------- +/* + BuiltInDarkThemes + + Each tint's hue and strength, and its accent's hue, chosen by eye in the runner. +*/ +std::vector const& BuiltInDarkThemes() +{ + static std::vector const themes = { + { GUIDefaultDarkThemeName(), CarbonPalette() }, + { "Ember", TintedCarbonPalette(25.0, 0.09, 30.0) }, + { "Forest", TintedCarbonPalette(140.0, 0.08, 155.0) }, + { "Midnight", TintedCarbonPalette(220.0, 0.10, 205.0) }, + { "Plum", TintedCarbonPalette(285.0, 0.09, 295.0) } + }; + + return themes; +} +//--------------------------------------------------------------------------- /* CarbonPalette @@ -236,6 +368,17 @@ COLORREF FailureTextColorOn(COLORREF background) return IsDarkColor(background) ? RGB(255, 100, 100) : RGB(200, 0, 0); } //--------------------------------------------------------------------------- +TWin32GUIBuiltInTheme const* FindBuiltInDarkTheme(std::string const& name) +{ + for (TWin32GUIBuiltInTheme const& theme : BuiltInDarkThemes()) + { + if (SameText(theme.Name, name)) + return &theme; + } + + return nullptr; +} +//--------------------------------------------------------------------------- bool IsDarkColor(COLORREF color) { int const luma = (GetRValue(color) * 299 + GetGValue(color) * 587 + GetBValue(color) * 114) / 1000; @@ -396,5 +539,49 @@ COLORREF SkipTextColorOn(COLORREF background) return IsDarkColor(background) ? RGB(255, 190, 70) : RGB(190, 120, 0); } //--------------------------------------------------------------------------- +/* + TintedCarbonPalette + + A gray (a color whose HSL saturation is under 0.1) keeps its lightness and gets the chroma 'tint' times + (1 - its lightness); a colored one keeps its saturation and lightness and gets 'accentHue'. +*/ +TWin32GUIPalette TintedCarbonPalette(double hue, double tint, double accentHue) +{ + // The colors that say how a test did. + COLORREF TWin32GUIPalette::* const kept[] = { + &TWin32GUIPalette::FailureText, &TWin32GUIPalette::ProgressFailedBottom, &TWin32GUIPalette::ProgressFailedTop, + &TWin32GUIPalette::SkipText, &TWin32GUIPalette::TreeStatusFailed, &TWin32GUIPalette::TreeStatusNotRun, + &TWin32GUIPalette::TreeStatusPassed, &TWin32GUIPalette::TreeStatusRunning, &TWin32GUIPalette::TreeStatusSkipped + }; + + TWin32GUIPalette palette = CarbonPalette(); + + for (TWin32GUIPaletteRole const& role : PaletteRoles()) + { + if (std::find(std::begin(kept), std::end(kept), role.Color) != std::end(kept)) + continue; + + COLORREF& color = palette.*role.Color; + double colorHue = 0.0; + double saturation = 0.0; + double lightness = 0.0; + ToHSL(color, colorHue, saturation, lightness); + + if (saturation < 0.1) + { + // The chroma as HSL's saturation, which is relative to the most chroma the lightness allows. + double const room = 1.0 - std::fabs(2.0 * lightness - 1.0); + double const chroma = tint * (1.0 - lightness); + color = FromHSL(hue, room > 0.0 ? (std::min)(1.0, chroma / room) : 0.0, lightness); + } + else + { + color = FromHSL(accentHue, saturation, lightness); + } + } + + return palette; +} +//--------------------------------------------------------------------------- } // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h index 557f0dd..19484ea 100644 --- a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h @@ -179,12 +179,30 @@ struct TWin32GUIPaletteRole char const* Name; }; + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIBuiltInTheme +// +// A dark theme the Win32 GUI runner has built in, which View > Theme > +// Dark Theme lists before the theme files (see BuiltInDarkThemes()). +///////////////////////////////////////////////////////////////////////////// +struct TWin32GUIBuiltInTheme +{ + // Its name, as the menu lists it and the layout file saves it. + std::string Name; + TWin32GUIPalette Palette; +}; + //--------------------------------------------------------------------------- // 'from' mixed with 'to' in the proportion 'numerator' / 'denominator', each channel rounded to the nearest: // 'from' at 0, 'to' at 'denominator', e.g. one row of a gradient. 'from' if 'denominator' isn't positive; the // proportion is kept within 0 and 1. COLORREF BlendColors(COLORREF from, COLORREF to, int numerator, int denominator); +// The dark themes built in: Carbon (GUIDefaultDarkThemeName(), CarbonPalette()) first, then Carbon tinted (see +// TintedCarbonPalette()): Ember (warm brown), Forest (green), Midnight (blue) and Plum (purple). Their names' +// first letters differ, for the menu's accelerators, from each other's and from the menu's other items'. +std::vector const& BuiltInDarkThemes(); // The colors of RAD Studio's Carbon style, the VCL GUI runner's dark theme, read from its .vsf file and from // the VCL GUI runner as it draws under it. TWin32GUIPalette CarbonPalette(); @@ -195,6 +213,9 @@ std::string CarbonThemeFileText(); // The color of a failed test's text on 'background': RGB(200, 0, 0), or RGB(255, 100, 100) on a dark // background (see IsDarkColor()), as the VCL GUI runner's. COLORREF FailureTextColorOn(COLORREF background); +// The built-in dark theme named 'name', ignoring (ASCII) case, e.g. as a layout file saved it; nullptr if +// there's none (see BuiltInDarkThemes()). +TWin32GUIBuiltInTheme const* FindBuiltInDarkTheme(std::string const& name); // Whether 'color' is dark, by its luma (Rec. 601 weights) being below 128, as the VCL GUI runner decides // whether a background needs lighter text colors. bool IsDarkColor(COLORREF color); @@ -208,6 +229,12 @@ TWin32GUIPalette PaletteWithThemeColors(TWin32GUIPalette palette, std::vector TWin32GUITheme::SetDarkTheme(std::string const& name) { std::vector problems; TWin32GUIPalette palette = CarbonPalette(); + TWin32GUIBuiltInTheme const* const builtIn = FindBuiltInDarkTheme(name); - if (name != GUIDefaultDarkThemeName()) + if (builtIn != nullptr) + { + palette = builtIn->Palette; + } + else { try { diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Theme.h b/win32/gui/src/ASWUnitTests_Win32GUI_Theme.h index d3d72c1..ca97a5c 100644 --- a/win32/gui/src/ASWUnitTests_Win32GUI_Theme.h +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Theme.h @@ -236,10 +236,10 @@ class TWin32GUITheme void RefreshMenu(HMENU menu); // Switches to the dark look or back to the light one, and redraws everything attached. void SetDark(bool dark); - // Makes 'name' the dark theme: GUIDefaultDarkThemeName(), Carbon, or a theme file in GUIThemesFolderPath(), - // ".ini", over Carbon's colors (see SetPalette()). Returns its problems as lines for the log (see - // GUIThemeProblemLine()): the file's (see ReadGUIThemeFile()), or that it's missing or can't be read, so - // Carbon is used. + // Makes 'name' the dark theme: one of BuiltInDarkThemes() (ignoring case), or a theme file in + // GUIThemesFolderPath(), ".ini", over Carbon's colors (see SetPalette()). Returns its problems as lines + // for the log (see GUIThemeProblemLine()): the file's (see ReadGUIThemeFile()), or that it's missing or can't + // be read, so Carbon is used. std::vector SetDarkTheme(std::string const& name); // Makes 'palette' the dark look's colors, redrawing everything attached while dark. void SetPalette(TWin32GUIPalette const& palette); diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp index a645820..97c9977 100644 --- a/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp @@ -28,6 +28,9 @@ limitations under the License. //--------------------------------------------------------------------------- #include #include +#include +#include +#include #include #include //--------------------------------------------------------------------------- @@ -37,6 +40,50 @@ limitations under the License. #include "ASWUnitTests_Win32GUI_Palette.h" //--------------------------------------------------------------------------- +namespace +{ + +//--------------------------------------------------------------------------- + +double ContrastRatio(COLORREF a, COLORREF b); +double RelativeLuminance(COLORREF color); + +//--------------------------------------------------------------------------- +/* + ContrastRatio + + WCAG 2's contrast ratio of two colors, 1 to 21; text needs at least 4.5 on its background to be readable. +*/ +double ContrastRatio(COLORREF a, COLORREF b) +{ + double const lighter = (std::max)(RelativeLuminance(a), RelativeLuminance(b)); + double const darker = (std::min)(RelativeLuminance(a), RelativeLuminance(b)); + + return (lighter + 0.05) / (darker + 0.05); +} +//--------------------------------------------------------------------------- +/* + RelativeLuminance + + WCAG 2's relative luminance of an sRGB color, 0 for black to 1 for white. +*/ +double RelativeLuminance(COLORREF color) +{ + auto const linear = [](BYTE channel) + { + double const value = channel / 255.0; + + return value <= 0.04045 ? value / 12.92 : std::pow((value + 0.055) / 1.055, 2.4); + }; + + return 0.2126 * linear(GetRValue(color)) + 0.7152 * linear(GetGValue(color)) + 0.0722 * linear(GetBValue(color)); +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + namespace ASWUnitTests { @@ -50,17 +97,25 @@ TTest_ASWUnitTests_Win32GUI_Palette::TTest_ASWUnitTests_Win32GUI_Palette() { RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_BlendColors_MixesEachChannelInProportion, "BlendColors_MixesEachChannelInProportion"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_AreCarbonThenItsTints, + "BuiltInDarkThemes_AreCarbonThenItsTints"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_KeepTextReadable, + "BuiltInDarkThemes_KeepTextReadable"); RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_CarbonPalette_HasCarbonsColors, "CarbonPalette_HasCarbonsColors"); RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_CarbonThemeFileText_ReadsBackAsCarbon, "CarbonThemeFileText_ReadsBackAsCarbon"); RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_FailureAndSkipTextColorOn_AreLighterOnADarkBackground, "FailureAndSkipTextColorOn_AreLighterOnADarkBackground"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_FindBuiltInDarkTheme_IgnoresCase, + "FindBuiltInDarkTheme_IgnoresCase"); RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_IsDarkColor_IsByTheLuma, "IsDarkColor_IsByTheLuma"); RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_PaletteRoles_ListEveryMemberOnce, "PaletteRoles_ListEveryMemberOnce"); RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_PaletteWithThemeColors_SetsOnlyTheirRoles, "PaletteWithThemeColors_SetsOnlyTheirRoles"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_TintedCarbonPalette_TintsOnlyTheGraysAndTheAccent, + "TintedCarbonPalette_TintsOnlyTheGraysAndTheAccent"); } //--------------------------------------------------------------------------- TTest_ASWUnitTests_Win32GUI_Palette::~TTest_ASWUnitTests_Win32GUI_Palette() @@ -105,6 +160,75 @@ void TTest_ASWUnitTests_Win32GUI_Palette::Test_BlendColors_MixesEachChannelInPro CheckEquals(black, BlendColors(black, white, 1, 0), __func__, __LINE__, "'from' with no denominator"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_AreCarbonThenItsTints() +{ + // Act + std::vector const& themes = BuiltInDarkThemes(); + + // Assert + std::string names; + // The menu's other items' accelerators: Save Theme Template and Open Themes Folder. + std::set letters = { 's', 'o' }; + bool lettersDiffer = true; + + for (TWin32GUIBuiltInTheme const& theme : themes) + { + names += theme.Name + "\n"; + lettersDiffer = lettersDiffer && !theme.Name.empty() && + letters.insert(static_cast(std::tolower(static_cast(theme.Name[0])))).second; + } + + CheckEquals(std::string("Carbon\nEmber\nForest\nMidnight\nPlum\n"), names, __func__, __LINE__, + "Carbon first, then the tints by name"); + CheckTrue(lettersDiffer, __func__, __LINE__, "each with a first letter of its own, for its accelerator"); + AssertFalse(themes.empty(), __func__, __LINE__, "Carbon is there"); + TWin32GUIPalette const carbon = CarbonPalette(); + bool sameAsCarbon = true; + + for (TWin32GUIPaletteRole const& role : PaletteRoles()) + sameAsCarbon = sameAsCarbon && themes.front().Palette.*role.Color == carbon.*role.Color; + + CheckEquals(GUIDefaultDarkThemeName(), themes.front().Name, __func__, __LINE__, "the default first"); + CheckTrue(sameAsCarbon, __func__, __LINE__, "in CarbonPalette()'s colors"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_KeepTextReadable() +{ + // Arrange: each text color the GUI draws on a background, with that background. + struct TPair + { + COLORREF TWin32GUIPalette::* Text; + COLORREF TWin32GUIPalette::* Background; + char const* Name; + }; + + TPair const pairs[] = { + { &TWin32GUIPalette::WindowText, &TWin32GUIPalette::Window, "WindowText on Window" }, + { &TWin32GUIPalette::ControlText, &TWin32GUIPalette::Panel, "ControlText on Panel" }, + { &TWin32GUIPalette::ControlText, &TWin32GUIPalette::MenuBackground, "ControlText on MenuBackground" }, + { &TWin32GUIPalette::HotText, &TWin32GUIPalette::Highlight, "HotText on Highlight" }, + { &TWin32GUIPalette::HotText, &TWin32GUIPalette::Selection, "HotText on Selection" }, + { &TWin32GUIPalette::FailureText, &TWin32GUIPalette::Window, "FailureText on Window" }, + { &TWin32GUIPalette::SkipText, &TWin32GUIPalette::Window, "SkipText on Window" }, + { &TWin32GUIPalette::TooltipText, &TWin32GUIPalette::TooltipBackground, "TooltipText on TooltipBackground" }, + }; + + // Act + std::string unreadable; + + for (TWin32GUIBuiltInTheme const& theme : BuiltInDarkThemes()) + { + for (TPair const& pair : pairs) + { + if (ContrastRatio(theme.Palette.*pair.Text, theme.Palette.*pair.Background) < 4.5) + unreadable += theme.Name + ": " + pair.Name + "\n"; + } + } + + // Assert + CheckEmpty(unreadable, __func__, __LINE__, "every text at WCAG's 4.5 to 1 or more on its background"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_Win32GUI_Palette::Test_CarbonPalette_HasCarbonsColors() { // Act @@ -195,6 +319,22 @@ void TTest_ASWUnitTests_Win32GUI_Palette::Test_FailureAndSkipTextColorOn_AreLigh "skip on Carbon's window color"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_FindBuiltInDarkTheme_IgnoresCase() +{ + // Act + TWin32GUIBuiltInTheme const* const midnight = FindBuiltInDarkTheme("mIDNIGHT"); + TWin32GUIBuiltInTheme const* const carbon = FindBuiltInDarkTheme("Carbon"); + + // Assert + AssertTrue(midnight != nullptr, __func__, __LINE__, "a built-in theme, in another case"); + CheckEquals(std::string("Midnight"), midnight->Name, __func__, __LINE__, "with its own name"); + AssertTrue(carbon != nullptr, __func__, __LINE__, "Carbon"); + CheckEquals(std::string("Carbon"), carbon->Name, __func__, __LINE__, "named so"); + CheckTrue(FindBuiltInDarkTheme("Midnight Blue") == nullptr, __func__, __LINE__, "none for a longer name"); + CheckTrue(FindBuiltInDarkTheme("Midnigh") == nullptr, __func__, __LINE__, "none for a shorter one"); + CheckTrue(FindBuiltInDarkTheme(std::string()) == nullptr, __func__, __LINE__, "none for an empty one"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_Win32GUI_Palette::Test_IsDarkColor_IsByTheLuma() { // Act, Assert: Rec. 601 luma, (299 R + 587 G + 114 B) / 1000, below 128. @@ -290,6 +430,43 @@ void TTest_ASWUnitTests_Win32GUI_Palette::Test_PaletteWithThemeColors_SetsOnlyTh CheckEmpty(changed, __func__, __LINE__, "the others keep their colors, and an unknown role is left out"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_TintedCarbonPalette_TintsOnlyTheGraysAndTheAccent() +{ + // Arrange + TWin32GUIPalette const carbon = CarbonPalette(); + + // Act: grays toward blue, the accent green; and no tint, with the accent's own hue. + TWin32GUIPalette const tinted = TintedCarbonPalette(240.0, 0.1, 120.0); + TWin32GUIPalette const untinted = TintedCarbonPalette(240.0, 0.0, 120.0); + + // Assert + CheckTrue(GetBValue(tinted.Window) > GetRValue(tinted.Window) && GetBValue(tinted.Window) > GetGValue(tinted.Window), + __func__, __LINE__, "a dark gray turns blue"); + // HSL's lightness, the highest channel and the lowest added, here blue and red. + CheckTrue(std::abs(GetBValue(tinted.Window) + GetRValue(tinted.Window) - 2 * GetRValue(carbon.Window)) <= 1, + __func__, __LINE__, "keeping its lightness"); + CheckTrue(GetBValue(tinted.WindowText) - GetRValue(tinted.WindowText) < GetBValue(tinted.Window) - + GetRValue(tinted.Window), __func__, __LINE__, "a light one less, so text stays nearly white"); + CheckEquals(RGB(255, 255, 255), tinted.HotText, __func__, __LINE__, "white stays white"); + CheckTrue(GetGValue(tinted.Highlight) > GetRValue(tinted.Highlight) && GetGValue(tinted.Highlight) > + GetBValue(tinted.Highlight), __func__, __LINE__, "the accent takes its own hue"); + CheckEquals(carbon.Window, untinted.Window, __func__, __LINE__, "no tint leaves a gray as it is"); + CheckEquals(carbon.Panel, untinted.Panel, __func__, __LINE__, "every gray"); + + // The colors that say how a test did. + COLORREF TWin32GUIPalette::* const kept[] = { + &TWin32GUIPalette::FailureText, &TWin32GUIPalette::ProgressFailedBottom, &TWin32GUIPalette::ProgressFailedTop, + &TWin32GUIPalette::SkipText, &TWin32GUIPalette::TreeStatusFailed, &TWin32GUIPalette::TreeStatusNotRun, + &TWin32GUIPalette::TreeStatusPassed, &TWin32GUIPalette::TreeStatusRunning, &TWin32GUIPalette::TreeStatusSkipped + }; + bool statusKept = true; + + for (COLORREF TWin32GUIPalette::* const color : kept) + statusKept = statusKept && tinted.*color == carbon.*color; + + CheckTrue(statusKept, __func__, __LINE__, "the status colors stay Carbon's"); +} +//--------------------------------------------------------------------------- } // namespace ASWUnitTests diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.h b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.h index 779169f..de1e077 100644 --- a/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.h +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.h @@ -42,12 +42,16 @@ class TTest_ASWUnitTests_Win32GUI_Palette : public TTestGroupBase private: // Test methods void Test_BlendColors_MixesEachChannelInProportion(); + void Test_BuiltInDarkThemes_AreCarbonThenItsTints(); + void Test_BuiltInDarkThemes_KeepTextReadable(); void Test_CarbonPalette_HasCarbonsColors(); void Test_CarbonThemeFileText_ReadsBackAsCarbon(); void Test_FailureAndSkipTextColorOn_AreLighterOnADarkBackground(); + void Test_FindBuiltInDarkTheme_IgnoresCase(); void Test_IsDarkColor_IsByTheLuma(); void Test_PaletteRoles_ListEveryMemberOnce(); void Test_PaletteWithThemeColors_SetsOnlyTheirRoles(); + void Test_TintedCarbonPalette_TintsOnlyTheGraysAndTheAccent(); public: TTest_ASWUnitTests_Win32GUI_Palette(); From 6e067b59c3c3f80a28da38aabf87f6ad727bb05c Mon Sep 17 00:00:00 2001 From: Anthony West Date: Thu, 29 Oct 2026 23:37:30 -0700 Subject: [PATCH 2/3] Add four of RAD Studio's dark VCL styles to both GUIs as built-in dark themes --- CHANGELOG.md | 5 + README.md | 20 +- gui/src/ASWUnitTests_GUI_Theme.cpp | 5 + gui/src/ASWUnitTests_GUI_Theme.h | 4 + gui/src/ASWUnitTests_GUI_View.cpp | 58 +++ gui/src/ASWUnitTests_GUI_View.h | 5 + gui/tests/Test_ASWUnitTests_GUI_Theme.cpp | 12 + gui/tests/Test_ASWUnitTests_GUI_Theme.h | 1 + gui/tests/Test_ASWUnitTests_GUI_View.cpp | 36 ++ gui/tests/Test_ASWUnitTests_GUI_View.h | 1 + vcl/gui/rad370/ASWUnitTests_VCL_GUI.cbproj | 2 +- vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp | 33 +- vcl/gui/src/ASWUnitTests_GUI_VCLSystem.cpp | 30 +- vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h | 14 +- vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.cpp | 28 ++ vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.h | 1 + .../src/ASWUnitTests_Win32GUI_MainWindow.cpp | 7 +- .../gui/src/ASWUnitTests_Win32GUI_Palette.cpp | 376 +++++++++++++++++- win32/gui/src/ASWUnitTests_Win32GUI_Palette.h | 28 +- win32/gui/src/ASWUnitTests_Win32GUI_Theme.cpp | 4 +- .../Test_ASWUnitTests_Win32GUI_Palette.cpp | 63 ++- 21 files changed, 670 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94ac846..b860e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,11 @@ versioned baseline. Forest (green), Midnight (blue) and Plum (purple), listed in **View > Theme > Dark Theme** above the theme files. The colors that show how tests did (the tree's status dots, failed and skipped text, a failed run's progress bar) stay Carbon's. +- Four of RAD Studio's dark VCL styles as built-in dark themes in both GUI runners, beside Carbon: + Charcoal Dark Slate, Cobalt XEMedia, Onyx Blue and Tablet Dark. The VCL GUI runner's project + links them in, and a VCL GUI project of your own lists the ones it links; the Win32 GUI runner + paints their colors. The Win32 GUI runner's theme files can also set `ButtonText`, + `ButtonHotText` and `SelectionText`, new roles for a button's text and the tree's selected text. - An ASWUnitTests icon, `resource\ASWUnitTests.ico`, which this repository's console and GUI runners now have on Windows, with the PNGs it's made from and `resource\make_icon.ps1`, which draws them. `ASWUNITTESTS_WIN32_GUI_SOURCES` includes `resource\ASWUnitTests_Icon.rc`, so a Win32 diff --git a/README.md b/README.md index edcd4b0..f3b0afa 100644 --- a/README.md +++ b/README.md @@ -395,13 +395,15 @@ Windows' own, stays light. **View > Theme > Dark Theme** chooses the dark look's colors: a built-in theme or a theme file of your own, used whenever the look is dark, with Dark or with Use Windows Setting (high contrast keeps -its own colors). Both runners have Carbon built in, and the Win32 GUI runner also has four tints of -it: Ember (warm brown), Forest (green), Midnight (blue) and Plum (purple), which keep Carbon's -colors for how tests did. Theme files go in `%APPDATA%\ASWUnitTests\Themes`, which **Open Themes -Folder** opens, and are shared by every test app; the menu lists each one by its file name, below -the built-in themes, and leaves out a file named like one of them. The choice is saved in the layout -file with the theme, and a problem with a theme file, such as a misspelled role, a color that isn't -one or a missing file, is reported in the Log tab, with Carbon's colors used in its place. +its own colors). Both runners have five of RAD Studio's dark VCL styles built in: Carbon, Charcoal +Dark Slate, Cobalt XEMedia, Onyx Blue and Tablet Dark, which the Win32 GUI runner paints in their +colors. The Win32 GUI runner also has four tints of Carbon: Ember (warm brown), Forest (green), +Midnight (blue) and Plum (purple), which keep Carbon's colors for how tests did. Theme files go in +`%APPDATA%\ASWUnitTests\Themes`, which **Open Themes Folder** opens, and are shared by every test +app; the menu lists each one by its file name, below the built-in themes, and leaves out a file +named like one of them. The choice is saved in the layout file with the theme, and a problem with a +theme file, such as a misspelled role, a color that isn't one or a missing file, is reported in the +Log tab, with Carbon's colors used in its place. The Win32 GUI runner's theme files are INI files, `.ini`, and **Save Theme Template...** saves Carbon's colors as one to start from, every role listed under a comment on what its group colors: @@ -498,7 +500,9 @@ files in `gui\src` (shared by the GUI runners) and `vcl\gui\src`, including a `WinMain()` modeled on `vcl\gui\rad370\ASWUnitTests_VCL_GUI.cpp`, which parses the command line, applies the saved theme and starts the main form. For the dark theme, also add RAD Studio's Carbon style to the application (Project > Options > Application > Appearance, which this repository's -project has as `Custom_Styles`); without it, Dark shows the light look. For the ASWUnitTests icon, +project has as `Custom_Styles`); without it, Dark shows the light look. The other built-in dark +themes (Charcoal Dark Slate, Cobalt XEMedia, Onyx Blue and Tablet Dark) are listed only if their +styles are added too. For the ASWUnitTests icon, set the application's icon to `resource\ASWUnitTests.ico` there too, as this repository's project does. diff --git a/gui/src/ASWUnitTests_GUI_Theme.cpp b/gui/src/ASWUnitTests_GUI_Theme.cpp index 3ebb5e0..aeaa49c 100644 --- a/gui/src/ASWUnitTests_GUI_Theme.cpp +++ b/gui/src/ASWUnitTests_GUI_Theme.cpp @@ -176,6 +176,11 @@ std::string GUIThemeProblemLine(std::string const& themeName, std::string const& return "Dark theme \"" + themeName + "\": " + problem; } //--------------------------------------------------------------------------- +std::vector GUIVCLStyleDarkThemeNames() +{ + return { "Charcoal Dark Slate", "Cobalt XEMedia", "Onyx Blue", "Tablet Dark" }; +} +//--------------------------------------------------------------------------- std::string LoadGUIDarkTheme(TGUIINIDocument const& ini) { std::optional const name = ini.ReadString(ViewSection, DarkThemeKey); diff --git a/gui/src/ASWUnitTests_GUI_Theme.h b/gui/src/ASWUnitTests_GUI_Theme.h index fcf3896..0e4fce8 100644 --- a/gui/src/ASWUnitTests_GUI_Theme.h +++ b/gui/src/ASWUnitTests_GUI_Theme.h @@ -108,6 +108,10 @@ std::vector GUIThemeNames(std::vector const& fileNames // A line for the log about one of the dark theme 'themeName''s problems (e.g. one of TGUIThemeFile's), as both // GUI runners word it: Dark theme "": std::string GUIThemeProblemLine(std::string const& themeName, std::string const& problem); +// The dark themes both runners have built in besides Carbon, sorted: RAD Studio's dark VCL styles, by their +// style names, which the VCL GUI runner links in and the Win32 GUI runner has the colors of +// (BuiltInDarkThemes()). +std::vector GUIVCLStyleDarkThemeNames(); // Reads the dark theme SaveGUIDarkTheme() wrote to 'ini', its [View] section's DarkTheme key: the name of a // theme file, or GUIDefaultDarkThemeName() when it's missing or empty, or is that name in another case. std::string LoadGUIDarkTheme(TGUIINIDocument const& ini); diff --git a/gui/src/ASWUnitTests_GUI_View.cpp b/gui/src/ASWUnitTests_GUI_View.cpp index e2be257..ed7a76f 100644 --- a/gui/src/ASWUnitTests_GUI_View.cpp +++ b/gui/src/ASWUnitTests_GUI_View.cpp @@ -24,6 +24,7 @@ limitations under the License. // Module header #include "ASWUnitTests_GUI_View.h" //--------------------------------------------------------------------------- +#include #include #include //--------------------------------------------------------------------------- @@ -176,6 +177,63 @@ std::string GUIMenuItemText(std::string const& text) return caption; } //--------------------------------------------------------------------------- +std::vector GUIMenuItemTextsWithAccelerators(std::vector const& texts, + std::string const& takenLetters) +{ + auto const lower = [](char c) + { + return c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; + }; + auto const isAccelerator = [](char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); + }; + + std::string taken; + + for (char const c : takenLetters) + taken += lower(c); + + std::vector captions; + + for (std::string const& text : texts) + { + std::size_t chosen = std::string::npos; + + // A letter that starts a word first, then any. + for (int pass = 0; pass < 2 && chosen == std::string::npos; ++pass) + { + for (std::size_t i = 0; i < text.size() && chosen == std::string::npos; ++i) + { + bool const startsWord = i == 0 || text[i - 1] == ' '; + + if (isAccelerator(text[i]) && (pass == 1 || startsWord) && taken.find(lower(text[i])) == + std::string::npos) + { + chosen = i; + } + } + } + + if (chosen != std::string::npos) + taken += lower(text[chosen]); + + std::string caption; + + for (std::size_t i = 0; i < text.size(); ++i) + { + if (i == chosen) + caption += '&'; + + caption += GUIMenuItemText(text.substr(i, 1)); + } + + captions.push_back(caption); + } + + return captions; +} +//--------------------------------------------------------------------------- std::vector GUIStatusPanels(TGUITestList const& testList, size_t runFinishedCount, size_t runTotalCount, std::optional elapsedSeconds, std::string const& shuffleText, std::string const& message) { diff --git a/gui/src/ASWUnitTests_GUI_View.h b/gui/src/ASWUnitTests_GUI_View.h index dc0595c..7e911aa 100644 --- a/gui/src/ASWUnitTests_GUI_View.h +++ b/gui/src/ASWUnitTests_GUI_View.h @@ -103,6 +103,11 @@ std::string GUIFirstLine(std::string const& text); // 'text', e.g. a theme file's name, as a menu item's caption: each '&' doubled, so it shows rather than making // the next letter the item's accelerator. std::string GUIMenuItemText(std::string const& text); +// 'texts', a menu's items in order, as captions (see GUIMenuItemText()), each with an accelerator of its own: the +// first ASCII letter or digit, ignoring case, that's neither in 'takenLetters' (the menu's other items') nor an +// earlier text's, preferring one that starts a word. A text with none left gets none. +std::vector GUIMenuItemTextsWithAccelerators(std::vector const& texts, + std::string const& takenLetters); // The status bar's panels' text, in order: the checked and shown tests, the run's progress, the passed, failed // and skipped counts, the run's elapsed time (empty if no run has started), the shuffle setting ('shuffleText', // from TGUIShuffle::StatusText()), and 'message'. diff --git a/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp b/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp index cf610e1..d20b92b 100644 --- a/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp +++ b/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp @@ -98,6 +98,8 @@ TTest_ASWUnitTests_GUI_Theme::TTest_ASWUnitTests_GUI_Theme() "GUIThemeIsDark_FollowsTheChoiceOrWindows"); RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_GUIThemeNames_ListsTheFilesOfOneKindSorted, "GUIThemeNames_ListsTheFilesOfOneKindSorted"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_GUIVCLStyleDarkThemeNames_AreSortedWithoutCarbon, + "GUIVCLStyleDarkThemeNames_AreSortedWithoutCarbon"); RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_LoadGUIDarkTheme_DefaultsToCarbon, "LoadGUIDarkTheme_DefaultsToCarbon"); RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_LoadGUITheme_DefaultsToWindows, "LoadGUITheme_DefaultsToWindows"); @@ -194,6 +196,16 @@ void TTest_ASWUnitTests_GUI_Theme::Test_GUIThemeNames_ListsTheFilesOfOneKindSort "an empty or missing folder"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_GUIVCLStyleDarkThemeNames_AreSortedWithoutCarbon() +{ + // Act + std::vector const names = GUIVCLStyleDarkThemeNames(); + + // Assert: RAD Studio's style names, which the VCL GUI runner's project links in. + CheckEquals(std::string("Charcoal Dark Slate\nCobalt XEMedia\nOnyx Blue\nTablet Dark\n"), JoinLines(names), __func__, + __LINE__, "the picked styles, sorted, Carbon not among them"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_Theme::Test_LoadGUIDarkTheme_DefaultsToCarbon() { // Act and Assert diff --git a/gui/tests/Test_ASWUnitTests_GUI_Theme.h b/gui/tests/Test_ASWUnitTests_GUI_Theme.h index 4ba4930..745a115 100644 --- a/gui/tests/Test_ASWUnitTests_GUI_Theme.h +++ b/gui/tests/Test_ASWUnitTests_GUI_Theme.h @@ -44,6 +44,7 @@ class TTest_ASWUnitTests_GUI_Theme : public TTestGroupBase void Test_FormatGUIThemeColor_RoundTripsThroughParse(); void Test_GUIThemeIsDark_FollowsTheChoiceOrWindows(); void Test_GUIThemeNames_ListsTheFilesOfOneKindSorted(); + void Test_GUIVCLStyleDarkThemeNames_AreSortedWithoutCarbon(); void Test_LoadGUIDarkTheme_DefaultsToCarbon(); void Test_LoadGUITheme_DefaultsToWindows(); void Test_LoadGUITheme_ReadsEachThemeIgnoringCase(); diff --git a/gui/tests/Test_ASWUnitTests_GUI_View.cpp b/gui/tests/Test_ASWUnitTests_GUI_View.cpp index 97dc302..0f01b4e 100644 --- a/gui/tests/Test_ASWUnitTests_GUI_View.cpp +++ b/gui/tests/Test_ASWUnitTests_GUI_View.cpp @@ -61,6 +61,8 @@ TTest_ASWUnitTests_GUI_View::TTest_ASWUnitTests_GUI_View() "GUIFirstLine_StopsAtTheFirstLineBreak"); RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIMenuItemText_DoublesEachAmpersand, "GUIMenuItemText_DoublesEachAmpersand"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIMenuItemTextsWithAccelerators_GiveEachALetterOfItsOwn, + "GUIMenuItemTextsWithAccelerators_GiveEachALetterOfItsOwn"); RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIStatusPanels_ShowCountsAndMessage, "GUIStatusPanels_ShowCountsAndMessage"); RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIStatusPanelWidths_OnePerPanel, "GUIStatusPanelWidths_OnePerPanel"); @@ -228,6 +230,40 @@ void TTest_ASWUnitTests_GUI_View::Test_GUIMenuItemText_DoublesEachAmpersand() CheckEmpty(GUIMenuItemText(std::string()), __func__, __LINE__, "empty"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIMenuItemTextsWithAccelerators_GiveEachALetterOfItsOwn() +{ + // Arrange + auto const join = [](std::vector const& captions) + { + std::string text; + + for (std::string const& caption : captions) + text += caption + "\n"; + + return text; + }; + + std::vector const themes = { + "Carbon", "Charcoal Dark Slate", "Cobalt XEMedia", "Ember", "Forest", "Midnight", "Onyx Blue", "Plum", + "Tablet Dark" + }; + + // Act and Assert + CheckEquals(std::string("&Carbon\nCharcoal &Dark Slate\nCobalt &XEMedia\n&Ember\n&Forest\n&Midnight\nOnyx &Blue\n" + "&Plum\n&Tablet Dark\n"), join(GUIMenuItemTextsWithAccelerators(themes, "so")), __func__, __LINE__, + "a word's first letter not taken before, by the menu's other items or an earlier text"); + CheckEquals(std::string("S&ave\n"), join(GUIMenuItemTextsWithAccelerators({ "Save" }, "S")), __func__, __LINE__, + "else any letter, ignoring case"); + CheckEquals(std::string("&A\nA&&&B\n"), join(GUIMenuItemTextsWithAccelerators({ "A", "A&B" }, "")), __func__, + __LINE__, "an '&' doubled, not an accelerator"); + CheckEquals(std::string("\xC3\x89&cume\n"), join(GUIMenuItemTextsWithAccelerators({ "\xC3\x89" "cume" }, "")), + __func__, __LINE__, "an ASCII letter only"); + CheckEquals(std::string("&x\nx\n"), join(GUIMenuItemTextsWithAccelerators({ "x", "x" }, "")), __func__, __LINE__, + "none when none is left"); + CheckEmpty(join(GUIMenuItemTextsWithAccelerators(std::vector(), "so")), __func__, __LINE__, + "no texts, no captions"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_View::Test_GUIStatusPanels_ShowCountsAndMessage() { // Arrange: three tests, one hidden by the filter box, one unchecked, one passed and one failed. diff --git a/gui/tests/Test_ASWUnitTests_GUI_View.h b/gui/tests/Test_ASWUnitTests_GUI_View.h index 0b6099f..002e15c 100644 --- a/gui/tests/Test_ASWUnitTests_GUI_View.h +++ b/gui/tests/Test_ASWUnitTests_GUI_View.h @@ -49,6 +49,7 @@ class TTest_ASWUnitTests_GUI_View : public TTestGroupBase void Test_GUIFilterBoxFilter_MatchesAnywhereIgnoringCase(); void Test_GUIFirstLine_StopsAtTheFirstLineBreak(); void Test_GUIMenuItemText_DoublesEachAmpersand(); + void Test_GUIMenuItemTextsWithAccelerators_GiveEachALetterOfItsOwn(); void Test_GUIStatusPanels_ShowCountsAndMessage(); void Test_GUIStatusPanelWidths_OnePerPanel(); void Test_GUIUnhandledExceptionRecord_FailsTheRunningTest(); diff --git a/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cbproj b/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cbproj index 4b33a31..fec860c 100644 --- a/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cbproj +++ b/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cbproj @@ -101,7 +101,7 @@ 1033 CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= false - Carbon|VCLSTYLE|$(BDSCOMMONDIR)\Styles\Carbon.vsf + Carbon|VCLSTYLE|$(BDSCOMMONDIR)\Styles\Carbon.vsf;"Charcoal Dark Slate|VCLSTYLE|$(BDSCOMMONDIR)\Styles\CharcoalDarkSlate.vsf";"Cobalt XEMedia|VCLSTYLE|$(BDSCOMMONDIR)\Styles\CobaltXEMedia.vsf";"Onyx Blue|VCLSTYLE|$(BDSCOMMONDIR)\Styles\OnyxBlue.vsf";"Tablet Dark|VCLSTYLE|$(BDSCOMMONDIR)\Styles\TabletDark.vsf" adortl;bcbie;bcbsmp;bindcomp;bindcompdbx;bindcompfmx;bindcompvcl;bindcompvclsmp;bindcompvclwinx;bindengine;CloudService;CustomIPTransport;dbexpress;dbrtl;dbxcds;DbxClientDriver;DbxCommonDriver;DBXInterBaseDriver;DBXMySQLDriver;DBXSqliteDriver;dsnap;dsnapcon;dsnapxml;FireDAC;FireDACADSDriver;FireDACCommon;FireDACCommonDriver;FireDACCommonODBC;FireDACIBDriver;FireDACMSAccDriver;FireDACMySQLDriver;FireDACPgDriver;FireDACSqliteDriver;fmx;fmxase;fmxdae;fmxFireDAC;fmxobj;FMXTee;FmxTeeUI;IndyCore;IndyIPClient;IndyIPCommon;IndyIPServer;IndyProtocols;IndySystem;inet;inetdb;inetdbxpress;inetstn;RESTBackendComponents;RESTComponents;rtl;Skia;soapmidas;soaprtl;soapserver;Tee;TeeDB;TeeUI;tethering;vcl;vclactnband;vcldb;vcldsnap;vcledge;vclFireDAC;vclie;vclimg;VCLRESTComponents;VclSmp;vcltouch;vclwinx;vclx;xmlrtl;$(PackageImports) diff --git a/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp b/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp index 3a20f78..acb354c 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp +++ b/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp @@ -29,6 +29,7 @@ limitations under the License. #pragma resource "*.dfm" //--------------------------------------------------------------------------- #include +#include #include #include #include @@ -889,9 +890,10 @@ void TFormASWUnitTestsGUIMain::FailRunOnUnhandledException(std::string const& de /* TFormASWUnitTestsGUIMain::FillDarkThemeMenu - Carbon first, then the VCL style files in GUIThemesFolderPath() (see GUIThemeNames()), so a file added or - removed since shows at once, above Open Themes Folder; the chosen one is checked. A chosen theme whose file is - missing is still listed, so choosing it again reports why it isn't used. + The built-in themes first (see BuiltInGUIDarkThemeNames()), Carbon at the top, then a line and the VCL style + files in GUIThemesFolderPath() (see GUIThemeNames()), so a file added or removed since shows at once, above + Open Themes Folder; the chosen one is checked. A chosen theme whose file is missing is still listed, so + choosing it again reports why it isn't used. */ void TFormASWUnitTestsGUIMain::FillDarkThemeMenu() { @@ -900,9 +902,10 @@ void TFormASWUnitTestsGUIMain::FillDarkThemeMenu() m_DarkThemeItems.clear(); - std::vector names = GUIThemeNames(FileNamesInFolder(GUIThemesFolderPath()), ".vsf", - { GUIDefaultDarkThemeName() }); - names.insert(names.begin(), GUIDefaultDarkThemeName()); + std::vector names = BuiltInGUIDarkThemeNames(); + size_t const builtInCount = names.size(); + std::vector const files = GUIThemeNames(FileNamesInFolder(GUIThemesFolderPath()), ".vsf", names); + names.insert(names.end(), files.begin(), files.end()); // Windows' file names ignore case, so a name saved in another case is still that file's. std::vector::const_iterator chosen = std::find_if(names.begin(), names.end(), @@ -915,20 +918,32 @@ void TFormASWUnitTestsGUIMain::FillDarkThemeMenu() chosen = names.insert(names.end(), m_DarkTheme); size_t const chosenIndex = static_cast(chosen - names.begin()); + // A built-in theme's accelerator, other than Open Themes Folder's O; a style file's name has none. + std::vector const builtInCaptions = GUIMenuItemTextsWithAccelerators( + std::vector(names.begin(), names.begin() + static_cast(builtInCount)), "o"); + int position = 0; for (size_t i = 0; i < names.size(); ++i) { + // A line between the built-in themes and the files. + if (i == builtInCount) + { + TMenuItem* const line = new TMenuItem(this); + m_DarkThemeItems.push_back(line); + line->Caption = "-"; + MI_ViewDarkTheme->Insert(position++, line); + } + TMenuItem* const item = new TMenuItem(this); m_DarkThemeItems.push_back(item); - // Carbon's C is its accelerator; a style file's name has none. - item->Caption = FromUTF8((i == 0 ? "&" : "") + GUIMenuItemText(names[i])); + item->Caption = FromUTF8(i < builtInCount ? builtInCaptions[i] : GUIMenuItemText(names[i])); item->Checked = i == chosenIndex; item->Enabled = Act_ThemeDark->Enabled; item->OnClick = DarkThemeItemClick; item->RadioItem = true; item->Tag = static_cast(i); - MI_ViewDarkTheme->Insert(static_cast(i), item); + MI_ViewDarkTheme->Insert(position++, item); } m_DarkThemeNames = names; diff --git a/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.cpp b/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.cpp index 62360cf..0f2e2b7 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.cpp +++ b/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.cpp @@ -61,6 +61,26 @@ bool ApplyGUITheme(EGUITheme theme, System::UnicodeString const& darkStyle) return Vcl::Themes::TStyleManager::TrySetStyle(styleName, false); } //--------------------------------------------------------------------------- +std::vector BuiltInGUIDarkThemeNames() +{ + std::vector names = { GUIDefaultDarkThemeName() }; + System::DynamicArray const styles = Vcl::Themes::TStyleManager::StyleNames; + + for (std::string const& name : GUIVCLStyleDarkThemeNames()) + { + for (int i = 0; i < styles.Length; ++i) + { + if (System::Sysutils::SameText(styles[i], FromUTF8(name))) + { + names.push_back(name); + break; + } + } + } + + return names; +} +//--------------------------------------------------------------------------- std::vector FileNamesInFolder(System::UnicodeString const& folderPath) { std::vector names; @@ -112,7 +132,8 @@ System::UnicodeString GUIThemesFolderPath() A style, once loaded, stays registered for the process, under the name the file gives it, and can't be loaded again, so a file loaded before gives the style it loaded, and a change to it shows after a restart. A file whose style has the name of one already registered, e.g. Carbon's own file renamed, can't be loaded - either. Every error gives Carbon, with a line in 'problems'. + either. Every error gives Carbon, with a line in 'problems'. A built-in style that isn't linked in is looked + for as a file. */ System::UnicodeString LoadGUIDarkStyle(System::UnicodeString const& themesFolder, std::string const& darkTheme, std::vector& problems) @@ -122,8 +143,11 @@ System::UnicodeString LoadGUIDarkStyle(System::UnicodeString const& themesFolder System::UnicodeString const carbon = "Carbon"; - if (darkTheme == GUIDefaultDarkThemeName()) - return carbon; + for (std::string const& name : BuiltInGUIDarkThemeNames()) + { + if (System::Sysutils::SameText(FromUTF8(name), FromUTF8(darkTheme))) + return FromUTF8(name); + } std::string problem; diff --git a/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h b/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h index 8251f39..c5439a0 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h +++ b/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h @@ -40,6 +40,10 @@ namespace ASWUnitTests // otherwise the default unstyled look. Returns whether the active style changed. Call it on the main thread; // before any form exists, each form is created in that style, and afterward it restyles the open ones. bool ApplyGUITheme(EGUITheme theme, System::UnicodeString const& darkStyle); +// The dark themes this runner has built in, for the View > Theme > Dark Theme menu: Carbon (see +// GUIDefaultDarkThemeName()), then each of GUIVCLStyleDarkThemeNames() whose VCL style is linked in (the GUI +// project's Custom_Styles), by its style name. +std::vector BuiltInGUIDarkThemeNames(); // The names of the files (not folders) in the folder, without the folder, converted to UTF-8; empty if the // folder doesn't exist or can't be listed. std::vector FileNamesInFolder(System::UnicodeString const& folderPath); @@ -52,11 +56,11 @@ System::UnicodeString GUISelectionFilePath(); // The folder that holds the theme files both GUI runners share, whatever executable runs them: // %APPDATA%\ASWUnitTests\Themes. The VCL GUI runner's are VCL style files, ".vsf". It may not exist yet. System::UnicodeString GUIThemesFolderPath(); -// The VCL style the dark theme 'darkTheme' is, for ApplyGUITheme(): Carbon, which the GUI project links in, for -// GUIDefaultDarkThemeName(); otherwise the style in ".vsf" in 'themesFolder', e.g. -// GUIThemesFolderPath(), loaded the first time it's needed. Adds a line for the log to 'problems' (see -// GUIThemeProblemLine()), and gives Carbon, when the file is missing, isn't a VCL style, or has the name of a -// style already loaded from elsewhere. +// The VCL style the dark theme 'darkTheme' is, for ApplyGUITheme(): the built-in style of that name, ignoring +// case (see BuiltInGUIDarkThemeNames()), Carbon included; otherwise the style in ".vsf" in +// 'themesFolder', e.g. GUIThemesFolderPath(), loaded the first time it's needed. Adds a line for the log to +// 'problems' (see GUIThemeProblemLine()), and gives Carbon, when the file is missing, isn't a VCL style, or has +// the name of a style already loaded from elsewhere. System::UnicodeString LoadGUIDarkStyle(System::UnicodeString const& themesFolder, std::string const& darkTheme, std::vector& problems); // The dark theme SaveGUIDarkThemeToLayoutFile() saved in the layout file at 'layoutPath', e.g. diff --git a/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.cpp b/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.cpp index 7c6d49b..ce3ad92 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.cpp +++ b/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.cpp @@ -33,6 +33,7 @@ limitations under the License. #include #include #include +#include #include //--------------------------------------------------------------------------- #include "ASWUnitTests_Registry.h" @@ -53,6 +54,8 @@ namespace ASWUnitTests TTest_ASWUnitTests_GUI_VCLSystem::TTest_ASWUnitTests_GUI_VCLSystem() : inherited("ASWUnitTests_GUI_VCLSystem_Tests") { + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_BuiltInGUIDarkThemeNames_AreCarbonAndTheLinkedStyles, + "BuiltInGUIDarkThemeNames_AreCarbonAndTheLinkedStyles"); RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_FileNamesInFolder_ListsOnlyTheFiles, "FileNamesInFolder_ListsOnlyTheFiles"); RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_GUILayoutFilePath_IsPerExeUnderAppData, @@ -95,6 +98,31 @@ void TTest_ASWUnitTests_GUI_VCLSystem::TearDown_Test(ITestCase& /*testCase*/) // /////// Begin tests after this line /////////////////////// +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_BuiltInGUIDarkThemeNames_AreCarbonAndTheLinkedStyles() +{ + // Arrange: the styles this runner links in, none in the console runner, all of them in the GUI. + System::DynamicArray const styles = Vcl::Themes::TStyleManager::StyleNames; + std::string expected = GUIDefaultDarkThemeName() + "\n"; + + for (std::string const& name : GUIVCLStyleDarkThemeNames()) + { + for (int i = 0; i < styles.Length; ++i) + { + if (System::Sysutils::SameText(styles[i], FromUTF8(name))) + expected += name + "\n"; + } + } + + // Act + std::string names; + + for (std::string const& name : BuiltInGUIDarkThemeNames()) + names += name + "\n"; + + // Assert + CheckEquals(expected, names, __func__, __LINE__, "Carbon first, then each linked style, in order"); +} //--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_VCLSystem::Test_FileNamesInFolder_ListsOnlyTheFiles() { diff --git a/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.h b/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.h index 73e0af8..9a2bb14 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.h +++ b/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.h @@ -41,6 +41,7 @@ class TTest_ASWUnitTests_GUI_VCLSystem : public TTestGroupBase typedef TTestGroupBase inherited; private: // Test methods + void Test_BuiltInGUIDarkThemeNames_AreCarbonAndTheLinkedStyles(); void Test_FileNamesInFolder_ListsOnlyTheFiles(); void Test_GUILayoutFilePath_IsPerExeUnderAppData(); void Test_GUISelectionFilePath_IsBesideTheLayoutFile(); diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp index 6695642..f1d810f 100644 --- a/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp +++ b/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp @@ -1563,6 +1563,10 @@ void TWin32GUIMainWindow::FillDarkThemeMenu() DeleteMenu(m_MI_ViewDarkTheme, 0, MF_BYPOSITION); UINT const enabled = m_CommandStates.Theme ? MF_ENABLED : MF_GRAYED; + // A built-in theme's accelerator, other than Save Theme Template's S and Open Themes Folder's O; a theme + // file's name has none. + std::vector const builtInCaptions = GUIMenuItemTextsWithAccelerators( + std::vector(names.begin(), names.begin() + static_cast(builtInCount)), "so"); for (std::size_t i = 0; i < names.size(); ++i) { @@ -1570,9 +1574,8 @@ void TWin32GUIMainWindow::FillDarkThemeMenu() if (i == builtInCount) AppendMenuW(m_MI_ViewDarkTheme, MF_SEPARATOR, 0, nullptr); - // A built-in theme's first letter is its accelerator; a theme file's name has none. UINT const id = static_cast(FirstDarkThemeCommand + i); - std::wstring const caption = UTF8ToWide((i < builtInCount ? "&" : "") + GUIMenuItemText(names[i])); + std::wstring const caption = UTF8ToWide(i < builtInCount ? builtInCaptions[i] : GUIMenuItemText(names[i])); AppendMenuW(m_MI_ViewDarkTheme, MF_STRING | enabled, id, caption.c_str()); // Only it, as nothing else is checked yet, so the range needn't span the line above the files. diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp index 7aec232..12da01f 100644 --- a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp @@ -201,16 +201,21 @@ COLORREF BlendColors(COLORREF from, COLORREF to, int numerator, int denominator) /* BuiltInDarkThemes - Each tint's hue and strength, and its accent's hue, chosen by eye in the runner. + Each tint's hue and strength, and its accent's hue, chosen by eye in the runner. The VCL styles' names are + GUIVCLStyleDarkThemeNames()'s. */ std::vector const& BuiltInDarkThemes() { static std::vector const themes = { { GUIDefaultDarkThemeName(), CarbonPalette() }, + { "Charcoal Dark Slate", CharcoalDarkSlatePalette() }, + { "Cobalt XEMedia", CobaltXEMediaPalette() }, { "Ember", TintedCarbonPalette(25.0, 0.09, 30.0) }, { "Forest", TintedCarbonPalette(140.0, 0.08, 155.0) }, { "Midnight", TintedCarbonPalette(220.0, 0.10, 205.0) }, - { "Plum", TintedCarbonPalette(285.0, 0.09, 295.0) } + { "Onyx Blue", OnyxBluePalette() }, + { "Plum", TintedCarbonPalette(285.0, 0.09, 295.0) }, + { "Tablet Dark", TabletDarkPalette() } }; return themes; @@ -238,9 +243,11 @@ TWin32GUIPalette CarbonPalette() palette.ButtonHotBorder = RGB(0x57, 0x57, 0x57); palette.ButtonHotFaceBottom = RGB(0x35, 0x35, 0x35); palette.ButtonHotFaceTop = RGB(0x47, 0x47, 0x47); + palette.ButtonHotText = RGB(0xFF, 0xFF, 0xFF); palette.ButtonOutline = RGB(0x19, 0x19, 0x19); palette.ButtonPressedFaceBottom = RGB(0x48, 0x48, 0x48); palette.ButtonPressedFaceTop = RGB(0x37, 0x37, 0x37); + palette.ButtonText = RGB(0xC0, 0xC0, 0xC0); palette.CheckBoxBorder = RGB(0x47, 0x47, 0x47); palette.CheckBoxFill = RGB(0x19, 0x19, 0x19); palette.CheckBoxHotBorder = RGB(0x5F, 0x5F, 0x5F); @@ -273,6 +280,7 @@ TWin32GUIPalette CarbonPalette() palette.ProgressTrack = RGB(0x19, 0x19, 0x19); palette.Selection = RGB(0x62, 0x62, 0x62); palette.SelectionFrame = RGB(0x38, 0x38, 0x38); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); palette.SpinArrow = RGB(0x1C, 0x1C, 0x1C); palette.SpinDisabledArrow = RGB(0x2A, 0x2A, 0x2A); palette.SpinDisabledFaceBottom = RGB(0x36, 0x38, 0x37); @@ -363,6 +371,180 @@ std::string CarbonThemeFileText() return text; } //--------------------------------------------------------------------------- +/* + CharcoalDarkSlatePalette + + The colors of RAD Studio's Charcoal Dark Slate style, read from its .vsf file as CarbonPalette()'s were + (plans/1.5/experiments/style_palette.dpr), with the values its elements don't give taken from the VCL GUI + runner under it; the selection and the colors that say how a test did are Carbon's, as the VCL GUI + runner's are under every style. +*/ +TWin32GUIPalette CharcoalDarkSlatePalette() +{ + TWin32GUIPalette palette = CarbonPalette(); + + palette.ButtonBorder = RGB(0x43, 0x43, 0x43); + palette.ButtonDefaultFrame = RGB(0x32, 0x32, 0x32); + palette.ButtonDisabledBorder = RGB(0x45, 0x45, 0x45); + palette.ButtonDisabledFaceBottom = RGB(0x39, 0x39, 0x39); + palette.ButtonDisabledFaceTop = RGB(0x4C, 0x4C, 0x4C); + palette.ButtonDisabledOutline = RGB(0x2F, 0x2F, 0x2F); + palette.ButtonFaceBottom = RGB(0x28, 0x28, 0x28); + palette.ButtonFaceTop = RGB(0x40, 0x40, 0x40); + palette.ButtonHotBorder = RGB(0x4A, 0x4A, 0x4A); + palette.ButtonHotFaceBottom = RGB(0x1F, 0x1F, 0x1F); + palette.ButtonHotFaceTop = RGB(0x32, 0x32, 0x32); + palette.ButtonHotText = RGB(0xFF, 0xFF, 0xFF); + palette.ButtonOutline = RGB(0x27, 0x27, 0x27); + palette.ButtonPressedFaceBottom = RGB(0x20, 0x20, 0x20); + palette.ButtonPressedFaceTop = RGB(0x14, 0x14, 0x14); + palette.ButtonText = RGB(0xA3, 0xA3, 0xA3); + palette.CheckBoxBorder = RGB(0x5D, 0x5D, 0x5D); + palette.CheckBoxFill = RGB(0x1F, 0x1F, 0x1F); + palette.CheckBoxHotBorder = RGB(0x70, 0x70, 0x70); + palette.CheckMark = RGB(0x7B, 0x7B, 0x7B); + palette.CheckMarkDisabled = RGB(0x4D, 0x4D, 0x4D); + palette.ControlText = RGB(0xC0, 0xC0, 0xC0); + palette.DisabledText = RGB(0x80, 0x80, 0x80); + palette.EditBorderInner = RGB(0x5E, 0x5E, 0x5E); + palette.EditBorderOuter = RGB(0x36, 0x36, 0x36); + palette.GripDot = RGB(0x41, 0x41, 0x41); + palette.HeaderBottom = RGB(0x3E, 0x3E, 0x3E); + palette.HeaderDivider = RGB(0x2D, 0x2D, 0x2D); + palette.HeaderHot = RGB(0x43, 0x43, 0x43); + palette.HeaderTop = RGB(0x49, 0x49, 0x49); + palette.Highlight = RGB(0x5F, 0x5F, 0x5F); + palette.HighlightFrame = RGB(0x8A, 0x8A, 0x8A); + palette.HotText = RGB(0xFF, 0xFF, 0xFF); + palette.MenuBackground = RGB(0x1F, 0x1F, 0x1F); + palette.MenuBarDisabledText = RGB(0x93, 0x93, 0x93); + palette.MenuBorderInner = RGB(0x58, 0x58, 0x58); + palette.MenuBorderOuter = RGB(0x36, 0x36, 0x36); + palette.MenuDisabledText = RGB(0x69, 0x69, 0x69); + palette.MenuSeparator = RGB(0x3B, 0x3B, 0x3B); + palette.Panel = RGB(0x3E, 0x3E, 0x3E); + palette.ProgressBorder = RGB(0x29, 0x29, 0x29); + palette.ProgressChunkBottom = RGB(0x5C, 0x5C, 0x5C); + palette.ProgressChunkTop = RGB(0x5C, 0x5C, 0x5C); + palette.ProgressTrack = RGB(0x31, 0x31, 0x31); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); + palette.SpinArrow = RGB(0x7D, 0x7D, 0x7D); + palette.SpinDisabledArrow = RGB(0x40, 0x40, 0x40); + palette.SpinDisabledFaceBottom = RGB(0x23, 0x23, 0x23); + palette.SpinDisabledFaceTop = RGB(0x23, 0x23, 0x23); + palette.SpinDisabledOutline = RGB(0x23, 0x23, 0x23); + palette.SpinFaceBottom = RGB(0x23, 0x23, 0x23); + palette.SpinFaceTop = RGB(0x23, 0x23, 0x23); + palette.SpinHotFaceBottom = RGB(0x23, 0x23, 0x23); + palette.SpinHotFaceTop = RGB(0x23, 0x23, 0x23); + palette.SpinOutline = RGB(0x23, 0x23, 0x23); + palette.SpinPressedEdge = RGB(0x23, 0x23, 0x23); + palette.StatusLine = RGB(0x40, 0x40, 0x40); + palette.TabActiveBottom = RGB(0x3E, 0x3E, 0x3E); + palette.TabActiveTop = RGB(0x57, 0x57, 0x57); + palette.TabBorder = RGB(0x40, 0x40, 0x40); + palette.TabHot = RGB(0x25, 0x25, 0x25); + palette.TabInactive = RGB(0x25, 0x25, 0x25); + palette.TabInactiveBorder = RGB(0x23, 0x23, 0x23); + palette.TabOuter = RGB(0x23, 0x23, 0x23); + palette.TooltipBackground = RGB(0x5B, 0x5B, 0x5B); + palette.TooltipText = RGB(0xFF, 0xFF, 0xFF); + palette.Window = RGB(0x23, 0x23, 0x23); + palette.WindowText = RGB(0xC0, 0xC0, 0xC0); + + palette.FailureText = FailureTextColorOn(palette.Window); + palette.SkipText = SkipTextColorOn(palette.Window); + palette.TreeLine = BlendColors(palette.Window, palette.WindowText, 2, 5); + return palette; +} +//--------------------------------------------------------------------------- +/* + CobaltXEMediaPalette + + The colors of RAD Studio's Cobalt XEMedia style, read from its .vsf file as CarbonPalette()'s were + (plans/1.5/experiments/style_palette.dpr), with the values its elements don't give taken from the VCL GUI + runner under it; the selection and the colors that say how a test did are Carbon's, as the VCL GUI + runner's are under every style. +*/ +TWin32GUIPalette CobaltXEMediaPalette() +{ + TWin32GUIPalette palette = CarbonPalette(); + + palette.ButtonBorder = RGB(0x07, 0x0B, 0x11); + palette.ButtonDefaultFrame = RGB(0x5A, 0x77, 0x9B); + palette.ButtonDisabledBorder = RGB(0x09, 0x0A, 0x0B); + palette.ButtonDisabledFaceBottom = RGB(0x16, 0x18, 0x1C); + palette.ButtonDisabledFaceTop = RGB(0x2B, 0x2E, 0x32); + palette.ButtonDisabledOutline = RGB(0x14, 0x17, 0x1A); + palette.ButtonFaceBottom = RGB(0x1A, 0x33, 0x53); + palette.ButtonFaceTop = RGB(0x46, 0x53, 0x66); + palette.ButtonHotBorder = RGB(0x07, 0x0F, 0x1C); + palette.ButtonHotFaceBottom = RGB(0x2A, 0x79, 0xB0); + palette.ButtonHotFaceTop = RGB(0x5A, 0x77, 0x9B); + palette.ButtonHotText = RGB(0xFF, 0xFF, 0xFF); + palette.ButtonOutline = RGB(0x11, 0x1A, 0x28); + palette.ButtonPressedFaceBottom = RGB(0x15, 0x4D, 0x75); + palette.ButtonPressedFaceTop = RGB(0x38, 0x4C, 0x66); + palette.ButtonText = RGB(0xC0, 0xC0, 0xC0); + palette.CheckBoxBorder = RGB(0x39, 0x57, 0x8B); + palette.CheckBoxFill = RGB(0x17, 0x2E, 0x4D); + palette.CheckBoxHotBorder = RGB(0x54, 0x6E, 0x9C); + palette.CheckMark = RGB(0x27, 0x75, 0xB8); + palette.CheckMarkDisabled = RGB(0x1B, 0x49, 0x74); + palette.ControlText = RGB(0xC0, 0xC0, 0xC0); + palette.DisabledText = RGB(0x41, 0x41, 0x41); + palette.EditBorderInner = RGB(0x04, 0x06, 0x0C); + palette.EditBorderOuter = RGB(0x18, 0x31, 0x53); + palette.GripDot = RGB(0x1B, 0x2B, 0x45); + palette.HeaderBottom = RGB(0x10, 0x17, 0x24); + palette.HeaderDivider = RGB(0x00, 0x00, 0x00); + palette.HeaderHot = RGB(0x0B, 0x11, 0x19); + palette.HeaderTop = RGB(0x19, 0x21, 0x2D); + palette.Highlight = RGB(0x0B, 0x36, 0x65); + palette.HighlightFrame = RGB(0x29, 0x45, 0x69); + palette.HotText = RGB(0xFF, 0xFF, 0xFF); + palette.MenuBackground = RGB(0x00, 0x00, 0x00); + palette.MenuBarDisabledText = RGB(0x41, 0x41, 0x41); + palette.MenuBorderInner = RGB(0x0D, 0x18, 0x25); + palette.MenuBorderOuter = RGB(0x21, 0x39, 0x50); + palette.MenuDisabledText = RGB(0x41, 0x41, 0x41); + palette.MenuSeparator = RGB(0x0B, 0x36, 0x65); + palette.Panel = RGB(0x01, 0x01, 0x01); + palette.ProgressBorder = RGB(0x18, 0x31, 0x53); + palette.ProgressChunkBottom = RGB(0x24, 0x8B, 0xD1); + palette.ProgressChunkTop = RGB(0x86, 0xB1, 0xCC); + palette.ProgressTrack = RGB(0x00, 0x00, 0x00); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); + palette.SpinArrow = RGB(0x4C, 0x6D, 0x8F); + palette.SpinDisabledArrow = RGB(0x2D, 0x41, 0x56); + palette.SpinDisabledFaceBottom = RGB(0x02, 0x04, 0x08); + palette.SpinDisabledFaceTop = RGB(0x02, 0x04, 0x08); + palette.SpinDisabledOutline = RGB(0x02, 0x04, 0x08); + palette.SpinFaceBottom = RGB(0x02, 0x04, 0x08); + palette.SpinFaceTop = RGB(0x02, 0x04, 0x08); + palette.SpinHotFaceBottom = RGB(0x02, 0x04, 0x08); + palette.SpinHotFaceTop = RGB(0x02, 0x04, 0x08); + palette.SpinOutline = RGB(0x02, 0x04, 0x08); + palette.SpinPressedEdge = RGB(0x02, 0x04, 0x08); + palette.StatusLine = RGB(0x03, 0x05, 0x07); + palette.TabActiveBottom = RGB(0x0E, 0x13, 0x1C); + palette.TabActiveTop = RGB(0x21, 0x2F, 0x45); + palette.TabBorder = RGB(0x0A, 0x0E, 0x15); + palette.TabHot = RGB(0x05, 0x07, 0x0A); + palette.TabInactive = RGB(0x05, 0x07, 0x0A); + palette.TabInactiveBorder = RGB(0x0D, 0x0D, 0x0D); + palette.TabOuter = RGB(0x32, 0x43, 0x5E); + palette.TooltipBackground = RGB(0x26, 0x46, 0x6C); + palette.TooltipText = RGB(0xC0, 0xC0, 0xC0); + palette.Window = RGB(0x04, 0x07, 0x0E); + palette.WindowText = RGB(0xFF, 0xFF, 0xFF); + + palette.FailureText = FailureTextColorOn(palette.Window); + palette.SkipText = SkipTextColorOn(palette.Window); + palette.TreeLine = BlendColors(palette.Window, palette.WindowText, 2, 5); + return palette; +} +//--------------------------------------------------------------------------- COLORREF FailureTextColorOn(COLORREF background) { return IsDarkColor(background) ? RGB(255, 100, 100) : RGB(200, 0, 0); @@ -386,6 +568,93 @@ bool IsDarkColor(COLORREF color) return luma < 128; } //--------------------------------------------------------------------------- +/* + OnyxBluePalette + + The colors of RAD Studio's Onyx Blue style, read from its .vsf file as CarbonPalette()'s were + (plans/1.5/experiments/style_palette.dpr), with the values its elements don't give taken from the VCL GUI + runner under it; the selection and the colors that say how a test did are Carbon's, as the VCL GUI + runner's are under every style. +*/ +TWin32GUIPalette OnyxBluePalette() +{ + TWin32GUIPalette palette = CarbonPalette(); + + palette.ButtonBorder = RGB(0xAD, 0xB0, 0xB4); + palette.ButtonDefaultFrame = RGB(0x74, 0xA0, 0xC4); + palette.ButtonDisabledBorder = RGB(0x36, 0x39, 0x3E); + palette.ButtonDisabledFaceBottom = RGB(0x23, 0x25, 0x28); + palette.ButtonDisabledFaceTop = RGB(0x42, 0x45, 0x49); + palette.ButtonDisabledOutline = RGB(0x2A, 0x2C, 0x30); + palette.ButtonFaceBottom = RGB(0x8C, 0x90, 0x95); + palette.ButtonFaceTop = RGB(0xD8, 0xDA, 0xDC); + palette.ButtonHotBorder = RGB(0x42, 0x72, 0x9A); + palette.ButtonHotFaceBottom = RGB(0x27, 0x41, 0x57); + palette.ButtonHotFaceTop = RGB(0x74, 0xA0, 0xC4); + palette.ButtonHotText = RGB(0xFF, 0xFF, 0xFF); + palette.ButtonOutline = RGB(0x2A, 0x2C, 0x30); + palette.ButtonPressedFaceBottom = RGB(0x3B, 0x5C, 0x77); + palette.ButtonPressedFaceTop = RGB(0x6D, 0x8C, 0xA6); + palette.ButtonText = RGB(0x18, 0x1A, 0x1D); + palette.CheckBoxBorder = RGB(0x6A, 0x6D, 0x72); + palette.CheckBoxFill = RGB(0x23, 0x25, 0x29); + palette.CheckBoxHotBorder = RGB(0x8C, 0x8E, 0x92); + palette.CheckMark = RGB(0xCC, 0xD0, 0xD4); + palette.CheckMarkDisabled = RGB(0x66, 0x69, 0x6E); + palette.ControlText = RGB(0xFF, 0xFF, 0xFF); + palette.DisabledText = RGB(0x80, 0x80, 0x80); + palette.EditBorderInner = RGB(0x29, 0x2C, 0x31); + palette.EditBorderOuter = RGB(0x1C, 0x1C, 0x1C); + palette.GripDot = RGB(0x4E, 0x4F, 0x57); + palette.HeaderBottom = RGB(0x3E, 0x41, 0x46); + palette.HeaderDivider = RGB(0x44, 0x47, 0x4B); + palette.HeaderHot = RGB(0x60, 0x63, 0x67); + palette.HeaderTop = RGB(0x4B, 0x4E, 0x52); + palette.Highlight = RGB(0x44, 0x75, 0x9D); + palette.HighlightFrame = RGB(0x41, 0x70, 0x96); + palette.HotText = RGB(0xFF, 0xFF, 0xFF); + palette.MenuBackground = RGB(0x31, 0x34, 0x39); + palette.MenuBarDisabledText = RGB(0x80, 0x80, 0x80); + palette.MenuBorderInner = RGB(0x3E, 0x41, 0x45); + palette.MenuBorderOuter = RGB(0x18, 0x1A, 0x1D); + palette.MenuDisabledText = RGB(0x80, 0x80, 0x80); + palette.MenuSeparator = RGB(0x3E, 0x40, 0x45); + palette.Panel = RGB(0x31, 0x34, 0x39); + palette.ProgressBorder = RGB(0x1C, 0x1C, 0x1C); + palette.ProgressChunkBottom = RGB(0x23, 0x3F, 0x57); + palette.ProgressChunkTop = RGB(0x69, 0x97, 0xBE); + palette.ProgressTrack = RGB(0x29, 0x2C, 0x31); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); + palette.SpinArrow = RGB(0xDD, 0xDE, 0xDF); + palette.SpinDisabledArrow = RGB(0x63, 0x67, 0x6B); + palette.SpinDisabledFaceBottom = RGB(0x26, 0x29, 0x2D); + palette.SpinDisabledFaceTop = RGB(0x26, 0x29, 0x2D); + palette.SpinDisabledOutline = RGB(0x26, 0x29, 0x2D); + palette.SpinFaceBottom = RGB(0x26, 0x29, 0x2D); + palette.SpinFaceTop = RGB(0x26, 0x29, 0x2D); + palette.SpinHotFaceBottom = RGB(0x26, 0x29, 0x2D); + palette.SpinHotFaceTop = RGB(0x26, 0x29, 0x2D); + palette.SpinOutline = RGB(0x26, 0x29, 0x2D); + palette.SpinPressedEdge = RGB(0x26, 0x29, 0x2D); + palette.StatusLine = RGB(0x29, 0x2C, 0x33); + palette.TabActiveBottom = RGB(0x2B, 0x2E, 0x33); + palette.TabActiveTop = RGB(0x35, 0x38, 0x3D); + palette.TabBorder = RGB(0x2C, 0x2F, 0x34); + palette.TabHot = RGB(0x21, 0x24, 0x28); + palette.TabInactive = RGB(0x21, 0x24, 0x28); + palette.TabInactiveBorder = RGB(0x20, 0x23, 0x27); + palette.TabOuter = RGB(0x20, 0x23, 0x27); + palette.TooltipBackground = RGB(0x12, 0x12, 0x12); + palette.TooltipText = RGB(0xFF, 0xFF, 0xFF); + palette.Window = RGB(0x29, 0x2C, 0x31); + palette.WindowText = RGB(0xFF, 0xFF, 0xFF); + + palette.FailureText = FailureTextColorOn(palette.Window); + palette.SkipText = SkipTextColorOn(palette.Window); + palette.TreeLine = BlendColors(palette.Window, palette.WindowText, 2, 5); + return palette; +} +//--------------------------------------------------------------------------- std::vector PaletteRoleNames() { std::vector names; @@ -410,7 +679,8 @@ std::vector const& PaletteRoles() static std::vector const roles = { ASWUNITTESTS_WIN32GUI_ROLE(ButtonBorder, "Push buttons: the border, the default (or focused) button's line inside it, the face's gradient\n" - "(bottom and top), normal, hot (under the mouse), pressed and disabled, and the outline around them."), + "(bottom and top), normal, hot (under the mouse), pressed and disabled, the outline around them, and\n" + "the text, normal and hot or pressed."), ASWUNITTESTS_WIN32GUI_ROLE(ButtonDefaultFrame, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ButtonDisabledBorder, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ButtonDisabledFaceBottom, nullptr), @@ -421,9 +691,11 @@ std::vector const& PaletteRoles() ASWUNITTESTS_WIN32GUI_ROLE(ButtonHotBorder, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ButtonHotFaceBottom, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ButtonHotFaceTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonHotText, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ButtonOutline, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ButtonPressedFaceBottom, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ButtonPressedFaceTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonText, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(CheckBoxBorder, "Check boxes (the test tree's too) and radio buttons: the box's or circle's border, its inside, its\n" "border under the mouse, and the mark (a cross, the partial state's square, a radio button's dot),\n" @@ -433,8 +705,8 @@ std::vector const& PaletteRoles() ASWUNITTESTS_WIN32GUI_ROLE(CheckMark, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(CheckMarkDisabled, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ControlText, - "The text of labels, buttons, radio buttons and check boxes, menus, the inactive tabs, the list's\n" - "header and the status bar."), + "The text of labels, radio buttons and check boxes, menus, the inactive tabs, the list's header and\n" + "the status bar."), ASWUNITTESTS_WIN32GUI_ROLE(DisabledText, "Disabled text, but the menus' (see MenuBarDisabledText)."), ASWUNITTESTS_WIN32GUI_ROLE(EditBorderInner, "The two-pixel border of the edit boxes, the tree, the list and the log: inner and outer."), @@ -449,7 +721,7 @@ std::vector const& PaletteRoles() ASWUNITTESTS_WIN32GUI_ROLE(HeaderTop, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(Highlight, "The menu item under the mouse or the keyboard, and its frame."), ASWUNITTESTS_WIN32GUI_ROLE(HighlightFrame, nullptr), - ASWUNITTESTS_WIN32GUI_ROLE(HotText, "Text over Highlight or Selection, and a hot or pressed button's."), + ASWUNITTESTS_WIN32GUI_ROLE(HotText, "Text over Highlight."), ASWUNITTESTS_WIN32GUI_ROLE(MenuBarDisabledText, "A disabled item's text, in the menu bar and in a menu."), ASWUNITTESTS_WIN32GUI_ROLE(MenuDisabledText, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(MenuBackground, @@ -468,8 +740,10 @@ std::vector const& PaletteRoles() ASWUNITTESTS_WIN32GUI_ROLE(ProgressFailedBottom, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ProgressFailedTop, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(ProgressTrack, nullptr), - ASWUNITTESTS_WIN32GUI_ROLE(Selection, "The selected node or row in the tree and the list, and its frame."), + ASWUNITTESTS_WIN32GUI_ROLE(Selection, + "The selected node or row in the tree and the list, its frame, and its text."), ASWUNITTESTS_WIN32GUI_ROLE(SelectionFrame, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SelectionText, nullptr), ASWUNITTESTS_WIN32GUI_ROLE(SkipText, "A skipped test's text in the log and the Failures & Skips list."), ASWUNITTESTS_WIN32GUI_ROLE(SpinArrow, "The results tabs' scroll arrows, when the tabs don't fit: the arrow, normal and disabled, each\n" @@ -539,6 +813,94 @@ COLORREF SkipTextColorOn(COLORREF background) return IsDarkColor(background) ? RGB(255, 190, 70) : RGB(190, 120, 0); } //--------------------------------------------------------------------------- +/* + TabletDarkPalette + + The colors of RAD Studio's Tablet Dark style, read from its .vsf file as CarbonPalette()'s were + (plans/1.5/experiments/style_palette.dpr), with the values its elements don't give taken from the VCL GUI + runner under it; the selection and the colors that say how a test did are Carbon's, as the VCL GUI + runner's are under every style. +*/ +TWin32GUIPalette TabletDarkPalette() +{ + TWin32GUIPalette palette = CarbonPalette(); + + palette.ButtonBorder = RGB(0x3D, 0x4A, 0x79); + palette.ButtonDefaultFrame = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonDisabledBorder = RGB(0x30, 0x33, 0x4B); + palette.ButtonDisabledFaceBottom = RGB(0x30, 0x33, 0x4B); + palette.ButtonDisabledFaceTop = RGB(0x30, 0x33, 0x4B); + palette.ButtonDisabledOutline = RGB(0x30, 0x33, 0x4B); + palette.ButtonFaceBottom = RGB(0x3D, 0x4A, 0x79); + palette.ButtonFaceTop = RGB(0x3D, 0x4A, 0x79); + palette.ButtonHotBorder = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonHotFaceBottom = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonHotFaceTop = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonHotText = RGB(0x00, 0x00, 0x00); + palette.ButtonOutline = RGB(0x3D, 0x4A, 0x79); + palette.ButtonPressedFaceBottom = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonPressedFaceTop = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonText = RGB(0xFF, 0xFF, 0xFF); + palette.CheckBoxBorder = RGB(0x6B, 0x73, 0x94); + palette.CheckBoxFill = RGB(0x2F, 0x36, 0x55); + palette.CheckBoxHotBorder = RGB(0x1A, 0xBC, 0x9C); + palette.CheckMark = RGB(0xE6, 0xE6, 0xE6); + palette.CheckMarkDisabled = RGB(0x6D, 0x74, 0x8B); + palette.ControlText = RGB(0xFF, 0xFF, 0xFF); + palette.DisabledText = RGB(0x80, 0x80, 0x80); + palette.EditBorderInner = RGB(0x0F, 0x0F, 0x11); + palette.EditBorderOuter = RGB(0x2B, 0x29, 0x37); + palette.GripDot = RGB(0x6B, 0x6B, 0x6B); + palette.HeaderBottom = RGB(0x2F, 0x2D, 0x3C); + palette.HeaderDivider = RGB(0x2F, 0x2D, 0x3C); + palette.HeaderHot = RGB(0x36, 0x34, 0x43); + palette.HeaderTop = RGB(0x2F, 0x2D, 0x3C); + palette.Highlight = RGB(0x1A, 0xBC, 0x9C); + palette.HighlightFrame = RGB(0x1A, 0xBC, 0x9C); + palette.HotText = RGB(0x00, 0x00, 0x00); + palette.MenuBackground = RGB(0x39, 0x42, 0x64); + palette.MenuBarDisabledText = RGB(0x80, 0x80, 0x80); + palette.MenuBorderInner = RGB(0x39, 0x42, 0x64); + palette.MenuBorderOuter = RGB(0x00, 0x00, 0x00); + palette.MenuDisabledText = RGB(0x80, 0x80, 0x80); + palette.MenuSeparator = RGB(0x1F, 0x1D, 0x28); + palette.Panel = RGB(0x2B, 0x29, 0x37); + palette.ProgressBorder = RGB(0x1A, 0x18, 0x22); + palette.ProgressChunkBottom = RGB(0x1A, 0xBC, 0x9C); + palette.ProgressChunkTop = RGB(0x1A, 0xBC, 0x9C); + palette.ProgressTrack = RGB(0x1A, 0x18, 0x22); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); + palette.SpinArrow = RGB(0x30, 0x2F, 0x3D); + palette.SpinDisabledArrow = RGB(0x2E, 0x2C, 0x3A); + palette.SpinDisabledFaceBottom = RGB(0x2B, 0x29, 0x37); + palette.SpinDisabledFaceTop = RGB(0x2B, 0x29, 0x37); + palette.SpinDisabledOutline = RGB(0x2B, 0x29, 0x37); + palette.SpinFaceBottom = RGB(0x2B, 0x29, 0x37); + palette.SpinFaceTop = RGB(0x2B, 0x29, 0x37); + palette.SpinHotFaceBottom = RGB(0x1D, 0x9F, 0x88); + palette.SpinHotFaceTop = RGB(0x27, 0x4E, 0x50); + palette.SpinOutline = RGB(0x2B, 0x29, 0x37); + palette.SpinPressedEdge = RGB(0x27, 0x4E, 0x50); + palette.StatusLine = RGB(0x38, 0x37, 0x46); + palette.TabActiveBottom = RGB(0x39, 0x42, 0x64); + palette.TabActiveTop = RGB(0x39, 0x42, 0x64); + palette.TabBorder = RGB(0x39, 0x42, 0x64); + palette.TabHot = RGB(0x2B, 0x29, 0x37); + palette.TabInactive = RGB(0x2B, 0x29, 0x37); + palette.TabInactiveBorder = RGB(0x49, 0x4A, 0x5A); + palette.TabOuter = RGB(0x3D, 0x4A, 0x79); + palette.TooltipBackground = RGB(0x39, 0x42, 0x64); + palette.TooltipText = RGB(0xFF, 0xFF, 0xFF); + palette.Window = RGB(0x39, 0x42, 0x64); + palette.WindowText = RGB(0xFF, 0xFF, 0xFF); + + // Lighter than FailureTextColorOn()'s red, which is too dark to read on the indigo window. + palette.FailureText = RGB(255, 150, 150); + palette.SkipText = SkipTextColorOn(palette.Window); + palette.TreeLine = BlendColors(palette.Window, palette.WindowText, 2, 5); + return palette; +} +//--------------------------------------------------------------------------- /* TintedCarbonPalette diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h index 19484ea..54fa5f8 100644 --- a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h @@ -52,7 +52,7 @@ struct TWin32GUIPalette { // Push buttons: the outline around them, the border, and the face's gradient from top to bottom, normal, // hot (under the mouse), pressed and disabled; the default button's (and the focused one's) line inside - // its border. + // its border; the text, and a hot or pressed button's. COLORREF ButtonBorder; COLORREF ButtonDefaultFrame; COLORREF ButtonDisabledBorder; @@ -64,9 +64,11 @@ struct TWin32GUIPalette COLORREF ButtonHotBorder; COLORREF ButtonHotFaceBottom; COLORREF ButtonHotFaceTop; + COLORREF ButtonHotText; COLORREF ButtonOutline; COLORREF ButtonPressedFaceBottom; COLORREF ButtonPressedFaceTop; + COLORREF ButtonText; // Check boxes (the test tree's too) and radio buttons: the box's or circle's border, its inside, and the // mark (a cross, the partial state's square, the radio button's dot); the border under the mouse, and the // mark of a disabled one. @@ -75,8 +77,8 @@ struct TWin32GUIPalette COLORREF CheckBoxHotBorder; COLORREF CheckMark; COLORREF CheckMarkDisabled; - // The text of labels, buttons, radio buttons and check boxes, menus, the inactive tab, the list's header - // and the status bar. + // The text of labels, radio buttons and check boxes, menus, the inactive tab, the list's header and the + // status bar. COLORREF ControlText; // Disabled text, but the menus' (see MenuBarDisabledText and MenuDisabledText). COLORREF DisabledText; @@ -95,7 +97,7 @@ struct TWin32GUIPalette // The menu item under the mouse or the keyboard, and its frame. COLORREF Highlight; COLORREF HighlightFrame; - // Text over Highlight or Selection, and a hot or pressed button's. + // Text over Highlight. COLORREF HotText; // A disabled item's text, in the menu bar and in a menu. COLORREF MenuBarDisabledText; @@ -115,9 +117,10 @@ struct TWin32GUIPalette COLORREF ProgressFailedBottom; COLORREF ProgressFailedTop; COLORREF ProgressTrack; - // The selected node or row in the tree and the list, and its frame. + // The selected node or row in the tree and the list, its frame, and its text. COLORREF Selection; COLORREF SelectionFrame; + COLORREF SelectionText; COLORREF SkipText; // Spin buttons, such as the results tabs' scroll arrows when the tabs don't fit: the outline around each, its // face's gradient from top to bottom, normal, hot and disabled, the edge a pressed one shows at its top and @@ -199,9 +202,10 @@ struct TWin32GUIBuiltInTheme // 'from' at 0, 'to' at 'denominator', e.g. one row of a gradient. 'from' if 'denominator' isn't positive; the // proportion is kept within 0 and 1. COLORREF BlendColors(COLORREF from, COLORREF to, int numerator, int denominator); -// The dark themes built in: Carbon (GUIDefaultDarkThemeName(), CarbonPalette()) first, then Carbon tinted (see -// TintedCarbonPalette()): Ember (warm brown), Forest (green), Midnight (blue) and Plum (purple). Their names' -// first letters differ, for the menu's accelerators, from each other's and from the menu's other items'. +// The dark themes built in: Carbon (GUIDefaultDarkThemeName(), CarbonPalette()) first, then by name the +// others: RAD Studio's dark VCL styles the VCL GUI runner has too (GUIVCLStyleDarkThemeNames(), e.g. +// OnyxBluePalette()), and Carbon tinted (see TintedCarbonPalette()): Ember (warm brown), Forest (green), Midnight +// (blue) and Plum (purple). std::vector const& BuiltInDarkThemes(); // The colors of RAD Studio's Carbon style, the VCL GUI runner's dark theme, read from its .vsf file and from // the VCL GUI runner as it draws under it. @@ -210,6 +214,10 @@ TWin32GUIPalette CarbonPalette(); // comments saying what the file is and what each group of roles colors, and CRLF line breaks: the template // View > Theme > Dark Theme > Save Theme Template writes. std::string CarbonThemeFileText(); +// The colors of RAD Studio's Charcoal Dark Slate VCL style, as CarbonPalette() has Carbon's. +TWin32GUIPalette CharcoalDarkSlatePalette(); +// The colors of RAD Studio's Cobalt XEMedia VCL style, as CarbonPalette() has Carbon's. +TWin32GUIPalette CobaltXEMediaPalette(); // The color of a failed test's text on 'background': RGB(200, 0, 0), or RGB(255, 100, 100) on a dark // background (see IsDarkColor()), as the VCL GUI runner's. COLORREF FailureTextColorOn(COLORREF background); @@ -219,6 +227,8 @@ TWin32GUIBuiltInTheme const* FindBuiltInDarkTheme(std::string const& name); // Whether 'color' is dark, by its luma (Rec. 601 weights) being below 128, as the VCL GUI runner decides // whether a background needs lighter text colors. bool IsDarkColor(COLORREF color); +// The colors of RAD Studio's Onyx Blue VCL style, as CarbonPalette() has Carbon's. +TWin32GUIPalette OnyxBluePalette(); // The names of PaletteRoles(), in their order, for ReadGUIThemeFile(). std::vector PaletteRoleNames(); // Every member of TWin32GUIPalette, once, in the struct's order, with its name and group. @@ -229,6 +239,8 @@ TWin32GUIPalette PaletteWithThemeColors(TWin32GUIPalette palette, std::vector(CDIS_SELECTED | CDIS_HOT | CDIS_FOCUS); - draw.clrText = selected ? m_Palette.HotText : m_Palette.WindowText; + draw.clrText = selected ? m_Palette.SelectionText : m_Palette.WindowText; draw.clrTextBk = selected ? m_Palette.Selection : m_Palette.Window; return selected ? CDRF_NOTIFYPOSTPAINT : CDRF_DODEFAULT; diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp index 97c9977..c5e66d6 100644 --- a/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp @@ -29,6 +29,7 @@ limitations under the License. #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ limitations under the License. #include "ASWUnitTests_Registry.h" //--------------------------------------------------------------------------- #include "ASWUnitTests_GUI_Theme.h" +#include "ASWUnitTests_GUI_View.h" #include "ASWUnitTests_Win32GUI_Palette.h" //--------------------------------------------------------------------------- @@ -166,21 +168,36 @@ void TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_AreCarbonThenIt std::vector const& themes = BuiltInDarkThemes(); // Assert + std::vector nameList; std::string names; - // The menu's other items' accelerators: Save Theme Template and Open Themes Folder. - std::set letters = { 's', 'o' }; - bool lettersDiffer = true; for (TWin32GUIBuiltInTheme const& theme : themes) { + nameList.push_back(theme.Name); names += theme.Name + "\n"; - lettersDiffer = lettersDiffer && !theme.Name.empty() && - letters.insert(static_cast(std::tolower(static_cast(theme.Name[0])))).second; } - CheckEquals(std::string("Carbon\nEmber\nForest\nMidnight\nPlum\n"), names, __func__, __LINE__, - "Carbon first, then the tints by name"); - CheckTrue(lettersDiffer, __func__, __LINE__, "each with a first letter of its own, for its accelerator"); + CheckEquals(std::string("Carbon\nCharcoal Dark Slate\nCobalt XEMedia\nEmber\nForest\nMidnight\nOnyx Blue\nPlum\n" + "Tablet Dark\n"), names, __func__, __LINE__, "Carbon first, then the VCL styles and the tints by name"); + + for (std::string const& style : GUIVCLStyleDarkThemeNames()) + { + CheckTrue(std::find(nameList.begin(), nameList.end(), style) != nameList.end(), __func__, __LINE__, + "the VCL GUI runner's built-in style " + style + ", by its name"); + } + + // The menu's other items' accelerators: Save Theme Template's and Open Themes Folder's. + std::set letters = { 's', 'o' }; + bool lettersDiffer = true; + + for (std::string const& caption : GUIMenuItemTextsWithAccelerators(nameList, "so")) + { + std::size_t const accelerator = caption.find('&'); + lettersDiffer = lettersDiffer && accelerator != std::string::npos && accelerator + 1 < caption.size() && + letters.insert(static_cast(std::tolower(static_cast(caption[accelerator + 1])))).second; + } + + CheckTrue(lettersDiffer, __func__, __LINE__, "each with an accelerator of its own in the menu"); AssertFalse(themes.empty(), __func__, __LINE__, "Carbon is there"); TWin32GUIPalette const carbon = CarbonPalette(); bool sameAsCarbon = true; @@ -194,23 +211,30 @@ void TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_AreCarbonThenIt //--------------------------------------------------------------------------- void TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_KeepTextReadable() { - // Arrange: each text color the GUI draws on a background, with that background. + // Arrange: each text color the GUI draws on a background, with that background, a gradient's by its middle, + // where the text is. struct TPair { COLORREF TWin32GUIPalette::* Text; COLORREF TWin32GUIPalette::* Background; + COLORREF TWin32GUIPalette::* BackgroundBottom; char const* Name; }; TPair const pairs[] = { - { &TWin32GUIPalette::WindowText, &TWin32GUIPalette::Window, "WindowText on Window" }, - { &TWin32GUIPalette::ControlText, &TWin32GUIPalette::Panel, "ControlText on Panel" }, - { &TWin32GUIPalette::ControlText, &TWin32GUIPalette::MenuBackground, "ControlText on MenuBackground" }, - { &TWin32GUIPalette::HotText, &TWin32GUIPalette::Highlight, "HotText on Highlight" }, - { &TWin32GUIPalette::HotText, &TWin32GUIPalette::Selection, "HotText on Selection" }, - { &TWin32GUIPalette::FailureText, &TWin32GUIPalette::Window, "FailureText on Window" }, - { &TWin32GUIPalette::SkipText, &TWin32GUIPalette::Window, "SkipText on Window" }, - { &TWin32GUIPalette::TooltipText, &TWin32GUIPalette::TooltipBackground, "TooltipText on TooltipBackground" }, + { &TWin32GUIPalette::WindowText, &TWin32GUIPalette::Window, nullptr, "WindowText on Window" }, + { &TWin32GUIPalette::ControlText, &TWin32GUIPalette::Panel, nullptr, "ControlText on Panel" }, + { &TWin32GUIPalette::ControlText, &TWin32GUIPalette::MenuBackground, nullptr, "ControlText on MenuBackground" }, + { &TWin32GUIPalette::ButtonText, &TWin32GUIPalette::ButtonFaceTop, &TWin32GUIPalette::ButtonFaceBottom, + "ButtonText on the button's face" }, + { &TWin32GUIPalette::ButtonHotText, &TWin32GUIPalette::ButtonHotFaceTop, &TWin32GUIPalette::ButtonHotFaceBottom, + "ButtonHotText on the hot button's face" }, + { &TWin32GUIPalette::HotText, &TWin32GUIPalette::Highlight, nullptr, "HotText on Highlight" }, + { &TWin32GUIPalette::SelectionText, &TWin32GUIPalette::Selection, nullptr, "SelectionText on Selection" }, + { &TWin32GUIPalette::FailureText, &TWin32GUIPalette::Window, nullptr, "FailureText on Window" }, + { &TWin32GUIPalette::SkipText, &TWin32GUIPalette::Window, nullptr, "SkipText on Window" }, + { &TWin32GUIPalette::TooltipText, &TWin32GUIPalette::TooltipBackground, nullptr, + "TooltipText on TooltipBackground" }, }; // Act @@ -220,7 +244,10 @@ void TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_KeepTextReadabl { for (TPair const& pair : pairs) { - if (ContrastRatio(theme.Palette.*pair.Text, theme.Palette.*pair.Background) < 4.5) + COLORREF const background = pair.BackgroundBottom == nullptr ? theme.Palette.*pair.Background : + BlendColors(theme.Palette.*pair.Background, theme.Palette.*pair.BackgroundBottom, 1, 2); + + if (ContrastRatio(theme.Palette.*pair.Text, background) < 4.5) unreadable += theme.Name + ": " + pair.Name + "\n"; } } From 9522608aa5d5247a4d9d176f91e5530b403cc0e7 Mon Sep 17 00:00:00 2001 From: Anthony West Date: Fri, 30 Oct 2026 00:02:14 -0700 Subject: [PATCH 3/3] Apply the VCL GUI's theme after the menu click that chose it, not during it --- vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp | 26 +++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp b/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp index acb354c..a4b4b85 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp +++ b/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp @@ -689,20 +689,28 @@ void TFormASWUnitTestsGUIMain::ApplyPanelSizes(TGUILayout const& layout) /* TFormASWUnitTestsGUIMain::ApplyTheme - Not during a run, since restyling recreates the windows the run is updating; SetRunning() calls it again - once the run has ended. The window is restyled once it handles the message the style change posts; see - FormWindowProc(). + Queued rather than at once: under a VCL style, a menu item's click comes from the form's style hook, which a + style change frees, and which uses itself again once the click returns (an access violation in + TFormStyleHook::TMainMenuBarStyleHook::Invalidate(), switching dark themes with the mouse). Not during a run, + since restyling recreates the windows the run is updating; SetRunning() calls it again once the run has + ended. The window is restyled once it handles the message the style change posts; see FormWindowProc(). */ void TFormASWUnitTestsGUIMain::ApplyTheme() { - if (m_Running) + TThread::ForceQueue(nullptr, [this]() { - m_ThemeChangePending = true; - return; - } + RunGuarded("changing the theme", [this]() + { + if (m_Running) + { + m_ThemeChangePending = true; + return; + } - m_ThemeChangePending = false; - ApplyGUITheme(m_Theme, m_DarkStyle); + m_ThemeChangePending = false; + ApplyGUITheme(m_Theme, m_DarkStyle); + }); + }); } //--------------------------------------------------------------------------- /*