Browse Source

Introduce ThemeVariant API

pull/8166/head
Max Katz 4 years ago
parent
commit
253ecd028d
  1. 6
      src/Avalonia.Base/Controls/IResourceDictionary.cs
  2. 7
      src/Avalonia.Base/Controls/IResourceNode.cs
  3. 91
      src/Avalonia.Base/Controls/ResourceDictionary.cs
  4. 125
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  5. 41
      src/Avalonia.Base/StyledElement.cs
  6. 22
      src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs
  7. 6
      src/Avalonia.Base/Styling/StyleBase.cs
  8. 6
      src/Avalonia.Base/Styling/Styles.cs
  9. 65
      src/Avalonia.Base/Styling/ThemeVariant.cs
  10. 23
      src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs
  11. 62
      src/Avalonia.Controls/Application.cs
  12. 23
      src/Avalonia.Controls/ThemeVariantScope.cs
  13. 52
      src/Avalonia.Controls/TopLevel.cs
  14. 46
      src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
  15. 8
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  16. 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  17. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  18. 8
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  19. 2
      src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs
  20. 21
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  21. 5
      src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs
  22. 4
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

6
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.
/// </summary>
IList<IResourceProvider> MergedDictionaries { get; }
/// <summary>
/// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios.
/// </summary>
IDictionary<ThemeVariant, IResourceProvider> ThemeDictionaries { get; }
}
}

7
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.
/// </summary>
/// <param name="key">The resource key.</param>
/// <param name="theme">Theme used to select theme dictionary.</param>
/// <param name="value">
/// 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
/// <returns>
/// True if the resource if found, otherwise false.
/// </returns>
bool TryGetResource(object key, out object? value);
bool TryGetResource(object key, ThemeVariant? theme, out object? value);
}
}

91
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<object, object?>? _inner;
private IResourceHost? _owner;
private AvaloniaList<IResourceProvider>? _mergedDictionaries;
private AvaloniaDictionary<ThemeVariant, IResourceProvider>? _themeDictionary;
/// <summary>
/// Initializes a new instance of the <see cref="ResourceDictionary"/> 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<ThemeVariant, IResourceProvider> ThemeDictionaries
{
get
{
if (_themeDictionary == null)
{
_themeDictionary = new AvaloniaDictionary<ThemeVariant, IResourceProvider>(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)
{

125
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);
}
/// <summary>
/// Finds the specified resource by searching up the logical tree and then global styles.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="theme">Theme used to select theme dictionary.</param>
/// <param name="key">The resource key.</param>
/// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns>
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;
}
/// <summary>
/// Tries to the specified resource by searching up the logical tree and then global styles.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="key">The resource key.</param>
/// <param name="theme">Theme used to select theme dictionary.</param>
/// <param name="value">On return, contains the resource if found, otherwise null.</param>
/// <returns>True if the resource was found; otherwise false.</returns>
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;
}
/// <inheritdoc cref="IResourceNode.TryGetResource" />
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<object?> 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<object?> 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<object?>
@ -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<object?> 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;
}
}
}
}

41
src/Avalonia.Base/StyledElement.cs

@ -71,6 +71,23 @@ namespace Avalonia
public static readonly StyledProperty<ControlTheme?> ThemeProperty =
AvaloniaProperty.Register<StyledElement, ControlTheme?>(nameof(Theme));
/// <summary>
/// Defines the <see cref="ActualThemeVariant"/> property.
/// </summary>
public static readonly StyledProperty<ThemeVariant> ActualThemeVariantProperty =
AvaloniaProperty.Register<StyledElement, ThemeVariant>(
nameof(ThemeVariant),
inherits: true,
defaultValue: ThemeVariant.Light);
/// <summary>
/// Defines the <see cref="RequestedThemeVariant"/> property.
/// </summary>
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
AvaloniaProperty.Register<StyledElement, ThemeVariant?>(
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);
}
/// <summary>
/// Gets the UI theme that is currently used by the element, which might be different than the <see cref="RequestedThemeVariantProperty"/>.
/// </summary>
/// <returns>
/// 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.
/// </returns>
public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty);
/// <summary>
/// Gets the styled element's logical children.
/// </summary>
@ -439,11 +465,11 @@ namespace Avalonia
void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
/// <inheritdoc/>
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);
}
/// <summary>
@ -621,6 +647,13 @@ namespace Avalonia
if (change.Property == ThemeProperty)
OnControlThemeChanged();
else if (change.Property == RequestedThemeVariantProperty)
{
if (change.GetNewValue<ThemeVariant>() 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;

22
src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs

@ -0,0 +1,22 @@
using System;
using Avalonia.Controls;
using Avalonia.Metadata;
namespace Avalonia.Styling;
/// <summary>
/// Interface for an application host element with a root theme variant.
/// </summary>
[Unstable]
public interface IGlobalThemeVariantProvider : IResourceHost
{
/// <summary>
/// Gets the UI theme variant that is used by the control (and its child elements) for resource determination.
/// </summary>
ThemeVariant ActualThemeVariant { get; }
/// <summary>
/// Raised when the theme variant is changed on the element or an ancestor of the element.
/// </summary>
event EventHandler? ActualThemeVariantChanged;
}

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

6
src/Avalonia.Base/Styling/Styles.cs

@ -115,16 +115,16 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
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;
}

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

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

62
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.
/// </remarks>
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IGlobalThemeVariantProvider, IApplicationPlatformEvents
{
/// <summary>
/// The application-global data templates.
@ -49,10 +50,22 @@ namespace Avalonia
public static readonly StyledProperty<object?> DataContextProperty =
StyledElement.DataContextProperty.AddOwner<Application>();
/// <inheritdoc cref="StyledElement.ActualThemeVariantProperty" />
public static readonly StyledProperty<ThemeVariant> ActualThemeVariantProperty =
StyledElement.ActualThemeVariantProperty.AddOwner<Application>();
/// <inheritdoc cref="StyledElement.RequestedThemeVariantProperty" />
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
StyledElement.RequestedThemeVariantProperty.AddOwner<Application>();
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
public event EventHandler<UrlOpenedEventArgs>? UrlsOpened;
/// <inheritdoc/>
public event EventHandler<UrlOpenedEventArgs>? UrlsOpened;
/// <inheritdoc/>
public event EventHandler? ActualThemeVariantChanged;
/// <summary>
/// Creates an instance of the <see cref="Application"/> class.
@ -75,6 +88,19 @@ namespace Avalonia
set { SetValue(DataContextProperty, value); }
}
/// <inheritdoc cref="ThemeVariantScope.RequestedThemeVariant"/>
public ThemeVariant? RequestedThemeVariant
{
get => GetValue(RequestedThemeVariantProperty);
set => SetValue(RequestedThemeVariantProperty, value);
}
/// <inheritdoc cref="ThemeVariantScope.ActualThemeVariant"/>
public ThemeVariant ActualThemeVariant
{
get => GetValue(ActualThemeVariantProperty);
}
/// <summary>
/// Gets the current instance of the <see cref="Application"/> class.
/// </summary>
@ -191,11 +217,11 @@ namespace Avalonia
public virtual void Initialize() { }
/// <inheritdoc/>
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<IPlatformSettings>();
settings.ColorValuesChanged += OnColorValuesChanged;
OnColorValuesChanged(settings, settings.GetColorValues());
AvaloniaLocator.CurrentMutable
.Bind<IAccessKeyHandler>().ToTransient<AccessKeyHandler>()
.Bind<IGlobalDataTemplates>().ToConstant(this)
.Bind<IGlobalStyles>().ToConstant(this)
.Bind<IGlobalThemeVariantProvider>().ToConstant(this)
.Bind<IFocusManager>().ToConstant(FocusManager)
.Bind<IInputManager>().ToConstant(InputManager)
.Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()
@ -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<ThemeVariant>() 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);
}
}
}

23
src/Avalonia.Controls/ThemeVariantScope.cs

@ -0,0 +1,23 @@
using Avalonia.Styling;
namespace Avalonia.Controls
{
/// <summary>
/// Decorator control that isolates controls subtree with locally defined <see cref="ThemeVariant"/>.
/// </summary>
public class ThemeVariantScope : Decorator
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Setting RequestedThemeVariant to <see cref="ThemeVariant.Default"/> will apply parent's actual theme variant on the current scope.
/// </remarks>
public ThemeVariant? RequestedThemeVariant
{
get => GetValue(RequestedThemeVariantProperty);
set => SetValue(RequestedThemeVariantProperty, value);
}
}
}

52
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<TopLevel>(KeyboardNavigationMode.Cycle);
AffectsMeasure<TopLevel>(ClientSizeProperty);
TransparencyLevelHintProperty.Changed.AddClassHandler<TopLevel>(
(tl, e) =>
{
if (tl.PlatformImpl != null)
{
tl.PlatformImpl.SetTransparencyLevelHint((WindowTransparencyLevel)e.NewValue!);
tl.HandleTransparencyLevelChanged(tl.PlatformImpl.TransparencyLevel);
}
});
}
/// <summary>
@ -161,6 +153,7 @@ namespace Avalonia.Controls
_keyboardNavigationHandler = TryGetService<IKeyboardNavigationHandler>(dependencyResolver);
_renderInterface = TryGetService<IPlatformRenderInterface>(dependencyResolver);
_globalStyles = TryGetService<IGlobalStyles>(dependencyResolver);
_applicationThemeHost = TryGetService<IGlobalThemeVariantProvider>(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);
}
/// <inheritdoc cref="ThemeVariantScope.RequestedThemeVariant"/>
public ThemeVariant? RequestedThemeVariant
{
get => GetValue(RequestedThemeVariantProperty);
set => SetValue(RequestedThemeVariantProperty, value);
}
/// <summary>
/// Occurs when physical Back Button is pressed or a back navigation has been requested.
/// </summary>
@ -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<WindowTransparencyLevel>());
HandleTransparencyLevelChanged(PlatformImpl.TransparencyLevel);
}
}
else if (change.Property == ActualThemeVariantProperty)
{
PlatformImpl?.SetFrameThemeVariant(change.GetNewValue<ThemeVariant>().ToPlatformThemeVariant() ?? PlatformThemeVariant.Light);
}
}
/// <summary>
/// Creates the layout manager for this <see cref="TopLevel" />.
/// </summary>
@ -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);

46
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<ThemeVariant?> RequestedThemeVariantProperty =
StyledElement.RequestedThemeVariantProperty.AddOwner<Application>();
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;
/// <summary>
/// Defines the <see cref="DataContext"/> 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.
/// </summary>
internal Rendering.IRenderer? RendererRoot { get; }
/// <inheritdoc cref="ThemeVariantScope.RequestedThemeVariant" />
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<ThemeVariant>();
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RequestedThemeVariantProperty)
{
_application.RequestedThemeVariant = change.GetNewValue<ThemeVariant>();
}
}
}
}

8
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;
/// <summary>
/// Gets or sets whether DevTools should use the dark mode theme
/// Gets or sets whether DevTools theme.
/// </summary>
public bool UseDarkMode { get; set; }
public ThemeVariant? ThemeVariant { get; set; }
}
}

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

4
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@ -9,9 +9,9 @@
<Window.DataTemplates>
<diag:ViewLocator/>
</Window.DataTemplates>
<Window.Styles>
<SimpleTheme Mode="Light"/>
<SimpleTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml"/>
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" />

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

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

21
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<IAvaloniaXamlIlParentStackProvider>();
var provideTarget = serviceProvider.GetService<IProvideValueTarget>();
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);
}
}
}

5
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;

4
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;

Loading…
Cancel
Save