diff --git a/src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs b/src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs new file mode 100644 index 0000000000..318988b3c5 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs @@ -0,0 +1,36 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal struct AndActivatorBuilder + { + private IStyleActivator? _single; + private AndActivator? _multiple; + + public void Add(IStyleActivator? activator) + { + if (activator == null) + { + return; + } + + if (_single is null && _multiple is null) + { + _single = activator; + } + else + { + if (_multiple is null) + { + _multiple = new AndActivator(); + _multiple.Add(_single!); + _single = null; + } + + _multiple.Add(activator); + } + } + + public IStyleActivator Get() => _single ?? _multiple!; + } +} diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs index 6428535a12..c1ab662887 100644 --- a/src/Avalonia.Styling/Styling/NotSelector.cs +++ b/src/Avalonia.Styling/Styling/NotSelector.cs @@ -42,7 +42,7 @@ namespace Avalonia.Styling { if (_selectorString == null) { - _selectorString = ":not(" + _argument.ToString() + ")"; + _selectorString = $"{_previous?.ToString()}:not({_argument})"; } return _selectorString; diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index 6d74eb8842..f14e1ad232 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -43,72 +43,28 @@ namespace Avalonia.Styling /// A . public SelectorMatch Match(IStyleable control, bool subscribe = true) { - IStyleActivator? activator = null; - AndActivator? activators = null; - var selector = this; - var alwaysThisType = true; - var hitCombinator = false; - - while (selector != null) + // First match the selector until a combinator is found. Selectors are stored from + // right-to-left, so MatchUntilCombinator reverses this order because the type selector + // will be on the left. + var match = MatchUntilCombinator(control, this, subscribe, out var combinator); + + // If the pre-combinator selector matches, we can now match the combinator, if any. + if (match.IsMatch && combinator is object) { - hitCombinator |= selector.IsCombinator; - - var match = selector.Evaluate(control, subscribe); + match = match.And(combinator.Match(control, subscribe)); - if (!match.IsMatch) - { - return hitCombinator ? SelectorMatch.NeverThisInstance : match; - } - else if (selector.InTemplate && control.TemplatedParent == null) - { - return SelectorMatch.NeverThisInstance; - } - else if (match.Result == SelectorMatchResult.AlwaysThisInstance) - { - alwaysThisType = false; - } - else if (match.Result == SelectorMatchResult.Sometimes) + // If we have a combinator then we can never say that we always match a control of + // this type, because by definition the combinator matches on things outside of the + // control. + match = match.Result switch { - if (match.Activator is null) - { - throw new AvaloniaInternalException( - "SelectorMatch returned Sometimes but there is no activator."); - } - - if (activator is null && activators is null) - { - activator = match.Activator; - } - else - { - if (activators is null) - { - activators = new AndActivator(); - activators.Add(activator!); - activator = null; - } - - activators.Add(match.Activator); - } - } - - selector = selector.MovePrevious(); + SelectorMatchResult.AlwaysThisType => SelectorMatch.AlwaysThisInstance, + SelectorMatchResult.NeverThisType => SelectorMatch.NeverThisInstance, + _ => match + }; } - if (activators is object) - { - return new SelectorMatch(activators); - } - else if (activator is object) - { - return new SelectorMatch(activator); - } - else - { - return alwaysThisType && !hitCombinator ? - SelectorMatch.AlwaysThisType : - SelectorMatch.AlwaysThisInstance; - } + return match; } /// @@ -126,5 +82,64 @@ namespace Avalonia.Styling /// Moves to the previous selector. /// protected abstract Selector? MovePrevious(); + + private static SelectorMatch MatchUntilCombinator( + IStyleable control, + Selector start, + bool subscribe, + out Selector? combinator) + { + combinator = null; + + var activators = new AndActivatorBuilder(); + var result = Match(control, start, subscribe, ref activators, ref combinator); + + return result == SelectorMatchResult.Sometimes ? + new SelectorMatch(activators.Get()) : + new SelectorMatch(result); + } + + private static SelectorMatchResult Match( + IStyleable control, + Selector selector, + bool subscribe, + ref AndActivatorBuilder activators, + ref Selector? combinator) + { + var previous = selector.MovePrevious(); + + // Selectors are stored from right-to-left, so we recurse into the selector in order to + // reverse this order, because the type selector will be on the left and is our best + // oppurtunity exit early. + if (previous != null && !previous.IsCombinator) + { + var previousMatch = Match(control, previous, subscribe, ref activators, ref combinator); + + if (previousMatch < SelectorMatchResult.Sometimes) + { + return previousMatch; + } + } + + // Match this selector. + var match = selector.Evaluate(control, subscribe); + + if (!match.IsMatch) + { + combinator = null; + return match.Result; + } + else if (match.Activator is object) + { + activators.Add(match.Activator!); + } + + if (previous?.IsCombinator == true) + { + combinator = previous; + } + + return match.Result; + } } } diff --git a/src/Avalonia.Styling/Styling/SelectorMatch.cs b/src/Avalonia.Styling/Styling/SelectorMatch.cs index 3cc84a0b57..a7e84dd9bd 100644 --- a/src/Avalonia.Styling/Styling/SelectorMatch.cs +++ b/src/Avalonia.Styling/Styling/SelectorMatch.cs @@ -81,7 +81,11 @@ namespace Avalonia.Styling Activator = match; } - private SelectorMatch(SelectorMatchResult result) + /// + /// Initializes a new instance of the class with the specified result. + /// + /// The match result. + public SelectorMatch(SelectorMatchResult result) { Result = result; Activator = null; @@ -102,5 +106,30 @@ namespace Avalonia.Styling /// change over time. /// public IStyleActivator? Activator { get; } + + /// + /// Logical ANDs this with another. + /// + /// + /// + public SelectorMatch And(in SelectorMatch other) + { + var result = (SelectorMatchResult)Math.Min((int)Result, (int)other.Result); + + if (result == SelectorMatchResult.Sometimes) + { + var activators = new AndActivatorBuilder(); + activators.Add(Activator); + activators.Add(other.Activator); + return new SelectorMatch(activators.Get()); + } + else + { + return new SelectorMatch(result); + } + } + + /// + public override string ToString() => Result.ToString(); } } diff --git a/src/Avalonia.Styling/Styling/TemplateSelector.cs b/src/Avalonia.Styling/Styling/TemplateSelector.cs index 7530339883..213b20161a 100644 --- a/src/Avalonia.Styling/Styling/TemplateSelector.cs +++ b/src/Avalonia.Styling/Styling/TemplateSelector.cs @@ -41,12 +41,11 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - IStyleable templatedParent = control.TemplatedParent as IStyleable; + var templatedParent = control.TemplatedParent as IStyleable; if (templatedParent == null) { - throw new InvalidOperationException( - "Cannot call Template selector on control with null TemplatedParent."); + return SelectorMatch.NeverThisInstance; } return _parent.Match(templatedParent, subscribe); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs index a1ced14108..b43b4f686b 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.VisualTree; @@ -152,6 +153,31 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(new[] { false, true, false }, values); } + [Fact] + public async Task Nested_PropertyEquals() + { + var control = new Canvas(); + var parent = new Border { Child = control }; + + var target = default(Selector) + .OfType() + .PropertyEquals(Border.TagProperty, "foo") + .Child() + .OfType() + .PropertyEquals(Canvas.TagProperty, "bar"); + + var match = target.Match(control); + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + + var activator = match.Activator; + + Assert.False(await activator.Take(1)); + control.Tag = "bar"; + Assert.False(await activator.Take(1)); + parent.Tag = "foo"; + Assert.True(await activator.Take(1)); + } + [Fact] public void TargetType_OfType() {