13 changed files with 416 additions and 118 deletions
@ -0,0 +1,55 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Styling; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a switchable theme for a control.
|
|||
/// </summary>
|
|||
public class ControlTheme : StyleBase |
|||
{ |
|||
private Styles? _styles; |
|||
|
|||
/// <summary>
|
|||
/// Gets the child styles of the control theme.
|
|||
/// </summary>
|
|||
public Styles Styles => _styles ??= new Styles(Owner); |
|||
|
|||
protected override IReadOnlyList<IStyle> GetChildrenCore() |
|||
{ |
|||
return (IReadOnlyList<IStyle>?)_styles ?? Array.Empty<IStyle>(); |
|||
} |
|||
|
|||
protected override bool GetHasResourcesCore() |
|||
{ |
|||
if (ResourcesCore?.Count > 0) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return ((IResourceNode?)_styles)?.HasResources ?? false; |
|||
} |
|||
|
|||
public override SelectorMatchResult TryAttach(IStyleable target, object? host) |
|||
{ |
|||
if (target == host) |
|||
{ |
|||
// If target and host are the same control, then we're applying styles to the
|
|||
// control that the theme is applied to.
|
|||
Attach(target); |
|||
_styles?.TryAttach(target, host); |
|||
return SelectorMatchResult.AlwaysThisType; |
|||
} |
|||
else |
|||
{ |
|||
// If the target is different to the host then we're applying styles to a templated
|
|||
// child of the host. The setters in the control theme itself don't apply here: only
|
|||
// the child styles.
|
|||
return _styles?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using Avalonia.Styling; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a themed element.
|
|||
/// </summary>
|
|||
public interface IThemed |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the theme style for the element.
|
|||
/// </summary>
|
|||
public IStyle? Theme { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,158 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Metadata; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for <see cref="Style"/> and ControlTheme.
|
|||
/// </summary>
|
|||
public abstract class StyleBase : AvaloniaObject, IStyle, IResourceProvider |
|||
{ |
|||
private IResourceHost? _owner; |
|||
|
|||
public IResourceHost? Owner |
|||
{ |
|||
get => _owner; |
|||
private set |
|||
{ |
|||
if (_owner != value) |
|||
{ |
|||
_owner = value; |
|||
OwnerChanged?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a dictionary of style resources.
|
|||
/// </summary>
|
|||
public IResourceDictionary Resources |
|||
{ |
|||
get => ResourcesCore ?? (Resources = new ResourceDictionary()); |
|||
set |
|||
{ |
|||
value = value ?? throw new ArgumentNullException(nameof(value)); |
|||
|
|||
var hadResources = ResourcesCore?.HasResources ?? false; |
|||
|
|||
ResourcesCore = value; |
|||
|
|||
if (Owner is object) |
|||
{ |
|||
ResourcesCore.AddOwner(Owner); |
|||
|
|||
if (hadResources || ResourcesCore.HasResources) |
|||
{ |
|||
Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the style's setters.
|
|||
/// </summary>
|
|||
[Content] |
|||
public IList<ISetter> Setters => SettersCore ??= new List<ISetter>(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the style's animations.
|
|||
/// </summary>
|
|||
public IList<IAnimation> Animations => AnimationsCore ??= new List<IAnimation>(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the style's child styles.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The default implementation is to return an empty list. This can be overridden in a
|
|||
/// derived class by overriding <see cref="GetChildrenCore"/>.
|
|||
/// </remarks>
|
|||
IReadOnlyList<IStyle> IStyle.Children => GetChildrenCore(); |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the style has resources.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The implementation of this property can be overridden in a derived class by overriding
|
|||
/// <see cref="GetHasResourcesCore"/>.
|
|||
/// </remarks>
|
|||
bool IResourceNode.HasResources => GetHasResourcesCore(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="Animations"/> for the control, without creating a collection
|
|||
/// if one does not already exist.
|
|||
/// </summary>
|
|||
protected List<IAnimation>? AnimationsCore { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="Resources"/> for the control, without creating a resource
|
|||
/// dictionary if one does not already exist.
|
|||
/// </summary>
|
|||
protected IResourceDictionary? ResourcesCore { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="Setters"/> for the control, without creating a collection
|
|||
/// if one does not already exist.
|
|||
/// </summary>
|
|||
protected List<ISetter>? SettersCore { get; private set; } |
|||
|
|||
public event EventHandler? OwnerChanged; |
|||
|
|||
public abstract SelectorMatchResult TryAttach(IStyleable target, object? host); |
|||
|
|||
public bool TryGetResource(object key, out object? result) |
|||
{ |
|||
result = null; |
|||
return ResourcesCore?.TryGetResource(key, out result) ?? false; |
|||
} |
|||
|
|||
protected void Attach(IStyleable target) |
|||
{ |
|||
if (SettersCore is object || AnimationsCore is object) |
|||
{ |
|||
var instance = new StyleInstance(this, target, SettersCore, AnimationsCore); |
|||
target.StyleApplied(instance); |
|||
instance.Start(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// When overridden in a derived class, gets the value for <see cref="IStyle.Children"/>.
|
|||
/// </summary>
|
|||
protected virtual IReadOnlyList<IStyle> GetChildrenCore() => Array.Empty<IStyle>(); |
|||
|
|||
/// <summary>
|
|||
/// When overridden in a derived class, gets the value for <see cref="IResourceNode.HasResources"/>.
|
|||
/// </summary>
|
|||
protected virtual bool GetHasResourcesCore() => ResourcesCore?.Count > 0; |
|||
|
|||
void IResourceProvider.AddOwner(IResourceHost owner) |
|||
{ |
|||
owner = owner ?? throw new ArgumentNullException(nameof(owner)); |
|||
|
|||
if (Owner != null) |
|||
{ |
|||
throw new InvalidOperationException("The Style already has a parent."); |
|||
} |
|||
|
|||
Owner = owner; |
|||
ResourcesCore?.AddOwner(owner); |
|||
} |
|||
|
|||
void IResourceProvider.RemoveOwner(IResourceHost owner) |
|||
{ |
|||
owner = owner ?? throw new ArgumentNullException(nameof(owner)); |
|||
|
|||
if (Owner == owner) |
|||
{ |
|||
Owner = null; |
|||
ResourcesCore?.RemoveOwner(owner); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Styling; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.UnitTests.Primitives |
|||
{ |
|||
public class TemplatedControlTests_Theming |
|||
{ |
|||
[Fact] |
|||
public void IThemed_Theme_Returns_Default_Theme_If_Theme_Property_Unset() |
|||
{ |
|||
var theme = CreateTheme(); |
|||
var target = new ThemedControl(theme); |
|||
|
|||
Assert.Same(theme, ((IThemed)target).Theme); |
|||
} |
|||
|
|||
[Fact] |
|||
public void IThemed_Theme_Returns_Theme_Property_If_Set() |
|||
{ |
|||
var theme1 = CreateTheme(); |
|||
var theme2 = CreateTheme(); |
|||
var target = new ThemedControl(theme1) { Theme = theme2 }; |
|||
|
|||
Assert.Same(theme2, ((IThemed)target).Theme); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Theme_Is_Applied_When_Attached_To_Logical_Tree() |
|||
{ |
|||
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
|||
var target = new ThemedControl(); |
|||
|
|||
Assert.Null(target.Template); |
|||
|
|||
var root = new TestRoot(target); |
|||
|
|||
Assert.NotNull(target.Template); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Theme_Is_Detached_When_Theme_Property_Changed() |
|||
{ |
|||
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
|||
var target = new ThemedControl(); |
|||
var root = new TestRoot(target); |
|||
|
|||
target.Theme = CreateTheme(); |
|||
|
|||
Assert.Null(target.Template); |
|||
} |
|||
|
|||
private static ControlTheme CreateTheme() |
|||
{ |
|||
var template = new FuncControlTemplate<ThemedControl>((o, n) => |
|||
new Border { Name = "PART_Border" }); |
|||
|
|||
return new ControlTheme |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter(ThemedControl.TemplateProperty, template), |
|||
} |
|||
}; |
|||
} |
|||
|
|||
private class ThemedControl : TemplatedControl |
|||
{ |
|||
private ControlTheme _defaultTheme; |
|||
|
|||
public ThemedControl(ControlTheme? defaultTheme = null) |
|||
{ |
|||
_defaultTheme = defaultTheme ?? CreateTheme(); |
|||
} |
|||
|
|||
protected override IStyle GetDefaultControlTheme() => _defaultTheme; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue