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);
}
}
}
}