committed by
GitHub
60 changed files with 1709 additions and 319 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,64 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.X11.NativeDialogs; |
|||
|
|||
internal class CompositeStorageProvider : IStorageProvider |
|||
{ |
|||
private readonly IEnumerable<Func<Task<IStorageProvider?>>> _factories; |
|||
public CompositeStorageProvider(IEnumerable<Func<Task<IStorageProvider?>>> factories) |
|||
{ |
|||
_factories = factories; |
|||
} |
|||
|
|||
public bool CanOpen => true; |
|||
public bool CanSave => true; |
|||
public bool CanPickFolder => true; |
|||
|
|||
private async Task<IStorageProvider> EnsureStorageProvider() |
|||
{ |
|||
foreach (var factory in _factories) |
|||
{ |
|||
var provider = await factory(); |
|||
if (provider is not null) |
|||
{ |
|||
return provider; |
|||
} |
|||
} |
|||
|
|||
throw new InvalidOperationException("Neither DBus nor GTK are available on the system"); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
|||
{ |
|||
var provider = await EnsureStorageProvider().ConfigureAwait(false); |
|||
return await provider.OpenFilePickerAsync(options).ConfigureAwait(false); |
|||
} |
|||
|
|||
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
|||
{ |
|||
var provider = await EnsureStorageProvider().ConfigureAwait(false); |
|||
return await provider.SaveFilePickerAsync(options).ConfigureAwait(false); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
|||
{ |
|||
var provider = await EnsureStorageProvider().ConfigureAwait(false); |
|||
return await provider.OpenFolderPickerAsync(options).ConfigureAwait(false); |
|||
} |
|||
|
|||
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark) |
|||
{ |
|||
var provider = await EnsureStorageProvider().ConfigureAwait(false); |
|||
return await provider.OpenFileBookmarkAsync(bookmark).ConfigureAwait(false); |
|||
} |
|||
|
|||
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) |
|||
{ |
|||
var provider = await EnsureStorageProvider().ConfigureAwait(false); |
|||
return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false); |
|||
} |
|||
} |
|||
@ -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