147 lines
7.0 KiB
C#
147 lines
7.0 KiB
C#
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);
|
||
|
|
}
|
||
|
|
}
|