using System; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml; public class ThemeDictionariesTests : XamlTestBase { public static ThemeVariant Custom { get; } = new(nameof(Custom), ThemeVariant.Light); [Fact] public void DynamicResource_Updated_When_Control_Theme_Changed() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black White "); var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); var themeVariantKey = new string(['D', 'a', 'r', 'k']); // Ensure that a non-interned string works themeVariantScope.RequestedThemeVariant = new ThemeVariant(themeVariantKey, null); Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void DynamicResource_Updated_When_Control_Theme_Changed_No_Xaml() { var themeVariantScope = new ThemeVariantScope { RequestedThemeVariant = ThemeVariant.Light, Resources = new ResourceDictionary { ThemeDictionaries = { [ThemeVariant.Dark] = new ResourceDictionary { ["DemoBackground"] = Brushes.Black }, [ThemeVariant.Light] = new ResourceDictionary { ["DemoBackground"] = Brushes.White } } }, Child = new Border() }; var border = (Border)themeVariantScope.Child!; border[!Border.BackgroundProperty] = new DynamicResourceExtension("DemoBackground"); DelayedBinding.ApplyBindings(border); Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void Intermediate_DynamicResource_Updated_When_Control_Theme_Changed() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black White "); var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void DynamicResource_In_ResourceProvider_Updated_When_Control_Theme_Changed() { var themeVariantScope = new ThemeVariantScope { RequestedThemeVariant = ThemeVariant.Light, Resources = new ResourceDictionary { ThemeDictionaries = { [ThemeVariant.Dark] = new ResourceDictionary { ["DemoBackground"] = Brushes.Black }, [ThemeVariant.Light] = new ResourceDictionary { ["DemoBackground"] = Brushes.White } } }, Child = new Border() }; var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(@" "); themeVariantScope.Resources.MergedDictionaries.Add(resources); var geo = (GeometryDrawing)themeVariantScope.FindResource("Geo"); Assert.NotNull(geo); Assert.Equal(Colors.White, ((ISolidColorBrush)geo.Brush)!.Color); themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)geo.Brush)!.Color); } [Fact] public void Intermediate_StaticResource_Can_Be_Reached_From_ThemeDictionaries() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black White "); var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black White "); var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key_From_Inner_File() { var documents = new[] { new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Inner.xaml"), @" "), new RuntimeXamlLoaderDocument(@" Green White ") }; var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); var dictionary = (ResourceDictionary)parsed[1]!; dictionary.TryGetResource("InnerKey", ThemeVariant.Dark, out var resource); var colorResource = Assert.IsType(resource); Assert.Equal(Colors.White, colorResource); dictionary.TryGetResource("InnerKey", ThemeVariant.Light, out resource); colorResource = Assert.IsType(resource); Assert.Equal(Colors.Green, colorResource); } [Fact] public void DynamicResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key_From_Inner_File() { var documents = new[] { new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Inner.xaml"), @" "), new RuntimeXamlLoaderDocument(@" Green White ") }; var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); var dictionary1 = (ResourceDictionary)parsed[0]!; var dictionary2 = (ResourceDictionary)parsed[1]!; var ownerApp = new Application(); // DynamicResource needs an owner to work ownerApp.RequestedThemeVariant = new ThemeVariant("FakeOne", null); ownerApp.Resources.MergedDictionaries.Add(dictionary1); ownerApp.Resources.MergedDictionaries.Add(dictionary2); dictionary2.TryGetResource("InnerKey", ThemeVariant.Dark, out var resource); var colorResource = Assert.IsAssignableFrom(resource); Assert.Equal(Colors.White, colorResource.Color); dictionary2.TryGetResource("InnerKey", ThemeVariant.Light, out resource); colorResource = Assert.IsAssignableFrom(resource); Assert.Equal(Colors.Green, colorResource.Color); } [Fact] public void DynamicResource_Inside_Control_Inside_Of_ThemeDictionaries_Should_Use_Control_Theme_Variant() { var documents = new[] { new RuntimeXamlLoaderDocument(@" Green White ") }; var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); var dictionary = (ResourceDictionary)parsed[0]!; dictionary.TryGetResource("Template", ThemeVariant.Dark, out var resource); var control = Assert.IsType((resource as Template)?.Build()); control.Resources.MergedDictionaries.Add(dictionary); Assert.Equal(Colors.Green, ((ISolidColorBrush)control[TextElement.ForegroundProperty]!).Color); control.Resources.MergedDictionaries.Remove(dictionary); dictionary.TryGetResource("Template", ThemeVariant.Light, out resource); control = Assert.IsType((resource as Template)?.Build()); control.Resources.MergedDictionaries.Add(dictionary); Assert.Equal(Colors.White, ((ISolidColorBrush)control[TextElement.ForegroundProperty]!).Color); } [Fact] public void StaticResource_Outside_Of_Dictionaries_Should_Use_Control_ThemeVariant() { using (AvaloniaLocator.EnterScope()) { var applicationThemeHost = new Mock(); applicationThemeHost.SetupGet(h => h.ActualThemeVariant).Returns(ThemeVariant.Dark); AvaloniaLocator.CurrentMutable.Bind().ToConstant(applicationThemeHost.Object); var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black White "); var border = (Border)themeVariantScope.Child!; themeVariantScope.RequestedThemeVariant = ThemeVariant.Light; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); } } [Fact] public void Inner_ThemeDictionaries_Works_Properly() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black White "); var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void Inner_Resource_Can_Reference_Parent_ThemeDictionaries() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black White "); var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void DynamicResource_Can_Access_Resources_Outside_Of_ThemeDictionaries() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black White "); var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void Inner_Dictionary_Does_Not_Affect_Parent_Resources() { // It might be a nice feature, but neither Avalonia nor UWP supports it. // Better to expect this limitation with a unit test. var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Red Black White "); var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.Red, ((ISolidColorBrush)border.Background)!.Color); themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Red, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void Custom_Theme_Can_Be_Defined_In_ThemeDictionaries() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black White Pink "); var border = (Border)themeVariantScope.Child!; themeVariantScope.RequestedThemeVariant = Custom; Assert.Equal(Colors.Pink, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void Custom_Theme_Fallbacks_To_Inherit_Theme_DynamicResource() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Black "); var border = (Border)themeVariantScope.Child!; themeVariantScope.RequestedThemeVariant = new ThemeVariant("Custom", ThemeVariant.Dark); Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void Custom_Theme_Fallbacks_To_Inherit_Theme_StaticResource() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Custom Dark Black "); var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } [Fact] public void Theme_Switch_Works_In_Nested_Scope() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" "); window.ApplyTemplate(); var scope = window.FindControl("Scope")!; var text = window.FindControl("Text")!; Assert.Equal(ThemeVariant.Dark, text.ActualThemeVariant); Assert.Equal(Color.Parse("#dedede"), ((ISolidColorBrush)text.Foreground!).Color); scope.RequestedThemeVariant = ThemeVariant.Light; Assert.Equal(ThemeVariant.Light, text.ActualThemeVariant); Assert.Equal(Colors.Black, ((ISolidColorBrush)text.Foreground!).Color); } } [Fact] public void Theme_Switch_Works_In_With_Popup() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" "); window.Show(); var scope = window.FindControl("Scope")!; var popup = window.FindControl("Popup")!; popup.IsOpen = true; var border = (Border)popup.Child!; Assert.Equal(ThemeVariant.Dark, popup.ActualThemeVariant); Assert.Equal(ThemeVariant.Dark, border.ActualThemeVariant); Assert.Equal(Color.Parse("#282828"), ((ISolidColorBrush)border.Background!).Color); scope.RequestedThemeVariant = ThemeVariant.Light; Assert.Equal(ThemeVariant.Light, popup.ActualThemeVariant); Assert.Equal(ThemeVariant.Light, border.ActualThemeVariant); Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background!).Color); } } } }