Browse Source

Refactored selector matching.

Match selectors from left-to-right, as before we were checking things like property equality (and creating a `PropertyEqualsActivator`) before checking that the control is of the correct type. Also hopefully makes the selector matching logic more readable.
pull/3636/head
Steven Kirk 7 years ago
parent
commit
13ba7f3d0b
  1. 36
      src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs
  2. 2
      src/Avalonia.Styling/Styling/NotSelector.cs
  3. 137
      src/Avalonia.Styling/Styling/Selector.cs
  4. 31
      src/Avalonia.Styling/Styling/SelectorMatch.cs
  5. 5
      src/Avalonia.Styling/Styling/TemplateSelector.cs
  6. 26
      tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs

36
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!;
}
}

2
src/Avalonia.Styling/Styling/NotSelector.cs

@ -42,7 +42,7 @@ namespace Avalonia.Styling
{ {
if (_selectorString == null) if (_selectorString == null)
{ {
_selectorString = ":not(" + _argument.ToString() + ")"; _selectorString = $"{_previous?.ToString()}:not({_argument})";
} }
return _selectorString; return _selectorString;

137
src/Avalonia.Styling/Styling/Selector.cs

@ -43,72 +43,28 @@ namespace Avalonia.Styling
/// <returns>A <see cref="SelectorMatch"/>.</returns> /// <returns>A <see cref="SelectorMatch"/>.</returns>
public SelectorMatch Match(IStyleable control, bool subscribe = true) public SelectorMatch Match(IStyleable control, bool subscribe = true)
{ {
IStyleActivator? activator = null; // First match the selector until a combinator is found. Selectors are stored from
AndActivator? activators = null; // right-to-left, so MatchUntilCombinator reverses this order because the type selector
var selector = this; // will be on the left.
var alwaysThisType = true; var match = MatchUntilCombinator(control, this, subscribe, out var combinator);
var hitCombinator = false;
// If the pre-combinator selector matches, we can now match the combinator, if any.
while (selector != null) if (match.IsMatch && combinator is object)
{ {
hitCombinator |= selector.IsCombinator; match = match.And(combinator.Match(control, subscribe));
var match = selector.Evaluate(control, subscribe);
if (!match.IsMatch) // 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
return hitCombinator ? SelectorMatch.NeverThisInstance : match; // control.
} match = match.Result switch
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 (match.Activator is null) SelectorMatchResult.AlwaysThisType => SelectorMatch.AlwaysThisInstance,
{ SelectorMatchResult.NeverThisType => SelectorMatch.NeverThisInstance,
throw new AvaloniaInternalException( _ => match
"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();
} }
if (activators is object) return match;
{
return new SelectorMatch(activators);
}
else if (activator is object)
{
return new SelectorMatch(activator);
}
else
{
return alwaysThisType && !hitCombinator ?
SelectorMatch.AlwaysThisType :
SelectorMatch.AlwaysThisInstance;
}
} }
/// <summary> /// <summary>
@ -126,5 +82,64 @@ namespace Avalonia.Styling
/// Moves to the previous selector. /// Moves to the previous selector.
/// </summary> /// </summary>
protected abstract Selector? MovePrevious(); 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;
}
} }
} }

31
src/Avalonia.Styling/Styling/SelectorMatch.cs

@ -81,7 +81,11 @@ namespace Avalonia.Styling
Activator = match; Activator = match;
} }
private SelectorMatch(SelectorMatchResult result) /// <summary>
/// Initializes a new instance of the <see cref="SelectorMatch"/> class with the specified result.
/// </summary>
/// <param name="result">The match result.</param>
public SelectorMatch(SelectorMatchResult result)
{ {
Result = result; Result = result;
Activator = null; Activator = null;
@ -102,5 +106,30 @@ namespace Avalonia.Styling
/// change over time. /// change over time.
/// </summary> /// </summary>
public IStyleActivator? Activator { get; } public IStyleActivator? Activator { get; }
/// <summary>
/// Logical ANDs this <see cref="SelectorMatch"/> with another.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
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);
}
}
/// <inheritdoc/>
public override string ToString() => Result.ToString();
} }
} }

5
src/Avalonia.Styling/Styling/TemplateSelector.cs

@ -41,12 +41,11 @@ namespace Avalonia.Styling
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{ {
IStyleable templatedParent = control.TemplatedParent as IStyleable; var templatedParent = control.TemplatedParent as IStyleable;
if (templatedParent == null) if (templatedParent == null)
{ {
throw new InvalidOperationException( return SelectorMatch.NeverThisInstance;
"Cannot call Template selector on control with null TemplatedParent.");
} }
return _parent.Match(templatedParent, subscribe); return _parent.Match(templatedParent, subscribe);

26
tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -152,6 +153,31 @@ namespace Avalonia.Styling.UnitTests
Assert.Equal(new[] { false, true, false }, values); 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<Border>()
.PropertyEquals(Border.TagProperty, "foo")
.Child()
.OfType<Canvas>()
.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] [Fact]
public void TargetType_OfType() public void TargetType_OfType()
{ {

Loading…
Cancel
Save