From a79ca4783e1f9f80b63c625dc06a3379826913d5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 17 Jan 2023 23:12:07 -0500 Subject: [PATCH 01/11] Update AvaloniaDictionary API --- .../Collections/AvaloniaDictionary.cs | 18 ++- .../AvaloniaDictionaryExtensions.cs | 112 ++++++++++++++++++ .../Collections/IAvaloniaDictionary.cs | 13 ++ .../IAvaloniaReadOnlyDictionary.cs | 14 +++ 4 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs create mode 100644 src/Avalonia.Base/Collections/IAvaloniaDictionary.cs create mode 100644 src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 35a391f2cb..d4c7137fdc 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -14,11 +14,7 @@ namespace Avalonia.Collections /// /// The type of the dictionary key. /// The type of the dictionary value. - public class AvaloniaDictionary : IDictionary, - IDictionary, - INotifyCollectionChanged, - INotifyPropertyChanged - where TKey : notnull + public class AvaloniaDictionary : IAvaloniaDictionary where TKey : notnull { private Dictionary _inner; @@ -29,6 +25,14 @@ namespace Avalonia.Collections { _inner = new Dictionary(); } + + /// + /// Initializes a new instance of the class. + /// + public AvaloniaDictionary(int capacity) + { + _inner = new Dictionary(capacity); + } /// /// Occurs when the collection changes. @@ -62,6 +66,10 @@ namespace Avalonia.Collections object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot; + IEnumerable IReadOnlyDictionary.Keys => _inner.Keys; + + IEnumerable IReadOnlyDictionary.Values => _inner.Values; + /// /// Gets or sets the named resource. /// diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs new file mode 100644 index 0000000000..e350a019d4 --- /dev/null +++ b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Reactive; + +namespace Avalonia.Collections +{ + /// + /// Defines extension methods for working with s. + /// + public static class AvaloniaDictionaryExtensions + { + /// + /// Invokes an action for each item in a collection and subsequently each item added or + /// removed from the collection. + /// + /// The key type of the collection items. + /// The value type of the collection items. + /// The collection. + /// + /// An action called initially for each item in the collection and subsequently for each + /// item added to the collection. The parameters passed are the index in the collection and + /// the item. + /// + /// + /// An action called for each item removed from the collection. The parameters passed are + /// the index in the collection and the item. + /// + /// + /// An action called when the collection is reset. This will be followed by calls to + /// for each item present in the collection after the reset. + /// + /// + /// Indicates if a weak subscription should be used to track changes to the collection. + /// + /// A disposable used to terminate the subscription. + internal static IDisposable ForEachItem( + this IAvaloniaReadOnlyDictionary collection, + Action added, + Action removed, + Action reset, + bool weakSubscription = false) + where TKey : notnull + { + void Add(IEnumerable items) + { + foreach (KeyValuePair pair in items) + { + added(pair.Key, pair.Value); + } + } + + void Remove(IEnumerable items) + { + foreach (KeyValuePair pair in items) + { + removed(pair.Key, pair.Value); + } + } + + NotifyCollectionChangedEventHandler handler = (_, e) => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewItems!); + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + Remove(e.OldItems!); + int newIndex = e.NewStartingIndex; + if(newIndex > e.OldStartingIndex) + { + newIndex -= e.OldItems!.Count; + } + Add(e.NewItems!); + break; + + case NotifyCollectionChangedAction.Remove: + Remove(e.OldItems!); + break; + + case NotifyCollectionChangedAction.Reset: + if (reset == null) + { + throw new InvalidOperationException( + "Reset called on collection without reset handler."); + } + + reset(); + Add(collection); + break; + } + }; + + Add(collection); + + if (weakSubscription) + { + return collection.WeakSubscribe(handler); + } + else + { + collection.CollectionChanged += handler; + + return Disposable.Create(() => collection.CollectionChanged -= handler); + } + } + } +} diff --git a/src/Avalonia.Base/Collections/IAvaloniaDictionary.cs b/src/Avalonia.Base/Collections/IAvaloniaDictionary.cs new file mode 100644 index 0000000000..b79cfe2b9c --- /dev/null +++ b/src/Avalonia.Base/Collections/IAvaloniaDictionary.cs @@ -0,0 +1,13 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Collections +{ + public interface IAvaloniaDictionary + : IDictionary, + IAvaloniaReadOnlyDictionary, + IDictionary + where TKey : notnull + { + } +} diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs new file mode 100644 index 0000000000..d772de2f59 --- /dev/null +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace Avalonia.Collections +{ + public interface IAvaloniaReadOnlyDictionary + : IReadOnlyDictionary, + INotifyCollectionChanged, + INotifyPropertyChanged + where TKey : notnull + { + } +} From 253ecd028d58112fc04eb79317b0cc053807cb97 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:05:34 -0500 Subject: [PATCH 02/11] Introduce ThemeVariant API --- .../Controls/IResourceDictionary.cs | 6 + src/Avalonia.Base/Controls/IResourceNode.cs | 7 +- .../Controls/ResourceDictionary.cs | 91 ++++++++++++- .../Controls/ResourceNodeExtensions.cs | 125 +++++++++++++++--- src/Avalonia.Base/StyledElement.cs | 41 +++++- .../Styling/IGlobalThemeVariantProvider.cs | 22 +++ src/Avalonia.Base/Styling/StyleBase.cs | 6 +- src/Avalonia.Base/Styling/Styles.cs | 6 +- src/Avalonia.Base/Styling/ThemeVariant.cs | 65 +++++++++ .../Styling/ThemeVariantTypeConverter.cs | 23 ++++ src/Avalonia.Controls/Application.cs | 62 ++++++++- src/Avalonia.Controls/ThemeVariantScope.cs | 23 ++++ src/Avalonia.Controls/TopLevel.cs | 52 ++++++-- .../Diagnostics/Controls/Application.cs | 46 ++++++- .../Diagnostics/DevToolsOptions.cs | 8 +- .../ViewModels/ControlDetailsViewModel.cs | 3 +- .../Diagnostics/Views/MainWindow.xaml | 4 +- .../Diagnostics/Views/MainWindow.xaml.cs | 8 +- .../Internal/ResourceSelectorConverter.cs | 2 +- .../StaticResourceExtension.cs | 21 ++- .../Styling/ResourceInclude.cs | 5 +- .../Styling/StyleInclude.cs | 4 +- 22 files changed, 552 insertions(+), 78 deletions(-) create mode 100644 src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs create mode 100644 src/Avalonia.Base/Styling/ThemeVariant.cs create mode 100644 src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs create mode 100644 src/Avalonia.Controls/ThemeVariantScope.cs diff --git a/src/Avalonia.Base/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs index 3a68dde31e..2bd1f65638 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 @@ -13,5 +14,10 @@ namespace Avalonia.Controls /// Gets a collection of child resource dictionaries. /// IList MergedDictionaries { get; } + + /// + /// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios. + /// + IDictionary ThemeDictionaries { get; } } } diff --git a/src/Avalonia.Base/Controls/IResourceNode.cs b/src/Avalonia.Base/Controls/IResourceNode.cs index d6c900f97f..d2fa3c7af3 100644 --- a/src/Avalonia.Base/Controls/IResourceNode.cs +++ b/src/Avalonia.Base/Controls/IResourceNode.cs @@ -1,5 +1,5 @@ -using System; -using Avalonia.Metadata; +using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -23,6 +23,7 @@ namespace Avalonia.Controls /// Tries to find a resource within the object. /// /// The resource key. + /// Theme used to select theme dictionary. /// /// When this method returns, contains the value associated with the specified key, /// if the key is found; otherwise, null. @@ -30,6 +31,6 @@ namespace Avalonia.Controls /// /// True if the resource if found, otherwise false. /// - bool TryGetResource(object key, out object? value); + bool TryGetResource(object key, ThemeVariant? theme, out object? value); } } diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index d6197c50c6..85e4487ba9 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -1,9 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -15,6 +18,7 @@ namespace Avalonia.Controls private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; + private AvaloniaDictionary? _themeDictionary; /// /// Initializes a new instance of the class. @@ -69,14 +73,14 @@ namespace Avalonia.Controls _mergedDictionaries.ForEachItem( x => { - if (Owner is object) + if (Owner is not null) { x.AddOwner(Owner); } }, x => { - if (Owner is object) + if (Owner is not null) { x.RemoveOwner(Owner); } @@ -88,6 +92,34 @@ namespace Avalonia.Controls } } + public IDictionary ThemeDictionaries + { + get + { + if (_themeDictionary == null) + { + _themeDictionary = new AvaloniaDictionary(2); + _themeDictionary.ForEachItem( + (_, x) => + { + if (Owner is not null) + { + x.AddOwner(Owner); + } + }, + (_, x) => + { + if (Owner is not null) + { + x.RemoveOwner(Owner); + } + }, + () => throw new NotSupportedException("Dictionary reset not supported")); + } + return _themeDictionary; + } + } + bool IResourceNode.HasResources { get @@ -152,16 +184,47 @@ namespace Avalonia.Controls return false; } - public bool TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { if (TryGetValue(key, out value)) return true; + if (_themeDictionary is not null) + { + IResourceProvider? themeResourceProvider; + if (theme is not null) + { + if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) + && themeResourceProvider.TryGetResource(key, theme, out value)) + { + return true; + } + + var themeInherit = theme.InheritVariant; + while (themeInherit is not null) + { + if (_themeDictionary.TryGetValue(themeInherit, out themeResourceProvider) + && themeResourceProvider.TryGetResource(key, theme, out value)) + { + return true; + } + + themeInherit = themeInherit.InheritVariant; + } + } + + if (_themeDictionary.TryGetValue(ThemeVariant.Default, out themeResourceProvider) + && themeResourceProvider.TryGetResource(key, theme, out value)) + { + return true; + } + } + if (_mergedDictionaries != null) { for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) { - if (_mergedDictionaries[i].TryGetResource(key, out value)) + if (_mergedDictionaries[i].TryGetResource(key, theme, out value)) { return true; } @@ -248,7 +311,7 @@ namespace Avalonia.Controls var hasResources = _inner?.Count > 0; - if (_mergedDictionaries is object) + if (_mergedDictionaries is not null) { foreach (var i in _mergedDictionaries) { @@ -256,6 +319,14 @@ namespace Avalonia.Controls hasResources |= i.HasResources; } } + if (_themeDictionary is not null) + { + foreach (var i in _themeDictionary.Values) + { + i.AddOwner(owner); + hasResources |= i.HasResources; + } + } if (hasResources) { @@ -273,7 +344,7 @@ namespace Avalonia.Controls var hasResources = _inner?.Count > 0; - if (_mergedDictionaries is object) + if (_mergedDictionaries is not null) { foreach (var i in _mergedDictionaries) { @@ -281,6 +352,14 @@ namespace Avalonia.Controls hasResources |= i.HasResources; } } + if (_themeDictionary is not null) + { + foreach (var i in _themeDictionary.Values) + { + i.RemoveOwner(owner); + hasResources |= i.HasResources; + } + } if (hasResources) { diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 0bf1073098..4b0bab0c92 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -1,6 +1,4 @@ using System; -using Avalonia.Data.Converters; -using Avalonia.LogicalTree; using Avalonia.Reactive; using Avalonia.Styling; @@ -41,21 +39,66 @@ namespace Avalonia.Controls control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - IResourceNode? current = control; + return control.TryFindResource(key, null, out value); + } + + /// + /// Finds the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// Theme used to select theme dictionary. + /// The resource key. + /// The resource, or if not found. + public static object? FindResource(this IResourceHost control, ThemeVariant? theme, object key) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + if (control.TryFindResource(key, theme, out var value)) + { + return value; + } + + return AvaloniaProperty.UnsetValue; + } + + /// + /// Tries to the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// The resource key. + /// Theme used to select theme dictionary. + /// On return, contains the resource if found, otherwise null. + /// True if the resource was found; otherwise false. + public static bool TryFindResource(this IResourceHost control, object key, ThemeVariant? theme, out object? value) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + IResourceHost? current = control; while (current != null) { - if (current.TryGetResource(key, out value)) + if (current.TryGetResource(key, theme, out value)) { return true; } - current = (current as IStyleHost)?.StylingParent as IResourceNode; + current = (current as IStyleHost)?.StylingParent as IResourceHost; } value = null; return false; } + + /// + public static bool TryGetResource(this IResourceHost control, object key, out object? value) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return control.TryGetResource(key, null, out value); + } public static IObservable GetResourceObservable( this IResourceHost control, @@ -95,24 +138,49 @@ namespace Avalonia.Controls protected override void Initialize() { _target.ResourcesChanged += ResourcesChanged; + if (_target is StyledElement themeStyleable) + { + themeStyleable.PropertyChanged += PropertyChanged; + } } protected override void Deinitialize() { _target.ResourcesChanged -= ResourcesChanged; + if (_target is StyledElement themeStyleable) + { + themeStyleable.PropertyChanged -= PropertyChanged; + } } protected override void Subscribed(IObserver observer, bool first) { - observer.OnNext(Convert(_target.FindResource(_key))); + observer.OnNext(GetValue()); } private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) { - PublishNext(Convert(_target.FindResource(_key))); + PublishNext(GetValue()); + } + + private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == StyledElement.ActualThemeVariantProperty) + { + PublishNext(GetValue()); + } } - private object? Convert(object? value) => _converter?.Invoke(value) ?? value; + private object? GetValue() + { + if (_target is not StyledElement themeStyleable + || !_target.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + { + value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; + } + + return _converter?.Invoke(value) ?? value; + } } private class FloatingResourceObservable : LightweightObservableBase @@ -134,7 +202,7 @@ namespace Avalonia.Controls _target.OwnerChanged += OwnerChanged; _owner = _target.Owner; - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged += ResourcesChanged; } @@ -148,43 +216,68 @@ namespace Avalonia.Controls protected override void Subscribed(IObserver observer, bool first) { - if (_target.Owner is object) + if (_target.Owner is not null) { - observer.OnNext(Convert(_target.Owner.FindResource(_key))); + observer.OnNext(GetValue()); } } private void PublishNext() { - if (_target.Owner is object) + if (_target.Owner is not null) { - PublishNext(Convert(_target.Owner.FindResource(_key))); + PublishNext(GetValue()); } } private void OwnerChanged(object? sender, EventArgs e) { - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged -= ResourcesChanged; } + if (_owner is StyledElement styleable) + { + styleable.PropertyChanged += PropertyChanged; + } _owner = _target.Owner; - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged += ResourcesChanged; } + if (_owner is StyledElement styleable2) + { + styleable2.PropertyChanged += PropertyChanged; + } PublishNext(); } + private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == StyledElement.ActualThemeVariantProperty) + { + PublishNext(); + } + } + private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) { PublishNext(); } - private object? Convert(object? value) => _converter?.Invoke(value) ?? value; + private object? GetValue() + { + if (!(_target.Owner is StyledElement themeStyleable) + || !_target.Owner.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + { + value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; + } + + return _converter?.Invoke(value) ?? value; + } } } } diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 6043175eee..d23c585299 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -71,6 +71,23 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); + /// + /// Defines the property. + /// + public static readonly StyledProperty ActualThemeVariantProperty = + AvaloniaProperty.Register( + nameof(ThemeVariant), + inherits: true, + defaultValue: ThemeVariant.Light); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + AvaloniaProperty.Register( + nameof(ThemeVariant), + defaultValue: ThemeVariant.Default); + private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; @@ -257,6 +274,15 @@ namespace Avalonia set => SetValue(ThemeProperty, value); } + /// + /// Gets the UI theme that is currently used by the element, which might be different than the . + /// + /// + /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. + /// Otherwise, current OS theme variant is returned. + /// + public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); + /// /// Gets the styled element's logical children. /// @@ -439,11 +465,11 @@ namespace Avalonia void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); /// - bool IResourceNode.TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { value = null; - return (_resources?.TryGetResource(key, out value) ?? false) || - (_styles?.TryGetResource(key, out value) ?? false); + return (_resources?.TryGetResource(key, theme, out value) ?? false) || + (_styles?.TryGetResource(key, theme, out value) ?? false); } /// @@ -621,6 +647,13 @@ namespace Avalonia if (change.Property == ThemeProperty) OnControlThemeChanged(); + else if (change.Property == RequestedThemeVariantProperty) + { + if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) + SetValue(ActualThemeVariantProperty, themeVariant); + else + ClearValue(ActualThemeVariantProperty); + } } private protected virtual void OnControlThemeChanged() @@ -658,7 +691,7 @@ namespace Avalonia { var theme = Theme; - // Explitly set Theme property takes precedence. + // Explicitly set Theme property takes precedence. if (theme is not null) return theme; diff --git a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs new file mode 100644 index 0000000000..2467d99b3b --- /dev/null +++ b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia.Controls; +using Avalonia.Metadata; + +namespace Avalonia.Styling; + +/// +/// Interface for an application host element with a root theme variant. +/// +[Unstable] +public interface IGlobalThemeVariantProvider : IResourceHost +{ + /// + /// Gets the UI theme variant that is used by the control (and its child elements) for resource determination. + /// + ThemeVariant ActualThemeVariant { get; } + + /// + /// Raised when the theme variant is changed on the element or an ancestor of the element. + /// + event EventHandler? ActualThemeVariantChanged; +} diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index e8fc40ca4c..7dfa516bce 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -74,16 +74,16 @@ namespace Avalonia.Styling public event EventHandler? OwnerChanged; - public bool TryGetResource(object key, out object? result) + public bool TryGetResource(object key, ThemeVariant? themeVariant, out object? result) { - if (_resources is not null && _resources.TryGetResource(key, out result)) + if (_resources is not null && _resources.TryGetResource(key, themeVariant, out result)) return true; if (_children is not null) { for (var i = 0; i < _children.Count; ++i) { - if (_children[i].TryGetResource(key, out result)) + if (_children[i].TryGetResource(key, themeVariant, out result)) return true; } } diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 1b1886335f..5d5b1617aa 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -115,16 +115,16 @@ namespace Avalonia.Styling } /// - public bool TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { - if (_resources != null && _resources.TryGetResource(key, out value)) + if (_resources != null && _resources.TryGetResource(key, theme, out value)) { return true; } for (var i = Count - 1; i >= 0; --i) { - if (this[i].TryGetResource(key, out value)) + if (this[i].TryGetResource(key, theme, out value)) { return true; } diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs new file mode 100644 index 0000000000..d9cb123925 --- /dev/null +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -0,0 +1,65 @@ +using System; +using System.ComponentModel; +using System.Text; +using Avalonia.Platform; + +namespace Avalonia.Styling; + +[TypeConverter(typeof(ThemeVariantTypeConverter))] +public sealed record ThemeVariant(object Key) +{ + public ThemeVariant(object key, ThemeVariant? inheritVariant) + : this(key) + { + InheritVariant = inheritVariant; + } + + public static ThemeVariant Default { get; } = new(nameof(Default)); + public static ThemeVariant Light { get; } = new(nameof(Light)); + public static ThemeVariant Dark { get; } = new(nameof(Dark)); + + public ThemeVariant? InheritVariant { get; init; } + + public override string ToString() + { + return Key.ToString() ?? $"ThemeVariant {{ Key = {Key} }}"; + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + + public bool Equals(ThemeVariant? other) + { + return Key == other?.Key; + } + + public static ThemeVariant FromPlatformThemeVariant(PlatformThemeVariant themeVariant) + { + return themeVariant switch + { + PlatformThemeVariant.Light => Light, + PlatformThemeVariant.Dark => Dark, + _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null) + }; + } + + public PlatformThemeVariant? ToPlatformThemeVariant() + { + if (this == Light) + { + return PlatformThemeVariant.Light; + } + else if (this == Dark) + { + return PlatformThemeVariant.Dark; + } + else if (InheritVariant is { } inheritVariant) + { + return inheritVariant.ToPlatformThemeVariant(); + } + + return null; + } +} diff --git a/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs new file mode 100644 index 0000000000..4da1b495f5 --- /dev/null +++ b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Avalonia.Styling; + +public class ThemeVariantTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + return value switch + { + nameof(ThemeVariant.Light) => ThemeVariant.Light, + nameof(ThemeVariant.Dark) => ThemeVariant.Dark, + _ => new ThemeVariant(value) + }; + } +} diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 5b652cce19..58cc02e8c5 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -4,6 +4,7 @@ using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; @@ -28,7 +29,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IGlobalThemeVariantProvider, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -49,10 +50,22 @@ namespace Avalonia public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); + /// + public static readonly StyledProperty ActualThemeVariantProperty = + StyledElement.ActualThemeVariantProperty.AddOwner(); + + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + StyledElement.RequestedThemeVariantProperty.AddOwner(); + /// public event EventHandler? ResourcesChanged; - public event EventHandler? UrlsOpened; + /// + public event EventHandler? UrlsOpened; + + /// + public event EventHandler? ActualThemeVariantChanged; /// /// Creates an instance of the class. @@ -75,6 +88,19 @@ namespace Avalonia set { SetValue(DataContextProperty, value); } } + /// + public ThemeVariant? RequestedThemeVariant + { + get => GetValue(RequestedThemeVariantProperty); + set => SetValue(RequestedThemeVariantProperty, value); + } + + /// + public ThemeVariant ActualThemeVariant + { + get => GetValue(ActualThemeVariantProperty); + } + /// /// Gets the current instance of the class. /// @@ -191,11 +217,11 @@ namespace Avalonia public virtual void Initialize() { } /// - bool IResourceNode.TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { value = null; - return (_resources?.TryGetResource(key, out value) ?? false) || - Styles.TryGetResource(key, out value); + return (_resources?.TryGetResource(key, theme, out value) ?? false) || + Styles.TryGetResource(key, theme, out value); } void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) @@ -222,10 +248,15 @@ namespace Avalonia FocusManager = new FocusManager(); InputManager = new InputManager(); + var settings = AvaloniaLocator.Current.GetRequiredService(); + settings.ColorValuesChanged += OnColorValuesChanged; + OnColorValuesChanged(settings, settings.GetColorValues()); + AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(this) .Bind().ToConstant(this) + .Bind().ToConstant(this) .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() @@ -290,5 +321,26 @@ namespace Avalonia set => SetAndRaise(NameProperty, ref _name, value); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RequestedThemeVariantProperty) + { + if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) + SetValue(ActualThemeVariantProperty, themeVariant); + else + ClearValue(ActualThemeVariantProperty); + } + else if (change.Property == ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); + } + } + + private void OnColorValuesChanged(object? sender, PlatformColorValues e) + { + SetValue(ActualThemeVariantProperty, ThemeVariant.FromPlatformThemeVariant(e.ThemeVariant), BindingPriority.Template); + } } } diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs new file mode 100644 index 0000000000..b9724251c7 --- /dev/null +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -0,0 +1,23 @@ +using Avalonia.Styling; + +namespace Avalonia.Controls +{ + /// + /// Decorator control that isolates controls subtree with locally defined . + /// + public class ThemeVariantScope : Decorator + { + /// + /// 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. + /// + /// + /// Setting RequestedThemeVariant to will apply parent's actual theme variant on the current scope. + /// + public ThemeVariant? RequestedThemeVariant + { + get => GetValue(RequestedThemeVariantProperty); + set => SetValue(RequestedThemeVariantProperty, value); + } + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index c0265e28b9..e92b310057 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Notifications; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; @@ -96,6 +97,7 @@ namespace Avalonia.Controls private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; + private readonly IGlobalThemeVariantProvider? _applicationThemeHost; private readonly PointerOverPreProcessor? _pointerOverPreProcessor; private readonly IDisposable? _pointerOverPreProcessorSubscription; private readonly IDisposable? _backGestureSubscription; @@ -114,16 +116,6 @@ namespace Avalonia.Controls { KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); AffectsMeasure(ClientSizeProperty); - - TransparencyLevelHintProperty.Changed.AddClassHandler( - (tl, e) => - { - if (tl.PlatformImpl != null) - { - tl.PlatformImpl.SetTransparencyLevelHint((WindowTransparencyLevel)e.NewValue!); - tl.HandleTransparencyLevelChanged(tl.PlatformImpl.TransparencyLevel); - } - }); } /// @@ -161,6 +153,7 @@ namespace Avalonia.Controls _keyboardNavigationHandler = TryGetService(dependencyResolver); _renderInterface = TryGetService(dependencyResolver); _globalStyles = TryGetService(dependencyResolver); + _applicationThemeHost = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); @@ -191,6 +184,11 @@ namespace Avalonia.Controls _globalStyles.GlobalStylesAdded += ((IStyleHost)this).StylesAdded; _globalStyles.GlobalStylesRemoved += ((IStyleHost)this).StylesRemoved; } + if (_applicationThemeHost is { }) + { + SetValue(ActualThemeVariantProperty, _applicationThemeHost.ActualThemeVariant, BindingPriority.Template); + _applicationThemeHost.ActualThemeVariantChanged += GlobalActualThemeVariantChanged; + } ClientSize = impl.ClientSize; FrameSize = impl.FrameSize; @@ -315,6 +313,13 @@ namespace Avalonia.Controls set => SetValue(TransparencyBackgroundFallbackProperty, value); } + /// + public ThemeVariant? RequestedThemeVariant + { + get => GetValue(RequestedThemeVariantProperty); + set => SetValue(RequestedThemeVariantProperty, value); + } + /// /// Occurs when physical Back Button is pressed or a back navigation has been requested. /// @@ -413,6 +418,24 @@ namespace Avalonia.Controls return visual == null ? null : visual.VisualRoot as TopLevel; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == TransparencyLevelHintProperty) + { + if (PlatformImpl != null) + { + PlatformImpl.SetTransparencyLevelHint(change.GetNewValue()); + HandleTransparencyLevelChanged(PlatformImpl.TransparencyLevel); + } + } + else if (change.Property == ActualThemeVariantProperty) + { + PlatformImpl?.SetFrameThemeVariant(change.GetNewValue().ToPlatformThemeVariant() ?? PlatformThemeVariant.Light); + } + } + /// /// Creates the layout manager for this . /// @@ -437,6 +460,10 @@ namespace Avalonia.Controls _globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded; _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved; } + if (_applicationThemeHost is { }) + { + _applicationThemeHost.ActualThemeVariantChanged -= GlobalActualThemeVariantChanged; + } Renderer?.Dispose(); Renderer = null!; @@ -589,6 +616,11 @@ namespace Avalonia.Controls _inputManager?.ProcessInput(e); } + private void GlobalActualThemeVariantChanged(object? sender, EventArgs e) + { + SetValue(ActualThemeVariantProperty, ((IGlobalThemeVariantProvider)sender!).ActualThemeVariant, BindingPriority.Template); + } + private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) { _pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index 00173dbb35..7426c4e2ed 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -1,19 +1,22 @@ using System; using Avalonia.Controls; +using Avalonia.Styling; using Lifetimes = Avalonia.Controls.ApplicationLifetimes; -using App = Avalonia.Application; namespace Avalonia.Diagnostics.Controls { class Application : AvaloniaObject - , Input.ICloseable + , Input.ICloseable, IDisposable { - private readonly App _application; + private readonly Avalonia.Application _application; public event EventHandler? Closed; - public Application(App application) + public static readonly StyledProperty RequestedThemeVariantProperty = + StyledElement.RequestedThemeVariantProperty.AddOwner(); + + public Application(Avalonia.Application application) { _application = application; @@ -33,9 +36,12 @@ namespace Avalonia.Diagnostics.Controls Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as Visual)?.VisualRoot?.Renderer, _ => null }; + + RequestedThemeVariant = application.RequestedThemeVariant; + _application.PropertyChanged += ApplicationOnPropertyChanged; } - internal App Instance => _application; + internal Avalonia.Application Instance => _application; /// /// Defines the property. @@ -114,5 +120,35 @@ namespace Avalonia.Diagnostics.Controls /// Gets the root of the visual tree, if the control is attached to a visual tree. /// internal Rendering.IRenderer? RendererRoot { get; } + + /// + public ThemeVariant? RequestedThemeVariant + { + get => GetValue(RequestedThemeVariantProperty); + set => SetValue(RequestedThemeVariantProperty, value); + } + + public void Dispose() + { + _application.PropertyChanged -= ApplicationOnPropertyChanged; + } + + private void ApplicationOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == Avalonia.Application.RequestedThemeVariantProperty) + { + RequestedThemeVariant = e.GetNewValue(); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RequestedThemeVariantProperty) + { + _application.RequestedThemeVariant = change.GetNewValue(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index 5fc274a4e9..3cfb0246eb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -1,4 +1,6 @@ -using Avalonia.Input; +using System; +using Avalonia.Input; +using Avalonia.Styling; namespace Avalonia.Diagnostics { @@ -42,8 +44,8 @@ namespace Avalonia.Diagnostics = Conventions.DefaultScreenshotHandler; /// - /// Gets or sets whether DevTools should use the dark mode theme + /// Gets or sets whether DevTools theme. /// - public bool UseDarkMode { get; set; } + public ThemeVariant? ThemeVariant { get; set; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 8bff9ccde0..1951914273 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -123,7 +123,8 @@ namespace Avalonia.Diagnostics.ViewModels private static (object resourceKey, bool isDynamic)? GetResourceInfo(object? value) { - if (value is StaticResourceExtension staticResource) + if (value is StaticResourceExtension staticResource + && staticResource.ResourceKey != null) { return (staticResource.ResourceKey, false); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 6c1da3ec00..748c2cc313 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -9,9 +9,9 @@ - + - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index dbc4c98f78..4768c88f75 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -263,13 +263,9 @@ namespace Avalonia.Diagnostics.Views public void SetOptions(DevToolsOptions options) { (DataContext as MainViewModel)?.SetOptions(options); - - if (options.UseDarkMode) + if (options.ThemeVariant is { } themeVariant) { - if (Styles[0] is SimpleTheme st) - { - st.Mode = SimpleThemeMode.Dark; - } + RequestedThemeVariant = themeVariant; } } diff --git a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs index a492dfed3a..e46b9276fc 100644 --- a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs +++ b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs @@ -9,7 +9,7 @@ namespace Avalonia.Dialogs.Internal { public object Convert(object key, Type targetType, object parameter, CultureInfo culture) { - TryGetResource((string)key, out var value); + TryGetResource((string)key, null, out var value); return value; } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 84b4f3bdba..cdd344becc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Markup.Data; @@ -7,6 +8,8 @@ using Avalonia.Markup.Xaml.Converters; using Avalonia.Markup.Xaml.XamlIl.Runtime; using Avalonia.Styling; +#nullable enable + namespace Avalonia.Markup.Xaml.MarkupExtensions { public class StaticResourceExtension @@ -20,12 +23,18 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions ResourceKey = resourceKey; } - public object ResourceKey { get; set; } + public object? ResourceKey { get; set; } public object ProvideValue(IServiceProvider serviceProvider) { + if (ResourceKey is not { } resourceKey) + { + throw new ArgumentException("StaticResourceExtension.ResourceKey must be set."); + } + var stack = serviceProvider.GetService(); var provideTarget = serviceProvider.GetService(); + var themeVariant = (provideTarget.TargetObject as StyledElement)?.ActualThemeVariant; var targetType = provideTarget.TargetProperty switch { @@ -36,14 +45,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (provideTarget.TargetObject is Setter { Property: not null } setter) { - targetType = setter.Property.PropertyType; + targetType = setter.Property?.PropertyType; } // Look upwards though the ambient context for IResourceNodes // which might be able to give us the resource. foreach (var parent in stack.Parents) { - if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value)) + if (parent is IResourceNode node && node.TryGetResource(resourceKey, themeVariant, out var value)) { return ColorToBrushConverter.Convert(value, targetType); } @@ -60,12 +69,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions return AvaloniaProperty.UnsetValue; } - throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found."); + throw new KeyNotFoundException($"Static resource '{resourceKey}' not found."); } - private object GetValue(StyledElement control, Type targetType) + private object GetValue(StyledElement control, Type? targetType) { - return ColorToBrushConverter.Convert(control.FindResource(ResourceKey), targetType); + return ColorToBrushConverter.Convert(control.FindResource(ResourceKey!), targetType); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index 595b37f7d1..4ff105cf1f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; +using Avalonia.Styling; #nullable enable @@ -74,11 +75,11 @@ namespace Avalonia.Markup.Xaml.Styling remove => Loaded.OwnerChanged -= value; } - bool IResourceNode.TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { if (!_isLoading) { - return Loaded.TryGetResource(key, out value); + return Loaded.TryGetResource(key, theme, out value); } value = null; diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index b87aa64297..27367fce5e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -91,11 +91,11 @@ namespace Avalonia.Markup.Xaml.Styling } } - public bool TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { if (!_isLoading) { - return Loaded.TryGetResource(key, out value); + return Loaded.TryGetResource(key, theme, out value); } value = null; From bd9c9783ab1b6110b26db8bf29ce9f044da71c64 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:05:40 -0500 Subject: [PATCH 03/11] Parse ThemeVariant compile time and merge ThemeDictionaries --- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 20 ++++ .../XamlMergeResourceGroupTransformer.cs | 100 ++++++++++++++++-- .../AvaloniaXamlIlWellKnownTypes.cs | 4 + 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 925bf0a4fa..365a07a7f6 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -291,6 +291,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } + if (type.Equals(types.ThemeVariant)) + { + var variantText = text.Trim(); + var foundConstProperty = types.ThemeVariant.Properties.FirstOrDefault(p => + p.Name == variantText && p.PropertyType == types.ThemeVariant); + var themeVariantTypeRef = new XamlAstClrTypeReference(node, types.ThemeVariant, false); + if (foundConstProperty is not null) + { + result = new XamlStaticExtensionNode(new XamlAstObjectNode(node, node.Type), themeVariantTypeRef, foundConstProperty.Name); + return true; + } + + result = new XamlAstNewClrObjectNode(node, themeVariantTypeRef, types.ThemeVariantConstructor, + new List() + { + new XamlConstantNode(node, context.Configuration.WellKnownTypes.String, variantText) + }); + return true; + } + result = null; return false; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs index 8c83c74248..db8d604154 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -4,6 +4,8 @@ using System.Linq; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; using XamlX.Ast; using XamlX.IL.Emitters; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; #nullable enable @@ -72,14 +74,20 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer } } - var manipulationGroup = new XamlManipulationGroupNode(node, new List()); + if (!mergeSourceNodes.Any()) + { + return node; + } + + var manipulationGroup = new List(); foreach (var sourceNode in mergeSourceNodes) { var (originalAssetPath, propertyNode) = AvaloniaXamlIncludeTransformer.ResolveSourceFromXamlInclude(context, "MergeResourceInclude", sourceNode, true); if (originalAssetPath is null) { - return node; + return context.ParseError( + $"Node MergeResourceInclude is unable to resolve \"{originalAssetPath}\" path.", propertyNode, node); } var targetDocument = context.Documents.FirstOrDefault(d => @@ -99,15 +107,95 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer $"MergeResourceInclude can only include another ResourceDictionary", propertyNode, node); } - manipulationGroup.Children.Add(singleRootObject.Manipulation); + manipulationGroup.Add(singleRootObject.Manipulation); } + + // Order of resources is defined by ResourceDictionary.TryGetResource. + // It is read by following priority: + // - own resources. + // - own theme dictionaries. + // - merged dictionaries. + // We need to maintain this order when we inject "compiled merged" resources. + // Doing this by injecting merged dictionaries in the beginning, so it can be overwritten by "own resources". + // MergedDictionaries are read first, so we need ot inject our merged values in the beginning. + var children = resourceDictionaryManipulation.Children; + children.InsertRange(0, manipulationGroup); - if (manipulationGroup.Children.Any()) + // Flatten resource assignments. + for (var i = 0; i < children.Count; i++) { - // MergedDictionaries are read first, so we need ot inject our merged values in the beginning. - resourceDictionaryManipulation.Children.Insert(0, manipulationGroup); + if (children[i] is XamlManipulationGroupNode group) + { + children.RemoveAt(i); + children.AddRange(group.Children); + i--; // step back, so new items can be reiterated. + } + } + + // Merge "ThemeDictionaries" as well. + for (var i = children.Count - 1; i >= 0; i--) + { + if (children[i] is XamlPropertyAssignmentNode assignmentNode + && assignmentNode.Property.Name == "ThemeDictionaries" + && assignmentNode.Values.Count == 2 + && assignmentNode.Values[0] is {} key + && assignmentNode.Values[1] is XamlValueWithManipulationNode + { + Manipulation: XamlObjectInitializationNode + { + Manipulation: XamlManipulationGroupNode valueGroup + } + }) + { + for (var j = i - 1; j >= 0; j--) + { + if (children[j] is XamlPropertyAssignmentNode sameKeyPrevAssignmentNode + && sameKeyPrevAssignmentNode.Property.Name == "ThemeDictionaries" + && sameKeyPrevAssignmentNode.Values.Count == 2 + && sameKeyPrevAssignmentNode.Values[1] is XamlValueWithManipulationNode + { + Manipulation: XamlObjectInitializationNode + { + Manipulation: XamlManipulationGroupNode sameKeyPrevValueGroup + } + } + && ThemeVariantNodeEquals(context, key, sameKeyPrevAssignmentNode.Values[0])) + { + sameKeyPrevValueGroup.Children.AddRange(valueGroup.Children); + children.RemoveAt(i); + break; + } + } + } } return node; } + + public static bool ThemeVariantNodeEquals(AstGroupTransformationContext context, IXamlAstValueNode left, IXamlAstValueNode right) + { + if (left is XamlConstantNode leftConst + && right is XamlConstantNode rightConst) + { + return leftConst.Constant == rightConst.Constant; + } + if (left is XamlStaticExtensionNode leftStaticExt + && right is XamlStaticExtensionNode rightStaticExt) + { + return leftStaticExt.Type.GetClrType().GetFullName() == rightStaticExt.Type.GetClrType().GetFullName() + && leftStaticExt.Member == rightStaticExt.Member; + } + if (left is XamlAstNewClrObjectNode leftClrObjectNode + && right is XamlAstNewClrObjectNode rightClrObjectNode) + { + var themeVariant = context.GetAvaloniaTypes().ThemeVariant; + return leftClrObjectNode.Type.GetClrType() == themeVariant + && leftClrObjectNode.Type == rightClrObjectNode.Type + && leftClrObjectNode.Constructor == rightClrObjectNode.Constructor + && ThemeVariantNodeEquals(context, leftClrObjectNode.Arguments.Single(), + leftClrObjectNode.Arguments.Single()); + } + + return false; + } } 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 aab6239a35..f9e14a7641 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -67,6 +67,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlConstructor FontFamilyConstructorUriName { get; } public IXamlType Thickness { get; } public IXamlConstructor ThicknessFullConstructor { get; } + public IXamlType ThemeVariant { get; } + public IXamlConstructor ThemeVariantConstructor { get; } public IXamlType Point { get; } public IXamlConstructor PointFullConstructor { get; } public IXamlType Vector { get; } @@ -188,6 +190,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Uri = cfg.TypeSystem.GetType("System.Uri"); FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily"); FontFamilyConstructorUriName = FontFamily.GetConstructor(new List { Uri, XamlIlTypes.String }); + ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant"); + ThemeVariantConstructor = ThemeVariant.GetConstructor(new List { XamlIlTypes.String }); (IXamlType, IXamlConstructor) GetNumericTypeInfo(string name, IXamlType componentType, int componentCount) { From 1d69936c7946a0d9df7cc5c014ec0a5b4093587c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:06:00 -0500 Subject: [PATCH 04/11] Update default themes, use ThemeDictionaries --- src/Avalonia.Themes.Fluent/Accents/Base.xaml | 511 +++++- .../Accents/BaseDark.xaml | 178 -- .../Accents/BaseLight.xaml | 181 -- .../Accents/FluentControlResources.xaml | 1551 +++++++++++++++++ .../Accents/FluentControlResourcesDark.xaml | 643 ------- .../Accents/FluentControlResourcesLight.xaml | 638 ------- .../Controls/FluentControls.xaml | 1 + .../Controls/ThemeVariantScope.xaml | 9 + src/Avalonia.Themes.Fluent/FluentTheme.xaml | 5 +- .../FluentTheme.xaml.cs | 53 +- src/Avalonia.Themes.Simple/Accents/Base.xaml | 185 +- .../Accents/BaseDark.xaml | 38 - .../Accents/BaseLight.xaml | 37 - .../Controls/SimpleControls.xaml | 1 + .../Controls/ThemeVariantScope.xaml | 8 + src/Avalonia.Themes.Simple/SimpleTheme.xaml | 6 +- .../SimpleTheme.xaml.cs | 70 +- src/Avalonia.Themes.Simple/SimpleThemeMode.cs | 8 - 18 files changed, 2187 insertions(+), 1936 deletions(-) delete mode 100644 src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml delete mode 100644 src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml delete mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml delete mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml create mode 100644 src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml delete mode 100644 src/Avalonia.Themes.Simple/Accents/BaseDark.xaml delete mode 100644 src/Avalonia.Themes.Simple/Accents/BaseLight.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml delete mode 100644 src/Avalonia.Themes.Simple/SimpleThemeMode.cs diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 479bcd8531..7512fa4cfa 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -2,38 +2,481 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="using:System" xmlns:converters="using:Avalonia.Controls.Converters"> - - - #FFF0F0F0 - #FF000000 - #FF6D6D6D - #FF3399FF - #FFFFFFFF - #FF0066CC - #FFFFFFFF - #FF000000 - avares://Avalonia.Themes.Fluent/Assets#Inter - 14 - - - True - 1 - 2 - 10,6,6,5 - 20 - 20 - 8,5,8,6 - - - 3 - 5 - - - scaleX(0.125) translateX(-2px) - scaleY(0.125) translateY(-2px) - - - - - + + avares://Avalonia.Themes.Fluent/Assets#Inter + 14 + + + True + 1 + 2 + 10,6,6,5 + 20 + 20 + 8,5,8,6 + + + 3 + 5 + + + scaleX(0.125) translateX(-2px) + scaleY(0.125) translateY(-2px) + + + + + + + + + + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FF171717 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FFCCCCCC + #FF7A7A7A + #FFCCCCCC + #FFF2F2F2 + #FFE6E6E6 + #FFF2F2F2 + #FFFFFFFF + #FF767676 + #19000000 + #33000000 + #C50500 + + #17000000 + #2E000000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #FFFFFFFF + + + + 374 + 0,2,0,2 + 1 + -1,0,-1,0 + 32 + 64 + 456 + 0 + 1 + 0 + + 12,11,12,12 + 96 + 40 + 758 + + + 0 + + + 0,4,0,4 + + + 12,0,12,0 + + + + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FFF2F2F2 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FF333333 + #FF858585 + #FF767676 + #FF171717 + #FF1F1F1F + #FF2B2B2B + #FFFFFFFF + #FF767676 + #19FFFFFF + #33FFFFFF + #FFF000 + + #18FFFFFF + #30FFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #FF000000 + + + 374 + 0,2,0,2 + 1 + -1,0,-1,0 + 32 + 64 + 456 + 0 + 1 + 0 + + 12,11,12,12 + 96 + 40 + 758 + + + 0 + + + 0,4,0,4 + + + 12,0,12,0 + + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml deleted file mode 100644 index 0192fb1b54..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ /dev/null @@ -1,178 +0,0 @@ - - - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FFF2F2F2 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FF333333 - #FF858585 - #FF767676 - #FF171717 - #FF1F1F1F - #FF2B2B2B - #FFFFFFFF - #FF767676 - #19FFFFFF - #33FFFFFF - #FFF000 - - #18FFFFFF - #30FFFFFF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #FF000000 - - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 - diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml deleted file mode 100644 index a9e5ed949a..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ /dev/null @@ -1,181 +0,0 @@ - - - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FF171717 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FFCCCCCC - #FF7A7A7A - #FFCCCCCC - #FFF2F2F2 - #FFE6E6E6 - #FFF2F2F2 - #FFFFFFFF - #FF767676 - #19000000 - #33000000 - #C50500 - - #17000000 - #2E000000 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #FFFFFFFF - - - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml new file mode 100644 index 0000000000..a9bc622221 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml @@ -0,0 +1,1551 @@ + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 64 + 1 + 1 + 11,5,11,7 + Normal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0,4,0,4 + + + 0 + + + 4 + 0 + + + 1 + 32 + 0,0 + 12,0,0,0 + 12,4,12,4 + + + + + + + + + + + + + + + + + + 11,9,11,10 + 11,4,11,7 + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4 + 2 + 0 + + + + + + + + + + + + + + + + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12 + 1 + + + + 8,5,8,7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 24 + 12,0,12,0 + 12,0,12,0 + SemiLight + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + 16 + 8 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 64 + 1 + 1 + 11,5,11,7 + Normal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0,4,0,4 + + + 0 + + + 4 + 0 + + + 1 + 32 + 0,0 + 12,0,0,0 + 12,4,12,4 + + + + + + + + + + + + + + + + + + 11,9,11,10 + 11,4,11,7 + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4 + 2 + 0 + + + + + + + + + + + + + + + + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12 + 1 + + + + 8,5,8,7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 24 + 12,0,12,0 + 12,0,12,0 + SemiLight + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + 16 + 8 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml deleted file mode 100644 index 810065fc9b..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ /dev/null @@ -1,643 +0,0 @@ - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 64 - 1 - 1 - 11,5,11,7 - Normal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0,4,0,4 - - - 0 - - - 4 - 0 - - - 1 - 32 - 0,0 - 12,0,0,0 - 12,4,12,4 - - - - - - - - - - - - - - - - - - 11,9,11,10 - 11,4,11,7 - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4 - 2 - 0 - - - - - - - - - - - - - - - - - - - - - - 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 12 - 1 - - - - 8,5,8,7 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 - 12,0,12,0 - 12,0,12,0 - SemiLight - - - - - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - 16 - 8 - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 32 - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml deleted file mode 100644 index bccc47b9b8..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ /dev/null @@ -1,638 +0,0 @@ - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 64 - 1 - 1 - 11,5,11,7 - Normal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0,4,0,4 - - - 0 - - - 4 - 0 - - - 1 - 32 - 0,0 - 12,0,0,0 - 12,4,12,4 - - - - - - - - - - - - - - - - - - 11,9,11,10 - 11,4,11,7 - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4 - 2 - 0 - - - - - - - - - - - - - - - - - - - - - - 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 12 - 1 - - - - 8,5,8,7 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 - 12,0,12,0 - 12,0,12,0 - SemiLight - - - - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - 16 - 8 - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 32 - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 2c3550a72f..532b0cff1b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -69,6 +69,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml b/src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml new file mode 100644 index 0000000000..21a5506b88 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 44ca60e2fa..e83257fd9f 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -6,13 +6,10 @@ + - - - - diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index a8297953a8..95539bc08a 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -6,12 +6,6 @@ using Avalonia.Styling; namespace Avalonia.Themes.Fluent { - public enum FluentThemeMode - { - Light, - Dark, - } - public enum DensityStyle { Normal, @@ -23,10 +17,6 @@ namespace Avalonia.Themes.Fluent /// public class FluentTheme : Styles { - private readonly IResourceDictionary _baseDark; - private readonly IResourceDictionary _fluentDark; - private readonly IResourceDictionary _baseLight; - private readonly IResourceDictionary _fluentLight; private readonly Styles _compactStyles; /// @@ -37,13 +27,8 @@ namespace Avalonia.Themes.Fluent { AvaloniaXamlLoader.Load(sp, this); - _baseDark = (IResourceDictionary)GetAndRemove("BaseDark"); - _fluentDark = (IResourceDictionary)GetAndRemove("FluentDark"); - _baseLight = (IResourceDictionary)GetAndRemove("BaseLight"); - _fluentLight = (IResourceDictionary)GetAndRemove("FluentLight"); _compactStyles = (Styles)GetAndRemove("CompactStyles"); - - EnsureThemeVariants(); + EnsureCompactStyles(); object GetAndRemove(string key) @@ -54,22 +39,10 @@ namespace Avalonia.Themes.Fluent return val; } } - - public static readonly StyledProperty ModeProperty = - AvaloniaProperty.Register(nameof(Mode)); - + public static readonly StyledProperty DensityStyleProperty = AvaloniaProperty.Register(nameof(DensityStyle)); - /// - /// Gets or sets the mode of the fluent theme (light, dark). - /// - public FluentThemeMode Mode - { - get => GetValue(ModeProperty); - set => SetValue(ModeProperty, value); - } - /// /// Gets or sets the density style of the fluent theme (normal, compact). /// @@ -82,11 +55,6 @@ namespace Avalonia.Themes.Fluent protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - - if (change.Property == ModeProperty) - { - EnsureThemeVariants(); - } if (change.Property == DensityStyleProperty) { @@ -94,23 +62,6 @@ namespace Avalonia.Themes.Fluent } } - private void EnsureThemeVariants() - { - var themeVariantResource1 = Mode == FluentThemeMode.Dark ? _baseDark : _baseLight; - var themeVariantResource2 = Mode == FluentThemeMode.Dark ? _fluentDark : _fluentLight; - var dict = Resources.MergedDictionaries; - if (dict.Count == 0) - { - dict.Add(themeVariantResource1); - dict.Add(themeVariantResource2); - } - else - { - dict[0] = themeVariantResource1; - dict[1] = themeVariantResource2; - } - } - private void EnsureCompactStyles() { if (DensityStyle == DensityStyle.Compact) diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index bffdbd8a27..0c1354e475 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -1,62 +1,131 @@ - - #CC119EDA - #99119EDA - #66119EDA - #33119EDA - #FF808080 - #FFFFFFFF - #FFFF0000 - #10FF0000 - - - - - - - - - - - - - - - - - 1 - 0.5 + + + + #FFFFFFFF + #FFAAAAAA + #FF888888 + #FF333333 + #FF868999 + #FFF5F5F5 + #FFC2C3C9 + #FF686868 + #FF5B5B5B + #FFF0F0F0 + #FFD0D0D0 + #FF808080 + #FF000000 + #FF086F9E - 10 - 12 - 16 + + + + + + + + + + + + + - 18 - 8 + + + + + + #FF282828 + #FF505050 + #FF808080 + #FFA0A0A0 + #FF282828 + #FF505050 + #FF686868 + #FF808080 + #FFEFEBEF + #FFA8A8A8 + #FF828282 + #FF505050 + #FFDEDEDE + #FF119EDA - 20 - 20 + + + + + + + + + + + + + + + + + + + + #CC119EDA + #99119EDA + #66119EDA + #33119EDA + #FF808080 + #FFFFFFFF + #FFFF0000 + #10FF0000 + + + + + + + + + + + + + + + + + 1 + 0.5 + + 10 + 12 + 16 + + 18 + 8 + + 20 + 20 diff --git a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml deleted file mode 100644 index 88c2681f65..0000000000 --- a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml +++ /dev/null @@ -1,38 +0,0 @@ - - - #FF282828 - #FF505050 - #FF808080 - #FFA0A0A0 - #FF282828 - #FF505050 - #FF686868 - #FF808080 - #FFEFEBEF - #FFA8A8A8 - #FF828282 - #FF505050 - #FFDEDEDE - #FF119EDA - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml deleted file mode 100644 index 77166a9d8a..0000000000 --- a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml +++ /dev/null @@ -1,37 +0,0 @@ - - - #FFFFFFFF - #FFAAAAAA - #FF888888 - #FF333333 - #FF868999 - #FFF5F5F5 - #FFC2C3C9 - #FF686868 - #FF5B5B5B - #FFF0F0F0 - #FFD0D0D0 - #FF808080 - #FF000000 - #FF086F9E - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 093adaeab2..479db9ed09 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -67,6 +67,7 @@ + diff --git a/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml b/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml new file mode 100644 index 0000000000..a6022fb263 --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml b/src/Avalonia.Themes.Simple/SimpleTheme.xaml index 5b0cae7fd2..f6d6ddfec9 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml @@ -4,12 +4,8 @@ - + - - - - diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs index 42dfafd7e0..31b3243993 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs @@ -4,68 +4,16 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Styling; -namespace Avalonia.Themes.Simple +namespace Avalonia.Themes.Simple; + +public class SimpleTheme : Styles { - public class SimpleTheme : Styles + /// + /// Initializes a new instance of the class. + /// + /// The parent's service provider. + public SimpleTheme(IServiceProvider? sp = null) { - public static readonly StyledProperty ModeProperty = - AvaloniaProperty.Register(nameof(Mode)); - - private readonly IResourceDictionary _simpleDark; - private readonly IResourceDictionary _simpleLight; - - /// - /// Initializes a new instance of the class. - /// - /// The parent's service provider. - public SimpleTheme(IServiceProvider? sp = null) - { - AvaloniaXamlLoader.Load(sp, this); - - _simpleDark = (IResourceDictionary)GetAndRemove("BaseDark"); - _simpleLight = (IResourceDictionary)GetAndRemove("BaseLight"); - EnsureThemeVariant(); - - object GetAndRemove(string key) - { - var val = Resources[key] - ?? throw new KeyNotFoundException($"Key {key} was not found in the resources"); - Resources.Remove(key); - return val; - } - } - - /// - /// Gets or sets the mode of the fluent theme (light, dark). - /// - public SimpleThemeMode Mode - { - get => GetValue(ModeProperty); - set => SetValue(ModeProperty, value); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == ModeProperty) - { - EnsureThemeVariant(); - } - } - - private void EnsureThemeVariant() - { - var themeVariantResource = Mode == SimpleThemeMode.Dark ? _simpleDark : _simpleLight; - var dict = Resources.MergedDictionaries; - if (dict.Count == 0) - { - dict.Add(themeVariantResource); - } - else - { - dict[0] = themeVariantResource; - } - } + AvaloniaXamlLoader.Load(sp, this); } } diff --git a/src/Avalonia.Themes.Simple/SimpleThemeMode.cs b/src/Avalonia.Themes.Simple/SimpleThemeMode.cs deleted file mode 100644 index 683c751f10..0000000000 --- a/src/Avalonia.Themes.Simple/SimpleThemeMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Avalonia.Themes.Simple -{ - public enum SimpleThemeMode - { - Light, - Dark - } -} From be22b361c84c022464a30a0ff963c96f04270df4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:12:52 -0500 Subject: [PATCH 05/11] Add theme variants specific tests --- .../Styling/ResourceDictionaryTests.cs | 8 +- .../Styling/StylesTests.cs | 2 +- .../Styling/ResourceBenchmarks.cs | 2 +- tests/Avalonia.Benchmarks/TestStyles.cs | 19 +- .../AvaloniaPropertyConverterTest.cs | 6 + .../DynamicResourceExtensionTests.cs | 2 +- .../ThemeDictionariesTests.cs | 444 ++++++++++++++++++ .../Xaml/BasicTests.cs | 4 +- .../Xaml/MergeResourceIncludeTests.cs | 103 ++++ .../Xaml/ResourceDictionaryTests.cs | 32 ++ tests/Avalonia.UnitTests/TestServices.cs | 2 +- 11 files changed, 611 insertions(+), 13 deletions(-) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs diff --git a/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs index 86b1b897d4..5527eda6ee 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs @@ -29,7 +29,7 @@ namespace Avalonia.Base.UnitTests.Styling { "foo", "bar" }, }; - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", null, out var result)); Assert.Equal("bar", result); } @@ -47,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Styling } }; - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", null, out var result)); Assert.Equal("bar", result); } @@ -64,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Styling { "foo", "baz" }, }); - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", null, out var result)); Assert.Equal("bar", result); } @@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.Styling } }; - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", null, out var result)); Assert.Equal("baz", result); } diff --git a/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs index a6777c9466..c9fc86e205 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs @@ -108,7 +108,7 @@ namespace Avalonia.Base.UnitTests.Styling } }; - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", ThemeVariant.Dark, out var result)); Assert.Equal("bar", result); } } diff --git a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs index 59953f457a..bc47e68bc1 100644 --- a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs +++ b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs @@ -44,7 +44,7 @@ namespace Avalonia.Benchmarks.Styling return new Styles { preHost, - new TestStyles(50, 3, 5), + new TestStyles(50, 3, 5, 0), postHost }; } diff --git a/tests/Avalonia.Benchmarks/TestStyles.cs b/tests/Avalonia.Benchmarks/TestStyles.cs index be2ad7d072..208f238101 100644 --- a/tests/Avalonia.Benchmarks/TestStyles.cs +++ b/tests/Avalonia.Benchmarks/TestStyles.cs @@ -1,10 +1,11 @@ -using Avalonia.Styling; +using Avalonia.Controls; +using Avalonia.Styling; namespace Avalonia.Benchmarks { public class TestStyles : Styles { - public TestStyles(int childStylesCount, int childInnerStyleCount, int childResourceCount) + public TestStyles(int childStylesCount, int childInnerStyleCount, int childResourceCount, int childThemeResourcesCount) { for (int i = 0; i < childStylesCount; i++) { @@ -18,7 +19,19 @@ namespace Avalonia.Benchmarks { childStyle.Resources.Add($"resource.{i}.{j}.{k}", null); } - + + if (childThemeResourcesCount > 0) + { + ResourceDictionary darkTheme, lightTheme; + childStyle.Resources.ThemeDictionaries[ThemeVariant.Dark] = darkTheme = new ResourceDictionary(); + childStyle.Resources.ThemeDictionaries[ThemeVariant.Light] = lightTheme = new ResourceDictionary(); + for (int k = 0; k < childThemeResourcesCount; k++) + { + darkTheme.Add($"resource.theme.{i}.{j}.{k}", null); + lightTheme.Add($"resource.theme.{i}.{j}.{k}", null); + } + } + childStyles.Add(childStyle); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index 9c2860eb26..d4d188f584 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -142,6 +142,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters 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/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index f2e1a99006..535b96420a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -938,7 +938,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public void AddOwner(IResourceHost owner) => Owner = owner; public void RemoveOwner(IResourceHost owner) => Owner = null; - public bool TryGetResource(object key, out object value) + public bool TryGetResource(object key, ThemeVariant themeVariant, out object value) { RequestedResources.Add(key); value = key; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs new file mode 100644 index 0000000000..56040c2186 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs @@ -0,0 +1,444 @@ +using Avalonia.Controls; +using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Media; +using Avalonia.Styling; +using Moq; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests; + +public class ThemeDictionariesTests : XamlTestBase +{ + [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); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + 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 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(Skip = "Not implemented")] + 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_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 = new ThemeVariant("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); + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 0cdc9ee3b1..c8be1c6d19 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -448,13 +448,13 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.True(style.Resources.Count > 0); - style.TryGetResource("Brush", out var brush); + style.TryGetResource("Brush", null, out var brush); Assert.NotNull(brush); Assert.IsAssignableFrom(brush); Assert.Equal(Colors.White, ((ISolidColorBrush)brush).Color); - style.TryGetResource("Double", out var d); + style.TryGetResource("Double", null, out var d); Assert.Equal(10.0, d); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs index 92807b2cb9..aa76756069 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using System.Xml; using Avalonia.Controls; @@ -128,4 +130,105 @@ public class MergeResourceIncludeTests Assert.Equal(Colors.Black, ((ISolidColorBrush)resources["brush5"]!).Color); Assert.Equal(Colors.White, ((ISolidColorBrush)resources["brush6"]!).Color); } + + [Fact] + public void MergeResourceInclude_Works_With_ThemeDictionaries() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + + + White + Black + + + Black + White + + +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + + + Red + Blue + + + Blue + Red + + +"), + new RuntimeXamlLoaderDocument(@" + + + + + +"), + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var resources = Assert.IsType(objects[2]); + Assert.Empty(resources.MergedDictionaries); + + Assert.Equal(Colors.White, Get("brush1", ThemeVariant.Light).Color); + Assert.Equal(Colors.Black, Get("brush2", ThemeVariant.Light).Color); + Assert.Equal(Colors.Black, Get("brush1", ThemeVariant.Dark).Color); + Assert.Equal(Colors.White, Get("brush2", ThemeVariant.Dark).Color); + + Assert.Equal(Colors.Red, Get("brush3", ThemeVariant.Light).Color); + Assert.Equal(Colors.Blue, Get("brush4", ThemeVariant.Light).Color); + Assert.Equal(Colors.Blue, Get("brush3", ThemeVariant.Dark).Color); + Assert.Equal(Colors.Red, Get("brush4", ThemeVariant.Dark).Color); + + ISolidColorBrush Get(string key, ThemeVariant themeVariant) + { + return resources.TryGetResource(key, themeVariant, out var res) ? + (ISolidColorBrush)res! : + throw new KeyNotFoundException(); + } + } + + [Fact] + public void MergeResourceInclude_Fails_With_ThemeDictionaries_Duplicate_Resources() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + + + White + + +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + + + Black + + +"), + new RuntimeXamlLoaderDocument(@" + + + + + +"), + }; + + Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); + } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index d74d85e2bc..6cab83751f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -276,6 +276,38 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Closest_Resource_Should_Be_Referenced() + { + using (StyledWindow()) + { + var xaml = @" + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var windowResources = (ResourceDictionary)window.Resources; + var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources; + + var brush = Assert.IsType(windowResources["Red2"]); + Assert.Equal(Colors.Red, brush.Color); + + Assert.False(windowResources.ContainsDeferredKey("Red")); + Assert.False(windowResources.ContainsDeferredKey("Red2")); + + Assert.True(buttonResources.ContainsDeferredKey("Red")); + } + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 40306a4513..339cb1462c 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -155,7 +155,7 @@ namespace Avalonia.UnitTests private static IStyle CreateSimpleTheme() { - return new SimpleTheme { Mode = SimpleThemeMode.Light }; + return new SimpleTheme(); } private static IPlatformRenderInterface CreateRenderInterfaceMock() From 151fe0031a31c34acbd50600bba03a916a5bf20b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:13:01 -0500 Subject: [PATCH 06/11] Update control catalog and samples --- samples/ControlCatalog/App.xaml | 24 +++++- samples/ControlCatalog/App.xaml.cs | 48 ++--------- samples/ControlCatalog/MainView.xaml | 19 ++++- samples/ControlCatalog/MainView.xaml.cs | 39 ++++----- samples/ControlCatalog/Models/CatalogTheme.cs | 6 +- .../Pages/DateTimePickerPage.xaml | 30 +++---- .../ControlCatalog/Pages/FlyoutsPage.axaml | 22 +++--- .../Pages/ItemsRepeaterPage.xaml | 2 +- .../ControlCatalog/Pages/SplitViewPage.xaml | 16 ++-- .../ControlCatalog/Pages/TextBlockPage.xaml | 2 +- samples/ControlCatalog/Pages/ThemePage.axaml | 79 +++++++++++++++++++ .../ControlCatalog/Pages/ThemePage.axaml.cs | 37 +++++++++ samples/IntegrationTestApp/App.axaml | 2 +- samples/PlatformSanityChecks/App.xaml | 2 +- samples/Previewer/App.xaml | 2 +- .../HamburgerMenu/HamburgerMenu.xaml | 34 +++++--- samples/Sandbox/App.axaml | 2 +- 17 files changed, 239 insertions(+), 127 deletions(-) create mode 100644 samples/ControlCatalog/Pages/ThemePage.axaml create mode 100644 samples/ControlCatalog/Pages/ThemePage.axaml.cs diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 8f32fa01dd..3b847adcbb 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -6,18 +6,34 @@ x:Class="ControlCatalog.App"> + + + + + #33000000 + #99000000 + #FFE6E6E6 + #FF000000 + + + #33FFFFFF + #99FFFFFF + #FF1F1F1F + #FFFFFFFF + + + #FF0078D7 + #FF005A9E + + - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 6c99eb5289..d71d51f068 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -16,7 +16,6 @@ namespace ControlCatalog private readonly Styles _themeStylesContainer = new(); private FluentTheme? _fluentTheme; private SimpleTheme? _simpleTheme; - private IResourceDictionary? _fluentBaseLightColors, _fluentBaseDarkColors; private IStyle? _colorPickerFluent, _colorPickerSimple; private IStyle? _dataGridFluent, _dataGridSimple; @@ -33,16 +32,12 @@ namespace ControlCatalog _fluentTheme = new FluentTheme(); _simpleTheme = new SimpleTheme(); - _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentAccentColors"]!); - _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentBaseColors"]!); _colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!; _colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!; _dataGridFluent = (IStyle)Resources["DataGridFluent"]!; _dataGridSimple = (IStyle)Resources["DataGridSimple"]!; - _fluentBaseLightColors = (IResourceDictionary)Resources["FluentBaseLightColors"]!; - _fluentBaseDarkColors = (IResourceDictionary)Resources["FluentBaseDarkColors"]!; - SetThemeVariant(CatalogTheme.FluentLight); + SetCatalogThemes(CatalogTheme.Fluent); } public override void OnFrameworkInitializationCompleted() @@ -61,19 +56,12 @@ namespace ControlCatalog private CatalogTheme _prevTheme; public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme; - public static void SetThemeVariant(CatalogTheme theme) + public static void SetCatalogThemes(CatalogTheme theme) { var app = (App)Current!; var prevTheme = app._prevTheme; app._prevTheme = theme; - var shouldReopenWindow = theme switch - { - CatalogTheme.FluentLight => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight, - CatalogTheme.FluentDark => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight, - CatalogTheme.SimpleLight => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight, - CatalogTheme.SimpleDark => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight, - _ => throw new ArgumentOutOfRangeException(nameof(theme), theme, null) - }; + var shouldReopenWindow = prevTheme != theme; if (app._themeStylesContainer.Count == 0) { @@ -81,36 +69,16 @@ namespace ControlCatalog app._themeStylesContainer.Add(new Style()); app._themeStylesContainer.Add(new Style()); } - - if (theme == CatalogTheme.FluentLight) - { - app._fluentTheme!.Mode = FluentThemeMode.Light; - app._themeStylesContainer[0] = app._fluentTheme; - app._themeStylesContainer[1] = app._colorPickerFluent!; - app._themeStylesContainer[2] = app._dataGridFluent!; - } - else if (theme == CatalogTheme.FluentDark) + + if (theme == CatalogTheme.Fluent) { - app._fluentTheme!.Mode = FluentThemeMode.Dark; - app._themeStylesContainer[0] = app._fluentTheme; + app._themeStylesContainer[0] = app._fluentTheme!; app._themeStylesContainer[1] = app._colorPickerFluent!; app._themeStylesContainer[2] = app._dataGridFluent!; } - else if (theme == CatalogTheme.SimpleLight) - { - app._simpleTheme!.Mode = SimpleThemeMode.Light; - app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseDarkColors!); - app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseLightColors!); - app._themeStylesContainer[0] = app._simpleTheme; - app._themeStylesContainer[1] = app._colorPickerSimple!; - app._themeStylesContainer[2] = app._dataGridSimple!; - } - else if (theme == CatalogTheme.SimpleDark) + else if (theme == CatalogTheme.Simple) { - app._simpleTheme!.Mode = SimpleThemeMode.Dark; - app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseLightColors!); - app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseDarkColors!); - app._themeStylesContainer[0] = app._simpleTheme; + app._themeStylesContainer[0] = app._simpleTheme!; app._themeStylesContainer[1] = app._colorPickerSimple!; app._themeStylesContainer[2] = app._dataGridSimple!; } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 166b98436e..4eb73632c1 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -165,6 +165,9 @@ + + + @@ -198,14 +201,22 @@ Full + + + Default + Light + Dark + + - FluentLight - FluentDark - SimpleLight - SimpleDark + Fluent + Simple PlatformThemeVariant.Light, - CatalogTheme.FluentDark => PlatformThemeVariant.Dark, - CatalogTheme.SimpleLight => PlatformThemeVariant.Light, - CatalogTheme.SimpleDark => PlatformThemeVariant.Dark, - _ => throw new ArgumentOutOfRangeException() - }); + App.SetCatalogThemes(theme); + } + }; + var themeVariants = this.Get("ThemeVariants"); + themeVariants.SelectedItem = Application.Current!.RequestedThemeVariant; + themeVariants.SelectionChanged += (sender, e) => + { + if (themeVariants.SelectedItem is ThemeVariant themeVariant) + { + Application.Current!.RequestedThemeVariant = themeVariant; } }; @@ -118,25 +119,13 @@ namespace ControlCatalog private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) { - var themes = this.Get("Themes"); - var currentTheme = (CatalogTheme?)themes.SelectedItem ?? CatalogTheme.FluentLight; - var newTheme = (currentTheme, e.ThemeVariant) switch - { - (CatalogTheme.FluentDark, PlatformThemeVariant.Light) => CatalogTheme.FluentLight, - (CatalogTheme.FluentLight, PlatformThemeVariant.Dark) => CatalogTheme.FluentDark, - (CatalogTheme.SimpleDark, PlatformThemeVariant.Light) => CatalogTheme.SimpleLight, - (CatalogTheme.SimpleLight, PlatformThemeVariant.Dark) => CatalogTheme.SimpleDark, - _ => currentTheme - }; - themes.SelectedItem = newTheme; - Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1; Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); - Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); - Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); - Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); + Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, 0.3); + Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, 0.5); + Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, 0.7); static Color ChangeColorLuminosity(Color color, double luminosityFactor) { diff --git a/samples/ControlCatalog/Models/CatalogTheme.cs b/samples/ControlCatalog/Models/CatalogTheme.cs index 37224ed26e..79b3182d20 100644 --- a/samples/ControlCatalog/Models/CatalogTheme.cs +++ b/samples/ControlCatalog/Models/CatalogTheme.cs @@ -2,9 +2,7 @@ { public enum CatalogTheme { - FluentLight, - FluentDark, - SimpleLight, - SimpleDark + Fluent, + Simple } } diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml index 47753f56b6..fc3ad9b895 100644 --- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml @@ -15,11 +15,11 @@ Spacing="16"> A simple DatePicker - - + @@ -31,7 +31,7 @@ - @@ -42,12 +42,12 @@ A DatePicker with day formatted and year hidden. - - + @@ -58,15 +58,15 @@ - + A simple TimePicker. - - + @@ -77,7 +77,7 @@ - @@ -88,11 +88,11 @@ A TimePicker with minute increments specified. - - + @@ -105,11 +105,11 @@ A TimePicker using a 12-hour clock. - - + @@ -122,11 +122,11 @@ A TimePicker using a 24-hour clock. - - + diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index c4d0bc3e67..54aa9d1b67 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -26,31 +26,31 @@ - - + diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml b/samples/ControlCatalog/Pages/SplitViewPage.xaml index 61bfb490b8..2edd895349 100644 --- a/samples/ControlCatalog/Pages/SplitViewPage.xaml +++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml @@ -32,7 +32,7 @@ - SystemControlBackgroundChromeMediumLowBrush + CatalogChromeMediumColor Red Blue Green @@ -48,7 +48,7 @@ - - + @@ -89,11 +89,11 @@ - - - - - + + + + + diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 6bb428e2c7..6511e2136a 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -9,7 +9,7 @@ @@ -101,7 +115,7 @@ VerticalAlignment="Center" Background="{DynamicResource TabItemHeaderSelectedPipeFill}" IsVisible="False" - CornerRadius="{DynamicResource ControlCornerRadius}"/> + CornerRadius="4"/> @@ -136,18 +150,18 @@ - - - + + + diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml index f601f9f78f..cf3e5e445a 100644 --- a/samples/Sandbox/App.axaml +++ b/samples/Sandbox/App.axaml @@ -3,6 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Sandbox.App"> - + From a0d22499cd12614daf2667b410fef62e00b7d747 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:39:11 -0500 Subject: [PATCH 07/11] Fix benchmarks build --- .../Controls/ResourceDictionary.cs | 2 +- .../Themes/ThemeBenchmark.cs | 22 +++++-------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 85e4487ba9..5123803f6e 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -192,7 +192,7 @@ namespace Avalonia.Controls if (_themeDictionary is not null) { IResourceProvider? themeResourceProvider; - if (theme is not null) + if (theme is not null && theme != ThemeVariant.Default) { if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) && themeResourceProvider.TryGetResource(key, theme, out value)) diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index 70636d1fe6..7c0a3f8bdf 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -29,26 +29,16 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] - [Arguments(FluentThemeMode.Dark)] - [Arguments(FluentThemeMode.Light)] - public bool InitFluentTheme(FluentThemeMode mode) + public bool InitFluentTheme() { - UnitTestApplication.Current.Styles[0] = new FluentTheme() - { - Mode = mode - }; + UnitTestApplication.Current.Styles[0] = new FluentTheme(); return ((IResourceHost)UnitTestApplication.Current).TryGetResource("SystemAccentColor", out _); } [Benchmark] - [Arguments(SimpleThemeMode.Dark)] - [Arguments(SimpleThemeMode.Light)] - public bool InitSimpleTheme(SimpleThemeMode mode) + public bool InitSimpleTheme() { - UnitTestApplication.Current.Styles[0] = new SimpleTheme() - { - Mode = mode - }; + UnitTestApplication.Current.Styles[0] = new SimpleTheme(); return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _); } @@ -58,7 +48,7 @@ namespace Avalonia.Benchmarks.Themes [Arguments(typeof(DatePicker))] public object FindFluentControlTheme(Type type) { - _reusableFluentTheme.TryGetResource(type, out var theme); + _reusableFluentTheme.TryGetResource(type, ThemeVariant.Default, out var theme); return theme; } @@ -68,7 +58,7 @@ namespace Avalonia.Benchmarks.Themes [Arguments(typeof(DatePicker))] public object FindSimpleControlTheme(Type type) { - _reusableSimpleTheme.TryGetResource(type, out var theme); + _reusableSimpleTheme.TryGetResource(type, ThemeVariant.Default, out var theme); return theme; } From de325add060f7dc42c813ff8d63b3affcb9def7c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:40:05 -0500 Subject: [PATCH 08/11] Fix code related warnings --- src/Avalonia.Base/StyledElement.cs | 2 +- src/Avalonia.Controls/Application.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index d23c585299..5bf022cd51 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -81,7 +81,7 @@ namespace Avalonia defaultValue: ThemeVariant.Light); /// - /// Defines the property. + /// Defines the RequestedThemeVariant property. /// public static readonly StyledProperty RequestedThemeVariantProperty = AvaloniaProperty.Register( diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 58cc02e8c5..3dcba4ded9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -95,7 +95,7 @@ namespace Avalonia set => SetValue(RequestedThemeVariantProperty, value); } - /// + /// public ThemeVariant ActualThemeVariant { get => GetValue(ActualThemeVariantProperty); From b43bd006b07b087114ea55052c2ffc8c50af2500 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 21:03:09 -0500 Subject: [PATCH 09/11] Fix samples build --- samples/BindingDemo/App.xaml | 7 ------- samples/MobileSandbox/App.xaml | 5 +++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml index 5a8e65ed22..84f54293ef 100644 --- a/samples/BindingDemo/App.xaml +++ b/samples/BindingDemo/App.xaml @@ -2,13 +2,6 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="BindingDemo.App"> - - - - - - - diff --git a/samples/MobileSandbox/App.xaml b/samples/MobileSandbox/App.xaml index 85c97c9dbe..6fb6ae297e 100644 --- a/samples/MobileSandbox/App.xaml +++ b/samples/MobileSandbox/App.xaml @@ -1,8 +1,9 @@ + x:Class="MobileSandbox.App" + RequestedThemeVariant="Dark"> - + From 58c23a7efcfb7ddcf4e63b3a8e475be5115528cf Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 30 Jan 2023 09:57:55 +0100 Subject: [PATCH 10/11] fix: misc compilation errors after merge PRs #10078 #10120 --- .../Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj | 3 --- src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs | 6 +++--- .../Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs | 2 +- src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 3 +++ 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj index cc8a40e2d4..60bb75a342 100644 --- a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj +++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj @@ -13,9 +13,6 @@ - - - diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 76fc4fa21d..bfc50db874 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -12,6 +12,7 @@ using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Key = Avalonia.Input.Key; using KeyEventArgs = System.Windows.Input.KeyEventArgs; using MouseButton = System.Windows.Input.MouseButton; @@ -90,8 +91,7 @@ namespace Avalonia.Win32.Interop.Wpf public IRenderer CreateRenderer(IRenderRoot root) { - var mgr = new PlatformRenderInterfaceContextManager(null); - return new ImmediateRenderer((Visual)root, () => mgr.CreateRenderTarget(_surfaces), mgr); + return new CompositingRenderer(root, Win32Platform.Compositor, () => _surfaces); } public void Dispose() @@ -134,7 +134,7 @@ namespace Avalonia.Win32.Interop.Wpf drawingContext.DrawImage(ImageSource, new System.Windows.Rect(0, 0, ActualWidth, ActualHeight)); } - void ITopLevelImpl.Invalidate(Rect rect) => InvalidateVisual(); + void ITopLevelImpl.SetInputRoot(IInputRoot inputRoot) => _inputRoot = inputRoot; diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs index 05fa9d9426..04b4a53580 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs @@ -25,7 +25,7 @@ namespace Avalonia.Win32.Interop.Wpf if (_bitmap == null || _bitmap.PixelWidth != (int) size.Width || _bitmap.PixelHeight != (int) size.Height) { _bitmap = new WriteableBitmap((int) size.Width, (int) size.Height, dpi.X, dpi.Y, - PixelFormats.Bgra32, null); + System.Windows.Media.PixelFormats.Bgra32, null); } return new LockedFramebuffer(_impl, _bitmap, dpi); } diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index eb51b7fd07..b7dca78845 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -27,4 +27,7 @@ $(NoWarn);CA1416 + + + From 35662ad4cbe5b4805a57b398d4fe376190412c93 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 31 Jan 2023 15:59:28 -0500 Subject: [PATCH 11/11] Allow only well known ThemeVariant values in xaml compiler, add more documentation. --- .../ControlCatalog/Pages/ThemePage.axaml.cs | 2 +- src/Avalonia.Base/Styling/ThemeVariant.cs | 62 +++++++++++++++---- .../Styling/ThemeVariantTypeConverter.cs | 3 +- src/Avalonia.Controls/Application.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 7 --- .../AvaloniaXamlIlWellKnownTypes.cs | 2 - .../{ => Xaml}/ThemeDictionariesTests.cs | 13 ++-- 8 files changed, 64 insertions(+), 29 deletions(-) rename tests/Avalonia.Markup.Xaml.UnitTests/{ => Xaml}/ThemeDictionariesTests.cs (97%) diff --git a/samples/ControlCatalog/Pages/ThemePage.axaml.cs b/samples/ControlCatalog/Pages/ThemePage.axaml.cs index af7b2fe37d..f0ae1a722d 100644 --- a/samples/ControlCatalog/Pages/ThemePage.axaml.cs +++ b/samples/ControlCatalog/Pages/ThemePage.axaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages selector.Items = new[] { - new ThemeVariant("Default"), + ThemeVariant.Default, ThemeVariant.Dark, ThemeVariant.Light, Pink diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index d9cb123925..8218533f4f 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -5,20 +5,60 @@ using Avalonia.Platform; namespace Avalonia.Styling; +/// +/// Specifies a UI theme variant that should be used for the +/// [TypeConverter(typeof(ThemeVariantTypeConverter))] -public sealed record ThemeVariant(object Key) -{ +public sealed record ThemeVariant +{ + /// + /// Creates a new instance of the + /// + /// Key of the theme variant by which variants are compared. + /// Reference to a theme variant which should be used, if resource wasn't found for the requested variant. + /// Thrown if inheritVariant is a reference to the which is ambiguous value to inherit. + /// Thrown if key is null. public ThemeVariant(object key, ThemeVariant? inheritVariant) - : this(key) { + Key = key ?? throw new ArgumentNullException(nameof(key)); InheritVariant = inheritVariant; + + if (inheritVariant == Default) + { + throw new ArgumentException("Inheriting default theme variant is not supported.", nameof(inheritVariant)); + } } + private ThemeVariant(object key) + { + Key = key; + } + + /// + /// Key of the theme variant by which variants are compared. + /// + public object Key { get; } + + /// + /// Reference to a theme variant which should be used, if resource wasn't found for the requested variant. + /// + public ThemeVariant? InheritVariant { get; } + + /// + /// Inherit theme variant from the parent. If set on Application, system theme is inherited. + /// Using Default as the ResourceDictionary.Key marks this dictionary as a fallback in case the theme variant or resource key is not found in other theme dictionaries. + /// public static ThemeVariant Default { get; } = new(nameof(Default)); + + /// + /// Use the Light theme variant. + /// public static ThemeVariant Light { get; } = new(nameof(Light)); - public static ThemeVariant Dark { get; } = new(nameof(Dark)); - public ThemeVariant? InheritVariant { get; init; } + /// + /// Use the Dark theme variant. + /// + public static ThemeVariant Dark { get; } = new(nameof(Dark)); public override string ToString() { @@ -35,7 +75,7 @@ public sealed record ThemeVariant(object Key) return Key == other?.Key; } - public static ThemeVariant FromPlatformThemeVariant(PlatformThemeVariant themeVariant) + public static explicit operator ThemeVariant(PlatformThemeVariant themeVariant) { return themeVariant switch { @@ -45,19 +85,19 @@ public sealed record ThemeVariant(object Key) }; } - public PlatformThemeVariant? ToPlatformThemeVariant() + public static explicit operator PlatformThemeVariant?(ThemeVariant themeVariant) { - if (this == Light) + if (themeVariant == Light) { return PlatformThemeVariant.Light; } - else if (this == Dark) + else if (themeVariant == Dark) { return PlatformThemeVariant.Dark; } - else if (InheritVariant is { } inheritVariant) + else if (themeVariant.InheritVariant is { } inheritVariant) { - return inheritVariant.ToPlatformThemeVariant(); + return (PlatformThemeVariant?)inheritVariant; } return null; diff --git a/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs index 4da1b495f5..acb2d7651b 100644 --- a/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs +++ b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs @@ -15,9 +15,10 @@ public class ThemeVariantTypeConverter : TypeConverter { return value switch { + nameof(ThemeVariant.Default) => ThemeVariant.Default, nameof(ThemeVariant.Light) => ThemeVariant.Light, nameof(ThemeVariant.Dark) => ThemeVariant.Dark, - _ => new ThemeVariant(value) + _ => throw new NotSupportedException("ThemeVariant type converter supports only build in variants. For custom variants please use x:Static markup extension.") }; } } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 3dcba4ded9..6d3ba3cf8a 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -340,7 +340,7 @@ namespace Avalonia private void OnColorValuesChanged(object? sender, PlatformColorValues e) { - SetValue(ActualThemeVariantProperty, ThemeVariant.FromPlatformThemeVariant(e.ThemeVariant), BindingPriority.Template); + SetValue(ActualThemeVariantProperty, (ThemeVariant)e.ThemeVariant, BindingPriority.Template); } } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7fe82a452e..676fa1519a 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -435,7 +435,7 @@ namespace Avalonia.Controls } else if (change.Property == ActualThemeVariantProperty) { - PlatformImpl?.SetFrameThemeVariant(change.GetNewValue().ToPlatformThemeVariant() ?? PlatformThemeVariant.Light); + PlatformImpl?.SetFrameThemeVariant((PlatformThemeVariant?)change.GetNewValue() ?? PlatformThemeVariant.Light); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 365a07a7f6..4068caac21 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -302,13 +302,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions result = new XamlStaticExtensionNode(new XamlAstObjectNode(node, node.Type), themeVariantTypeRef, foundConstProperty.Name); return true; } - - result = new XamlAstNewClrObjectNode(node, themeVariantTypeRef, types.ThemeVariantConstructor, - new List() - { - new XamlConstantNode(node, context.Configuration.WellKnownTypes.String, variantText) - }); - return true; } result = null; 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 a4a3bcce94..16f6a32ae1 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -69,7 +69,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType Thickness { get; } public IXamlConstructor ThicknessFullConstructor { get; } public IXamlType ThemeVariant { get; } - public IXamlConstructor ThemeVariantConstructor { get; } public IXamlType Point { get; } public IXamlConstructor PointFullConstructor { get; } public IXamlType Vector { get; } @@ -193,7 +192,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily"); FontFamilyConstructorUriName = FontFamily.GetConstructor(new List { Uri, XamlIlTypes.String }); ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant"); - ThemeVariantConstructor = ThemeVariant.GetConstructor(new List { XamlIlTypes.String }); (IXamlType, IXamlConstructor) GetNumericTypeInfo(string name, IXamlType componentType, int componentCount) { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs similarity index 97% rename from tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs rename to tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs index 56040c2186..c5b62cdff2 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -6,10 +6,12 @@ using Avalonia.Styling; using Moq; using Xunit; -namespace Avalonia.Markup.Xaml.UnitTests; +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() { @@ -353,13 +355,14 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Red, ((ISolidColorBrush)border.Background)!.Color); } - + [Fact] public void Custom_Theme_Can_Be_Defined_In_ThemeDictionaries() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" @@ -370,7 +373,7 @@ public class ThemeDictionariesTests : XamlTestBase White - + Pink @@ -380,9 +383,9 @@ public class ThemeDictionariesTests : XamlTestBase "); var border = (Border)themeVariantScope.Child!; - - themeVariantScope.RequestedThemeVariant = new ThemeVariant("Custom"); + themeVariantScope.RequestedThemeVariant = Custom; + Assert.Equal(Colors.Pink, ((ISolidColorBrush)border.Background)!.Color); }