diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 9dcbd7d2c4..aff6fad990 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -23,16 +23,32 @@ namespace Avalonia.Styling /// public Type? TargetType { get; set; } - internal override bool HasSelector => TargetType is not null; + /// + /// Gets or sets a control theme that is the basis of the current theme. + /// + public ControlTheme? BasedOn { get; set; } - internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) + 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."); - return control.StyleKey == TargetType ? - SelectorMatch.AlwaysThisType : - SelectorMatch.NeverThisType; + var result = BasedOn?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType; + + if (HasSettersOrAnimations && target.StyleKey == TargetType) + { + Attach(target, null); + result = SelectorMatchResult.AlwaysThisType; + } + + var childResult = TryAttachChildren(target, host); + + if (childResult > result) + result = childResult; + + return result; } internal override void SetParent(StyleBase? parent) diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 6d31f7cb18..c8945a713d 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -15,9 +15,17 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { - if (parent is StyleBase s && s.HasSelector) + if (parent is Style s && s.Selector is not null) { - return s.Match(control, null, subscribe); + return s.Selector.Match(control, s.Parent, subscribe); + } + else if (parent is ControlTheme theme) + { + if (theme.TargetType is null) + throw new InvalidOperationException("ControlTheme has no TargetType."); + return control.StyleKey == theme.TargetType ? + SelectorMatch.AlwaysThisType : + SelectorMatch.NeverThisType; } throw new InvalidOperationException( diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index ca20ff2b4b..7a6b746488 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -28,7 +28,32 @@ namespace Avalonia.Styling /// public Selector? Selector { get; set; } - internal override bool HasSelector => Selector is not null; + public override SelectorMatchResult TryAttach(IStyleable target, object? host) + { + _ = target ?? throw new ArgumentNullException(nameof(target)); + + var result = SelectorMatchResult.NeverThisType; + + if (HasSettersOrAnimations) + { + var match = Selector?.Match(target, Parent, true) ?? + (target == host ? + SelectorMatch.AlwaysThisInstance : + SelectorMatch.NeverThisInstance); + + if (match.IsMatch) + Attach(target, match.Activator); + + result = match.Result; + } + + var childResult = TryAttachChildren(target, host); + + if (childResult > result) + result = childResult; + + return result; + } /// /// Returns a string representation of the style. @@ -46,14 +71,6 @@ namespace Avalonia.Styling } } - internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) - { - return Selector?.Match(control, Parent, subscribe) ?? - (control == host ? - SelectorMatch.AlwaysThisInstance : - SelectorMatch.NeverThisInstance); - } - internal override void SetParent(StyleBase? parent) { if (parent is Style parentStyle && parentStyle.Selector is not null) diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index b6bfec62bd..306a4cf010 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; +using Avalonia.Styling.Activators; namespace Avalonia.Styling { @@ -64,43 +65,14 @@ namespace Avalonia.Styling bool IResourceNode.HasResources => _resources?.Count > 0; IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); - internal abstract bool HasSelector { get; } + 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 SelectorMatchResult TryAttach(IStyleable target, object? host) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - - var result = SelectorMatchResult.NeverThisType; - - if (_setters?.Count > 0 || _animations?.Count > 0) - { - var match = Match(target, host, subscribe: true); - - if (match.IsMatch) - { - var instance = new StyleInstance(this, target, _setters, _animations, match.Activator); - target.StyleApplied(instance); - instance.Start(); - } - - result = match.Result; - } - - if (_children is not null) - { - _childCache ??= new StyleCache(); - var childResult = _childCache.TryAttach(_children, target, host); - if (childResult > result) - result = childResult; - } - - return result; - } + public abstract SelectorMatchResult TryAttach(IStyleable target, object? host); public bool TryGetResource(object key, out object? result) { @@ -108,19 +80,20 @@ namespace Avalonia.Styling return _resources?.TryGetResource(key, out result) ?? false; } - /// - /// Evaluates the style's selector against the specified target element. - /// - /// The control. - /// The element that hosts the style. - /// - /// Whether the match should subscribe to changes in order to track the match over time, - /// or simply return an immediate result. - /// - /// - /// A describing how the style matches the control. - /// - internal abstract SelectorMatch Match(IStyleable control, object? host, bool subscribe); + 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; diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 0c0808987a..737cf1e048 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -28,10 +28,10 @@ public class StyledElementTests_Theming Assert.NotNull(target.Template); var border = Assert.IsType(target.VisualChild); - Assert.Equal(border.Background, Brushes.Red); + Assert.Equal(Brushes.Red, border.Background); target.Classes.Add("foo"); - Assert.Equal(border.Background, Brushes.Green); + Assert.Equal(Brushes.Green, border.Background); } [Fact] @@ -73,7 +73,7 @@ public class StyledElementTests_Theming var root = CreateRoot(target); var canvas = Assert.IsType(target.VisualChild); - Assert.Equal(canvas.Background, Brushes.Red); + Assert.Equal(Brushes.Red, canvas.Background); target.Theme = null; @@ -97,7 +97,28 @@ public class StyledElementTests_Theming var border = Assert.IsType(target.VisualChild); Assert.NotNull(target.Template); - Assert.Equal(border.Background, Brushes.Red); + 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(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) @@ -197,6 +218,36 @@ public class StyledElementTests_Theming }; } + 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()) + { + Setters = + { + new Setter(Border.BorderBrushProperty, Brushes.Yellow), + } + }, + new Style(x => x.Nesting().Class("foo").Template().OfType()) + { + Setters = + { + new Setter(Border.BorderBrushProperty, Brushes.Cyan), + } + }, + } + }; + } + private class ThemedControl : TemplatedControl { public IVisual? VisualChild => VisualChildren?.SingleOrDefault();