committed by
GitHub
49 changed files with 1446 additions and 221 deletions
@ -0,0 +1,67 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines a switchable theme for a control.
|
||||
|
/// </summary>
|
||||
|
public class ControlTheme : StyleBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ControlTheme"/> class.
|
||||
|
/// </summary>
|
||||
|
public ControlTheme() { } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ControlTheme"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="targetType">The value for <see cref="TargetType"/>.</param>
|
||||
|
public ControlTheme(Type targetType) => TargetType = targetType; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the type for which this control theme is intended.
|
||||
|
/// </summary>
|
||||
|
public Type? TargetType { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a control theme that is the basis of the current theme.
|
||||
|
/// </summary>
|
||||
|
public ControlTheme? BasedOn { get; set; } |
||||
|
|
||||
|
public override SelectorMatchResult TryAttach(IStyleable target, object? host) |
||||
|
{ |
||||
|
_ = target ?? throw new ArgumentNullException(nameof(target)); |
||||
|
|
||||
|
if (TargetType is null) |
||||
|
throw new InvalidOperationException("ControlTheme has no TargetType."); |
||||
|
|
||||
|
var result = BasedOn?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType; |
||||
|
|
||||
|
if (HasSettersOrAnimations && TargetType.IsAssignableFrom(target.StyleKey)) |
||||
|
{ |
||||
|
Attach(target, null); |
||||
|
result = SelectorMatchResult.AlwaysThisType; |
||||
|
} |
||||
|
|
||||
|
var childResult = TryAttachChildren(target, host); |
||||
|
|
||||
|
if (childResult > result) |
||||
|
result = childResult; |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
if (TargetType is not null) |
||||
|
return "ControlTheme: " + TargetType.Name; |
||||
|
else |
||||
|
return "ControlTheme"; |
||||
|
} |
||||
|
|
||||
|
internal override void SetParent(StyleBase? parent) |
||||
|
{ |
||||
|
throw new InvalidOperationException("ControlThemes cannot be added as a nested style."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Animation; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Metadata; |
||||
|
using Avalonia.Styling.Activators; |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Base class for <see cref="Style"/> and <see cref="ControlTheme"/>.
|
||||
|
/// </summary>
|
||||
|
public abstract class StyleBase : AvaloniaObject, IStyle, IResourceProvider |
||||
|
{ |
||||
|
private IResourceHost? _owner; |
||||
|
private StyleChildren? _children; |
||||
|
private IResourceDictionary? _resources; |
||||
|
private List<ISetter>? _setters; |
||||
|
private List<IAnimation>? _animations; |
||||
|
private StyleCache? _childCache; |
||||
|
|
||||
|
public IList<IStyle> Children => _children ??= new(this); |
||||
|
|
||||
|
public IResourceHost? Owner |
||||
|
{ |
||||
|
get => _owner; |
||||
|
private set |
||||
|
{ |
||||
|
if (_owner != value) |
||||
|
{ |
||||
|
_owner = value; |
||||
|
OwnerChanged?.Invoke(this, EventArgs.Empty); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IStyle? Parent { get; private set; } |
||||
|
|
||||
|
public IResourceDictionary Resources |
||||
|
{ |
||||
|
get => _resources ?? (Resources = new ResourceDictionary()); |
||||
|
set |
||||
|
{ |
||||
|
value = value ?? throw new ArgumentNullException(nameof(value)); |
||||
|
|
||||
|
var hadResources = _resources?.HasResources ?? false; |
||||
|
|
||||
|
_resources = value; |
||||
|
|
||||
|
if (Owner is object) |
||||
|
{ |
||||
|
_resources.AddOwner(Owner); |
||||
|
|
||||
|
if (hadResources || _resources.HasResources) |
||||
|
{ |
||||
|
Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IList<ISetter> Setters => _setters ??= new List<ISetter>(); |
||||
|
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>(); |
||||
|
|
||||
|
bool IResourceNode.HasResources => _resources?.Count > 0; |
||||
|
IReadOnlyList<IStyle> IStyle.Children => (IReadOnlyList<IStyle>?)_children ?? Array.Empty<IStyle>(); |
||||
|
|
||||
|
internal bool HasSettersOrAnimations => _setters?.Count > 0 || _animations?.Count > 0; |
||||
|
|
||||
|
public void Add(ISetter setter) => Setters.Add(setter); |
||||
|
public void Add(IStyle style) => Children.Add(style); |
||||
|
|
||||
|
public event EventHandler? OwnerChanged; |
||||
|
|
||||
|
public abstract SelectorMatchResult TryAttach(IStyleable target, object? host); |
||||
|
|
||||
|
public bool TryGetResource(object key, out object? result) |
||||
|
{ |
||||
|
result = null; |
||||
|
return _resources?.TryGetResource(key, out result) ?? false; |
||||
|
} |
||||
|
|
||||
|
internal void Attach(IStyleable target, IStyleActivator? activator) |
||||
|
{ |
||||
|
var instance = new StyleInstance(this, target, _setters, _animations, activator); |
||||
|
target.StyleApplied(instance); |
||||
|
instance.Start(); |
||||
|
} |
||||
|
|
||||
|
internal SelectorMatchResult TryAttachChildren(IStyleable target, object? host) |
||||
|
{ |
||||
|
if (_children is null || _children.Count == 0) |
||||
|
return SelectorMatchResult.NeverThisType; |
||||
|
_childCache ??= new StyleCache(); |
||||
|
return _childCache.TryAttach(_children, target, host); |
||||
|
} |
||||
|
|
||||
|
internal virtual void SetParent(StyleBase? parent) => Parent = parent; |
||||
|
|
||||
|
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; |
||||
|
_resources?.AddOwner(owner); |
||||
|
} |
||||
|
|
||||
|
void IResourceProvider.RemoveOwner(IResourceHost owner) |
||||
|
{ |
||||
|
owner = owner ?? throw new ArgumentNullException(nameof(owner)); |
||||
|
|
||||
|
if (Owner == owner) |
||||
|
{ |
||||
|
Owner = null; |
||||
|
_resources?.RemoveOwner(owner); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
using System.Linq; |
||||
|
using XamlX; |
||||
|
using XamlX.Ast; |
||||
|
using XamlX.Transform; |
||||
|
using XamlX.Transform.Transformers; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers |
||||
|
{ |
||||
|
class AvaloniaXamlIlControlThemeTransformer : IXamlAstTransformer |
||||
|
{ |
||||
|
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
||||
|
{ |
||||
|
if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme")) |
||||
|
return node; |
||||
|
|
||||
|
// Check if we've already transformed this node.
|
||||
|
if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlTargetTypeMetadataNode) |
||||
|
return node; |
||||
|
|
||||
|
var targetTypeNode = on.Children.OfType<XamlAstXamlPropertyValueNode>() |
||||
|
.FirstOrDefault(p => p.Property.GetClrProperty().Name == "TargetType") ?? |
||||
|
throw new XamlParseException("ControlTheme must have a TargetType.", node); |
||||
|
|
||||
|
IXamlType targetType; |
||||
|
|
||||
|
if (targetTypeNode.Values[0] is XamlTypeExtensionNode extension) |
||||
|
targetType = extension.Value.GetClrType(); |
||||
|
else if (targetTypeNode.Values[0] is XamlAstTextNode text) |
||||
|
targetType = TypeReferenceResolver.ResolveType(context, text.Text, false, text, true).GetClrType(); |
||||
|
else |
||||
|
throw new XamlParseException("Could not determine TargetType for ControlTheme.", targetTypeNode); |
||||
|
|
||||
|
return new AvaloniaXamlIlTargetTypeMetadataNode(on, |
||||
|
new XamlAstClrTypeReference(targetTypeNode, targetType, false), |
||||
|
AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Styling; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Styling |
||||
|
{ |
||||
|
public class ControlThemeTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void ControlTheme_Cannot_Be_Added_To_Styles() |
||||
|
{ |
||||
|
var target = new ControlTheme(typeof(Button)); |
||||
|
var styles = new Styles(); |
||||
|
|
||||
|
Assert.Throws<InvalidOperationException>(() => styles.Add(target)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ControlTheme_Cannot_Be_Added_To_Style_Children() |
||||
|
{ |
||||
|
var target = new ControlTheme(typeof(Button)); |
||||
|
var style = new Style(); |
||||
|
|
||||
|
Assert.Throws<InvalidOperationException>(() => style.Children.Add(target)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ControlTheme_Cannot_Be_Added_To_ControlTheme_Children() |
||||
|
{ |
||||
|
var target = new ControlTheme(typeof(Button)); |
||||
|
var other = new ControlTheme(typeof(CheckBox)); |
||||
|
|
||||
|
Assert.Throws<InvalidOperationException>(() => other.Children.Add(target)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Style_Without_Selector_Cannot_Be_Added_To_Children() |
||||
|
{ |
||||
|
var target = new ControlTheme(typeof(Button)); |
||||
|
var child = new Style(); |
||||
|
|
||||
|
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Style_Without_Nesting_Selector_Cannot_Be_Added_To_Children() |
||||
|
{ |
||||
|
var target = new ControlTheme(typeof(Button)); |
||||
|
var child = new Style(x => x.OfType<Button>().Template().OfType<Border>()); |
||||
|
|
||||
|
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Style_With_NonTemplate_Child_Selector_Cannot_Be_Added_To_Children() |
||||
|
{ |
||||
|
var target = new ControlTheme(typeof(Button)); |
||||
|
var child = new Style(x => x.Nesting().Child().OfType<Border>()); |
||||
|
|
||||
|
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Style_With_NonTemplate_Descendent_Selector_Cannot_Be_Added_To_Children() |
||||
|
{ |
||||
|
var target = new ControlTheme(typeof(Button)); |
||||
|
var child = new Style(x => x.Nesting().Descendant().OfType<Border>()); |
||||
|
|
||||
|
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Style_With_NonTemplate_Child_Template_Selector_Cannot_Be_Added_To_Children() |
||||
|
{ |
||||
|
var target = new ControlTheme(typeof(Button)); |
||||
|
var child = new Style(x => x.Nesting().Child().Template().OfType<Border>()); |
||||
|
|
||||
|
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Style_With_Double_Template_Selector_Cannot_Be_Added_To_Children() |
||||
|
{ |
||||
|
var target = new ControlTheme(typeof(Button)); |
||||
|
var child = new Style(x => x.Nesting().Template().OfType<ToggleButton>().Template().OfType<Border>()); |
||||
|
|
||||
|
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,418 @@ |
|||||
|
using System.Linq; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Controls.Templates; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Styling; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Avalonia.VisualTree; |
||||
|
using Xunit; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Styling; |
||||
|
|
||||
|
public class StyledElementTests_Theming |
||||
|
{ |
||||
|
public class InlineTheme |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Theme_Is_Applied_When_Attached_To_Logical_Tree() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = CreateTarget(); |
||||
|
|
||||
|
Assert.Null(target.Template); |
||||
|
|
||||
|
var root = CreateRoot(target); |
||||
|
Assert.NotNull(target.Template); |
||||
|
|
||||
|
var border = Assert.IsType<Border>(target.VisualChild); |
||||
|
Assert.Equal(Brushes.Red, border.Background); |
||||
|
|
||||
|
target.Classes.Add("foo"); |
||||
|
Assert.Equal(Brushes.Green, border.Background); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Theme_Is_Applied_To_Derived_Class_When_Attached_To_Logical_Tree() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = new DerivedThemedControl |
||||
|
{ |
||||
|
Theme = CreateTheme(), |
||||
|
}; |
||||
|
|
||||
|
Assert.Null(target.Template); |
||||
|
|
||||
|
var root = CreateRoot(target); |
||||
|
Assert.NotNull(target.Template); |
||||
|
|
||||
|
var border = Assert.IsType<Border>(target.VisualChild); |
||||
|
Assert.Equal(Brushes.Red, border.Background); |
||||
|
|
||||
|
target.Classes.Add("foo"); |
||||
|
Assert.Equal(Brushes.Green, border.Background); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Theme_Is_Detached_When_Theme_Property_Cleared() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = CreateTarget(); |
||||
|
var root = CreateRoot(target); |
||||
|
|
||||
|
Assert.NotNull(target.Template); |
||||
|
|
||||
|
target.Theme = null; |
||||
|
Assert.Null(target.Template); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
|
||||
|
var theme = new ControlTheme |
||||
|
{ |
||||
|
TargetType = typeof(ThemedControl), |
||||
|
Children = |
||||
|
{ |
||||
|
new Style(x => x.Nesting().Template().OfType<Canvas>()) |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(Panel.BackgroundProperty, Brushes.Red), |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var target = CreateTarget(theme); |
||||
|
target.Template = new FuncControlTemplate<ThemedControl>((o, n) => new Canvas()); |
||||
|
|
||||
|
var root = CreateRoot(target); |
||||
|
|
||||
|
var canvas = Assert.IsType<Canvas>(target.VisualChild); |
||||
|
Assert.Equal(Brushes.Red, canvas.Background); |
||||
|
|
||||
|
target.Theme = null; |
||||
|
|
||||
|
Assert.IsType<Canvas>(target.VisualChild); |
||||
|
Assert.Null(canvas.Background); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = new ThemedControl(); |
||||
|
var root = CreateRoot(target); |
||||
|
|
||||
|
Assert.Null(target.Template); |
||||
|
|
||||
|
target.Theme = CreateTheme(); |
||||
|
Assert.Null(target.Template); |
||||
|
|
||||
|
root.LayoutManager.ExecuteLayoutPass(); |
||||
|
|
||||
|
var border = Assert.IsType<Border>(target.VisualChild); |
||||
|
Assert.NotNull(target.Template); |
||||
|
Assert.Equal(Brushes.Red, border.Background); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void BasedOn_Theme_Is_Applied_When_Attached_To_Logical_Tree() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = CreateTarget(CreateDerivedTheme()); |
||||
|
|
||||
|
Assert.Null(target.Template); |
||||
|
|
||||
|
var root = CreateRoot(target); |
||||
|
Assert.NotNull(target.Template); |
||||
|
Assert.Equal(Brushes.Blue, target.BorderBrush); |
||||
|
|
||||
|
var border = Assert.IsType<Border>(target.VisualChild); |
||||
|
Assert.Equal(Brushes.Red, border.Background); |
||||
|
Assert.Equal(Brushes.Yellow, border.BorderBrush); |
||||
|
|
||||
|
target.Classes.Add("foo"); |
||||
|
Assert.Equal(Brushes.Green, border.Background); |
||||
|
Assert.Equal(Brushes.Cyan, border.BorderBrush); |
||||
|
} |
||||
|
|
||||
|
private static ThemedControl CreateTarget(ControlTheme? theme = null) |
||||
|
{ |
||||
|
return new ThemedControl |
||||
|
{ |
||||
|
Theme = theme ?? CreateTheme(), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static TestRoot CreateRoot(IControl child) |
||||
|
{ |
||||
|
var result = new TestRoot(child); |
||||
|
result.LayoutManager.ExecuteInitialLayoutPass(); |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class ImplicitTheme |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Implicit_Theme_Is_Applied_When_Attached_To_Logical_Tree() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = CreateTarget(); |
||||
|
var root = CreateRoot(target); |
||||
|
Assert.NotNull(target.Template); |
||||
|
|
||||
|
var border = Assert.IsType<Border>(target.VisualChild); |
||||
|
Assert.Equal(Brushes.Red, border.Background); |
||||
|
|
||||
|
target.Classes.Add("foo"); |
||||
|
Assert.Equal(Brushes.Green, border.Background); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Implicit_Theme_Is_Cleared_When_Removed_From_Logical_Tree() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = CreateTarget(); |
||||
|
var root = CreateRoot(target); |
||||
|
|
||||
|
Assert.NotNull(((IStyleable)target).GetEffectiveTheme()); |
||||
|
|
||||
|
root.Child = null; |
||||
|
|
||||
|
Assert.Null(((IStyleable)target).GetEffectiveTheme()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Nested_Style_Can_Override_Property_In_Inner_Templated_Control() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = new ThemedControl2 |
||||
|
{ |
||||
|
Theme = new ControlTheme(typeof(ThemedControl2)) |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter( |
||||
|
TemplatedControl.TemplateProperty, |
||||
|
new FuncControlTemplate<ThemedControl2>((o, n) => new ThemedControl())), |
||||
|
}, |
||||
|
Children = |
||||
|
{ |
||||
|
new Style(x => x.Nesting().Template().OfType<ThemedControl>()) |
||||
|
{ |
||||
|
Setters = { new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(7)), } |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
var root = CreateRoot(target); |
||||
|
var inner = Assert.IsType<ThemedControl>(target.VisualChild); |
||||
|
|
||||
|
Assert.Equal(new CornerRadius(7), inner.CornerRadius); |
||||
|
} |
||||
|
|
||||
|
private static ThemedControl CreateTarget() => new ThemedControl(); |
||||
|
|
||||
|
private static TestRoot CreateRoot(IControl child) |
||||
|
{ |
||||
|
var result = new TestRoot(); |
||||
|
result.Resources.Add(typeof(ThemedControl), CreateTheme()); |
||||
|
result.Child = child; |
||||
|
result.LayoutManager.ExecuteInitialLayoutPass(); |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class ThemeFromStyle |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Theme_Is_Applied_When_Attached_To_Logical_Tree() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = CreateTarget(); |
||||
|
|
||||
|
Assert.Null(target.Theme); |
||||
|
Assert.Null(target.Template); |
||||
|
|
||||
|
var root = CreateRoot(target); |
||||
|
|
||||
|
Assert.NotNull(target.Theme); |
||||
|
Assert.NotNull(target.Template); |
||||
|
|
||||
|
var border = Assert.IsType<Border>(target.VisualChild); |
||||
|
Assert.Equal(border.Background, Brushes.Red); |
||||
|
|
||||
|
target.Classes.Add("foo"); |
||||
|
Assert.Equal(border.Background, Brushes.Green); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Theme_Can_Be_Changed_By_Style_Class() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = CreateTarget(); |
||||
|
var theme1 = CreateTheme(); |
||||
|
var theme2 = new ControlTheme(typeof(ThemedControl)); |
||||
|
var root = new TestRoot() |
||||
|
{ |
||||
|
Styles = |
||||
|
{ |
||||
|
new Style(x => x.OfType<ThemedControl>()) |
||||
|
{ |
||||
|
Setters = { new Setter(StyledElement.ThemeProperty, theme1) } |
||||
|
}, |
||||
|
new Style(x => x.OfType<ThemedControl>().Class("bar")) |
||||
|
{ |
||||
|
Setters = { new Setter(StyledElement.ThemeProperty, theme2) } |
||||
|
}, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
root.Child = target; |
||||
|
root.LayoutManager.ExecuteInitialLayoutPass(); |
||||
|
|
||||
|
Assert.Same(theme1, target.Theme); |
||||
|
Assert.NotNull(target.Template); |
||||
|
|
||||
|
target.Classes.Add("bar"); |
||||
|
Assert.Same(theme2, target.Theme); |
||||
|
Assert.Null(target.Template); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Theme_Can_Be_Set_To_LocalValue_While_Updating_Due_To_Style_Class() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.RealStyler); |
||||
|
var target = CreateTarget(); |
||||
|
var theme1 = CreateTheme(); |
||||
|
var theme2 = new ControlTheme(typeof(ThemedControl)); |
||||
|
var theme3 = new ControlTheme(typeof(ThemedControl)); |
||||
|
var root = new TestRoot() |
||||
|
{ |
||||
|
Styles = |
||||
|
{ |
||||
|
new Style(x => x.OfType<ThemedControl>()) |
||||
|
{ |
||||
|
Setters = { new Setter(StyledElement.ThemeProperty, theme1) } |
||||
|
}, |
||||
|
new Style(x => x.OfType<ThemedControl>().Class("bar")) |
||||
|
{ |
||||
|
Setters = { new Setter(StyledElement.ThemeProperty, theme2) } |
||||
|
}, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
root.Child = target; |
||||
|
root.LayoutManager.ExecuteInitialLayoutPass(); |
||||
|
|
||||
|
Assert.Same(theme1, target.Theme); |
||||
|
Assert.NotNull(target.Template); |
||||
|
|
||||
|
target.Classes.Add("bar"); |
||||
|
|
||||
|
// At this point, theme2 has been promoted to a local value internally in StyledElement;
|
||||
|
// make sure that setting a new local value here doesn't cause it to be cleared when we
|
||||
|
// do a layout pass because StyledElement thinks its clearing the promoted theme.
|
||||
|
target.Theme = theme3; |
||||
|
|
||||
|
root.LayoutManager.ExecuteLayoutPass(); |
||||
|
|
||||
|
Assert.Same(target.Theme, theme3); |
||||
|
} |
||||
|
|
||||
|
private static ThemedControl CreateTarget() |
||||
|
{ |
||||
|
return new ThemedControl(); |
||||
|
} |
||||
|
|
||||
|
private static TestRoot CreateRoot(IControl child) |
||||
|
{ |
||||
|
var result = new TestRoot() |
||||
|
{ |
||||
|
Styles = |
||||
|
{ |
||||
|
new Style(x => x.OfType<ThemedControl>()) |
||||
|
{ |
||||
|
Setters = { new Setter(StyledElement.ThemeProperty, CreateTheme()) } |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
result.Child = child; |
||||
|
result.LayoutManager.ExecuteInitialLayoutPass(); |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static ControlTheme CreateTheme() |
||||
|
{ |
||||
|
var template = new FuncControlTemplate<ThemedControl>((o, n) => new Border()); |
||||
|
|
||||
|
return new ControlTheme |
||||
|
{ |
||||
|
TargetType = typeof(ThemedControl), |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(TemplatedControl.TemplateProperty, template), |
||||
|
new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(5)), |
||||
|
}, |
||||
|
Children = |
||||
|
{ |
||||
|
new Style(x => x.Nesting().Template().OfType<Border>()) |
||||
|
{ |
||||
|
Setters = { new Setter(Border.BackgroundProperty, Brushes.Red) } |
||||
|
}, |
||||
|
new Style(x => x.Nesting().Class("foo").Template().OfType<Border>()) |
||||
|
{ |
||||
|
Setters = { new Setter(Border.BackgroundProperty, Brushes.Green) } |
||||
|
}, |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static ControlTheme CreateDerivedTheme() |
||||
|
{ |
||||
|
return new ControlTheme |
||||
|
{ |
||||
|
TargetType = typeof(ThemedControl), |
||||
|
BasedOn = CreateTheme(), |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(Border.BorderBrushProperty, Brushes.Blue), |
||||
|
}, |
||||
|
Children = |
||||
|
{ |
||||
|
new Style(x => x.Nesting().Template().OfType<Border>()) |
||||
|
{ |
||||
|
Setters = { new Setter(Border.BorderBrushProperty, Brushes.Yellow) } |
||||
|
}, |
||||
|
new Style(x => x.Nesting().Class("foo").Template().OfType<Border>()) |
||||
|
{ |
||||
|
Setters = { new Setter(Border.BorderBrushProperty, Brushes.Cyan) } |
||||
|
}, |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private class ThemedControl : TemplatedControl |
||||
|
{ |
||||
|
public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); |
||||
|
} |
||||
|
|
||||
|
private class ThemedControl2 : TemplatedControl |
||||
|
{ |
||||
|
public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); |
||||
|
} |
||||
|
|
||||
|
private class DerivedThemedControl : ThemedControl |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,147 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Controls.Templates; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Styling; |
||||
|
using BenchmarkDotNet.Attributes; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Benchmarks.Styling |
||||
|
{ |
||||
|
[MemoryDiagnoser] |
||||
|
public class ControlTheme_Apply |
||||
|
{ |
||||
|
private ControlTheme _theme; |
||||
|
private ControlTheme _otherTheme; |
||||
|
private List<Style> _styles = new(); |
||||
|
|
||||
|
public ControlTheme_Apply() |
||||
|
{ |
||||
|
RuntimeHelpers.RunClassConstructor(typeof(TestControl).TypeHandle); |
||||
|
|
||||
|
_theme = CreateControlTheme(Brushes.Red); |
||||
|
_otherTheme = CreateControlTheme(Brushes.Orange); |
||||
|
|
||||
|
for (var i = 0; i < 100; ++i) |
||||
|
{ |
||||
|
_styles.Add(new Style(x => x.OfType<TestControl>()) |
||||
|
{ |
||||
|
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Yellow) } |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Benchmark] |
||||
|
public void Apply_Control_Theme() |
||||
|
{ |
||||
|
var target = new TestControl(); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
|
||||
|
_theme.TryAttach(target, null); |
||||
|
target.ApplyTemplate(); |
||||
|
_theme.TryAttach(target.VisualChild, null); |
||||
|
|
||||
|
target.EndBatchUpdate(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
[Benchmark] |
||||
|
public void Apply_Remove_Control_Theme() |
||||
|
{ |
||||
|
var target = new TestControl(); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
|
||||
|
_theme.TryAttach(target, null); |
||||
|
target.ApplyTemplate(); |
||||
|
_theme.TryAttach(target.VisualChild, null); |
||||
|
|
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
// Switching to another theme will cause the current theme to be removed but won't
|
||||
|
// immediately apply the new theme, so for the benefit of the benchmark it has the
|
||||
|
// effect of simply removing the theme.
|
||||
|
target.Theme = _otherTheme; |
||||
|
} |
||||
|
|
||||
|
[Benchmark] |
||||
|
public void Apply_Control_Theme_With_Styles() |
||||
|
{ |
||||
|
var target = new TestControl(); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
|
||||
|
_theme.TryAttach(target, null); |
||||
|
target.ApplyTemplate(); |
||||
|
_theme.TryAttach(target.VisualChild, null); |
||||
|
|
||||
|
foreach (var style in _styles) |
||||
|
style.TryAttach(target, null); |
||||
|
|
||||
|
target.EndBatchUpdate(); |
||||
|
} |
||||
|
|
||||
|
[Benchmark] |
||||
|
public void Apply_Remove_Control_Theme_With_Styles() |
||||
|
{ |
||||
|
var target = new TestControl(); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
|
||||
|
_theme.TryAttach(target, null); |
||||
|
target.ApplyTemplate(); |
||||
|
_theme.TryAttach(target.VisualChild, null); |
||||
|
|
||||
|
foreach (var style in _styles) |
||||
|
style.TryAttach(target, null); |
||||
|
|
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
// Switching to another theme will cause the current theme to be removed but won't
|
||||
|
// immediately apply the new theme, so for the benefit of the benchmark it has the
|
||||
|
// effect of simply removing the theme.
|
||||
|
target.Theme = _otherTheme; |
||||
|
} |
||||
|
|
||||
|
private static ControlTheme CreateControlTheme(IBrush background) |
||||
|
{ |
||||
|
return new ControlTheme(typeof(TestControl)) |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(TestControl.BackgroundProperty, Brushes.Transparent), |
||||
|
new Setter(TestControl.TemplateProperty, new FuncControlTemplate<TestControl>((_, x) => |
||||
|
new Border())), |
||||
|
}, |
||||
|
Children = |
||||
|
{ |
||||
|
new Style(x => x.Nesting().Template().OfType<Border>()) |
||||
|
{ |
||||
|
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Red), } |
||||
|
}, |
||||
|
new Style(x => x.Nesting().Class(":pointerover").Template().OfType<Border>()) |
||||
|
{ |
||||
|
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Green), } |
||||
|
}, |
||||
|
new Style(x => x.Nesting().Class(":pressed").Template().OfType<Border>()) |
||||
|
{ |
||||
|
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Blue), } |
||||
|
}, |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private class TestControl : TemplatedControl |
||||
|
{ |
||||
|
public IStyleable VisualChild => (IStyleable)VisualChildren[0]; |
||||
|
} |
||||
|
|
||||
|
private class TestClass2 : Control |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Avalonia.VisualTree; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.UnitTests.Xaml |
||||
|
{ |
||||
|
public class ControlThemeTests : XamlTestBase |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void ControlTheme_Can_Be_StaticResource() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
||||
|
{ |
||||
|
var xaml = $@"
|
||||
|
<Window xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
||||
|
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'> |
||||
|
<Window.Resources> |
||||
|
{ControlThemeXaml} |
||||
|
</Window.Resources> |
||||
|
|
||||
|
<u:TestTemplatedControl Theme='{{StaticResource MyTheme}}'/> |
||||
|
</Window>";
|
||||
|
|
||||
|
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); |
||||
|
var button = Assert.IsType<TestTemplatedControl>(window.Content); |
||||
|
|
||||
|
window.LayoutManager.ExecuteInitialLayoutPass(); |
||||
|
|
||||
|
Assert.NotNull(button.Template); |
||||
|
|
||||
|
var child = Assert.Single(button.GetVisualChildren()); |
||||
|
var border = Assert.IsType<Border>(child); |
||||
|
|
||||
|
Assert.Equal(Brushes.Red, border.Background); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ControlTheme_Can_Be_Set_In_Style() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
||||
|
{ |
||||
|
var xaml = $@"
|
||||
|
<Window xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
||||
|
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'> |
||||
|
<Window.Resources> |
||||
|
{ControlThemeXaml} |
||||
|
</Window.Resources> |
||||
|
|
||||
|
<Window.Styles> |
||||
|
<Style Selector='u|TestTemplatedControl'> |
||||
|
<Setter Property='Theme' Value='{{StaticResource MyTheme}}'/> |
||||
|
</Style> |
||||
|
</Window.Styles> |
||||
|
|
||||
|
<u:TestTemplatedControl/> |
||||
|
</Window>";
|
||||
|
|
||||
|
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); |
||||
|
var button = Assert.IsType<TestTemplatedControl>(window.Content); |
||||
|
|
||||
|
window.LayoutManager.ExecuteInitialLayoutPass(); |
||||
|
|
||||
|
Assert.NotNull(button.Template); |
||||
|
|
||||
|
var child = Assert.Single(button.GetVisualChildren()); |
||||
|
var border = Assert.IsType<Border>(child); |
||||
|
|
||||
|
Assert.Equal(Brushes.Red, border.Background); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private const string ControlThemeXaml = @"
|
||||
|
<ControlTheme x:Key='MyTheme' TargetType='u:TestTemplatedControl'> |
||||
|
<Setter Property='Template'> |
||||
|
<ControlTemplate> |
||||
|
<Border/> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
<Style Selector='^ /template/ Border'> |
||||
|
<Setter Property='Background' Value='Red'/> |
||||
|
</Style> |
||||
|
</ControlTheme>";
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
using Avalonia.Controls.Primitives; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.UnitTests.Xaml |
||||
|
{ |
||||
|
public class TestTemplatedControl : TemplatedControl |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue