From d56f8e1c0f75e7865c8259ec58528a812087959b Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 10 Nov 2026 15:43:39 +0100 Subject: [PATCH 1/5] Add a theme switcher to the sidebar and fix black text in dark mode The sidebar button cycles Windows, Light and Dark; the choice applies at once and is kept in window.json. The SidebarLink, ChoiceCard and Switch styles now set their own Foreground: replacing Fluent's implicit style let the classic theme's black text show through on dark backgrounds. Co-Authored-By: Claude Opus 5.5 --- src/OrthoStreamXP/Gui/App.xaml.cs | 1 + src/OrthoStreamXP/Gui/AppTheme.cs | 54 +++++++++++++++++++ src/OrthoStreamXP/Gui/MainWindow.xaml | 10 ++++ src/OrthoStreamXP/Gui/Theme.xaml | 8 +++ .../Gui/ViewModels/MainViewModel.cs | 23 ++++++++ tests/OrthoStreamXP.Tests/AppThemeTests.cs | 39 ++++++++++++++ 6 files changed, 135 insertions(+) create mode 100644 src/OrthoStreamXP/Gui/AppTheme.cs create mode 100644 tests/OrthoStreamXP.Tests/AppThemeTests.cs diff --git a/src/OrthoStreamXP/Gui/App.xaml.cs b/src/OrthoStreamXP/Gui/App.xaml.cs index c4e43ba..858f194 100644 --- a/src/OrthoStreamXP/Gui/App.xaml.cs +++ b/src/OrthoStreamXP/Gui/App.xaml.cs @@ -25,6 +25,7 @@ protected override void OnStartup(StartupEventArgs e) Log.Configure(LogOutput.None, null); DispatcherUnhandledException += OnUnhandledException; _main = new MainViewModel(); + _main.Preferences.Theme.Apply(); var window = new MainWindow(_main); MainWindow = window; window.Show(); diff --git a/src/OrthoStreamXP/Gui/AppTheme.cs b/src/OrthoStreamXP/Gui/AppTheme.cs new file mode 100644 index 0000000..b453f63 --- /dev/null +++ b/src/OrthoStreamXP/Gui/AppTheme.cs @@ -0,0 +1,54 @@ +using System.Windows; + +namespace OrthoStreamXP.Gui; + +/// The window's colours: the same as Windows, or always light or dark. +internal enum AppTheme +{ + System, + Light, + Dark, +} + +/// The sidebar's theme button: what it shows for a theme, and which theme a click switches to. +internal sealed record ThemeChoice(string Label, string Glyph, AppTheme Theme) +{ + /// The themes in the order a click goes through them. + public static IReadOnlyList All { get; } = + [ + new("Windows theme", "", AppTheme.System), + new("Light theme", "", AppTheme.Light), + new("Dark theme", "", AppTheme.Dark), + ]; + + public static ThemeChoice Of(AppTheme theme) => All.FirstOrDefault(c => c.Theme == theme) ?? All[0]; + + /// The theme after this one, back to the first after the last. + public ThemeChoice Next => All[(IndexOf(Theme) + 1) % All.Count]; + + private static int IndexOf(AppTheme theme) + { + for (int i = 0; i < All.Count; i++) + if (All[i].Theme == theme) return i; + return 0; + } +} + +internal static class AppThemeExtensions +{ +#pragma warning disable WPF0001 // WPF's Fluent theme is marked experimental. + /// The WPF Fluent theme mode for the choice. + public static ThemeMode ToThemeMode(this AppTheme theme) => theme switch + { + AppTheme.Light => ThemeMode.Light, + AppTheme.Dark => ThemeMode.Dark, + _ => ThemeMode.System, + }; + + /// Switches the running application to the theme; the window's DynamicResource brushes follow. + public static void Apply(this AppTheme theme) + { + if (Application.Current is { } app) app.ThemeMode = theme.ToThemeMode(); + } +#pragma warning restore WPF0001 +} diff --git a/src/OrthoStreamXP/Gui/MainWindow.xaml b/src/OrthoStreamXP/Gui/MainWindow.xaml index 419800c..8fe8c29 100644 --- a/src/OrthoStreamXP/Gui/MainWindow.xaml +++ b/src/OrthoStreamXP/Gui/MainWindow.xaml @@ -66,6 +66,16 @@ + + + diff --git a/src/OrthoStreamXP/Gui/Theme.xaml b/src/OrthoStreamXP/Gui/Theme.xaml index 876aee5..ae7db94 100644 --- a/src/OrthoStreamXP/Gui/Theme.xaml +++ b/src/OrthoStreamXP/Gui/Theme.xaml @@ -8,6 +8,11 @@ + +