From d136d03fd1badabb9161f532b5a64df6bd26c5e4 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 23 Apr 2023 22:24:17 +0200 Subject: [PATCH 1/4] Fix slnf Headless path --- Avalonia.Desktop.slnf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 76620e8b93..73e38f8cb9 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -23,8 +23,6 @@ "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", - "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", - "src\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Avalonia.MicroCom\\Avalonia.MicroCom.csproj", "src\\Avalonia.Native\\Avalonia.Native.csproj", "src\\Avalonia.OpenGL\\Avalonia.OpenGL.csproj", @@ -33,6 +31,8 @@ "src\\Avalonia.Themes.Fluent\\Avalonia.Themes.Fluent.csproj", "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj", "src\\Avalonia.X11\\Avalonia.X11.csproj", + "src\\Headless\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", + "src\\Headless\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", @@ -65,4 +65,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} From 1117332e4d6e83244f8c3dbf44d4f2f10f4d4005 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 23 Apr 2023 22:24:56 +0200 Subject: [PATCH 2/4] Add AccentColor support for KDE --- .../DBusPlatformSettings.cs | 78 ++++++++++++++----- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index a25bb68458..2b124499b3 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Avalonia.Logging; +using Avalonia.Media; using Avalonia.Platform; using Tmds.DBus.SourceGenerator; @@ -9,7 +10,10 @@ namespace Avalonia.FreeDesktop internal class DBusPlatformSettings : DefaultPlatformSettings { private readonly OrgFreedesktopPortalSettings? _settings; + private PlatformColorValues? _lastColorValues; + private PlatformThemeVariant? _themeVariant; + private Color? _accentColor; public DBusPlatformSettings() { @@ -21,24 +25,33 @@ namespace Avalonia.FreeDesktop _ = TryGetInitialValueAsync(); } - public override PlatformColorValues GetColorValues() - { - return _lastColorValues ?? base.GetColorValues(); - } + public override PlatformColorValues GetColorValues() => _lastColorValues ?? base.GetColorValues(); private async Task TryGetInitialValueAsync() { try { var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); - _lastColorValues = GetColorValuesFromSetting(value); - OnColorValuesChanged(_lastColorValues); + _themeVariant = ReadAsColorScheme(value); + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.freedesktop.appearance.color-scheme value", ex); + } + + try + { + var value = await _settings!.ReadAsync("org.kde.kdeglobals.General", "AccentColor"); + _accentColor = ReadAsAccentColor(value); } catch (Exception ex) { - _lastColorValues = base.GetColorValues(); - Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.kde.kdeglobals.General.AccentColor value", ex); } + + _lastColorValues = BuildPlatformColorValues(); + if (_lastColorValues is not null) + OnColorValuesChanged(_lastColorValues); } private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple) @@ -46,25 +59,48 @@ namespace Avalonia.FreeDesktop if (exception is not null) return; - if (valueTuple is ("org.freedesktop.appearance", "color-scheme", { } value)) + switch (valueTuple) { - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ - _lastColorValues = GetColorValuesFromSetting(value); - OnColorValuesChanged(_lastColorValues); + case ("org.freedesktop.appearance", "color-scheme", { } colorScheme): + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ + _themeVariant = ReadAsColorScheme(colorScheme); + _lastColorValues = BuildPlatformColorValues(); + OnColorValuesChanged(_lastColorValues!); + break; + case ("org.kde.kdeglobals.General", "AccentColor", { } accentColor): + _accentColor = ReadAsAccentColor(accentColor); + _lastColorValues = BuildPlatformColorValues(); + OnColorValuesChanged(_lastColorValues!); + break; } } - private static PlatformColorValues GetColorValuesFromSetting(DBusVariantItem value) + private PlatformColorValues? BuildPlatformColorValues() + { + if (_themeVariant is { } themeVariant && _accentColor is { } accentColor) + return new PlatformColorValues { ThemeVariant = themeVariant, AccentColor1 = accentColor }; + if (_themeVariant is { } themeVariant1) + return new PlatformColorValues { ThemeVariant = themeVariant1 }; + if (_accentColor is { } accentColor1) + return new PlatformColorValues { AccentColor1 = accentColor1 }; + return null; + } + + private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value) { var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; - return new PlatformColorValues - { - ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light - }; + return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light; + } + + private static Color ReadAsAccentColor(DBusVariantItem value) + { + var colorStr = ((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value; + var rgb = colorStr.Split(','); + return new Color(255, byte.Parse(rgb[0]), byte.Parse(rgb[1]), byte.Parse(rgb[2])); } } } From f77b67db97487825d172401f118e205c6bae0510 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 23 Apr 2023 22:32:51 +0200 Subject: [PATCH 3/4] Move comment --- src/Avalonia.FreeDesktop/DBusPlatformSettings.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index 2b124499b3..8b2b38bb82 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -62,11 +62,6 @@ namespace Avalonia.FreeDesktop switch (valueTuple) { case ("org.freedesktop.appearance", "color-scheme", { } colorScheme): - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ _themeVariant = ReadAsColorScheme(colorScheme); _lastColorValues = BuildPlatformColorValues(); OnColorValuesChanged(_lastColorValues!); @@ -92,6 +87,11 @@ namespace Avalonia.FreeDesktop private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value) { + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light; } From a507e92b31f24bc5cf88bedea89f47eb93c2ce42 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 24 Apr 2023 02:00:23 -0400 Subject: [PATCH 4/4] Fix theme-dependend markup extensions not knowing current theme context --- .../Controls/IResourceDictionary.cs | 2 +- .../Controls/IThemeVariantProvider.cs | 22 +++ .../Controls/ResourceDictionary.cs | 12 +- .../Controls/ResourceNodeExtensions.cs | 46 +++--- .../Styling/IThemeVariantHost.cs | 1 - .../AvaloniaXamlIlCompiler.cs | 3 +- ...iaXamlIlThemeVariantProviderTransformer.cs | 31 ++++ .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../DynamicResourceExtension.cs | 6 +- .../StaticResourceExtension.cs | 22 ++- .../Styling/ResourceInclude.cs | 4 +- .../Themes/FluentBenchmark.cs | 22 ++- .../Themes/ThemeBenchmark.cs | 4 +- .../Xaml/ThemeDictionariesTests.cs | 136 +++++++++++++++++- 14 files changed, 271 insertions(+), 42 deletions(-) create mode 100644 src/Avalonia.Base/Controls/IThemeVariantProvider.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs diff --git a/src/Avalonia.Base/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs index 2bd1f65638..6712498bf4 100644 --- a/src/Avalonia.Base/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/IResourceDictionary.cs @@ -18,6 +18,6 @@ namespace Avalonia.Controls /// /// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios. /// - IDictionary ThemeDictionaries { get; } + IDictionary ThemeDictionaries { get; } } } diff --git a/src/Avalonia.Base/Controls/IThemeVariantProvider.cs b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs new file mode 100644 index 0000000000..d1dca2efbf --- /dev/null +++ b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs @@ -0,0 +1,22 @@ +using Avalonia.Metadata; +using Avalonia.Styling; + +namespace Avalonia.Controls; + +/// +/// Resource provider with theme variant awareness. +/// Can be used with . +/// +/// +/// This is a helper interface for the XAML compiler to make Key property accessibly by the markup extensions. +/// Which means, it can only be used with ResourceDictionaries and markup extensions in the XAML code. +/// This API might be removed in the future minor updates. +/// +[Unstable] +public interface IThemeVariantProvider : IResourceProvider +{ + /// + /// Key property set by the compiler. + /// + ThemeVariant? Key { get; set; } +} diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 231a19baab..b928cf0672 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : IResourceDictionary + public class ResourceDictionary : IResourceDictionary, IThemeVariantProvider { private object? lastDeferredItemKey; private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; - private AvaloniaDictionary? _themeDictionary; + private AvaloniaDictionary? _themeDictionary; /// /// Initializes a new instance of the class. @@ -93,13 +93,13 @@ namespace Avalonia.Controls } } - public IDictionary ThemeDictionaries + public IDictionary ThemeDictionaries { get { if (_themeDictionary == null) { - _themeDictionary = new AvaloniaDictionary(2); + _themeDictionary = new AvaloniaDictionary(2); _themeDictionary.ForEachItem( (_, x) => { @@ -120,6 +120,8 @@ namespace Avalonia.Controls return _themeDictionary; } } + + ThemeVariant? IThemeVariantProvider.Key { get; set; } bool IResourceNode.HasResources { @@ -192,7 +194,7 @@ namespace Avalonia.Controls if (_themeDictionary is not null) { - IResourceProvider? themeResourceProvider; + IThemeVariantProvider? themeResourceProvider; if (theme is not null && theme != ThemeVariant.Default) { if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 8aed1545a5..382ebac0e3 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -119,7 +119,19 @@ namespace Avalonia.Controls resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); key = key ?? throw new ArgumentNullException(nameof(key)); - return new FloatingResourceObservable(resourceProvider, key, converter); + return new FloatingResourceObservable(resourceProvider, key, null, converter); + } + + public static IObservable GetResourceObservable( + this IResourceProvider resourceProvider, + object key, + ThemeVariant? defaultThemeVariant, + Func? converter = null) + { + resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return new FloatingResourceObservable(resourceProvider, key, defaultThemeVariant, converter); } private class ResourceObservable : LightweightObservableBase @@ -128,7 +140,10 @@ namespace Avalonia.Controls private readonly object _key; private readonly Func? _converter; - public ResourceObservable(IResourceHost target, object key, Func? converter) + public ResourceObservable( + IResourceHost target, + object key, + Func? converter) { _target = target; _key = key; @@ -170,11 +185,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (_target is not IThemeVariantHost themeVariantHost - || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = (_target as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } @@ -183,14 +195,20 @@ namespace Avalonia.Controls private class FloatingResourceObservable : LightweightObservableBase { private readonly IResourceProvider _target; + private readonly ThemeVariant? _overrideThemeVariant; private readonly object _key; private readonly Func? _converter; private IResourceHost? _owner; - public FloatingResourceObservable(IResourceProvider target, object key, Func? converter) + public FloatingResourceObservable( + IResourceProvider target, + object key, + ThemeVariant? overrideThemeVariant, + Func? converter) { _target = target; _key = key; + _overrideThemeVariant = overrideThemeVariant; _converter = converter; } @@ -233,7 +251,7 @@ namespace Avalonia.Controls { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost) { themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } @@ -244,12 +262,11 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost2) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost2) { themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; } - PublishNext(); } @@ -265,11 +282,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (!(_target.Owner is IThemeVariantHost themeVariantHost) - || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = _overrideThemeVariant ?? (_target.Owner as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.Owner?.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs index 01583148a8..740887970b 100644 --- a/src/Avalonia.Base/Styling/IThemeVariantHost.cs +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -7,7 +7,6 @@ namespace Avalonia.Styling; /// /// Interface for the host element with a theme variant. /// -[Unstable] public interface IThemeVariantHost : IResourceHost { /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 5ca2b09eba..23c67df810 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -58,7 +58,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), - new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer(), + new AvaloniaXamlIlThemeVariantProviderTransformer() ); InsertBefore( new AvaloniaXamlIlOptionMarkupExtensionTransformer()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs new file mode 100644 index 0000000000..05df8be1b6 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs @@ -0,0 +1,31 @@ +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; + +internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + var type = context.GetAvaloniaTypes().IThemeVariantProvider; + if (!(node is XamlAstObjectNode on + && type.IsAssignableFrom(on.Type.GetClrType()))) + return node; + + var keyDirective = on.Children.FirstOrDefault(n => n is XamlAstXmlDirective d + && d.Namespace == XamlNamespaces.Xaml2006 && + d.Name == "Key") as XamlAstXmlDirective; + if (keyDirective is null) + return node; + + var keyProp = type.Properties.First(p => p.Name == "Key"); + on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective, + new XamlAstClrProperty(keyDirective, keyProp, context.Configuration), + keyDirective.Values, true)); + + return node; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 63683da0db..8ab84f4615 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -110,6 +110,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } + public IXamlType IThemeVariantProvider { get; } public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } public IXamlType Style { get; } @@ -250,6 +251,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers cfg.TypeSystem.GetType("System.Func`2").MakeGenericType( cfg.TypeSystem.GetType("System.IServiceProvider"), XamlIlTypes.Object)); + IThemeVariantProvider = cfg.TypeSystem.GetType("Avalonia.Controls.IThemeVariantProvider"); UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind }); Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style"); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index e1b594e331..7f52c872ed 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Xaml.Converters; using Avalonia.Media; +using Avalonia.Styling; namespace Avalonia.Markup.Xaml.MarkupExtensions { @@ -10,6 +11,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { private object? _anchor; private BindingPriority _priority; + private ThemeVariant? _currentThemeVariant; public DynamicResourceExtension() { @@ -36,6 +38,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions (object?)serviceProvider.GetFirstParent(); } + _currentThemeVariant = StaticResourceExtension.GetDictionaryVariant(serviceProvider); + return this; } @@ -59,7 +63,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions } else if (_anchor is IResourceProvider resourceProvider) { - var source = resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + var source = resourceProvider.GetResourceObservable(ResourceKey, _currentThemeVariant, GetConverter(targetProperty)); return InstancedBinding.OneWay(source, _priority); } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 3de669b1e4..c23c31e24c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -33,7 +33,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var provideTarget = serviceProvider.GetService(); var targetObject = provideTarget?.TargetObject; var targetProperty = provideTarget?.TargetProperty; - var themeVariant = (targetObject as IThemeVariantHost)?.ActualThemeVariant; + var themeVariant = (targetObject as IThemeVariantHost)?.ActualThemeVariant + ?? GetDictionaryVariant(serviceProvider); var targetType = targetProperty switch { @@ -78,6 +79,25 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { return ColorToBrushConverter.Convert(control.FindResource(ResourceKey!), targetType); } + + internal static ThemeVariant? GetDictionaryVariant(IServiceProvider serviceProvider) + { + var parents = serviceProvider.GetService()?.Parents; + if (parents is null) + { + return null; + } + + foreach (var parent in parents) + { + if (parent is IThemeVariantProvider { Key: { } setKey }) + { + return setKey; + } + } + + return null; + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index fbcfdde565..eee02ea0d8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -13,7 +13,7 @@ namespace Avalonia.Markup.Xaml.Styling /// When used in runtime, this type might be unsafe with trimming and AOT. /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] - public class ResourceInclude : IResourceProvider + public class ResourceInclude : IResourceProvider, IThemeVariantProvider { private readonly IServiceProvider? _serviceProvider; private readonly Uri? _baseUri; @@ -65,6 +65,8 @@ namespace Avalonia.Markup.Xaml.Styling /// public Uri? Source { get; set; } + ThemeVariant? IThemeVariantProvider.Key { get; set; } + bool IResourceNode.HasResources => Loaded.HasResources; public event EventHandler? OwnerChanged diff --git a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs index e9b82d5381..8eadb3a3f0 100644 --- a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs @@ -1,7 +1,9 @@ using System; +using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; using Moq; @@ -30,27 +32,23 @@ namespace Avalonia.Benchmarks.Themes _app.Dispose(); } - [Benchmark] - public void RepeatButton() + [Benchmark()] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateButton() { - var button = new RepeatButton(); + var button = new Button(); _root.Child = button; _root.LayoutManager.ExecuteLayoutPass(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); } private static IDisposable CreateApp() { var services = new TestServices( - assetLoader: new AssetLoader(), - globalClock: new MockGlobalClock(), - platform: new AppBuilder().RuntimePlatform, - renderInterface: new MockPlatformRenderInterface(), - standardCursorFactory: Mock.Of(), - theme: () => LoadFluentTheme(), + renderInterface: new NullRenderingPlatform(), dispatcherImpl: new NullThreadingPlatform(), - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl(), - windowingPlatform: new MockWindowingPlatform()); + standardCursorFactory: new NullCursorFactory(), + theme: () => LoadFluentTheme()); return UnitTestApplication.Start(services); } diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index 7c0a3f8bdf..ac174e4bc2 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -1,5 +1,5 @@ using System; - +using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; @@ -29,6 +29,7 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] public bool InitFluentTheme() { UnitTestApplication.Current.Styles[0] = new FluentTheme(); @@ -36,6 +37,7 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] public bool InitSimpleTheme() { UnitTestApplication.Current.Styles[0] = new SimpleTheme(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs index 3ac4677694..2def84bb18 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -1,9 +1,12 @@ -using System.Linq; +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; @@ -140,7 +143,7 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } - [Fact(Skip = "Not implemented")] + [Fact] public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" @@ -183,6 +186,135 @@ public class ThemeDictionariesTests : XamlTestBase 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()