using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Threading; using SbLauncher; // Exercises real WPF animation clocks, including interruption and settling. // Synthetic view model only: no local settings, network, or sing-box process. public static class ThemeTransitions { [STAThread] public static int Main(string[] args) { var app = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown }; Window window = null; var vm = new MainVm(); try { string root = args.Length > 0 ? args[0] : AppDomain.CurrentDomain.BaseDirectory; window = (Window)XamlReader.Parse(File.ReadAllText(Path.Combine(root, "Ui.xaml"))); window.DataContext = vm; vm.ThemeApplier = delegate(string theme) { Theme.Apply(window, theme); }; vm.SetTheme(Theme.Dark); var dark = (ResourceDictionary)window.Resources["Theme.Dark"]; var light = (ResourceDictionary)window.Resources["Theme.Light"]; var sun = (TextBlock)window.FindName("ThemeSun"); var moon = (TextBlock)window.FindName("ThemeMoon"); Color darkText = ((SolidColorBrush)dark["Text"]).Color; Color lightText = ((SolidColorBrush)light["Text"]).Color; Require(SameColor(TextColor(window), darkText) && sun.Opacity == 1 && moon.Opacity == 0, "Startup is immediate and shows the correct icon"); vm.SetTheme(Theme.Light); Require(SameColor(TextColor(window), lightText) && sun.Opacity == 0 && moon.Opacity == 1, "Unloaded screenshot rendering is immediate in both themes"); vm.SetTheme(Theme.Dark); window.WindowStartupLocation = WindowStartupLocation.Manual; window.Left = -20000; window.Top = -20000; window.ShowInTaskbar = false; window.ShowActivated = false; window.Show(); PumpFor(100); vm.SetTheme(Theme.Light); Require(object.ReferenceEquals(window.Resources["Muted"], Application.Current.Resources["Muted"]), "Semantic colors and XAML share the transition brushes"); if (SystemParameters.ClientAreaAnimation) { Require(SameColor(TextColor(window), darkText), "No initial palette flash"); PumpFor(170); Color halfway = TextColor(window); Require(!SameColor(halfway, darkText) && !SameColor(halfway, lightText), "Text colors interpolate"); Require(sun.Opacity > 0 && sun.Opacity < 1 && moon.Opacity > 0 && moon.Opacity < 1, "Both icons participate in the crossfade"); var sunTransform = (TransformGroup)sun.RenderTransform; Require(((RotateTransform)sunTransform.Children[1]).Angle != 0 && ((ScaleTransform)sunTransform.Children[0]).ScaleX < 1, "Outgoing icon rotates and scales"); var background = (GradientBrush)window.Resources["Bg"]; Color middleStop = background.GradientStops[0].Color; double sunOpacity = sun.Opacity; vm.SetTheme(Theme.Dark); Require(SameColor(TextColor(window), halfway) && SameColor(((GradientBrush)window.Resources["Bg"]).GradientStops[0].Color, middleStop) && Math.Abs(sun.Opacity - sunOpacity) < 0.001, "Reversal continues from current colors and icon position"); } else Require(SameColor(TextColor(window), lightText) && moon.Opacity == 1, "Windows reduced animation preference is respected"); for (int i = 0; i < 12; i++) { vm.SetTheme(i % 2 == 0 ? Theme.Light : Theme.Dark); PumpFor(35); } vm.SetTheme(Theme.Light); PumpFor(650); Require(SameColor(TextColor(window), lightText) && sun.Opacity == 0 && moon.Opacity == 1, "Latest request wins after rapid switching"); Require(vm.ThemeAction == "Включить тёмную тему", "Accessible action matches the destination"); CheckPalette(window, light); vm.SetTheme(Theme.Dark); PumpFor(650); Require(SameColor(TextColor(window), darkText) && sun.Opacity == 1 && moon.Opacity == 0, "Reverse transition reaches its exact final state"); CheckPalette(window, dark); Require(((SolidColorBrush)dark["Text"]).Color == darkText && ((SolidColorBrush)light["Text"]).Color == lightText && !((SolidColorBrush)dark["Text"]).HasAnimatedProperties && !((SolidColorBrush)light["Text"]).HasAnimatedProperties, "Source palettes are unchanged"); Console.WriteLine("Theme transition checks passed."); return 0; } catch (Exception ex) { Console.Error.WriteLine(ex); return 1; } finally { vm.DisposeTimers(); if (window != null) window.Close(); app.Shutdown(); } } static Color TextColor(Window window) { return ((SolidColorBrush)window.Resources["Text"]).Color; } // ColorAnimation interpolates in scRGB; compare the rendered 8-bit channels // rather than exact floating-point equality with XAML's sRGB representation. static bool SameColor(Color a, Color b) { return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B; } static void CheckPalette(Window window, ResourceDictionary expected) { foreach (object key in expected.Keys) { var solid = expected[key] as SolidColorBrush; if (solid != null) Require(SameColor(((SolidColorBrush)window.Resources[key]).Color, solid.Color), "Final color: " + key); var gradient = expected[key] as GradientBrush; if (gradient != null) { var actual = (GradientBrush)window.Resources[key]; for (int i = 0; i < gradient.GradientStops.Count; i++) Require(SameColor(actual.GradientStops[i].Color, gradient.GradientStops[i].Color) && actual.GradientStops[i].Offset == gradient.GradientStops[i].Offset, "Final gradient: " + key + " / " + i); } } } static void PumpFor(int milliseconds) { var frame = new DispatcherFrame(); var timer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(milliseconds) }; timer.Tick += delegate { timer.Stop(); frame.Continue = false; }; timer.Start(); Dispatcher.PushFrame(frame); } static void Require(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); Console.WriteLine("PASS " + message); } }