Browse Source

Add ControlTheme.BasedOn.

Steven Kirk 3 years ago
parent
commit
05fdc04464

+ 21 - 5
src/Avalonia.Base/Styling/ControlTheme.cs

@@ -23,16 +23,32 @@ namespace Avalonia.Styling
         /// </summary>
         public Type? TargetType { get; set; }
 
-        internal override bool HasSelector => TargetType is not null;
+        /// <summary>
+        /// Gets or sets a control theme that is the basis of the current theme.
+        /// </summary>
+        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)

+ 10 - 2
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(

+ 26 - 9
src/Avalonia.Base/Styling/Style.cs

@@ -28,7 +28,32 @@ namespace Avalonia.Styling
         /// </summary>
         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;
+        }
 
         /// <summary>
         /// 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)

+ 17 - 44
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> IStyle.Children => (IReadOnlyList<IStyle>?)_children ?? Array.Empty<IStyle>();
 
-        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;
         }
 
-        /// <summary>
-        /// Evaluates the style's selector against the specified target element.
-        /// </summary>
-        /// <param name="control">The control.</param>
-        /// <param name="host">The element that hosts the style.</param>
-        /// <param name="subscribe">
-        /// Whether the match should subscribe to changes in order to track the match over time,
-        /// or simply return an immediate result.
-        /// </param>
-        /// <returns>
-        /// A <see cref="SelectorMatchResult"/> describing how the style matches the control.
-        /// </returns>
-        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;
 

+ 55 - 4
tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs

@@ -28,10 +28,10 @@ public class StyledElementTests_Theming
             Assert.NotNull(target.Template);
 
             var border = Assert.IsType<Border>(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<Canvas>(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<Border>(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<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)
@@ -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<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();