From dd2f8216fbf73e810715f7d619127f900e90d3b5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 13 Aug 2022 03:12:26 -0400 Subject: [PATCH] Update API layer --- samples/ControlCatalog/Pages/ThemePage.axaml | 4 +- .../ControlCatalog/Pages/ThemePage.axaml.cs | 7 +- .../Controls/IResourceDictionary.cs | 1 + src/Avalonia.Base/Controls/IResourceNode.cs | 1 + .../Controls/ResourceDictionary.cs | 1 + .../Controls/ResourceNodeExtensions.cs | 39 ++- src/Avalonia.Base/StyledElement.cs | 17 ++ .../IApplicationThemeVariantHost.cs} | 7 +- src/Avalonia.Base/Styling/IStyleable.cs | 10 + .../{Themes => Styling}/ThemeVariant.cs | 13 +- .../ThemeVariantTypeConverter.cs | 2 +- .../Themes/IApplicationThemeHost.cs | 9 - src/Avalonia.Controls/Application.cs | 10 +- src/Avalonia.Controls/Control.cs | 9 +- src/Avalonia.Controls/IControl.cs | 3 +- .../{ThemeControl.cs => ThemeVariantScope.cs} | 17 +- src/Avalonia.Controls/TopLevel.cs | 12 +- src/Avalonia.Controls/Window.cs | 5 +- .../Diagnostics/Controls/Application.cs | 3 +- .../Controls/FluentControls.xaml | 2 +- .../Controls/TextBox.xaml | 2 +- ...emeControl.xaml => ThemeVariantScope.xaml} | 4 +- .../Controls/SimpleControls.xaml | 2 +- ...emeControl.xaml => ThemeVariantScope.xaml} | 4 +- .../MarkupExtensions/ResourceInclude.cs | 1 + .../StaticResourceExtension.cs | 48 ++-- .../AvaloniaPropertyConverterTest.cs | 6 + .../ThemeDictionariesTests.cs | 228 +++++++++--------- .../Avalonia.UnitTests/UnitTestApplication.cs | 2 +- 29 files changed, 240 insertions(+), 229 deletions(-) rename src/Avalonia.Base/{Themes/IThemeStyleable.cs => Styling/IApplicationThemeVariantHost.cs} (75%) rename src/Avalonia.Base/{Themes => Styling}/ThemeVariant.cs (74%) rename src/Avalonia.Base/{Themes => Styling}/ThemeVariantTypeConverter.cs (95%) delete mode 100644 src/Avalonia.Base/Themes/IApplicationThemeHost.cs rename src/Avalonia.Controls/{ThemeControl.cs => ThemeVariantScope.cs} (55%) rename src/Avalonia.Themes.Fluent/Controls/{ThemeControl.xaml => ThemeVariantScope.xaml} (83%) rename src/Avalonia.Themes.Simple/Controls/{ThemeControl.xaml => ThemeVariantScope.xaml} (78%) diff --git a/samples/ControlCatalog/Pages/ThemePage.axaml b/samples/ControlCatalog/Pages/ThemePage.axaml index 7286b1a46d5..8cb702a77df 100644 --- a/samples/ControlCatalog/Pages/ThemePage.axaml +++ b/samples/ControlCatalog/Pages/ThemePage.axaml @@ -54,7 +54,7 @@ - + - + diff --git a/samples/ControlCatalog/Pages/ThemePage.axaml.cs b/samples/ControlCatalog/Pages/ThemePage.axaml.cs index 375b5de7406..6b6787113bb 100644 --- a/samples/ControlCatalog/Pages/ThemePage.axaml.cs +++ b/samples/ControlCatalog/Pages/ThemePage.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Styling; namespace ControlCatalog.Pages { @@ -13,7 +14,7 @@ public ThemePage() AvaloniaXamlLoader.Load(this); var selector = this.FindControl("Selector")!; - var themeControl = this.FindControl("ThemeControl")!; + var themeVariantScope = this.FindControl("ThemeVariantScope")!; selector.Items = new[] { @@ -29,11 +30,11 @@ public ThemePage() var theme = (ThemeVariant)selector.SelectedItem!; if ((string)theme.Key == "Default") { - themeControl.ClearValue(ThemeControl.ThemeVariantProperty); + themeVariantScope.ClearValue(ThemeVariantProperty); } else { - themeControl.ThemeVariant = theme; + themeVariantScope.ThemeVariant = theme; } }; } diff --git a/src/Avalonia.Base/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs index e3c1a3f9943..2bd1f65638e 100644 --- a/src/Avalonia.Base/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/IResourceDictionary.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Styling; #nullable enable diff --git a/src/Avalonia.Base/Controls/IResourceNode.cs b/src/Avalonia.Base/Controls/IResourceNode.cs index c762590a0cb..d2fa3c7af3d 100644 --- a/src/Avalonia.Base/Controls/IResourceNode.cs +++ b/src/Avalonia.Base/Controls/IResourceNode.cs @@ -1,4 +1,5 @@ using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 91f15a2a94c..a003fd4ed38 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -5,6 +5,7 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Templates; +using Avalonia.Styling; namespace Avalonia.Controls { diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index e8e0dedef40..6e7f64763ec 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Reactive; +using Avalonia.Styling; #nullable enable @@ -38,9 +39,7 @@ public static bool TryFindResource(this IResourceHost control, object key, out o control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - var theme = AvaloniaLocator.Current.GetService()?.ThemeVariant; - - return control.TryFindResource(key, theme, out value); + return control.TryFindResource(key, null, out value); } /// @@ -98,9 +97,7 @@ public static bool TryGetResource(this IResourceHost control, object key, out ob control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - var theme = AvaloniaLocator.Current.GetService()?.ThemeVariant; - - return control.TryGetResource(key, theme, out value); + return control.TryGetResource(key, null, out value); } public static IObservable GetResourceObservable( @@ -141,7 +138,7 @@ public ResourceObservable(IResourceHost target, object key, Func observer, bool first) { - if (_target.Owner is object) + if (_target.Owner is not null) { observer.OnNext(GetValue()); } @@ -224,7 +221,7 @@ protected override void Subscribed(IObserver observer, bool first) private void PublishNext() { - if (_target.Owner is object) + if (_target.Owner is not null) { PublishNext(GetValue()); } @@ -232,24 +229,24 @@ private void PublishNext() private void OwnerChanged(object? sender, EventArgs e) { - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is IThemeStyleable themeStyleable) + if (_owner is IStyleable styleable) { - themeStyleable.ThemeVariantChanged -= ThemeVariantChanged; + styleable.ThemeVariantChanged -= ThemeVariantChanged; } _owner = _target.Owner; - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is IThemeStyleable themeStyleable2) + if (_owner is IStyleable styleable2) { - themeStyleable2.ThemeVariantChanged += ThemeVariantChanged; + styleable2.ThemeVariantChanged += ThemeVariantChanged; } PublishNext(); @@ -267,9 +264,9 @@ private void ThemeVariantChanged(object? sender, EventArgs e) private object? GetValue() { - if (!(_target.Owner is IThemeStyleable themeStyleable) + if (!(_target.Owner is IStyleable themeStyleable) || themeStyleable.ThemeVariant is null - || !themeStyleable.TryFindResource(_key, themeStyleable.ThemeVariant, out var value)) + || !_target.Owner.TryFindResource(_key, themeStyleable.ThemeVariant, out var value)) { value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; } diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 67af2475e3b..254d56807bf 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -60,6 +60,15 @@ public class StyledElement : Animatable, IDataContextProvider, IStyledElement, I public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); + /// + /// Defines the property. + /// + public static readonly StyledProperty ThemeVariantProperty = + AvaloniaProperty.Register( + nameof(ThemeVariant), + inherits: true, + defaultValue: ThemeVariant.Light); + private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; @@ -246,6 +255,10 @@ public ControlTheme? Theme set => SetValue(ThemeProperty, value); } + ThemeVariant IStyleable.ThemeVariant => GetValue(ThemeVariantProperty); + + public event EventHandler? ThemeVariantChanged; + /// /// Gets the styled element's logical children. /// @@ -659,6 +672,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang InvalidateStyles(); } + else if (change.Property == ThemeVariantProperty) + { + ThemeVariantChanged?.Invoke(this, EventArgs.Empty); + } } private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) diff --git a/src/Avalonia.Base/Themes/IThemeStyleable.cs b/src/Avalonia.Base/Styling/IApplicationThemeVariantHost.cs similarity index 75% rename from src/Avalonia.Base/Themes/IThemeStyleable.cs rename to src/Avalonia.Base/Styling/IApplicationThemeVariantHost.cs index dd712b719a9..dc563e7a816 100644 --- a/src/Avalonia.Base/Themes/IThemeStyleable.cs +++ b/src/Avalonia.Base/Styling/IApplicationThemeVariantHost.cs @@ -1,13 +1,12 @@ using System; - using Avalonia.Controls; -namespace Avalonia; +namespace Avalonia.Styling; /// -/// Interface for elements that supports dynamic theme. +/// Interface for a host element with a root theme. /// -public interface IThemeStyleable : IResourceHost +public interface IApplicationThemeVariantHost : IResourceHost { /// /// Gets the UI theme that is used by the control (and its child elements) for resource determination. diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs index 254da4d85c3..11a362a8814 100644 --- a/src/Avalonia.Base/Styling/IStyleable.cs +++ b/src/Avalonia.Base/Styling/IStyleable.cs @@ -31,6 +31,16 @@ public interface IStyleable : IAvaloniaObject, INamed /// ControlTheme? GetEffectiveTheme(); + /// + /// Gets the UI theme that is used by the control (and its child elements) for resource determination. + /// + ThemeVariant ThemeVariant { get; } + + /// + /// Raised when the theme is changed on the element or an ancestor of the element. + /// + event EventHandler? ThemeVariantChanged; + /// /// Notifies the element that a style has been applied. /// diff --git a/src/Avalonia.Base/Themes/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs similarity index 74% rename from src/Avalonia.Base/Themes/ThemeVariant.cs rename to src/Avalonia.Base/Styling/ThemeVariant.cs index 23b42e74b16..868172aa801 100644 --- a/src/Avalonia.Base/Themes/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -1,10 +1,10 @@ using System; using System.ComponentModel; -namespace Avalonia; +namespace Avalonia.Styling; [TypeConverter(typeof(ThemeVariantTypeConverter))] -public class ThemeVariant +public class ThemeVariant : IEquatable { public ThemeVariant(object key) { @@ -31,8 +31,17 @@ public override int GetHashCode() public override bool Equals(object? obj) { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; return obj is ThemeVariant theme && Key.Equals(theme.Key); } + + public bool Equals(ThemeVariant? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return Key.Equals(obj.Key); + } public override string ToString() { diff --git a/src/Avalonia.Base/Themes/ThemeVariantTypeConverter.cs b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs similarity index 95% rename from src/Avalonia.Base/Themes/ThemeVariantTypeConverter.cs rename to src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs index 5eb84dbe062..4da1b495f5d 100644 --- a/src/Avalonia.Base/Themes/ThemeVariantTypeConverter.cs +++ b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Globalization; -namespace Avalonia; +namespace Avalonia.Styling; public class ThemeVariantTypeConverter : TypeConverter { diff --git a/src/Avalonia.Base/Themes/IApplicationThemeHost.cs b/src/Avalonia.Base/Themes/IApplicationThemeHost.cs deleted file mode 100644 index 7341628f0db..00000000000 --- a/src/Avalonia.Base/Themes/IApplicationThemeHost.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Avalonia; - -/// -/// Interface for a host element with a root theme. -/// -public interface IApplicationThemeHost : IThemeStyleable -{ - -} diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index f237b929fca..188b9767637 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -31,7 +31,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IApplicationThemeHost, IApplicationPlatformEvents + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IApplicationThemeVariantHost, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -53,9 +53,9 @@ public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemp public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); - /// + /// public static readonly StyledProperty ThemeVariantProperty = - ThemeControl.ThemeVariantProperty.AddOwner(); + StyledElement.ThemeVariantProperty.AddOwner(); /// public event EventHandler? ResourcesChanged; @@ -87,7 +87,7 @@ public object? DataContext set { SetValue(DataContextProperty, value); } } - /// + /// public ThemeVariant ThemeVariant { get => GetValue(ThemeVariantProperty); @@ -245,7 +245,7 @@ public virtual void RegisterServices() .Bind().ToTransient() .Bind().ToConstant(this) .Bind().ToConstant(this) - .Bind().ToConstant(this) + .Bind().ToConstant(this) .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index f19af92aa20..b200ed0c126 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -172,10 +172,7 @@ public FlowDirection FlowDirection get => GetValue(FlowDirectionProperty); set => SetValue(FlowDirectionProperty, value); } - - ThemeVariant IThemeStyleable.ThemeVariant => GetValue(ThemeControl.ThemeVariantProperty); - public event EventHandler? ThemeVariantChanged; - + /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// @@ -551,10 +548,6 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } } - else if (change.Property == ThemeControl.ThemeVariantProperty) - { - ThemeVariantChanged?.Invoke(this, EventArgs.Empty); - } } /// diff --git a/src/Avalonia.Controls/IControl.cs b/src/Avalonia.Controls/IControl.cs index 64a8bb0ed5c..3395fc1059e 100644 --- a/src/Avalonia.Controls/IControl.cs +++ b/src/Avalonia.Controls/IControl.cs @@ -15,8 +15,7 @@ public interface IControl : IVisual, ILayoutable, IInputElement, INamed, - IStyledElement, - IThemeStyleable + IStyledElement { new IControl? Parent { get; } } diff --git a/src/Avalonia.Controls/ThemeControl.cs b/src/Avalonia.Controls/ThemeVariantScope.cs similarity index 55% rename from src/Avalonia.Controls/ThemeControl.cs rename to src/Avalonia.Controls/ThemeVariantScope.cs index d1dc3f90c18..9d1aa4e97d2 100644 --- a/src/Avalonia.Controls/ThemeControl.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -1,25 +1,18 @@ -namespace Avalonia.Controls +using Avalonia.Styling; + +namespace Avalonia.Controls { /// /// Decorator control that isolates controls subtree with locally defined property. /// - public class ThemeControl : Decorator + public class ThemeVariantScope : Decorator { - /// - /// Defines the property. - /// - public static readonly StyledProperty ThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ThemeVariant), - inherits: true, - defaultValue: ThemeVariant.Light); - /// /// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination. /// The UI theme you specify with ThemeVariant can override the app-level ThemeVariant. /// /// - /// To reset local value and inherit parent theme, call with as an argument. + /// To reset local value and inherit parent theme, call with as an argument. /// public ThemeVariant ThemeVariant { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index efbda171ebe..d0a15749071 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -60,11 +60,7 @@ public abstract class TopLevel : ContentControl, /// public static readonly StyledProperty TransparencyLevelHintProperty = AvaloniaProperty.Register(nameof(TransparencyLevelHint), WindowTransparencyLevel.None); - - /// - public static readonly StyledProperty ThemeVariantProperty = - ThemeControl.ThemeVariantProperty.AddOwner(); - + /// /// Defines the property. /// @@ -90,7 +86,7 @@ private static readonly WeakEvent private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; - private readonly IApplicationThemeHost? _applicationThemeHost; + private readonly IApplicationThemeVariantHost? _applicationThemeHost; private readonly PointerOverPreProcessor? _pointerOverPreProcessor; private readonly IDisposable? _pointerOverPreProcessorSubscription; private Size _clientSize; @@ -156,7 +152,7 @@ public TopLevel(ITopLevelImpl impl, IAvaloniaDependencyResolver? dependencyResol _keyboardNavigationHandler = TryGetService(dependencyResolver); _renderInterface = TryGetService(dependencyResolver); _globalStyles = TryGetService(dependencyResolver); - _applicationThemeHost = TryGetService(dependencyResolver); + _applicationThemeHost = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); @@ -276,7 +272,7 @@ public IBrush TransparencyBackgroundFallback set => SetValue(TransparencyBackgroundFallbackProperty, value); } - /// + /// public ThemeVariant ThemeVariant { get => GetValue(ThemeVariantProperty); diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 214091b9446..04efff2e877 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -179,10 +179,7 @@ public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot /// public static readonly RoutedEvent WindowOpenedEvent = RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); - - public static readonly StyledProperty ThemeVariantProperty = - ThemeControl.ThemeVariantProperty.AddOwner(); - + private readonly NameScope _nameScope = new NameScope(); private object? _dialogResult; private readonly Size _maxPlatformClientSize; diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index a1c70949b60..8cbd1b07323 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls; +using Avalonia.Styling; using Lifetimes = Avalonia.Controls.ApplicationLifetimes; using App = Avalonia.Application; @@ -15,7 +16,7 @@ class Application : AvaloniaObject public event EventHandler? Closed; public static readonly StyledProperty ThemeProperty = - ThemeControl.ThemeVariantProperty.AddOwner(); + StyledElement.ThemeVariantProperty.AddOwner(); public Application(App application) { diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index dc192bdcf2b..47f2349e8cf 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -69,7 +69,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml index 17c69da8fd6..f2d02275d92 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml @@ -238,7 +238,7 @@ + IsVisible="{Binding !$parent[ToggleButton].IsChecked}"/> diff --git a/src/Avalonia.Themes.Fluent/Controls/ThemeControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml similarity index 83% rename from src/Avalonia.Themes.Fluent/Controls/ThemeControl.xaml rename to src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml index ee567d94c22..21a5506b880 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ThemeControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml @@ -1,7 +1,7 @@ - + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 9e750795a00..f8dca96a774 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -65,7 +65,7 @@ - + diff --git a/src/Avalonia.Themes.Simple/Controls/ThemeControl.xaml b/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml similarity index 78% rename from src/Avalonia.Themes.Simple/Controls/ThemeControl.xaml rename to src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml index 573450f14bc..a6022fb2638 100644 --- a/src/Avalonia.Themes.Simple/Controls/ThemeControl.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml @@ -1,7 +1,7 @@ - + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 77b5a5ad9ec..da3c7f523b3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using Avalonia.Controls; +using Avalonia.Styling; #nullable enable diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index bf226769221..eb6946f0ada 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -29,8 +29,9 @@ public object ProvideValue(IServiceProvider serviceProvider) { var stack = serviceProvider.GetService(); var provideTarget = serviceProvider.GetService(); - var themeVariant = ResolveThemeVariant(provideTarget, stack); - + var themeVariant = (provideTarget.TargetObject as IStyleable)?.ThemeVariant; + IResourceDictionary? containingDictionary = null; + var targetType = provideTarget.TargetProperty switch { AvaloniaProperty ap => ap.PropertyType, @@ -44,7 +45,8 @@ public object ProvideValue(IServiceProvider serviceProvider) } var previousWasControlTheme = false; - + var firstParentVisited = false; + // Look upwards though the ambient context for IResourceNodes // which might be able to give us the resource. foreach (var parent in stack.Parents) @@ -53,6 +55,24 @@ public object ProvideValue(IServiceProvider serviceProvider) { return ColorToBrushConverter.Convert(value, targetType); } + + // To get a fallback theme variant, check if static resource was invoked inside of the ResourceDictionary.ThemeDictionaries. + if (themeVariant is null && firstParentVisited && containingDictionary is not null) + { + if (parent is IResourceDictionary parentDictionary + && parentDictionary.ThemeDictionaries + .FirstOrDefault(p => p.Value == containingDictionary).Key is { } key) + { + themeVariant = key; + } + containingDictionary = null; + } + + if (!firstParentVisited) + { + firstParentVisited = true; + containingDictionary = parent as IResourceDictionary; + } // HACK: Temporary fix for #8678. Hard-coded to only work for the DevTools main // window as we don't want 3rd parties to start relying on this hack. @@ -88,28 +108,6 @@ private object GetValue(IStyledElement control, Type? targetType) { return ColorToBrushConverter.Convert(control.FindResource(ResourceKey), targetType); } - - private ThemeVariant? ResolveThemeVariant( - IProvideValueTarget provideTarget, IAvaloniaXamlIlParentStackProvider stack) - { - // If target is a control, use its theme variant. - if (provideTarget.TargetObject is IThemeStyleable themeStyleable) - { - return themeStyleable.ThemeVariant; - } - - // Check if static resource was invoked inside of the ResourceDictionary.ThemeDictionaries - if (stack.Parents.FirstOrDefault() is ResourceDictionary themeDictionary - && stack.Parents.Skip(1).FirstOrDefault() is ResourceDictionary parentDictionary - && parentDictionary.ThemeDictionaries - .FirstOrDefault(p => p.Value == themeDictionary).Key is { } key) - { - return key; - } - - // Otherwise fallback to the global app theme. - return AvaloniaLocator.Current.GetService()?.ThemeVariant; - } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index 75e21a7138b..47117f2308c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -142,6 +142,12 @@ public ControlTheme GetEffectiveTheme() throw new NotImplementedException(); } + public ThemeVariant ThemeVariant + { + get { throw new NotImplementedException(); } + } + public event EventHandler ThemeVariantChanged; + public void DetachStyles() { throw new NotImplementedException(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs index 0bf71c99b0a..f621c3c4fca 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs @@ -2,6 +2,7 @@ using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Media; +using Avalonia.Styling; using Moq; using Xunit; @@ -12,11 +13,11 @@ public class ThemeDictionariesTests : XamlTestBase [Fact] public void DynamicResource_Updated_When_Control_Theme_Changed() { - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - + @@ -27,15 +28,15 @@ public void DynamicResource_Updated_When_Control_Theme_Changed() - + -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); - themeControl.ThemeVariant = ThemeVariant.Dark; + themeVariantScope.ThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } @@ -43,7 +44,7 @@ public void DynamicResource_Updated_When_Control_Theme_Changed() [Fact] public void DynamicResource_Updated_When_Control_Theme_Changed_No_Xaml() { - var themeControl = new ThemeControl + var themeVariantScope = new ThemeVariantScope { ThemeVariant = ThemeVariant.Light, Resources = new ResourceDictionary @@ -56,14 +57,14 @@ public void DynamicResource_Updated_When_Control_Theme_Changed_No_Xaml() }, Child = new Border() }; - var border = (Border)themeControl.Child!; + var border = (Border)themeVariantScope.Child!; border[!Border.BackgroundProperty] = new DynamicResourceExtension("DemoBackground"); DelayedBinding.ApplyBindings(border); Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); - themeControl.ThemeVariant = ThemeVariant.Dark; + themeVariantScope.ThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } @@ -71,11 +72,11 @@ public void DynamicResource_Updated_When_Control_Theme_Changed_No_Xaml() [Fact] public void Intermediate_DynamicResource_Updated_When_Control_Theme_Changed() { - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - + @@ -87,15 +88,15 @@ public void Intermediate_DynamicResource_Updated_When_Control_Theme_Changed() - + -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); - themeControl.ThemeVariant = ThemeVariant.Dark; + themeVariantScope.ThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } @@ -103,11 +104,11 @@ public void Intermediate_DynamicResource_Updated_When_Control_Theme_Changed() [Fact] public void Intermediate_StaticResource_Can_Be_Reached_From_ThemeDictionaries() { - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - + @@ -120,15 +121,15 @@ public void Intermediate_StaticResource_Can_Be_Reached_From_ThemeDictionaries() - + -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); - themeControl.ThemeVariant = ThemeVariant.Dark; + themeVariantScope.ThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } @@ -136,11 +137,11 @@ public void Intermediate_StaticResource_Can_Be_Reached_From_ThemeDictionaries() [Fact] public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key() { - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - + @@ -151,7 +152,7 @@ public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key - + @@ -167,58 +168,30 @@ public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); - themeControl.ThemeVariant = ThemeVariant.Dark; + themeVariantScope.ThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } - [Fact] - public void StaticResource_Outside_Of_ThemeDictionaries_Should_Use_Application_ThemeVariant() - { - using (AvaloniaLocator.EnterScope()) - { - var applicationThemeHost = new Mock(); - applicationThemeHost.SetupGet(h => h.ThemeVariant).Returns(ThemeVariant.Dark); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(applicationThemeHost.Object); - - var dictionary = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(@" - - - - Blue - - - Red - - - -"); - - var brush = Assert.IsType(dictionary["Brush"]); - Assert.Equal(Colors.Red, brush.Color); - } - } - [Fact] public void StaticResource_Outside_Of_Dictionaries_Should_Use_Control_ThemeVariant() { using (AvaloniaLocator.EnterScope()) { - var applicationThemeHost = new Mock(); + var applicationThemeHost = new Mock(); applicationThemeHost.SetupGet(h => h.ThemeVariant).Returns(ThemeVariant.Dark); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(applicationThemeHost.Object); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(applicationThemeHost.Object); - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - + @@ -229,13 +202,13 @@ public void StaticResource_Outside_Of_Dictionaries_Should_Use_Control_ThemeVaria - + -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; - themeControl.ThemeVariant = ThemeVariant.Light; + themeVariantScope.ThemeVariant = ThemeVariant.Light; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); } } @@ -243,8 +216,8 @@ public void StaticResource_Outside_Of_Dictionaries_Should_Use_Control_ThemeVaria [Fact] public void Inner_ThemeDictionaries_Works_Properly() { - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - @@ -261,12 +234,12 @@ public void Inner_ThemeDictionaries_Works_Properly() -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); - themeControl.ThemeVariant = ThemeVariant.Dark; + themeVariantScope.ThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } @@ -274,11 +247,11 @@ public void Inner_ThemeDictionaries_Works_Properly() [Fact] public void Inner_Resource_Can_Reference_Parent_ThemeDictionaries() { - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - + @@ -289,7 +262,7 @@ public void Inner_Resource_Can_Reference_Parent_ThemeDictionaries() - + @@ -298,12 +271,12 @@ public void Inner_Resource_Can_Reference_Parent_ThemeDictionaries() -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); - themeControl.ThemeVariant = ThemeVariant.Dark; + themeVariantScope.ThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } @@ -311,11 +284,11 @@ public void Inner_Resource_Can_Reference_Parent_ThemeDictionaries() [Fact] public void DynamicResource_Can_Access_Resources_Outside_Of_ThemeDictionaries() { - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - + @@ -328,15 +301,15 @@ public void DynamicResource_Can_Access_Resources_Outside_Of_ThemeDictionaries() Black White - + -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); - themeControl.ThemeVariant = ThemeVariant.Dark; + themeVariantScope.ThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } @@ -346,16 +319,16 @@ 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 themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - + Red - + @@ -371,12 +344,12 @@ public void Inner_Dictionary_Does_Not_Affect_Parent_Resources() -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.Red, ((ISolidColorBrush)border.Background)!.Color); - themeControl.ThemeVariant = ThemeVariant.Dark; + themeVariantScope.ThemeVariant = ThemeVariant.Dark; Assert.Equal(Colors.Red, ((ISolidColorBrush)border.Background)!.Color); } @@ -384,11 +357,11 @@ public void Inner_Dictionary_Does_Not_Affect_Parent_Resources() [Fact] public void Custom_Theme_Can_Be_Defined_In_ThemeDictionaries() { - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - + @@ -402,42 +375,69 @@ public void Custom_Theme_Can_Be_Defined_In_ThemeDictionaries() - + -"); - var border = (Border)themeControl.Child!; +"); + var border = (Border)themeVariantScope.Child!; - themeControl.ThemeVariant = new ThemeVariant("Custom"); + themeVariantScope.ThemeVariant = new ThemeVariant("Custom"); Assert.Equal(Colors.Pink, ((ISolidColorBrush)border.Background)!.Color); } [Fact] - public void Custom_Theme_Fallbacks_To_Inherit_Theme() + public void Custom_Theme_Fallbacks_To_Inherit_Theme_DynamicResource() { - var themeControl = (ThemeControl)AvaloniaRuntimeXamlLoader.Load(@" - - - + + Black - - White + + + + +"); + var border = (Border)themeVariantScope.Child!; + + themeVariantScope.ThemeVariant = 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)themeControl.Child!; - - themeControl.ThemeVariant = new ThemeVariant("Custom", ThemeVariant.Dark); + +"); + var border = (Border)themeVariantScope.Child!; Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 319aa2fc7c5..b4fe0b6b554 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -57,7 +57,7 @@ public override void RegisterServices() .Bind().ToConstant(Services.FocusManager) .Bind().ToConstant(Services.GlobalClock) .BindToSelf(this) - .BindToSelf(this) + .BindToSelf(this) .Bind().ToConstant(Services.InputManager) .Bind().ToConstant(Services.KeyboardDevice?.Invoke()) .Bind().ToConstant(Services.KeyboardNavigation)