From 00d59956fd1a35351ca3265627ebc47441a60aec Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Wed, 7 Oct 2026 17:39:26 -0500 Subject: [PATCH 1/3] Start new clocks with neutral colors instead of the accent A fresh install, and the System preset, used the Windows accent color for the clock text. Windows allows any shade, so a dark accent in dark mode, like plum on the dark background, or a light accent like yellow in light mode left the text almost invisible. The text is now near-white in dark mode and near-black in light mode, so it's readable with any accent. Existing clocks keep their colors. --- DesktopClock.Tests/ClockThemeTests.cs | 26 ++++++++++++++++++++ DesktopClock/ClockTheme.cs | 2 +- DesktopClock/Utilities/SystemThemeService.cs | 7 ++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/DesktopClock.Tests/ClockThemeTests.cs b/DesktopClock.Tests/ClockThemeTests.cs index 0484365..5e28d2d 100644 --- a/DesktopClock.Tests/ClockThemeTests.cs +++ b/DesktopClock.Tests/ClockThemeTests.cs @@ -82,4 +82,30 @@ public void Apply_SetsAppearanceFromTheme() Assert.Equal(1.5, settings.OutlineThickness); Assert.Equal(1, settings.TextOpacity); } + + [Fact] + public void SystemTheme_IsReadableWithAnyAccentColor() + { + // The first preset is the look a fresh install starts with; it follows light or dark mode but not the accent, which can be any shade. + var system = ClockTheme.GetBuiltInThemes().First(); + + Assert.True(Contrast(system.TextColor, system.OuterColor) >= 7, $"{system.TextColor} on {system.OuterColor}"); + } + + /// + /// The WCAG contrast ratio between two colors, from 1 (same) to 21 (black and white). + /// + private static double Contrast(Color a, Color b) + { + static double Channel(byte value) + { + var c = value / 255.0; + return c <= 0.03928 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); + } + + static double Luminance(Color c) => (0.2126 * Channel(c.R)) + (0.7152 * Channel(c.G)) + (0.0722 * Channel(c.B)); + + var (light, dark) = (Math.Max(Luminance(a), Luminance(b)), Math.Min(Luminance(a), Luminance(b))); + return (light + 0.05) / (dark + 0.05); + } } diff --git a/DesktopClock/ClockTheme.cs b/DesktopClock/ClockTheme.cs index 5a09b61..a687d96 100644 --- a/DesktopClock/ClockTheme.cs +++ b/DesktopClock/ClockTheme.cs @@ -90,7 +90,7 @@ public void Apply(Settings settings) } /// - /// The factory look: accent-colored text seeded from the Windows theme, like a fresh install. + /// The factory look: neutral text matching the Windows light or dark theme, like a fresh install. /// private static ClockTheme CreateSystemTheme() { diff --git a/DesktopClock/Utilities/SystemThemeService.cs b/DesktopClock/Utilities/SystemThemeService.cs index 69f74f2..116d345 100644 --- a/DesktopClock/Utilities/SystemThemeService.cs +++ b/DesktopClock/Utilities/SystemThemeService.cs @@ -12,7 +12,9 @@ public static class SystemThemeService { // Fallbacks match the app's existing default colors. private static readonly Color DefaultAccentColor = Color.FromRgb(0, 120, 215); + private static readonly Color LightThemeTextColor = Color.FromRgb(33, 33, 33); private static readonly Color LightThemeOuterColor = Color.FromRgb(247, 247, 247); + private static readonly Color DarkThemeTextColor = Color.FromRgb(242, 242, 242); private static readonly Color DarkThemeOuterColor = Color.FromRgb(32, 32, 32); // Theme and colorization values stored under HKCU. @@ -23,7 +25,7 @@ public static class SystemThemeService private const string ColorizationColorValueName = "ColorizationColor"; /// - /// Tries to build default text/outer colors from the system theme and accent color. + /// Tries to build default text/outer colors that match the system's light or dark theme. /// /// /// Returns false when the system theme cannot be read (older Windows or missing keys). @@ -36,7 +38,8 @@ public static bool TryGetThemeDefaults(out Color textColor, out Color outerColor if (!TryGetSystemThemeIsLight(out var isLightTheme)) return false; - textColor = GetSystemAccentColor(); + // The text doesn't use the accent color, since Windows allows any shade and a dark accent in dark mode or a light one in light mode can't be read. + textColor = isLightTheme ? LightThemeTextColor : DarkThemeTextColor; outerColor = isLightTheme ? LightThemeOuterColor : DarkThemeOuterColor; return true; } From 31566dceb00ccdd50e972f1c673eedf638ec5118 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Wed, 7 Oct 2026 17:40:26 -0500 Subject: [PATCH 2/3] Keep the Accent preset readable with light accents The Accent preset always put white text on the accent color, which was hard to read with light accents like yellow or light green. It now picks white or near-black text, whichever stands out more against the accent. Darker accents, including the default Windows blue, keep white text. --- DesktopClock.Tests/ClockThemeTests.cs | 43 +++++++++++++++++++-------- DesktopClock/ClockTheme.cs | 32 +++++++++++++++++++- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/DesktopClock.Tests/ClockThemeTests.cs b/DesktopClock.Tests/ClockThemeTests.cs index 5e28d2d..1b00739 100644 --- a/DesktopClock.Tests/ClockThemeTests.cs +++ b/DesktopClock.Tests/ClockThemeTests.cs @@ -89,23 +89,40 @@ public void SystemTheme_IsReadableWithAnyAccentColor() // The first preset is the look a fresh install starts with; it follows light or dark mode but not the accent, which can be any shade. var system = ClockTheme.GetBuiltInThemes().First(); - Assert.True(Contrast(system.TextColor, system.OuterColor) >= 7, $"{system.TextColor} on {system.OuterColor}"); + Assert.True(ClockTheme.Contrast(system.TextColor, system.OuterColor) >= 7, $"{system.TextColor} on {system.OuterColor}"); } - /// - /// The WCAG contrast ratio between two colors, from 1 (same) to 21 (black and white). - /// - private static double Contrast(Color a, Color b) + [Theory] + [InlineData("#3B003B", "#FFFFFF")] // Plum + [InlineData("#0078D4", "#FFFFFF")] // Default Windows blue keeps white text. + [InlineData("#D50056", "#FFFFFF")] // Dark pink + [InlineData("#FFB900", "#1A1A1A")] // Gold + [InlineData("#00CC6A", "#1A1A1A")] // Light green + [InlineData("#FFFFFF", "#1A1A1A")] + [InlineData("#000000", "#FFFFFF")] + public void ReadableTextOn_PicksWhiteOrDarkByContrast(string background, string expected) { - static double Channel(byte value) - { - var c = value / 255.0; - return c <= 0.03928 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); - } + var text = ClockTheme.ReadableTextOn((Color)ColorConverter.ConvertFromString(background)); + + Assert.Equal((Color)ColorConverter.ConvertFromString(expected), text); + } + + [Fact] + public void AccentTheme_IsReadableWithAnyAccentColor() + { + var accent = ClockTheme.GetBuiltInThemes()[1]; - static double Luminance(Color c) => (0.2126 * Channel(c.R)) + (0.7152 * Channel(c.G)) + (0.0722 * Channel(c.B)); + // Whichever of white or near-black reads better is at least about 4.2:1 on any background. + Assert.True(ClockTheme.Contrast(accent.TextColor, accent.OuterColor) >= 4, $"{accent.TextColor} on {accent.OuterColor}"); + } - var (light, dark) = (Math.Max(Luminance(a), Luminance(b)), Math.Min(Luminance(a), Luminance(b))); - return (light + 0.05) / (dark + 0.05); + [Fact] + public void ReadableTextOn_IsAtLeastFourToOneOnEveryGray() + { + for (var v = 0; v <= 255; v++) + { + var background = Color.FromRgb((byte)v, (byte)v, (byte)v); + Assert.True(ClockTheme.Contrast(ClockTheme.ReadableTextOn(background), background) >= 4, $"gray {v}"); + } } } diff --git a/DesktopClock/ClockTheme.cs b/DesktopClock/ClockTheme.cs index a687d96..e0d0f8e 100644 --- a/DesktopClock/ClockTheme.cs +++ b/DesktopClock/ClockTheme.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Windows.Media; using DesktopClock.Properties; @@ -42,11 +43,13 @@ public ClockTheme(string name, string fontFamily, string fontWeight, Color textC /// public static IReadOnlyList GetBuiltInThemes() { + var accent = SystemThemeService.GetSystemAccentColor(); + return new[] { CreateSystemTheme(), new ClockTheme(Loc.Get("ThemeAccent"), "Segoe UI", "SemiBold", - Color.FromRgb(0xFF, 0xFF, 0xFF), SystemThemeService.GetSystemAccentColor(), + ReadableTextOn(accent), accent, backgroundEnabled: true, backgroundOpacity: 1, backgroundCornerRadius: 1, outlineThickness: 0.2), new ClockTheme(Loc.Get("ThemeSmoke"), "Segoe UI", "Normal", Color.FromRgb(0xF2, 0xF2, 0xF2), Color.FromRgb(0x0A, 0x0A, 0x10), @@ -69,6 +72,33 @@ public static IReadOnlyList GetBuiltInThemes() }; } + /// + /// White or near-black text, whichever stands out more on the given background, so a light accent like yellow still gets readable text. + /// + public static Color ReadableTextOn(Color background) + { + var white = Color.FromRgb(0xFF, 0xFF, 0xFF); + var black = Color.FromRgb(0x1A, 0x1A, 0x1A); + return Contrast(white, background) >= Contrast(black, background) ? white : black; + } + + /// + /// The WCAG contrast ratio between two colors, from 1 for the same color to 21 for black and white. + /// + public static double Contrast(Color a, Color b) + { + static double Channel(byte value) + { + var c = value / 255.0; + return c <= 0.03928 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); + } + + static double Luminance(Color c) => (0.2126 * Channel(c.R)) + (0.7152 * Channel(c.G)) + (0.0722 * Channel(c.B)); + + var (lighter, darker) = (Math.Max(Luminance(a), Luminance(b)), Math.Min(Luminance(a), Luminance(b))); + return (lighter + 0.05) / (darker + 0.05); + } + /// /// Applies this look to the given settings; the clock updates immediately. /// From ecb4a389d155767226340a8f772f7ef5d022ab55 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Wed, 7 Oct 2026 17:49:25 -0500 Subject: [PATCH 3/3] Test the theme colors without depending on the PC's theme The readability tests read this PC's own accent and light or dark mode, so they only covered one case and wouldn't catch a regression elsewhere. The default colors and the Accent preset now come from small functions that take the mode or accent, and the tests check both modes and both a dark and a light accent directly. --- DesktopClock.Tests/ClockThemeTests.cs | 25 ++++++++++++-------- DesktopClock/ClockTheme.cs | 13 ++++++---- DesktopClock/Utilities/SystemThemeService.cs | 13 +++++++--- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/DesktopClock.Tests/ClockThemeTests.cs b/DesktopClock.Tests/ClockThemeTests.cs index 1b00739..4a85d13 100644 --- a/DesktopClock.Tests/ClockThemeTests.cs +++ b/DesktopClock.Tests/ClockThemeTests.cs @@ -3,6 +3,7 @@ using System.Windows; using System.Windows.Media; using DesktopClock.Properties; +using DesktopClock.Utilities; namespace DesktopClock.Tests; @@ -83,13 +84,15 @@ public void Apply_SetsAppearanceFromTheme() Assert.Equal(1, settings.TextOpacity); } - [Fact] - public void SystemTheme_IsReadableWithAnyAccentColor() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ThemeDefaults_AreReadableInLightAndDarkMode(bool isLightTheme) { - // The first preset is the look a fresh install starts with; it follows light or dark mode but not the accent, which can be any shade. - var system = ClockTheme.GetBuiltInThemes().First(); + // A fresh install and the System preset follow light or dark mode but not the accent, which can be any shade. + var (text, outer) = SystemThemeService.GetThemeDefaults(isLightTheme); - Assert.True(ClockTheme.Contrast(system.TextColor, system.OuterColor) >= 7, $"{system.TextColor} on {system.OuterColor}"); + Assert.True(ClockTheme.Contrast(text, outer) >= 7, $"{text} on {outer}"); } [Theory] @@ -107,13 +110,15 @@ public void ReadableTextOn_PicksWhiteOrDarkByContrast(string background, string Assert.Equal((Color)ColorConverter.ConvertFromString(expected), text); } - [Fact] - public void AccentTheme_IsReadableWithAnyAccentColor() + [Theory] + [InlineData("#3B003B", "#FFFFFF")] // Plum keeps white text. + [InlineData("#FFB900", "#1A1A1A")] // Gold gets dark text. + public void AccentTheme_PicksReadableTextForTheAccent(string accent, string expectedText) { - var accent = ClockTheme.GetBuiltInThemes()[1]; + var theme = ClockTheme.CreateAccentTheme((Color)ColorConverter.ConvertFromString(accent)); - // Whichever of white or near-black reads better is at least about 4.2:1 on any background. - Assert.True(ClockTheme.Contrast(accent.TextColor, accent.OuterColor) >= 4, $"{accent.TextColor} on {accent.OuterColor}"); + Assert.Equal((Color)ColorConverter.ConvertFromString(accent), theme.OuterColor); + Assert.Equal((Color)ColorConverter.ConvertFromString(expectedText), theme.TextColor); } [Fact] diff --git a/DesktopClock/ClockTheme.cs b/DesktopClock/ClockTheme.cs index e0d0f8e..f48e47b 100644 --- a/DesktopClock/ClockTheme.cs +++ b/DesktopClock/ClockTheme.cs @@ -43,14 +43,10 @@ public ClockTheme(string name, string fontFamily, string fontWeight, Color textC /// public static IReadOnlyList GetBuiltInThemes() { - var accent = SystemThemeService.GetSystemAccentColor(); - return new[] { CreateSystemTheme(), - new ClockTheme(Loc.Get("ThemeAccent"), "Segoe UI", "SemiBold", - ReadableTextOn(accent), accent, - backgroundEnabled: true, backgroundOpacity: 1, backgroundCornerRadius: 1, outlineThickness: 0.2), + CreateAccentTheme(SystemThemeService.GetSystemAccentColor()), new ClockTheme(Loc.Get("ThemeSmoke"), "Segoe UI", "Normal", Color.FromRgb(0xF2, 0xF2, 0xF2), Color.FromRgb(0x0A, 0x0A, 0x10), backgroundEnabled: true, backgroundOpacity: 0.55, backgroundCornerRadius: 1, outlineThickness: 0.2), @@ -72,6 +68,13 @@ public static IReadOnlyList GetBuiltInThemes() }; } + /// + /// The Accent look: the Windows accent as the background, with whichever text color reads best on it. + /// + public static ClockTheme CreateAccentTheme(Color accent) => + new(Loc.Get("ThemeAccent"), "Segoe UI", "SemiBold", ReadableTextOn(accent), accent, + backgroundEnabled: true, backgroundOpacity: 1, backgroundCornerRadius: 1, outlineThickness: 0.2); + /// /// White or near-black text, whichever stands out more on the given background, so a light accent like yellow still gets readable text. /// diff --git a/DesktopClock/Utilities/SystemThemeService.cs b/DesktopClock/Utilities/SystemThemeService.cs index 116d345..65e300f 100644 --- a/DesktopClock/Utilities/SystemThemeService.cs +++ b/DesktopClock/Utilities/SystemThemeService.cs @@ -38,12 +38,19 @@ public static bool TryGetThemeDefaults(out Color textColor, out Color outerColor if (!TryGetSystemThemeIsLight(out var isLightTheme)) return false; - // The text doesn't use the accent color, since Windows allows any shade and a dark accent in dark mode or a light one in light mode can't be read. - textColor = isLightTheme ? LightThemeTextColor : DarkThemeTextColor; - outerColor = isLightTheme ? LightThemeOuterColor : DarkThemeOuterColor; + (textColor, outerColor) = GetThemeDefaults(isLightTheme); return true; } + /// + /// The default text and background colors for the light or dark theme. + /// + /// + /// The text doesn't use the accent color, since Windows allows any shade and a dark accent in dark mode or a light one in light mode can't be read. + /// + public static (Color Text, Color Outer) GetThemeDefaults(bool isLightTheme) => + isLightTheme ? (LightThemeTextColor, LightThemeOuterColor) : (DarkThemeTextColor, DarkThemeOuterColor); + /// /// Whether Windows is using the light app theme; defaults to light when it can't be read. ///